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
Core Types EIP-7702 Types UserOperation Types Solana Types Account & Quote 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
Wrong signing method for account type
Problem : Using signTypedData()
for Kernel 3.3 accounts or signMessage()
for role-based accounts.Solution : Check your account type and use the correct signing method:
Role-based: Use signTypedData()
Kernel 3.3 (EIP-7702): Use signMessage()
with UserOperation hash
Invalid Solana signature format
Missing delegation signature for EIP-7702
Problem : EIP-7702 operations fail due to missing delegation signatures.Solution : Always check if delegation is required and sign it first:if ( operation . delegation ) {
const signedTuple = await signerAccount . signAuthorization ( authTuple );
// ... handle delegation signature
}
UserOperation hash mismatch
Problem : Kernel 3.3 account signatures fail due to incorrect UserOperation hash calculation.Solution : Ensure proper deserialization and use correct entrypoint:const deserializedUserOp = deserializeUserOp ( operation . userOp );
const userOpHash = getUserOperationHash < '0.7' >({
userOperation: deserializedUserOp ,
entryPointAddress: entryPoint07Address ,
entryPointVersion: '0.7' ,
chainId: chainId ,
});
Next Steps