iOS (Swift)

Working with Particle Wallet within iOS applications

Particle Wallet for iOS

Particle Wallet acts as an optional interface alongside an application to utilize an account derived by social login. On iOS, it provides a standard wallet UI for sending, swapping, and purchasing cryptocurrency natively within a linked account. The underlying interface is fundamentally customizable and modularized, allowing for an application-specific wallet experience.

A detailed overview of the configuration and utilization of the Particle Wallet iOS SDK is included below.


Getting Started

Configuration and implementation of Particle Wallet within iOS applications is relatively straightforward, although there are several prerequisites to ensure you avoid potential compatibility issues. These are as follows.

Prerequisites

  • Xcode 15.0 or later.
  • CocoaPods 1.14.3 or higher.
  • Your project must target these platform versions or later:
    • iOS 14.

Configuration

To begin the initialization process, you'll need to head over to the Particle dashboard to create a project alongside a corresponding application. These will control specific customizations, statistics, etc. for your particular instance of Particle's Wallet-as-a-Service. After creating an application, you'll need to retrieve three key values to use later: projectId, clientKey, and appId later.

  1. Sign up/log in to the Particle dashboard.

  1. Create a new project or enter an existing one.

  1. Create a new iOS application, or skip this step if you already have one.

  1. Retrieve the project ID (projectId), the client key (clientKey), and the application ID (appId).

‎‎

‎‎

Installation

Now that you've created a project and an application within the Particle dashboard, you'll need to go ahead and install the Particle Wallet SDK using CocoaPads (assuming you meet the minimum version requirements).

  1. If you don't already have a Podfile, create one from the root of your project directory with:

    pod init
    
  2. Within the newly created Podfile, add the ParticleWalletAPI and ParticleWalletGUI pod.

    pod 'ParticleWalletAPI'
    pod 'ParticleWalletGUI'
    pod 'ParticleWalletConnect'
    
  3. Install the pods, then open your corresponding .xcworkspace file to see the project in Xcode.

    pod install --repo-update
    open your-project.xcworkspace
    
  4. Add the Privacy/Cameras usage description within your info.plist file to ensure proper permissions exist to take photos for usage of QR codes

Podfile Configuration

Additionally, you'll need to ensure that your resulting Podfile is properly configured, as in the example below.

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

And if you're using ParticleWalletGUI (which is optional if you're using a custom interface), then you'll also need to include the following:

pod 'SkeletonView', :git => 'https://github.com/SunZhiC/SkeletonView.git', :branch => 'main'

Examples of Utilization, API

Get Price

Beginning with ParticleWalletAPI, you can use Particle Wallet to directly call data API methods and retrieve information that may be relevant to the underlying account and wallet. One of these methods includes getPrice on either ParticleWalletAPI.getSolanaService or ParticleWalletAPI.getEvmService. getPrice is used for the retrieval of the real-time price of tokens defined within the methods first parameter, addresses (in which the native token is "native"), denominated in currencies listed within currencies. E.g.:

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)

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)

Tokens by Address

getTokensAndNFTs alongside getTokensAndNFTsFromDB (returns from database without RPC calls) return a list of ERC20/721/SPL tokens associated with a given address. This can be called on either ParticleWalletAPI.getSolanaService or ParticleWalletAPI.getEvmService, both taking a singular parameter (address), dictating the specific account to be queried. E.g.:

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

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

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

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

Transaction History by Address

Another solid example of utilizing ParticleWalletAPI is the retrieval of transaction history by address. Either getTransactions or getTransactionsFromDB (returns from database without RPC calls) will return a list of transactions associated with a given address. This can be called on either ParticleWalletAPI.getSolanaService or ParticleWalletAPI.getEvmService. For getEvmService, you'll only need to pass in a given targeted address, otherwise for getSolanaService, you have the option of passing additional parameters such as beforeSignature, untilSignature, and limit. E.g.:

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)

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)

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

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

Create Transaction

ParticleWalletAPI.createTransactionfacilitates the construction of a transaction object derived from the standard from, to, amount (value), and data fields. This transaction, once constructed with ParticleWalletAPI.createTransaction, can be passed for in-UI proposal with auth.sendTransaction or adapter.signAndSendTransaction. Specifically, eight key parameters are available within ParticleWalletAPI.getEvmService().createTransaction:

  • from, user public address, which to sign the transaction.
  • 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.
  • data, data, default is 0x
  • contractParams, Optional, which is mutually exclusive with data, used to calculate the data.
  • type, default is 0x2, support EIP1559
  • gasFeeLevel, default is medium.
  • action, default is normal, support cancel and speedUp.
// before send make sure you account has enough native balance.
let sender = auth.evm.getAddress()
let receiver = "0xAC6d81182998EA5c196a4424EA6AB250C7eb175b"
// send 0.0001 native, need convert to the minimal unit.
let amount = BDouble(0.0001 * pow(10, 18)).rounded()

let transaction = try await ParticleWalletAPI.getEvmService().createTransaction(from: sender, to: receiver, value: amount.toHexString()).value 
let signature = try await auth.evm.sendTransaction(transaction)
// before send make sure you account has enough native balance for pay gasfee and enough erc20 tokens.
let from = auth.evm.getAddress()
// send 0.0001 erc20 token, need convert to the minimal unit, suppose the token decimals is 18.
let amount = BDouble(0.0001 * pow(10, 18)).rounded()
// the erc20 token contract address
let contractAddress = "0xa36085F69e2889c224210F603D836748e7dC0088"
// this is receiver address
let receiver = "0xAC6d81182998EA5c196a4424EA6AB250C7eb175b"
    
let contractParams = ContractParams.erc20Transfer(contractAddress: contractAddress, to: receiver, amount: amount)
// Note the to address is the token contract address, not receiver address.
let transaction = try await ParticleWalletAPI.getEvmService().createTransaction(from: from, to: contractAddress, value: nil, contractParams: contractParams).value 
let signature = auth.evm.sendTransaction(transaction)
// before send make sure you account has enough native balance for pay gasfee and this erc721 NFT.
let from = auth.evm.getAddress()
// this is your nft contract address
let contractAddress = "0xD18e451c11A6852Fb92291Dc59bE35a59d143836"
// this is the receiver address
let receiver = "0xAC6d81182998EA5c196a4424EA6AB250C7eb175b"
// your erc721 NFT token id, 
let tokenId = "2302"
    
let contractParams = ContractParams.erc721SafeTransferFrom(contractAddress: contractAddress, from: from, to: receiver, tokenId: tokenId)
// Note the to address is the nft contract address, not receiver address.
let transaction = ParticleWalletAPI.getEvmService().createTransaction(from: from, to: contractAddress, value: nil, contractParams: contractParams).value
let signature = try await auth.evm.sendTransaction(transaction) 
// before send make sure you account has enough native balance for pay gasfee and this erc1155 NFT.
let from = auth.evm.getAddress()
// this is your nft contract address
let contractAddress = "0xD18e451c11A6852Fb92291Dc59bE35a59d143836"
// this is the receiver address
let receiver = "0xAC6d81182998EA5c196a4424EA6AB250C7eb175b"
// your erc721 NFT token id, 
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")
// Note the to address is the nft contract address, not receiver address.
let transaction = ParticleWalletAPI.getEvmService().createTransaction(from: from, to: contractAddress, value: nil, contractParams: contractParams).value
let signature = try await auth.evm.sendTransaction(transaction) 
//  before send make sure you account has enough native balance for pay gasfee
let data = getContractData() 
let from = auth.evm.getAddress()
let to = ""

let transacion = try await ParticleWalletAPI.getEvmService().createTransaction(from: from, to: to, data: data).value 
let signature = try await auth.evm.sendTransaction(transaction)

Contract Interaction

Read Contract

Specifically, four key parameters are available within ParticleWalletAPI.getEvmService().readContract:

  • contractAddress, contract address.
  • methodName, method name in the contract.
  • params, the parameters for the method, each parameter requires a hexadecimal string.
  • abiJsonString, the abi json string for the method.
// an example for abiJsonString "[{"inputs":[{"internalType":"uint256","name":"quantity","type":"uint256"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"mint","outputs":\[],"stateMutability":"nonpayable","type":"function"}]"
let params = ContractParams.customAbiEncodeFunctionCall(
  contractAddress: "0xd000f000aa1f8accbd5815056ea32a54777b2fc4", 
  methodName: "balanceOf", 
  params: ["0xBbc1CA8776EfDeC12C75e218C64e96ce52aC6671"], 
  abiJsonString: nil)  
let result = try await ParticleWalletAPI.getEvmService().readContract(contractParams: params).value

Write Contract

Specifically, four five parameters are available within ParticleWalletAPI.getEvmService().writeContract:

The first four parameters are the same as readContract.

  • from, your public address.
let params = ContractParams.customAbiEncodeFunctionCall(
  contractAddress: "0xd000f000aa1f8accbd5815056ea32a54777b2fc4", 
  methodName: "mint", 
  params: ["0x1"], 
  abiJsonString: nil)  
let from = auth.evm.getAddress()
let result = try await ParticleWalletAPI.getEvmService().writeContract(contractParams: params, from: from).value 

Examples of Utilization, Wallet

In addition to ParticleWalletAPI, you can also use ParticleWalletGUI as the primary means of programmatically configuring and interacting with Particle Wallet. To begin, ParticleWalletGUI relies on WalletConnect through Particle Connect to enable usage of external Web3 wallet (such as Metamask or Phantom) within Particle Wallet. To ensure this works properly, you'll need to call setWalletConnectV2ProjectId on ParticleWalletConnect, passing in your WalletConnect project ID retrieved from the WalletConnect dashboard, as is shown below.

ParticleWalletConnect.setWalletConnectV2ProjectId("WalletConnect project ID")

Custom Wallet UI

With ParticleWalletGUI, one of the primary functions is configuring the wallet interface for your specific application. This is managed through numerous methods, all of which toggle different interface components, enabling deep customization. These methods include:

  • setSwapDisabled, whether or not the "Swap" functionality is displayed within the interface. This takes one parameter, either true or false, with the default being false.
  • setSupportWalletConnect, controls the visibility of the WalletConnect feature within the interface. It accepts a Boolean parameter, true or false, and is set to true by default.
  • setSupportDappBrowser, determines whether the DApp browser is available on the wallet page. It takes a Boolean parameter, true or false, with true as the default value
  • setShowTestNetwork, decides if the test network should be shown or hidden. This accepts a Boolean value, true or false, defaulting to false
  • setSupportChain, specifies the blockchain networks supported by Particle Connect. Accepts an array of chains.
  • setShowManageWallet, toggles the visibility of the manage wallet page. It takes a Boolean value, true or false, with true as the default.
  • setShowLanguageSetting, controls the display of language settings in the settings page. It accepts a Boolean value, true or false, with the default being false.
  • setShowAppearanceSetting, decides whether the appearance settings are shown in the settings page. It takes a Boolean value, true or false, defaulting to false.
  • setSupportAddToken, toggles the option to add tokens, with true showing the add token button and false hiding it. The default value is true.
  • setDisplayTokenAddresses, sets specific token addresses to be displayed in the wallet; other tokens won't be displayed. It accepts an array of token addresses or nil to reset.
  • setDisplayNFTContractAddresses, configures the wallet to display NFTs exclusively from specified NFT contract addresses. It accepts an array of addresses or nil to reset.
  • setPriorityTokenAddresses, sets priority token addresses that will appear at the top of the list. It accepts an array of token addresses or nil to reset (reflected on the wallet page's token list and token send page).
  • setPriorityNFTContractAddresses, specifies NFT contract addresses for priority display at the top of the list. It takes an array of NFT contract addresses.
  • setCustomTokenAddresses, allows for the specification of custom token addresses to be displayed in the token list, unless hidden by the user. This method is overridden if setDisplayTokenAddresses is used (reflected on the wallet page token list and token send select page).
  • loadCustomUIJsonString, sets a custom UI by passing a JSON string.
  • setCustomWalletName, configures a custom name and icon for the wallet, supported for the WalletType of particle and authcore. It requires specifying the wallet type, name, and icon URL.
  • setCustomLocalizable, allows for setting custom localizable strings

E.g.:

ParticleWalletGUI.setPayDisabled(false)

ParticleWalletGUI.setSwapDisabled(false)

ParticleWalletGUI.setSupportWalletConnect(false)

ParticleWalletGUI.setSupportDappBrowser(false)

ParticleWalletGUI.setShowTestNetwork(false)

ParticleWalletGUI.setSupportChain([.bsc, .arbitrum, .harmony])

ParticleWalletGUI.setShowManageWallet(true) 

ParticleWalletGUI.setShowLanguageSetting(true)

ParticleWalletGUI.setShowAppearanceSetting(true)

ParticleWalletGUI.setSupportAddToken(false)

ParticleWalletGUI.setDisplayTokenAddresses([tokenAddress])

ParticleWalletGUI.([nftContractAddress])

ParticleWalletGUI.setPriorityTokenAddresses([tokenAddress])

ParticleWalletGUI.setPriorityNFTContractAddresses([nftContractAddress])

ParticleWalletGUI.setCustomTokenAddresses([tokenAddress])


let jsonString = ""
try! ParticleWalletGUI.loadCustomUIJsonString(jsonString)

ConnectManager.setCustomWalletName(walletType: .particle, name: .init(name: "Wallet Name", icon: "Icon URL"))

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

Additionally, if you'd like to support the WalletConnect feature within the wallet itself, you'll need to open AppDelegate.swift and paste in the below snippet:

// 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

Before opening the UI, if you'd like to switch the WalletType (walletType in this case) reflected and used within the wallet itself, you can use ParticleWalletGUI.switchWallet, passing in the specific walletType (see for more information) and targeted user address, publicAddress:

ParticleWalletGUI.switchWallet(walletType: .metaMask, publicAddress: "YOUR_PUBLIC_ADDRESS")

Open Wallet

To programmatically open the wallet interface, you'll need to use PNRouter. This will act as the primary mechanism for opening and managing specific pages of Particle Wallet. In this case, you can call PNRouter.navigatorWallet to throw the main page within Particle Wallet. E.g.:

PNRouter.navigatorWallet()

tabViewController Embed

If you'd like to embed the wallet interface within your tabViewController, you'll need to call PNRouter.extractWallet, passing in optional parameters such as hiddenBackButton for configuration before extraction. This will return a navigation controller, which can then be used within tabViewController.viewControllers. E.g.:

let walletVc = PNRouter.extractWallet(hiddenBackButton: true)

tabViewController.viewControllers = [otherVc1, walletVc, otherVc2]

Open Send Token

To open the page coordinating sending a token (token referring to ERC20/SPL tokens), you can call PNRouter.navigatorTokenSend; if you're sending a specific ERC20/SPL token, you can also pass in tokenSendConfig, a TokenSendConfig object containing the following:

  • tokenAddress, the address of the token to be sent (required).
  • toAddress, the recipient's address (optional).
  • toAmount, the amount to be sent (optional).

Otherwise, you can instead use navigator, passing in routhPath as .tokenSend, to open the page with the default native token. E.g.:

let tokenSendConfig = TokenSendConfig(tokenAddress: nil, toAddress: nil, amount: nil)
PNRouter.navigatorTokenSend(tokenSendConfig: tokenSendConfig)

PNRouter.navigator(routhPath: .tokenSend)

Open Receive Token

A page displaying both the user's address and an associated QR code can be opened through PNRouter.navigatorTokenReceive. If you'd like to generate a specific QR code for a token, placing the token logo at the center of the QR code, you can pass in tokenReceiveConfig, a TokenReceiveConfig object containing the tokenAddress of the token in question. E.g.:

PNRouter.navigatorTokenReceive()

let config = TokenReceiveConfig(tokenAddress: "")
PNRouter.navigatorTokenReceive(tokenReceiveConfig: config)

Open Transaction Records

Transaction records (history) for both ERC20/721 tokens can be programmatically opened through PNRouter.navigatorTokenTransactionRecords, passing in tokenTransactionRecordsConfig, a TokenTransactionRecordsConfig object taking the tokenAddress of a specific token to search for.

Otherwise, if you want to display default (native) token transaction records, you can instead use PNRouter.navigator, passing in routhPath as .tokenTransactionRecords.

let tokenTransactionRecordsConfig = TokenTransactionRecordsConfig(tokenAddress: tokenAddress)
PNRouter.navigatorTokenTransactionRecords(tokenTransactionRecordsConfig: tokenTransactionRecordsConfig)

PNRouter.navigator(routhPath: .tokenTransactionRecords)

Open Send NFT

If you'd like to force the NFT sending menu to open, you can use PNRouter.navigatorNFTSend, passing in nftSendConfig, a NFTSendConfig object that takes the following parameters:

  • address, the contract address of the NFT being sent.
  • toAddress, the recipient's address.
  • tokenId, the specific token ID of the NFT you'd like to send (belonging to the collection under address).
  • amount, the volume of NFTs that you'd like to send.

E.g.:

let nftContractAddress = ""
let receiverAddress = ""
let tokenId = ""
let amount = BInt(1)
let config = NFTSendConfig(address: nftContractAddress, toAddress: receiverAddress, tokenId: tokenId, amount: amount)
PNRouter.navigatorNFTSend(nftSendConfig: config)

Open NFT Details

Particle Wallet also natively supports viewing specific NFTs (traits, description, image, etc.). Forcing this menu programmatically can happen through PNRouter.navigatorNFTDetails, passing in nftDetailsConfig, a NFTDetailsConfig object that takes the address of the NFT and the specific tokenId; for Solana. tokenId can be left blank. E.g.:

let address = ""
let tokenId = ""
let config = NFTDetailsConfig(address: address, tokenId: tokenId)
PNRouter.navigatorNFTDetails(nftDetailsConfig: config)

Open Buy Crypto

Particle Wallet also has a native onramp aggregator, allowing users to bring fiat on-chain through various onramp providers. Opening this programmatically can happen through PNRouter.navigatorBuy, passing in several optional parameters to customize the values used within the onramp. Upon calling, this will throw a popup or total redirect to a corresponding configuration of https://ramp.particle.network

The specific parameters that can be used within PNRouter.navigatorBuy are listed below:

NameDescriptionTypeRequired
network[Solana, Ethereum, Binance Smart Chain, Polygon, Tron, Optimism, etc.]stringFalse (True if Particle is not connected)
fiatCoinFiat currency denomination.stringFalse
cryptoCoinCryptocurrency denomination.stringFalse
fiatAmtThe amount of fiat to be automatically filled in as the purchase volume.numberFalse
boolLock selection of fiat currency in the buy menu.boolFalse
fixCryptoCoinLock selection of cryptocurrency in the buy menu.boolFalse
fixFiatAmtLock selection of fiat amount in the buy menu.boolFalse
walletAddressThe wallet address to receive the cryptocurrency.stringFalse (True if Particle is not connected)
let buyCryptoConfig = BuyCryptoConfig(walletAddress: "YOUR WALLET ADDRESS",
                                              network: .ethereum,
                                              cryptoCoin: "ETH",
                                              fiatAmt: 1000,
                                              fiatCoin: "USD",
                                              fixFiatCoin: false,
                                              fixFiatAmt: false,
                                              fixCryptoCoin: false,
                                              theme: "light",
                                              language: "en-US")
        
PNRouter.navigatorBuy(buyCryptoConfig: buyCryptoConfig)

Open Swap

Additionally, Particle Wallet has a built-in swap functionality (retrieves multiple quotes from different providers such as 1inch, iZUMi, etc., routing the swap through the best one) for Mainnets (Solana and EVM). The Swap menu can be opened via PNRouter.navigatorSwap, which alone will open the default swap menu without values filled in, although you can pass in a SwapConfig object, containing:

  • fromTokenAddress, the token to swap from.
  • toTokenAddress, the token to swap to.
  • fromTokenAmount, the amount of fromTokenAddress to be automatically reflected within the UI.
PNRouter.navigatorSwap()

let config = SwapConfig(fromTokenAddress: nil,
                toTokenAddress: nil,
                fromTokenAmount: nil)
PNRouter.navigatorSwap(swapConfig: config)

Open DApp Browser Page

Finally, Particle Wallet also includes a dApp Browser, allowing users to open different dApps (web apps), automatically connecting with the account loaded into the instance of Particle Wallet. This enables account usage across any dApp. This can be programmatically opened through PNRouter.navigatorDappBrowser, taking one parameter, url, which will dictate the site opened. E.g.:

if let url = URL(string: "app.uniswap.org") {
  PNRouter.navigatorDappBrowser(url: url) 
}