Leveraging Particle's AA SDK within iOS applications
Account Abstraction for iOS
Particle Network's AA SDK, existing at the center of its Smart Wallet-as-a-Service, facilitates flexible end-to-end utilization of ERC-4337 account abstraction, natively supporting modular smart account implementations, Paymasters, and natural Bundler usage. Within iOS applications, the Particle Network AA SDK can be used (in tandem with Particle Auth or Particle Connect) to refine user experience through account abstraction.
Details on configuration, initialization, and utilization are listed below.
Getting Started
To get started, you'll need to have already integrated either Particle Connect or Particle Auth within your iOS application. If you haven't, head over to those pages, follow the configuration process, and return here. Otherwise, you can begin by including ParticleAA
within your Podfile, as shown below:
pod 'ParticleAA'
This new addition to your Podfile can then be settled and installed by running the command below:
pod install --repo-update
Important details before initialization
Before initializing the SDK, there are a few points to keep in mind, specifically regarding the utilization of Paymasters (to sponsor gas fees, pay for gas in ERC-20 tokens, etc.)
- All Testnets automatically have the Verifying Particle Network Omnichain Verifying Paymaster enabled. Transactions requesting it will automatically be sponsored and thus gasless.
- On the occasion that you'd like to use the Particle Network Omnichain Verifying Paymaster for Mainnets, you'll need to deposit USDT on either Ethereum or BNB Chain within the Particle dashboard. This USDT will then automatically be converted as needed into the native token of the network you're requesting (and qualifying for) sponsorship on.
- Alternatively, if you'd like to instead use Biconomy's Verifying Paymaster, you can head over to the Biconomy dashboard, create a new Paymaster, and fill in
biconomyApiKeys
withinAAservice.initialize
.- The Particle Network AA SDK automatically uses Biconomy's Token Paymaster; transactions that request it will be able to leverage it without additional configuration
Initialization
To begin, before using the SDK, you'll need to initialize it; without this, the setAAService
within ParticleNetwork
will fail or raise issues down the line.
You can initialize the SDK through AAService.initialize
, which requires the following parameters:
name
, the name of the smart account implementation to be used, the choices here are:.biconomyV1
, a Biconomy smart account..biconomyV2
, a Biconomy smart account..cyberConnect
, a CyberConnect smart account..simple
, a SimpleAccount implementation..light
, a Light Account.
- Optionally,
biconomyApiKeys
. If you'd prefer natively using Biconomy's Paymaster rather than the built-in Paymaster from Particle Network, then you can do so by passing in an array ofChainInfo
objects mapped to corresponding Biconomy API keys, retrieved from the Biconomy dashboard.
Once AAService.initialize
has been called, you'll need to use ParticleNetwork.setAAService
, passing in an instance of AAService
. This will tell Particle Auth to use your smart account rather than your EOA. Either before or after doing this, you'll also need to call aaService.enableAAMode
.
// the biconomy api keys can be empty collection.
let biconomyApiKeys: [Int: String] = [
ParticleNetwork.ChainInfo.ethereum.chainId: "YOUR_BICONOMY_API_KEY",
ParticleNetwork.ChainInfo.bnbChain.chainId: "YOUR_BICONOMY_API_KEY",
ParticleNetwork.ChainInfo.polygon.chainId: "YOUR_BICONOMY_API_KEY",
]
AAService.initialize(name: .biconomyV1, biconomyApiKeys: biconomyApiKeys)
let aaService = AAService()
aaService.enableAAMode()
ParticleNetwork.setAAService(aaService)
Examples of Utilization
Disable AA Mode
If you'd like to disable AA mode (stop using account abstraction) after enabling it during the initialization process (through enableAAMode
), then you'll need to call aaService.disableAAMode
, with aaService
being an instance of AAService
.
However, if you aren't sure whether AA mode is enabled or not, you can call aaService.isAAModeEnable
, which returns a Boolean representing this status.
E.g.:
aaService.disableAAMode()
let isEnable = aayService.isAAModeEnable()
Get Smart Account
You can also retrieve the address of the smart account, among other details, by calling the getSmartAccount
method on aaService
(which can be defined by ParticleNetwork.getAAService
if needed), in which you can pass by
(your Signer/owner address, which should be an EOA), and chainInfo
, which should be a ChainInfo
object representing the chain on which you're querying account information. E.g.:
let smartAccount = try await aaService.getSmartAccount(by: eoaAddress, chainInfo: chainInfo).value
Send Transaction
Sending a transaction (UserOperation) is also quite simple, primarily just deviating in the fee mechanism being used for it. There are two ways to send transactions with the AA SDK. The first is with signAndSendTransaction
, which will send a singular transaction to the network. Alternatively, you can use quickSendTransactions
for sending batched transactions within a single UserOperation.
ParticleAuthService.signAndSendTransaction
takes the following parameters:
transaction
, a stringified standard transaction object.feeMode
, the mechanism to be used for paying gas fees, can be:.gasless
, for sponsored transactions; this will happen automatically for Testnets, and will pull from your previously defined (or configured) Paymaster for Mainnets..native
, paying for gas fees in a native token (such as ETH)..token
, paying for gas fees in an ERC-20 token (such as USDC), and thus takes one parameter:feeQuote
, to be used when leveraging a Token Paymaster.
- Optionally,
chainInfo
: aChainInfo
object representing the chain on which this transaction will be executed.
Alternatively, for aaService.quickSendTransactions
, the parameters will be the same with the following exceptions:
transaction
being an array of transactions.feeMode
is same with upon.messageSigner
, which is the Signer/owner authenticating the transaction.wholeFeeQuote
, which for eachfeeMode
should be passed with a complete fee quote object.- Optionally,
chainInfo
is same with upon.
E.g.:
// Gasless
await ParticleAuthService.signAndSendTransaction(transaction, feeMode: .gasless, chainInfo: chainInfo)
await aaService.quickSendTransactions([transaction], feeMode: .gasless, messageSigner: self, wholeFeeQuote: wholeFeeQuote, chainInfo: chainInfo)
// Native
await ParticleAuthService.signAndSendTransaction(transaction, feeMode: .native, chainInfo: chainInfo)
await aaService.quickSendTransactions([transaction], feeMode: .native, messageSigner: self, wholeFeeQuote: wholeFeeQuote, chainInfo: chainInfo)
// ERC-20 Token
await ParticleAuthService.signAndSendTransaction(transaction, feeMode: .token(feeQuote), chainInfo: chainInfo)
await aaService.quickSendTransactions([transaction], feeMode: .token(feeQuote), messageSigner: self, wholeFeeQuote: wholeFeeQuote, chainInfo: chainInfo)
Implement MessageSigner protocol
class YourViewController {
var publicAddress: String = ""
}
extension YourViewController: MessageSigner {
func signMessage(_ message: String, chainInfo: ParticleNetworkBase.ParticleNetwork.ChainInfo?) -> RxSwift.Single<String> {
// if you are using ParticleAuthService
return ParticleAuthService.signMessage(message, chainInfo: chainInfo)
// if you are using ParticleConnectService
// get current adapter by walletType and publicAddress
// here we assume currentWalletType is Metamask
let currentWalletType = WalletType.metaMask
let adapters = ParticleConnect.getAllAdapters().filter {
$0.walletType == currentWalletType
}
if let adapter = adapters.first {
return adapter.signMessage(publicAddress: self.publicAddress, message: message)
} else {
return .error(NSError(domain: "", code: 0))
}
}
func getEoaAddress() -> String {
// if you are using ParticleAuthService
return ParticleAuthService.getAddress()
// if you are using ParticleConnectService
return self.publicAddress
}
}
Check if you can send gasless/native/token.
extension YourViewController {
func sendTransactionInAA() async throws {
let aa = ParticleNetwork.getAAService()!
let chainInfo = ParticleNetwork.getChainInfo()
let eoaAddress = ""
let transaction = ""
let wholeFeeQuote = try await aa.rpcGetFeeQuotes(eoaAddress: eoaAddress, transactions: [transaction], chainInfo: chainInfo).value
if wholeFeeQuote.gasless != nil {
// There are two ways to send gasless
// 1, call adapter or ParticleAuthService, requires one transaction
let txHash1 = try await ParticleAuthService.signAndSendTransaction(transaction, feeMode: .gasless, chainInfo: chainInfo).value
// 2, call aa, support multi transactions, requires implement MessageSigner delegate.
let txHash2 = try await aa.quickSendTransactions([transaction], feeMode: .gasless, messageSigner: self, wholeFeeQuote: wholeFeeQuote, chainInfo: chainInfo).value
} else {
// you can't gasless
}
let nativeFeeQuote = AA.FeeQuote(json: wholeFeeQuote.native.feeQuote, tokenPaymasterAddress: nil)
if nativeFeeQuote.isEnoughForPay {
// There are two ways to send, pay token
// 1, call adapter or ParticleAuthService, requires one transaction
let txHash1 = try await ParticleAuthService.signAndSendTransaction(transaction, feeMode: .native, chainInfo: chainInfo).value
// 2, call aa, support multi transactions, requires implement MessageSigner delegate.
let txHash2 = try await aa.quickSendTransactions([transaction], feeMode: .native, messageSigner: self, wholeFeeQuote: wholeFeeQuote, chainInfo: chainInfo).value
} else {
// you don't have enough native to pay
}
if let token = wholeFeeQuote.token {
let tokenPaymasterAddress = token.tokenPaymasterAddress
let tokenFeeQuotes = token.feeQuotes.map {
AA.FeeQuote(json: $0, tokenPaymasterAddress: tokenPaymasterAddress)
}.filter {
// filter out balance >= fee
$0.isEnoughForPay
}
if tokenFeeQuotes.count > 0 {
// select a feeQuote, here we select the first one.
let feeQuote = tokenFeeQuotes[0]
// There are two ways to send, pay token
// 1, call adapter or ParticleAuthService, requires one transaction
let txHash1 = try await ParticleAuthService.signAndSendTransaction(transaction, feeMode: .token(feeQuote), chainInfo: chainInfo).value
// 2, call aa, support multi transactions, requires implement MessageSigner delegate.
let txHash2 = try await aa.quickSendTransactions([transaction], feeMode: .token(feeQuote), messageSigner: self, wholeFeeQuote: wholeFeeQuote, chainInfo: chainInfo).value
} else {
// you can't select token to pay
}
}
}
}
Manually Deploy Smart Account
An undeployed smart account will automatically be deployed upon the first transaction (UserOperation) it sends through the SDK (the deployment transaction is bundled/batched with the other). If you'd like to initiate deployment manually, bypassing automatic deployment, then you can use aaService.deployWalletContract
, which will create, request signature for, and send a deployment transaction. This method requires messageSigner
(the Signer of the transaction), and the feeMode
of the deployment transaction).
The status of deployment can be retrieved with smartAccount.isDeploy
, which takes an owner/Signer eoaAddress
.
E.g.:
let isDeploy = try await aaService.isDeploy(eoaAddress: eoaAddress).value
aaService.deployWalletContract(messageSigner: signer, feeMode: .gasless)
Master reference
For a direct, raw view into every method, below is a table containing every relevant one alongside specific parameters and a short description. For methods listed that weren't covered in the above examples, live implementation often mimics the previously covered common structure throughout the SDK.
Methods | Parameters (* indicates optional) |
---|---|
rpcGetSmartAccount | eoaAddresses: [String], chainInfo*: ParticleNetwork.ChainInfo |
rpcGetFeeQuotes | eoaAddress: String, transactions: [String], chainInfo*: ParticleNetwork.ChainInfo |
rpcCreateUserOp | eoaAddress: String, transactions: [String], feeQuote: AA.FeeQuote, tokenPaymasterAddress: String, chainInfo*: ParticleNetwork.ChainInfo |
rpcSendUserOp | eoaAddress: String, userOp: JSON, chainInfo*: ParticleNetwork.ChainInfo |
isDeploy | eoaAddress: String, chainInfo*: ParticleNetwork.ChainInfo |
deployWalletContract | messageSigner: MessageSigner, feeMode*: AA.FeeMode, chainInfo*: ParticleNetwork.ChainInfo |
isAAModeEnable | |
enableAAMode | |
disableAAMode | |
getSmartAccount | by eosAddress: String, chainInfo*: ParticleNetwork.ChainInfo |
getSmartAccounts | by publicAddresses: [String], chainInfo*: ParticleNetwork.ChainInfo |
quickSendTransactions | transactions: [String], feeMode*: AA.FeeMode, messageSigner: MessageSigner, wholeFeeQuote*: AA.WholeFeeQuote, chainInfo*: ParticleNetwork.ChainInfo |
isSupportChainInfo | _ chainInfo: ParticleNetwork.ChainInfo |
getSupportChainInfos |