@prism-ing/wallet 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +596 -0
- package/SPEC.md +192 -0
- package/dist/backends/squads-recovery-backend.d.ts +59 -0
- package/dist/backends/squads-recovery-backend.d.ts.map +1 -0
- package/dist/backends/squads-recovery-backend.js +81 -0
- package/dist/backends/squads-recovery-backend.js.map +1 -0
- package/dist/backends/squads-types.d.ts +74 -0
- package/dist/backends/squads-types.d.ts.map +1 -0
- package/dist/backends/squads-types.js +22 -0
- package/dist/backends/squads-types.js.map +1 -0
- package/dist/backends/zerodev-policy-mapper.d.ts +41 -0
- package/dist/backends/zerodev-policy-mapper.d.ts.map +1 -0
- package/dist/backends/zerodev-policy-mapper.js +127 -0
- package/dist/backends/zerodev-policy-mapper.js.map +1 -0
- package/dist/backends/zerodev-session-backend.d.ts +43 -0
- package/dist/backends/zerodev-session-backend.d.ts.map +1 -0
- package/dist/backends/zerodev-session-backend.js +63 -0
- package/dist/backends/zerodev-session-backend.js.map +1 -0
- package/dist/backends/zerodev-types.d.ts +104 -0
- package/dist/backends/zerodev-types.d.ts.map +1 -0
- package/dist/backends/zerodev-types.js +13 -0
- package/dist/backends/zerodev-types.js.map +1 -0
- package/dist/create-wallet.d.ts +89 -0
- package/dist/create-wallet.d.ts.map +1 -0
- package/dist/create-wallet.js +235 -0
- package/dist/create-wallet.js.map +1 -0
- package/dist/cross-chain.d.ts +64 -0
- package/dist/cross-chain.d.ts.map +1 -0
- package/dist/cross-chain.js +200 -0
- package/dist/cross-chain.js.map +1 -0
- package/dist/errors.d.ts +115 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +97 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/base58.d.ts +8 -0
- package/dist/internal/base58.d.ts.map +1 -0
- package/dist/internal/base58.js +34 -0
- package/dist/internal/base58.js.map +1 -0
- package/dist/internal/eip712.d.ts +41 -0
- package/dist/internal/eip712.d.ts.map +1 -0
- package/dist/internal/eip712.js +182 -0
- package/dist/internal/eip712.js.map +1 -0
- package/dist/internal/file-spend-persistence.d.ts +9 -0
- package/dist/internal/file-spend-persistence.d.ts.map +1 -0
- package/dist/internal/file-spend-persistence.js +58 -0
- package/dist/internal/file-spend-persistence.js.map +1 -0
- package/dist/internal/onebalance-client.d.ts +59 -0
- package/dist/internal/onebalance-client.d.ts.map +1 -0
- package/dist/internal/onebalance-client.js +2 -0
- package/dist/internal/onebalance-client.js.map +1 -0
- package/dist/internal/onebalance-http-client.d.ts +25 -0
- package/dist/internal/onebalance-http-client.d.ts.map +1 -0
- package/dist/internal/onebalance-http-client.js +161 -0
- package/dist/internal/onebalance-http-client.js.map +1 -0
- package/dist/internal/onebalance-types.d.ts +201 -0
- package/dist/internal/onebalance-types.d.ts.map +1 -0
- package/dist/internal/onebalance-types.js +39 -0
- package/dist/internal/onebalance-types.js.map +1 -0
- package/dist/internal/platform.d.ts +14 -0
- package/dist/internal/platform.d.ts.map +1 -0
- package/dist/internal/platform.js +22 -0
- package/dist/internal/platform.js.map +1 -0
- package/dist/internal/quote-verifier.d.ts +71 -0
- package/dist/internal/quote-verifier.d.ts.map +1 -0
- package/dist/internal/quote-verifier.js +172 -0
- package/dist/internal/quote-verifier.js.map +1 -0
- package/dist/internal/recovery-manager.d.ts +29 -0
- package/dist/internal/recovery-manager.d.ts.map +1 -0
- package/dist/internal/recovery-manager.js +161 -0
- package/dist/internal/recovery-manager.js.map +1 -0
- package/dist/result.d.ts +132 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +114 -0
- package/dist/result.js.map +1 -0
- package/dist/schemas.d.ts +184 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +76 -0
- package/dist/schemas.js.map +1 -0
- package/dist/session-keys.d.ts +53 -0
- package/dist/session-keys.d.ts.map +1 -0
- package/dist/session-keys.js +345 -0
- package/dist/session-keys.js.map +1 -0
- package/dist/signers/node-signing-backend.d.ts +11 -0
- package/dist/signers/node-signing-backend.d.ts.map +1 -0
- package/dist/signers/node-signing-backend.js +120 -0
- package/dist/signers/node-signing-backend.js.map +1 -0
- package/dist/signers/ows-adapter.d.ts +70 -0
- package/dist/signers/ows-adapter.d.ts.map +1 -0
- package/dist/signers/ows-adapter.js +53 -0
- package/dist/signers/ows-adapter.js.map +1 -0
- package/dist/signers/ows-signing-backend.d.ts +25 -0
- package/dist/signers/ows-signing-backend.d.ts.map +1 -0
- package/dist/signers/ows-signing-backend.js +192 -0
- package/dist/signers/ows-signing-backend.js.map +1 -0
- package/dist/signers/secure-enclave-backend.d.ts +19 -0
- package/dist/signers/secure-enclave-backend.d.ts.map +1 -0
- package/dist/signers/secure-enclave-backend.js +201 -0
- package/dist/signers/secure-enclave-backend.js.map +1 -0
- package/dist/signers/secure-enclave-types.d.ts +98 -0
- package/dist/signers/secure-enclave-types.d.ts.map +1 -0
- package/dist/signers/secure-enclave-types.js +12 -0
- package/dist/signers/secure-enclave-types.js.map +1 -0
- package/dist/types.d.ts +371 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +85 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @prism-ing/wallet
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **Session keys**: `createSessionKeyManager()` creates policy-scoped ephemeral signers for agents. Policies constrain time, assets, amounts, recipients, and chains. Optional `SessionKeyBackend` for on-chain enforcement via ZeroDev permission validators.
|
|
8
|
+
- **Quote verification**: `executeCrossChain()` now verifies OneBalance quotes before signing — checks expiration, asset/amount match, sender integrity, and tamper-proof signature. Configurable via `accountAddress` and `skipVerification` params.
|
|
9
|
+
- **Dynamic recovery challenge**: Recovery proofs are now bound to the specific signer rotation (current + new address) and a 5-minute time window, preventing replay attacks. Replaces the previous static challenge.
|
|
10
|
+
- New error codes: `SESSION_KEY_EXPIRED`, `SESSION_KEY_POLICY_VIOLATION`, `QUOTE_VERIFICATION_FAILED`.
|
|
11
|
+
- New exports: `createSessionKeyManager`, `validateSessionOperation`, `verifyQuoteIntegrity`, `verifyEvmOperation`, `buildRecoveryChallenge`.
|
|
12
|
+
|
|
13
|
+
## 0.1.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- Initial public release: OWS signing (Node.js software signer), OneBalance account abstraction, cross-chain execution, social recovery via guardian rotation.
|
|
18
|
+
|
|
19
|
+
## 0.0.1
|
|
20
|
+
|
|
21
|
+
- Initial implementation: OWS Node.js software signer, OneBalance account abstraction, recovery manager
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Prism
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
# @prism-ing/wallet
|
|
2
|
+
|
|
3
|
+
Self-custodial smart contract wallet SDK. Hardware-backed key security via iOS Secure Enclave, cross-chain execution via OneBalance Resource Locks, social recovery via ZeroDev (EVM) and Squads V4 (Solana). Built for agents, trading apps, and mobile wallets.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @prism-ing/wallet
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Optional peer dependencies (install only what you need):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @zerodev/sdk # On-chain session key enforcement
|
|
15
|
+
pnpm add @sqds/multisig # Solana social recovery
|
|
16
|
+
pnpm add viem # EVM utilities
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { createProductionWallet } from '@prism-ing/wallet';
|
|
23
|
+
|
|
24
|
+
const result = await createProductionWallet({
|
|
25
|
+
signer: { walletName: 'my-agent' },
|
|
26
|
+
oneBalanceApiKey: process.env.ONEBALANCE_API_KEY,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!result.ok) {
|
|
30
|
+
console.error(result.error.code);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const wallet = result.value;
|
|
35
|
+
console.log('EVM address:', wallet.evmAddress);
|
|
36
|
+
console.log('Solana address:', wallet.solanaAddress);
|
|
37
|
+
|
|
38
|
+
const balance = await wallet.getBalance();
|
|
39
|
+
console.log(`Balance: $${balance.totalUsd}`);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Same wallet name = same keys = same addresses, every time. Keys persist encrypted at `~/.ows/`.
|
|
43
|
+
|
|
44
|
+
## Architecture
|
|
45
|
+
|
|
46
|
+
Three systems compose to give you a persistent, cross-chain, recoverable wallet:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Signing Backends (platform-specific key management)
|
|
50
|
+
├── iOS Secure Enclave: keys encrypted by hardware P-256 key, biometric-gated
|
|
51
|
+
├── OWS (Node.js): secp256k1 + ed25519 from BIP-39, encrypted at ~/.ows/
|
|
52
|
+
└── Node Software: in-memory signing via @noble/curves (dev/testing)
|
|
53
|
+
|
|
54
|
+
OneBalance (Account Abstraction + Cross-Chain)
|
|
55
|
+
├── ERC-4337 counterfactual smart account from signer's public key
|
|
56
|
+
├── Deterministic address: same signer = same smart account
|
|
57
|
+
├── Resource Locks: instant cross-chain execution without source chain finality
|
|
58
|
+
└── Aggregated balance across all supported chains
|
|
59
|
+
|
|
60
|
+
Smart Contract Wallet (the actual "wallet")
|
|
61
|
+
├── Signer key authorizes operations, but is NOT the wallet itself
|
|
62
|
+
├── Funds live in the smart contract, not controlled by the raw key
|
|
63
|
+
├── Signer can be rotated without moving funds (recovery)
|
|
64
|
+
└── On-chain policy enforcement via ZeroDev permission validators
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**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.
|
|
68
|
+
|
|
69
|
+
## Private Key Management
|
|
70
|
+
|
|
71
|
+
### Security Model by Platform
|
|
72
|
+
|
|
73
|
+
| Property | Node.js (Agent) | iOS (Mobile) |
|
|
74
|
+
|----------|-----------------|--------------|
|
|
75
|
+
| Key storage | Encrypted at `~/.ows/` (AES-256-GCM, scrypt KDF) | Encrypted by Secure Enclave P-256 key, stored in iOS Keychain |
|
|
76
|
+
| Auth | Passphrase or API key | Biometric (Face ID / Touch ID) |
|
|
77
|
+
| Key isolation | In-process only, never serialized | Decrypted in-process only after biometric, never serialized |
|
|
78
|
+
| EVM signing | Software secp256k1 | Software secp256k1, biometric-gated |
|
|
79
|
+
| Solana signing | Software ed25519 | Software ed25519, biometric-gated |
|
|
80
|
+
| Seed phrases | None exposed. OWS encrypts mnemonic internally. | None. Keys are non-exportable. |
|
|
81
|
+
|
|
82
|
+
### Security Assumptions
|
|
83
|
+
|
|
84
|
+
1. **Private keys never leave the device in plaintext.** On iOS, keys are encrypted by the Secure Enclave's hardware P-256 key and stored in the Keychain. On Node.js, keys are encrypted with scrypt + AES-256-GCM.
|
|
85
|
+
|
|
86
|
+
2. **Signing requires authentication.** On iOS, every signing operation triggers Face ID / Touch ID via the `SecureEnclaveNativeBridge`. On Node.js, the OWS vault requires a passphrase or API key to decrypt.
|
|
87
|
+
|
|
88
|
+
3. **No key material in `OWSKeyPair`.** The `OWSKeyPair` type contains only addresses and a platform identifier. On iOS, it stores a Keychain tag referencing the encrypted key, not the key itself.
|
|
89
|
+
|
|
90
|
+
4. **The Secure Enclave does not support secp256k1 natively.** iOS uses the Secure Enclave's P-256 key to encrypt/decrypt secp256k1 and ed25519 private keys. Signing happens in-process after biometric-gated decryption, not inside the enclave chip.
|
|
91
|
+
|
|
92
|
+
5. **OneBalance is a trusted co-signer.** Cross-chain execution relies on OneBalance for quote generation and settlement. The SDK mitigates this trust with [client-side quote verification](#quote-verification) but cannot independently route transactions.
|
|
93
|
+
|
|
94
|
+
### Agent Mode
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// First run: creates wallet, encrypts keys at ~/.ows/
|
|
98
|
+
// Every subsequent run: loads same wallet, same addresses
|
|
99
|
+
const result = await createProductionWallet({
|
|
100
|
+
signer: { walletName: 'trading-agent' },
|
|
101
|
+
oneBalanceApiKey: process.env.ONEBALANCE_API_KEY,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
With passphrase protection:
|
|
106
|
+
```ts
|
|
107
|
+
const result = await createProductionWallet({
|
|
108
|
+
signer: {
|
|
109
|
+
walletName: 'treasury',
|
|
110
|
+
passphrase: process.env.WALLET_PASSPHRASE,
|
|
111
|
+
},
|
|
112
|
+
oneBalanceApiKey: process.env.ONEBALANCE_API_KEY,
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
With OWS agent API key (policy-gated signing):
|
|
117
|
+
```ts
|
|
118
|
+
const result = await createProductionWallet({
|
|
119
|
+
signer: {
|
|
120
|
+
walletName: 'treasury',
|
|
121
|
+
apiKey: 'ows_key_a1b2c3d4...',
|
|
122
|
+
},
|
|
123
|
+
oneBalanceApiKey: process.env.ONEBALANCE_API_KEY,
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### iOS Mobile App
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { createProductionWallet } from '@prism-ing/wallet';
|
|
131
|
+
import { SecureEnclaveModule } from './native/SecureEnclaveModule';
|
|
132
|
+
|
|
133
|
+
const result = await createProductionWallet(
|
|
134
|
+
{
|
|
135
|
+
signer: { walletName: 'user' },
|
|
136
|
+
oneBalanceApiKey: config.oneBalanceApiKey,
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
nativeBridge: SecureEnclaveModule, // React Native native module
|
|
140
|
+
biometricPrompt: 'Authorize this trade', // Face ID / Touch ID text
|
|
141
|
+
recoveryBackends: { evm: evmBackend, solana: solanaBackend },
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
On iOS, `createProductionWallet` detects the platform and auto-selects the Secure Enclave backend when `nativeBridge` is provided. The rest of the stack (OneBalance, cross-chain, recovery) works identically.
|
|
147
|
+
|
|
148
|
+
### Custom Signer (bring your own keys)
|
|
149
|
+
|
|
150
|
+
Routers and the wallet factory accept any `PrismSigner`. No dependency on `@prism-ing/wallet` for signing:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import type { PrismSigner } from '@prism-ing/wallet';
|
|
154
|
+
|
|
155
|
+
const mySigner: PrismSigner = {
|
|
156
|
+
evmAddress: myAddress,
|
|
157
|
+
solanaAddress: mySolanaAddress,
|
|
158
|
+
signMessage: (msg) => mySigningLib.sign(msg),
|
|
159
|
+
signTypedData: (payload) => mySigningLib.signTypedData(payload),
|
|
160
|
+
signTransaction: (tx) => mySigningLib.signSolana(tx),
|
|
161
|
+
};
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Session Keys
|
|
165
|
+
|
|
166
|
+
Session keys are ephemeral, policy-scoped signers for agents and automated processes. They wrap the root `PrismSigner` and enforce constraints on every signing operation. If a session key is compromised, the blast radius is bounded by its policy.
|
|
167
|
+
|
|
168
|
+
### How Session Keys Improve Security
|
|
169
|
+
|
|
170
|
+
Without session keys, an agent holds the root signer and can sign anything: any amount, any token, any chain, any recipient. A compromised agent means total loss.
|
|
171
|
+
|
|
172
|
+
With session keys, the agent holds a scoped signer that the SDK restricts before every signing operation:
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
Root Signer (full authority)
|
|
176
|
+
└── Session Key (policy-restricted)
|
|
177
|
+
├── Can only trade USDC
|
|
178
|
+
├── Max $1,000 per operation
|
|
179
|
+
├── Max $5,000 total
|
|
180
|
+
├── Only on Base and Arbitrum
|
|
181
|
+
├── Only to whitelisted addresses
|
|
182
|
+
└── Expires in 1 hour
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Two enforcement layers:**
|
|
186
|
+
|
|
187
|
+
1. **Client-side (always active):** The session signer checks policy before every `signMessage`, `signTypedData`, and `signTransaction`. Violations throw immediately, preventing the signature from being produced.
|
|
188
|
+
|
|
189
|
+
2. **On-chain (optional, via ZeroDev):** When a `SessionKeyBackend` is provided, policies are also registered as ZeroDev Kernel v3.1 permission validators. Even if a compromised client bypasses the JS check, the UserOp reverts on-chain.
|
|
190
|
+
|
|
191
|
+
### Basic Usage
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import { createSessionKeyManager, validateSessionOperation } from '@prism-ing/wallet';
|
|
195
|
+
|
|
196
|
+
const manager = createSessionKeyManager(wallet.signer);
|
|
197
|
+
|
|
198
|
+
const session = manager.createSessionKey({
|
|
199
|
+
expiresAt: Math.floor(Date.now() / 1000) + 3600, // 1 hour
|
|
200
|
+
allowedAssets: ['ob:usdc'], // USDC only
|
|
201
|
+
maxAmountPerOp: '1000000000', // 1,000 USDC max per swap
|
|
202
|
+
maxTotalAmount: '5000000000', // 5,000 USDC cumulative cap
|
|
203
|
+
allowedChains: [8453, 42161], // Base + Arbitrum only
|
|
204
|
+
allowedRecipients: ['0xabc...'], // Restrict destinations
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Pre-validate before execution (optional, fail-fast)
|
|
208
|
+
const violation = validateSessionOperation(session, {
|
|
209
|
+
asset: 'ob:usdc',
|
|
210
|
+
amount: '500000000',
|
|
211
|
+
chainId: 8453,
|
|
212
|
+
});
|
|
213
|
+
if (violation !== undefined) {
|
|
214
|
+
console.error('Policy violation:', violation.code);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Use the session signer — policy enforced on every sign call
|
|
219
|
+
const result = await executeCrossChain(
|
|
220
|
+
oneBalanceClient,
|
|
221
|
+
session.signer, // <-- scoped signer, not root
|
|
222
|
+
{ fromAsset: 'ob:usdc', toAsset: 'ob:eth', amount: '500000000' },
|
|
223
|
+
accounts,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Revoke when the task is complete
|
|
227
|
+
manager.revokeSessionKey(session.sessionId);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Policy Dimensions
|
|
231
|
+
|
|
232
|
+
| Dimension | Field | Effect |
|
|
233
|
+
|-----------|-------|--------|
|
|
234
|
+
| Time | `expiresAt` | Session expires after this Unix timestamp (required) |
|
|
235
|
+
| Assets | `allowedAssets` | Only these OneBalance asset IDs can be operated on |
|
|
236
|
+
| Per-op amount | `maxAmountPerOp` | Single operation cannot exceed this (smallest unit) |
|
|
237
|
+
| Cumulative | `maxTotalAmount` | Total spend across all operations is capped |
|
|
238
|
+
| Recipients | `allowedRecipients` | Only these addresses can be destinations |
|
|
239
|
+
| Chains | `allowedChains` | Only these chain IDs are permitted |
|
|
240
|
+
|
|
241
|
+
### On-Chain Enforcement (ZeroDev)
|
|
242
|
+
|
|
243
|
+
For production agent deployments, client-side enforcement alone is not sufficient. A compromised process could bypass the JS policy checks. On-chain enforcement ensures the smart contract itself rejects unauthorized operations.
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { createZeroDevSessionBackend, createSessionKeyManager } from '@prism-ing/wallet';
|
|
247
|
+
|
|
248
|
+
// Create the ZeroDev backend with your own SDK integration
|
|
249
|
+
const sessionBackend = createZeroDevSessionBackend({
|
|
250
|
+
registerOnChain: async (sessionAddress, policies) => {
|
|
251
|
+
// policies is ZeroDevPolicy[] — mapped from SessionKeyPolicy
|
|
252
|
+
// Each policy has a discriminated `type` field:
|
|
253
|
+
// 'timestamp' → validAfter/validUntil
|
|
254
|
+
// 'call' → contract/function/argument restrictions
|
|
255
|
+
// 'gas' → max gas spend per session
|
|
256
|
+
// 'rate_limit' → operations per interval
|
|
257
|
+
//
|
|
258
|
+
// Use @zerodev/permissions to register:
|
|
259
|
+
// 1. toECDSASigner({ signer: sessionPrivateKey })
|
|
260
|
+
// 2. toTimestampPolicy/toCallPolicy/toGasPolicy/toRateLimitPolicy
|
|
261
|
+
// 3. toPermissionValidator(client, { signer, policies, entryPoint, kernelVersion })
|
|
262
|
+
// 4. toInitConfig(validator) for pre-installed permissions
|
|
263
|
+
return txHash;
|
|
264
|
+
},
|
|
265
|
+
revokeOnChain: async (sessionAddress) => {
|
|
266
|
+
// Remove the permission validator from the Kernel account
|
|
267
|
+
return txHash;
|
|
268
|
+
},
|
|
269
|
+
defaultGasBudgetWei: 100_000_000_000_000_000n, // 0.1 ETH
|
|
270
|
+
callPolicyVersion: '0.0.4', // Alchemy bundler compatible
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Session keys are now enforced client-side AND on-chain
|
|
274
|
+
const manager = createSessionKeyManager(wallet.signer, sessionBackend);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Policy mapping details:**
|
|
278
|
+
|
|
279
|
+
The SDK automatically maps `SessionKeyPolicy` to ZeroDev on-chain policies:
|
|
280
|
+
|
|
281
|
+
- `expiresAt` maps to a **timestamp policy** with `validUntil`
|
|
282
|
+
- `allowedAssets` maps to a **call policy** restricting ERC-20 `transfer()` calls to known token contracts
|
|
283
|
+
- `maxAmountPerOp` maps to a `LESS_THAN_OR_EQUAL` parameter condition on the transfer amount
|
|
284
|
+
- `allowedRecipients` maps to `ONE_OF` parameter conditions on the transfer recipient
|
|
285
|
+
- A **gas policy** (default 0.1 ETH) is always included to prevent gas drain
|
|
286
|
+
- A **rate limit policy** is included when amount caps are set (100 ops/hour)
|
|
287
|
+
|
|
288
|
+
Use `mapSessionPolicyToZeroDev(policy)` directly if you need to inspect or customize the mapped policies before registration.
|
|
289
|
+
|
|
290
|
+
## Social Recovery
|
|
291
|
+
|
|
292
|
+
Recovery rotates the smart contract's authorized signer without moving funds. The original key becomes useless. This works because the signer is not the wallet — it's an authorization key for a smart contract that holds the funds.
|
|
293
|
+
|
|
294
|
+
### How Recovery Works
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
Device Lost / Key Compromised
|
|
298
|
+
↓
|
|
299
|
+
Generate new keys (new device, new OWS wallet, or new Secure Enclave key)
|
|
300
|
+
↓
|
|
301
|
+
Guardian authorizes rotation (passkey, second device, or trusted contact)
|
|
302
|
+
↓
|
|
303
|
+
EVM: ZeroDev removeValidator(old) + addValidator(new) on Kernel contract
|
|
304
|
+
Solana: Squads AddMember(new) + RemoveMember(old) on multisig
|
|
305
|
+
↓
|
|
306
|
+
Same wallet address, same funds, new authorized signer
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Guardian Types
|
|
310
|
+
|
|
311
|
+
| Guardian | How It Works | Best For |
|
|
312
|
+
|----------|-------------|----------|
|
|
313
|
+
| Passkey (iCloud Keychain) | WebAuthn credential signs recovery challenge | Single-device users |
|
|
314
|
+
| Device | Second device's signing key authorizes rotation | Multi-device users |
|
|
315
|
+
| Contact | Trusted contact's EVM address authorizes rotation | High-security setups |
|
|
316
|
+
|
|
317
|
+
### Setting Up Recovery
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
const recovery = wallet.recover();
|
|
321
|
+
|
|
322
|
+
// Register a passkey guardian (iCloud Keychain)
|
|
323
|
+
const passkey = await recovery.setupPasskey();
|
|
324
|
+
|
|
325
|
+
// Add a second device as guardian
|
|
326
|
+
await recovery.addDeviceGuardian(deviceEvmAddress, deviceSolanaAddress);
|
|
327
|
+
|
|
328
|
+
// Add a trusted contact
|
|
329
|
+
await recovery.addContactGuardian(contactEvmAddress);
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Performing Recovery
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
// On a new device, with new keys:
|
|
336
|
+
const newWallet = await createProductionWallet({
|
|
337
|
+
signer: { walletName: 'recovered-wallet' },
|
|
338
|
+
oneBalanceApiKey: process.env.ONEBALANCE_API_KEY,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const recovery = oldWalletRecoveryManager;
|
|
342
|
+
const recoveryTx = await recovery.initiateRecovery(newWallet.value.signer);
|
|
343
|
+
// Smart contract's authorized signer now points to newWallet.signer
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### EVM Recovery (ZeroDev)
|
|
347
|
+
|
|
348
|
+
The EVM recovery backend rotates the Kernel v3.1 smart account's authorized validator:
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import type { EvmRecoveryBackend } from '@prism-ing/wallet';
|
|
352
|
+
|
|
353
|
+
const evmBackend: EvmRecoveryBackend = {
|
|
354
|
+
async addGuardian(guardianAddress) {
|
|
355
|
+
// Register guardian on ZeroDev social recovery module
|
|
356
|
+
},
|
|
357
|
+
async rotateValidator(oldSigner, newSigner, guardianProof) {
|
|
358
|
+
// ZeroDev: removeValidator(old) + addValidator(new)
|
|
359
|
+
return txHash;
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Solana Recovery (Squads V4)
|
|
365
|
+
|
|
366
|
+
The Squads backend manages multisig membership for Solana key recovery:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { createSquadsRecoveryBackend } from '@prism-ing/wallet';
|
|
370
|
+
|
|
371
|
+
const solanaBackend = createSquadsRecoveryBackend({
|
|
372
|
+
bridge: {
|
|
373
|
+
async executeConfigTransaction(actions) {
|
|
374
|
+
// Wraps @sqds/multisig: create config tx → propose → approve → execute
|
|
375
|
+
// Actions: AddMember, RemoveMember, ChangeThreshold
|
|
376
|
+
return txSignature;
|
|
377
|
+
},
|
|
378
|
+
async getThreshold() { return currentThreshold; },
|
|
379
|
+
async getMemberCount() { return currentMemberCount; },
|
|
380
|
+
},
|
|
381
|
+
autoIncrementThreshold: true, // Adding a guardian increases threshold
|
|
382
|
+
});
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Key design decisions:**
|
|
386
|
+
|
|
387
|
+
- `configAuthority: null` — all changes go through the proposal flow (no admin override)
|
|
388
|
+
- `AddMember` executes before `RemoveMember` in the same config transaction, preventing a zero-member state
|
|
389
|
+
- Threshold auto-increments when adding guardians, requiring more consensus for future rotations
|
|
390
|
+
- Guardians get Voter-only permissions; the wallet signer gets full permissions
|
|
391
|
+
|
|
392
|
+
### Recovery Security
|
|
393
|
+
|
|
394
|
+
Recovery challenges are dynamic and bound to the specific rotation:
|
|
395
|
+
|
|
396
|
+
```
|
|
397
|
+
challenge = keccak256(
|
|
398
|
+
"prism-recovery-v1" // Domain tag (prevents cross-protocol replay)
|
|
399
|
+
|| currentSigner // The signer being rotated away
|
|
400
|
+
|| newSigner // The signer being rotated to
|
|
401
|
+
|| timeWindow // 5-minute quantized timestamp
|
|
402
|
+
)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
This means:
|
|
406
|
+
- A proof for A to B cannot be replayed for A to C (different `newSigner`)
|
|
407
|
+
- A captured proof expires within 5 minutes (time-bounded)
|
|
408
|
+
- A proof for wallet X cannot be used on wallet Y (different `currentSigner`)
|
|
409
|
+
|
|
410
|
+
Use `buildRecoveryChallenge(currentSigner, newSigner)` for custom recovery flows.
|
|
411
|
+
|
|
412
|
+
## Quote Verification
|
|
413
|
+
|
|
414
|
+
`executeCrossChain` automatically verifies OneBalance quotes before signing. This prevents blind-signing attacks where a compromised or man-in-the-middle API could present malicious EIP-712 payloads.
|
|
415
|
+
|
|
416
|
+
**Checks performed automatically:**
|
|
417
|
+
|
|
418
|
+
1. Quote has not expired
|
|
419
|
+
2. OneBalance tamper-proof signature is present
|
|
420
|
+
3. Origin asset matches the requested `fromAsset`
|
|
421
|
+
4. Origin amount is within 1% of the requested amount (configurable)
|
|
422
|
+
5. Destination asset matches the requested `toAsset`
|
|
423
|
+
6. EVM operation `userOp.sender` matches `accountAddress` (when provided)
|
|
424
|
+
|
|
425
|
+
```ts
|
|
426
|
+
const result = await executeCrossChain(client, signer, {
|
|
427
|
+
fromAsset: 'ob:usdc',
|
|
428
|
+
toAsset: 'ob:eth',
|
|
429
|
+
amount: '1000000',
|
|
430
|
+
accountAddress: wallet.evmAddress, // enables sender verification
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (!result.ok && result.error.code === 'QUOTE_VERIFICATION_FAILED') {
|
|
434
|
+
console.error('Quote integrity failed:', result.error.reason);
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Manual verification** (for custom flows):
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
import { verifyQuoteIntegrity, verifyEvmOperation } from '@prism-ing/wallet';
|
|
442
|
+
|
|
443
|
+
const error = verifyQuoteIntegrity(quote, {
|
|
444
|
+
fromAsset: 'ob:usdc',
|
|
445
|
+
toAsset: 'ob:eth',
|
|
446
|
+
amount: '1000000',
|
|
447
|
+
accountAddress: '0x1234...',
|
|
448
|
+
maxOriginSlippageFraction: 0.005, // 0.5% tolerance (default 1%)
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
## Cross-Chain Execution
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
import { executeCrossChain } from '@prism-ing/wallet';
|
|
456
|
+
|
|
457
|
+
const result = await executeCrossChain(
|
|
458
|
+
oneBalanceClient,
|
|
459
|
+
wallet.signer,
|
|
460
|
+
{
|
|
461
|
+
fromAsset: 'ob:usdc',
|
|
462
|
+
toAsset: 'ob:eth',
|
|
463
|
+
amount: '1000000',
|
|
464
|
+
slippageTolerance: 50,
|
|
465
|
+
accountAddress: wallet.evmAddress,
|
|
466
|
+
},
|
|
467
|
+
accounts,
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
if (result.ok) {
|
|
471
|
+
console.log('Quote ID:', result.value.quoteId);
|
|
472
|
+
|
|
473
|
+
// Poll execution status
|
|
474
|
+
const status = await wallet.getTransactionStatus(result.value.quoteId);
|
|
475
|
+
console.log('Status:', status.status); // 'submitted' | 'confirmed' | 'failed'
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
## API Reference
|
|
480
|
+
|
|
481
|
+
### `createProductionWallet(config, options?)`
|
|
482
|
+
|
|
483
|
+
Creates a wallet with platform-appropriate signing + OneBalance. Returns `Result<WalletAccount, WalletError>`.
|
|
484
|
+
|
|
485
|
+
```ts
|
|
486
|
+
interface WalletConfig {
|
|
487
|
+
signer: SignerConfig;
|
|
488
|
+
oneBalanceApiKey: string;
|
|
489
|
+
accountType?: 'kernel-v3.1-ecdsa';
|
|
490
|
+
chains?: number[];
|
|
491
|
+
recovery?: RecoveryConfig;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
interface ProductionWalletOptions {
|
|
495
|
+
nativeBridge?: SecureEnclaveNativeBridge; // iOS Secure Enclave
|
|
496
|
+
keyTag?: string; // iOS Keychain tag
|
|
497
|
+
biometricPrompt?: string; // Face ID / Touch ID text
|
|
498
|
+
recoveryBackends?: RecoveryBackends; // Guardian backends
|
|
499
|
+
}
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### `WalletAccount`
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
interface WalletAccount {
|
|
506
|
+
signer: PrismSigner;
|
|
507
|
+
evmAddress: Address;
|
|
508
|
+
solanaAddress: string;
|
|
509
|
+
getBalance(): Promise<UnifiedBalance>;
|
|
510
|
+
deposit(params: DepositParams): Promise<TxResult>;
|
|
511
|
+
getTransactionStatus(quoteId: string): Promise<TxResult>;
|
|
512
|
+
recover(): RecoveryManager;
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### `PrismSigner`
|
|
517
|
+
|
|
518
|
+
The core signing interface. Routers depend on this, not on `@prism-ing/wallet`.
|
|
519
|
+
|
|
520
|
+
```ts
|
|
521
|
+
interface PrismSigner {
|
|
522
|
+
signMessage(message: Hex): Promise<Hex>;
|
|
523
|
+
signTypedData(payload: TypedDataPayload): Promise<Hex>;
|
|
524
|
+
signTransaction<T>(tx: T): Promise<T>;
|
|
525
|
+
readonly evmAddress: Address;
|
|
526
|
+
readonly solanaAddress: string;
|
|
527
|
+
}
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### Error Handling
|
|
531
|
+
|
|
532
|
+
All operations return `Result<T, WalletError>` instead of throwing:
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
const result = await createProductionWallet(config);
|
|
536
|
+
|
|
537
|
+
if (!result.ok) {
|
|
538
|
+
if (isRetryableError(result.error)) {
|
|
539
|
+
// ONEBALANCE_TIMEOUT, ONEBALANCE_API_ERROR — safe to retry
|
|
540
|
+
}
|
|
541
|
+
if (isUserFacingError(result.error)) {
|
|
542
|
+
// SIGNING_REJECTED, INSUFFICIENT_BALANCE — show to user
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
| Code | Retryable | User-Facing | Description |
|
|
549
|
+
|------|-----------|-------------|-------------|
|
|
550
|
+
| `ENCLAVE_UNAVAILABLE` | No | No | Secure Enclave not available on this platform |
|
|
551
|
+
| `ONEBALANCE_TIMEOUT` | Yes | No | OneBalance API did not respond in time |
|
|
552
|
+
| `ONEBALANCE_API_ERROR` | Yes | No | OneBalance API returned an error |
|
|
553
|
+
| `RECOVERY_GUARDIAN_INVALID` | No | Yes | Invalid guardian address |
|
|
554
|
+
| `SIGNING_REJECTED` | No | Yes | User rejected signing (biometric fail / cancel) |
|
|
555
|
+
| `ACCOUNT_NOT_INITIALIZED` | No | No | Operation on uninitialized account |
|
|
556
|
+
| `INSUFFICIENT_BALANCE` | No | Yes | Not enough balance for operation |
|
|
557
|
+
| `KEY_NOT_FOUND` | No | No | Requested key pair not found in signing backend |
|
|
558
|
+
| `VALIDATION_ERROR` | No | Yes | Input failed Zod validation |
|
|
559
|
+
| `SESSION_KEY_EXPIRED` | No | No | Session key has expired or been revoked |
|
|
560
|
+
| `SESSION_KEY_POLICY_VIOLATION` | No | Yes | Operation violates session key policy |
|
|
561
|
+
| `QUOTE_VERIFICATION_FAILED` | No | No | OneBalance quote failed integrity verification |
|
|
562
|
+
|
|
563
|
+
## Defense-in-Depth Layers
|
|
564
|
+
|
|
565
|
+
```
|
|
566
|
+
Layer 1: Key Storage
|
|
567
|
+
iOS: Secure Enclave encryption + biometric gate
|
|
568
|
+
Node: OWS scrypt + AES-256-GCM encrypted vault
|
|
569
|
+
|
|
570
|
+
Layer 2: Session Key Policies
|
|
571
|
+
Client-side: JS enforcement before every signature
|
|
572
|
+
On-chain: ZeroDev permission validators (UserOp reverts if violated)
|
|
573
|
+
|
|
574
|
+
Layer 3: Quote Verification
|
|
575
|
+
6-point client-side integrity check before signing any OneBalance quote
|
|
576
|
+
|
|
577
|
+
Layer 4: Recovery Challenges
|
|
578
|
+
Dynamic keccak256 challenges bound to signer pair + 5-minute time window
|
|
579
|
+
|
|
580
|
+
Layer 5: Smart Contract Validation
|
|
581
|
+
ERC-4337 smart account validates all operations on-chain
|
|
582
|
+
ZeroDev Kernel v3.1 enforces session key policies at contract level
|
|
583
|
+
|
|
584
|
+
Layer 6: Network Resilience
|
|
585
|
+
Exponential backoff with jitter on OneBalance API (3 retries)
|
|
586
|
+
Only retries on timeouts, network errors, and 5xx responses
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
## Documentation
|
|
590
|
+
|
|
591
|
+
- [SPEC.md](./SPEC.md) — Design decisions, security model, error codes
|
|
592
|
+
- [CHANGELOG.md](./CHANGELOG.md) — Version history
|
|
593
|
+
|
|
594
|
+
## License
|
|
595
|
+
|
|
596
|
+
MIT
|