@zerc20/wallet-sdk 0.0.0-stage → 0.2.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.
Files changed (84) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +26 -2
  3. package/dist/burnWorker.d.ts +1 -0
  4. package/dist/burnWorker.js +21 -0
  5. package/dist/burnWorker.js.map +1 -0
  6. package/dist/chain.d.ts +359 -0
  7. package/dist/chain.js +155 -0
  8. package/dist/chain.js.map +1 -0
  9. package/dist/context.d.ts +48 -0
  10. package/dist/context.js +40 -0
  11. package/dist/context.js.map +1 -0
  12. package/dist/core.d.ts +43 -0
  13. package/dist/core.js +106 -0
  14. package/dist/core.js.map +1 -0
  15. package/dist/errors.d.ts +38 -0
  16. package/dist/errors.js +26 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.js +19 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/network.d.ts +66 -0
  22. package/dist/network.js +55 -0
  23. package/dist/network.js.map +1 -0
  24. package/dist/pos.d.ts +93 -0
  25. package/dist/pos.js +302 -0
  26. package/dist/pos.js.map +1 -0
  27. package/dist/prove.d.ts +13 -0
  28. package/dist/prove.js +53 -0
  29. package/dist/prove.js.map +1 -0
  30. package/dist/rageExit.d.ts +5 -0
  31. package/dist/rageExit.js +146 -0
  32. package/dist/rageExit.js.map +1 -0
  33. package/dist/scan.d.ts +17 -0
  34. package/dist/scan.js +47 -0
  35. package/dist/scan.js.map +1 -0
  36. package/dist/seed.d.ts +8 -0
  37. package/dist/seed.js +35 -0
  38. package/dist/seed.js.map +1 -0
  39. package/dist/servers.d.ts +60 -0
  40. package/dist/servers.js +117 -0
  41. package/dist/servers.js.map +1 -0
  42. package/dist/services.d.ts +62 -0
  43. package/dist/services.js +131 -0
  44. package/dist/services.js.map +1 -0
  45. package/dist/spend.d.ts +151 -0
  46. package/dist/spend.js +745 -0
  47. package/dist/spend.js.map +1 -0
  48. package/dist/state.d.ts +174 -0
  49. package/dist/state.js +45 -0
  50. package/dist/state.js.map +1 -0
  51. package/dist/sync.d.ts +84 -0
  52. package/dist/sync.js +615 -0
  53. package/dist/sync.js.map +1 -0
  54. package/dist/tree.d.ts +19 -0
  55. package/dist/tree.js +63 -0
  56. package/dist/tree.js.map +1 -0
  57. package/dist/wallet.d.ts +303 -0
  58. package/dist/wallet.js +1002 -0
  59. package/dist/wallet.js.map +1 -0
  60. package/package.json +60 -4
  61. package/src/burnWorker.ts +25 -0
  62. package/src/chain.ts +228 -0
  63. package/src/context.ts +84 -0
  64. package/src/core.ts +147 -0
  65. package/src/errors.ts +63 -0
  66. package/src/index.ts +41 -0
  67. package/src/network.ts +94 -0
  68. package/src/pos.ts +415 -0
  69. package/src/prove.ts +67 -0
  70. package/src/rageExit.ts +168 -0
  71. package/src/scan.ts +62 -0
  72. package/src/seed.ts +46 -0
  73. package/src/servers.ts +172 -0
  74. package/src/services.ts +195 -0
  75. package/src/snarkjs.d.ts +11 -0
  76. package/src/spend.ts +912 -0
  77. package/src/state.ts +211 -0
  78. package/src/sync.ts +698 -0
  79. package/src/tree.ts +108 -0
  80. package/src/wallet.ts +1316 -0
  81. package/wasm/zerc20_wallet_core.d.ts +176 -0
  82. package/wasm/zerc20_wallet_core.js +783 -0
  83. package/wasm/zerc20_wallet_core_bg.wasm +0 -0
  84. package/wasm/zerc20_wallet_core_bg.wasm.d.ts +32 -0
package/src/state.ts ADDED
@@ -0,0 +1,211 @@
1
+ // What a wallet keeps between sessions: one encrypted blob the app stores wherever it likes. Everything in it can
2
+ // be rebuilt from the seed and the account's address (given time and a birthday); it is a cache that keeps every
3
+ // sync incremental, plus the operations in flight, which are written before each step that leaves the device.
4
+
5
+ import { chacha20poly1305 } from '@noble/ciphers/chacha.js';
6
+ import { hkdf } from '@noble/hashes/hkdf.js';
7
+ import { sha256 } from '@noble/hashes/sha2.js';
8
+ import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils.js';
9
+ import type { Receipt, SyncState } from '@zerc20/storage-sdk';
10
+ import type { Hex } from 'viem';
11
+ import type { ErrorCode } from './errors.js';
12
+ import type { Watch } from './scan.js';
13
+ import type { SpendData } from './spend.js';
14
+
15
+ /** Where a note or deposit sits in the commitment tree. */
16
+ export interface Position {
17
+ index: number;
18
+ /** Seen past the final count: a reorg may still move it. */
19
+ tentative: boolean;
20
+ /** Set once spent: by this wallet, or seen among the nullifiers. */
21
+ spent: boolean;
22
+ /** Looking for its nullifier, once final. */
23
+ nullifier?: Watch;
24
+ /** Its block's time (ms), read with its commitment: the same on every device. Unset until read. */
25
+ atMs?: number;
26
+ }
27
+
28
+ /** What a payment, withdrawal or merge was, kept on its change or merged note in storage (the storage SDK's
29
+ * `Receipt`): every device of the wallet shows it once that note lands. */
30
+ export interface NoteReceipt {
31
+ kind: Receipt['kind'];
32
+ amount: bigint;
33
+ fee: bigint;
34
+ /** Whom it paid: an address, or a receive code's burn address; zero for a merge. */
35
+ counterparty: Hex;
36
+ }
37
+
38
+ /** A note this wallet may own: announced through storage (to its address or one of its receive codes), or a note to
39
+ * self. */
40
+ export interface NoteRecord {
41
+ chainId: bigint;
42
+ tokenId: number;
43
+ value: bigint;
44
+ salt: Hex;
45
+ source: 'received' | 'self';
46
+ /** For a note to self, what it is the change or the output of. */
47
+ receipt?: NoteReceipt;
48
+ /** Who the payer claimed to be (unverified), if anyone. */
49
+ peer: Hex | null;
50
+ /** When storage first held it (ns), for notes it delivered. */
51
+ atNs: bigint | null;
52
+ /** For a note paid to a receive code, the code's index: the code's own key owns it. Otherwise the wallet's key does. */
53
+ keyIndex?: number;
54
+ /** Its hash under the key that owns it. */
55
+ noteHash: Hex;
56
+ /** Its tag, watched until it is found where no reorg moves it. */
57
+ watch: Watch | null;
58
+ /** Found in the tree, and `confirmed` once its commitment was read: a 4-byte tag can match someone else's. */
59
+ found: (Position & { confirmed?: boolean })[];
60
+ }
61
+
62
+ /** A burn address of this wallet and what was sent to it. */
63
+ export interface DepositRecord {
64
+ chainId: bigint;
65
+ /** The token it was handed out for, whose burn indices it counts in. Transfers of any token to it are found and
66
+ * spendable. */
67
+ tokenId: number;
68
+ burnIndex: number;
69
+ salt: Hex;
70
+ address: Hex;
71
+ createdMs: number;
72
+ /** A deposit-relayer address that flushes into it, if one was registered. */
73
+ depositAddress: Hex | null;
74
+ /** Handed out by one of this wallet's points of sale, which recorded it in its lane (a burn block, `burnIndex` -1):
75
+ * paid by deposit only. The wallet's own are recorded in its own lane, from 0 up, and each is also a receive code. */
76
+ managed?: true;
77
+ /** Chosen by a payer, who told it in storage (a burn), as claimed: its salt is the payer's, with no index
78
+ * (`burnIndex` -1), and it is never handed out. */
79
+ payer?: Hex;
80
+ /** The transfer's tag under each token. */
81
+ watches: { tokenId: number; watch: Watch }[];
82
+ found: (Position & {
83
+ tokenId: number;
84
+ sender: Hex | null;
85
+ amount: bigint | null;
86
+ /** The ASP's decision, once it has made one: only an approved deposit is spent privately. */
87
+ asp: 'approved' | 'rejected' | null;
88
+ /** Until then, when it should come (ms): an estimate. */
89
+ decidedByMs?: number;
90
+ })[];
91
+ }
92
+
93
+ /** A deposit this wallet paid into someone else's burn address (`deposit({ to })`), as storage keeps it for every
94
+ * device of the wallet: its transfer is watched as a deposit's is, to show once it landed, and never spent. */
95
+ export interface SentRecord extends Pick<
96
+ DepositRecord,
97
+ 'chainId' | 'tokenId' | 'address' | 'createdMs' | 'watches' | 'found'
98
+ > {
99
+ /** Whom it paid. */
100
+ to: Hex;
101
+ }
102
+
103
+ /** What this wallet did that leaves no note to self, kept in storage's own lane so that every device of the wallet
104
+ * shows it: a deposit returned to its sender by rage exit, or a deposit address registered for a burn address.
105
+ * `unwritten` until storage holds it: every sync tries again. */
106
+ export type ReceiptRecord = { chainId: bigint; tokenId: number; burnAddress: Hex; atMs: number; unwritten?: true } & (
107
+ | { kind: 'returned'; index: number; recipient: Hex; amount: bigint; txHash: Hex }
108
+ | { kind: 'deposit-address'; depositAddress: Hex }
109
+ );
110
+
111
+ /** A sample of the indexer's final counts, to bound where a note announced at some time can be. */
112
+ export interface Sample {
113
+ atMs: number;
114
+ commitments: number;
115
+ nullifiers: number;
116
+ }
117
+
118
+ export interface ChainState {
119
+ chainId: bigint;
120
+ samples: Sample[];
121
+ /** The next burn address index, per token. */
122
+ burnNext: Record<number, number>;
123
+ }
124
+
125
+ /** An operation's progress, written before every step that leaves the device, so `sync` can resume it. */
126
+ export type OperationRecord = DepositOperation | SpendOperation;
127
+
128
+ interface OperationBase {
129
+ id: string;
130
+ chainId: bigint;
131
+ tokenId: number;
132
+ amount: bigint;
133
+ createdMs: number;
134
+ status: 'pending' | 'done' | 'failed';
135
+ error?: string;
136
+ /** Why it failed, for the app to act on. */
137
+ errorCode?: ErrorCode;
138
+ /** While pending: why its next step keeps not going through, tried again every sync, and since when. */
139
+ retrying?: { message: string; code?: ErrorCode; sinceMs: number };
140
+ }
141
+
142
+ /** A deposit from the account into a burn address. */
143
+ export interface DepositOperation extends OperationBase {
144
+ kind: 'deposit';
145
+ data: {
146
+ burnAddress: Hex;
147
+ /** Whose burn address it is, when not this wallet's (`deposit({ to })`). */
148
+ to?: Hex;
149
+ /** Its transaction, once sent. */
150
+ txHash?: Hex;
151
+ };
152
+ }
153
+
154
+ /** JoinSplits, one step at a time: a send or a withdrawal's one, a consolidation's rounds. */
155
+ export interface SpendOperation extends OperationBase {
156
+ kind: 'send' | 'withdraw' | 'consolidate';
157
+ data: SpendData;
158
+ }
159
+
160
+ export interface WalletState {
161
+ version: 1;
162
+ address: Hex;
163
+ /** The note storage history, `null` until its first refresh. */
164
+ storage: SyncState | null;
165
+ chains: ChainState[];
166
+ notes: NoteRecord[];
167
+ deposits: DepositRecord[];
168
+ sent: SentRecord[];
169
+ receipts: ReceiptRecord[];
170
+ ops: OperationRecord[];
171
+ }
172
+
173
+ export const emptyWalletState = (address: Hex): WalletState => ({
174
+ version: 1,
175
+ address,
176
+ storage: null,
177
+ chains: [],
178
+ notes: [],
179
+ deposits: [],
180
+ sent: [],
181
+ receipts: [],
182
+ ops: [],
183
+ });
184
+
185
+ const TAG = new TextEncoder().encode('zerc20/wallet/state/v1');
186
+ const keyOf = (seed: Uint8Array) => hkdf(sha256, seed, TAG, new TextEncoder().encode('wallet-state'), 32);
187
+
188
+ /** `state` as bytes only the seed opens: `nonce(12) ‖ ChaCha20-Poly1305(JSON)`. */
189
+ export function sealState(seed: Uint8Array, state: WalletState): Uint8Array {
190
+ const json = JSON.stringify(state, (_, v: unknown) =>
191
+ typeof v === 'bigint' ? { $n: v.toString() } : v instanceof Uint8Array ? { $b: bytesToHex(v) } : v,
192
+ );
193
+ const nonce = randomBytes(12);
194
+ const sealed = chacha20poly1305(keyOf(seed), nonce, TAG).encrypt(new TextEncoder().encode(json));
195
+ return new Uint8Array([...nonce, ...sealed]);
196
+ }
197
+
198
+ /** The state `sealState` wrote, or `null` if `bytes` is not one this seed opens. */
199
+ export function openState(seed: Uint8Array, bytes: Uint8Array): WalletState | null {
200
+ try {
201
+ const plain = chacha20poly1305(keyOf(seed), bytes.subarray(0, 12), TAG).decrypt(bytes.subarray(12));
202
+ const state = JSON.parse(new TextDecoder().decode(plain), (_, v: unknown) => {
203
+ if (typeof v === 'object' && v !== null && '$n' in v) return BigInt(v.$n as string);
204
+ if (typeof v === 'object' && v !== null && '$b' in v) return hexToBytes(v.$b as string);
205
+ return v;
206
+ }) as WalletState;
207
+ return state.version === 1 ? state : null;
208
+ } catch {
209
+ return null;
210
+ }
211
+ }