/ @prism-ing/wallet

@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/wallet

Optional 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 utilities

Why 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.signer directly to your framework's signing hook — it's structurally compatible with any interface that accepts signMessage / 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 chains

Key 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 WalletAccount

Session 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', and expiresAt 1 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.signer

Pending 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 level

License

MIT