OneBalance uses different signing patterns depending on your account type and target blockchain. This guide covers all signing methods with complete code examples.

Overview

OneBalance transactions require signing operations to authorize spending and execute actions. The signing process varies by:
  • Account type: Role-based, Kernel 3.3 (EIP-7702), or native Solana accounts
  • Operation type: Token transfers, contract calls, or cross-chain operations
  • Target blockchain: EVM chains use different patterns than Solana
All OneBalance operations require proper signing before execution. The API provides structured data that must be signed using the correct method for your account type.

Account Types and Signing Methods

Role-Based Accounts

Sign EIP-712 typed data using signTypedData() method

Kernel 3.3 Accounts (EIP-7702)

Sign UserOperation hash using signMessage() method

Solana Accounts

Sign versioned transactions using wallet or private key

TypeScript Types

Essential types for implementing signing functionality:
import { HashTypedDataParameters } from 'viem';

export type Hex = `0x${string}`;

export enum ContractAccountType {
  RoleBased = 'role-based',
  KernelV31 = 'kernel-v3.1-ecdsa',
  KernelV33 = 'kernel-v3.3-ecdsa',
  Solana = 'solana',
}

export interface ChainOperation {
  userOp: SerializedUserOperation;
  typedDataToSign: HashTypedDataParameters;
  assetType: string;
  amount: string;
  delegation?: Delegation;
}

EVM Account Signing

Role-Based Account Signing

Role-based accounts use EIP-712 typed data signing for all operations:
import { createWalletClient, custom } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

// Using private key
const account = privateKeyToAccount('0x...' as Hex);
const signature = await account.signTypedData(operation.typedDataToSign);

// Using browser wallet
const walletClient = createWalletClient({
  transport: custom(window.ethereum),
  account: '0x...'
});
const signature = await walletClient.signTypedData(operation.typedDataToSign);

EIP-7702 Account Signing

EIP-7702 specifically uses Kernel 3.3 accounts which require signing the UserOperation hash, not typed data:
Note: Kernel 3.3 accounts (EIP-7702) must sign UserOperation hash using signMessage(), not typed data. Using the wrong signing method will cause transaction failures.
import { privateKeyToAccount } from 'viem/accounts';
import { entryPoint07Address, getUserOperationHash } from 'viem/account-abstraction';

function deserializeUserOp(userOp: SerializedUserOperation): UserOperation<'0.7'> {
  return {
    sender: userOp.sender,
    nonce: BigInt(userOp.nonce),
    factory: userOp.factory,
    factoryData: userOp.factoryData,
    callData: userOp.callData,
    callGasLimit: BigInt(userOp.callGasLimit),
    verificationGasLimit: BigInt(userOp.verificationGasLimit),
    preVerificationGas: BigInt(userOp.preVerificationGas),
    maxFeePerGas: BigInt(userOp.maxFeePerGas),
    maxPriorityFeePerGas: BigInt(userOp.maxPriorityFeePerGas),
    paymaster: userOp.paymaster,
    paymasterVerificationGasLimit: userOp.paymasterVerificationGasLimit
      ? BigInt(userOp.paymasterVerificationGasLimit)
      : undefined,
    paymasterPostOpGasLimit: userOp.paymasterPostOpGasLimit 
      ? BigInt(userOp.paymasterPostOpGasLimit) 
      : undefined,
    paymasterData: userOp.paymasterData,
    signature: userOp.signature,
  };
}

async function signKernel33Operation(
  operation: ChainOperation,
  privateKey: Hex
): Promise<ChainOperation> {
  const signerAccount = privateKeyToAccount(privateKey);
  const chainId = Number(operation.typedDataToSign.domain.chainId);
  
  // Handle delegation signing for EIP-7702 if needed
  if (operation.delegation) {
    const authTuple = {
      contractAddress: operation.delegation.contractAddress,
      nonce: operation.delegation.nonce,
      chainId: chainId,
    };
    const signedTuple = await signerAccount.signAuthorization(authTuple);
    
    if (signedTuple.yParity == null) {
      throw new Error('Y parity is required');
    }
    
    operation.delegation.signature = {
      chainId: chainId,
      contractAddress: signedTuple.address,
      nonce: signedTuple.nonce,
      r: signedTuple.r,
      s: signedTuple.s,
      v: `0x${Number(signedTuple.v).toString(16).padStart(2, '0')}` as Hex,
      yParity: signedTuple.yParity,
      type: 'Signed',
    };
  }
  
  // Sign UserOperation hash for Kernel accounts
  const deserializedUserOp = deserializeUserOp(operation.userOp);
  const userOpHash = getUserOperationHash<'0.7'>({
    userOperation: deserializedUserOp,
    entryPointAddress: entryPoint07Address,
    entryPointVersion: '0.7',
    chainId: chainId,
  });
  
  return {
    ...operation,
    userOp: { 
      ...operation.userOp, 
      signature: await signerAccount.signMessage({ message: { raw: userOpHash } }) 
    },
  };
}

Universal EVM Signing Function

Here’s a function that handles both account types automatically:
import { privateKeyToAccount } from 'viem/accounts';

enum ContractAccountType {
  RoleBased = 'role-based',
  KernelV31 = 'kernel-v3.1-ecdsa',
  KernelV33 = 'kernel-v3.3-ecdsa'
}

async function signOperation(
  operation: ChainOperation,
  privateKey: Hex,
  accountType: ContractAccountType = ContractAccountType.RoleBased,
): Promise<ChainOperation> {
  const signerAccount = privateKeyToAccount(privateKey);
  
  // Kernel 3.1 and 3.3 accounts sign UserOperation hash
  if (accountType === ContractAccountType.KernelV31 || 
      accountType === ContractAccountType.KernelV33) {
    
    if (!operation.userOp || !operation.typedDataToSign?.domain?.chainId) {
      throw new Error('UserOperation and Chain ID are required for Kernel signing.');
    }
    
    const chainId = Number(operation.typedDataToSign.domain.chainId);
    
    // Handle delegation signing for EIP-7702
    if (operation.delegation) {
      const authTuple = {
        contractAddress: operation.delegation.contractAddress,
        nonce: operation.delegation.nonce,
        chainId: chainId,
      };
      const signedTuple = await signerAccount.signAuthorization(authTuple);
      
      if (signedTuple.yParity == null) {
        throw new Error('Y parity is required');
      }
      
      operation.delegation.signature = {
        chainId: chainId,
        contractAddress: signedTuple.address,
        nonce: signedTuple.nonce,
        r: signedTuple.r,
        s: signedTuple.s,
        v: `0x${Number(signedTuple.v).toString(16).padStart(2, '0')}` as Hex,
        yParity: signedTuple.yParity,
        type: 'Signed',
      };
    }
    
    // Sign UserOperation hash for Kernel 3.1/3.3 accounts
    const deserializedUserOp = deserializeUserOp(operation.userOp);
    const userOpHash = getUserOperationHash<'0.7'>({
      userOperation: deserializedUserOp,
      entryPointAddress: entryPoint07Address,
      entryPointVersion: '0.7',
      chainId: chainId,
    });
    
    return {
      ...operation,
      userOp: { 
        ...operation.userOp, 
        signature: await signerAccount.signMessage({ message: { raw: userOpHash } }) 
      },
    };
  }
  
  // Role-based accounts sign typed data
  if (!operation.typedDataToSign) {
    throw new Error('TypedData is required for role-based account signing.');
  }
  
  return {
    ...operation,
    userOp: { 
      ...operation.userOp, 
      signature: await signerAccount.signTypedData(operation.typedDataToSign) 
    },
  };
}

Solana Account Signing

Solana uses a different transaction structure and signing process than EVM chains:
Solana vs EVM: Solana operations provide dataToSign as base64-encoded message data, not typed data structures. This data must be deserialized into a VersionedTransaction for signing.

Browser Wallet Signing

For browser wallets like Phantom or Solflare:
import { MessageV0, VersionedTransaction } from '@solana/web3.js';
import bs58 from 'bs58';

/**
 * Signs a Solana operation using a browser wallet
 * @param dataToSign - Base64 encoded data from quote response
 * @param wallet - Connected Solana wallet (Phantom, Solflare, etc.)
 * @returns Base58 encoded signature
 */
async function signSolanaOperation(dataToSign: string, wallet: any): Promise<string> {
  try {
    // 1. Convert base64 data to message buffer
    const msgBuffer = Buffer.from(dataToSign, 'base64');

    // 2. Deserialize into MessageV0
    const message = MessageV0.deserialize(msgBuffer);

    // 3. Create versioned transaction
    const transaction = new VersionedTransaction(message);

    // 4. Sign with wallet
    const signedTx = await wallet.signTransaction(transaction);

    // 5. Extract signature and encode as base58
    const signature = bs58.encode(Buffer.from(signedTx.signatures[signedTx.signatures.length - 1]));

    return signature;
  } catch (error) {
    console.error('Error signing Solana operation:', error);
    throw new Error(`Failed to sign Solana transaction: ${error}`);
  }
}

Private Key Signing

For server-side operations or direct private key access:
import { MessageV0, VersionedTransaction, PublicKey } from '@solana/web3.js';
import bs58 from 'bs58';

/**
 * Signs a Solana chain operation with a private key
 * @param accountAddress - The address of the account to sign the chain operation
 * @param privateKey - The private key in base58 format
 * @param chainOp - The chain operation object from quote response
 * @returns The signed chain operation with signature added
 */
export function signSolanaChainOperation(
  accountAddress: string,
  privateKey: string,
  chainOp: SolanaOperation,
): SolanaOperation {
  const msgBuffer = Buffer.from(chainOp.dataToSign, 'base64');
  
  const message = MessageV0.deserialize(msgBuffer);
  
  const transaction = new VersionedTransaction(message);
  
  const decodedKey = bs58.decode(privateKey);
  transaction.sign([
    {
      publicKey: new PublicKey(accountAddress),
      secretKey: Buffer.from(decodedKey),
    },
  ]);
  
  const signature = bs58.encode(Buffer.from(transaction.signatures[transaction.signatures.length - 1]));
  return {
    ...chainOp,
    signature,
  };
}

Quote-Level Solana Signing

For signing complete V3 quotes with multiple operations:
// Types available from the TypeScript Types section above

/**
 * Sign a Solana quote using the provided wallet
 * @param quote - Quote response to sign
 * @param solanaWallet - Solana wallet for signing
 * @returns Signed quote ready for execution
 */
export async function signSolanaQuote(quote: QuoteV3, solanaWallet: any): Promise<QuoteV3> {
  try {
    const signedOperations = await Promise.all(
      quote.originChainsOperations.map(async operation => {
        if ('type' in operation && operation.type === 'solana') {
          const solanaOp = operation as SolanaOperation;

          // Skip if already signed (signature is not empty or "0x")
          if (solanaOp.signature && solanaOp.signature !== '0x' && solanaOp.signature !== '') {
            return solanaOp;
          }

          const signature = await signSolanaOperation(solanaOp.dataToSign, solanaWallet);

          // Create the signed operation, ensuring we replace the signature properly
          const signedOp = {
            ...solanaOp,
            signature: signature, // Replace the "0x" placeholder with actual signature
          };

          return signedOp;
        }
        return operation;
      })
    );

    const signedQuote = {
      ...quote,
      originChainsOperations: signedOperations,
    };

    return signedQuote;
  } catch (error) {
    console.error('Error signing Solana quote:', error);
    throw error;
  }
}

Common Patterns

Sequential Quote Signing

For quotes with multiple operations that need to be signed in sequence:
// Helper to run an array of lazy promises in sequence
export const sequentialPromises = (promises: (() => Promise<any>)[]): Promise<any[]> => {
  return promises.reduce<Promise<any[]>>(
    (acc, curr) => acc.then(results => curr().then(result => [...results, result])),
    Promise.resolve([])
  );
  };

export const signQuote = async (quote: Quote, embeddedWallet: ConnectedWallet) => {
  const signWithEmbeddedWallet = signOperation(embeddedWallet);

  const signedQuote = {
    ...quote,
  };

  signedQuote.originChainsOperations = await sequentialPromises(
    quote.originChainsOperations.map(signWithEmbeddedWallet)
  );

  if (quote.destinationChainOperation) {
    signedQuote.destinationChainOperation = await signWithEmbeddedWallet(
      quote.destinationChainOperation
    )();
  }

  return signedQuote;
};

Error Handling Best Practices

export function getReadableStatus(status: string): string {
  switch (status) {
    case 'PENDING':
      return 'Transaction pending...';
    case 'IN_PROGRESS':
      return 'Processing transaction...';
    case 'COMPLETED':
      return 'Transaction completed successfully!';
    case 'FAILED':
      return 'Transaction failed';
    case 'REFUNDED':
      return 'Transaction refunded';
    default:
      return `Status: ${status}`;
  }
}

// Always wrap signing operations in try-catch
async function safeSignOperation(operation: any, signer: any) {
  try {
    return await signOperation(operation, signer);
  } catch (error) {
    console.error('Signing failed:', error);
    throw new Error(`Failed to sign operation: ${error.message}`);
  }
}

Troubleshooting

Common Issues

Next Steps