@unlink-xyz/core 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.
Files changed (185) hide show
  1. package/dist/account/zkAccount.d.ts +38 -0
  2. package/dist/account/zkAccount.d.ts.map +1 -0
  3. package/dist/account/zkAccount.js +128 -0
  4. package/dist/clients/broadcaster.d.ts +33 -0
  5. package/dist/clients/broadcaster.d.ts.map +1 -0
  6. package/dist/clients/broadcaster.js +23 -0
  7. package/dist/clients/http.d.ts +23 -0
  8. package/dist/clients/http.d.ts.map +1 -0
  9. package/dist/clients/http.js +57 -0
  10. package/dist/clients/indexer.d.ts +44 -0
  11. package/dist/clients/indexer.d.ts.map +1 -0
  12. package/dist/clients/indexer.js +67 -0
  13. package/dist/config.d.ts +12 -0
  14. package/dist/config.d.ts.map +1 -0
  15. package/dist/config.js +29 -0
  16. package/dist/core.d.ts +10 -0
  17. package/dist/core.d.ts.map +1 -0
  18. package/dist/core.js +12 -0
  19. package/dist/errors.d.ts +10 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +18 -0
  22. package/dist/index.d.ts +18 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +16 -0
  25. package/dist/key-derivation/babyjubjub.d.ts +9 -0
  26. package/dist/key-derivation/babyjubjub.d.ts.map +1 -0
  27. package/dist/key-derivation/babyjubjub.js +9 -0
  28. package/dist/key-derivation/bech32.d.ts +22 -0
  29. package/dist/key-derivation/bech32.d.ts.map +1 -0
  30. package/dist/key-derivation/bech32.js +86 -0
  31. package/dist/key-derivation/bip32.d.ts +17 -0
  32. package/dist/key-derivation/bip32.d.ts.map +1 -0
  33. package/dist/key-derivation/bip32.js +41 -0
  34. package/dist/key-derivation/bip39.d.ts +22 -0
  35. package/dist/key-derivation/bip39.d.ts.map +1 -0
  36. package/dist/key-derivation/bip39.js +56 -0
  37. package/dist/key-derivation/bytes.d.ts +19 -0
  38. package/dist/key-derivation/bytes.d.ts.map +1 -0
  39. package/dist/key-derivation/bytes.js +92 -0
  40. package/dist/key-derivation/hash.d.ts +3 -0
  41. package/dist/key-derivation/hash.d.ts.map +1 -0
  42. package/dist/key-derivation/hash.js +10 -0
  43. package/dist/key-derivation/index.d.ts +8 -0
  44. package/dist/key-derivation/index.d.ts.map +1 -0
  45. package/dist/key-derivation/index.js +7 -0
  46. package/dist/key-derivation/wallet-node.d.ts +45 -0
  47. package/dist/key-derivation/wallet-node.d.ts.map +1 -0
  48. package/dist/key-derivation/wallet-node.js +109 -0
  49. package/dist/keys.d.ts +22 -0
  50. package/dist/keys.d.ts.map +1 -0
  51. package/dist/keys.js +41 -0
  52. package/dist/prover/config.d.ts +60 -0
  53. package/dist/prover/config.d.ts.map +1 -0
  54. package/dist/prover/config.js +80 -0
  55. package/dist/prover/index.d.ts +2 -0
  56. package/dist/prover/index.d.ts.map +1 -0
  57. package/dist/prover/index.js +1 -0
  58. package/dist/prover/prover.d.ts +59 -0
  59. package/dist/prover/prover.d.ts.map +1 -0
  60. package/dist/prover/prover.js +274 -0
  61. package/dist/prover/registry.d.ts +39 -0
  62. package/dist/prover/registry.d.ts.map +1 -0
  63. package/dist/prover/registry.js +57 -0
  64. package/dist/schema.d.ts +4 -0
  65. package/dist/schema.d.ts.map +1 -0
  66. package/dist/schema.js +14 -0
  67. package/dist/state/ciphertext-store.d.ts +12 -0
  68. package/dist/state/ciphertext-store.d.ts.map +1 -0
  69. package/dist/state/ciphertext-store.js +25 -0
  70. package/dist/state/index.d.ts +3 -0
  71. package/dist/state/index.d.ts.map +1 -0
  72. package/dist/state/index.js +2 -0
  73. package/dist/state/leaf-store.d.ts +17 -0
  74. package/dist/state/leaf-store.d.ts.map +1 -0
  75. package/dist/state/leaf-store.js +35 -0
  76. package/dist/state/merkle/hydrator.d.ts +27 -0
  77. package/dist/state/merkle/hydrator.d.ts.map +1 -0
  78. package/dist/state/merkle/hydrator.js +36 -0
  79. package/dist/state/merkle/index.d.ts +3 -0
  80. package/dist/state/merkle/index.d.ts.map +1 -0
  81. package/dist/state/merkle/index.js +2 -0
  82. package/dist/state/merkle/merkle-tree.d.ts +34 -0
  83. package/dist/state/merkle/merkle-tree.d.ts.map +1 -0
  84. package/dist/state/merkle/merkle-tree.js +104 -0
  85. package/dist/state/merkle-tree.d.ts +34 -0
  86. package/dist/state/merkle-tree.d.ts.map +1 -0
  87. package/dist/state/merkle-tree.js +104 -0
  88. package/dist/state/note-store.d.ts +37 -0
  89. package/dist/state/note-store.d.ts.map +1 -0
  90. package/dist/state/note-store.js +133 -0
  91. package/dist/state/nullifier-store.d.ts +13 -0
  92. package/dist/state/nullifier-store.d.ts.map +1 -0
  93. package/dist/state/nullifier-store.js +21 -0
  94. package/dist/state/records.d.ts +57 -0
  95. package/dist/state/records.d.ts.map +1 -0
  96. package/dist/state/records.js +1 -0
  97. package/dist/state/root-store.d.ts +13 -0
  98. package/dist/state/root-store.d.ts.map +1 -0
  99. package/dist/state/root-store.js +30 -0
  100. package/dist/state/store/ciphertext-store.d.ts +12 -0
  101. package/dist/state/store/ciphertext-store.d.ts.map +1 -0
  102. package/dist/state/store/ciphertext-store.js +25 -0
  103. package/dist/state/store/index.d.ts +10 -0
  104. package/dist/state/store/index.d.ts.map +1 -0
  105. package/dist/state/store/index.js +8 -0
  106. package/dist/state/store/job-store.d.ts +12 -0
  107. package/dist/state/store/job-store.d.ts.map +1 -0
  108. package/dist/state/store/job-store.js +118 -0
  109. package/dist/state/store/jobs.d.ts +50 -0
  110. package/dist/state/store/jobs.d.ts.map +1 -0
  111. package/dist/state/store/jobs.js +1 -0
  112. package/dist/state/store/leaf-store.d.ts +17 -0
  113. package/dist/state/store/leaf-store.d.ts.map +1 -0
  114. package/dist/state/store/leaf-store.js +35 -0
  115. package/dist/state/store/note-store.d.ts +38 -0
  116. package/dist/state/store/note-store.d.ts.map +1 -0
  117. package/dist/state/store/note-store.js +142 -0
  118. package/dist/state/store/nullifier-store.d.ts +17 -0
  119. package/dist/state/store/nullifier-store.d.ts.map +1 -0
  120. package/dist/state/store/nullifier-store.js +30 -0
  121. package/dist/state/store/records.d.ts +57 -0
  122. package/dist/state/store/records.d.ts.map +1 -0
  123. package/dist/state/store/records.js +1 -0
  124. package/dist/state/store/root-store.d.ts +13 -0
  125. package/dist/state/store/root-store.d.ts.map +1 -0
  126. package/dist/state/store/root-store.js +30 -0
  127. package/dist/state/store/store.d.ts +34 -0
  128. package/dist/state/store/store.d.ts.map +1 -0
  129. package/dist/state/store/store.js +22 -0
  130. package/dist/state/store.d.ts +26 -0
  131. package/dist/state/store.d.ts.map +1 -0
  132. package/dist/state/store.js +19 -0
  133. package/dist/storage/index.d.ts +4 -0
  134. package/dist/storage/index.d.ts.map +1 -0
  135. package/dist/storage/index.js +2 -0
  136. package/dist/storage/indexeddb.d.ts +27 -0
  137. package/dist/storage/indexeddb.d.ts.map +1 -0
  138. package/dist/storage/indexeddb.js +205 -0
  139. package/dist/storage/memory.d.ts +25 -0
  140. package/dist/storage/memory.d.ts.map +1 -0
  141. package/dist/storage/memory.js +87 -0
  142. package/dist/transactions/deposit.d.ts +18 -0
  143. package/dist/transactions/deposit.d.ts.map +1 -0
  144. package/dist/transactions/deposit.js +173 -0
  145. package/dist/transactions/index.d.ts +7 -0
  146. package/dist/transactions/index.d.ts.map +1 -0
  147. package/dist/transactions/index.js +4 -0
  148. package/dist/transactions/note-sync.d.ts +46 -0
  149. package/dist/transactions/note-sync.d.ts.map +1 -0
  150. package/dist/transactions/note-sync.js +320 -0
  151. package/dist/transactions/reconcile.d.ts +22 -0
  152. package/dist/transactions/reconcile.d.ts.map +1 -0
  153. package/dist/transactions/reconcile.js +39 -0
  154. package/dist/transactions/transact.d.ts +34 -0
  155. package/dist/transactions/transact.d.ts.map +1 -0
  156. package/dist/transactions/transact.js +561 -0
  157. package/dist/transactions/types.d.ts +114 -0
  158. package/dist/transactions/types.d.ts.map +1 -0
  159. package/dist/transactions/types.js +1 -0
  160. package/dist/tsconfig.tsbuildinfo +1 -0
  161. package/dist/types.d.ts +27 -0
  162. package/dist/types.d.ts.map +1 -0
  163. package/dist/types.js +1 -0
  164. package/dist/utils/async.d.ts +10 -0
  165. package/dist/utils/async.d.ts.map +1 -0
  166. package/dist/utils/async.js +13 -0
  167. package/dist/utils/bigint.d.ts +9 -0
  168. package/dist/utils/bigint.d.ts.map +1 -0
  169. package/dist/utils/bigint.js +29 -0
  170. package/dist/utils/crypto.d.ts +12 -0
  171. package/dist/utils/crypto.d.ts.map +1 -0
  172. package/dist/utils/crypto.js +39 -0
  173. package/dist/utils/json-codec.d.ts +9 -0
  174. package/dist/utils/json-codec.d.ts.map +1 -0
  175. package/dist/utils/json-codec.js +25 -0
  176. package/dist/utils/polling.d.ts +7 -0
  177. package/dist/utils/polling.d.ts.map +1 -0
  178. package/dist/utils/polling.js +6 -0
  179. package/dist/utils/signature.d.ts +9 -0
  180. package/dist/utils/signature.d.ts.map +1 -0
  181. package/dist/utils/signature.js +12 -0
  182. package/dist/utils/validators.d.ts +30 -0
  183. package/dist/utils/validators.d.ts.map +1 -0
  184. package/dist/utils/validators.js +70 -0
  185. package/package.json +52 -0
@@ -0,0 +1,173 @@
1
+ import { Interface } from "ethers";
2
+ import { v4 as uuidv4 } from "uuid";
3
+ import { createIndexerClient } from "../clients/indexer.js";
4
+ import { serviceConfig } from "../config.js";
5
+ import { ByteLength, ByteUtils } from "../key-derivation/bytes.js";
6
+ import { createMerkleTrees, DEFAULT_JOB_TIMEOUT_MS, rebuildTreeFromStore, } from "../state/index.js";
7
+ import { isNotFoundError, sleep } from "../utils/async.js";
8
+ import { parseHexToBigInt } from "../utils/bigint.js";
9
+ import { computeCommitment, deriveNpk, encryptNote } from "../utils/crypto.js";
10
+ import { DEFAULT_POLL_INTERVAL_MS, DEFAULT_POLL_TIMEOUT_MS, MAX_POLL_INTERVAL_MS, } from "../utils/polling.js";
11
+ import { ensureAddress, ensureChainId } from "../utils/validators.js";
12
+ export const DEPOSIT_ABI = [
13
+ "function deposit(address _depositor, (uint256 npk, uint256 amount, address token)[] notes, (uint256[3] data)[] ciphertexts)",
14
+ ];
15
+ const depositInterface = new Interface(DEPOSIT_ABI);
16
+ function validateDepositRequest(request) {
17
+ ensureChainId(request.chainId);
18
+ ensureAddress("pool address", request.poolAddress);
19
+ ensureAddress("depositor", request.depositor);
20
+ ensureAddress("token", request.note.token);
21
+ if (request.note.mpk < 0n) {
22
+ throw new Error("mpk must be non-negative");
23
+ }
24
+ if (request.note.random < 0n) {
25
+ throw new Error("random must be non-negative");
26
+ }
27
+ if (request.note.amount < 0n) {
28
+ throw new Error("amount must be non-negative");
29
+ }
30
+ const tokenScalar = BigInt(request.note.token);
31
+ if (tokenScalar < 0n) {
32
+ throw new Error("token must map to non-negative scalar");
33
+ }
34
+ }
35
+ export function createDepositClient(stateStore, options) {
36
+ if (!stateStore) {
37
+ throw new Error("stateStore dependency is required");
38
+ }
39
+ if (!options?.fetch) {
40
+ throw new Error("fetch dependency is required");
41
+ }
42
+ const merkleTrees = options.merkleTrees ?? createMerkleTrees();
43
+ const indexerClient = options.indexerClient ??
44
+ createIndexerClient(serviceConfig.indexerBaseUrl, {
45
+ fetch: options.fetch,
46
+ });
47
+ function buildCalldata(request, npk) {
48
+ return depositInterface.encodeFunctionData("deposit", [
49
+ request.depositor,
50
+ [
51
+ // TODO(julienbrs) handle multiple notes
52
+ {
53
+ npk: npk,
54
+ amount: request.note.amount,
55
+ token: request.note.token,
56
+ },
57
+ ],
58
+ [
59
+ {
60
+ data: encryptNote(request.note).data,
61
+ },
62
+ ],
63
+ ]);
64
+ }
65
+ async function persistDepositSuccess(job, record, indexedRoot) {
66
+ await Promise.all([
67
+ stateStore.putLeaf({
68
+ chainId: job.chainId,
69
+ index: record.index,
70
+ commitment: record.commitment,
71
+ }),
72
+ stateStore.putRoot({
73
+ chainId: job.chainId,
74
+ root: indexedRoot,
75
+ }),
76
+ stateStore.putPendingJob({
77
+ ...job,
78
+ status: "succeeded",
79
+ lastCheckedAt: Date.now(),
80
+ txHash: record.txHash ?? job.txHash ?? null,
81
+ predictedCommitment: {
82
+ ...job.predictedCommitment,
83
+ index: record.index,
84
+ root: indexedRoot,
85
+ },
86
+ }),
87
+ ]);
88
+ }
89
+ return {
90
+ async request(request) {
91
+ validateDepositRequest(request);
92
+ const npk = deriveNpk(request);
93
+ const commitmentValue = computeCommitment(request, npk);
94
+ const commitmentHex = ByteUtils.nToHex(commitmentValue, ByteLength.UINT_256, true);
95
+ const calldata = buildCalldata(request, npk);
96
+ const relayId = uuidv4();
97
+ const job = {
98
+ relayId,
99
+ kind: "deposit",
100
+ chainId: request.chainId,
101
+ status: "pending",
102
+ broadcasterRelayId: null,
103
+ txHash: null,
104
+ createdAt: Date.now(),
105
+ timeoutMs: DEFAULT_JOB_TIMEOUT_MS,
106
+ predictedCommitment: {
107
+ hex: commitmentHex,
108
+ },
109
+ };
110
+ await stateStore.putPendingJob(job);
111
+ return { relayId, calldata, commitment: commitmentHex };
112
+ },
113
+ async syncPendingDeposit(relayId) {
114
+ const job = await stateStore.getPendingJob(relayId);
115
+ if (!job || job.kind !== "deposit") {
116
+ throw new Error(`unknown deposit relay ${relayId}`);
117
+ }
118
+ await rebuildTreeFromStore({
119
+ chainId: job.chainId,
120
+ trees: merkleTrees,
121
+ loadLeaf: stateStore.getLeaf.bind(stateStore),
122
+ });
123
+ const commitmentHex = job.predictedCommitment.hex;
124
+ const record = await waitForCommitment(job.chainId, commitmentHex);
125
+ const { index, root: indexedRoot } = record;
126
+ const commitmentValue = parseHexToBigInt(commitmentHex);
127
+ const { index: localIndex, root: localRoot } = merkleTrees.addLeaf(job.chainId, commitmentValue);
128
+ // TODO: if local state drift detected, need to re-sync since last correct state
129
+ if (localIndex !== index) {
130
+ throw new Error("local merkle tree desynchronized (index mismatch), local index: " +
131
+ localIndex +
132
+ ", indexed index: " +
133
+ index);
134
+ }
135
+ if (localRoot.toLowerCase() !== indexedRoot.toLowerCase()) {
136
+ throw new Error("local merkle tree desynchronized (root mismatch), local root: " +
137
+ localRoot +
138
+ ", indexed root: " +
139
+ indexedRoot);
140
+ }
141
+ await persistDepositSuccess(job, record, indexedRoot);
142
+ return {
143
+ chainId: job.chainId,
144
+ index,
145
+ commitment: record.commitment,
146
+ root: indexedRoot,
147
+ };
148
+ },
149
+ };
150
+ async function waitForCommitment(chainId, commitmentHex) {
151
+ const timeout = options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
152
+ const startedAt = Date.now();
153
+ let delay = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
154
+ while (Date.now() - startedAt <= timeout) {
155
+ try {
156
+ const record = await indexerClient.getCommitment({
157
+ chainId,
158
+ commitment: commitmentHex,
159
+ });
160
+ if (record)
161
+ return record;
162
+ }
163
+ catch (err) {
164
+ if (!isNotFoundError(err)) {
165
+ throw err;
166
+ }
167
+ }
168
+ await sleep(delay);
169
+ delay = Math.min(delay * 2, MAX_POLL_INTERVAL_MS);
170
+ }
171
+ throw new Error("commitment not found in indexer before timeout");
172
+ }
173
+ }
@@ -0,0 +1,7 @@
1
+ export { createDepositClient } from "./deposit.js";
2
+ export { createTransactService } from "./transact.js";
3
+ export { createJobReconciler } from "./reconcile.js";
4
+ export { createNoteSyncService } from "./note-sync.js";
5
+ export type { NoteSyncStateStore } from "./note-sync.js";
6
+ export type { DepositRelayResult, DepositNoteInput, DepositRequest, DepositSyncResult, DepositStateStore, Proof, OutputNoteInput, SpendNoteReference, TransactRelayResult, TransactRequest, TransactSyncResult, TransactStateStore, WithdrawalNoteInput, } from "./types.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../transactions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACzD,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,EACL,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,GACpB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { createDepositClient } from "./deposit.js";
2
+ export { createTransactService } from "./transact.js";
3
+ export { createJobReconciler } from "./reconcile.js";
4
+ export { createNoteSyncService } from "./note-sync.js";
@@ -0,0 +1,46 @@
1
+ import type { ZkAccount } from "../account/zkAccount.js";
2
+ import type { FetchLike } from "../clients/http.js";
3
+ import { createIndexerClient } from "../clients/indexer.js";
4
+ import { type LeafRecord, type LocalMerkleTrees, type NoteRecord, type RootRecord } from "../state/index.js";
5
+ export type NoteSyncStateStore = {
6
+ putLeaf(record: LeafRecord): Promise<void>;
7
+ getLeaf(chainId: number, index: number): Promise<LeafRecord | null>;
8
+ clearLeaves(chainId: number): Promise<void>;
9
+ putRoot(record: RootRecord): Promise<void>;
10
+ putNote(record: NoteRecord): Promise<void>;
11
+ getNote(chainId: number, index: number): Promise<NoteRecord | null>;
12
+ markNoteSpent(chainId: number, index: number, spentAt?: number): Promise<NoteRecord>;
13
+ putCiphertext(chainId: number, index: number, payload: Uint8Array): Promise<void>;
14
+ countNullifiers(chainId: number): Promise<number>;
15
+ putNullifier(record: {
16
+ chainId: number;
17
+ nullifier: string;
18
+ noteIndex?: number;
19
+ }): Promise<void>;
20
+ };
21
+ export type NoteSyncOptions = {
22
+ merkleTrees?: LocalMerkleTrees;
23
+ indexerClient?: ReturnType<typeof createIndexerClient>;
24
+ fetch?: FetchLike;
25
+ limit?: number;
26
+ };
27
+ type ChainSyncStatus = {
28
+ inFlight: boolean;
29
+ lastSuccess: number | null;
30
+ lastError?: string;
31
+ };
32
+ export declare function createNoteSyncService(stateStore: NoteSyncStateStore, options?: NoteSyncOptions): {
33
+ sync: (chainId: number, account: ZkAccount, opts?: {
34
+ start?: number;
35
+ forceFullScan?: boolean;
36
+ }) => Promise<NoteRecord[]>;
37
+ syncChain: (chainId: number, account: ZkAccount, opts?: {
38
+ forceFullResync?: boolean;
39
+ }) => Promise<void>;
40
+ syncChains: (chainIds: number[], account: ZkAccount, opts?: {
41
+ forceFullResync?: boolean;
42
+ }) => Promise<void>;
43
+ getStatus: () => Map<number, ChainSyncStatus>;
44
+ };
45
+ export {};
46
+ //# sourceMappingURL=note-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"note-sync.d.ts","sourceRoot":"","sources":["../../transactions/note-sync.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAK5D,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,UAAU,EAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,MAAM,kBAAkB,GAAG;IAC/B,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACpE,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACpE,aAAa,CACX,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,UAAU,CAAC,CAAC;IACvB,aAAa,CACX,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,YAAY,CAAC,MAAM,EAAE;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,aAAa,CAAC,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;IACvD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAqBF,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,kBAAkB,EAC9B,OAAO,GAAE,eAAoB;oBAgUlB,MAAM,WACN,SAAS,SACX;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,OAAO,CAAA;KAAE,KACjD,OAAO,CAAC,UAAU,EAAE,CAAC;yBAnEb,MAAM,WACN,SAAS,SACZ;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,KAClC,OAAO,CAAC,IAAI,CAAC;2BA+CJ,MAAM,EAAE,WACT,SAAS,SACX;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,KACnC,OAAO,CAAC,IAAI,CAAC;;EAqEjB"}
@@ -0,0 +1,320 @@
1
+ import { poseidon } from "@railgun-community/circomlibjs";
2
+ import { createIndexerClient } from "../clients/indexer.js";
3
+ import { serviceConfig } from "../config.js";
4
+ import { CoreError } from "../errors.js";
5
+ import { ByteLength, ByteUtils } from "../key-derivation/bytes.js";
6
+ import { createMerkleTrees, rebuildTreeFromStore, } from "../state/index.js";
7
+ import { formatUint256, parseHexToBigInt } from "../utils/bigint.js";
8
+ import { decryptNote } from "../utils/crypto.js";
9
+ import { ensureChainId } from "../utils/validators.js";
10
+ const DEFAULT_BATCH_SIZE = 256;
11
+ function buildCiphertext(bytes) {
12
+ return {
13
+ data: [
14
+ parseHexToBigInt(bytes[0]),
15
+ parseHexToBigInt(bytes[1]),
16
+ parseHexToBigInt(bytes[2]),
17
+ ],
18
+ };
19
+ }
20
+ function encodeCiphertext(ciphertext) {
21
+ const payload = new Uint8Array(ByteLength.UINT_256 * ciphertext.data.length);
22
+ ciphertext.data.forEach((value, index) => {
23
+ const start = index * ByteLength.UINT_256;
24
+ payload.set(ByteUtils.hexStringToBytes(formatUint256(value)), start);
25
+ });
26
+ return payload;
27
+ }
28
+ export function createNoteSyncService(stateStore, options = {}) {
29
+ const trees = options.merkleTrees ?? createMerkleTrees();
30
+ const limit = options.limit ?? DEFAULT_BATCH_SIZE;
31
+ const status = new Map();
32
+ // Initialize indexer client
33
+ let indexerClient = options.indexerClient;
34
+ if (!indexerClient) {
35
+ const fetchImpl = options.fetch ?? (typeof fetch === "function" ? fetch : undefined);
36
+ if (!fetchImpl) {
37
+ throw new Error("fetch dependency is required to sync indexer commitments");
38
+ }
39
+ indexerClient = createIndexerClient(serviceConfig.indexerBaseUrl, {
40
+ fetch: fetchImpl,
41
+ });
42
+ }
43
+ // Count-based optimization: avoid per-nullifier lookups when counts are unchanged
44
+ async function shouldQueryNullifiers(chainId) {
45
+ const [onChainCount, localCount] = await Promise.all([
46
+ indexerClient.getNullifierCount(chainId),
47
+ stateStore.countNullifiers(chainId),
48
+ ]);
49
+ return onChainCount !== localCount;
50
+ }
51
+ function createNoteRecord(params) {
52
+ const { chainId, index, commitment, token, amount, npk, mpk, random, nullifier, spentAt, } = params;
53
+ const base = {
54
+ chainId,
55
+ index,
56
+ token,
57
+ value: amount.toString(),
58
+ commitment,
59
+ npk: formatUint256(npk),
60
+ mpk: formatUint256(mpk),
61
+ random: formatUint256(random),
62
+ nullifier,
63
+ };
64
+ return spentAt === undefined ? base : { ...base, spentAt };
65
+ }
66
+ async function recordCommitment(chainId, record, allowRelaxedOrder = false) {
67
+ const value = parseHexToBigInt(record.commitment);
68
+ const currentLeafCount = trees.getLeafCount(chainId);
69
+ if (record.index >= currentLeafCount) {
70
+ const { index } = trees.addLeaf(chainId, value);
71
+ if (index !== record.index && !allowRelaxedOrder) {
72
+ throw new CoreError("indexed commitments out of order");
73
+ }
74
+ await stateStore.putLeaf({
75
+ chainId,
76
+ index: record.index,
77
+ commitment: record.commitment,
78
+ });
79
+ }
80
+ await stateStore.putRoot({ chainId, root: record.root });
81
+ }
82
+ async function tryStoreNote(chainId, record, account, shouldCheckNullifiers) {
83
+ const ciphertext = buildCiphertext(record.ciphertext);
84
+ let note;
85
+ try {
86
+ note = decryptNote(ciphertext, account.masterPublicKey);
87
+ }
88
+ catch (err) {
89
+ if (err instanceof CoreError) {
90
+ return null;
91
+ }
92
+ throw err;
93
+ }
94
+ const random = note.random;
95
+ const amount = note.amount;
96
+ const token = note.token;
97
+ const npk = poseidon([account.masterPublicKey, random]);
98
+ const commitment = poseidon([npk, BigInt(token), amount]);
99
+ if (commitment !== parseHexToBigInt(record.commitment)) {
100
+ return null;
101
+ }
102
+ const nullifier = poseidon([account.nullifyingKey, BigInt(record.index)]);
103
+ const nullifierHex = formatUint256(nullifier);
104
+ const ciphertextBytes = encodeCiphertext(ciphertext);
105
+ await stateStore.putCiphertext(chainId, record.index, ciphertextBytes);
106
+ const baseNoteParams = {
107
+ chainId,
108
+ index: record.index,
109
+ commitment: record.commitment,
110
+ token,
111
+ amount,
112
+ npk,
113
+ mpk: account.masterPublicKey,
114
+ random,
115
+ nullifier: nullifierHex,
116
+ };
117
+ // Check if this note already exists in local storage
118
+ const existingNote = await stateStore.getNote(chainId, record.index);
119
+ // If note is already marked as spent locally, don't query indexer
120
+ // to ask for nullifier because we already know it's spent
121
+ if (existingNote?.spentAt !== undefined) {
122
+ // Ensure nullifier is stored for count optimization (idempotent)
123
+ await stateStore.putNullifier({
124
+ chainId,
125
+ nullifier: nullifierHex,
126
+ noteIndex: record.index,
127
+ });
128
+ return existingNote;
129
+ }
130
+ // Skip indexer nullifier query if nullifier counts match
131
+ if (!shouldCheckNullifiers) {
132
+ if (existingNote) {
133
+ return existingNote;
134
+ }
135
+ const stored = createNoteRecord(baseNoteParams);
136
+ await stateStore.putNote(stored);
137
+ return stored;
138
+ }
139
+ // Counts mismatched: query individual nullifier to check if this note was spent
140
+ const onChainNullifier = await indexerClient.getNullifier({
141
+ chainId,
142
+ nullifier: nullifierHex,
143
+ });
144
+ if (onChainNullifier) {
145
+ const spentAtMs = onChainNullifier.spentAt * 1000;
146
+ // Store nullifier
147
+ await stateStore.putNullifier({
148
+ chainId,
149
+ nullifier: nullifierHex,
150
+ noteIndex: record.index,
151
+ });
152
+ if (existingNote) {
153
+ // Update existing unspent note to mark as spent
154
+ await stateStore.markNoteSpent(chainId, record.index, spentAtMs);
155
+ return existingNote;
156
+ }
157
+ const stored = createNoteRecord({
158
+ ...baseNoteParams,
159
+ spentAt: spentAtMs,
160
+ });
161
+ await stateStore.putNote(stored);
162
+ return stored;
163
+ }
164
+ // Skip re-storing unspent notes that already exist
165
+ if (existingNote) {
166
+ return existingNote;
167
+ }
168
+ const stored = createNoteRecord(baseNoteParams);
169
+ await stateStore.putNote(stored);
170
+ return stored;
171
+ }
172
+ // Pulls commitments from the indexer starting at `start`, updating local tree and notes.
173
+ async function ingestFrom(chainId, start, account) {
174
+ const shouldCheckNullifiers = await shouldQueryNullifiers(chainId);
175
+ let cursor = start;
176
+ for (;;) {
177
+ const batch = await indexerClient.fetchCommitmentBatch({
178
+ chainId,
179
+ start: cursor,
180
+ limit,
181
+ });
182
+ if (batch.commitments.length === 0) {
183
+ break;
184
+ }
185
+ for (const record of batch.commitments) {
186
+ const expectedIndex = trees.getLeafCount(chainId);
187
+ if (record.index !== expectedIndex) {
188
+ throw new Error("indexed commitments out of order");
189
+ }
190
+ const value = ByteUtils.hexToBigInt(record.commitment);
191
+ const { index } = trees.addLeaf(chainId, value);
192
+ if (index !== record.index) {
193
+ throw new Error("local merkle tree desynchronized");
194
+ }
195
+ await stateStore.putLeaf({
196
+ chainId,
197
+ index: record.index,
198
+ commitment: record.commitment,
199
+ });
200
+ await stateStore.putRoot({ chainId, root: record.root });
201
+ await tryStoreNote(chainId, record, account, shouldCheckNullifiers);
202
+ cursor = record.index + 1;
203
+ }
204
+ if (batch.commitments.length < limit) {
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ // Clears local state for a chain and rehydrates fully from the indexer.
210
+ async function fullResync(chainId, account) {
211
+ trees.reset(chainId);
212
+ await stateStore.clearLeaves(chainId);
213
+ await ingestFrom(chainId, 0, account);
214
+ }
215
+ /**
216
+ * Syncs a single chain into local state, optionally forcing a full rebuild.
217
+ * Skips overlapping runs via `inFlight` and falls back to full resync on
218
+ * ordering/root inconsistencies.
219
+ */
220
+ async function syncChain(chainId, account, opts = {}) {
221
+ ensureChainId(chainId);
222
+ const current = status.get(chainId) ?? {
223
+ inFlight: false,
224
+ lastSuccess: null,
225
+ };
226
+ if (current.inFlight) {
227
+ return;
228
+ }
229
+ status.set(chainId, { ...current, inFlight: true, lastError: undefined });
230
+ try {
231
+ const start = opts.forceFullResync
232
+ ? 0
233
+ : await rebuildTreeFromStore({
234
+ chainId,
235
+ trees,
236
+ loadLeaf: stateStore.getLeaf.bind(stateStore),
237
+ });
238
+ if (opts.forceFullResync) {
239
+ await fullResync(chainId, account);
240
+ }
241
+ else {
242
+ try {
243
+ await ingestFrom(chainId, start, account);
244
+ }
245
+ catch (err) {
246
+ // Fallback to full resync on ordering/root mismatch.
247
+ await fullResync(chainId, account);
248
+ }
249
+ }
250
+ status.set(chainId, {
251
+ inFlight: false,
252
+ lastSuccess: Date.now(),
253
+ });
254
+ }
255
+ catch (err) {
256
+ status.set(chainId, {
257
+ inFlight: false,
258
+ lastSuccess: current.lastSuccess ?? null,
259
+ lastError: err instanceof Error ? err.message : String(err),
260
+ });
261
+ throw err;
262
+ }
263
+ }
264
+ /**
265
+ * Syncs multiple chains sequentially.
266
+ */
267
+ async function syncChains(chainIds, account, opts) {
268
+ for (const chainId of chainIds) {
269
+ await syncChain(chainId, account, opts);
270
+ }
271
+ }
272
+ /**
273
+ * Legacy sync method that returns synced notes.
274
+ * Kept for backward compatibility.
275
+ */
276
+ async function sync(chainId, account, opts) {
277
+ ensureChainId(chainId);
278
+ await rebuildTreeFromStore({
279
+ chainId,
280
+ trees,
281
+ loadLeaf: stateStore.getLeaf.bind(stateStore),
282
+ });
283
+ const synced = [];
284
+ let start = opts?.start ?? 0;
285
+ const shouldCheckNullifiers = await shouldQueryNullifiers(chainId);
286
+ let keepFetching = true;
287
+ while (keepFetching) {
288
+ const batch = await indexerClient.fetchCommitmentBatch({
289
+ chainId,
290
+ start,
291
+ limit,
292
+ });
293
+ if (batch.commitments.length === 0) {
294
+ keepFetching = false;
295
+ continue;
296
+ }
297
+ for (const record of batch.commitments) {
298
+ await recordCommitment(chainId, record, !!opts?.forceFullScan);
299
+ const storedNote = await tryStoreNote(chainId, record, account, shouldCheckNullifiers);
300
+ if (storedNote) {
301
+ synced.push(storedNote);
302
+ }
303
+ start = record.index + 1;
304
+ }
305
+ if (batch.commitments.length < limit) {
306
+ keepFetching = false;
307
+ }
308
+ }
309
+ return synced;
310
+ }
311
+ function getStatus() {
312
+ return status;
313
+ }
314
+ return {
315
+ sync,
316
+ syncChain,
317
+ syncChains,
318
+ getStatus,
319
+ };
320
+ }
@@ -0,0 +1,22 @@
1
+ import type { JobStatus, PendingJobKind } from "../state/index.js";
2
+ import type { BaseStateStore, DepositSyncResult, TransactSyncResult } from "./types.js";
3
+ type DepositClient = {
4
+ syncPendingDeposit(relayId: string): Promise<DepositSyncResult>;
5
+ };
6
+ type TransactService = {
7
+ syncPendingTransact(relayId: string): Promise<TransactSyncResult>;
8
+ };
9
+ type ReconcilerDeps = {
10
+ stateStore: BaseStateStore;
11
+ depositClient: DepositClient;
12
+ transactService: TransactService;
13
+ };
14
+ export declare function createJobReconciler(deps: ReconcilerDeps): {
15
+ reconcileRelay: (relayId: string) => Promise<DepositSyncResult | TransactSyncResult>;
16
+ reconcileAll: (filter?: {
17
+ kind?: PendingJobKind;
18
+ statuses?: JobStatus[];
19
+ }) => Promise<unknown[]>;
20
+ };
21
+ export {};
22
+ //# sourceMappingURL=reconcile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../transactions/reconcile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,cAAc,EAEf,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EACV,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,KAAK,aAAa,GAAG;IACnB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;CACjE,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACnE,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,UAAU,EAAE,cAAc,CAAC;IAC3B,aAAa,EAAE,aAAa,CAAC;IAC7B,eAAe,EAAE,eAAe,CAAC;CAClC,CAAC;AAIF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,cAAc;8BAUf,MAAM;4BASnC;QACN,IAAI,CAAC,EAAE,cAAc,CAAC;QACtB,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;KACxB;EAuBJ"}
@@ -0,0 +1,39 @@
1
+ const ACTIVE_STATUSES = ["pending", "submitted", "broadcasting"];
2
+ export function createJobReconciler(deps) {
3
+ const { stateStore, depositClient, transactService } = deps;
4
+ async function reconcileJob(job) {
5
+ if (job.kind === "deposit") {
6
+ return depositClient.syncPendingDeposit(job.relayId);
7
+ }
8
+ return transactService.syncPendingTransact(job.relayId);
9
+ }
10
+ async function reconcileRelay(relayId) {
11
+ const job = await stateStore.getPendingJob(relayId);
12
+ if (!job) {
13
+ throw new Error(`unknown relay ${relayId}`);
14
+ }
15
+ return reconcileJob(job);
16
+ }
17
+ async function reconcileAll(filter = {}) {
18
+ const jobs = await stateStore.listPendingJobs({
19
+ kind: filter.kind,
20
+ statuses: filter.statuses ?? ACTIVE_STATUSES,
21
+ });
22
+ const results = [];
23
+ for (const job of jobs) {
24
+ try {
25
+ const res = await reconcileJob(job);
26
+ results.push(res);
27
+ }
28
+ catch (err) {
29
+ // Allow caller to decide how to handle errors; we keep going.
30
+ results.push(err);
31
+ }
32
+ }
33
+ return results;
34
+ }
35
+ return {
36
+ reconcileRelay,
37
+ reconcileAll,
38
+ };
39
+ }
@@ -0,0 +1,34 @@
1
+ import { IMTMerkleProof } from "@zk-kit/imt";
2
+ import { FetchLike } from "../clients/http.js";
3
+ import { LocalMerkleTrees } from "../state/index.js";
4
+ import { TransactRelayResult, TransactRequest, TransactStateStore, TransactSyncResult } from "./types.js";
5
+ export declare const TRANSACT_ABI = "function transact(((uint256[2] pA, uint256[2][2] pB, uint256[2] pC) proof, uint256 merkleRoot, uint256[] nullifierHashes, uint256[] newCommitments, (uint64 chainID, address poolAddress) context, (uint256 npk, uint256 amount, address token) withdrawal, (uint256[3] data)[] ciphertexts)[] _transactions)";
6
+ type TransactServiceOptions = {
7
+ merkleTrees?: LocalMerkleTrees;
8
+ fetch?: FetchLike;
9
+ pollIntervalMs?: number;
10
+ pollTimeoutMs?: number;
11
+ };
12
+ /**
13
+ * Computes the bound parameters hash from chain ID, pool address, and withdrawal parameters.
14
+ * This hash binds the transaction to specific chain and withdrawal context.
15
+ */
16
+ export declare function computeBoundParamsHash(chainId: number, poolAddress: string): bigint;
17
+ export type SerializedWitness = {
18
+ root: string;
19
+ leaf: string;
20
+ pathElements: string[][];
21
+ pathIndices: number[];
22
+ leafIndex: number;
23
+ };
24
+ export declare function serializeWitness(proof: IMTMerkleProof, index: number): SerializedWitness;
25
+ export declare function deserializeWitness(serialized: SerializedWitness): IMTMerkleProof;
26
+ /**
27
+ * Simulates the transact pipeline: builds Merkle witnesses, derives nullifiers, and updates local state.
28
+ */
29
+ export declare function createTransactService(stateStore: TransactStateStore, options?: TransactServiceOptions): {
30
+ transact(request: TransactRequest): Promise<TransactRelayResult>;
31
+ syncPendingTransact(relayId: string): Promise<TransactSyncResult>;
32
+ };
33
+ export {};
34
+ //# sourceMappingURL=transact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transact.d.ts","sourceRoot":"","sources":["../../transactions/transact.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAI7C,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAK/C,OAAO,EAGL,gBAAgB,EAGjB,MAAM,mBAAmB,CAAC;AAqB3B,OAAO,EAGL,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,YAAY,kTACwR,CAAC;AA8ClT,KAAK,sBAAsB,GAAG;IAC5B,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAoCF;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,GAClB,MAAM,CASR;AAED,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC;IACzB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,cAAc,EACrB,KAAK,EAAE,MAAM,GACZ,iBAAiB,CAUnB;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,iBAAiB,GAC5B,cAAc,CAUhB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,kBAAkB,EAC9B,OAAO,GAAE,sBAA2B;sBA0VV,eAAe,GAAG,OAAO,CAAC,mBAAmB,CAAC;iCA+LnC,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;EAoF1E"}