On-chain onboarding involves the creation of a smart wallet infrastructure for the user and their registration in the Wirex Pay Accounts Contract. This is the foundation of the non-custodial model—ensuring that the user maintains full ownership and control over their assets through a decentralized Account Abstraction.
- Each user must be provisioned with an Account Abstraction wallet, which can be created using ZeroDev.
- Once the wallet is created, the user must be registered on-chain in the Wirex Pay Accounts Contract, linking their wallet to the system for identity and permission verification.
Key Components
- Account Abstraction Wallet: A smart wallet owned by the user, deployed using ZeroDev. This wallet is used to authorize and execute on-chain transactions.
- ExecutionDelayPolicy: A custom validation module that introduces a time-lock mechanism for specific user actions (e.g., fund transfers). This prevents users from front-running or reversing transactions after a card charge is initiated, adding a layer of fraud protection.
- FundsManagementExecutor: A custom executor module that allows the Wirex Pay platform to securely execute on-chain charge operations on behalf of the user for off-chain activities (e.g., card transactions, withdrawals).
- Contract Registry: A smart contract deployed per supported chain that exposes addresses of key system components (like the
Accounts
contract,ExecutionDelayPolicy
, andFundsManagementExecutor
). The address of the registry will be provided by the Wirex Pay team for each chain.
Requirements
To create an Account Abstraction wallet, the partner must either:
- Control a per-user private key, or
- Use an embedded wallet provider, such as Privy, which can return a signer via a connected wallet instance, or
- Use a form of wallet connection that would allow access to EIP1193 provider.
Steps to Complete On-Chain Onboarding
- Create the Account Abstraction Wallet using ZeroDev SDK.
- Install Custom Modules:`
ExecutionDelayPolicy
for time-locked securityFundsManagementExecutor
for Wirex Pay-controlled charges
- Register the Wallet in the Accounts Contract, linking it with your
PartnerId
to finalize onboarding.
The wallet must be fully deployed and both modules installed before registering the user on-chain.
Complete example
import {ConnectedWallet} from '@privy-io/react-auth';
import {
concatHex, createPublicClient, createWalletClient, custom, EIP1193Provider, encodeFunctionData, http, pad, zeroAddress, zeroHash,
} from 'viem';
import {baseSepolia} from 'viem/chains';
import {signerToEcdsaValidator} from '@zerodev/ecdsa-validator';
import {
createKernelAccount, createKernelAccountClient, createZeroDevPaymasterClient, KernelAccountClient, KernelV3_1AccountAbi,
} from '@zerodev/sdk';
import {getEntryPoint, KERNEL_V3_1, VALIDATOR_TYPE} from '@zerodev/sdk/constants';
import {toSudoPolicy} from '@zerodev/permissions/policies';
import {toPermissionValidator} from '@zerodev/permissions';
import {toECDSASigner} from '@zerodev/permissions/signers';
import {privateKeyToAccount} from 'viem/accounts';
// RPC clients and RPC addresses
const PUBLIC_RPC = 'https://sepolia.base.org';
const PAYMASTER_RPC = 'https://rpc.zerodev.app/api/v2/paymaster/a1ed4ae3-7093-4bf0-aeec-59e95f094662?provider=PIMLICO';
const BUNDLER_RPC = 'https://rpc.zerodev.app/api/v2/bundler/a1ed4ae3-7093-4bf0-aeec-59e95f094662?provider=PIMLICO';
const bundlerTransport = http(BUNDLER_RPC);
const publicClient: any = createPublicClient({
chain: baseSepolia,
transport: http(PUBLIC_RPC),
});
const kernelPublicClient: any = createPublicClient({
transport: bundlerTransport,
chain: baseSepolia,
});
const kernelPaymasterClient: any = createZeroDevPaymasterClient({
chain: baseSepolia,
transport: http(PAYMASTER_RPC),
});
// As of now this is the current ContractRegistry address for Base Sepolia chain
const ContractRegistryAddress = '0xC801a0f38E20500B5840c4927553b3409890bc3c';
// This is a unique identifier of your partner integration. It would be provided to you by Wirex Pay team
const PartnerId = '0x00000000000000000000000000000011'
// Privy flow, ConnectedWallet object is either an embedded wallet or an external waller linked during onboarding
export async function createAccountAbstraction_Privy (mainWallet: ConnectedWallet, wasCreated: boolean) {
let nativeProvider = await mainWallet.getEthereumProvider();
return await createAccountAbstraction_EIP1193Provider(nativeProvider as EIP1193Provider, wasCreated);
}
// EIP1193 flow. Provider should be a EIP1193 compatible object implementing all required functions.
export function createAccountAbstraction_EIP1193Provider (provider: EIP1193Provider, wasCreated: boolean) {
const signer = createWalletClient({
chain: baseSepolia,
transport: custom(provider),
});
return createAccountAbstraction_Base(signer, wasCreated);
}
// Private key flow. The private key should be a valid private key hex string
export function createAccountAbstraction_PrivateKey (privateKey: `0x${string}`, wasCreated: boolean) {
let mainWallet = privateKeyToAccount(privateKey);
return createAccountAbstraction_Base(mainWallet, wasCreated);
}
export async function createAccountAbstraction_Base (signer: any, wasCreated: boolean): Promise<KernelAccountClient> {
// Create an account client using the bare signer for connected wallet in order to retrieve its address.
let simpleValidator = await createEcdsaValidator(signer);
let account = await createKernelAccount(kernelPublicClient, {
entryPoint: getEntryPoint('0.7'),
kernelVersion: KERNEL_V3_1,
plugins: {
sudo: simpleValidator,
},
});
let client = createKernelAccountClient({
account: account,
chain: baseSepolia,
bundlerTransport: bundlerTransport,
paymaster: {
getPaymasterData (userOperation) {
return kernelPaymasterClient.sponsorUserOperation({userOperation});
},
},
});
// If this is a first creation than the AA has to be properly deployed on chain and required modules must be configured before Wirex Pay system can work with it.
if (!wasCreated) {
// Prepare call data for calls to install execution delay policy and funds management executor
let executorCallData = await getInstallExecutorCallData(client.account.address);
let policyCallData = await getInstallPolicyCallData(client.account.address, signer);
let calls = await client.account!.encodeCalls([
executorCallData,
policyCallData
]);
// Execute install operations on the account abstraction. This would execute on-chain operation physically deploying AA contract and
// installing required modules
let trxHash = await client.sendUserOperation({
callData: calls,
});
await client.waitForUserOperationReceipt({
hash: trxHash,
timeout: 30 * 1000,
});
}
// Recreate AA client using its initial address and execution delay sudo policy
let accountAddress = client.account.address;
let validator = await createExecutionDelayValidator(signer);
account = await createKernelAccount(kernelPublicClient, {
entryPoint: getEntryPoint('0.7'),
address: accountAddress,
kernelVersion: KERNEL_V3_1,
plugins: {
sudo: validator,
},
});
client = createKernelAccountClient({
account: account,
chain: baseSepolia,
bundlerTransport: bundlerTransport,
paymaster: {
getPaymasterData (userOperation) {
return kernelPaymasterClient.sponsorUserOperation({userOperation});
},
},
});
// If this is initial deployment than newly deployed AA must be registered in Wirex Pay Accounts contract.
if (!wasCreated) {
// Create call data for registering in Wirex Pay Accounts contract
let accountCreateCallData = await getAccountCreateCallData();
let calls = await client.account!.encodeCalls([
accountCreateCallData
]);
// Execute registration operations on the account abstraction.
let trxHash = await client.sendUserOperation({
callData: calls,
});
await client.waitForUserOperationReceipt({
hash: trxHash,
timeout: 30 * 1000,
});
}
return client
}
// Call data helper methods
async function getInstallExecutorCallData (targetWalletAddress: `0x${string}`) {
let initData = concatHex([
zeroAddress,
zeroHash,
]);
let executorAddress = await publicClient.readContract({
address: ContractRegistryAddress,
abi: ContractRegistryAbi, // Can be retrieved using block explorer of your liking
functionName: 'contractByName',
args: ['FundsManagement'],
}) as `0x${string}`;
let data = encodeFunctionData({
abi: KernelV3_1AccountAbi,
functionName: 'installModule',
args: [
BigInt(2),
executorAddress,
initData,
],
});
return {
to: targetWalletAddress,
value: BigInt(0),
data: data,
}
}
async function getInstallPolicyCallData (targetWalletAddress: `0x${string}`, signer: any) {
let policy = await createExecutionDelayValidator(signer);
let rootValidatorId = concatHex([
VALIDATOR_TYPE[policy.validatorType],
pad(policy.getIdentifier(), {
size: 20,
dir: 'right',
}),
]);
let data = encodeFunctionData({
abi: KernelV3_1AccountAbi,
functionName: 'changeRootValidator',
args: [
rootValidatorId,
zeroAddress,
await policy.getEnableData(targetWalletAddress),
'0x',
],
});
return {
to: targetWalletAddress,
value: BigInt(0),
data: data,
}
}
export async function getAccountCreateCallData() {
let accountsAddress = await publicClient.readContract({
address: ContractRegistryAddress,
abi: ContractRegistryAbi, // Can be retrieved using block explorer of your liking
functionName: 'contractByName',
args: ['Accounts'],
}) as `0x${string}`;
let data = encodeFunctionData({
abi: AccountsAbi, // Can be retrieved using block explorer of your liking
functionName: 'createUserAccountWithWallet',
args: [
PartnerId,
],
});
return {
to: accountsAddress,
value: BigInt(0),
data: data,
};
}
// Validator objects helpers
function createEcdsaValidator (signer: any) {
return signerToEcdsaValidator(kernelPublicClient, {
signer,
entryPoint: getEntryPoint('0.7'),
kernelVersion: KERNEL_V3_1,
});
}
async function createExecutionDelayValidator (signer: any) {
let policyAddress = await publicClient.readContract({
address: ContractRegistryAddress,
abi: ContractRegistryAbi, // Can be retrieved using block explorer of your liking
functionName: 'contractByName',
args: ['ExecutionDelayPolicy'],
}) as `0x${string}`;
const rootPolicy = toSudoPolicy({
policyAddress,
});
let res = await toPermissionValidator(kernelPublicClient, {
entryPoint: getEntryPoint('0.7'),
signer: await toECDSASigner({signer: signer}),
kernelVersion: KERNEL_V3_1,
policies: [rootPolicy],
});
res.address = policyAddress;
return res;
}