OmnesMSA API Docs
Sdk

TypeScript SDK

A comprehensive TypeScript SDK for the MSA API with full type safety and modern developer experience.

The SmartWallet SDK provides a type-safe, modern way to interact with the MSA API. Built with TypeScript-first approach, it offers complete control over UserOperation construction and signing.

For advanced use cases, MSA provides the SmartWallet SDK - a lower-level SDK that gives you complete control over UserOperation construction and signing.

Installation

npm install @omnes/smartwallet-ts-sdk

Key Features

  • βœ… Complete UserOp Control: Build and sign UserOperations manually
  • βœ… Multiple Signers: PKSigner, GCPSigner, MultisigSigner, PasskeySigner
  • βœ… Self & Remote Bundler: Choose your bundler strategy
  • βœ… Custom Signatures: Implement your own signing logic
  • βœ… Gas Optimization: Fine-tune gas settings

Settings

This SDK is quite flexible and accepts several different settings:

  1. Set up different message sign methods, such as: private key, HSM (GCP), multisig, and passkey.
  2. Let the SDK sign the userOp hash or implement custom code/signer to sign it;
  3. Use remote or self bundler;

1. Message Sign Method

The message sign method signs the messages used when creating a new account and installing its validator. The SDK provides various methods embedded in the different Signers included in it:

  • Private key (PKSigner) βœ…
  • GCP HSM (GCPSigner) βœ…
  • Multisig (MultisigSigner) βœ…
  • Passkey (PasskeySigner) ⚠️ (a detailed example will be given later)

We also plan to support other HSM signer schemes, such as:

  • Azure HSM
  • Oracle HSM

You can also implement a custom signer. The only requirement is that it follows the interface below:

interface Signer {
    signMessage(hash: Uint8Array): Promise<Uint8Array>;
    signTx(hashedTx: Uint8Array): Promise<Uint8Array>;
}

After creating your signer, you just need to provide it when you create the SmartWallet client:

const smartWallet = await SmartWallet.create(
    signer.signMessage, // message sign method
    signer.signMessage, // this is the method to sign the userOp hash.
    signer.getEVMAddress(), // regular method defined for all signers provided by the SmartWallet SDK library
    rpcURL,
    apiKey
);

2. UserOp Hash Sign Method

There are situations where you don't want to use the same message sign method to sign the userOp hash. Therefore, you can specify a different sign method. For example, you may want to have a master private key that signs the messages to create the account and allow another private key to sign the userOp hash:

const companySigner = await PKSigner.create(privateKey as `0x${string}`);
const secondSigner = await PKSigner.create(privateKey as `0x${string}`);

const smartWallet = await SmartWallet.create(
    companySigner.signMessage, // message sign method
    secondSigner.signMessage, // this is the method to sign the userOp hash.
    companySigner.getEVMAddress(), // regular method defined for all signers provided by the SmartWallet SDK library
    rpcURL,
    apiKey
);

There is yet another situation where you don't want/need the SDK to sign the userOp hash. For that, you can first generate the UserOperations, sign the userOp hashes separately, and finally send the operations to the blockchain via bundler. A good example for that is using passkey to sign the userOp hash, since the signing needs to happen on the frontend.

First, you need to retrieve the passkey's public key for the user whose operation you want to send to the blockchain.

Important: when generating the passkey, you need to enable the UP and UV flags and use the P-256 curve. Below are the option definitions that should be used when registering a new passkey:

const publicKeyOptions = {
    pubKeyCredParams: [
        {
            type: "public-key" as const,
            alg: -7, // ES256 (P-256 curve)
        },
    ],
    attestation: "direct" as AttestationConveyancePreference,
    authenticatorSelection: {
        authenticatorAttachment: "platform" as AuthenticatorAttachment, // Prefer platform authenticators (computer's built-in)
        residentKey: "required" as ResidentKeyRequirement,
        requireResidentKey: true,
        userVerification: "required" as UserVerificationRequirement, // UV flag
    }
}

Note: the retrieved public key is base64URL encoded and in DER format. KEEP IT SO! Do NOT convert or decode it to any other format. The SDK requires the public key from a passkey to be in this encoding and format.

Specify the operations and generate the UserOperations. We'll instantiate a private key signer and use its signMessage method for account creation message signing:

import { PKSigner, SmartWallet, OperationSettings, AccountOperations, Operation } from '@omnes/smartwallet-ts-sdk';
import { toBytes, toHex } from 'viem';

// Load config variables
const privateKey = process.env.PRIVATE_KEY;
const rpcURL = process.env.RPC_URL as string;
const apiKey = process.env.API_KEY as string;

// Create a signer (private key, GCP HSM, etc.)
const signer = await PKSigner.create(privateKey as `0x${string}`);

// build a user operation
const operations: Operation = {
    to: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC token on Ethereum
    value: BigInt(0), // SDK expects bigint. Value to send in wei.
    funcSignature: "transfer(address,uint256)", // function signature as used in Solidity.
    params: [
        "0xD84F7ac8aB9dB8a259244d629d9c539F2159e6EC",
        100000000 // 100 USDC
    ] // function parameters
};

const accountOperations: AccountOperations[] = [
    {
        account: {
            walletCustody: 2, // Passkey custody
            salt: "salt", // you can use any salt you want to create the account
            publicKeys: [retrievedPublicKey], // Passkey public key(s), base64URL encoded and in DER format
        },
        operations: [operations],
        settings: {} // Empty settings object - SDK will provide defaults
    }
];

Note: the salt determines the final address of your smart wallet. That means you can't reuse the salt if you want to create a new smart wallet.

Then, we encode to UserOperations:

const result = await smartWallet2.buildUserOperations(
    accountOperations,
    [], // useABI - you can define your own ABI here. This is used to parse errors and events.
    [] // signers - if more than one signer, you can provide an array of signer addresses
);

The result follows this structure:

{
    userOps: PackedUserOperation[],
    failedUserOps: PackedUserOperation[],
    accessList: Address[]
}

where PackedUserOperation is:

interface PackedUserOperation {
    sender: Address;
    nonce: bigint;
    initCode: `0x${string}`;
    callData: `0x${string}`;
    accountGasLimits: `0x${string}`;
    preVerificationGas: bigint;
    gasFees: `0x${string}`;
    paymasterAndData: `0x${string}`;
    signature: `0x${string}`;
    userOpHash: `0x${string}`;
    dependsOn: number;
}

Sign the returned UserOperations (result.userOps) using the registered passkey. Below is an example of how to do this in your frontend:

// Extract userOpHashes from userOps
const userOpHashes: string[] = []
if (userOps && result.userOps.length > 0) {
    for (const userOp of userOps) {
        if (userOp.userOpHash) {
            userOpHashes.push(userOp.userOpHash)
        }
    }
}

// Helper function to convert hex to Uint8Array
function hexToUint8Array(hex: string): Uint8Array {
    const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
    const matches = cleanHex.match(/.{1,2}/g);
    return new Uint8Array(matches ? matches.map(byte => parseInt(byte, 16)) : []);
}

// Helper function to convert ArrayBuffer to hex
function arrayBufferToHex(buffer: ArrayBuffer): string {
    return '0x' + Array.from(new Uint8Array(buffer))
        .map(b => b.toString(16).padStart(2, '0'))
        .join('');
}

// Helper function to convert base64URL to ArrayBuffer
function base64UrlToBuffer(base64url: string): ArrayBuffer {
    const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
    const binaryString = atob(base64);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
}

interface PasskeySignature {
    authenticatorData: string;
    clientDataJSON: string;
    r: string;
    s: string;
}

const signedHashes: PasskeySignature[] = [];

for (const hash of userOpHashes) {
    // Convert hex string to Uint8Array for challenge
    const challengeBytes = hexToUint8Array(hash);

    // Create authentication options
    const publicKeyOptions: PublicKeyCredentialRequestOptions = {
        challenge: challengeBytes,
        allowCredentials: [
            {
                id: base64UrlToBuffer(credentialId),
                type: "public-key",
                transports: ["internal", "usb", "nfc", "ble", "hybrid"],
            },
        ],
        userVerification: "required" as UserVerificationRequirement,
        timeout: 60000,
    }

    // Get credentials and sign
    const credential = await navigator.credentials.get({
        publicKey: publicKeyOptions
    }) as PublicKeyCredential

    if (credential && credential.response) {
        // Extract the signature components from the response
        const response = credential.response as AuthenticatorAssertionResponse;

        // Extract authenticatorData and clientDataJSON
        const authenticatorData = arrayBufferToHex(response.authenticatorData);
        let clientDataJSON = new TextDecoder().decode(response.clientDataJSON);

        // Remove the demo field from clientDataJSON
        try {
            const clientData = JSON.parse(clientDataJSON);
            delete clientData.other_keys_can_be_added_here;
            clientDataJSON = JSON.stringify(clientData);
        } catch (e) {
            console.warn("Failed to clean clientDataJSON:", e);
        }
        // Parse the signature to extract r and s components
        const signatureArray = new Uint8Array(response.signature);

        // Parse DER signature to extract r and s
        // This is a simplified DER parsing for ECDSA signatures
        let rStart = 4; // Skip DER header
        let rLength = signatureArray[rStart - 1];
        let r = signatureArray.slice(rStart, rStart + rLength);

        // Remove leading zero if present (for positive numbers)
        if (r[0] === 0) {
            r = r.slice(1);
        }

        let sStart = rStart + rLength + 2; // Skip to s component
        let sLength = signatureArray[sStart - 1];
        let s = signatureArray.slice(sStart, sStart + sLength);

        // Remove leading zero if present (for positive numbers)
        if (s[0] === 0) {
            s = s.slice(1);
        }

        // Convert to big.Int strings
        const rBigInt = BigInt('0x' + Array.from(r).map(b => b.toString(16).padStart(2, '0')).join(''));
        const sBigInt = BigInt('0x' + Array.from(s).map(b => b.toString(16).padStart(2, '0')).join(''));

        const passkeySignature: PasskeySignature = {
            authenticatorData: authenticatorData,
            clientDataJSON: clientDataJSON,
            r: rBigInt.toString(),
            s: sBigInt.toString()
        };

        signedHashes.push(passkeySignature);
    }
}

Note 1: you need to convert the retrieved userOp hashes from hex to bytes array (uint8 array) so that the returned signature from the passkey is in the right format.

Note 2: you need to handle the returned authenticator data (authenticatorData), client data (clientDataJSON), and signature (signature):

  • authenticatorData: convert it from bytes array to hex string. The result should be something like:
    0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000
  • clientDataJSON: from bytes array to stringified JSON (without other_keys_can_be_added_here field). The result should be something like:
    '{"type":"webauthn.get","challenge":"aGVsbG8gd2ViMw","origin":"http://localhost:3000","crossOrigin":false}'
  • signature: from bytes array in DER format to its respective R and S values.

Now we can send the UserOperations to the blockchain:

import { encodeAbiParameters, toHex } from 'viem';

const encodedSignatures = request.signatures.map(sig => {
    return encodeAbiParameters(
        [
            {
                type: 'tuple', components: [
                    { type: 'bytes' },
                    { type: 'string' },
                    { type: 'uint256' },
                    { type: 'uint256' },
                    { type: 'uint256' },
                    { type: 'uint256' }
                ]
            }
        ],
        [
            [
                sig.authenticatorData,
                sig.clientDataJSON,
                BigInt(23), // always 23
                BigInt(1), // always 1
                BigInt(sig.r),
                BigInt(sig.s)
            ]
        ]);
});

const response = await smartWallet.provideSignaturesAndRequestSendUserOperations(
    result.userOps,
    result.accessList,
    encodedSignatures
);

console.log(response.txHash)

Note: you need to encode the passkey signature structure to ABI encoding.

The response follows this structure:

interface Response {
    status: TxStatus;
    txHash: Hash;
    logs: Record<string, any>[];
    cummulatedGasUsed: number;
    gasUsed: number;
    effectiveGasPrice: bigint;
    errors: Record<string, any>[];
    returnData: Record<string, any>[];
    credits: number;
}

3. Bundler

In terms of bundler settings, you can use either the remote or a local/self bundler. You can use the remote bundler only if you opt-in to this service.

Self Bundler

To use a self bundler:

const smartWallet = await SmartWallet.create(
    messagesSignMethod,
    userOpHashSignMethod,
    senderAddress,
    rpcURL,
    apiKey
);

Then, specify the function from your bundler that signs the transaction. You can use a signer from the SDK library or define a custom function that must follow the signTx function from the Signer interface above.provideSignaturesAndSendUserOperations Let's use a GCP HSM signer from the SDK library.

import { GCPSigner } from '@omnes/smartwallet-ts-sdk';

const selfBundler = await GCPSigner.create(
    googleCredentials, 
    googleProjectId,
    kmsLocationId, 
    kmsKeyRingId, 
    clientId, 
    versionId
);

Finally, attach this self bundler to the SmartWallet instance:

smartWallet.attachBundler(selfBundler.signTx);

Remote Bundler

To instantiate the SmartWallet SDK using the remote bundler:

const smartWallet = await SmartWallet.create(
    messagesSignMethod,
    userOpHashSignMethod,
    senderAddress,
    rpcURL,
    apiKey
);

If, for any reason, you desire to use a local/self bundler even though you've opted-in to the remote bundler, you can use:

const smartWallet = await SmartWallet.create(
    messagesSignMethod,
    userOpHashSignMethod,
    senderAddress,
    rpcURL,
    apiKey,
    null,
    true
);

Define your self bundler. Let's use a regular private key bundler:

import { PKSigner } from '@omnes/smartwallet-ts-sdk';

const selfBundler = await PKSigner.create(privateKey as `0x${string}`);

Finally, attach the bundler to the SmartWallet instance:

smartWallet.attachBundler(selfBundler.signTx);

Coontract Addresses

You may have noticed that we didn't specify the addresses of the contracts that are generated when you set up your account. If you are using our contract addresses storing service, you don't need pass those addresses when you instantiate the SmartWallet. However, if you haven't opted-in to that service, you are required to pass the addresses.

The structure that defines the addresses is shown below:

interface Contracts {
    factory?: Address;
    validator?: Address;
    validatorType?: number;
    paymaster?: Address;
    paymasterType?: number;
    aggregator?: Address;
    aggregatorType?: number;
}

You need to pass at least the factory and validator addresses (factory and validator) as well as the validator type (validatorType). If you've set up a paymaster, pass the address (paymaster) and its type (paymasterType) as well. If you haven't set-up a paymaster, the SDK understands that you will sponsor all the transaction costs and no refund will be transferred to you.

Similarly, input the aggregator address (aggregator) and type (aggregatorType) if you have set up one.

The types are:

enum ValidatorType {
    UNSET = 0,
    ECDSA_VALIDATOR = 1,
    PASSKEY_VALIDATOR = 2,
    ECDSA_PASSKEY_VALIDATOR = 3,
    MULTISIG_VALIDATOR = 4,
    MULTISIG_PASSKEY_VALIDATOR = 5,
    MULTICHAIN_VALIDATOR = 6,
    CUSTOM_VALIDATOR = 7,
}

enum PaymasterType {
    UNSET = 0,
    SPONSOR_PAYMASTER = 1,
    DEPOSIT_PAYMASTER = 2,
    MIX_PAYMASTER = 3,
}

enum AggregatorType {
    UNSET = 0,
    K1_AGGREGATOR = 1,
    MERKLE_TREE_AGGREGATOR = 2,
}

An example for instantiating the SmartWallet with the addresses:

const contracts = {
    factory: "0xD84F7ac8aB9dB8a259244d629d9c539F2159e6EC";
    validator: "0x843821106057e0a15e10adbC69238b051707FFd1";
    validatorType: ValidatorType.ECDSA_VALIDATOR;
    paymaster: "0x1d5bE4673345116E5870f93694fe6c1207d81637";
    paymasterType: PaymasterType.SPONSOR_PAYMASTER;
}

const smartWallet = await SmartWallet.create(
    messagesSignMethod,
    userOpHashSignMethod,
    senderAddress,
    rpcURL,
    apiKey,
    contracts
);

πŸ’° Paymaster - Gas-Free Transactions

The Paymaster feature allows you to sponsor gas fees for your users' transactions, creating a seamless Web3 experience where users don't need to worry about gas costs!

How It Works

When a paymaster address is configured (either passed as input or stored in our service), the SDK automatically integrates the paymaster flow into every UserOperation. This means:

  • βœ… Users don't pay gas - Transactions are sponsored
  • βœ… Automatic integration - No extra code needed
  • βœ… Flexible funding - Add funds via dashboard anytime

Setting Up Paymaster

You can configure a paymaster in two ways:

1. Using Contract Addresses

Pass the paymaster address when creating the SmartWallet instance:

const contracts = {
    factory: "0xD84F7ac8aB9dB8a259244d629d9c539F2159e6EC",
    validator: "0x843821106057e0a15e10adbC69238b051707FFd1",
    validatorType: ValidatorType.ECDSA_VALIDATOR,
    paymaster: "0x1d5bE4673345116E5870f93694fe6c1207d81637", // Your paymaster address
    paymasterType: PaymasterType.SPONSOR_PAYMASTER, // or DEPOSIT_PAYMASTER, MIX_PAYMASTER
}

const smartWallet = await SmartWallet.create(
    messagesSignMethod,
    userOpHashSignMethod,
    senderAddress,
    rpcURL,
    apiKey,
    contracts
);

2. Using Our Storage Service

If you've opted-in to our contract addresses storing service, the paymaster will be automatically configured. Just make sure your paymaster is set up in the SmartWallet dashboard.

Paymaster Types

The SDK supports different paymaster types:

enum PaymasterType {
    UNSET = 0,
    SPONSOR_PAYMASTER = 1,    // Fully sponsored transactions
    DEPOSIT_PAYMASTER = 2,    // User deposits, paymaster pays
    MIX_PAYMASTER = 3,        // Hybrid model
}
  • SPONSOR_PAYMASTER: You pay all gas fees for users
  • DEPOSIT_PAYMASTER: Users deposit funds, paymaster handles gas
  • MIX_PAYMASTER: Flexible hybrid model

Managing Paymaster Funds

You can add funds to your paymaster contract using the SmartWallet dashboard. The SDK will automatically use the paymaster for all transactions once configured.

Using Public Paymaster

To use a public/shared paymaster, simply set its address in the contracts object:

const contracts = {
    // ... other addresses
    paymaster: "0xPublicPaymasterAddress...",
    paymasterType: PaymasterType.SPONSOR_PAYMASTER,
}

No Paymaster? No Problem!

If you haven't set up a paymaster, the SDK will work normally - users will pay their own gas fees. The paymaster flow is completely optional and only activates when a paymaster address is provided.

Note: Without a paymaster, you'll sponsor all transaction costs yourself, and no refund will be transferred to you.

Available Signers

PKSigner (Private Key)

import { PKSigner } from '@omnes/smartwallet-ts-sdk';

const signer = await PKSigner.create(privateKey as `0x${string}`);
console.log('Address:', signer.getEVMAddress());

GCPSigner (Google Cloud KMS)

import { GCPSigner } from '@omnes/smartwallet-ts-sdk';

const signer = await GCPSigner.create(
    googleCredentials,    // JSON string of GCP credentials
    googleProjectId,      // GCP project ID
    kmsLocationId,        // KMS location (e.g., 'us-east1')
    kmsKeyRingId,         // KMS key ring ID
    clientId,             // Client ID (crypto key name)
    versionId             // Key version ID
);

console.log('Address:', signer.getEVMAddress());

MultisigSigner

import { MultisigSigner, PKSigner } from '@omnes/smartwallet-ts-sdk';

const signer1 = await PKSigner.create(privateKey1 as `0x${string}`);
const signer2 = await PKSigner.create(privateKey2 as `0x${string}`);
const signer3 = await PKSigner.create(privateKey3 as `0x${string}`);

const multisigSigner = MultisigSigner.create([signer1, signer2, signer3]);

// Get addresses
const addresses = multisigSigner.getEVMAddresses();
console.log('Signer addresses:', addresses);

PasskeySigner

import { PasskeySigner } from '@omnes/smartwallet-ts-sdk';

const signer = PasskeySigner.create(webSocketURL);

Miscellaneous

Read

The read function performs several storage reads on the blockchain. Example:

const result = smartWallet.read([
    {
        to: "0x843821106057e0a15e10adbC69238b051707FFd1";
        funcSignature: "balanceOf(address)";
        params: [
            "0xD84F7ac8aB9dB8a259244d629d9c539F2159e6EC"
        ];
        returnTypes: ["address"];
    }
]);

console.log(result);

Complete Example

import { SmartWallet, PKSigner, AccountOperations, Operation } from '@omnes/smartwallet-ts-sdk';

async function completeExample() {
  // 1. Create signer
  const signer = await PKSigner.create(privateKey as `0x${string}`);
  
  // 2. Create SmartWallet
  const smartWallet = await SmartWallet.create(
    signer.signMessage,
    signer.signMessage,
    signer.getEVMAddress(),
    rpcURL,
    apiKey
  );
  
  // 3. Build operations
  const operations: Operation = {
    to: tokenAddress,
    value: BigInt(0),
    funcSignature: 'transfer(address,uint256)',
    params: [recipient, amount]
  };
  
  const accountOperations: AccountOperations[] = [{
    account: {
      walletCustody: 1,
      salt: 'user@example.com',
      publicKeys: []
    },
    operations: [operations],
    settings: {}
  }];
  
  // 4. Execute
  const result = await smartWallet.buildAndRequestSendUserOperations(
    accountOperations,
    [],
    []
  );
  
  console.log('Transaction hash:', result.response.txHash);
  console.log('Status:', result.response.status);
  
  return result;
}

SDK Features

The SmartWallet SDK provides:

  • βœ… Complete UserOp Control: Build and sign UserOperations manually
  • βœ… Multiple Signers: PKSigner, GCPSigner, MultisigSigner, PasskeySigner
  • βœ… Self & Remote Bundler: Choose your bundler strategy
  • βœ… Custom Signatures: Implement your own signing logic
  • βœ… Gas Optimization: Fine-tune gas settings
  • βœ… Passkey Support: Full passkey integration
  • βœ… Multisig Support: Complete multisig functionality

Next Steps


πŸ’‘ Need Help? Join our developer community or check our GitHub repository for examples and issue tracking. For advanced use cases, use the SmartWallet SDK for complete control.