npm package: @prism-ing/wallet
Description: Production-grade, framework-agnostic wallet SDK. OWS signing + ZeroDev + Squads recovery. No third-party API keys required.
Latest version: 0.1.1

# @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`](../onebalance).

## Install

```bash
pnpm add @prism-ing/wallet
```

Optional peer dependencies (install only what you need):

```bash
pnpm add @zerodev/sdk    # On-chain session key enforcement
pnpm add @sqds/multisig  # Solana social recovery
pnpm add viem            # EVM utilities
```

## Quickstart

### Base wallet (signing + recovery, no API key)

```ts
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/`.

### Enhanced wallet (with OneBalance account abstraction)

```ts
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 |

```ts
import { createWallet, createOWSSigningBackend } from '@prism-ing/wallet';

const result = await createWallet(
  { signer: { walletName: 'my-agent' } },
  { createSigner: createOWSSigningBackend },
);
```

For testing with ephemeral keys:

```ts
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

```ts
// 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:
```ts
const result = await createProductionWallet({
  signer: {
    walletName: 'treasury',
    passphrase: process.env.WALLET_PASSPHRASE,
  },
});
```

With OWS agent API key (policy-gated signing):
```ts
const result = await createProductionWallet({
  signer: {
    walletName: 'treasury',
    apiKey: 'ows_key_a1b2c3d4...',
  },
});
```

### iOS Mobile App

```ts
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:

```ts
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),
};
```

## 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.

### Basic Usage

```ts
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:

```ts
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)

```ts
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.

### Setting Up Recovery (EVM — ZeroDev)

```ts
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.

```ts
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

```ts
// 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.

```ts
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.

```ts
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.

```ts
interface BaseWalletAccount {
  readonly signer: PrismSigner;
  readonly evmAddress: Address;       // Raw EOA
  readonly solanaAddress: string;
  recover(): RecoveryManager;
}
```

### `EnhancedWalletAccount`

Returned when an `AccountAbstractionProvider` is injected. Extends `BaseWalletAccount`.

```ts
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

```ts
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.

```ts
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`.

```ts
interface PrismSigner {
  signMessage(message: Hex): Promise<Hex>;
  signTypedData(payload: TypedDataPayload): Promise<Hex>;
  signTransaction<T>(tx: T): Promise<T>;
  readonly evmAddress: Address;       // Raw EOA
  readonly solanaAddress: string;
}
```

### Error Handling

All operations return `Result<T, WalletError>` instead of throwing:

```ts
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
```

## Documentation

- [SPEC.md](./SPEC.md) — Design decisions, security model, error codes
- [CHANGELOG.md](./CHANGELOG.md) — Version history

## License

MIT