@unlink-xyz/core 0.1.0 → 0.1.2

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 (110) hide show
  1. package/.eslintrc.json +4 -0
  2. package/account/zkAccount.test.ts +316 -0
  3. package/account/zkAccount.ts +222 -0
  4. package/clients/broadcaster.ts +67 -0
  5. package/clients/http.ts +94 -0
  6. package/clients/indexer.ts +150 -0
  7. package/config.ts +39 -0
  8. package/core.ts +17 -0
  9. package/dist/account/railgun-imports-prototype.d.ts +12 -0
  10. package/dist/account/railgun-imports-prototype.d.ts.map +1 -0
  11. package/dist/account/railgun-imports-prototype.js +30 -0
  12. package/dist/clients/indexer.d.ts.map +1 -1
  13. package/dist/clients/indexer.js +1 -1
  14. package/dist/state/hydrator.d.ts +16 -0
  15. package/dist/state/hydrator.d.ts.map +1 -0
  16. package/dist/state/hydrator.js +18 -0
  17. package/dist/state/job-store.d.ts +12 -0
  18. package/dist/state/job-store.d.ts.map +1 -0
  19. package/dist/state/job-store.js +118 -0
  20. package/dist/state/jobs.d.ts +50 -0
  21. package/dist/state/jobs.d.ts.map +1 -0
  22. package/dist/state/jobs.js +1 -0
  23. package/dist/state.d.ts +83 -0
  24. package/dist/state.d.ts.map +1 -0
  25. package/dist/state.js +171 -0
  26. package/dist/transactions/deposit.d.ts +0 -2
  27. package/dist/transactions/deposit.d.ts.map +1 -1
  28. package/dist/transactions/deposit.js +5 -9
  29. package/dist/transactions/note-sync.d.ts.map +1 -1
  30. package/dist/transactions/note-sync.js +1 -1
  31. package/dist/transactions/shield.d.ts +5 -0
  32. package/dist/transactions/shield.d.ts.map +1 -0
  33. package/dist/transactions/shield.js +93 -0
  34. package/dist/transactions/transact.d.ts +0 -5
  35. package/dist/transactions/transact.d.ts.map +1 -1
  36. package/dist/transactions/transact.js +2 -2
  37. package/dist/transactions/utils.d.ts +10 -0
  38. package/dist/transactions/utils.d.ts.map +1 -0
  39. package/dist/transactions/utils.js +17 -0
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/dist/utils/time.d.ts +2 -0
  42. package/dist/utils/time.d.ts.map +1 -0
  43. package/dist/utils/time.js +3 -0
  44. package/dist/utils/witness.d.ts +11 -0
  45. package/dist/utils/witness.d.ts.map +1 -0
  46. package/dist/utils/witness.js +19 -0
  47. package/errors.ts +20 -0
  48. package/index.ts +17 -0
  49. package/key-derivation/babyjubjub.ts +11 -0
  50. package/key-derivation/bech32.test.ts +90 -0
  51. package/key-derivation/bech32.ts +124 -0
  52. package/key-derivation/bip32.ts +56 -0
  53. package/key-derivation/bip39.ts +76 -0
  54. package/key-derivation/bytes.ts +118 -0
  55. package/key-derivation/hash.ts +13 -0
  56. package/key-derivation/index.ts +7 -0
  57. package/key-derivation/wallet-node.ts +155 -0
  58. package/keys.ts +47 -0
  59. package/package.json +4 -5
  60. package/prover/config.ts +104 -0
  61. package/prover/index.ts +1 -0
  62. package/prover/prover.integration.test.ts +162 -0
  63. package/prover/prover.test.ts +309 -0
  64. package/prover/prover.ts +405 -0
  65. package/prover/registry.test.ts +90 -0
  66. package/prover/registry.ts +82 -0
  67. package/schema.ts +17 -0
  68. package/setup-artifacts.sh +57 -0
  69. package/state/index.ts +2 -0
  70. package/state/merkle/hydrator.ts +69 -0
  71. package/state/merkle/index.ts +12 -0
  72. package/state/merkle/merkle-tree.test.ts +50 -0
  73. package/state/merkle/merkle-tree.ts +163 -0
  74. package/state/store/ciphertext-store.ts +28 -0
  75. package/state/store/index.ts +24 -0
  76. package/state/store/job-store.ts +162 -0
  77. package/state/store/jobs.ts +64 -0
  78. package/state/store/leaf-store.ts +39 -0
  79. package/state/store/note-store.ts +177 -0
  80. package/state/store/nullifier-store.ts +39 -0
  81. package/state/store/records.ts +61 -0
  82. package/state/store/root-store.ts +34 -0
  83. package/state/store/store.ts +25 -0
  84. package/state.test.ts +235 -0
  85. package/storage/index.ts +3 -0
  86. package/storage/indexeddb.test.ts +99 -0
  87. package/storage/indexeddb.ts +235 -0
  88. package/storage/memory.test.ts +59 -0
  89. package/storage/memory.ts +93 -0
  90. package/transactions/deposit.test.ts +160 -0
  91. package/transactions/deposit.ts +227 -0
  92. package/transactions/index.ts +20 -0
  93. package/transactions/note-sync.test.ts +155 -0
  94. package/transactions/note-sync.ts +452 -0
  95. package/transactions/reconcile.ts +73 -0
  96. package/transactions/transact.test.ts +451 -0
  97. package/transactions/transact.ts +811 -0
  98. package/transactions/types.ts +141 -0
  99. package/tsconfig.json +14 -0
  100. package/types/global.d.ts +15 -0
  101. package/types.ts +24 -0
  102. package/utils/async.ts +15 -0
  103. package/utils/bigint.ts +34 -0
  104. package/utils/crypto.test.ts +69 -0
  105. package/utils/crypto.ts +58 -0
  106. package/utils/json-codec.ts +38 -0
  107. package/utils/polling.ts +6 -0
  108. package/utils/signature.ts +16 -0
  109. package/utils/validators.test.ts +64 -0
  110. package/utils/validators.ts +86 -0
@@ -0,0 +1,69 @@
1
+ import { parseHexToBigInt } from "../../utils/bigint.js";
2
+
3
+ export type LeafLoader = (
4
+ chainId: number,
5
+ index: number,
6
+ ) => Promise<{ commitment: string } | null>;
7
+
8
+ export async function rebuildTreeFromStore({
9
+ chainId,
10
+ trees,
11
+ loadLeaf,
12
+ }: {
13
+ chainId: number;
14
+ trees: {
15
+ reset(chainId: number): void;
16
+ addLeaf(chainId: number, value: bigint): { index: number };
17
+ getLeafCount?(chainId: number): number;
18
+ };
19
+ loadLeaf: LeafLoader;
20
+ }) {
21
+ trees.reset(chainId);
22
+ let idx = 0;
23
+ for (;;) {
24
+ const leaf = await loadLeaf(chainId, idx);
25
+ if (!leaf) break;
26
+ const { index } = trees.addLeaf(chainId, parseHexToBigInt(leaf.commitment));
27
+ if (index !== idx) {
28
+ throw new Error(
29
+ `stored leaves are inconsistent with local tree, expected ${idx}, got ${index}`,
30
+ );
31
+ }
32
+ idx += 1;
33
+ }
34
+ return idx;
35
+ }
36
+
37
+ export type HydrateChainParams = {
38
+ chainId: number;
39
+ trees: {
40
+ reset(chainId: number): void;
41
+ addLeaf(chainId: number, value: bigint): { index: number };
42
+ };
43
+ loadLeaf: LeafLoader;
44
+ hydrated: Set<number>;
45
+ };
46
+
47
+ // TODO: Will be used for inmemory merkle tree handling for caching.
48
+ // Atm everytime we need to access the tree we have to rebuild it from the store.
49
+ // This function will help to avoid multiple rebuilds for the same chain
50
+ export async function hydrateChain({
51
+ chainId,
52
+ trees,
53
+ loadLeaf,
54
+ hydrated,
55
+ }: HydrateChainParams) {
56
+ if (hydrated.has(chainId)) return;
57
+ trees.reset(chainId);
58
+ let idx = 0;
59
+ for (;;) {
60
+ const leaf = await loadLeaf(chainId, idx);
61
+ if (!leaf) break;
62
+ const { index } = trees.addLeaf(chainId, parseHexToBigInt(leaf.commitment));
63
+ if (index !== idx) {
64
+ throw new Error("stored leaves are inconsistent with local tree");
65
+ }
66
+ idx += 1;
67
+ }
68
+ hydrated.add(chainId);
69
+ }
@@ -0,0 +1,12 @@
1
+ export {
2
+ createLocalMerkleTree,
3
+ createMerkleTrees,
4
+ type LocalMerkleTree,
5
+ type LocalMerkleTrees,
6
+ type MerkleProof,
7
+ } from "./merkle-tree.js";
8
+ export {
9
+ hydrateChain,
10
+ rebuildTreeFromStore,
11
+ type HydrateChainParams,
12
+ } from "./hydrator.js";
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { createLocalMerkleTree } from "./merkle-tree.js";
4
+
5
+ describe("local merkle tree", () => {
6
+ it("returns deterministic roots for identical insert sequences", () => {
7
+ const inserts = [0x01n, 0x02n, 0x03n];
8
+
9
+ const treeA = createLocalMerkleTree();
10
+ const rootsA = inserts.map((leaf) => treeA.addLeaf(leaf).root);
11
+
12
+ const treeB = createLocalMerkleTree();
13
+ const rootsB = inserts.map((leaf) => treeB.addLeaf(leaf).root);
14
+
15
+ expect(rootsA).toEqual(rootsB);
16
+ expect(treeA.getRoot()).toBe(treeB.getRoot());
17
+ });
18
+
19
+ it("tracks inserted leaf count and updates root", () => {
20
+ const tree = createLocalMerkleTree();
21
+ const initialRoot = tree.getRoot();
22
+
23
+ const first = tree.addLeaf(1n);
24
+ expect(tree.getLeafCount()).toBe(1);
25
+ expect(first.index).toBe(0);
26
+ expect(first.root).not.toBe(initialRoot);
27
+
28
+ const second = tree.addLeaf(0x02n);
29
+ expect(tree.getLeafCount()).toBe(2);
30
+ expect(second.index).toBe(1);
31
+ expect(second.root).not.toBe(first.root);
32
+ });
33
+
34
+ it("exposes zero hashes for valid levels and rejects invalid ones", () => {
35
+ const tree = createLocalMerkleTree();
36
+
37
+ expect(tree.getZero(0)).toMatch(/^0x[0-9a-f]+$/);
38
+ expect(() => tree.getZero(-1)).toThrow("invalid level");
39
+ expect(() => tree.getZero(tree.depth + 1)).toThrow("invalid level");
40
+ });
41
+
42
+ it("starts with zero root until the first insert", () => {
43
+ const tree = createLocalMerkleTree();
44
+ const emptyRoot = tree.getRoot();
45
+
46
+ expect(emptyRoot).toBe(tree.getZero(tree.depth));
47
+ tree.addLeaf(5n);
48
+ expect(tree.getRoot()).not.toBe(emptyRoot);
49
+ });
50
+ });
@@ -0,0 +1,163 @@
1
+ import { poseidon } from "@railgun-community/circomlibjs";
2
+ import {
3
+ IMT,
4
+ IMTMerkleProof,
5
+ type IMTHashFunction,
6
+ type IMTNode,
7
+ } from "@zk-kit/imt";
8
+
9
+ import { ByteLength, ByteUtils } from "../../key-derivation/bytes.js";
10
+ import { ensureChainId } from "../../utils/validators.js";
11
+
12
+ type BigIntTree = IMT & {
13
+ root: bigint;
14
+ zeroes: bigint[];
15
+ leaves: bigint[];
16
+ };
17
+
18
+ export type AddLeafResult = {
19
+ index: number;
20
+ root: string;
21
+ };
22
+
23
+ export type MerkleProof = {
24
+ root: string;
25
+ leaf: string;
26
+ pathElements: string[][];
27
+ pathIndices: number[];
28
+ leafIndex: number;
29
+ };
30
+
31
+ const DEFAULT_DEPTH = 16;
32
+ const DEFAULT_ARITY = 2;
33
+ const ZERO_BYTE_LENGTH = ByteLength.UINT_256;
34
+
35
+ // Simple adapter: IMTNode[] -> poseidon(bigint[])
36
+ const hash: IMTHashFunction = (values: IMTNode[]) =>
37
+ poseidon(values.map((v) => BigInt(v)));
38
+
39
+ const formatNode = (value: bigint) =>
40
+ ByteUtils.nToHex(value, ZERO_BYTE_LENGTH, true);
41
+
42
+ export function createLocalMerkleTree() {
43
+ const zeroLeaf = 0n;
44
+ const capacity = 2 ** DEFAULT_DEPTH;
45
+ const tree = new IMT(
46
+ hash,
47
+ DEFAULT_DEPTH,
48
+ zeroLeaf,
49
+ DEFAULT_ARITY,
50
+ ) as BigIntTree;
51
+
52
+ function addLeaf(value: bigint): AddLeafResult {
53
+ if (tree.leaves.length >= capacity) {
54
+ throw new Error("merkle tree is full");
55
+ }
56
+
57
+ const insertedIndex = tree.leaves.length;
58
+ tree.insert(value);
59
+
60
+ return {
61
+ index: insertedIndex,
62
+ root: formatNode(tree.root),
63
+ };
64
+ }
65
+
66
+ function getRoot() {
67
+ return formatNode(tree.root);
68
+ }
69
+
70
+ function getLeafCount() {
71
+ return tree.leaves.length;
72
+ }
73
+
74
+ function getLeaf(index: number) {
75
+ const leaf = tree.leaves[index];
76
+ if (leaf === undefined) {
77
+ throw new Error("leaf does not exist in this tree");
78
+ }
79
+ return formatNode(BigInt(leaf));
80
+ }
81
+
82
+ function createMerkleProof(index: number): IMTMerkleProof {
83
+ return tree.createProof(index);
84
+ }
85
+
86
+ return {
87
+ addLeaf,
88
+ getRoot,
89
+ getLeafCount,
90
+ getLeaf,
91
+ createMerkleProof,
92
+ getZero(level: number) {
93
+ if (level === tree.depth) {
94
+ return getRoot();
95
+ }
96
+ const zero = tree.zeroes[level];
97
+ if (zero === undefined) {
98
+ throw new Error("invalid level");
99
+ }
100
+ return formatNode(zero);
101
+ },
102
+ get depth() {
103
+ return tree.depth;
104
+ },
105
+ get capacity() {
106
+ return capacity;
107
+ },
108
+ };
109
+ }
110
+
111
+ export type LocalMerkleTree = ReturnType<typeof createLocalMerkleTree>;
112
+
113
+ export type LocalMerkleTrees = {
114
+ get(chainId: number): LocalMerkleTree | undefined;
115
+ getOrCreate(chainId: number): LocalMerkleTree;
116
+ addLeaf(chainId: number, value: bigint): AddLeafResult;
117
+ createMerkleProof(chainId: number, index: number): IMTMerkleProof;
118
+ getRoot(chainId: number): string;
119
+ getLeafCount(chainId: number): number;
120
+ reset(chainId: number): void;
121
+ };
122
+
123
+ export function createMerkleTrees(): LocalMerkleTrees {
124
+ const trees = new Map<number, LocalMerkleTree>();
125
+
126
+ const resetTree = (chainId: number) => {
127
+ ensureChainId(chainId);
128
+ const tree = createLocalMerkleTree();
129
+ trees.set(chainId, tree);
130
+ return tree;
131
+ };
132
+ const getOrCreate = (chainId: number) => {
133
+ ensureChainId(chainId);
134
+ let tree = trees.get(chainId);
135
+ if (!tree) {
136
+ tree = resetTree(chainId);
137
+ }
138
+ return tree;
139
+ };
140
+
141
+ return {
142
+ get(chainId) {
143
+ ensureChainId(chainId);
144
+ return trees.get(chainId);
145
+ },
146
+ getOrCreate,
147
+ addLeaf(chainId, value) {
148
+ return getOrCreate(chainId).addLeaf(value);
149
+ },
150
+ createMerkleProof(chainId, index) {
151
+ return getOrCreate(chainId).createMerkleProof(index);
152
+ },
153
+ getRoot(chainId) {
154
+ return getOrCreate(chainId).getRoot();
155
+ },
156
+ getLeafCount(chainId) {
157
+ return getOrCreate(chainId).getLeafCount();
158
+ },
159
+ reset(chainId) {
160
+ resetTree(chainId);
161
+ },
162
+ };
163
+ }
@@ -0,0 +1,28 @@
1
+ import { keys, validateKey } from "../../keys.js";
2
+ import type { Storage } from "../../types.js";
3
+ import { ensureChainId, ensurePositiveInt } from "../../utils/validators.js";
4
+
5
+ export function createCiphertextStore(storage: Storage) {
6
+ return {
7
+ /**
8
+ * Store encrypted note payloads alongside their on-chain commitments.
9
+ */
10
+ async putCiphertext(chainId: number, index: number, payload: Uint8Array) {
11
+ ensureChainId(chainId);
12
+ ensurePositiveInt("ciphertext index", index);
13
+ const key = keys.ciphertext(chainId, index);
14
+ validateKey(key);
15
+ await storage.put(key, new Uint8Array(payload));
16
+ },
17
+
18
+ /**
19
+ * Retrieve encrypted note payloads; returns null when the ciphertext is missing.
20
+ */
21
+ async getCiphertext(chainId: number, index: number) {
22
+ ensureChainId(chainId);
23
+ ensurePositiveInt("ciphertext index", index);
24
+ const data = await storage.get(keys.ciphertext(chainId, index));
25
+ return data ? new Uint8Array(data) : null;
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,24 @@
1
+ export { createStateStore } from "./store.js";
2
+ export { createLeafStore } from "./leaf-store.js";
3
+ export { createNoteStore } from "./note-store.js";
4
+ export { createNullifierStore } from "./nullifier-store.js";
5
+ export { createRootStore } from "./root-store.js";
6
+ export { createCiphertextStore } from "./ciphertext-store.js";
7
+ export { createJobStore } from "./job-store.js";
8
+ export {
9
+ DEFAULT_JOB_TIMEOUT_MS,
10
+ type JobStatus,
11
+ type PendingJobKind,
12
+ type PendingJobRecord,
13
+ type PendingDepositJob,
14
+ type PendingTransactJob,
15
+ type PendingTransactContext,
16
+ type PendingTransactOutput,
17
+ } from "./jobs.js";
18
+ export type {
19
+ LeafRecord,
20
+ NoteInsert,
21
+ NoteRecord,
22
+ NullifierRecord,
23
+ RootRecord,
24
+ } from "./records.js";
@@ -0,0 +1,162 @@
1
+ import { keys } from "../../keys.js";
2
+ import type { Storage } from "../../types.js";
3
+ import { decodeJson, encodeJson, getJson } from "../../utils/json-codec.js";
4
+ import {
5
+ ensureAddress,
6
+ ensureChainId,
7
+ ensurePositiveInt,
8
+ } from "../../utils/validators.js";
9
+ import type {
10
+ JobStatus,
11
+ PendingDepositJob,
12
+ PendingJobKind,
13
+ PendingJobRecord,
14
+ PendingTransactContext,
15
+ PendingTransactJob,
16
+ } from "../store/jobs.js";
17
+ import { DEFAULT_JOB_TIMEOUT_MS } from "../store/jobs.js";
18
+
19
+ const VALID_STATUSES: JobStatus[] = [
20
+ "pending",
21
+ "submitted",
22
+ "broadcasting",
23
+ "succeeded",
24
+ "failed",
25
+ "dead",
26
+ ] as const;
27
+
28
+ function assertStatus(status: string): asserts status is JobStatus {
29
+ if (!VALID_STATUSES.includes(status as JobStatus)) {
30
+ throw new Error(`invalid job status: ${status}`);
31
+ }
32
+ }
33
+
34
+ function assertKind(kind: string): asserts kind is PendingJobKind {
35
+ if (kind !== "deposit" && kind !== "transact") {
36
+ throw new Error(`invalid job kind: ${kind}`);
37
+ }
38
+ }
39
+
40
+ function normalizeTimestamps<T extends PendingJobRecord>(job: T): T {
41
+ const createdAt =
42
+ typeof job.createdAt === "number" && Number.isFinite(job.createdAt)
43
+ ? job.createdAt
44
+ : Date.now();
45
+ const timeoutMs =
46
+ typeof job.timeoutMs === "number" && job.timeoutMs > 0
47
+ ? job.timeoutMs
48
+ : DEFAULT_JOB_TIMEOUT_MS;
49
+
50
+ return {
51
+ ...job,
52
+ createdAt,
53
+ timeoutMs,
54
+ };
55
+ }
56
+
57
+ function validateTransactContext(context: PendingTransactContext) {
58
+ ensurePositiveInt("context index", context.index);
59
+ if (!context.nullifier) {
60
+ throw new Error("context nullifier is required");
61
+ }
62
+ const { witness } = context;
63
+ if (!witness) {
64
+ throw new Error("context witness is required");
65
+ }
66
+ ensurePositiveInt("witness leafIndex", witness.leafIndex);
67
+ if (
68
+ !Array.isArray(witness.pathElements) ||
69
+ !Array.isArray(witness.pathIndices)
70
+ ) {
71
+ throw new Error("witness pathElements and pathIndices are required");
72
+ }
73
+ }
74
+
75
+ function validateTransactJob(job: PendingTransactJob) {
76
+ ensureAddress("pool address", job.poolAddress);
77
+ if (!job.calldata) {
78
+ throw new Error("calldata is required for transact job");
79
+ }
80
+ job.contexts.forEach(validateTransactContext);
81
+ job.predictedOutputs.forEach((output) => {
82
+ if (!output.hex) {
83
+ throw new Error("predicted output hex is required");
84
+ }
85
+ ensurePositiveInt("predicted output index", output.index);
86
+ });
87
+ }
88
+
89
+ function validateDepositJob(job: PendingDepositJob) {
90
+ if (!job.predictedCommitment?.hex) {
91
+ throw new Error("predicted commitment hex is required");
92
+ }
93
+ if (job.predictedCommitment.index !== undefined) {
94
+ ensurePositiveInt(
95
+ "predicted commitment index",
96
+ job.predictedCommitment.index,
97
+ );
98
+ }
99
+ }
100
+
101
+ function validateJob(job: PendingJobRecord) {
102
+ if (!job.relayId) {
103
+ throw new Error("relayId is required");
104
+ }
105
+ ensureChainId(job.chainId);
106
+ assertStatus(job.status);
107
+ assertKind(job.kind);
108
+ ensurePositiveInt("job createdAt", job.createdAt);
109
+ ensurePositiveInt("job timeoutMs", job.timeoutMs);
110
+ if (job.lastCheckedAt !== undefined) {
111
+ ensurePositiveInt("job lastCheckedAt", job.lastCheckedAt);
112
+ }
113
+ if (job.kind === "deposit") {
114
+ validateDepositJob(job);
115
+ } else {
116
+ validateTransactJob(job);
117
+ }
118
+ }
119
+
120
+ function buildJobKey(relayId: string) {
121
+ return keys.job(relayId);
122
+ }
123
+
124
+ export function createJobStore(storage: Storage) {
125
+ return {
126
+ async putPendingJob(job: PendingJobRecord) {
127
+ const normalized = normalizeTimestamps(job);
128
+ validateJob(normalized);
129
+ await storage.put(
130
+ buildJobKey(normalized.relayId),
131
+ encodeJson(normalized),
132
+ );
133
+ },
134
+
135
+ async getPendingJob(relayId: string) {
136
+ return getJson<PendingJobRecord>(storage, buildJobKey(relayId));
137
+ },
138
+
139
+ async listPendingJobs(
140
+ filter: {
141
+ kind?: PendingJobKind;
142
+ statuses?: JobStatus[];
143
+ } = {},
144
+ ) {
145
+ const entries = await storage.iter({ prefix: "jobs:" });
146
+ const statuses = filter.statuses ?? VALID_STATUSES;
147
+ const filtered = entries
148
+ .map(({ value }) => decodeJson<PendingJobRecord>(value))
149
+ .filter(
150
+ (job) =>
151
+ (filter.kind === undefined || job.kind === filter.kind) &&
152
+ statuses.includes(job.status),
153
+ );
154
+ filtered.sort((a, b) => a.createdAt - b.createdAt);
155
+ return filtered;
156
+ },
157
+
158
+ async deletePendingJob(relayId: string) {
159
+ await storage.delete(buildJobKey(relayId));
160
+ },
161
+ };
162
+ }
@@ -0,0 +1,64 @@
1
+ export type JobStatus =
2
+ | "pending"
3
+ | "submitted"
4
+ | "broadcasting"
5
+ | "succeeded"
6
+ | "failed"
7
+ | "dead";
8
+
9
+ export type PendingJobKind = "deposit" | "transact";
10
+
11
+ export type PendingJobBase = {
12
+ relayId: string;
13
+ kind: PendingJobKind;
14
+ chainId: number;
15
+ status: JobStatus;
16
+ broadcasterRelayId?: string | null;
17
+ txHash?: string | null;
18
+ createdAt: number;
19
+ lastCheckedAt?: number;
20
+ timeoutMs: number;
21
+ error?: string | null;
22
+ };
23
+
24
+ export type PendingDepositJob = PendingJobBase & {
25
+ kind: "deposit";
26
+ predictedCommitment: {
27
+ hex: string;
28
+ index?: number;
29
+ root?: string;
30
+ };
31
+ };
32
+
33
+ export type PendingTransactContext = {
34
+ index: number;
35
+ nullifier: string;
36
+ // Serialized Merkle proof with hex-encoded nodes to keep it JSON friendly.
37
+ witness: {
38
+ root: string;
39
+ leaf: string;
40
+ pathElements: string[][];
41
+ pathIndices: number[];
42
+ leafIndex: number;
43
+ };
44
+ root: string;
45
+ };
46
+
47
+ export type PendingTransactOutput = {
48
+ hex: string;
49
+ index: number;
50
+ root?: string;
51
+ };
52
+
53
+ export type PendingTransactJob = PendingJobBase & {
54
+ kind: "transact";
55
+ poolAddress: string;
56
+ calldata: string;
57
+ contexts: PendingTransactContext[];
58
+ predictedOutputs: PendingTransactOutput[];
59
+ expectedRoot?: string;
60
+ };
61
+
62
+ export type PendingJobRecord = PendingDepositJob | PendingTransactJob;
63
+
64
+ export const DEFAULT_JOB_TIMEOUT_MS = 5 * 60 * 1000;
@@ -0,0 +1,39 @@
1
+ import { keys } from "../../keys.js";
2
+ import type { Storage } from "../../types.js";
3
+ import { getJson, putJson } from "../../utils/json-codec.js";
4
+ import { ensureChainId, ensurePositiveInt } from "../../utils/validators.js";
5
+ import type { LeafRecord } from "../store/records.js";
6
+
7
+ export function createLeafStore(storage: Storage) {
8
+ return {
9
+ /**
10
+ * Cache public leaf data pulled from the on-chain Merkle tree.
11
+ */
12
+ async putLeaf(leaf: LeafRecord) {
13
+ ensureChainId(leaf.chainId);
14
+ ensurePositiveInt("leaf index", leaf.index);
15
+ await putJson(storage, keys.leaf(leaf.chainId, leaf.index), leaf);
16
+ },
17
+
18
+ /**
19
+ * Load a leaf commitment by tree index.
20
+ */
21
+ async getLeaf(chainId: number, index: number) {
22
+ ensureChainId(chainId);
23
+ ensurePositiveInt("leaf index", index);
24
+ return getJson<LeafRecord>(storage, keys.leaf(chainId, index));
25
+ },
26
+
27
+ /**
28
+ * Remove all cached leaves for a chain.
29
+ */
30
+ async clearLeaves(chainId: number) {
31
+ ensureChainId(chainId);
32
+ const prefix = `leaves:${chainId}:`;
33
+ const entries = await storage.iter({ prefix });
34
+ if (entries.length === 0) return;
35
+ const deletions = entries.map(({ key }) => ({ del: key }));
36
+ await storage.batch(deletions);
37
+ },
38
+ };
39
+ }