OnChain Onboarding

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, and FundsManagementExecutor). 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

  1. Create the Account Abstraction Wallet using ZeroDev SDK.
  2. Install Custom Modules:`
    1. ExecutionDelayPolicy for time-locked security
    2. FundsManagementExecutor for Wirex Pay-controlled charges
  3. 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;
}