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-sdkKey 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:
- Set up different message sign methods, such as: private key, HSM (GCP), multisig, and passkey.
- Let the SDK sign the userOp hash or implement custom code/signer to sign it;
- 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:0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000clientDataJSON: from bytes array to stringified JSON (withoutother_keys_can_be_added_herefield). 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
- JavaScript Examples - JavaScript HTTP examples (using fetch)
- Python Examples - Python HTTP examples (using requests)
- cURL Examples - Command-line examples
- Advanced Features - Learn about multisig and passkey wallets
- Troubleshooting - Common issues and solutions
π‘ 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.