@unlink-xyz/core 0.1.2 → 0.1.3-canary.0877bfe
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/dist/account/{zkAccount.d.ts → account.d.ts} +7 -5
- package/dist/account/account.d.ts.map +1 -0
- package/dist/account/{zkAccount.js → account.js} +57 -43
- package/dist/browser/index.js +108202 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/circuits.json +74 -0
- package/dist/clients/broadcaster.d.ts +7 -2
- package/dist/clients/broadcaster.d.ts.map +1 -1
- package/dist/clients/broadcaster.js +9 -1
- package/dist/clients/http.d.ts +6 -0
- package/dist/clients/http.d.ts.map +1 -1
- package/dist/clients/http.js +24 -9
- package/dist/clients/indexer.d.ts +11 -0
- package/dist/clients/indexer.d.ts.map +1 -1
- package/dist/clients/indexer.js +40 -11
- package/dist/config.d.ts +28 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +33 -26
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +5 -0
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +5 -2
- package/dist/crypto-adapters/auto-init.d.ts +2 -0
- package/dist/crypto-adapters/auto-init.d.ts.map +1 -0
- package/dist/crypto-adapters/auto-init.js +7 -0
- package/dist/crypto-adapters/index.d.ts +22 -0
- package/dist/crypto-adapters/index.d.ts.map +1 -0
- package/dist/crypto-adapters/index.js +47 -0
- package/dist/crypto-adapters/polyfills.d.ts +5 -0
- package/dist/crypto-adapters/polyfills.d.ts.map +1 -0
- package/dist/crypto-adapters/polyfills.js +8 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -0
- package/dist/history/index.d.ts +3 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/index.js +2 -0
- package/dist/history/service.d.ts +46 -0
- package/dist/history/service.d.ts.map +1 -0
- package/dist/history/service.js +354 -0
- package/dist/history/types.d.ts +21 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/keys/address.d.ts +13 -0
- package/dist/keys/address.d.ts.map +1 -0
- package/dist/keys/address.js +55 -0
- package/dist/keys/derive.d.ts +37 -0
- package/dist/keys/derive.d.ts.map +1 -0
- package/dist/keys/derive.js +112 -0
- package/dist/keys/hex.d.ts +17 -0
- package/dist/keys/hex.d.ts.map +1 -0
- package/dist/keys/hex.js +66 -0
- package/dist/keys/index.d.ts +5 -0
- package/dist/keys/index.d.ts.map +1 -0
- package/dist/keys/index.js +4 -0
- package/dist/keys/mnemonic.d.ts +8 -0
- package/dist/keys/mnemonic.d.ts.map +1 -0
- package/dist/keys/mnemonic.js +23 -0
- package/dist/keys.d.ts +4 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js +4 -0
- package/dist/prover/config.d.ts +1 -15
- package/dist/prover/config.d.ts.map +1 -1
- package/dist/prover/config.js +1 -11
- package/dist/prover/prover.d.ts +15 -4
- package/dist/prover/prover.d.ts.map +1 -1
- package/dist/prover/prover.js +115 -98
- package/dist/prover/registry.d.ts +3 -30
- package/dist/prover/registry.d.ts.map +1 -1
- package/dist/prover/registry.js +12 -51
- package/dist/state/merkle/hydrator.d.ts.map +1 -1
- package/dist/state/merkle/hydrator.js +3 -2
- package/dist/state/merkle/index.d.ts +1 -1
- package/dist/state/merkle/index.d.ts.map +1 -1
- package/dist/state/merkle/index.js +1 -1
- package/dist/state/merkle/merkle-tree.d.ts +8 -0
- package/dist/state/merkle/merkle-tree.d.ts.map +1 -1
- package/dist/state/merkle/merkle-tree.js +16 -7
- package/dist/state/store/ciphertext-store.d.ts +4 -0
- package/dist/state/store/ciphertext-store.d.ts.map +1 -1
- package/dist/state/store/ciphertext-store.js +12 -0
- package/dist/state/store/history-store.d.ts +24 -0
- package/dist/state/store/history-store.d.ts.map +1 -0
- package/dist/state/store/history-store.js +53 -0
- package/dist/state/store/index.d.ts +3 -2
- package/dist/state/store/index.d.ts.map +1 -1
- package/dist/state/store/index.js +1 -0
- package/dist/state/store/job-store.d.ts +7 -7
- package/dist/state/store/job-store.d.ts.map +1 -1
- package/dist/state/store/job-store.js +65 -39
- package/dist/state/store/jobs.d.ts +65 -18
- package/dist/state/store/jobs.d.ts.map +1 -1
- package/dist/state/store/leaf-store.d.ts.map +1 -1
- package/dist/state/store/leaf-store.js +0 -3
- package/dist/state/store/note-store.d.ts +7 -7
- package/dist/state/store/note-store.d.ts.map +1 -1
- package/dist/state/store/note-store.js +38 -34
- package/dist/state/store/nullifier-store.d.ts +9 -0
- package/dist/state/store/nullifier-store.d.ts.map +1 -1
- package/dist/state/store/nullifier-store.js +32 -2
- package/dist/state/store/records.d.ts +31 -2
- package/dist/state/store/records.d.ts.map +1 -1
- package/dist/state/store/root-store.d.ts.map +1 -1
- package/dist/state/store/root-store.js +0 -4
- package/dist/state/store/store.d.ts +61 -27
- package/dist/state/store/store.d.ts.map +1 -1
- package/dist/state/store/store.js +92 -1
- package/dist/storage/indexeddb.js +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/storage/memory.js +5 -1
- package/dist/transactions/deposit.d.ts +12 -15
- package/dist/transactions/deposit.d.ts.map +1 -1
- package/dist/transactions/deposit.js +203 -152
- package/dist/transactions/index.d.ts +7 -4
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/index.js +7 -2
- package/dist/transactions/note-selection.d.ts +17 -0
- package/dist/transactions/note-selection.d.ts.map +1 -0
- package/dist/transactions/note-selection.js +201 -0
- package/dist/transactions/note-sync.d.ts +5 -33
- package/dist/transactions/note-sync.d.ts.map +1 -1
- package/dist/transactions/note-sync.js +320 -155
- package/dist/transactions/reconcile.d.ts +10 -12
- package/dist/transactions/reconcile.d.ts.map +1 -1
- package/dist/transactions/reconcile.js +53 -7
- package/dist/transactions/transact.d.ts +13 -24
- package/dist/transactions/transact.d.ts.map +1 -1
- package/dist/transactions/transact.js +393 -507
- package/dist/transactions/transaction-planner.d.ts +34 -0
- package/dist/transactions/transaction-planner.d.ts.map +1 -0
- package/dist/transactions/transaction-planner.js +116 -0
- package/dist/transactions/transfer-planner.d.ts +36 -0
- package/dist/transactions/transfer-planner.d.ts.map +1 -0
- package/dist/transactions/transfer-planner.js +85 -0
- package/dist/transactions/types/deposit.d.ts +67 -0
- package/dist/transactions/types/deposit.d.ts.map +1 -0
- package/dist/transactions/types/domain.d.ts +67 -0
- package/dist/transactions/types/domain.d.ts.map +1 -0
- package/dist/transactions/types/domain.js +4 -0
- package/dist/transactions/types/index.d.ts +18 -0
- package/dist/transactions/types/index.d.ts.map +1 -0
- package/dist/transactions/types/index.js +17 -0
- package/dist/transactions/types/options.d.ts +45 -0
- package/dist/transactions/types/options.d.ts.map +1 -0
- package/dist/transactions/types/options.js +1 -0
- package/dist/transactions/types/planning.d.ts +80 -0
- package/dist/transactions/types/planning.d.ts.map +1 -0
- package/dist/transactions/types/planning.js +1 -0
- package/dist/transactions/types/state-stores.d.ts +103 -0
- package/dist/transactions/types/state-stores.d.ts.map +1 -0
- package/dist/transactions/types/state-stores.js +1 -0
- package/dist/transactions/types/transact.d.ts +76 -0
- package/dist/transactions/types/transact.d.ts.map +1 -0
- package/dist/transactions/types/transact.js +1 -0
- package/dist/transactions/withdrawal-planner.d.ts +58 -0
- package/dist/transactions/withdrawal-planner.d.ts.map +1 -0
- package/dist/transactions/withdrawal-planner.js +128 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsup.browser.config.d.ts +7 -0
- package/dist/tsup.browser.config.d.ts.map +1 -0
- package/dist/tsup.browser.config.js +34 -0
- package/dist/utils/amounts.d.ts +39 -0
- package/dist/utils/amounts.d.ts.map +1 -0
- package/dist/utils/amounts.js +89 -0
- package/dist/utils/async.d.ts +9 -0
- package/dist/utils/async.d.ts.map +1 -1
- package/dist/utils/async.js +24 -0
- package/dist/utils/bigint.js +7 -7
- package/dist/utils/crypto.d.ts +11 -5
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +12 -6
- package/dist/utils/format.d.ts +25 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +33 -0
- package/dist/utils/json-codec.js +1 -1
- package/dist/utils/notes.d.ts +15 -0
- package/dist/utils/notes.d.ts.map +1 -0
- package/dist/utils/notes.js +14 -0
- package/dist/utils/polling.d.ts +5 -0
- package/dist/utils/polling.d.ts.map +1 -1
- package/dist/utils/polling.js +5 -0
- package/dist/utils/random.d.ts +13 -0
- package/dist/utils/random.d.ts.map +1 -0
- package/dist/utils/random.js +27 -0
- package/dist/utils/secure-memory.d.ts +25 -0
- package/dist/utils/secure-memory.d.ts.map +1 -0
- package/dist/utils/secure-memory.js +28 -0
- package/dist/utils/signature.d.ts +6 -0
- package/dist/utils/signature.d.ts.map +1 -1
- package/dist/utils/signature.js +8 -6
- package/dist/utils/validators.d.ts +21 -10
- package/dist/utils/validators.d.ts.map +1 -1
- package/dist/utils/validators.js +37 -11
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +13 -0
- package/package.json +28 -11
- package/.eslintrc.json +0 -4
- package/account/zkAccount.test.ts +0 -316
- package/account/zkAccount.ts +0 -222
- package/clients/broadcaster.ts +0 -67
- package/clients/http.ts +0 -94
- package/clients/indexer.ts +0 -150
- package/config.ts +0 -39
- package/core.ts +0 -17
- package/dist/account/railgun-imports-prototype.d.ts +0 -12
- package/dist/account/railgun-imports-prototype.d.ts.map +0 -1
- package/dist/account/railgun-imports-prototype.js +0 -30
- package/dist/account/zkAccount.d.ts.map +0 -1
- package/dist/key-derivation/babyjubjub.d.ts +0 -9
- package/dist/key-derivation/babyjubjub.d.ts.map +0 -1
- package/dist/key-derivation/babyjubjub.js +0 -9
- package/dist/key-derivation/bech32.d.ts +0 -22
- package/dist/key-derivation/bech32.d.ts.map +0 -1
- package/dist/key-derivation/bech32.js +0 -86
- package/dist/key-derivation/bip32.d.ts +0 -17
- package/dist/key-derivation/bip32.d.ts.map +0 -1
- package/dist/key-derivation/bip32.js +0 -41
- package/dist/key-derivation/bip39.d.ts +0 -22
- package/dist/key-derivation/bip39.d.ts.map +0 -1
- package/dist/key-derivation/bip39.js +0 -56
- package/dist/key-derivation/bytes.d.ts +0 -19
- package/dist/key-derivation/bytes.d.ts.map +0 -1
- package/dist/key-derivation/bytes.js +0 -92
- package/dist/key-derivation/hash.d.ts +0 -3
- package/dist/key-derivation/hash.d.ts.map +0 -1
- package/dist/key-derivation/hash.js +0 -10
- package/dist/key-derivation/index.d.ts +0 -8
- package/dist/key-derivation/index.d.ts.map +0 -1
- package/dist/key-derivation/index.js +0 -7
- package/dist/key-derivation/wallet-node.d.ts +0 -45
- package/dist/key-derivation/wallet-node.d.ts.map +0 -1
- package/dist/key-derivation/wallet-node.js +0 -109
- package/dist/state/ciphertext-store.d.ts +0 -12
- package/dist/state/ciphertext-store.d.ts.map +0 -1
- package/dist/state/ciphertext-store.js +0 -25
- package/dist/state/hydrator.d.ts +0 -16
- package/dist/state/hydrator.d.ts.map +0 -1
- package/dist/state/hydrator.js +0 -18
- package/dist/state/job-store.d.ts +0 -12
- package/dist/state/job-store.d.ts.map +0 -1
- package/dist/state/job-store.js +0 -118
- package/dist/state/jobs.d.ts +0 -50
- package/dist/state/jobs.d.ts.map +0 -1
- package/dist/state/jobs.js +0 -1
- package/dist/state/leaf-store.d.ts +0 -17
- package/dist/state/leaf-store.d.ts.map +0 -1
- package/dist/state/leaf-store.js +0 -35
- package/dist/state/merkle-tree.d.ts +0 -34
- package/dist/state/merkle-tree.d.ts.map +0 -1
- package/dist/state/merkle-tree.js +0 -104
- package/dist/state/note-store.d.ts +0 -37
- package/dist/state/note-store.d.ts.map +0 -1
- package/dist/state/note-store.js +0 -133
- package/dist/state/nullifier-store.d.ts +0 -13
- package/dist/state/nullifier-store.d.ts.map +0 -1
- package/dist/state/nullifier-store.js +0 -21
- package/dist/state/records.d.ts +0 -57
- package/dist/state/records.d.ts.map +0 -1
- package/dist/state/root-store.d.ts +0 -13
- package/dist/state/root-store.d.ts.map +0 -1
- package/dist/state/root-store.js +0 -30
- package/dist/state/store.d.ts +0 -26
- package/dist/state/store.d.ts.map +0 -1
- package/dist/state/store.js +0 -19
- package/dist/state.d.ts +0 -83
- package/dist/state.d.ts.map +0 -1
- package/dist/state.js +0 -171
- package/dist/transactions/shield.d.ts +0 -5
- package/dist/transactions/shield.d.ts.map +0 -1
- package/dist/transactions/shield.js +0 -93
- package/dist/transactions/types.d.ts +0 -114
- package/dist/transactions/types.d.ts.map +0 -1
- package/dist/transactions/utils.d.ts +0 -10
- package/dist/transactions/utils.d.ts.map +0 -1
- package/dist/transactions/utils.js +0 -17
- package/dist/utils/time.d.ts +0 -2
- package/dist/utils/time.d.ts.map +0 -1
- package/dist/utils/time.js +0 -3
- package/dist/utils/witness.d.ts +0 -11
- package/dist/utils/witness.d.ts.map +0 -1
- package/dist/utils/witness.js +0 -19
- package/errors.ts +0 -20
- package/index.ts +0 -17
- package/key-derivation/babyjubjub.ts +0 -11
- package/key-derivation/bech32.test.ts +0 -90
- package/key-derivation/bech32.ts +0 -124
- package/key-derivation/bip32.ts +0 -56
- package/key-derivation/bip39.ts +0 -76
- package/key-derivation/bytes.ts +0 -118
- package/key-derivation/hash.ts +0 -13
- package/key-derivation/index.ts +0 -7
- package/key-derivation/wallet-node.ts +0 -155
- package/keys.ts +0 -47
- package/prover/config.ts +0 -104
- package/prover/index.ts +0 -1
- package/prover/prover.integration.test.ts +0 -162
- package/prover/prover.test.ts +0 -309
- package/prover/prover.ts +0 -405
- package/prover/registry.test.ts +0 -90
- package/prover/registry.ts +0 -82
- package/schema.ts +0 -17
- package/setup-artifacts.sh +0 -57
- package/state/index.ts +0 -2
- package/state/merkle/hydrator.ts +0 -69
- package/state/merkle/index.ts +0 -12
- package/state/merkle/merkle-tree.test.ts +0 -50
- package/state/merkle/merkle-tree.ts +0 -163
- package/state/store/ciphertext-store.ts +0 -28
- package/state/store/index.ts +0 -24
- package/state/store/job-store.ts +0 -162
- package/state/store/jobs.ts +0 -64
- package/state/store/leaf-store.ts +0 -39
- package/state/store/note-store.ts +0 -177
- package/state/store/nullifier-store.ts +0 -39
- package/state/store/records.ts +0 -61
- package/state/store/root-store.ts +0 -34
- package/state/store/store.ts +0 -25
- package/state.test.ts +0 -235
- package/storage/index.ts +0 -3
- package/storage/indexeddb.test.ts +0 -99
- package/storage/indexeddb.ts +0 -235
- package/storage/memory.test.ts +0 -59
- package/storage/memory.ts +0 -93
- package/transactions/deposit.test.ts +0 -160
- package/transactions/deposit.ts +0 -227
- package/transactions/index.ts +0 -20
- package/transactions/note-sync.test.ts +0 -155
- package/transactions/note-sync.ts +0 -452
- package/transactions/reconcile.ts +0 -73
- package/transactions/transact.test.ts +0 -451
- package/transactions/transact.ts +0 -811
- package/transactions/types.ts +0 -141
- package/tsconfig.json +0 -14
- package/types/global.d.ts +0 -15
- package/types.ts +0 -24
- package/utils/async.ts +0 -15
- package/utils/bigint.ts +0 -34
- package/utils/crypto.test.ts +0 -69
- package/utils/crypto.ts +0 -58
- package/utils/json-codec.ts +0 -38
- package/utils/polling.ts +0 -6
- package/utils/signature.ts +0 -16
- package/utils/validators.test.ts +0 -64
- package/utils/validators.ts +0 -86
- /package/dist/{transactions → history}/types.js +0 -0
- /package/dist/{state/records.js → transactions/types/deposit.js} +0 -0
|
@@ -1,59 +1,31 @@
|
|
|
1
|
-
import { poseidon } from "@railgun-community/circomlibjs";
|
|
2
1
|
import { AbiCoder, Interface, keccak256 } from "ethers";
|
|
3
2
|
import { createBroadcasterClient } from "../clients/broadcaster.js";
|
|
3
|
+
import { resolveFetch } from "../clients/http.js";
|
|
4
4
|
import { createIndexerClient } from "../clients/indexer.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { createServiceConfig } from "../config.js";
|
|
6
|
+
import { poseidon } from "../crypto-adapters/index.js";
|
|
7
|
+
import { CoreError, ProofError, ValidationError } from "../errors.js";
|
|
7
8
|
import { proveTransaction } from "../prover/index.js";
|
|
8
|
-
import {
|
|
9
|
-
import { isNotFoundError, sleep } from "../utils/async.js";
|
|
10
|
-
import {
|
|
9
|
+
import { DEFAULT_JOB_TIMEOUT_MS, rebuildTreeFromStore, resolveMerkleTrees, } from "../state/index.js";
|
|
10
|
+
import { isNotFoundError, sleep, withTimeout } from "../utils/async.js";
|
|
11
|
+
import { formatUint256, parseHexToBigInt } from "../utils/bigint.js";
|
|
11
12
|
import { deriveCommitment, encryptNote } from "../utils/crypto.js";
|
|
12
13
|
import { DEFAULT_POLL_INTERVAL_MS, DEFAULT_POLL_TIMEOUT_MS, MAX_POLL_INTERVAL_MS, } from "../utils/polling.js";
|
|
13
14
|
import { signTransactMessage } from "../utils/signature.js";
|
|
14
|
-
import {
|
|
15
|
-
export const TRANSACT_ABI = "function transact(((uint256[2] pA, uint256[2][2] pB, uint256[2] pC) proof, uint256 merkleRoot, uint256[] nullifierHashes, uint256[] newCommitments, (uint64
|
|
15
|
+
import { SNARK_SCALAR_FIELD } from "../utils/validators.js";
|
|
16
|
+
export 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)";
|
|
17
|
+
/** Default timeout for proof generation in milliseconds (60 seconds) */
|
|
18
|
+
export const DEFAULT_PROOF_TIMEOUT_MS = 60_000;
|
|
16
19
|
const transactInterface = new Interface([TRANSACT_ABI]);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
throw new CoreError("at least one input note is required");
|
|
24
|
-
}
|
|
25
|
-
request.inputs.forEach((input, idx) => {
|
|
26
|
-
if (!Number.isInteger(input.index) || input.index < 0) {
|
|
27
|
-
throw new CoreError(`inputs[${idx}].index must be a non-negative integer`);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
ensureWithdrawalInput("request.withdrawal", request.withdrawal);
|
|
31
|
-
request.outputs.forEach((output, idx) => {
|
|
32
|
-
if (output.mpk < 0n) {
|
|
33
|
-
throw new CoreError(`outputs[${idx}].mpk must be non-negative`);
|
|
34
|
-
}
|
|
35
|
-
if (output.random < 0n) {
|
|
36
|
-
throw new CoreError(`outputs[${idx}].random must be non-negative`);
|
|
37
|
-
}
|
|
38
|
-
ensureNoteCommitmentInput(`outputs[${idx}]`, {
|
|
39
|
-
npk: poseidon([output.mpk, output.random]),
|
|
40
|
-
amount: output.amount,
|
|
41
|
-
token: output.token,
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Computes the bound parameters hash from chain ID, pool address, and withdrawal parameters.
|
|
47
|
-
* This hash binds the transaction to specific chain and withdrawal context.
|
|
48
|
-
*/
|
|
49
|
-
function computeBoundParamsHash(chainId, poolAddress) {
|
|
50
|
-
const chainIdBigInt = BigInt(chainId);
|
|
51
|
-
const poolAddressBigInt = BigInt(poolAddress);
|
|
52
|
-
const hashInput = [chainIdBigInt, poolAddressBigInt];
|
|
20
|
+
const DEFAULT_WITHDRAWAL = {
|
|
21
|
+
npk: 0n,
|
|
22
|
+
amount: 0n,
|
|
23
|
+
token: "0x0000000000000000000000000000000000000000",
|
|
24
|
+
};
|
|
25
|
+
export function computeBoundParamsHash(chainId, poolAddress) {
|
|
53
26
|
const coder = AbiCoder.defaultAbiCoder();
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
return parseHexToBigInt(result) % SNARK_SCALAR_FIELD;
|
|
27
|
+
const encoded = coder.encode(["uint64", "uint160"], [BigInt(chainId), BigInt(poolAddress)]);
|
|
28
|
+
return parseHexToBigInt(keccak256(encoded)) % SNARK_SCALAR_FIELD;
|
|
57
29
|
}
|
|
58
30
|
export function serializeWitness(proof, index) {
|
|
59
31
|
return {
|
|
@@ -64,498 +36,412 @@ export function serializeWitness(proof, index) {
|
|
|
64
36
|
leafIndex: index,
|
|
65
37
|
};
|
|
66
38
|
}
|
|
67
|
-
export function deserializeWitness(
|
|
39
|
+
export function deserializeWitness(s) {
|
|
68
40
|
return {
|
|
69
|
-
root: parseHexToBigInt(
|
|
70
|
-
leaf: parseHexToBigInt(
|
|
71
|
-
siblings:
|
|
72
|
-
pathIndices:
|
|
73
|
-
leafIndex:
|
|
41
|
+
root: parseHexToBigInt(s.root),
|
|
42
|
+
leaf: parseHexToBigInt(s.leaf),
|
|
43
|
+
siblings: s.pathElements.map((level) => level.map(parseHexToBigInt)),
|
|
44
|
+
pathIndices: s.pathIndices,
|
|
45
|
+
leafIndex: s.leafIndex,
|
|
74
46
|
};
|
|
75
47
|
}
|
|
76
48
|
/**
|
|
77
|
-
*
|
|
49
|
+
* Build proof for a single transaction item.
|
|
50
|
+
* This is the core proof generation logic extracted for parallelization.
|
|
78
51
|
*/
|
|
79
|
-
|
|
80
|
-
if (!
|
|
81
|
-
throw new
|
|
52
|
+
async function buildSingleTransactionProof(store, account, chainId, poolAddress, tx, trees, baseIndex, opts) {
|
|
53
|
+
if (!tx.inputs?.length)
|
|
54
|
+
throw new ValidationError("at least one input note is required");
|
|
55
|
+
// Build input contexts (witnesses + nullifiers)
|
|
56
|
+
const contexts = [];
|
|
57
|
+
let merkleRoot;
|
|
58
|
+
for (const input of tx.inputs) {
|
|
59
|
+
const note = await store.getNote(chainId, input.index);
|
|
60
|
+
if (!note)
|
|
61
|
+
throw new ValidationError(`note ${input.index} not found`);
|
|
62
|
+
if (note.spentAt !== undefined)
|
|
63
|
+
throw new ValidationError(`note ${input.index} already spent`);
|
|
64
|
+
const tree = trees.getOrCreate(chainId);
|
|
65
|
+
if (input.index >= tree.getLeafCount())
|
|
66
|
+
throw new ValidationError(`note ${input.index} out of range`);
|
|
67
|
+
const witness = tree.createMerkleProof(input.index);
|
|
68
|
+
const root = formatUint256(BigInt(witness.root));
|
|
69
|
+
if (merkleRoot && merkleRoot !== root)
|
|
70
|
+
throw new ValidationError("inputs must share same merkle root");
|
|
71
|
+
merkleRoot = root;
|
|
72
|
+
const nullifier = poseidon([account.nullifyingKey, BigInt(input.index)]);
|
|
73
|
+
contexts.push({
|
|
74
|
+
index: input.index,
|
|
75
|
+
nullifier,
|
|
76
|
+
nullifierHex: formatUint256(nullifier),
|
|
77
|
+
witness,
|
|
78
|
+
});
|
|
82
79
|
}
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
// Compute output commitments for regular notes
|
|
81
|
+
const outputs = tx.outputs.map((o, i) => {
|
|
82
|
+
const commitment = deriveCommitment({
|
|
83
|
+
npk: poseidon([o.mpk, o.random]),
|
|
84
|
+
amount: o.amount,
|
|
85
|
+
token: o.token,
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
value: commitment,
|
|
89
|
+
hex: formatUint256(commitment),
|
|
90
|
+
index: baseIndex + i,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
// Handle withdrawal
|
|
94
|
+
const withdrawal = tx.withdrawal ?? DEFAULT_WITHDRAWAL;
|
|
95
|
+
const hasWithdrawal = withdrawal.amount > 0n;
|
|
96
|
+
if (hasWithdrawal && withdrawal.token !== tx.token) {
|
|
97
|
+
throw new ValidationError("Withdrawal token must match transaction token");
|
|
98
|
+
}
|
|
99
|
+
const withdrawalCommitment = hasWithdrawal
|
|
100
|
+
? deriveCommitment({
|
|
101
|
+
npk: withdrawal.npk,
|
|
102
|
+
amount: withdrawal.amount,
|
|
103
|
+
token: withdrawal.token,
|
|
88
104
|
})
|
|
89
105
|
: null;
|
|
90
|
-
|
|
91
|
-
|
|
106
|
+
// Build arrays for circuit
|
|
107
|
+
const allCommitmentsOut = hasWithdrawal
|
|
108
|
+
? [...outputs.map((o) => o.value), withdrawalCommitment]
|
|
109
|
+
: outputs.map((o) => o.value);
|
|
110
|
+
const allNpkOut = hasWithdrawal
|
|
111
|
+
? [...tx.outputs.map((o) => poseidon([o.mpk, o.random])), withdrawal.npk]
|
|
112
|
+
: tx.outputs.map((o) => poseidon([o.mpk, o.random]));
|
|
113
|
+
const allValueOut = hasWithdrawal
|
|
114
|
+
? [...tx.outputs.map((o) => o.amount), withdrawal.amount]
|
|
115
|
+
: tx.outputs.map((o) => o.amount);
|
|
116
|
+
// Build prover input
|
|
117
|
+
const boundParams = computeBoundParamsHash(chainId, poolAddress);
|
|
118
|
+
const pubSignals = [
|
|
119
|
+
parseHexToBigInt(merkleRoot),
|
|
120
|
+
boundParams,
|
|
121
|
+
...contexts.map((c) => c.nullifier),
|
|
122
|
+
...allCommitmentsOut,
|
|
123
|
+
];
|
|
124
|
+
const inputNotes = await Promise.all(tx.inputs.map((i) => store.getNote(chainId, i.index).then((n) => n)));
|
|
125
|
+
const sig = signTransactMessage(account.spendingKeyPair.privateKey, poseidon(pubSignals));
|
|
126
|
+
const proofInput = {
|
|
127
|
+
merkleRoot: parseHexToBigInt(merkleRoot),
|
|
128
|
+
boundParamsHash: boundParams,
|
|
129
|
+
nullifiers: contexts.map((c) => c.nullifier),
|
|
130
|
+
commitmentsOut: allCommitmentsOut,
|
|
131
|
+
token: BigInt(tx.token),
|
|
132
|
+
publicKey: account.spendingKeyPair.pubkey,
|
|
133
|
+
signature: [sig.R8[0], sig.R8[1], sig.S],
|
|
134
|
+
randomIn: inputNotes.map((n) => BigInt(n.random)),
|
|
135
|
+
valueIn: inputNotes.map((n) => BigInt(n.value)),
|
|
136
|
+
pathElements: contexts.map((c) => c.witness.siblings.map((level) => level.map((s) => s))),
|
|
137
|
+
leavesIndices: contexts.map((c) => BigInt(c.index)),
|
|
138
|
+
nullifyingKey: account.nullifyingKey,
|
|
139
|
+
npkOut: allNpkOut,
|
|
140
|
+
valueOut: allValueOut,
|
|
141
|
+
};
|
|
142
|
+
const proofTimeoutMs = opts.proofTimeoutMs ?? DEFAULT_PROOF_TIMEOUT_MS;
|
|
143
|
+
const txProof = await withTimeout(proveTransaction(proofInput, { rpcUrl: opts.rpcUrl }), proofTimeoutMs, new ProofError(`Proof generation timed out after ${proofTimeoutMs}ms`)).catch((e) => {
|
|
144
|
+
if (e instanceof ProofError)
|
|
145
|
+
throw e;
|
|
146
|
+
throw new ProofError(`Proof generation failed: ${e.message}`);
|
|
147
|
+
});
|
|
148
|
+
const proof = {
|
|
149
|
+
pA: [BigInt(txProof.proof.pi_a[0]), BigInt(txProof.proof.pi_a[1])],
|
|
150
|
+
pB: [
|
|
151
|
+
[BigInt(txProof.proof.pi_b[0][1]), BigInt(txProof.proof.pi_b[0][0])],
|
|
152
|
+
[BigInt(txProof.proof.pi_b[1][1]), BigInt(txProof.proof.pi_b[1][0])],
|
|
153
|
+
],
|
|
154
|
+
pC: [BigInt(txProof.proof.pi_c[0]), BigInt(txProof.proof.pi_c[1])],
|
|
155
|
+
pubSignals,
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
proof,
|
|
159
|
+
witnesses: contexts.map((c) => c.witness),
|
|
160
|
+
nullifiers: contexts.map((c) => c.nullifierHex),
|
|
161
|
+
predictedCommitments: outputs.map((o) => o.hex),
|
|
162
|
+
merkleRoot: merkleRoot,
|
|
163
|
+
token: tx.token,
|
|
164
|
+
withdrawal,
|
|
165
|
+
ciphertexts: tx.outputs.map((note) => ({
|
|
166
|
+
data: encryptNote(note).data,
|
|
167
|
+
})),
|
|
168
|
+
contexts,
|
|
169
|
+
outputs,
|
|
170
|
+
inputNotes,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// Public API
|
|
175
|
+
// ============================================================================
|
|
176
|
+
/**
|
|
177
|
+
* Execute private transaction(s): build ZK proofs (in parallel), encrypt outputs, and broadcast.
|
|
178
|
+
* Accepts 1 or more transactions - single tx just passes [{...}].
|
|
179
|
+
*/
|
|
180
|
+
export async function transact(store, req, opts) {
|
|
181
|
+
if (!req.transactions?.length)
|
|
182
|
+
throw new ValidationError("at least one transaction is required");
|
|
183
|
+
const serviceConfig = createServiceConfig(opts.rpcUrl);
|
|
184
|
+
const trees = resolveMerkleTrees(opts);
|
|
185
|
+
const fetchFn = resolveFetch(opts.fetch);
|
|
186
|
+
const broadcaster = fetchFn
|
|
187
|
+
? createBroadcasterClient(serviceConfig.broadcasterBaseUrl, {
|
|
188
|
+
fetch: fetchFn,
|
|
189
|
+
})
|
|
92
190
|
: null;
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
if (note.spentAt !== undefined) {
|
|
108
|
-
throw new CoreError(`note ${input.index} is already spent`);
|
|
109
|
-
}
|
|
110
|
-
const tree = trees.getOrCreate(request.chainId);
|
|
111
|
-
if (input.index >= tree.getLeafCount()) {
|
|
112
|
-
throw new CoreError(`note ${input.index} out of range for tree`);
|
|
113
|
-
}
|
|
114
|
-
const proof = tree.createMerkleProof(input.index);
|
|
115
|
-
const proofRoot = formatUint256(BigInt(proof.root));
|
|
116
|
-
if (!sharedRoot) {
|
|
117
|
-
sharedRoot = proofRoot;
|
|
118
|
-
}
|
|
119
|
-
else if (sharedRoot !== proofRoot) {
|
|
120
|
-
throw new CoreError("input notes must share the same Merkle root");
|
|
121
|
-
}
|
|
122
|
-
const mpk = parseHexToBigInt(note.mpk);
|
|
123
|
-
const random = parseHexToBigInt(note.random);
|
|
124
|
-
const derivedNpk = poseidon([mpk, random]);
|
|
125
|
-
const storedNpk = parseHexToBigInt(note.npk);
|
|
126
|
-
if (storedNpk !== derivedNpk) {
|
|
127
|
-
throw new CoreError(`note ${input.index} npk mismatch`);
|
|
128
|
-
}
|
|
129
|
-
const amount = BigInt(note.value);
|
|
130
|
-
if (amount < 0n) {
|
|
131
|
-
throw new CoreError(`note ${input.index} amount must be non-negative`);
|
|
132
|
-
}
|
|
133
|
-
ensureAddress("note token", note.token);
|
|
134
|
-
const tokenScalar = BigInt(note.token);
|
|
135
|
-
const commitment = poseidon([derivedNpk, tokenScalar, amount]);
|
|
136
|
-
const storedCommitment = parseHexToBigInt(note.commitment);
|
|
137
|
-
if (storedCommitment !== commitment) {
|
|
138
|
-
throw new CoreError(`note ${input.index} commitment mismatch`);
|
|
139
|
-
}
|
|
140
|
-
const proofLeaf = BigInt(proof.leaf);
|
|
141
|
-
if (proofLeaf !== storedCommitment) {
|
|
142
|
-
throw new CoreError(`note ${input.index} proof leaf mismatch`);
|
|
143
|
-
}
|
|
144
|
-
const nullifierValue = poseidon([
|
|
145
|
-
request.zkAccount.nullifyingKey,
|
|
146
|
-
BigInt(input.index),
|
|
147
|
-
]);
|
|
148
|
-
const context = {
|
|
149
|
-
index: input.index,
|
|
150
|
-
nullifier: {
|
|
151
|
-
value: nullifierValue,
|
|
152
|
-
hex: formatUint256(nullifierValue),
|
|
153
|
-
},
|
|
154
|
-
witness: proof,
|
|
155
|
-
};
|
|
156
|
-
contexts.push(context);
|
|
157
|
-
}
|
|
158
|
-
if (!sharedRoot) {
|
|
159
|
-
throw new CoreError("at least one input note is required");
|
|
160
|
-
}
|
|
161
|
-
return { root: sharedRoot, contexts };
|
|
191
|
+
const pollInterval = Math.min(opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
192
|
+
const pollTimeout = opts.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
193
|
+
await rebuildTreeFromStore({
|
|
194
|
+
chainId: req.chainId,
|
|
195
|
+
trees,
|
|
196
|
+
loadLeaf: store.getLeaf.bind(store),
|
|
197
|
+
});
|
|
198
|
+
// Calculate base indices for each transaction's outputs
|
|
199
|
+
let baseIndex = trees.getLeafCount(req.chainId);
|
|
200
|
+
const baseIndices = [];
|
|
201
|
+
for (const tx of req.transactions) {
|
|
202
|
+
baseIndices.push(baseIndex);
|
|
203
|
+
baseIndex += tx.outputs.length;
|
|
162
204
|
}
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
205
|
+
// Generate proofs in PARALLEL for all transactions
|
|
206
|
+
const proofPromises = req.transactions.map((tx, i) => buildSingleTransactionProof(store, req.account, req.chainId, req.poolAddress, tx, trees, baseIndices[i], opts));
|
|
207
|
+
const results = await Promise.all(proofPromises);
|
|
208
|
+
// Build combined calldata
|
|
209
|
+
const calldata = transactInterface.encodeFunctionData("transact", [
|
|
210
|
+
results.map((r) => ({
|
|
211
|
+
proof: { pA: r.proof.pA, pB: r.proof.pB, pC: r.proof.pC },
|
|
212
|
+
merkleRoot: parseHexToBigInt(r.merkleRoot),
|
|
213
|
+
nullifierHashes: r.contexts.map((c) => c.nullifier),
|
|
214
|
+
newCommitments: r.proof.pubSignals.slice(2 + r.contexts.length),
|
|
215
|
+
context: { chainId: BigInt(req.chainId), poolAddress: req.poolAddress },
|
|
216
|
+
withdrawal: r.withdrawal,
|
|
217
|
+
ciphertexts: r.ciphertexts,
|
|
218
|
+
})),
|
|
219
|
+
]);
|
|
220
|
+
const relayId = globalThis.crypto?.randomUUID?.() ?? `tx-${Date.now().toString(16)}`;
|
|
221
|
+
// Determine if this is a withdrawal (any transaction has withdrawal)
|
|
222
|
+
const hasAnyWithdrawal = results.some((r) => r.withdrawal.amount > 0n);
|
|
223
|
+
const historyKind = hasAnyWithdrawal ? "Withdraw" : "Send";
|
|
224
|
+
// Compute net deltas per token for history preview
|
|
225
|
+
const deltasByToken = new Map();
|
|
226
|
+
for (let i = 0; i < req.transactions.length; i++) {
|
|
227
|
+
const tx = req.transactions[i];
|
|
228
|
+
const r = results[i];
|
|
229
|
+
const totalOutputsToRecipients = tx.outputs
|
|
230
|
+
.filter((o) => o.owner === "recipient")
|
|
231
|
+
.reduce((sum, o) => sum + o.amount, 0n);
|
|
232
|
+
// For withdrawals: user loses the withdrawal amount
|
|
233
|
+
// For sends: user loses what they send to recipients (change returns to them)
|
|
234
|
+
const netDelta = r.withdrawal.amount > 0n
|
|
235
|
+
? -r.withdrawal.amount
|
|
236
|
+
: -totalOutputsToRecipients;
|
|
237
|
+
const existing = deltasByToken.get(tx.token) ?? 0n;
|
|
238
|
+
deltasByToken.set(tx.token, existing + netDelta);
|
|
173
239
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
});
|
|
240
|
+
const jobBase = {
|
|
241
|
+
relayId,
|
|
242
|
+
chainId: req.chainId,
|
|
243
|
+
status: "pending",
|
|
244
|
+
txHash: null,
|
|
245
|
+
createdAt: Date.now(),
|
|
246
|
+
timeoutMs: DEFAULT_JOB_TIMEOUT_MS,
|
|
247
|
+
poolAddress: req.poolAddress,
|
|
248
|
+
calldata,
|
|
249
|
+
transactions: results.map((r, i) => {
|
|
250
|
+
const tx = req.transactions[i];
|
|
186
251
|
return {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (!indexerClient) {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
const pending = new Set(outputs.map((output) => output.hex.toLowerCase()));
|
|
198
|
-
const records = new Map();
|
|
199
|
-
let delay = pollIntervalMs;
|
|
200
|
-
const deadline = Date.now() + pollTimeoutMs;
|
|
201
|
-
while (pending.size > 0 && Date.now() <= deadline) {
|
|
202
|
-
for (const commitment of [...pending]) {
|
|
203
|
-
const record = await indexerClient
|
|
204
|
-
.getCommitment({ chainId, commitment })
|
|
205
|
-
.catch((err) => {
|
|
206
|
-
if (!isNotFoundError(err)) {
|
|
207
|
-
throw err;
|
|
252
|
+
token: tx.token,
|
|
253
|
+
nullifiers: r.nullifiers,
|
|
254
|
+
predictedCommitments: r.outputs.map((o) => ({ hex: o.hex })),
|
|
255
|
+
withdrawal: r.withdrawal.amount > 0n
|
|
256
|
+
? {
|
|
257
|
+
amount: r.withdrawal.amount.toString(),
|
|
258
|
+
recipient: r.withdrawal.npk.toString(),
|
|
208
259
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
throw new CoreError(`missing indexed commitment ${output.hex} after fetch`);
|
|
231
|
-
}
|
|
232
|
-
return record;
|
|
260
|
+
: undefined,
|
|
261
|
+
};
|
|
262
|
+
}),
|
|
263
|
+
historyPreview: {
|
|
264
|
+
kind: historyKind,
|
|
265
|
+
amounts: [...deltasByToken.entries()].map(([token, delta]) => ({
|
|
266
|
+
token,
|
|
267
|
+
delta: delta.toString(),
|
|
268
|
+
})),
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
const job = hasAnyWithdrawal
|
|
272
|
+
? { ...jobBase, kind: "withdraw" }
|
|
273
|
+
: { ...jobBase, kind: "transfer" };
|
|
274
|
+
// Submit to broadcaster
|
|
275
|
+
let txHash = null;
|
|
276
|
+
if (broadcaster) {
|
|
277
|
+
const submission = await broadcaster.submitRelay({
|
|
278
|
+
clientTxId: relayId,
|
|
279
|
+
chainId: req.chainId,
|
|
280
|
+
payload: { kind: "call_data", to: req.poolAddress, data: calldata },
|
|
233
281
|
});
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const submission = await broadcasterClient.submitRelay({
|
|
241
|
-
clientTxId: relayId,
|
|
242
|
-
chainId: pending.chainId,
|
|
243
|
-
payload: {
|
|
244
|
-
kind: "call_data",
|
|
245
|
-
to: pending.poolAddress,
|
|
246
|
-
data: pending.calldata,
|
|
247
|
-
},
|
|
282
|
+
if (!submission.accepted) {
|
|
283
|
+
await store.putJob({
|
|
284
|
+
...job,
|
|
285
|
+
status: "failed",
|
|
286
|
+
lastCheckedAt: Date.now(),
|
|
287
|
+
error: submission.message ?? "broadcaster rejected",
|
|
248
288
|
});
|
|
249
|
-
|
|
250
|
-
throw new CoreError(submission.message ??
|
|
251
|
-
"broadcaster rejected transaction relay submission");
|
|
252
|
-
}
|
|
253
|
-
pending.broadcasterRelayId = submission.id;
|
|
289
|
+
throw new CoreError(submission.message ?? "broadcaster rejected");
|
|
254
290
|
}
|
|
255
|
-
const deadline = Date.now() +
|
|
291
|
+
const deadline = Date.now() + pollTimeout;
|
|
256
292
|
while (Date.now() <= deadline) {
|
|
257
|
-
const status = await
|
|
293
|
+
const status = await broadcaster.getRelayStatus(relayId);
|
|
258
294
|
if (status.state === "succeeded") {
|
|
259
|
-
|
|
260
|
-
|
|
295
|
+
txHash = status.txHash ?? null;
|
|
296
|
+
break;
|
|
261
297
|
}
|
|
262
298
|
if (status.state === "failed" || status.state === "dead") {
|
|
299
|
+
await store.putJob({
|
|
300
|
+
...job,
|
|
301
|
+
status: "failed",
|
|
302
|
+
lastCheckedAt: Date.now(),
|
|
303
|
+
error: status.error ?? "broadcaster relay failed",
|
|
304
|
+
});
|
|
263
305
|
throw new CoreError(status.error ?? "broadcaster relay failed");
|
|
264
306
|
}
|
|
265
|
-
await sleep(
|
|
307
|
+
await sleep(pollInterval);
|
|
308
|
+
}
|
|
309
|
+
if (!txHash && Date.now() > deadline) {
|
|
310
|
+
await store.putJob({
|
|
311
|
+
...job,
|
|
312
|
+
status: "failed",
|
|
313
|
+
lastCheckedAt: Date.now(),
|
|
314
|
+
error: "broadcaster relay timed out",
|
|
315
|
+
});
|
|
316
|
+
throw new CoreError("broadcaster relay timed out");
|
|
266
317
|
}
|
|
267
|
-
throw new CoreError("broadcaster relay timed out");
|
|
268
318
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
319
|
+
await store.putJob({
|
|
320
|
+
...job,
|
|
321
|
+
status: "broadcasting",
|
|
322
|
+
txHash,
|
|
323
|
+
});
|
|
324
|
+
// Build result with individual transaction results
|
|
325
|
+
const transactionResults = results.map((r) => ({
|
|
326
|
+
proof: r.proof,
|
|
327
|
+
witnesses: r.witnesses,
|
|
328
|
+
nullifiers: r.nullifiers,
|
|
329
|
+
predictedCommitments: r.predictedCommitments,
|
|
330
|
+
}));
|
|
331
|
+
return {
|
|
332
|
+
relayId,
|
|
333
|
+
calldata,
|
|
334
|
+
transactions: transactionResults,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Wait for a pending transaction to be indexed and update local state.
|
|
339
|
+
*/
|
|
340
|
+
export async function syncTransact(store, relayId, opts) {
|
|
341
|
+
const record = await store.getJob(relayId);
|
|
342
|
+
if (!record || (record.kind !== "transfer" && record.kind !== "withdraw"))
|
|
343
|
+
throw new CoreError(`unknown pool transaction relay ${relayId}`);
|
|
344
|
+
const job = record;
|
|
345
|
+
const serviceConfig = createServiceConfig(opts.rpcUrl);
|
|
346
|
+
const trees = resolveMerkleTrees(opts);
|
|
347
|
+
const fetchFn = resolveFetch(opts.fetch);
|
|
348
|
+
const indexer = fetchFn
|
|
349
|
+
? createIndexerClient(serviceConfig.indexerBaseUrl, { fetch: fetchFn })
|
|
350
|
+
: null;
|
|
351
|
+
const pollInterval = Math.min(opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
352
|
+
const pollTimeout = opts.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
353
|
+
await rebuildTreeFromStore({
|
|
354
|
+
chainId: job.chainId,
|
|
355
|
+
trees,
|
|
356
|
+
loadLeaf: store.getLeaf.bind(store),
|
|
357
|
+
});
|
|
358
|
+
// Collect all predicted commitments from all transactions
|
|
359
|
+
const allPredictedOutputs = job.transactions.flatMap((tx) => tx.predictedCommitments.map((c) => c.hex));
|
|
360
|
+
// Wait for indexed commitments
|
|
361
|
+
const indexed = [];
|
|
362
|
+
if (indexer && allPredictedOutputs.length > 0) {
|
|
363
|
+
const pending = new Set(allPredictedOutputs.map((h) => h.toLowerCase()));
|
|
364
|
+
const deadline = Date.now() + pollTimeout;
|
|
365
|
+
let delay = pollInterval;
|
|
366
|
+
while (pending.size > 0 && Date.now() <= deadline) {
|
|
367
|
+
for (const hex of [...pending]) {
|
|
368
|
+
const rec = await indexer
|
|
369
|
+
.getCommitment({ chainId: job.chainId, commitment: hex })
|
|
370
|
+
.catch((e) => {
|
|
371
|
+
if (!isNotFoundError(e))
|
|
372
|
+
throw e;
|
|
373
|
+
return null;
|
|
285
374
|
});
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
375
|
+
if (rec) {
|
|
376
|
+
indexed.push(rec);
|
|
377
|
+
pending.delete(hex);
|
|
378
|
+
}
|
|
290
379
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
throw new CoreError(`local merkle tree desynchronized, indexed ${entry.index}, local ${index}`);
|
|
380
|
+
if (pending.size > 0) {
|
|
381
|
+
await sleep(delay);
|
|
382
|
+
delay = Math.min(delay * 2, MAX_POLL_INTERVAL_MS);
|
|
295
383
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
384
|
+
}
|
|
385
|
+
if (pending.size > 0) {
|
|
386
|
+
await store.putJob({
|
|
387
|
+
...job,
|
|
388
|
+
status: "failed",
|
|
389
|
+
lastCheckedAt: Date.now(),
|
|
390
|
+
error: "commitments not indexed before timeout",
|
|
301
391
|
});
|
|
302
|
-
|
|
303
|
-
stateStore.putLeaf({
|
|
304
|
-
chainId,
|
|
305
|
-
index,
|
|
306
|
-
commitment: entry.commitment,
|
|
307
|
-
}),
|
|
308
|
-
stateStore.putRoot({
|
|
309
|
-
chainId,
|
|
310
|
-
root,
|
|
311
|
-
}),
|
|
312
|
-
]);
|
|
392
|
+
throw new CoreError("commitments not indexed before timeout");
|
|
313
393
|
}
|
|
314
|
-
return applied;
|
|
315
|
-
}
|
|
316
|
-
async function persistTransactSuccess(job, pending, persisted) {
|
|
317
|
-
await stateStore.putRoot({
|
|
318
|
-
chainId: pending.chainId,
|
|
319
|
-
root: persisted.latestRoot,
|
|
320
|
-
});
|
|
321
|
-
await stateStore.putPendingJob({
|
|
322
|
-
...job,
|
|
323
|
-
status: "succeeded",
|
|
324
|
-
broadcasterRelayId: pending.broadcasterRelayId ?? job.broadcasterRelayId,
|
|
325
|
-
txHash: pending.txHash ?? job.txHash ?? null,
|
|
326
|
-
lastCheckedAt: Date.now(),
|
|
327
|
-
predictedOutputs: job.predictedOutputs.map((output) => {
|
|
328
|
-
const matched = persisted.rootedCommitments.find((rc) => rc.hex.toLowerCase() === output.hex.toLowerCase());
|
|
329
|
-
return {
|
|
330
|
-
...output,
|
|
331
|
-
index: matched?.index ?? output.index,
|
|
332
|
-
root: matched?.root ?? output.root,
|
|
333
|
-
};
|
|
334
|
-
}),
|
|
335
|
-
expectedRoot: persisted.latestRoot,
|
|
336
|
-
});
|
|
337
394
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
395
|
+
// Get latest root from indexed records (syncChain handles actual storage)
|
|
396
|
+
const sortedIndexed = indexed.sort((a, b) => a.index - b.index);
|
|
397
|
+
const latestRoot = sortedIndexed.at(-1)?.root ?? trees.getRoot(job.chainId);
|
|
398
|
+
// Collect all nullifiers from all transactions and mark notes as spent
|
|
399
|
+
const allNullifiers = job.transactions.flatMap((tx) => tx.nullifiers);
|
|
400
|
+
const timestamp = Date.now();
|
|
401
|
+
// Look up note indices by matching nullifiers to notes in the store
|
|
402
|
+
const allNotes = await store.listNotes({
|
|
403
|
+
chainId: job.chainId,
|
|
404
|
+
includeSpent: true,
|
|
405
|
+
});
|
|
406
|
+
const nullifierToNoteIndex = new Map();
|
|
407
|
+
for (const note of allNotes) {
|
|
408
|
+
if (note.nullifier) {
|
|
409
|
+
nullifierToNoteIndex.set(note.nullifier, note.index);
|
|
341
410
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
});
|
|
352
|
-
const { root, contexts } = await buildInputContexts(request);
|
|
353
|
-
const assignedOutputs = computeAssignedCommitments(request.chainId, request.outputs);
|
|
354
|
-
// Compute bound parameters hash
|
|
355
|
-
const boundParamsHash = computeBoundParamsHash(request.chainId, request.poolAddress);
|
|
356
|
-
// Public signals must match the circuit's public input order:
|
|
357
|
-
// [merkleRoot, boundParamsHash, ...nullifiers, ...commitmentsOut]
|
|
358
|
-
const pubSignals = [
|
|
359
|
-
parseHexToBigInt(root),
|
|
360
|
-
boundParamsHash,
|
|
361
|
-
...contexts.map((context) => context.nullifier.value),
|
|
362
|
-
...assignedOutputs.map((output) => output.value),
|
|
363
|
-
];
|
|
364
|
-
// Retrieve input notes to access random and value fields
|
|
365
|
-
const inputNotes = await Promise.all(request.inputs.map(async (input) => {
|
|
366
|
-
const note = await stateStore.getNote(request.chainId, input.index);
|
|
367
|
-
if (!note) {
|
|
368
|
-
throw new CoreError(`note ${input.index} not found for chain`);
|
|
369
|
-
}
|
|
370
|
-
return note;
|
|
371
|
-
}));
|
|
372
|
-
// Generate signature over the message (hash of public signals)
|
|
373
|
-
const message = poseidon(pubSignals);
|
|
374
|
-
const signature = signTransactMessage(request.zkAccount.spendingKeyPair.privateKey, message);
|
|
375
|
-
// Construct the complete raw input for the prover
|
|
376
|
-
const rawInput = {
|
|
377
|
-
merkleRoot: root,
|
|
378
|
-
boundParamsHash: boundParamsHash.toString(),
|
|
379
|
-
nullifiers: contexts.map((c) => c.nullifier.hex),
|
|
380
|
-
commitmentsOut: assignedOutputs.map((o) => o.hex),
|
|
381
|
-
token: request.token,
|
|
382
|
-
publicKey: request.zkAccount.spendingKeyPair.pubkey.map((pk) => pk.toString()),
|
|
383
|
-
signature: [
|
|
384
|
-
signature.R8[0].toString(),
|
|
385
|
-
signature.R8[1].toString(),
|
|
386
|
-
signature.S.toString(),
|
|
387
|
-
],
|
|
388
|
-
randomIn: inputNotes.map((note) => note.random),
|
|
389
|
-
valueIn: inputNotes.map((note) => note.value),
|
|
390
|
-
pathElements: contexts.map((c) => c.witness.siblings.map((s) => s.toString())),
|
|
391
|
-
leavesIndices: contexts.map((c) => c.index),
|
|
392
|
-
nullifyingKey: formatUint256(request.zkAccount.nullifyingKey),
|
|
393
|
-
npkOut: request.outputs.map((o) => formatUint256(poseidon([o.mpk, o.random]))),
|
|
394
|
-
valueOut: request.outputs.map((o) => o.amount.toString()),
|
|
395
|
-
};
|
|
396
|
-
const proofInput = {
|
|
397
|
-
merkleRoot: BigInt(rawInput.merkleRoot),
|
|
398
|
-
boundParamsHash: BigInt(rawInput.boundParamsHash),
|
|
399
|
-
nullifiers: rawInput.nullifiers.map((n) => BigInt(n)),
|
|
400
|
-
commitmentsOut: rawInput.commitmentsOut.map((c) => BigInt(c)),
|
|
401
|
-
token: BigInt(rawInput.token),
|
|
402
|
-
publicKey: rawInput.publicKey.map((pk) => BigInt(pk)),
|
|
403
|
-
signature: rawInput.signature.map((s) => BigInt(s)),
|
|
404
|
-
randomIn: rawInput.randomIn.map((r) => BigInt(r)),
|
|
405
|
-
valueIn: rawInput.valueIn.map((v) => BigInt(v)),
|
|
406
|
-
pathElements: rawInput.pathElements.map((pe) => pe.map((e) => BigInt(e))),
|
|
407
|
-
leavesIndices: rawInput.leavesIndices.map((i) => BigInt(i)),
|
|
408
|
-
nullifyingKey: BigInt(rawInput.nullifyingKey),
|
|
409
|
-
npkOut: rawInput.npkOut.map((npk) => BigInt(npk)),
|
|
410
|
-
valueOut: rawInput.valueOut.map((v) => BigInt(v)),
|
|
411
|
-
};
|
|
412
|
-
const txProof = await proveTransaction(proofInput).catch((e) => {
|
|
413
|
-
throw new CoreError(`Proof generation failed: ${e.message}`);
|
|
414
|
-
});
|
|
415
|
-
ensureWithdrawalInput("request.withdrawal", request.withdrawal);
|
|
416
|
-
const proof = {
|
|
417
|
-
pA: [BigInt(txProof.proof.pi_a[0]), BigInt(txProof.proof.pi_a[1])],
|
|
418
|
-
pB: [
|
|
419
|
-
[
|
|
420
|
-
BigInt(txProof.proof.pi_b[0][1]),
|
|
421
|
-
BigInt(txProof.proof.pi_b[0][0]),
|
|
422
|
-
],
|
|
423
|
-
[
|
|
424
|
-
BigInt(txProof.proof.pi_b[1][1]),
|
|
425
|
-
BigInt(txProof.proof.pi_b[1][0]),
|
|
426
|
-
],
|
|
427
|
-
],
|
|
428
|
-
pC: [BigInt(txProof.proof.pi_c[0]), BigInt(txProof.proof.pi_c[1])],
|
|
429
|
-
pubSignals,
|
|
430
|
-
};
|
|
431
|
-
const calldata = transactInterface.encodeFunctionData("transact", [
|
|
432
|
-
[
|
|
433
|
-
{
|
|
434
|
-
proof: { ...proof, pubSignals: undefined },
|
|
435
|
-
merkleRoot: parseHexToBigInt(root),
|
|
436
|
-
nullifierHashes: contexts.map((context) => context.nullifier.value),
|
|
437
|
-
newCommitments: assignedOutputs.map((output) => output.value),
|
|
438
|
-
context: {
|
|
439
|
-
chainID: BigInt(request.chainId),
|
|
440
|
-
poolAddress: request.poolAddress,
|
|
441
|
-
},
|
|
442
|
-
withdrawal: request.withdrawal,
|
|
443
|
-
ciphertexts: request.outputs.map((note) => ({
|
|
444
|
-
data: encryptNote(note).data,
|
|
445
|
-
})),
|
|
446
|
-
},
|
|
447
|
-
],
|
|
448
|
-
]);
|
|
449
|
-
const relayId = allocateRelayId();
|
|
450
|
-
const job = {
|
|
451
|
-
relayId,
|
|
452
|
-
kind: "transact",
|
|
453
|
-
chainId: request.chainId,
|
|
454
|
-
status: "pending",
|
|
455
|
-
broadcasterRelayId: null,
|
|
456
|
-
txHash: null,
|
|
457
|
-
createdAt: Date.now(),
|
|
458
|
-
timeoutMs: DEFAULT_JOB_TIMEOUT_MS,
|
|
459
|
-
poolAddress: request.poolAddress,
|
|
460
|
-
calldata,
|
|
461
|
-
contexts: contexts.map((context) => ({
|
|
462
|
-
index: context.index,
|
|
463
|
-
nullifier: context.nullifier.hex,
|
|
464
|
-
witness: serializeWitness(context.witness, context.index),
|
|
465
|
-
root,
|
|
466
|
-
})),
|
|
467
|
-
predictedOutputs: assignedOutputs.map((output) => ({
|
|
468
|
-
hex: output.hex,
|
|
469
|
-
index: output.index,
|
|
470
|
-
})),
|
|
471
|
-
expectedRoot: root,
|
|
472
|
-
};
|
|
473
|
-
// Submit to broadcaster immediately so reconciliation never has to send the relay.
|
|
474
|
-
const pendingForBroadcast = {
|
|
475
|
-
chainId: job.chainId,
|
|
476
|
-
root,
|
|
477
|
-
contexts,
|
|
478
|
-
assignedOutputs,
|
|
479
|
-
poolAddress: job.poolAddress,
|
|
480
|
-
calldata: job.calldata,
|
|
481
|
-
broadcasterRelayId: job.broadcasterRelayId,
|
|
482
|
-
txHash: job.txHash,
|
|
483
|
-
};
|
|
484
|
-
await submitAndAwaitBroadcasterRelay(relayId, pendingForBroadcast);
|
|
485
|
-
await stateStore.putPendingJob({
|
|
411
|
+
}
|
|
412
|
+
// Resolve every nullifier to its note index, fail fast if any is unknown
|
|
413
|
+
const resolved = [];
|
|
414
|
+
for (const nullifier of allNullifiers) {
|
|
415
|
+
const noteIndex = nullifierToNoteIndex.get(nullifier);
|
|
416
|
+
if (noteIndex === undefined) {
|
|
417
|
+
const short = nullifier.slice(0, 10) + "…";
|
|
418
|
+
const error = `Unknown nullifier ${short} - note not found in local state. This may indicate a sync issue.`;
|
|
419
|
+
await store.putJob({
|
|
486
420
|
...job,
|
|
487
|
-
status: "
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
});
|
|
491
|
-
return {
|
|
492
|
-
relayId,
|
|
493
|
-
calldata,
|
|
494
|
-
proof,
|
|
495
|
-
witnesses: contexts.map((context) => context.witness),
|
|
496
|
-
nullifiers: contexts.map((context) => context.nullifier.hex),
|
|
497
|
-
predictedCommitments: assignedOutputs.map((output) => output.hex),
|
|
498
|
-
};
|
|
499
|
-
},
|
|
500
|
-
async syncPendingTransact(relayId) {
|
|
501
|
-
const job = await stateStore.getPendingJob(relayId);
|
|
502
|
-
if (!job || job.kind !== "transact") {
|
|
503
|
-
throw new Error(`unknown transact relay ${relayId}`);
|
|
504
|
-
}
|
|
505
|
-
const contexts = job.contexts.map((context) => ({
|
|
506
|
-
index: context.index,
|
|
507
|
-
nullifier: {
|
|
508
|
-
value: parseHexToBigInt(context.nullifier),
|
|
509
|
-
hex: context.nullifier,
|
|
510
|
-
},
|
|
511
|
-
witness: deserializeWitness(context.witness),
|
|
512
|
-
}));
|
|
513
|
-
const assignedOutputs = job.predictedOutputs.map((output) => ({
|
|
514
|
-
hex: output.hex,
|
|
515
|
-
value: parseHexToBigInt(output.hex),
|
|
516
|
-
index: output.index,
|
|
517
|
-
}));
|
|
518
|
-
const pending = {
|
|
519
|
-
chainId: job.chainId,
|
|
520
|
-
root: job.expectedRoot ??
|
|
521
|
-
job.contexts[0]?.root ??
|
|
522
|
-
trees.getRoot(job.chainId),
|
|
523
|
-
contexts,
|
|
524
|
-
assignedOutputs,
|
|
525
|
-
poolAddress: job.poolAddress,
|
|
526
|
-
calldata: job.calldata,
|
|
527
|
-
broadcasterRelayId: job.broadcasterRelayId ?? null,
|
|
528
|
-
txHash: job.txHash ?? null,
|
|
529
|
-
};
|
|
530
|
-
await rebuildTreeFromStore({
|
|
531
|
-
chainId: job.chainId,
|
|
532
|
-
trees,
|
|
533
|
-
loadLeaf: stateStore.getLeaf.bind(stateStore),
|
|
534
|
-
});
|
|
535
|
-
await stateStore.putRoot({
|
|
536
|
-
chainId: pending.chainId,
|
|
537
|
-
root: pending.root,
|
|
538
|
-
});
|
|
539
|
-
const indexedRecords = await waitForIndexedCommitments(pending.chainId, pending.assignedOutputs);
|
|
540
|
-
const rootedCommitments = await applyIndexerUpdates(pending.chainId, indexedRecords);
|
|
541
|
-
const timestamp = Date.now();
|
|
542
|
-
await persistNullifiersAndSpend(pending.chainId, pending.contexts, timestamp);
|
|
543
|
-
const lastCommitment = rootedCommitments[rootedCommitments.length - 1] ?? null;
|
|
544
|
-
const latestRoot = lastCommitment
|
|
545
|
-
? lastCommitment.root
|
|
546
|
-
: trees.getRoot(pending.chainId);
|
|
547
|
-
await persistTransactSuccess(job, pending, {
|
|
548
|
-
rootedCommitments,
|
|
549
|
-
latestRoot,
|
|
421
|
+
status: "failed",
|
|
422
|
+
lastCheckedAt: Date.now(),
|
|
423
|
+
error,
|
|
550
424
|
});
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
425
|
+
throw new CoreError(error);
|
|
426
|
+
}
|
|
427
|
+
resolved.push({ nullifier, noteIndex });
|
|
428
|
+
}
|
|
429
|
+
const txHash = job.txHash ?? undefined;
|
|
430
|
+
await Promise.all(resolved.flatMap(({ nullifier, noteIndex }) => [
|
|
431
|
+
store.putNullifier({ chainId: job.chainId, nullifier, noteIndex }),
|
|
432
|
+
store.markNoteSpent(job.chainId, noteIndex, timestamp, txHash),
|
|
433
|
+
]));
|
|
434
|
+
await store.putJob({
|
|
435
|
+
...job,
|
|
436
|
+
status: "succeeded",
|
|
437
|
+
lastCheckedAt: timestamp,
|
|
438
|
+
});
|
|
439
|
+
return {
|
|
440
|
+
chainId: job.chainId,
|
|
441
|
+
root: latestRoot,
|
|
442
|
+
nullifiers: allNullifiers,
|
|
443
|
+
newCommitments: indexed.map((r) => r.commitment),
|
|
444
|
+
txHash: job.txHash ?? undefined,
|
|
445
|
+
indexedCommitments: indexed,
|
|
560
446
|
};
|
|
561
447
|
}
|