@unlink-xyz/core 0.1.3-canary.fd5dddf → 0.1.4
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/README.md +9 -0
- package/dist/account/account.d.ts +31 -2
- package/dist/account/account.d.ts.map +1 -1
- package/dist/account/accounts.d.ts +42 -0
- package/dist/account/accounts.d.ts.map +1 -0
- package/dist/account/seed.d.ts +45 -0
- package/dist/account/seed.d.ts.map +1 -0
- package/dist/account/serialization.d.ts +6 -0
- package/dist/account/serialization.d.ts.map +1 -0
- package/dist/browser/index.js +34424 -86406
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/wallet/index.js +55942 -0
- package/dist/browser/wallet/index.js.map +1 -0
- package/dist/clients/broadcaster.d.ts +1 -0
- package/dist/clients/broadcaster.d.ts.map +1 -1
- package/dist/clients/indexer.d.ts +5 -0
- package/dist/clients/indexer.d.ts.map +1 -1
- package/dist/config.d.ts +6 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/crypto/adapters/index.d.ts +17 -0
- package/dist/crypto/adapters/index.d.ts.map +1 -0
- package/dist/crypto/adapters/polyfills.d.ts +5 -0
- package/dist/crypto/adapters/polyfills.d.ts.map +1 -0
- package/dist/crypto/encrypt.d.ts +33 -0
- package/dist/crypto/encrypt.d.ts.map +1 -0
- package/dist/crypto/secure-memory.d.ts.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6721 -23
- package/dist/index.js.map +1 -0
- package/dist/keys/derive.d.ts +2 -2
- package/dist/keys/derive.d.ts.map +1 -1
- package/dist/keys/hex.d.ts +1 -4
- package/dist/keys/hex.d.ts.map +1 -1
- package/dist/keys/mnemonic.d.ts +0 -2
- package/dist/keys/mnemonic.d.ts.map +1 -1
- package/dist/keys.d.ts +1 -0
- package/dist/keys.d.ts.map +1 -1
- package/dist/prover/config.d.ts +54 -9
- package/dist/prover/config.d.ts.map +1 -1
- package/dist/prover/integrity.d.ts +20 -0
- package/dist/prover/integrity.d.ts.map +1 -0
- package/dist/prover/prover.d.ts +16 -31
- package/dist/prover/prover.d.ts.map +1 -1
- package/dist/state/merkle/hydrator.d.ts +21 -19
- package/dist/state/merkle/hydrator.d.ts.map +1 -1
- package/dist/state/merkle/index.d.ts +1 -1
- package/dist/state/merkle/index.d.ts.map +1 -1
- package/dist/state/store/ciphertext-store.d.ts +7 -0
- package/dist/state/store/ciphertext-store.d.ts.map +1 -1
- package/dist/state/store/index.d.ts +1 -1
- package/dist/state/store/index.d.ts.map +1 -1
- package/dist/state/store/job-store.d.ts.map +1 -1
- package/dist/state/store/jobs.d.ts +14 -16
- package/dist/state/store/jobs.d.ts.map +1 -1
- package/dist/state/store/leaf-store.d.ts +4 -0
- package/dist/state/store/leaf-store.d.ts.map +1 -1
- package/dist/state/store/nullifier-store.d.ts.map +1 -1
- package/dist/state/store/records.d.ts +8 -0
- package/dist/state/store/records.d.ts.map +1 -1
- package/dist/state/store/store.d.ts +18 -0
- package/dist/state/store/store.d.ts.map +1 -1
- package/dist/storage/indexeddb.d.ts.map +1 -1
- package/dist/storage/memory.d.ts.map +1 -1
- package/dist/transactions/adapter.d.ts +31 -0
- package/dist/transactions/adapter.d.ts.map +1 -0
- package/dist/transactions/deposit.d.ts +1 -1
- package/dist/transactions/deposit.d.ts.map +1 -1
- package/dist/transactions/index.d.ts +4 -2
- package/dist/transactions/index.d.ts.map +1 -1
- package/dist/transactions/note-sync.d.ts +3 -3
- package/dist/transactions/note-sync.d.ts.map +1 -1
- package/dist/transactions/reconcile.d.ts +1 -1
- package/dist/transactions/reconcile.d.ts.map +1 -1
- package/dist/transactions/transact.d.ts +21 -2
- package/dist/transactions/transact.d.ts.map +1 -1
- package/dist/transactions/transaction-planner.d.ts +1 -1
- package/dist/transactions/transaction-planner.d.ts.map +1 -1
- package/dist/transactions/transfer-planner.d.ts +2 -1
- package/dist/transactions/transfer-planner.d.ts.map +1 -1
- package/dist/transactions/types/deposit.d.ts +3 -3
- package/dist/transactions/types/deposit.d.ts.map +1 -1
- package/dist/transactions/types/domain.d.ts +3 -0
- package/dist/transactions/types/domain.d.ts.map +1 -1
- package/dist/transactions/types/options.d.ts +14 -5
- package/dist/transactions/types/options.d.ts.map +1 -1
- package/dist/transactions/types/planning.d.ts +2 -0
- package/dist/transactions/types/planning.d.ts.map +1 -1
- package/dist/transactions/types/state-stores.d.ts +53 -5
- package/dist/transactions/types/state-stores.d.ts.map +1 -1
- package/dist/transactions/types/transact.d.ts +10 -3
- package/dist/transactions/types/transact.d.ts.map +1 -1
- package/dist/transactions/withdrawal-planner.d.ts +1 -1
- package/dist/transactions/withdrawal-planner.d.ts.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/tsup.config.d.ts +8 -0
- package/dist/tsup.config.d.ts.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/amounts.d.ts +0 -13
- package/dist/utils/amounts.d.ts.map +1 -1
- package/dist/utils/async.js +37 -34
- package/dist/utils/async.js.map +1 -0
- package/dist/utils/bigint.d.ts +0 -2
- package/dist/utils/bigint.d.ts.map +1 -1
- package/dist/utils/random.d.ts +5 -0
- package/dist/utils/random.d.ts.map +1 -1
- package/dist/utils/validators.d.ts.map +1 -1
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/wallet/adapter.d.ts +21 -0
- package/dist/wallet/adapter.d.ts.map +1 -0
- package/dist/wallet/burner/service.d.ts +32 -0
- package/dist/wallet/burner/service.d.ts.map +1 -0
- package/dist/wallet/burner/types.d.ts +47 -0
- package/dist/wallet/burner/types.d.ts.map +1 -0
- package/dist/wallet/index.d.ts +20 -0
- package/dist/wallet/index.d.ts.map +1 -0
- package/dist/wallet/index.js +6462 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/sdk.d.ts +48 -0
- package/dist/wallet/sdk.d.ts.map +1 -0
- package/dist/wallet/types.d.ts +457 -0
- package/dist/wallet/types.d.ts.map +1 -0
- package/dist/wallet/unlink-wallet.d.ts +187 -0
- package/dist/wallet/unlink-wallet.d.ts.map +1 -0
- package/package.json +16 -6
- package/dist/account/account.js +0 -142
- package/dist/circuits.json +0 -74
- package/dist/clients/broadcaster.js +0 -30
- package/dist/clients/http.js +0 -72
- package/dist/clients/indexer.js +0 -94
- package/dist/config.js +0 -36
- package/dist/constants.js +0 -5
- package/dist/core.js +0 -15
- package/dist/crypto-adapters/auto-init.d.ts +0 -2
- package/dist/crypto-adapters/auto-init.d.ts.map +0 -1
- package/dist/crypto-adapters/auto-init.js +0 -7
- package/dist/crypto-adapters/index.d.ts +0 -22
- package/dist/crypto-adapters/index.d.ts.map +0 -1
- package/dist/crypto-adapters/index.js +0 -47
- package/dist/crypto-adapters/polyfills.d.ts +0 -5
- package/dist/crypto-adapters/polyfills.d.ts.map +0 -1
- package/dist/crypto-adapters/polyfills.js +0 -8
- package/dist/errors.js +0 -36
- package/dist/history/index.js +0 -2
- package/dist/history/service.js +0 -354
- package/dist/history/types.js +0 -1
- package/dist/keys/address.js +0 -55
- package/dist/keys/derive.js +0 -112
- package/dist/keys/hex.js +0 -66
- package/dist/keys/index.js +0 -4
- package/dist/keys/mnemonic.js +0 -23
- package/dist/keys.js +0 -45
- package/dist/prover/config.js +0 -70
- package/dist/prover/index.js +0 -1
- package/dist/prover/prover.js +0 -291
- package/dist/prover/registry.js +0 -18
- package/dist/schema.js +0 -14
- package/dist/state/index.js +0 -2
- package/dist/state/merkle/hydrator.js +0 -37
- package/dist/state/merkle/index.js +0 -2
- package/dist/state/merkle/merkle-tree.js +0 -113
- package/dist/state/store/ciphertext-store.js +0 -37
- package/dist/state/store/history-store.js +0 -53
- package/dist/state/store/index.js +0 -9
- package/dist/state/store/job-store.js +0 -144
- package/dist/state/store/jobs.js +0 -1
- package/dist/state/store/leaf-store.js +0 -32
- package/dist/state/store/note-store.js +0 -146
- package/dist/state/store/nullifier-store.js +0 -60
- package/dist/state/store/records.js +0 -1
- package/dist/state/store/root-store.js +0 -26
- package/dist/state/store/store.js +0 -113
- package/dist/storage/index.js +0 -2
- package/dist/storage/indexeddb.js +0 -205
- package/dist/storage/memory.js +0 -91
- package/dist/transactions/deposit.js +0 -220
- package/dist/transactions/index.js +0 -9
- package/dist/transactions/note-selection.js +0 -201
- package/dist/transactions/note-sync.js +0 -485
- package/dist/transactions/reconcile.js +0 -85
- package/dist/transactions/transact.js +0 -450
- package/dist/transactions/transaction-planner.js +0 -116
- package/dist/transactions/transfer-planner.js +0 -85
- package/dist/transactions/types/deposit.js +0 -1
- package/dist/transactions/types/domain.js +0 -4
- package/dist/transactions/types/index.js +0 -17
- package/dist/transactions/types/options.js +0 -1
- package/dist/transactions/types/planning.js +0 -1
- package/dist/transactions/types/state-stores.js +0 -1
- package/dist/transactions/types/transact.js +0 -1
- package/dist/transactions/withdrawal-planner.js +0 -128
- package/dist/tsup.browser.config.js +0 -34
- package/dist/types.js +0 -1
- package/dist/utils/amounts.js +0 -89
- package/dist/utils/bigint.js +0 -29
- package/dist/utils/crypto.d.ts +0 -18
- package/dist/utils/crypto.d.ts.map +0 -1
- package/dist/utils/crypto.js +0 -45
- package/dist/utils/format.js +0 -33
- package/dist/utils/json-codec.js +0 -25
- package/dist/utils/notes.js +0 -14
- package/dist/utils/polling.js +0 -11
- package/dist/utils/random.js +0 -27
- package/dist/utils/secure-memory.d.ts.map +0 -1
- package/dist/utils/secure-memory.js +0 -28
- package/dist/utils/signature.js +0 -14
- package/dist/utils/validators.js +0 -96
- package/dist/vitest.config.js +0 -13
- /package/dist/{utils → crypto}/secure-memory.d.ts +0 -0
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { CoreError } from "../errors.js";
|
|
2
|
-
const ACTIVE_STATUSES = ["pending", "submitted", "broadcasting"];
|
|
3
|
-
export function createJobReconciler(deps) {
|
|
4
|
-
const { stateStore, depositClient, transactService } = deps;
|
|
5
|
-
async function markChecked(job, err) {
|
|
6
|
-
const latest = (await stateStore.getJob(job.relayId)) ?? job;
|
|
7
|
-
const message = err instanceof Error
|
|
8
|
-
? err.message
|
|
9
|
-
: err !== undefined
|
|
10
|
-
? String(err)
|
|
11
|
-
: null;
|
|
12
|
-
await stateStore.putJob({
|
|
13
|
-
...latest,
|
|
14
|
-
lastCheckedAt: Date.now(),
|
|
15
|
-
...(message ? { error: message } : {}),
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
async function markDead(job, reason) {
|
|
19
|
-
const latest = (await stateStore.getJob(job.relayId)) ?? job;
|
|
20
|
-
if (latest.status === "succeeded")
|
|
21
|
-
return;
|
|
22
|
-
await stateStore.putJob({
|
|
23
|
-
...latest,
|
|
24
|
-
status: "dead",
|
|
25
|
-
lastCheckedAt: Date.now(),
|
|
26
|
-
error: reason,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
function isTimedOut(job) {
|
|
30
|
-
return Date.now() - job.createdAt > job.timeoutMs;
|
|
31
|
-
}
|
|
32
|
-
async function reconcileJob(job) {
|
|
33
|
-
const latest = (await stateStore.getJob(job.relayId)) ?? job;
|
|
34
|
-
if (!ACTIVE_STATUSES.includes(latest.status)) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
if (latest.status !== "succeeded" && isTimedOut(latest)) {
|
|
38
|
-
await markDead(latest, "job timed out");
|
|
39
|
-
return new Error("job timed out");
|
|
40
|
-
}
|
|
41
|
-
if (latest.kind === "deposit") {
|
|
42
|
-
return depositClient.syncPendingDeposit(latest.relayId);
|
|
43
|
-
}
|
|
44
|
-
return transactService.syncPendingTransact(latest.relayId);
|
|
45
|
-
}
|
|
46
|
-
async function reconcileRelay(relayId) {
|
|
47
|
-
const job = await stateStore.getJob(relayId);
|
|
48
|
-
if (!job) {
|
|
49
|
-
throw new CoreError(`unknown relay ${relayId}`);
|
|
50
|
-
}
|
|
51
|
-
try {
|
|
52
|
-
const res = await reconcileJob(job);
|
|
53
|
-
await markChecked(job);
|
|
54
|
-
return res;
|
|
55
|
-
}
|
|
56
|
-
catch (err) {
|
|
57
|
-
await markChecked(job, err);
|
|
58
|
-
throw err;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
async function reconcileAll(filter = {}) {
|
|
62
|
-
const jobs = await stateStore.listJobs({
|
|
63
|
-
kind: filter.kind,
|
|
64
|
-
statuses: filter.statuses ?? ACTIVE_STATUSES,
|
|
65
|
-
});
|
|
66
|
-
const results = [];
|
|
67
|
-
for (const job of jobs) {
|
|
68
|
-
try {
|
|
69
|
-
const res = await reconcileJob(job);
|
|
70
|
-
await markChecked(job);
|
|
71
|
-
results.push(res);
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
await markChecked(job, err);
|
|
75
|
-
// Allow caller to decide how to handle errors; we keep going.
|
|
76
|
-
results.push(err);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return results;
|
|
80
|
-
}
|
|
81
|
-
return {
|
|
82
|
-
reconcileRelay,
|
|
83
|
-
reconcileAll,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
@@ -1,450 +0,0 @@
|
|
|
1
|
-
import { AbiCoder, Interface, keccak256 } from "ethers";
|
|
2
|
-
import { createBroadcasterClient } from "../clients/broadcaster.js";
|
|
3
|
-
import { resolveFetch } from "../clients/http.js";
|
|
4
|
-
import { createIndexerClient } from "../clients/indexer.js";
|
|
5
|
-
import { createServiceConfig } from "../config.js";
|
|
6
|
-
import { poseidon } from "../crypto-adapters/index.js";
|
|
7
|
-
import { CoreError, ProofError, ValidationError } from "../errors.js";
|
|
8
|
-
import { proveTransaction } from "../prover/index.js";
|
|
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";
|
|
12
|
-
import { deriveCommitment, encryptNote } from "../utils/crypto.js";
|
|
13
|
-
import { DEFAULT_POLL_INTERVAL_MS, DEFAULT_POLL_TIMEOUT_MS, MAX_POLL_INTERVAL_MS, } from "../utils/polling.js";
|
|
14
|
-
import { signTransactMessage } from "../utils/signature.js";
|
|
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;
|
|
19
|
-
const transactInterface = new Interface([TRANSACT_ABI]);
|
|
20
|
-
const DEFAULT_WITHDRAWAL = {
|
|
21
|
-
npk: 0n,
|
|
22
|
-
amount: 0n,
|
|
23
|
-
token: "0x0000000000000000000000000000000000000000",
|
|
24
|
-
};
|
|
25
|
-
export function computeBoundParamsHash(chainId, poolAddress) {
|
|
26
|
-
const coder = AbiCoder.defaultAbiCoder();
|
|
27
|
-
const encoded = coder.encode(["uint64", "uint160"], [BigInt(chainId), BigInt(poolAddress)]);
|
|
28
|
-
return parseHexToBigInt(keccak256(encoded)) % SNARK_SCALAR_FIELD;
|
|
29
|
-
}
|
|
30
|
-
export function serializeWitness(proof, index) {
|
|
31
|
-
return {
|
|
32
|
-
root: formatUint256(BigInt(proof.root)),
|
|
33
|
-
leaf: formatUint256(BigInt(proof.leaf)),
|
|
34
|
-
pathElements: proof.siblings.map((level) => level.map((node) => formatUint256(BigInt(node)))),
|
|
35
|
-
pathIndices: proof.pathIndices ?? [],
|
|
36
|
-
leafIndex: index,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
export function deserializeWitness(s) {
|
|
40
|
-
return {
|
|
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,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Build proof for a single transaction item.
|
|
50
|
-
* This is the core proof generation logic extracted for parallelization.
|
|
51
|
-
*/
|
|
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
|
-
});
|
|
79
|
-
}
|
|
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,
|
|
104
|
-
})
|
|
105
|
-
: null;
|
|
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
|
-
})
|
|
190
|
-
: null;
|
|
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;
|
|
204
|
-
}
|
|
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, // Skip root + boundParams + nullifiers
|
|
215
|
-
r.withdrawal.amount > 0n
|
|
216
|
-
? r.proof.pubSignals.length - 1 // Exclude withdrawal commitment (not inserted into tree)
|
|
217
|
-
: undefined),
|
|
218
|
-
context: { chainId: BigInt(req.chainId), poolAddress: req.poolAddress },
|
|
219
|
-
withdrawal: r.withdrawal,
|
|
220
|
-
ciphertexts: r.ciphertexts,
|
|
221
|
-
})),
|
|
222
|
-
]);
|
|
223
|
-
const relayId = globalThis.crypto?.randomUUID?.() ?? `tx-${Date.now().toString(16)}`;
|
|
224
|
-
// Determine if this is a withdrawal (any transaction has withdrawal)
|
|
225
|
-
const hasAnyWithdrawal = results.some((r) => r.withdrawal.amount > 0n);
|
|
226
|
-
const historyKind = hasAnyWithdrawal ? "Withdraw" : "Send";
|
|
227
|
-
// Compute net deltas per token for history preview
|
|
228
|
-
const deltasByToken = new Map();
|
|
229
|
-
for (let i = 0; i < req.transactions.length; i++) {
|
|
230
|
-
const tx = req.transactions[i];
|
|
231
|
-
const r = results[i];
|
|
232
|
-
const totalOutputsToRecipients = tx.outputs
|
|
233
|
-
.filter((o) => o.owner === "recipient")
|
|
234
|
-
.reduce((sum, o) => sum + o.amount, 0n);
|
|
235
|
-
// For withdrawals: user loses the withdrawal amount
|
|
236
|
-
// For sends: user loses what they send to recipients (change returns to them)
|
|
237
|
-
const netDelta = r.withdrawal.amount > 0n
|
|
238
|
-
? -r.withdrawal.amount
|
|
239
|
-
: -totalOutputsToRecipients;
|
|
240
|
-
const existing = deltasByToken.get(tx.token) ?? 0n;
|
|
241
|
-
deltasByToken.set(tx.token, existing + netDelta);
|
|
242
|
-
}
|
|
243
|
-
const jobBase = {
|
|
244
|
-
relayId,
|
|
245
|
-
chainId: req.chainId,
|
|
246
|
-
status: "pending",
|
|
247
|
-
txHash: null,
|
|
248
|
-
createdAt: Date.now(),
|
|
249
|
-
timeoutMs: DEFAULT_JOB_TIMEOUT_MS,
|
|
250
|
-
poolAddress: req.poolAddress,
|
|
251
|
-
calldata,
|
|
252
|
-
transactions: results.map((r, i) => {
|
|
253
|
-
const tx = req.transactions[i];
|
|
254
|
-
return {
|
|
255
|
-
token: tx.token,
|
|
256
|
-
nullifiers: r.nullifiers,
|
|
257
|
-
predictedCommitments: r.outputs.map((o) => ({ hex: o.hex })),
|
|
258
|
-
withdrawal: r.withdrawal.amount > 0n
|
|
259
|
-
? {
|
|
260
|
-
amount: r.withdrawal.amount.toString(),
|
|
261
|
-
recipient: r.withdrawal.npk.toString(),
|
|
262
|
-
}
|
|
263
|
-
: undefined,
|
|
264
|
-
};
|
|
265
|
-
}),
|
|
266
|
-
historyPreview: {
|
|
267
|
-
kind: historyKind,
|
|
268
|
-
amounts: [...deltasByToken.entries()].map(([token, delta]) => ({
|
|
269
|
-
token,
|
|
270
|
-
delta: delta.toString(),
|
|
271
|
-
})),
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
const job = hasAnyWithdrawal
|
|
275
|
-
? { ...jobBase, kind: "withdraw" }
|
|
276
|
-
: { ...jobBase, kind: "transfer" };
|
|
277
|
-
// Submit to broadcaster
|
|
278
|
-
let txHash = null;
|
|
279
|
-
if (broadcaster) {
|
|
280
|
-
const submission = await broadcaster.submitRelay({
|
|
281
|
-
clientTxId: relayId,
|
|
282
|
-
chainId: req.chainId,
|
|
283
|
-
payload: { kind: "call_data", to: req.poolAddress, data: calldata },
|
|
284
|
-
});
|
|
285
|
-
if (!submission.accepted) {
|
|
286
|
-
await store.putJob({
|
|
287
|
-
...job,
|
|
288
|
-
status: "failed",
|
|
289
|
-
lastCheckedAt: Date.now(),
|
|
290
|
-
error: submission.message ?? "broadcaster rejected",
|
|
291
|
-
});
|
|
292
|
-
throw new CoreError(submission.message ?? "broadcaster rejected");
|
|
293
|
-
}
|
|
294
|
-
const deadline = Date.now() + pollTimeout;
|
|
295
|
-
while (Date.now() <= deadline) {
|
|
296
|
-
const status = await broadcaster.getRelayStatus(relayId);
|
|
297
|
-
if (status.state === "succeeded") {
|
|
298
|
-
txHash = status.txHash ?? null;
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
if (status.state === "failed" || status.state === "dead") {
|
|
302
|
-
await store.putJob({
|
|
303
|
-
...job,
|
|
304
|
-
status: "failed",
|
|
305
|
-
lastCheckedAt: Date.now(),
|
|
306
|
-
error: status.error ?? "broadcaster relay failed",
|
|
307
|
-
});
|
|
308
|
-
throw new CoreError(status.error ?? "broadcaster relay failed");
|
|
309
|
-
}
|
|
310
|
-
await sleep(pollInterval);
|
|
311
|
-
}
|
|
312
|
-
if (!txHash && Date.now() > deadline) {
|
|
313
|
-
await store.putJob({
|
|
314
|
-
...job,
|
|
315
|
-
status: "failed",
|
|
316
|
-
lastCheckedAt: Date.now(),
|
|
317
|
-
error: "broadcaster relay timed out",
|
|
318
|
-
});
|
|
319
|
-
throw new CoreError("broadcaster relay timed out");
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
await store.putJob({
|
|
323
|
-
...job,
|
|
324
|
-
status: "broadcasting",
|
|
325
|
-
txHash,
|
|
326
|
-
});
|
|
327
|
-
// Build result with individual transaction results
|
|
328
|
-
const transactionResults = results.map((r) => ({
|
|
329
|
-
proof: r.proof,
|
|
330
|
-
witnesses: r.witnesses,
|
|
331
|
-
nullifiers: r.nullifiers,
|
|
332
|
-
predictedCommitments: r.predictedCommitments,
|
|
333
|
-
}));
|
|
334
|
-
return {
|
|
335
|
-
relayId,
|
|
336
|
-
calldata,
|
|
337
|
-
transactions: transactionResults,
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
/**
|
|
341
|
-
* Wait for a pending transaction to be indexed and update local state.
|
|
342
|
-
*/
|
|
343
|
-
export async function syncTransact(store, relayId, opts) {
|
|
344
|
-
const record = await store.getJob(relayId);
|
|
345
|
-
if (!record || (record.kind !== "transfer" && record.kind !== "withdraw"))
|
|
346
|
-
throw new CoreError(`unknown pool transaction relay ${relayId}`);
|
|
347
|
-
const job = record;
|
|
348
|
-
const serviceConfig = createServiceConfig(opts.rpcUrl);
|
|
349
|
-
const trees = resolveMerkleTrees(opts);
|
|
350
|
-
const fetchFn = resolveFetch(opts.fetch);
|
|
351
|
-
const indexer = fetchFn
|
|
352
|
-
? createIndexerClient(serviceConfig.indexerBaseUrl, { fetch: fetchFn })
|
|
353
|
-
: null;
|
|
354
|
-
const pollInterval = Math.min(opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
355
|
-
const pollTimeout = opts.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
356
|
-
await rebuildTreeFromStore({
|
|
357
|
-
chainId: job.chainId,
|
|
358
|
-
trees,
|
|
359
|
-
loadLeaf: store.getLeaf.bind(store),
|
|
360
|
-
});
|
|
361
|
-
// Collect all predicted commitments from all transactions
|
|
362
|
-
const allPredictedOutputs = job.transactions.flatMap((tx) => tx.predictedCommitments.map((c) => c.hex));
|
|
363
|
-
// Wait for indexed commitments
|
|
364
|
-
const indexed = [];
|
|
365
|
-
if (indexer && allPredictedOutputs.length > 0) {
|
|
366
|
-
const pending = new Set(allPredictedOutputs.map((h) => h.toLowerCase()));
|
|
367
|
-
const deadline = Date.now() + pollTimeout;
|
|
368
|
-
let delay = pollInterval;
|
|
369
|
-
while (pending.size > 0 && Date.now() <= deadline) {
|
|
370
|
-
for (const hex of [...pending]) {
|
|
371
|
-
const rec = await indexer
|
|
372
|
-
.getCommitment({ chainId: job.chainId, commitment: hex })
|
|
373
|
-
.catch((e) => {
|
|
374
|
-
if (!isNotFoundError(e))
|
|
375
|
-
throw e;
|
|
376
|
-
return null;
|
|
377
|
-
});
|
|
378
|
-
if (rec) {
|
|
379
|
-
indexed.push(rec);
|
|
380
|
-
pending.delete(hex);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
if (pending.size > 0) {
|
|
384
|
-
await sleep(delay);
|
|
385
|
-
delay = Math.min(delay * 2, MAX_POLL_INTERVAL_MS);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
if (pending.size > 0) {
|
|
389
|
-
await store.putJob({
|
|
390
|
-
...job,
|
|
391
|
-
status: "failed",
|
|
392
|
-
lastCheckedAt: Date.now(),
|
|
393
|
-
error: "commitments not indexed before timeout",
|
|
394
|
-
});
|
|
395
|
-
throw new CoreError("commitments not indexed before timeout");
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
// Get latest root from indexed records (syncChain handles actual storage)
|
|
399
|
-
const sortedIndexed = indexed.sort((a, b) => a.index - b.index);
|
|
400
|
-
const latestRoot = sortedIndexed.at(-1)?.root ?? trees.getRoot(job.chainId);
|
|
401
|
-
// Collect all nullifiers from all transactions and mark notes as spent
|
|
402
|
-
const allNullifiers = job.transactions.flatMap((tx) => tx.nullifiers);
|
|
403
|
-
const timestamp = Date.now();
|
|
404
|
-
// Look up note indices by matching nullifiers to notes in the store
|
|
405
|
-
const allNotes = await store.listNotes({
|
|
406
|
-
chainId: job.chainId,
|
|
407
|
-
includeSpent: true,
|
|
408
|
-
});
|
|
409
|
-
const nullifierToNoteIndex = new Map();
|
|
410
|
-
for (const note of allNotes) {
|
|
411
|
-
if (note.nullifier) {
|
|
412
|
-
nullifierToNoteIndex.set(note.nullifier, note.index);
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
// Resolve every nullifier to its note index, fail fast if any is unknown
|
|
416
|
-
const resolved = [];
|
|
417
|
-
for (const nullifier of allNullifiers) {
|
|
418
|
-
const noteIndex = nullifierToNoteIndex.get(nullifier);
|
|
419
|
-
if (noteIndex === undefined) {
|
|
420
|
-
const short = nullifier.slice(0, 10) + "…";
|
|
421
|
-
const error = `Unknown nullifier ${short} - note not found in local state. This may indicate a sync issue.`;
|
|
422
|
-
await store.putJob({
|
|
423
|
-
...job,
|
|
424
|
-
status: "failed",
|
|
425
|
-
lastCheckedAt: Date.now(),
|
|
426
|
-
error,
|
|
427
|
-
});
|
|
428
|
-
throw new CoreError(error);
|
|
429
|
-
}
|
|
430
|
-
resolved.push({ nullifier, noteIndex });
|
|
431
|
-
}
|
|
432
|
-
const txHash = job.txHash ?? undefined;
|
|
433
|
-
await Promise.all(resolved.flatMap(({ nullifier, noteIndex }) => [
|
|
434
|
-
store.putNullifier({ chainId: job.chainId, nullifier, noteIndex }),
|
|
435
|
-
store.markNoteSpent(job.chainId, noteIndex, timestamp, txHash),
|
|
436
|
-
]));
|
|
437
|
-
await store.putJob({
|
|
438
|
-
...job,
|
|
439
|
-
status: "succeeded",
|
|
440
|
-
lastCheckedAt: timestamp,
|
|
441
|
-
});
|
|
442
|
-
return {
|
|
443
|
-
chainId: job.chainId,
|
|
444
|
-
root: latestRoot,
|
|
445
|
-
nullifiers: allNullifiers,
|
|
446
|
-
newCommitments: indexed.map((r) => r.commitment),
|
|
447
|
-
txHash: job.txHash ?? undefined,
|
|
448
|
-
indexedCommitments: indexed,
|
|
449
|
-
};
|
|
450
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { poseidon } from "../crypto-adapters/index.js";
|
|
2
|
-
import { ProofError, ValidationError } from "../errors.js";
|
|
3
|
-
import { CIRCUIT_REGISTRY } from "../prover/registry.js";
|
|
4
|
-
import { randomBigint } from "../utils/random.js";
|
|
5
|
-
import { selectNotesForAmount } from "./note-selection.js";
|
|
6
|
-
/**
|
|
7
|
-
* Select the smallest circuit that fits the given input/output counts.
|
|
8
|
-
*/
|
|
9
|
-
function selectCircuitForTransaction(inputCount, outputCount) {
|
|
10
|
-
// Sort circuits by total size (inputs + outputs) ascending
|
|
11
|
-
const circuits = Object.values(CIRCUIT_REGISTRY).sort((a, b) => a.inputs + a.outputs - (b.inputs + b.outputs));
|
|
12
|
-
const circuit = circuits.find((c) => c.inputs >= inputCount && c.outputs >= outputCount);
|
|
13
|
-
if (!circuit) {
|
|
14
|
-
throw new ProofError(`No circuit supports ${inputCount} inputs and ${outputCount} outputs. ` +
|
|
15
|
-
`Available: ${circuits.map((c) => `${c.inputs}x${c.outputs}`).join(", ")}`);
|
|
16
|
-
}
|
|
17
|
-
return circuit;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Generate a planned output note with derived npk and commitment.
|
|
21
|
-
*/
|
|
22
|
-
function generateOutput(mpk, amount, token, owner) {
|
|
23
|
-
const random = randomBigint();
|
|
24
|
-
const npk = poseidon([mpk, random]);
|
|
25
|
-
const tokenScalar = BigInt(token);
|
|
26
|
-
const commitment = poseidon([npk, tokenScalar, amount]);
|
|
27
|
-
return {
|
|
28
|
-
mpk,
|
|
29
|
-
random,
|
|
30
|
-
token,
|
|
31
|
-
amount,
|
|
32
|
-
owner,
|
|
33
|
-
npk,
|
|
34
|
-
commitment,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Plan a transaction: select notes, choose circuit, generate outputs.
|
|
39
|
-
*
|
|
40
|
-
* This function handles:
|
|
41
|
-
* 1. Note selection using consolidation-first algorithm
|
|
42
|
-
* 2. Circuit selection (smallest that fits)
|
|
43
|
-
* 3. Output generation with cryptographic derivations
|
|
44
|
-
*
|
|
45
|
-
* @param notes - Available unspent notes (pre-filtered by token/chain by caller)
|
|
46
|
-
* @param recipients - List of recipients with their amounts
|
|
47
|
-
* @param senderMpk - Sender's master public key (for change output)
|
|
48
|
-
* @param token - Token address being transferred
|
|
49
|
-
* @returns Complete transaction plan ready for building TransactRequest
|
|
50
|
-
* @throws ValidationError if insufficient balance, no valid circuit, or other constraints fail
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* ```typescript
|
|
54
|
-
* const plan = planTransaction(
|
|
55
|
-
* spendableNotes,
|
|
56
|
-
* [{ mpk: recipientMpk, amount: 100n }],
|
|
57
|
-
* senderMpk,
|
|
58
|
-
* "0x...",
|
|
59
|
-
* );
|
|
60
|
-
* // plan.inputs - selected notes to spend
|
|
61
|
-
* // plan.outputs - generated outputs (recipient + change)
|
|
62
|
-
* // plan.circuit - circuit to use for proof
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
export function planTransaction(notes, recipients, senderMpk, token) {
|
|
66
|
-
// Validate inputs
|
|
67
|
-
if (!recipients.length) {
|
|
68
|
-
throw new ValidationError("At least one recipient is required");
|
|
69
|
-
}
|
|
70
|
-
// Validate each recipient has a positive amount
|
|
71
|
-
for (let i = 0; i < recipients.length; i++) {
|
|
72
|
-
const recipient = recipients[i];
|
|
73
|
-
if (recipient && recipient.amount <= 0n) {
|
|
74
|
-
throw new ValidationError(`Recipient at index ${i} has invalid amount: ${recipient.amount}. All recipients must have amount > 0`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
const totalSent = recipients.reduce((sum, r) => sum + r.amount, 0n);
|
|
78
|
-
// Select notes using consolidation-first algorithm
|
|
79
|
-
// Note: maxInputs/maxOutputs defaults are derived from CIRCUIT_REGISTRY in selectNotesForAmount
|
|
80
|
-
const selection = selectNotesForAmount(notes, totalSent, {
|
|
81
|
-
recipientCount: recipients.length,
|
|
82
|
-
});
|
|
83
|
-
const { selected: inputs, totalInput, change } = selection;
|
|
84
|
-
// Determine output count: 1 per recipient + 1 for change (if any)
|
|
85
|
-
const outputCount = recipients.length + (change > 0n ? 1 : 0);
|
|
86
|
-
// Select smallest fitting circuit
|
|
87
|
-
const circuit = selectCircuitForTransaction(inputs.length, outputCount);
|
|
88
|
-
// Generate outputs
|
|
89
|
-
const outputs = [];
|
|
90
|
-
// Recipient outputs
|
|
91
|
-
for (const recipient of recipients) {
|
|
92
|
-
outputs.push(generateOutput(recipient.mpk, recipient.amount, token, "recipient"));
|
|
93
|
-
}
|
|
94
|
-
// Change output (if any)
|
|
95
|
-
if (change > 0n) {
|
|
96
|
-
outputs.push(generateOutput(senderMpk, change, token, "self"));
|
|
97
|
-
}
|
|
98
|
-
// Create outputNotes for TransactParams (without derived fields, but with owner metadata)
|
|
99
|
-
const outputNotes = outputs.map((o) => ({
|
|
100
|
-
mpk: o.mpk,
|
|
101
|
-
random: o.random,
|
|
102
|
-
token: o.token,
|
|
103
|
-
amount: o.amount,
|
|
104
|
-
owner: o.owner,
|
|
105
|
-
}));
|
|
106
|
-
return {
|
|
107
|
-
inputs,
|
|
108
|
-
outputs,
|
|
109
|
-
outputNotes,
|
|
110
|
-
circuit,
|
|
111
|
-
totalInput,
|
|
112
|
-
totalSent,
|
|
113
|
-
change,
|
|
114
|
-
token,
|
|
115
|
-
};
|
|
116
|
-
}
|