iOS

Add Wallet Service to Your iOS Project

Prerequisites

  • Install the following:

    • Xcode 14.1 or later

    • CocoaPods 1.12.1 or higher

  • Make sure that your project meets the following requirements:

    • Your project must target these platform versions or later:

      • iOS 14

Create a Particle Project and App

Before you can add the Wallet Service to your iOS app, you need to create a Particle project to connect to your iOS app. Visit Particle Dashboard to learn more about Particle projects and apps.

👉 Sign up/log in and create your project now

Add the Wallet Service SDK to your app

Wallet Service supports installation with CocoaPods.

Here's how to install the Wallet Service using CocoaPods:

  1. Create a Podfile if you don't already have one. From the root of your project directory, run the following command:

pod init

2. To your Podfile, add the Auth Service pods that you want to use in your app.

// Particle Wallet API
pod 'ParticleWalletAPI'
//  Particle Wallet GUI, you can remove it if custom GUI.
pod 'ParticleWalletGUI'

3. Install the pods, then open your .xcworkspace file to see the project in Xcode:

pod install --repo-update
open our-project.xcworkspace

4. Add Privacy - Camera Usage Description in your info.plist file, we need to take photos for the QR code.

The Wallet Service can only be used after a successful log-in with Connect Service.

Wallet GUI depends on Connect Service, you must import Connect Service.

If you want to receive release updates, subscribe to our GitHub repository.

Edit Podfile

It is required for every iOS project that integrates the Wallet Service SDK.

// From 0.9.12, you should add more in podfile
If you use ParticleWalletGUI, you need add this one.
pod 'SkeletonView', :git => 'https://github.com/SunZhiC/SkeletonView.git', :branch => 'main'

// paste there code into pod file
post_install do |installer|
installer.pods_project.targets.each do |target|
  target.build_configurations.each do |config|
  config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
    end
  end
 end

Wallet Core Reference

Solana Service

Get the real-time exchange rate of the Solana token

// native token address is "native", you can add other token mint address.
let addresses: [String] = ["native"]
ParticleWalletAPI.getSolanaService().getPrice(by: addresses, currencies: ["usd"]).subscribe { [weak self] result in
    guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Get the token list and NFT list by giving an address

// address is user public address
let address: String = ""
ParticleWalletAPI.getSolanaService().getTokensAndNFTs(by: address).subscribe { [weak self] result in
    guard let self = self else { return }
    // handle result
}.disposed(by: bag)

// also get from database
ParticleWalletAPI.getEvmService().getTokensAndNFTsFromDB(by: address).subscribe { [weak self] _ in
    guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Get the parsed transaction history by giving an address

// address is user public address
let address: String = ""
ParticleWalletAPI.getSolanaService().getTransactions(by: address, beforeSignature: nil, untilSignature: nil, limit: 1000).subscribe { [weak self] result in
    guard let self = self else { return }
    // handle result
}.disposed(by: bag)

// also get from database
ParticleWalletAPI.getSolanaService().getTransactionsFromDB(by: address, beforeSignature: nil, untilSignature: nil, limit: 1000).subscribe { [weak self] result in
    guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Serialize unsigned transaction

// call enhanced method: enhancedSerializeTransaction
let transactionType: SolanaTransactionType = .transferSol
let sender: String = ""
let receiver: String = ""
let lamports: BInt = BInt(0)
let mintAddress: String? = nil
let payer: String? = nil
ParticleWalletAPI.getSolanaService().serializeTransaction(type: transactionType, sender: sender, receiver: receiver, lamports: lamports, mintAddress: mintAddress, payer: payer).subscribe { [weak self] result in
    guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Get the token info list

// get solana chain all SPL token info
ParticleWalletAPI.getSolanaService().getTokenList().subscribe { [weak self] result in
    guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Access 👉 any RPC

// such as getBalance
let method: String = "getBalance"
let params: [Encodable?] = ["8FE27ioQh3T7o22QsYVT5Re8NnHFqmFNbdqwiF3ywuZQ"]
ParticleWalletAPI.getSolanaService().rpc(method: method, params: params).subscribe { [weak self] result in
    guard let self = self else { return }
    // handle result
}.disposed(by: bag)

EVM Service

Get the real-time exchange rate of the EVM token

// native token address is "native", you can add other token contract address.
let addresses = ["native"]
ParticleWalletAPI.getEvmService().getPrice(by: addresses, vsCurrencies: ["usd"]).subscribe { [weak self] _ in
guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Get the token list and NFT list by giving an address

// address is user public address
let address = ""
ParticleWalletAPI.getEvmService().getTokensAndNFTs(by: address).subscribe { [weak self] result in
guard let self = self else { return }
    // handle result
}.disposed(by: bag)

// also get from database, no rpc request.
ParticleWalletAPI.getEvmService().getTokensAndNFTsFromDB(by: address).subscribe { [weak self] result in
guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Get only tokens or only NFTs

// get only tokens
let address = ""
ParticleWalletAPI.getEvmService().getTokens(by: address, tokenAddresses: []).subscribe { result in
    // handle result
}.disposed(by: self.bag)

// get only NFTs
let address = ""
ParticleWalletAPI.getEvmService().getNFTs(by: address, tokenAddresses: []).subscribe { result in
    // handle result
}.disposed(by: self.bag)

Get the parsed transaction history by giving an address

// address is user public address
let address = ""
ParticleWalletAPI.getEvmService().getTransactions(by: address).subscribe { [weak self] _ in
guard let self = self else { return }
    // handle result
}.disposed(by: bag)

// also get from database
ParticleWalletAPI.getEvmService().getTransactionsFromDB(by: address).subscribe { [weak self] _ in
guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Get the token info list

// get any EVM chain all token info
ParticleWalletAPI.getEvmService().getTokenList().subscribe { [weak self] _ in
guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Access 👉 any RPC

// such as eth_getBalance
let method = "eth_getBalance"
let params: [Encodable?] = ["0xfe3b557e8fb62b89f4916b721be55ceb828dbd73", "latest"]
ParticleWalletAPI.getEvmService().rpc(method: method, params: params).subscribe { [weak self] _ in
    guard let self = self else { return }
    // handle result
}.disposed(by: bag)

Serialize the data of any contract

// call custom abi function
func customMethodAbiEncode() {
    let contractAddress = ""
    let methodName = ""
    let params: [String] = []
    let abiJsonString = ""
    ParticleWalletAPI.getEvmService().abiEncodeFunctionCall(contractAddress: contractAddress, methodName: methodName, params: params, abiJsonString: abiJsonString).subscribe { [weak self] _ in
        guard let self = self else { return }
        // handle result
    }.disposed(by: bag)
}
// call 'transfer(to, amount)' function in erc20
func erc20Transfer() {
    let contractAddress = ""
    let to = ""
    let amount = BInt(1)
    ParticleWalletAPI.getEvmService().erc20Transfer(contractAddress: contractAddress, to: to, amount: amount).subscribe { [weak self] _ in
        guard let self = self else { return }
        // handle result
    }.disposed(by: bag)
}
// call 'approve(spender, amount)' function in erc20 
func erc20Approve() {
    let contractAddress = ""
    let spender = ""
    let amount = BInt(1)
    ParticleWalletAPI.getEvmService().erc20Approve(contractAddress: contractAddress, spender: spender, amount: amount).subscribe { [weak self] _ in
        guard let self = self else { return }
        // handle result
    }.disposed(by: bag)
}
// call 'transferFrom(from, to, amount)' function in erc20
func erc20TransferFrom() {
    let contractAddress = ""
    let from = ""
    let to = ""
    let amount = BInt(1)
    ParticleWalletAPI.getEvmService().erc20TransferFrom(contractAddress: contractAddress, from: from, to: to, amount: amount).subscribe { [weak self] _ in
        guard let self = self else { return }
        // handle result
    }.disposed(by: bag)
}
// call 'safeTransferFrom(from, to, tokenId)' function in erc721
func erc721SafeTransferFrom() {
    let contractAddress = ""
    let from = ""
    let to = ""
    let tokenId = ""
    ParticleWalletAPI.getEvmService().erc721SafeTransferFrom(contractAddress: contractAddress, from: from, to: to, tokenId: tokenId).subscribe { [weak self] result in
        guard let self = self else { return }
        // handle result
    }.disposed(by: bag)
}
// call 'safeTransferFrom(from, to, id, amount, data)' function in erc1155
func erc1155SafeTransferFrom() {
    let contractAddress = ""
    let from = ""
    let to = ""
    let id = ""
    let amount = BInt(0)
    let data: [UInt8] = []
    ParticleWalletAPI.getEvmService().erc1155SafeTransferFrom(contractAddress: contractAddress, from: from, to: to, id: id, amount: amount, data: data).subscribe { [weak self] result in
        guard let self = self else { return }
        // handle result
    }.disposed(by: bag)
}

Create Transaction

/// Create a transaciton
/// - Parameters:
///   - from: From address
///   - to:If you send a erc20, erc721, erc1155 or interact with a contract, this is the contract address, if you send native, this is receiver address.
///   - contractParams: Acontract parameters
///   - value: Native value, default is nil
///   - type: TxData type, 0x2 is EIP1155, 0x1 is EIP2930, 0x0 is legacy.
///   - nonce: Default value "0x0", particle auth manage nonce without cancel or speed transaction.
///   - gasFeeLevel: Default is medium, transaction gas fee level.
///   - action: Default is normal, means send, if you cancel/speed tracsaction, set the vaule
/// - Returns: A transacion
public func createTransaction(from: String, to: String, value: String? = nil, contractParams: ContractParams? = nil, type: String = "0x2", nonce: String = "0x0", gasFeeLevel: GasFeeLevel = .medium, action: Action = .normal) -> Single<String>


/// Create a transaciton if you have got the data by yourself.
/// - Parameters:
///   - from: From address
///   - to: If you send a erc20, erc721, erc1155 or interact with a contract, this is the contract address, if you send native, this is receiver address.
///   - value: Native value, default is nil, expressed as a hex string.
///   - data: Data
///   - type: TxData type, 0x2 is EIP1155, 0x1 is EIP2930, 0x0 is legacy.
///   - gasFeeLevel: Gas fee level
///   - action: Normal is send, or cancel, speedUp.
/// - Returns: A transacion presented in hex string.
public func createTransaction(from: String, to: String, value: String? = nil, data: String = "0x", type: String = "0x2", gasFeeLevel: GasFeeLevel = .medium, action: Action = .normal) -> Single<String>

func sendNativeEVM() {
    showLoading()
    // firstly, make sure current user has some native token for test there methods
    // send 0.0001 native from self to receiver
    let sender = ParticleAuthService.getAddress()
    let receiver = "0xAC6d81182998EA5c196a4424EA6AB250C7eb175b"
    let amount = BDouble(0.0001 * pow(10, 18)).rounded()
    
    ParticleWalletAPI.getEvmService().createTransaction(from: sender, to: receiver, value: amount.toHexString()).flatMap { transaction -> Single<String> in
        print(transaction)
        return ParticleAuthService.signAndSendTransaction(transaction)
    }.subscribe { [weak self] result in
        guard let self = self else { return }
        switch result {
        case .failure(let error):
            print(error)
        case .success(let signature):
            print(signature)
        }
        self.hideLoading()
    }.disposed(by: self.bag)
}

func sendErc20Token() {
    showLoading()
    // firstly, make sure current user has some native token and erc20 token for test there methods
    // send 0.0001 erc20 token from self to receiver
    let from = ParticleAuthService.getAddress()
    // this token's decimals is 18, convert 0.0001 to minimum unit
    let amount = BDouble(0.0001 * pow(10, 18)).rounded()
    // this is your token contract address
    let contractAddress = "0xa36085F69e2889c224210F603D836748e7dC0088"
    // this is receiver address
    let receiver = "0xAC6d81182998EA5c196a4424EA6AB250C7eb175b"
    
    let contractParams = ContractParams.erc20Transfer(contractAddress: contractAddress, to: receiver, amount: amount)
    
    // because you want to send erc20 token, interact with contact, 'to' should be the contract address.
    // and value could be nil.
    ParticleWalletAPI.getEvmService().createTransaction(from: from, to: contractAddress, value: nil, contractParams: contractParams).flatMap { transaction -> Single<String> in
        print(transaction)
        return ParticleAuthService.signAndSendTransaction(transaction)
    }.subscribe { [weak self] result in
        guard let self = self else { return }
        switch result {
        case .failure(let error):
            print(error)
        case .success(let signature):
            print(signature)
        }
        self.hideLoading()
    }.disposed(by: self.bag)
}

func sendErc721NFT() {
    showLoading()
    // firstly, make sure current user has some native token and the NFT for test there methods
    // send 1 erc721 NFT from self to receiver
    let from = ParticleAuthService.getAddress()
    // this is your nft contract address
    let contractAddress = "0xD18e451c11A6852Fb92291Dc59bE35a59d143836"
    // this is receiver address
    let receiver = "0xAC6d81182998EA5c196a4424EA6AB250C7eb175b"
    // your NFT token id, make sure you own the NFT
    let tokenId = "2302"
    
    let contractParams = ContractParams.erc721SafeTransferFrom(contractAddress: contractAddress, from: from, to: receiver, tokenId: tokenId)
    
    // because you want to send erc721 NFT, interact with contact, 'to' should be the contract address.
    // and value could be nil.
    ParticleWalletAPI.getEvmService().createTransaction(from: from, to: contractAddress, value: nil, contractParams: contractParams).flatMap { transaction -> Single<String> in
        print(transaction)
        return ParticleAuthService.signAndSendTransaction(transaction)
    }.subscribe { [weak self] result in
        guard let self = self else { return }
        switch result {
        case .failure(let error):
            print(error)
        case .success(let signature):
            print(signature)
        }
        self.hideLoading()
    }.disposed(by: self.bag)
}

func sendErc1155NFT() {
    showLoading()
    // firstly, make sure current user has some native token and the NFT for test there methods
    // send 10 erc1155 NFT from self to receiver
    let from = ParticleAuthService.getAddress()
    // this is your nft contract address
    let contractAddress = "0xD18e451c11A6852Fb92291Dc59bE35a59d143836"
    // this is receiver address
    let receiver = "0xAC6d81182998EA5c196a4424EA6AB250C7eb175b"
    // your NFT token id, make sure you own the NFT
    let tokenId = "2302"
    // send amount
    let tokenAmount: BInt = 10
    
    let contractParams = ContractParams.erc1155SafeTransferFrom(contractAddress: contractAddress, from: from, to: receiver, id: tokenId, amount: tokenAmount, data: "0x")
    
    // because you want to send erc1155 NFT, interact with contact, 'to' should be the contract address.
    // and value could be nil.
    ParticleWalletAPI.getEvmService().createTransaction(from: from, to: contractAddress, value: nil, contractParams: contractParams).flatMap { transaction -> Single<String> in
        print(transaction)
        return ParticleAuthService.signAndSendTransaction(transaction)
    }.subscribe { [weak self] result in
        guard let self = self else { return }
        switch result {
        case .failure(let error):
            print(error)
        case .success(let signature):
            print(signature)
        }
        self.hideLoading()
    }.disposed(by: self.bag)
}

func deployContract() {
    // firstly, make sure current user has some native token to pay gas fee.
    // deploy a contract then sign and send with Particle Auth.
    let data = getContractData()
    let from = ParticleAuthService.getAddress()
    let to = ""
    ParticleWalletAPI.getEvmService().createTransaction(from: from, to: to, data: data).flatMap {
        transaction -> Single<String> in
        print("transaction = \(transaction)")
        return ParticleAuthService.signAndSendTransaction(transaction)
    }.subscribe { [weak self] result in
        switch result {
        case .failure(let error):
            print(error)
        case .success(let signature):
            print(signature)
        }
    }.disposed(by: bag)
}

You can create contractParams object by these ContractParamsstatic methods

  • ContractParams.erc20Transfer()

  • ContractParams.erc20Approve()

  • ContractParams.erc20TransferFrom()

  • ContractParams.erc721SafeTransferFrom()

  • ContractParams.erc1155SafeTransferFrom()

  • ContractParams.customAbiEncodeFunctionCall()

Read Contract

// example for evm read contract
func readContract() {
    let params = ContractParams.customAbiEncodeFunctionCall(contractAddress: "0xd000f000aa1f8accbd5815056ea32a54777b2fc4", methodName: "balanceOf", params: ["0xBbc1CA8776EfDeC12C75e218C64e96ce52aC6671"])
    ParticleWalletAPI.getEvmService().readContract(contractParams: params).subscribe {
        [weak self] result in
            switch result {
            case .failure(let error):
                print(error)
            case .success(let json):
                print(json)
            }
    }.disposed(by: self.bag)
}

Write Contract

// example for evm write contract
// mint 1 NFT in ethereum mainnet, the from is sender address or user address in general.
// the params format requires hex strings
// result is a transaciton string which presented in hex format.
func writeContract() {
    let params = ContractParams.customAbiEncodeFunctionCall(contractAddress: "0xd000f000aa1f8accbd5815056ea32a54777b2fc4", methodName: "mint", params: ["0x1"])
    let from = "0x0cf3ffe33e45ad43fcd0aa7016c590b5f629d9aa"
    ParticleWalletAPI.getEvmService().writeContract(contractParams: params, from: from)
    .subscribe {
        [weak self] result in
            switch result {
            case .failure(let error):
                print(error)
            case .success(let transaction):
                print(transaction)
            }
    }.disposed(by: self.bag)
}

Wallet UI Reference

Migrating to WalletConnect v2

ParticleWalletGUI is dependent on ParticleWalletConnect, start from version 0.15.0, you should call this method to set wallet connect v2 project id.

ParticleWalletConnect.setWalletConnectV2ProjectId("your wallet connect v2 project id")

Account Abstraction Setting

If you have add ParticleAA, but don't allow user to switch smart account between biconomy | cyberConnect | simple, you can set false to below method.

// show smart account setting in setting page, default value is true;
ParticleWalletGUI.setShowSmartAccountSetting(true)

Custom Wallet UI

Control show or hide test network, swap feature, buy crypto feature, support wallet connect, manage wallet page, support chain, set language, set user interface style and more.

Because ParticleWalletGUI dependent on Particle Connect, Particle Connect initialize chain will add to support chain automatically.

// You can disable buy crypto feature in UI, default is false.
ParticleWalletGUI.setPayDisabled(false)

// You can disable swap feature in UI, default value is false.
ParticleWalletGUI.setSwapDisabled(false)

// You can disable wallet connect feature, default value is true.
ParticleWalletGUI.setSupportWalletConnect(false)

// You can disable dapp browser in wallet page, default value is true.
ParticleWalletGUI.setSupportDappBrowser(false)

// show or hide test network, default value is false.
ParticleWalletGUI.setShowTestNetwork(false)

// support chain, Particle Connect initialize chain will add as a support chain automatically.
// default support all chains in Particle Network base
ParticleWalletGUI.setSupportChain([.bsc, .arbitrum, .harmony])

// show or hide manage wallet page, default value is true.
ParticleWalletGUI.setShowManageWallet(true)

// show language setting in setting page, default value is false.
ParticleWalletGUI.setShowLanguageSetting(true)

// show appearance setting in setting page, default value is false.
ParticleWalletGUI.setShowAppearanceSetting(true)

// set support add token, default value is true, true will show add token button, false will hide add token button.
ParticleWalletGUI.setSupportAddToken(false)

// Set display token addresses 
// If you called this method, 
// Wallet SDK will only show these tokens in the token addresses, 
// works only on the wallet page token list.
// You can pass nil to reset.
ParticleWalletGUI.setDisplayTokenAddresses([tokenAddress])

// Set display NFT contract addresses
// If you called this method, Wallet SDK will only show NFTs in the NFT contract addresses.
// You can pass nil to reset.
ParticleWalletGUI.setDisplayNFTContractAddresses([nftContractAddress])

// Set priority token addresses, priority token will show in top part of the list.
// If you called this method, 
// Wallet SDK will show these tokens in top part of the list, 
// works on the wallet page token list, token send select page.
// You can pass nil to reset.
ParticleWalletGUI.setPriorityTokenAddresses([tokenAddress])

// Set priority NFT contract addresses, priority NFT will show in top part of list.
// If you called this method, Wallet SDK will only show NFTs in top part of list.
// You can pass nil to reset.
ParticleWalletGUI.setPriorityNFTContractAddresses([nftContractAddress])

// Set custom token addresses
// If you called this method, Wallet SDK will show these tokens in token list, unless the user hide them
// If you called setDisplayTokenAddresses, this method won't work in wallet page token list.
// The method works on the wallet page token list and token send select page.
ParticleWalletGUI.setCustomTokenAddresses([tokenAddress])

// Set custom ui is pass json string
let jsonString = "your custom ui json string, keys should be the same with customUIConfig.json in demo"
try! ParticleWalletGUI.loadCustomUIJsonString(jsonString)

// Set your wallet name and icon, only support WalletType particle and authcore
ConnectManager.setCustomWalletName(walletType: .particle, name: .init(name: "your wallet name", icon: "your wallet icon url"))

// Set your custom localizable string
let customLocalizable = [Language.en: ["network fee": "Service Fee",
        "particle auth wallet": "Your Wallet Name"]]
ParticleWalletGUI.setCustomLocalizable(customLocalizable)

If your want to support wallet connect feature in GUI, should call method handleWalletConnectUrl.

// in AppDelegate.swift, under application(_:open:options:)
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
    if ParticleWalletGUI.handleWalletConnectUrl(url, withScheme: "particlewallet") {
        return true
    } else {
        return ParticleConnect.handleUrl(url)
    }
}

Switch Wallet

Particle Wallet GUI support multi wallets, if you switch to another wallet or connect another wallet , you should call this method to make sure Particle Wallet GUI show the right address.

/// Switch wallet before wallet open any ParticleWalletGUI page.
/// If you swich another wallet in your app, you should call this method to tell gui, which to show later.
/// GUI will sync with ParticleConnect and set default wallet with wallet type and public address.
/// - Parameters:
///   - walletType: Wallet type
///   - publicAddress: Public address
/// - Returns: If wallet is found, return ture and set the default wallet, otherwise return false.
ParticleWalletGUI.switchWallet(walletType: .metaMask, publicAddress: "YOUR_PUBLIC_ADDRESS")

Open Wallet

PNRouter.navigatorWallet()

Embed wallet page as a child in your tabViewController

// the walletVc is a navigator controller, don't embed it to other navigation controller.
let walletVc = PNRouter.extractWallet(hiddenBackButton: true)

// set it to your tabViewController
tabViewController.viewControllers = [otherVc1, walletVc, otherVc2]

Open Send Token

// open send token
let tokenSendConfig = TokenSendConfig(tokenAddress: nil, toAddress: nil, amount: nil)
PNRouter.navigatorTokenSend(tokenSendConfig: tokenSendConfig)

// open send default token by chain name
PNRouter.navigator(routhPath: .tokenSend)

TokenSendConfig has three parameters:

  1. tokenAddress: token mint address(optional)

  2. toAddress: receiver address (optional)

  3. amount: send token amount, token minimum unit (optional)

Open Receive Token

Display your address QR code.

// display default QR code
PNRouter.navigatorTokenReceive()
// display QR code with token image in center.