@aspect-wallet/sdk
v0.1.2
Published
TypeScript SDK for ERC-4337 Account Abstraction smart wallets. Gasless transactions, social logins, passkeys, session keys, multi-factor auth, and account recovery.
Maintainers
Readme
@aspect-wallet/sdk
TypeScript SDK for building on the Aspect Wallet ERC-4337 Account Abstraction system. Create smart wallets, send gasless transactions, authenticate with social logins, and manage session keys -- all from your frontend.
Features
| Feature | Description | |---------|-------------| | Smart Wallets | Deterministic CREATE2 addresses, lazy deployment on first transaction | | Gasless Transactions | VerifyingPaymaster sponsors gas -- users pay nothing | | Social Login | Google, Apple, Facebook, Email OTP -- bridged to on-chain signers via Turnkey | | Passkeys | WebAuthn/P256 biometric signing with RIP-7212 precompile support | | Session Keys | Scoped ephemeral keys with allowlist, spend limits, time bounds. Signs entirely in-browser (no auth round-trip) | | Multi-Factor Auth | On-chain enforced: device+guardian, K-of-N multi-sig, timelock | | Account Recovery | Social recovery via guardians with timelock, two-step key rotation | | Emergency Freeze | Instantly revoke all session keys and freeze account on compromise | | Audit Trail | Full operation history, real-time events, webhook notifications | | Multi-Chain | Arbitrum, Base, Polygon, Ethereum -- same address across chains |
Installation
npm install @aspect-wallet/sdk viemQuick Start
import { FcxWalletClient } from '@aspect-wallet/sdk';
const client = new FcxWalletClient({
apiKey: 'fcx_live_abc123...',
chain: 'arbitrum-sepolia',
bundlerUrl: 'https://bundler.yourapp.com/rpc', // optional
paymasterUrl: 'https://paymaster.yourapp.com', // optional
});
// Resolve the deterministic wallet address for an owner (no on-chain call needed)
const wallet = await client.wallet.getAddress({ owner: signerAddress });
// wallet.address -- the smart account address
// wallet.deployed -- false until the first UserOp
// Check the on-chain state (balance, modules, frozen flag, …)
const state = await client.wallet.getState(wallet.address);FcxWalletClient exposes the following sub-modules:
| Property | Class | Purpose |
|----------|-------|---------|
| client.wallet | WalletFactory | Address + state queries |
| client.sponsor | PaymasterClient | Gas sponsorship requests |
| client.bundler | BundlerClient | JSON-RPC submission / receipts |
| client.audit | AuditClient | History, events, webhooks |
| client.chains | ChainRegistry | Chain configs (per project) |
Execution is performed by composing UserOpBuilder + a signer + client.sponsor + client.bundler. The end-to-end flow is shown below.
Wallet Lifecycle
Counterfactual Address
Users get a wallet address instantly -- no on-chain transaction needed. Assets can be sent to this address before the contract exists.
import { computeWalletAddress } from '@aspect-wallet/sdk';
// Pure computation -- no RPC, no API call
const address = computeWalletAddress(
factoryAddress, // from ChainRegistry / your project config
implementationAddress, // UUPS implementation behind the ERC-1167 clone
ownerAddress,
0n, // salt
);// Or query through the platform (also returns deployment status)
const wallet = await client.wallet.getAddress({
owner: signerAddress,
salt: 0n,
});
// wallet.address, wallet.deployed, wallet.factory, wallet.salt, wallet.chainLazy Deployment
The wallet deploys automatically on the first UserOp, via initCode. UserOpBuilder.setInitCode({ factory, owner, salt }) encodes the factory call for you.
Sending a UserOperation
FcxWalletClient does not expose a monolithic execute() helper -- you assemble a UserOp from the three building blocks (UserOpBuilder, a signer, PaymasterClient, BundlerClient). This keeps every step visible and overridable.
import {
UserOpBuilder,
EoaSigner,
getUserOpHash,
} from '@aspect-wallet/sdk';
const signer = new EoaSigner(privateKey); // 0x…hex
const chainId = BigInt(client.chainId);
// 1. Build
const userOp = await new UserOpBuilder(client.bundler, client.entryPoint, chainId)
.setSender(walletAddress)
.setCallData({ target, value, data })
// First deploy only — binds the initCode
// .setInitCode({ factory, owner, salt: 0n })
.setNonce({ key: 0n })
.build();
// 2. Sponsorship (optional — skip for self-paid gas)
const approval = await client.sponsor.approve(userOp);
userOp.paymasterAndData = approval.paymasterAndData;
// 3. Hash + sign
const hash = getUserOpHash(userOp, client.entryPoint, chainId);
userOp.signature = await signer.sign(hash);
// EoaSigner returns moduleId(4) + ecdsaSig(65) = 69 bytes
// 4. Submit
const userOpHash = await client.bundler.sendUserOperation(userOp);
// 5. Wait for on-chain receipt
const receipt = await client.bundler.waitForReceipt(userOpHash, {
timeout: 60_000,
pollingInterval: 2_000,
});Hash Computation
getUserOpHash(userOp, entryPoint, chainId) matches EntryPoint.getUserOpHash() exactly -- variable-length fields (initCode, callData, paymasterAndData) are individually hashed, then keccak256(abi.encode(innerHash, entryPoint, chainId)) anti-replays across chains.
Batch Calls
const userOp = await new UserOpBuilder(client.bundler, client.entryPoint, chainId)
.setSender(walletAddress)
.setCallDataBatch([
{ target: tokenA, value: 0n, data: approveData },
{ target: dex, value: 0n, data: swapData },
])
.build();
// All calls are atomic: all succeed, or the whole batch reverts.Gas Sponsorship (Paymaster)
Users transact with zero ETH. The VerifyingPaymaster contract pays gas, authorised by your backend signing service. paymasterAndData is 97 bytes: address(20) + validUntil(6) + validAfter(6) + signature(65).
const approval = await client.sponsor.approve(userOp);
// approval.paymasterAndData, approval.validUntil, approval.validAfter, approval.estimatedCost
// Budget + policy queries
const budget = await client.sponsor.getBudget();
const policy = await client.sponsor.getPolicy();PaymasterClient can also be constructed standalone if you need sponsorship without the full FcxWalletClient:
import { PaymasterClient, ApiClient } from '@aspect-wallet/sdk';
const api = new ApiClient('https://paymaster.yourapp.com', apiKey);
const paymaster = new PaymasterClient(api, {
chainId: 421614,
entryPointAddress: '0x5FF1…',
});Authentication & Social Login
All auth clients take an ApiClient for the auth server. Wire them up once, share a SessionManager so a login via any method updates the same session.
import {
ApiClient,
AuthClient,
OAuthClient,
PasskeyAuthClient,
SessionManager,
} from '@aspect-wallet/sdk';
const api = new ApiClient('https://auth.yourapp.com', apiKey);
const session = new SessionManager(api);
const email = new AuthClient(api); // AuthClient owns its own SessionManager
const oauth = new OAuthClient(api, session);
const passkey = new PasskeyAuthClient(api, session);Email OTP
await email.sendEmailOtp('[email protected]');
// Rate limited: 3 attempts per OTP, 5 OTPs per hour per email
const info = await email.verifyEmailOtp({
email: '[email protected]',
code: '847291',
});
// info.jwt, info.refreshToken, info.user { id, email, signerAddress, … }, info.expiresAtOAuth (Google / Apple / Facebook)
const info = await oauth.loginWithGoogle();
const info = await oauth.loginWithApple();
const info = await oauth.loginWithFacebook();
// First login: creates a Turnkey sub-org + signing key
// Returning login: loads the existing keyPasskey Authentication
Passkeys use WebAuthn. The P256 public key is stored on-chain inside WebAuthnValidationModule — never in the backend database. Credentials live in the device secure enclave.
// Registration (call this after email/OAuth login)
const reg = await passkey.registerPasskey({ displayName: 'My MacBook' });
// reg.credentialId -- base64url; persist for future loginWithPasskey calls
// reg.pubKeyX / reg.pubKeyY -- hex uint256 for on-chain installModule
// Login from any device that has the credential
const info = await passkey.loginWithPasskey(walletAddress);Installing the WebAuthn module on-chain (moduleId = 4) binds the public key to the account so on-chain verification works:
import { encodeAbiParameters } from 'viem';
const installData = encodeAbiParameters(
[{ type: 'uint256' }, { type: 'uint256' }],
[BigInt(reg.pubKeyX), BigInt(reg.pubKeyY)],
);
// Submit a UserOp that calls ModuleRouter.installModule(4, installData)
// using UserOpBuilder + a signer as shown above.Signing
Signer Interface
All signers implement ISigner:
interface ISigner {
type: 'eoa' | 'passkey' | 'turnkey' | 'session-key' | 'multi-sig';
address: Address;
moduleId: number;
sign(userOpHash: Hex): Promise<Hex>;
}Every signature is prefixed with a 4-byte moduleId so ModuleRouter on-chain routes it to the correct validator.
EOA Signer
import { EoaSigner } from '@aspect-wallet/sdk';
const signer = new EoaSigner(privateKey); // Hex
const sig = await signer.sign(userOpHash);
// moduleId(4) + ecdsaSig(65) = 69 bytes, moduleId = 0 (SingleSigner)Session Key Signer
import { SessionKeySigner } from '@aspect-wallet/sdk';
const signer = new SessionKeySigner(sessionPrivateKey);
const sig = await signer.sign(userOpHash);
// Same 65-byte ECDSA wire format, moduleId = 1 (SessionKey)Passkey Signer
PasskeySigner calls navigator.credentials.get() to trigger the biometric prompt, parses the DER-encoded P256 signature from the authenticator, normalises s to low-s, and ABI-encodes the result.
import { PasskeySigner } from '@aspect-wallet/sdk';
const signer = new PasskeySigner(walletAddress, credentialId);
const sig = await signer.sign(userOpHash);
// moduleId(4) + abi.encode(authenticatorData, clientDataJSON, r, s)
// moduleId = 4 (WebAuthn); ~300–500 bytes total
// Verified on-chain via RIP-7212 precompile or daimo-eth/p256-verifier fallback.For tests, pass the optional third signFn to skip the browser ceremony:
new PasskeySigner(walletAddress, credentialId, async (challenge) => ({
authenticatorData: '0x…',
clientDataJSON: '{"type":"webauthn.get",...}',
r: 0x…n,
s: 0x…n,
}));Multi-Sig Collector
import { MultiSigCollector } from '@aspect-wallet/sdk';
const collector = new MultiSigCollector(threshold, signerAddresses);
collector.addSignature(sigFromOwner1);
collector.addSignature(sigFromOwner2);
if (collector.isComplete) {
userOp.signature = await collector.sign(userOpHash); // concatenated packed sigs
}Modular Signature Prefix (advanced)
import { encodeModuleSignature } from '@aspect-wallet/sdk';
const modularSig = encodeModuleSignature(0, ecdsaSignature);
// Module IDs: 0=SingleSigner, 1=SessionKey, 2=DeviceGuardian, 3=MultiSig, 4=WebAuthnSession Keys
Session keys are ephemeral scoped keys. Once installed, they let a dApp execute transactions without user interaction per action — the key lives in the browser and signs locally. Permission hooks enforce limits on-chain.
Low-Level: SessionKeyManager (API-driven)
import { SessionKeyManager, SessionKeyTemplates } from '@aspect-wallet/sdk';
const keys = new SessionKeyManager(client.api);
// Custom permissions
const info = await keys.create({
permissions: {
allowlist: [
{ target: gameContract, selector: '0x12345678' },
{ target: gameContract, selector: '0xabcdef01' },
],
spendLimit: { perTx: 0n, cumulative: 0n },
timeRange: {
validAfter: Math.floor(Date.now() / 1000),
validUntil: Math.floor(Date.now() / 1000) + 4 * 3600,
},
requiredPaymaster: paymasterAddress,
},
});
// Or use a pre-built template
const perms = SessionKeyTemplates.gaming({
gameContract: '0x…',
duration: 4 * 3600,
paymaster: '0x…',
});
// Execute via the managed API (server co-signs)
await keys.execute(info.moduleId, { target, value: 0n, data });
// Revoke
await keys.revoke(info.moduleId);Available templates on SessionKeyTemplates: gaming, defi, subscription, nftMint.
High-Level: SessionKeyLifecycle (browser-native)
For apps that want the key to live entirely in the browser (no auth round-trip on execute), use SessionKeyLifecycle. It generates the keypair locally, persists it in pluggable storage, listens for server-sent expiry events over an authenticated SSE stream, and auto-renews before the key expires.
import {
SessionKeyLifecycle,
WebSessionKeyStorage,
} from '@aspect-wallet/sdk';
const lifecycle = new SessionKeyLifecycle({
walletAddress: '0x…',
platformUrl: 'https://platform.yourapp.com',
paymasterUrl: 'https://paymaster.yourapp.com',
bundlerUrl: 'https://bundler.yourapp.com/rpc',
rpcUrl: 'https://arb-sepolia-rpc.example.com',
chainId: 421614,
jwt: ownerSessionJwt,
template: 'gaming',
storage: new WebSessionKeyStorage(),
onExpiring: ({ remainingSeconds }) => toast(`Key expires in ${remainingSeconds}s`),
onRenewed: ({ newExpiresAt }) => toast(`Renewed until ${new Date(newExpiresAt * 1000)}`),
onError: (err) => console.error(err),
});
await lifecycle.install(); // generate + register with the platform
lifecycle.start(); // begin the SSE listener (auto-renew)
const result = await lifecycle.execute({
target: noteStoreAddress,
value: 0n,
data: addNoteCallData,
});
// result.userOpHash, result.txHash?
await lifecycle.revoke(); // on logout / unmount
lifecycle.stop(); // close the SSE streamStandalone: executeViaSessionKey
Want the bare pipeline without the lifecycle manager? Call it directly:
import { executeViaSessionKey } from '@aspect-wallet/sdk';
const { userOpHash } = await executeViaSessionKey({
sessionPrivateKey,
moduleId: 1,
walletAddress: '0x…',
target,
value: 0n,
data,
paymasterUrl: 'https://paymaster.yourapp.com',
bundlerUrl: 'https://bundler.yourapp.com/rpc',
rpcUrl: 'https://arb-sepolia-rpc.example.com',
chainId: 421614,
});Storage Adapters
| Platform | Implementation | Backend | Lifecycle |
|----------|---------------|---------|-----------|
| Web | WebSessionKeyStorage | window.sessionStorage | Cleared on tab close |
| Node / test | MemorySessionKeyStorage | Map<string, string> | Process lifetime |
| Native (BYO) | Implement SessionKeyStorage | iOS Keychain / Android KeyStore | Persists across launches |
Multi-Factor Authentication
On-chain enforced MFA -- the blockchain rejects transactions that don't meet the multi-factor requirements.
4-Tier Step-Up Selection
import { TierSelector } from '@aspect-wallet/sdk';
const tier = TierSelector.select({
value: parseEther('5'),
target: tokenContract,
selector: '0xa9059cbb', // transfer(address,uint256)
});
// tier = 3 → requires device + guardian| Tier | When | Signers | Latency | |------|------|---------|---------| | 1 | Low-value, scoped actions | Session key (auto-sign) | Instant | | 2 | Normal transactions | Owner key | 1 biometric prompt | | 3 | High-value transfers | Owner + Guardian | 2 approvals | | 4 | Account management | K-of-N multi-sig + timelock | Hours/days |
Guardian Management
import { GuardianManager } from '@aspect-wallet/sdk';
const guardians = new GuardianManager(client.api);
await guardians.addGuardian({ guardian: '0x…', label: 'Mom' });
await guardians.addGuardian({ guardian: '0x…', label: 'Backend Co-Signer' });
await guardians.setThreshold(2); // 2-of-3 required
const list = await guardians.listGuardians(walletAddress);
const threshold = await guardians.getThreshold(walletAddress);
await guardians.removeGuardian('0x…');Multi-Sig Proposals
import { MultiSigManager } from '@aspect-wallet/sdk';
const multisig = new MultiSigManager(client.api);
// Owner 1 proposes
const proposal = await multisig.proposeMultiSig({ target, value: 0n, data });
// Owner 2 approves -- if threshold met, the UserOp is submitted automatically
await multisig.approveMultiSig(proposal.id);
const current = await multisig.getProposal(proposal.id);
const pending = await multisig.listPending(walletAddress);Account Recovery
Social Recovery (Guardian-Based)
import { SocialRecovery } from '@aspect-wallet/sdk';
const recovery = new SocialRecovery(client.api);
await recovery.initiateRecovery({ account: userWallet, newOwner: newKeyAddress });
await recovery.approveRecovery({ account: userWallet });
// After timelock → execute
await recovery.executeRecovery({ account: userWallet });
// Owner cancellation (during timelock)
await recovery.cancelRecovery();
const status = await recovery.getStatus(walletAddress);
// { pending, approvals, threshold, executeAfter, canExecute, canCancel, newOwner? }Two-Step Key Rotation
import { KeyRotation } from '@aspect-wallet/sdk';
const rotation = new KeyRotation(client.api);
await rotation.initiateKeyRotation({ newOwner: newSignerAddress }); // current owner signs
await rotation.acceptKeyRotation(); // new owner signs
// Old key loses access immediately; wallet address is unchanged.
const pending = await rotation.getPendingOwner(walletAddress);Multi-Device
import { DeviceManager } from '@aspect-wallet/sdk';
const devices = new DeviceManager(client.api);
await devices.addDevice({ name: 'iPhone 15', type: 'passkey', moduleId: 2 });
await devices.removeDevice(moduleId); // revoke a lost device
const list = await devices.listDevices(walletAddress);Security & Emergency
Emergency Freeze
import { FreezeManager } from '@aspect-wallet/sdk';
const freeze = new FreezeManager(client.api);
await freeze.freeze({ reason: 'suspicious_activity' }); // revokes all session keys
await freeze.unfreeze(); // requires MFA Tier 3/4
const isFrozen = await freeze.isFrozen(walletAddress);Bulk Session Key Revocation
import { RevokeManager } from '@aspect-wallet/sdk';
const revoke = new RevokeManager(client.api);
await revoke.revokeAllSessionKeys();
const { revokedCount } = await revoke.revokeSessionKeys({ expiredOnly: true });Watchtower Alerts
import { WatchtowerClient } from '@aspect-wallet/sdk';
const watchtower = new WatchtowerClient(client.api);
await watchtower.subscribe({
account: walletAddress,
alerts: ['recovery_initiated', 'owner_changed', 'large_transfer'],
channels: {
email: '[email protected]',
webhook: 'https://myapp.com/webhooks/security',
},
});
const alerts = await watchtower.getAlerts(walletAddress);Audit & Events
const history = await client.audit.getHistory(walletAddress, {
limit: 50,
types: ['execute', 'deploy', 'module_install'],
});
// history.total, history.operations[] { userOpHash, transactionHash, target, value,
// signer, signerType, sponsored, paymaster?, gasCost, success, blockNumber, timestamp }
// Real-time events
const unsubscribe = client.audit.subscribe(walletAddress, {
events: ['userop.mined', 'security.alert'],
onEvent: (e) => console.log(e.type, e.userOpHash),
onError: console.error,
});
// Webhook management
await client.audit.webhooks.create({
url: 'https://myapp.com/webhooks',
secret: 'whsec_…',
events: ['userop.mined', 'session_key.installed'],
});Chain Configuration
Contract addresses come from the platform (ChainRegistry) at runtime -- keyed to your project via the API key.
const chains = await client.chains.list(); // all supported chains
const config = await client.chains.getConfig('arbitrum-sepolia');
// config.chainId, config.entryPoint, config.contracts { factory, paymaster, …modules }
// Switch chains on the same client
client.switchChain('base');
// Or spin up a second client for cross-chain work
const baseClient = client.forChain('base');Bundler Client
import { BundlerClient } from '@aspect-wallet/sdk';
const bundler = new BundlerClient('https://bundler.yourapp.com/rpc', entryPointAddress);
const hash = await bundler.sendUserOperation(signedUserOp);
const est = await bundler.estimateUserOperationGas(userOp);
// est.preVerificationGas, est.verificationGasLimit, est.callGasLimit (hex)
const receipt = await bundler.waitForReceipt(hash, { timeout: 60_000, pollingInterval: 2_000 });
const points = await bundler.getSupportedEntryPoints();Error Handling
All SDK errors are instances of FcxError and carry a stable code:
import { FcxError } from '@aspect-wallet/sdk';
try {
await client.sponsor.approve(userOp);
} catch (e) {
if (e instanceof FcxError) {
switch (e.code) {
case 'USEROP_SIGNATURE_INVALID': // -32507
case 'USEROP_SIMULATION_FAILED': // -32500
case 'SPONSOR_BUDGET_EXHAUSTED':
case 'SPONSOR_POLICY_REJECTED':
case 'SESSION_KEY_EXPIRED':
case 'SESSION_KEY_NOT_FOUND':
case 'SESSION_KEY_NOT_ALLOWED':
case 'SESSION_STORAGE_UNAVAILABLE':
case 'MFA_THRESHOLD_NOT_MET':
case 'AUTH_SESSION_EXPIRED':
case 'API_KEY_INVALID':
case 'WALLET_FROZEN':
// handle the specific failure mode
break;
}
console.error(e.code, e.message, e.details, e.retryable);
}
}FcxError also exposes httpStatus, rpcCode, userOpHash?, and static helpers FcxError.fromRpcError() and FcxError.fromHttpError().
TypeScript Types
All domain types are exported from the package root and from the /types sub-export:
import type {
UserOperation,
ExecutionResult,
SessionInfo,
WalletInfo,
WalletState,
SessionKeyPermissions,
SessionKeyInfo,
PaymasterApproval,
SponsorshipBudget,
RecoveryStatus,
AuditOperation,
ChainId,
ChainInfo,
ChainConfig,
} from '@aspect-wallet/sdk';
// Or the narrow types-only import
import type { UserOperation } from '@aspect-wallet/sdk/types';Architecture
Your Frontend App
│
▼
@aspect-wallet/sdk (this package)
│
├── BundlerClient ───── eth_sendUserOperation ──► EntryPoint (on-chain)
│ │
├── PaymasterClient ─── POST /api/paymaster/sign ├── Smart Account (proxy)
│ ├── Factory (CREATE2 / ERC-1167)
├── AuthClient ──────── POST /auth/email/verify ├── VerifyingPaymaster
│ OAuthClient POST /auth/oauth/{provider} └── Validation Modules
│ PasskeyAuthClient POST /auth/passkey/* ├── SingleSigner
│ ├── WebAuthn (P256)
├── SessionKeyLifecycle POST /session-keys/install ├── MultiSig (K-of-N)
│ GET /session-keys/events (SSE) ├── SessionKey + hooks
│ │ (Allowlist,
└── ChainRegistry / AuditClient / WatchtowerClient │ SpendLimit,
│ TimeRange,
│ PaymasterGuard)
└── SocialRecoverySupported Chains
- Arbitrum One, Arbitrum Sepolia
- Base, Base Sepolia
- Polygon, Polygon Amoy
- Ethereum, Sepolia
Same CREATE2 address across every chain where the factory is deployed at the same address.
Requirements
- Node.js >= 18
viem2.x- EVM chain with ERC-4337 EntryPoint v0.6 deployed at
0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789
License
MIT
