@prism-ing/wallet
v1.0.4
Published
Production-grade, framework-agnostic wallet SDK. OWS signing + ZeroDev + Squads recovery. No third-party API keys required.
Maintainers
Readme
@prism-ing/wallet
Self-custodial wallet SDK. Hardware-backed key security via iOS Secure Enclave, social recovery via ZeroDev (EVM) and Squads V4 (Solana). No third-party API keys required.
For cross-chain balances, deposits, and smart account addresses, add @prism-ing/onebalance.
Install
pnpm add @prism-ing/walletOptional peer dependencies (install only what you need):
pnpm add @zerodev/sdk # On-chain session key enforcement
pnpm add @sqds/multisig # Solana social recovery
pnpm add viem # EVM utilitiesWhy Prism Wallet?
| | Prism | Coinbase AgentKit | Raw viem/ethers | |---|---|---|---| | No seed phrases | Yes | No | No | | Session key policies | Yes (6 dimensions, on-chain) | No | No | | Solana support | Yes | No | No | | Result API (no throws) | Yes | No | No | | Self-custodial | Yes | Partial | Yes | | Zero required API keys | Yes | No | Yes |
Quickstart
Base wallet (signing + recovery, no API key)
import { createProductionWallet } from '@prism-ing/wallet';
const result = await createProductionWallet({
signer: { walletName: 'my-agent' },
});
if (!result.ok) {
console.error(result.error.code);
process.exit(1);
}
const wallet = result.value;
console.log('EVM address:', wallet.evmAddress);
console.log('Solana address:', wallet.solanaAddress);Same wallet name = same keys = same addresses, every time. Keys persist encrypted at ~/.ows/.
Used in an AI agent (ElizaOS, LangChain, AutoGPT)? Pass
wallet.signerdirectly to your framework's signing hook — it's structurally compatible with any interface that acceptssignMessage/signTypedData.
Enhanced wallet (with OneBalance account abstraction)
import { createProductionWallet } from '@prism-ing/wallet';
import { createOneBalanceProvider } from '@prism-ing/onebalance';
const result = await createProductionWallet(
{ signer: { walletName: 'my-agent' } },
{ accountAbstraction: createOneBalanceProvider({ apiKey: process.env.ONEBALANCE_API_KEY }) },
);
if (!result.ok) {
console.error(result.error.code);
process.exit(1);
}
const wallet = result.value;
console.log('Smart account:', wallet.smartAccountAddress);
const balance = await wallet.getBalance();
console.log(`Balance: $${balance.totalUsd}`);When you inject an AccountAbstractionProvider, the factory returns an EnhancedWalletAccount with getBalance(), deposit(), getTransactionStatus(), and a guaranteed smartAccountAddress.
Signing Backends
createProductionWallet auto-selects the correct backend for your platform. If you use the lower-level createWallet, choose a backend explicitly:
| Backend | Factory | Platform | Key Storage | Use Case |
|---------|---------|----------|-------------|----------|
| OWS | createOWSSigningBackend(config) | Node.js | Encrypted at ~/.ows/ (AES-256-GCM, scrypt KDF) | Production agents — persistent, encrypted keys |
| Node Software | createNodeSigningBackend() | Node.js | In-memory only | Dev/testing — random keypairs, never persisted |
| Secure Enclave | createSecureEnclaveBackend(config) | iOS | Hardware-encrypted in iOS Keychain | Mobile apps — biometric-gated signing |
import { createWallet, createOWSSigningBackend } from '@prism-ing/wallet';
const result = await createWallet(
{ signer: { walletName: 'my-agent' } },
{ createSigner: createOWSSigningBackend },
);For testing with ephemeral keys:
import { createWallet, createNodeSigningBackend } from '@prism-ing/wallet';
const result = await createWallet(
{ signer: { walletName: 'test' } },
{ createSigner: createNodeSigningBackend },
);Architecture
Two layers compose to give you a persistent, recoverable wallet. Account abstraction is an optional third:
Signing Backends (platform-specific key management)
├── iOS Secure Enclave: keys encrypted by hardware P-256 key, biometric-gated
├── OWS (Node.js): secp256k1 + ed25519 from BIP-39, encrypted at ~/.ows/
└── Node Software: in-memory signing via @noble/curves (dev/testing)
Smart Contract Wallet (the actual "wallet")
├── Signer key authorizes operations, but is NOT the wallet itself
├── Funds live in the smart contract, not controlled by the raw key
├── Signer can be rotated without moving funds (recovery)
└── On-chain policy enforcement via ZeroDev permission validators
Account Abstraction Provider (optional — e.g., @prism-ing/onebalance)
├── ERC-4337 counterfactual smart account from signer's public key
├── Resource Locks: instant cross-chain execution
└── Aggregated balance across all supported chainsKey insight: The signer key is not the wallet. It authorizes operations on a smart contract. If compromised, guardians can rotate it out without moving funds. This separation is fundamental to every security property.
Private Key Management
Security Model by Platform
| Property | Node.js (Agent) | iOS (Mobile) |
|----------|-----------------|--------------|
| Key storage | Encrypted at ~/.ows/ (AES-256-GCM, scrypt KDF) | Encrypted by Secure Enclave P-256 key, stored in iOS Keychain |
| Auth | Passphrase or API key | Biometric (Face ID / Touch ID) |
| Key isolation | In-process only, never serialized | Decrypted in-process only after biometric, never serialized |
| EVM signing | Software secp256k1 | Software secp256k1, biometric-gated |
| Solana signing | Software ed25519 | Software ed25519, biometric-gated |
| Seed phrases | None exposed. OWS encrypts mnemonic internally. | None. Keys are non-exportable. |
Agent Mode
// First run: creates wallet, encrypts keys at ~/.ows/
// Every subsequent run: loads same wallet, same addresses
const result = await createProductionWallet({
signer: { walletName: 'trading-agent' },
});With passphrase protection:
const result = await createProductionWallet({
signer: {
walletName: 'treasury',
passphrase: process.env.WALLET_PASSPHRASE,
},
});With OWS agent API key (policy-gated signing):
const result = await createProductionWallet({
signer: {
walletName: 'treasury',
apiKey: 'ows_key_a1b2c3d4...',
},
});iOS Mobile App
import { createProductionWallet } from '@prism-ing/wallet';
import { createOneBalanceProvider } from '@prism-ing/onebalance';
import { SecureEnclaveModule } from './native/SecureEnclaveModule';
const result = await createProductionWallet(
{ signer: { walletName: 'user' } },
{
nativeBridge: SecureEnclaveModule,
biometricPrompt: 'Authorize this trade',
recoveryBackends: { evm: evmBackend, solana: solanaBackend },
accountAbstraction: createOneBalanceProvider({ apiKey: config.oneBalanceApiKey }),
},
);On iOS, createProductionWallet detects the platform and auto-selects the Secure Enclave backend when nativeBridge is provided.
Custom Signer (bring your own keys)
Routers and the wallet factory accept any PrismSigner. No dependency on @prism-ing/wallet for signing:
import type { PrismSigner } from '@prism-ing/wallet';
const mySigner: PrismSigner = {
evmAddress: myAddress,
solanaAddress: mySolanaAddress,
signMessage: (msg) => mySigningLib.sign(msg),
signTypedData: (payload) => mySigningLib.signTypedData(payload),
signTransaction: (tx) => mySigningLib.signSolana(tx),
};Why Result Types?
Traditional wallet libraries throw on failure. With Prism, every async operation returns a Result<T, E> — check .ok before using the value. TypeScript enforces this at compile time.
// Without Result types — silent footgun
try {
const wallet = await someLib.createWallet(config);
// wallet might be undefined, might have thrown
} catch (e) {
// e is `unknown` — you have no idea what failed
}
// With Prism — exhaustive, typed, no surprises
const result = await createProductionWallet(config);
if (!result.ok) {
// result.error is WalletError — a discriminated union with specific codes
if (result.error.code === 'PROVIDER_TIMEOUT') {
// safe to retry
}
return;
}
const wallet = result.value; // TypeScript knows this is WalletAccountSession Keys
Session keys are ephemeral, policy-scoped signers for agents and automated processes. They wrap the root PrismSigner and enforce constraints on every signing operation.
Agent safety: A session with
allowedAssets: ['ob:usdc'],maxAmountPerOp: '1000000000', andexpiresAt1 hour from now cannot sign anything outside that policy — even if the agent framework passes a different payload. ZeroDev reverts on-chain if violated. Rogue agents cannot drain the wallet.
Basic Usage
import { createSessionKeyManager, validateSessionOperation } from '@prism-ing/wallet';
const manager = createSessionKeyManager(wallet.signer);
const session = manager.createSessionKey({
expiresAt: Math.floor(Date.now() / 1000) + 3600,
allowedAssets: ['ob:usdc'],
maxAmountPerOp: '1000000000',
maxTotalAmount: '5000000000',
allowedChains: [8453, 42161],
allowedRecipients: ['0xabc...'],
});
// Use the session signer — policy enforced on every sign call
// session.signer is a PrismSigner scoped to the policy
// Revoke when the task is complete
manager.revokeSessionKey(session.sessionId);Spend Persistence
By default, cumulative spend tracking for maxTotalAmount lives in memory and resets on process restart. For long-running agents, persist it to disk:
import { createSessionKeyManager, createFileSpendPersistence } from '@prism-ing/wallet';
const persistence = createFileSpendPersistence(); // stores at ~/.prism/sessions/
const manager = createSessionKeyManager(wallet.signer, undefined, persistence);If maxTotalAmount is set on a policy, you must provide either a SessionKeyBackend (on-chain enforcement) or a SpendPersistence adapter. Without one, the manager throws at creation time.
On-Chain Enforcement (ZeroDev)
import { createZeroDevSessionBackend, createSessionKeyManager } from '@prism-ing/wallet';
const sessionBackend = createZeroDevSessionBackend({
registerOnChain: async (sessionAddress, policies) => txHash,
revokeOnChain: async (sessionAddress) => txHash,
defaultGasBudgetWei: 100_000_000_000_000_000n,
callPolicyVersion: '0.0.4',
});
const manager = createSessionKeyManager(wallet.signer, sessionBackend);Social Recovery
Recovery rotates the smart contract's authorized signer without moving funds. No API key required. When using the optional ZeroDev or Squads V4 recovery backends, your use of those services is subject to their respective terms of service (ZeroDev, Squads).
Setting Up Recovery (EVM — ZeroDev)
const recovery = wallet.recover();
// Register a passkey guardian (iCloud Keychain)
const passkey = await recovery.setupPasskey();
// Add a second device as guardian
await recovery.addDeviceGuardian(deviceEvmAddress, deviceSolanaAddress);
// Add a trusted contact
await recovery.addContactGuardian(contactEvmAddress);Setting Up Recovery (Solana — Squads V4)
Squads V4 multisig recovery uses a threshold of members to authorize signer rotation on Solana.
import { createSquadsRecoveryBackend, FULL_PERMISSIONS, VOTER_PERMISSIONS } from '@prism-ing/wallet';
const solanaBackend = createSquadsRecoveryBackend({
bridge: {
async executeConfigTransaction(actions) {
// Your Squads V4 SDK integration:
// create config tx → propose → approve → execute
return txSignature;
},
async getThreshold() { return 2; },
async getMemberCount() { return 3; },
},
autoIncrementThreshold: true, // bump threshold when adding members (default)
});
const result = await createProductionWallet(
{ signer: { walletName: 'user' } },
{ recoveryBackends: { solana: solanaBackend } },
);Permission presets: FULL_PERMISSIONS (proposer + voter + executor) for primary guardians, VOTER_PERMISSIONS (voter only) for contacts who should only approve recovery but not initiate it.
Performing Recovery
// On a new device, with new keys:
const newWallet = await createProductionWallet({
signer: { walletName: 'recovered-wallet' },
});
const recovery = oldWalletRecoveryManager;
const recoveryTx = await recovery.initiateRecovery(newWallet.value.signer);
// Smart contract's authorized signer now points to newWallet.signerPending Recovery (iOS)
On iOS with Secure Enclave, createProductionWallet may return a WalletPendingRecovery instead of a full wallet. This happens when recovery guardians must be configured before the wallet is usable.
import { createProductionWallet, isPendingRecovery } from '@prism-ing/wallet';
const result = await createProductionWallet(
{ signer: { walletName: 'user' } },
{ nativeBridge: SecureEnclaveModule, accountAbstraction: provider },
);
if (!result.ok) { /* handle error */ }
const wallet = result.value;
if (isPendingRecovery(wallet)) {
// Must set up at least one guardian before wallet activates
const recovery = wallet.recover();
await recovery.setupPasskey();
await recovery.addDeviceGuardian(deviceEvmAddress, deviceSolanaAddress);
// Now activate the full wallet
const activated = wallet.activateWallet();
if (!activated.ok) { /* handle error */ }
// activated.value is a WalletAccount (Base or Enhanced)
} else {
// Already a full wallet — use normally
}API Reference
createProductionWallet(config, options?)
Creates a wallet with platform-appropriate signing. Returns Result<BaseWalletAccount, WalletError> without a provider, or Result<EnhancedWalletAccount, WalletError> with one.
interface WalletConfig {
signer: SignerConfig;
recovery?: RecoveryConfig;
}
interface ProductionWalletOptions {
nativeBridge?: SecureEnclaveNativeBridge;
keyTag?: string;
biometricPrompt?: string;
recoveryBackends?: RecoveryBackends;
}
// Add accountAbstraction to get an EnhancedWalletAccount:
interface ProductionWalletOptionsWithAA extends ProductionWalletOptions {
accountAbstraction: AccountAbstractionProvider;
}BaseWalletAccount
Returned when no AccountAbstractionProvider is injected.
interface BaseWalletAccount {
readonly signer: PrismSigner;
readonly evmAddress: Address; // Raw EOA
readonly solanaAddress: string;
recover(): RecoveryManager;
}EnhancedWalletAccount
Returned when an AccountAbstractionProvider is injected. Extends BaseWalletAccount.
interface EnhancedWalletAccount extends BaseWalletAccount {
readonly smartAccountAddress: Address; // Counterfactual ERC-4337 address
getBalance(): Promise<UnifiedBalance>;
deposit(params: DepositParams): Promise<TxResult>;
getTransactionStatus(quoteId: string): Promise<TxResult>;
}Type Guards
import { isEnhancedWallet, isPendingRecovery } from '@prism-ing/wallet';
if (isPendingRecovery(wallet)) {
// wallet is WalletPendingRecovery — must set up guardians then call activateWallet()
} else if (isEnhancedWallet(wallet)) {
// wallet is EnhancedWalletAccount — has getBalance(), deposit(), smartAccountAddress
} else {
// wallet is BaseWalletAccount — signing + recovery only
}AccountAbstractionProvider
The interface for opt-in account abstraction. OneBalance is the reference implementation.
interface AccountAbstractionProvider {
predictAddress(signerAddress: Address, accountType: string): Promise<Address>;
getBalance(accounts: readonly SmartAccount[]): Promise<UnifiedBalance>;
deposit(params: DepositParams, signer: PrismSigner, accounts: readonly SmartAccount[]): Promise<TxResult>;
getTransactionStatus(quoteId: string): Promise<TxResult>;
}PrismSigner
The core signing interface. Routers depend on this, not on @prism-ing/wallet.
interface PrismSigner {
signMessage(message: Hex): Promise<Hex>;
signTypedData(payload: TypedDataPayload): Promise<Hex>;
signTransaction<T>(tx: T): Promise<Result<T, WalletError>>;
readonly evmAddress: Address; // Raw EOA
readonly solanaAddress: string;
}Error Handling
All operations return Result<T, WalletError> instead of throwing:
const result = await createProductionWallet(config);
if (!result.ok) {
if (isRetryableError(result.error)) {
// PROVIDER_TIMEOUT, PROVIDER_API_ERROR — safe to retry
}
if (isUserFacingError(result.error)) {
// SIGNING_REJECTED, INSUFFICIENT_BALANCE — show to user
}
return;
}| Code | Retryable | User-Facing | Description |
|------|-----------|-------------|-------------|
| ENCLAVE_UNAVAILABLE | No | No | Secure Enclave not available on this platform |
| PROVIDER_TIMEOUT | Yes | No | Account abstraction provider did not respond in time |
| PROVIDER_API_ERROR | Yes | No | Account abstraction provider returned an error |
| RECOVERY_GUARDIAN_INVALID | No | Yes | Invalid guardian address |
| SIGNING_REJECTED | No | Yes | User rejected signing (biometric fail / cancel) |
| ACCOUNT_NOT_INITIALIZED | No | No | Operation on uninitialized account |
| INSUFFICIENT_BALANCE | No | Yes | Not enough balance for operation |
| KEY_NOT_FOUND | No | No | Requested key pair not found in signing backend |
| VALIDATION_ERROR | No | Yes | Input failed Zod validation |
| SESSION_KEY_EXPIRED | No | No | Session key has expired or been revoked |
| SESSION_KEY_POLICY_VIOLATION | No | Yes | Operation violates session key policy |
| QUOTE_VERIFICATION_FAILED | No | No | Quote failed integrity verification |
Defense-in-Depth Layers
Layer 1: Key Storage
iOS: Secure Enclave encryption + biometric gate
Node: OWS scrypt + AES-256-GCM encrypted vault
Layer 2: Session Key Policies
Client-side: JS enforcement before every signature
On-chain: ZeroDev permission validators (UserOp reverts if violated)
Layer 3: Quote Verification (via @prism-ing/onebalance)
6-point client-side integrity check before signing any quote
Layer 4: Recovery Challenges
Dynamic keccak256 challenges bound to signer pair + 5-minute time window
Layer 5: Smart Contract Validation
ERC-4337 smart account validates all operations on-chain
ZeroDev Kernel v3.1 enforces session key policies at contract levelLicense
MIT
