@deserialize/multi-vm-wallet 1.4.12 → 1.5.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/.claude/settings.local.json +7 -1
- package/BUILD_OPTIMIZATION_PLAN.md +640 -0
- package/BUILD_RESULTS.md +282 -0
- package/BUN_MIGRATION.md +415 -0
- package/CHANGELOG_SECURITY.md +573 -0
- package/IMPLEMENTATION_SUMMARY.md +494 -0
- package/SECURITY_AUDIT.md +1124 -0
- package/bun.lock +553 -0
- package/dist/IChainWallet.js +0 -5
- package/dist/bip32Old.js +0 -885
- package/dist/bip32Small.js +0 -79
- package/dist/bipTest.js +0 -362
- package/dist/constant.js +0 -17
- package/dist/english.js +0 -1
- package/dist/evm/aa-service/index.d.ts +0 -5
- package/dist/evm/aa-service/index.js +0 -14
- package/dist/evm/aa-service/lib/account-adapter.d.ts +0 -22
- package/dist/evm/aa-service/lib/account-adapter.js +0 -24
- package/dist/evm/aa-service/lib/kernel-account.d.ts +0 -30
- package/dist/evm/aa-service/lib/kernel-account.js +2 -67
- package/dist/evm/aa-service/lib/kernel-modules.d.ts +0 -177
- package/dist/evm/aa-service/lib/kernel-modules.js +4 -202
- package/dist/evm/aa-service/lib/session-keys.d.ts +0 -118
- package/dist/evm/aa-service/lib/session-keys.js +7 -151
- package/dist/evm/aa-service/lib/type.d.ts +0 -55
- package/dist/evm/aa-service/lib/type.js +0 -10
- package/dist/evm/aa-service/services/account-abstraction.d.ts +0 -426
- package/dist/evm/aa-service/services/account-abstraction.js +0 -461
- package/dist/evm/aa-service/services/bundler.d.ts +0 -6
- package/dist/evm/aa-service/services/bundler.js +0 -54
- package/dist/evm/evm.d.ts +10 -67
- package/dist/evm/evm.js +340 -102
- package/dist/evm/index.js +0 -3
- package/dist/evm/script.js +3 -17
- package/dist/evm/smartWallet.d.ts +0 -173
- package/dist/evm/smartWallet.js +0 -206
- package/dist/evm/smartWallet.types.d.ts +0 -6
- package/dist/evm/smartWallet.types.js +0 -8
- package/dist/evm/transaction.utils.d.ts +0 -242
- package/dist/evm/transaction.utils.js +4 -320
- package/dist/evm/transactionParsing.d.ts +0 -11
- package/dist/evm/transactionParsing.js +28 -147
- package/dist/evm/utils.d.ts +0 -46
- package/dist/evm/utils.js +1 -57
- package/dist/helpers/index.d.ts +0 -4
- package/dist/helpers/index.js +8 -44
- package/dist/helpers/routeScan.js +0 -1
- package/dist/index.js +0 -1
- package/dist/old.js +0 -884
- package/dist/price.js +0 -1
- package/dist/price.types.js +0 -2
- package/dist/rate-limiter.d.ts +28 -0
- package/dist/rate-limiter.js +95 -0
- package/dist/retry-logic.d.ts +14 -0
- package/dist/retry-logic.js +120 -0
- package/dist/savings/index.d.ts +1 -0
- package/dist/savings/index.js +16 -2
- package/dist/savings/saving-manager.d.ts +46 -0
- package/dist/savings/saving-manager.js +176 -0
- package/dist/savings/savings-operations.d.ts +39 -0
- package/dist/savings/savings-operations.js +141 -0
- package/dist/savings/smart-savings.d.ts +0 -63
- package/dist/savings/smart-savings.js +0 -78
- package/dist/savings/types.d.ts +0 -69
- package/dist/savings/types.js +0 -7
- package/dist/savings/validation.d.ts +9 -0
- package/dist/savings/validation.js +85 -0
- package/dist/svm/constant.js +0 -1
- package/dist/svm/index.js +0 -1
- package/dist/svm/svm.d.ts +7 -13
- package/dist/svm/svm.js +263 -46
- package/dist/svm/transactionParsing.d.ts +0 -7
- package/dist/svm/transactionParsing.js +3 -41
- package/dist/svm/transactionSender.js +0 -9
- package/dist/svm/utils.d.ts +0 -12
- package/dist/svm/utils.js +9 -60
- package/dist/test.d.ts +0 -4
- package/dist/test.js +15 -95
- package/dist/transaction-utils.d.ts +38 -0
- package/dist/transaction-utils.js +168 -0
- package/dist/types.d.ts +36 -0
- package/dist/types.js +0 -1
- package/dist/utils.js +0 -1
- package/dist/vm-validation.d.ts +11 -0
- package/dist/vm-validation.js +151 -0
- package/dist/vm.d.ts +14 -16
- package/dist/vm.js +64 -53
- package/dist/walletBip32.d.ts +2 -0
- package/dist/walletBip32.js +33 -66
- package/package.json +9 -4
- package/test-discovery.ts +235 -0
- package/test-pocket-discovery.ts +84 -0
- package/tsconfig.json +18 -11
- package/tsconfig.prod.json +10 -0
- package/utils/IChainWallet.ts +2 -0
- package/utils/evm/evm.ts +560 -39
- package/utils/rate-limiter.ts +179 -0
- package/utils/retry-logic.ts +271 -0
- package/utils/savings/EXAMPLES.md +883 -0
- package/utils/savings/SECURITY.md +731 -0
- package/utils/savings/index.ts +1 -1
- package/utils/savings/saving-manager.ts +656 -0
- package/utils/savings/savings-operations.ts +509 -0
- package/utils/savings/validation.ts +187 -0
- package/utils/svm/svm.ts +467 -20
- package/utils/test.ts +26 -3
- package/utils/transaction-utils.ts +394 -0
- package/utils/types.ts +100 -0
- package/utils/vm-validation.ts +280 -0
- package/utils/vm.ts +202 -24
- package/utils/walletBip32.ts +63 -3
- package/dist/IChainWallet.js.map +0 -1
- package/dist/bip32.d.ts +0 -9
- package/dist/bip32.js +0 -172
- package/dist/bip32.js.map +0 -1
- package/dist/bip32Old.js.map +0 -1
- package/dist/bip32Small.js.map +0 -1
- package/dist/bipTest.js.map +0 -1
- package/dist/constant.js.map +0 -1
- package/dist/english.js.map +0 -1
- package/dist/evm/SMART_WALLET_EXAMPLES.d.ts +0 -20
- package/dist/evm/SMART_WALLET_EXAMPLES.js +0 -451
- package/dist/evm/SMART_WALLET_EXAMPLES.js.map +0 -1
- package/dist/evm/aa-service/index.js.map +0 -1
- package/dist/evm/aa-service/lib/account-adapter.js.map +0 -1
- package/dist/evm/aa-service/lib/kernel-account.js.map +0 -1
- package/dist/evm/aa-service/lib/kernel-modules.js.map +0 -1
- package/dist/evm/aa-service/lib/session-keys.js.map +0 -1
- package/dist/evm/aa-service/lib/type.js.map +0 -1
- package/dist/evm/aa-service/services/account-abstraction.js.map +0 -1
- package/dist/evm/aa-service/services/bundler.js.map +0 -1
- package/dist/evm/evm.js.map +0 -1
- package/dist/evm/index.js.map +0 -1
- package/dist/evm/script.js.map +0 -1
- package/dist/evm/smartWallet.js.map +0 -1
- package/dist/evm/smartWallet.types.js.map +0 -1
- package/dist/evm/transaction.utils.js.map +0 -1
- package/dist/evm/transactionParsing.js.map +0 -1
- package/dist/evm/utils.js.map +0 -1
- package/dist/helpers/index.js.map +0 -1
- package/dist/helpers/routeScan.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/old.js.map +0 -1
- package/dist/price.js.map +0 -1
- package/dist/price.types.js.map +0 -1
- package/dist/privacy/artifact-manager.d.ts +0 -117
- package/dist/privacy/artifact-manager.js +0 -251
- package/dist/privacy/artifact-manager.js.map +0 -1
- package/dist/privacy/broadcaster-client.d.ts +0 -166
- package/dist/privacy/broadcaster-client.js +0 -261
- package/dist/privacy/broadcaster-client.js.map +0 -1
- package/dist/privacy/index.d.ts +0 -34
- package/dist/privacy/index.js +0 -56
- package/dist/privacy/index.js.map +0 -1
- package/dist/privacy/network-config.d.ts +0 -57
- package/dist/privacy/network-config.js +0 -118
- package/dist/privacy/network-config.js.map +0 -1
- package/dist/privacy/poi-helper.d.ts +0 -161
- package/dist/privacy/poi-helper.js +0 -249
- package/dist/privacy/poi-helper.js.map +0 -1
- package/dist/privacy/railgun-engine.d.ts +0 -135
- package/dist/privacy/railgun-engine.js +0 -205
- package/dist/privacy/railgun-engine.js.map +0 -1
- package/dist/privacy/railgun-privacy-wallet.d.ts +0 -288
- package/dist/privacy/railgun-privacy-wallet.js +0 -539
- package/dist/privacy/railgun-privacy-wallet.js.map +0 -1
- package/dist/privacy/types.d.ts +0 -229
- package/dist/privacy/types.js +0 -26
- package/dist/privacy/types.js.map +0 -1
- package/dist/savings/index.js.map +0 -1
- package/dist/savings/saving-actions.d.ts +0 -0
- package/dist/savings/saving-actions.js +0 -78
- package/dist/savings/saving-actions.js.map +0 -1
- package/dist/savings/savings-manager.d.ts +0 -126
- package/dist/savings/savings-manager.js +0 -234
- package/dist/savings/savings-manager.js.map +0 -1
- package/dist/savings/smart-savings.js.map +0 -1
- package/dist/savings/types.js.map +0 -1
- package/dist/svm/constant.js.map +0 -1
- package/dist/svm/index.js.map +0 -1
- package/dist/svm/svm.js.map +0 -1
- package/dist/svm/transactionParsing.js.map +0 -1
- package/dist/svm/transactionSender.js.map +0 -1
- package/dist/svm/utils.js.map +0 -1
- package/dist/test.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/dist/vm.js.map +0 -1
- package/dist/walletBip32.js.map +0 -1
- package/utils/savings/saving-actions.ts +0 -92
- package/utils/savings/savings-manager.ts +0 -271
|
@@ -0,0 +1,883 @@
|
|
|
1
|
+
# Savings Feature Usage Examples
|
|
2
|
+
|
|
3
|
+
This document provides practical examples for implementing the savings feature in different types of wallet applications.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
1. [Browser Extension Example](#browser-extension-example)
|
|
8
|
+
2. [React Native Mobile App Example](#react-native-mobile-app-example)
|
|
9
|
+
3. [Common Operations](#common-operations)
|
|
10
|
+
4. [Error Handling](#error-handling)
|
|
11
|
+
5. [Testing](#testing)
|
|
12
|
+
|
|
13
|
+
## Browser Extension Example
|
|
14
|
+
|
|
15
|
+
Complete implementation for a Chrome extension using stateless operations.
|
|
16
|
+
|
|
17
|
+
### Background Script (background.js)
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { SavingsOperations } from '@wallet/utils/savings/savings-operations';
|
|
21
|
+
import { JsonRpcProvider } from 'ethers';
|
|
22
|
+
|
|
23
|
+
// Stateless operations - no mnemonic stored
|
|
24
|
+
const operations = new SavingsOperations();
|
|
25
|
+
|
|
26
|
+
// Session management
|
|
27
|
+
class ExtensionWalletSession {
|
|
28
|
+
private password: string | null = null;
|
|
29
|
+
private lockTimeout?: NodeJS.Timeout;
|
|
30
|
+
private readonly AUTO_LOCK_DELAY = 15 * 60 * 1000; // 15 minutes
|
|
31
|
+
|
|
32
|
+
async unlock(password: string): Promise<boolean> {
|
|
33
|
+
try {
|
|
34
|
+
// Verify password by attempting to decrypt
|
|
35
|
+
await this.decryptMnemonic(password);
|
|
36
|
+
|
|
37
|
+
// Store password for session
|
|
38
|
+
this.password = password;
|
|
39
|
+
|
|
40
|
+
// Set auto-lock timer
|
|
41
|
+
this.resetLockTimer();
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lock(): void {
|
|
50
|
+
this.password = null;
|
|
51
|
+
if (this.lockTimeout) {
|
|
52
|
+
clearTimeout(this.lockTimeout);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
isUnlocked(): boolean {
|
|
57
|
+
return this.password !== null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private resetLockTimer(): void {
|
|
61
|
+
if (this.lockTimeout) {
|
|
62
|
+
clearTimeout(this.lockTimeout);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.lockTimeout = setTimeout(() => {
|
|
66
|
+
this.lock();
|
|
67
|
+
// Notify popup that wallet is locked
|
|
68
|
+
chrome.runtime.sendMessage({ type: 'WALLET_LOCKED' });
|
|
69
|
+
}, this.AUTO_LOCK_DELAY);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async decryptMnemonic(password: string): Promise<string> {
|
|
73
|
+
const { encryptedMnemonic } = await chrome.storage.local.get('encryptedMnemonic');
|
|
74
|
+
|
|
75
|
+
if (!encryptedMnemonic) {
|
|
76
|
+
throw new Error('No wallet found');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Decrypt using Web Crypto API
|
|
80
|
+
const key = await this.deriveKey(password);
|
|
81
|
+
const decrypted = await this.decrypt(encryptedMnemonic, key);
|
|
82
|
+
|
|
83
|
+
return decrypted;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private async deriveKey(password: string): Promise<CryptoKey> {
|
|
87
|
+
const encoder = new TextEncoder();
|
|
88
|
+
const passwordBuffer = encoder.encode(password);
|
|
89
|
+
|
|
90
|
+
// Import password as key material
|
|
91
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
92
|
+
'raw',
|
|
93
|
+
passwordBuffer,
|
|
94
|
+
'PBKDF2',
|
|
95
|
+
false,
|
|
96
|
+
['deriveKey']
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Get salt from storage (generated during wallet creation)
|
|
100
|
+
const { salt } = await chrome.storage.local.get('salt');
|
|
101
|
+
|
|
102
|
+
// Derive encryption key using PBKDF2
|
|
103
|
+
return await crypto.subtle.deriveKey(
|
|
104
|
+
{
|
|
105
|
+
name: 'PBKDF2',
|
|
106
|
+
salt: new Uint8Array(salt),
|
|
107
|
+
iterations: 100000,
|
|
108
|
+
hash: 'SHA-256'
|
|
109
|
+
},
|
|
110
|
+
keyMaterial,
|
|
111
|
+
{ name: 'AES-GCM', length: 256 },
|
|
112
|
+
false,
|
|
113
|
+
['decrypt']
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private async decrypt(encryptedData: ArrayBuffer, key: CryptoKey): Promise<string> {
|
|
118
|
+
const decoder = new TextDecoder();
|
|
119
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
120
|
+
{ name: 'AES-GCM', iv: new Uint8Array(encryptedData.slice(0, 12)) },
|
|
121
|
+
key,
|
|
122
|
+
encryptedData.slice(12)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return decoder.decode(decrypted);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async executeOperation<T>(
|
|
129
|
+
operation: (mnemonic: string) => Promise<T>
|
|
130
|
+
): Promise<T> {
|
|
131
|
+
if (!this.password) {
|
|
132
|
+
throw new Error('Wallet locked');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Reset timeout on each operation
|
|
136
|
+
this.resetLockTimer();
|
|
137
|
+
|
|
138
|
+
// Get mnemonic for this operation only
|
|
139
|
+
const mnemonic = await this.decryptMnemonic(this.password);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
return await operation(mnemonic);
|
|
143
|
+
} finally {
|
|
144
|
+
// Mnemonic goes out of scope and will be garbage collected
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const session = new ExtensionWalletSession();
|
|
150
|
+
|
|
151
|
+
// Message handlers
|
|
152
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
153
|
+
(async () => {
|
|
154
|
+
try {
|
|
155
|
+
switch (message.type) {
|
|
156
|
+
case 'UNLOCK_WALLET':
|
|
157
|
+
const unlocked = await session.unlock(message.password);
|
|
158
|
+
sendResponse({ success: unlocked });
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case 'LOCK_WALLET':
|
|
162
|
+
session.lock();
|
|
163
|
+
sendResponse({ success: true });
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case 'GET_POCKET_BALANCE':
|
|
167
|
+
const balance = await session.executeOperation(async (mnemonic) => {
|
|
168
|
+
const provider = new JsonRpcProvider(message.rpcUrl);
|
|
169
|
+
return await operations.getPocketBalance(
|
|
170
|
+
mnemonic,
|
|
171
|
+
{
|
|
172
|
+
accountIndex: message.pocketIndex,
|
|
173
|
+
walletIndex: 0
|
|
174
|
+
},
|
|
175
|
+
provider
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
sendResponse({ success: true, balance });
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case 'TRANSFER_FROM_POCKET':
|
|
182
|
+
const result = await session.executeOperation(async (mnemonic) => {
|
|
183
|
+
const provider = new JsonRpcProvider(message.rpcUrl);
|
|
184
|
+
return await operations.transferFromPocket(
|
|
185
|
+
mnemonic,
|
|
186
|
+
{
|
|
187
|
+
accountIndex: message.pocketIndex,
|
|
188
|
+
walletIndex: 0,
|
|
189
|
+
to: message.to,
|
|
190
|
+
amount: BigInt(message.amount)
|
|
191
|
+
},
|
|
192
|
+
provider,
|
|
193
|
+
message.chain
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
sendResponse({ success: true, result });
|
|
197
|
+
break;
|
|
198
|
+
|
|
199
|
+
case 'GET_POCKET_ADDRESS':
|
|
200
|
+
const address = await session.executeOperation(async (mnemonic) => {
|
|
201
|
+
return operations.getPocketAddress(mnemonic, message.pocketIndex, 0);
|
|
202
|
+
});
|
|
203
|
+
sendResponse({ success: true, address });
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
default:
|
|
207
|
+
sendResponse({ success: false, error: 'Unknown message type' });
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
sendResponse({ success: false, error: error.message });
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
|
|
214
|
+
return true; // Keep channel open for async response
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Popup Script (popup.tsx)
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import React, { useState, useEffect } from 'react';
|
|
222
|
+
import { formatUnits } from 'ethers';
|
|
223
|
+
|
|
224
|
+
function PocketView() {
|
|
225
|
+
const [pockets, setPockets] = useState<Array<{ index: number; balance: string }>>([]);
|
|
226
|
+
const [loading, setLoading] = useState(false);
|
|
227
|
+
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
loadPockets();
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
async function loadPockets() {
|
|
233
|
+
setLoading(true);
|
|
234
|
+
try {
|
|
235
|
+
// Load balances for pockets 0-4
|
|
236
|
+
const promises = [0, 1, 2, 3, 4].map(async (index) => {
|
|
237
|
+
const response = await chrome.runtime.sendMessage({
|
|
238
|
+
type: 'GET_POCKET_BALANCE',
|
|
239
|
+
pocketIndex: index,
|
|
240
|
+
rpcUrl: 'https://eth.llamarpc.com'
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (response.success) {
|
|
244
|
+
return {
|
|
245
|
+
index,
|
|
246
|
+
balance: formatUnits(response.balance, 18)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const results = await Promise.all(promises);
|
|
253
|
+
setPockets(results.filter(p => p !== null && parseFloat(p.balance) > 0));
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.error('Failed to load pockets:', error);
|
|
256
|
+
} finally {
|
|
257
|
+
setLoading(false);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function transferFromPocket(pocketIndex: number, to: string, amount: string) {
|
|
262
|
+
setLoading(true);
|
|
263
|
+
try {
|
|
264
|
+
const response = await chrome.runtime.sendMessage({
|
|
265
|
+
type: 'TRANSFER_FROM_POCKET',
|
|
266
|
+
pocketIndex,
|
|
267
|
+
to,
|
|
268
|
+
amount: parseUnits(amount, 18).toString(),
|
|
269
|
+
rpcUrl: 'https://eth.llamarpc.com',
|
|
270
|
+
chain: {
|
|
271
|
+
chainId: 1,
|
|
272
|
+
name: 'Ethereum',
|
|
273
|
+
rpcUrl: 'https://eth.llamarpc.com'
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
if (response.success) {
|
|
278
|
+
alert(`Transaction sent: ${response.result.hash}`);
|
|
279
|
+
loadPockets(); // Reload balances
|
|
280
|
+
} else {
|
|
281
|
+
alert(`Error: ${response.error}`);
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
alert(`Error: ${error.message}`);
|
|
285
|
+
} finally {
|
|
286
|
+
setLoading(false);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<div className="pocket-view">
|
|
292
|
+
<h2>Savings Pockets</h2>
|
|
293
|
+
|
|
294
|
+
{loading && <p>Loading...</p>}
|
|
295
|
+
|
|
296
|
+
<div className="pockets-list">
|
|
297
|
+
{pockets.map(pocket => (
|
|
298
|
+
<div key={pocket.index} className="pocket-item">
|
|
299
|
+
<h3>Pocket {pocket.index}</h3>
|
|
300
|
+
<p>Balance: {pocket.balance} ETH</p>
|
|
301
|
+
<button onClick={() => {
|
|
302
|
+
const to = prompt('Recipient address:');
|
|
303
|
+
const amount = prompt('Amount (ETH):');
|
|
304
|
+
if (to && amount) {
|
|
305
|
+
transferFromPocket(pocket.index, to, amount);
|
|
306
|
+
}
|
|
307
|
+
}}>
|
|
308
|
+
Withdraw
|
|
309
|
+
</button>
|
|
310
|
+
</div>
|
|
311
|
+
))}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export default PocketView;
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## React Native Mobile App Example
|
|
321
|
+
|
|
322
|
+
Complete implementation for a mobile app using stateful manager.
|
|
323
|
+
|
|
324
|
+
### Wallet Service
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { SavingsManager } from '@wallet/utils/savings/saving-manager';
|
|
328
|
+
import { createWalletClient, createPublicClient, http } from 'viem';
|
|
329
|
+
import { mainnet } from 'viem/chains';
|
|
330
|
+
import * as Keychain from 'react-native-keychain';
|
|
331
|
+
import TouchID from 'react-native-touch-id';
|
|
332
|
+
import { AppState, AppStateStatus } from 'react-native';
|
|
333
|
+
|
|
334
|
+
class WalletService {
|
|
335
|
+
private manager: SavingsManager | null = null;
|
|
336
|
+
private lockTimer?: NodeJS.Timeout;
|
|
337
|
+
private readonly AUTO_LOCK_DELAY = 5 * 60 * 1000; // 5 minutes
|
|
338
|
+
|
|
339
|
+
constructor() {
|
|
340
|
+
// Listen for app state changes
|
|
341
|
+
AppState.addEventListener('change', this.handleAppStateChange);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Initialize wallet with biometric authentication
|
|
346
|
+
*/
|
|
347
|
+
async initialize(): Promise<boolean> {
|
|
348
|
+
try {
|
|
349
|
+
// Require biometric authentication
|
|
350
|
+
await TouchID.authenticate('Unlock wallet', {
|
|
351
|
+
title: 'Authentication Required',
|
|
352
|
+
fallbackLabel: 'Use Passcode'
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Get mnemonic from secure storage
|
|
356
|
+
const credentials = await Keychain.getGenericPassword({
|
|
357
|
+
service: 'com.myapp.wallet'
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (!credentials) {
|
|
361
|
+
throw new Error('Wallet not found');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Create manager
|
|
365
|
+
this.manager = new SavingsManager(
|
|
366
|
+
credentials.password,
|
|
367
|
+
{
|
|
368
|
+
chainId: 1,
|
|
369
|
+
name: 'Ethereum',
|
|
370
|
+
rpcUrl: 'https://eth.llamarpc.com'
|
|
371
|
+
},
|
|
372
|
+
0 // wallet index
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Set auto-lock timer
|
|
376
|
+
this.resetLockTimer();
|
|
377
|
+
|
|
378
|
+
return true;
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error('Failed to initialize wallet:', error);
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Lock wallet and clear sensitive data
|
|
387
|
+
*/
|
|
388
|
+
lock(): void {
|
|
389
|
+
if (this.manager) {
|
|
390
|
+
this.manager.dispose();
|
|
391
|
+
this.manager = null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (this.lockTimer) {
|
|
395
|
+
clearTimeout(this.lockTimer);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
isUnlocked(): boolean {
|
|
400
|
+
return this.manager !== null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private resetLockTimer(): void {
|
|
404
|
+
if (this.lockTimer) {
|
|
405
|
+
clearTimeout(this.lockTimer);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
this.lockTimer = setTimeout(() => {
|
|
409
|
+
this.lock();
|
|
410
|
+
}, this.AUTO_LOCK_DELAY);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private handleAppStateChange = (nextAppState: AppStateStatus) => {
|
|
414
|
+
if (nextAppState === 'background' || nextAppState === 'inactive') {
|
|
415
|
+
// Lock immediately when app backgrounds
|
|
416
|
+
this.lock();
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get manager instance (throws if locked)
|
|
422
|
+
*/
|
|
423
|
+
getManager(): SavingsManager {
|
|
424
|
+
if (!this.manager) {
|
|
425
|
+
throw new Error('Wallet locked - please authenticate');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Reset auto-lock timer on each access
|
|
429
|
+
this.resetLockTimer();
|
|
430
|
+
|
|
431
|
+
return this.manager;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Create a new savings pocket
|
|
436
|
+
*/
|
|
437
|
+
async createPocket(pocketIndex: number): Promise<string> {
|
|
438
|
+
const manager = this.getManager();
|
|
439
|
+
const pocket = manager.getPocket(pocketIndex);
|
|
440
|
+
return pocket.address;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Get pocket balance
|
|
445
|
+
*/
|
|
446
|
+
async getPocketBalance(pocketIndex: number, tokens: string[]): Promise<any> {
|
|
447
|
+
const manager = this.getManager();
|
|
448
|
+
return await manager.getPocketTokenBalance(tokens, pocketIndex);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Transfer to pocket
|
|
453
|
+
*/
|
|
454
|
+
async transferToPocket(
|
|
455
|
+
walletClient: any,
|
|
456
|
+
pocketIndex: number,
|
|
457
|
+
amount: string
|
|
458
|
+
): Promise<any> {
|
|
459
|
+
const manager = this.getManager();
|
|
460
|
+
return await manager.transferToPocket(walletClient, pocketIndex, amount);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Transfer from pocket back to main wallet
|
|
465
|
+
*/
|
|
466
|
+
async transferFromPocket(
|
|
467
|
+
pocketIndex: number,
|
|
468
|
+
amount: number,
|
|
469
|
+
token: string = 'native'
|
|
470
|
+
): Promise<any> {
|
|
471
|
+
const manager = this.getManager();
|
|
472
|
+
return await manager.sendToMainWallet(pocketIndex, amount, token as any);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export const walletService = new WalletService();
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### React Component
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
import React, { useState, useEffect } from 'react';
|
|
483
|
+
import { View, Text, Button, FlatList, Alert } from 'react-native';
|
|
484
|
+
import { walletService } from './WalletService';
|
|
485
|
+
|
|
486
|
+
function PocketsScreen() {
|
|
487
|
+
const [pockets, setPockets] = useState<Array<{ index: number; address: string; balance: any }>>([]);
|
|
488
|
+
const [loading, setLoading] = useState(false);
|
|
489
|
+
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
loadPockets();
|
|
492
|
+
}, []);
|
|
493
|
+
|
|
494
|
+
async function loadPockets() {
|
|
495
|
+
setLoading(true);
|
|
496
|
+
try {
|
|
497
|
+
// Check if wallet is unlocked
|
|
498
|
+
if (!walletService.isUnlocked()) {
|
|
499
|
+
const unlocked = await walletService.initialize();
|
|
500
|
+
if (!unlocked) {
|
|
501
|
+
Alert.alert('Error', 'Failed to unlock wallet');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Load pockets 0-4
|
|
507
|
+
const pocketData = await Promise.all(
|
|
508
|
+
[0, 1, 2, 3, 4].map(async (index) => {
|
|
509
|
+
try {
|
|
510
|
+
const address = await walletService.createPocket(index);
|
|
511
|
+
const balance = await walletService.getPocketBalance(index, []);
|
|
512
|
+
|
|
513
|
+
return { index, address, balance };
|
|
514
|
+
} catch (error) {
|
|
515
|
+
console.error(`Failed to load pocket ${index}:`, error);
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
})
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
setPockets(pocketData.filter(p => p !== null));
|
|
522
|
+
} catch (error) {
|
|
523
|
+
Alert.alert('Error', error.message);
|
|
524
|
+
} finally {
|
|
525
|
+
setLoading(false);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function handleWithdraw(pocketIndex: number) {
|
|
530
|
+
Alert.prompt(
|
|
531
|
+
'Withdraw from Pocket',
|
|
532
|
+
'Enter amount (ETH):',
|
|
533
|
+
async (amount) => {
|
|
534
|
+
try {
|
|
535
|
+
setLoading(true);
|
|
536
|
+
|
|
537
|
+
const result = await walletService.transferFromPocket(
|
|
538
|
+
pocketIndex,
|
|
539
|
+
parseFloat(amount) * 1e18, // Convert to wei
|
|
540
|
+
'native'
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
Alert.alert('Success', `Transaction: ${result.hash}`);
|
|
544
|
+
loadPockets(); // Reload
|
|
545
|
+
} catch (error) {
|
|
546
|
+
Alert.alert('Error', error.message);
|
|
547
|
+
} finally {
|
|
548
|
+
setLoading(false);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return (
|
|
555
|
+
<View style={{ padding: 20 }}>
|
|
556
|
+
<Text style={{ fontSize: 24, fontWeight: 'bold' }}>Savings Pockets</Text>
|
|
557
|
+
|
|
558
|
+
{loading && <Text>Loading...</Text>}
|
|
559
|
+
|
|
560
|
+
<FlatList
|
|
561
|
+
data={pockets}
|
|
562
|
+
keyExtractor={(item) => item.index.toString()}
|
|
563
|
+
renderItem={({ item }) => (
|
|
564
|
+
<View style={{ padding: 15, borderWidth: 1, marginVertical: 5 }}>
|
|
565
|
+
<Text style={{ fontWeight: 'bold' }}>Pocket {item.index}</Text>
|
|
566
|
+
<Text>Address: {item.address.slice(0, 10)}...</Text>
|
|
567
|
+
<Text>
|
|
568
|
+
Balance: {item.balance[0]?.balance.formatted || '0'} ETH
|
|
569
|
+
</Text>
|
|
570
|
+
<Button title="Withdraw" onPress={() => handleWithdraw(item.index)} />
|
|
571
|
+
</View>
|
|
572
|
+
)}
|
|
573
|
+
/>
|
|
574
|
+
|
|
575
|
+
<Button title="Refresh" onPress={loadPockets} />
|
|
576
|
+
</View>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export default PocketsScreen;
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
## Common Operations
|
|
584
|
+
|
|
585
|
+
### Creating a Wallet
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
import { SavingsManager, SavingsOperations } from '@wallet/utils/savings';
|
|
589
|
+
import { generateMnemonic } from 'bip39';
|
|
590
|
+
|
|
591
|
+
// Generate new mnemonic
|
|
592
|
+
const mnemonic = generateMnemonic();
|
|
593
|
+
|
|
594
|
+
// Encrypt and store securely (implementation depends on platform)
|
|
595
|
+
await secureStorage.storeMnemonic(mnemonic, password);
|
|
596
|
+
|
|
597
|
+
// Option 1: Stateful manager
|
|
598
|
+
const manager = new SavingsManager(mnemonic, chainConfig);
|
|
599
|
+
|
|
600
|
+
// Option 2: Stateless operations
|
|
601
|
+
const operations = new SavingsOperations();
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Getting Pocket Address
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
// Stateless
|
|
608
|
+
const address = operations.getPocketAddress(mnemonic, 1, 0);
|
|
609
|
+
console.log(`Pocket 1 address: ${address}`);
|
|
610
|
+
|
|
611
|
+
// Stateful
|
|
612
|
+
const pocket = manager.getPocket(1);
|
|
613
|
+
console.log(`Pocket 1 address: ${pocket.address}`);
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Checking Pocket Balance
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
import { formatUnits } from 'ethers';
|
|
620
|
+
|
|
621
|
+
// Stateless
|
|
622
|
+
const balance = await operations.getPocketBalance(
|
|
623
|
+
mnemonic,
|
|
624
|
+
{ accountIndex: 1, walletIndex: 0 },
|
|
625
|
+
provider
|
|
626
|
+
);
|
|
627
|
+
console.log(`Balance: ${formatUnits(balance, 18)} ETH`);
|
|
628
|
+
|
|
629
|
+
// Stateful
|
|
630
|
+
const balances = await manager.getPocketTokenBalance(
|
|
631
|
+
['0xTokenAddress'], // Token addresses to check
|
|
632
|
+
1 // Pocket index
|
|
633
|
+
);
|
|
634
|
+
console.log(`Native: ${balances[0].balance.formatted}`);
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### Transferring to Pocket
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { parseUnits } from 'ethers';
|
|
641
|
+
import { createWalletClient, http } from 'viem';
|
|
642
|
+
import { mainnet } from 'viem/chains';
|
|
643
|
+
|
|
644
|
+
// Create wallet client for main wallet
|
|
645
|
+
const walletClient = createWalletClient({
|
|
646
|
+
account: mainAccount,
|
|
647
|
+
chain: mainnet,
|
|
648
|
+
transport: http()
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// Transfer to pocket (stateful only)
|
|
652
|
+
const result = await manager.transferToPocket(
|
|
653
|
+
walletClient,
|
|
654
|
+
1, // Pocket index
|
|
655
|
+
'0.1' // Amount as string
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
console.log(`Transaction: ${result.hash}`);
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### Transferring from Pocket
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
// Stateless
|
|
665
|
+
const result = await operations.transferFromPocket(
|
|
666
|
+
mnemonic,
|
|
667
|
+
{
|
|
668
|
+
accountIndex: 1,
|
|
669
|
+
walletIndex: 0,
|
|
670
|
+
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
|
|
671
|
+
amount: parseUnits('0.1', 18)
|
|
672
|
+
},
|
|
673
|
+
provider,
|
|
674
|
+
chain
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
// Stateful
|
|
678
|
+
const result = await manager.sendToMainWallet(
|
|
679
|
+
1, // Pocket index
|
|
680
|
+
100000000000000000, // Amount in wei
|
|
681
|
+
'native' // or token address
|
|
682
|
+
);
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### Transferring Tokens from Pocket
|
|
686
|
+
|
|
687
|
+
```typescript
|
|
688
|
+
// Stateless
|
|
689
|
+
const result = await operations.transferTokenFromPocket(
|
|
690
|
+
mnemonic,
|
|
691
|
+
{
|
|
692
|
+
accountIndex: 1,
|
|
693
|
+
walletIndex: 0,
|
|
694
|
+
tokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
|
|
695
|
+
to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
|
|
696
|
+
amount: parseUnits('100', 6) // 100 USDC (6 decimals)
|
|
697
|
+
},
|
|
698
|
+
provider,
|
|
699
|
+
chain
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
// Stateful
|
|
703
|
+
const result = await manager.sendToMainWallet(
|
|
704
|
+
1, // Pocket index
|
|
705
|
+
100000000, // 100 USDC in base units
|
|
706
|
+
'0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // Token address
|
|
707
|
+
);
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
## Error Handling
|
|
711
|
+
|
|
712
|
+
### Input Validation Errors
|
|
713
|
+
|
|
714
|
+
```typescript
|
|
715
|
+
import { SavingsValidation } from '@wallet/utils/savings/validation';
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
// This will throw if invalid
|
|
719
|
+
SavingsValidation.validateAddress('invalid', 'Recipient');
|
|
720
|
+
SavingsValidation.validateAmount(0n, 'Transfer amount');
|
|
721
|
+
} catch (error) {
|
|
722
|
+
// Handle validation error
|
|
723
|
+
console.error('Validation failed:', error.message);
|
|
724
|
+
// Show user-friendly message
|
|
725
|
+
alert(`Invalid input: ${error.message}`);
|
|
726
|
+
}
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### Transaction Errors
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
try {
|
|
733
|
+
const result = await operations.transferFromPocket(
|
|
734
|
+
mnemonic,
|
|
735
|
+
options,
|
|
736
|
+
provider,
|
|
737
|
+
chain
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
if (!result.status) {
|
|
741
|
+
throw new Error('Transaction failed');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
console.log('Success:', result.hash);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (error.message.includes('insufficient funds')) {
|
|
747
|
+
alert('Insufficient balance in pocket');
|
|
748
|
+
} else if (error.message.includes('user rejected')) {
|
|
749
|
+
alert('Transaction cancelled');
|
|
750
|
+
} else {
|
|
751
|
+
alert(`Transaction failed: ${error.message}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### Authentication Errors
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
try {
|
|
760
|
+
await walletService.initialize();
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (error.message.includes('authentication failed')) {
|
|
763
|
+
alert('Biometric authentication failed. Please try again.');
|
|
764
|
+
} else if (error.message.includes('not found')) {
|
|
765
|
+
// No wallet exists - show onboarding
|
|
766
|
+
navigation.navigate('CreateWallet');
|
|
767
|
+
} else {
|
|
768
|
+
alert(`Error: ${error.message}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
## Testing
|
|
774
|
+
|
|
775
|
+
### Unit Tests
|
|
776
|
+
|
|
777
|
+
```typescript
|
|
778
|
+
import { SavingsOperations } from '@wallet/utils/savings/savings-operations';
|
|
779
|
+
import { SavingsValidation } from '@wallet/utils/savings/validation';
|
|
780
|
+
|
|
781
|
+
describe('Savings Operations', () => {
|
|
782
|
+
const mnemonic = 'test mnemonic phrase...';
|
|
783
|
+
const operations = new SavingsOperations();
|
|
784
|
+
|
|
785
|
+
it('should derive consistent addresses', () => {
|
|
786
|
+
const address1 = operations.getPocketAddress(mnemonic, 0, 0);
|
|
787
|
+
const address2 = operations.getPocketAddress(mnemonic, 0, 0);
|
|
788
|
+
|
|
789
|
+
expect(address1).toBe(address2);
|
|
790
|
+
expect(address1).toMatch(/^0x[a-fA-F0-9]{40}$/);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it('should validate addresses', () => {
|
|
794
|
+
expect(() => {
|
|
795
|
+
SavingsValidation.validateAddress('0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb');
|
|
796
|
+
}).not.toThrow();
|
|
797
|
+
|
|
798
|
+
expect(() => {
|
|
799
|
+
SavingsValidation.validateAddress('invalid');
|
|
800
|
+
}).toThrow();
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
it('should validate amounts', () => {
|
|
804
|
+
expect(() => {
|
|
805
|
+
SavingsValidation.validateAmount(1000n);
|
|
806
|
+
}).not.toThrow();
|
|
807
|
+
|
|
808
|
+
expect(() => {
|
|
809
|
+
SavingsValidation.validateAmount(0n);
|
|
810
|
+
}).toThrow('must be positive');
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
### Integration Tests
|
|
816
|
+
|
|
817
|
+
```typescript
|
|
818
|
+
import { SavingsManager } from '@wallet/utils/savings/saving-manager';
|
|
819
|
+
import { JsonRpcProvider } from 'ethers';
|
|
820
|
+
|
|
821
|
+
describe('Savings Manager Integration', () => {
|
|
822
|
+
let manager: SavingsManager;
|
|
823
|
+
const testMnemonic = 'test test test test test test test test test test test junk';
|
|
824
|
+
|
|
825
|
+
beforeEach(() => {
|
|
826
|
+
manager = new SavingsManager(
|
|
827
|
+
testMnemonic,
|
|
828
|
+
{
|
|
829
|
+
chainId: 1,
|
|
830
|
+
name: 'Ethereum',
|
|
831
|
+
rpcUrl: 'https://eth.llamarpc.com'
|
|
832
|
+
},
|
|
833
|
+
0
|
|
834
|
+
);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
afterEach(() => {
|
|
838
|
+
manager.dispose();
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should create pockets with unique addresses', () => {
|
|
842
|
+
const pocket0 = manager.getPocket(0);
|
|
843
|
+
const pocket1 = manager.getPocket(1);
|
|
844
|
+
|
|
845
|
+
expect(pocket0.address).not.toBe(pocket1.address);
|
|
846
|
+
expect(pocket0.derivationPath).not.toBe(pocket1.derivationPath);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it('should verify pocket addresses', () => {
|
|
850
|
+
const pocket = manager.getPocket(0);
|
|
851
|
+
const isValid = manager.verifyPocketAddress(0, pocket.address);
|
|
852
|
+
|
|
853
|
+
expect(isValid).toBe(true);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('should clear sensitive data on dispose', () => {
|
|
857
|
+
manager.dispose();
|
|
858
|
+
|
|
859
|
+
// Should throw after disposal
|
|
860
|
+
expect(() => manager.getPocket(0)).toThrow();
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
## Best Practices Summary
|
|
866
|
+
|
|
867
|
+
1. **Use Stateless for Extensions**: Browser extensions benefit from stateless operations due to background script lifecycle.
|
|
868
|
+
|
|
869
|
+
2. **Use Stateful for Mobile**: Mobile apps can safely use stateful managers with proper lifecycle management.
|
|
870
|
+
|
|
871
|
+
3. **Always Authenticate**: Require biometric/password authentication before sensitive operations.
|
|
872
|
+
|
|
873
|
+
4. **Handle App Lifecycle**: Clear sensitive data when app backgrounds or closes.
|
|
874
|
+
|
|
875
|
+
5. **Validate Inputs**: Use provided validation utilities before operations.
|
|
876
|
+
|
|
877
|
+
6. **Handle Errors Gracefully**: Provide user-friendly error messages.
|
|
878
|
+
|
|
879
|
+
7. **Test Thoroughly**: Write unit and integration tests for security features.
|
|
880
|
+
|
|
881
|
+
8. **Educate Users**: Provide clear instructions and warnings about security.
|
|
882
|
+
|
|
883
|
+
For more information, see [SECURITY.md](./SECURITY.md) for complete security guidance.
|