@veilo/sdk-core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/cjs/notes/index.d.ts +1 -0
- package/dist/cjs/notes/index.js +1 -0
- package/dist/cjs/notes/scan.d.ts +74 -0
- package/dist/cjs/notes/scan.js +110 -0
- package/dist/cjs/notes/selection.d.ts +56 -0
- package/dist/cjs/notes/selection.js +67 -0
- package/dist/cjs/relayer/api.d.ts +11 -1
- package/dist/cjs/relayer/api.js +13 -0
- package/dist/cjs/relayer/client.d.ts +14 -1
- package/dist/cjs/relayer/client.js +22 -0
- package/dist/cjs/relayer/types.d.ts +31 -0
- package/dist/esm/notes/index.d.ts +1 -0
- package/dist/esm/notes/index.js +1 -0
- package/dist/esm/notes/scan.d.ts +74 -0
- package/dist/esm/notes/scan.js +105 -0
- package/dist/esm/notes/selection.d.ts +56 -0
- package/dist/esm/notes/selection.js +66 -0
- package/dist/esm/relayer/api.d.ts +11 -1
- package/dist/esm/relayer/api.js +11 -0
- package/dist/esm/relayer/client.d.ts +14 -1
- package/dist/esm/relayer/client.js +22 -0
- package/dist/esm/relayer/types.d.ts +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -283,6 +283,52 @@ configuration flag — see [Support](#support). Reaching past the package's
|
|
|
283
283
|
`exports` map to import internal modules is unsupported and not covered by
|
|
284
284
|
semver.
|
|
285
285
|
|
|
286
|
+
### Finding your notes on the public feed
|
|
287
|
+
|
|
288
|
+
Every row in the compact feed is ciphertext plus a one-byte view tag; nothing on
|
|
289
|
+
it says who a note belongs to. You find yours by trial decryption, and the view
|
|
290
|
+
tag makes that cheap — it rejects roughly 255 of every 256 foreign rows with a
|
|
291
|
+
single hash instead of a full decrypt.
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
import { scanCompactNotes, fetchNotesByCommitment } from "@veilo/sdk-core";
|
|
295
|
+
|
|
296
|
+
const { notes, scanned, nextCursor } = await scanCompactNotes(walletSecretKey, {
|
|
297
|
+
onPage: ({ matched }) => console.log(`${matched} found so far`),
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
It paginates to exhaustion by default and derives the X25519 key once for the
|
|
302
|
+
whole scan. Stop early with `maxPages` or by returning `false` from `onPage`;
|
|
303
|
+
`nextCursor` is then non-null and can be passed back later to resume. The feed
|
|
304
|
+
carries only unspent, unclaimed notes, and it needs no auth token — scanning
|
|
305
|
+
reveals nothing without your key.
|
|
306
|
+
|
|
307
|
+
`fetchNotesByCommitment(commitments)` looks up known commitments instead,
|
|
308
|
+
chunked at the server's 50-per-request cap and issued concurrently. Commitments
|
|
309
|
+
the relayer does not know are simply absent from the result.
|
|
310
|
+
|
|
311
|
+
### Consolidating notes before a large spend
|
|
312
|
+
|
|
313
|
+
When no one or two notes cover an amount, `selectNotesForAmount` reports
|
|
314
|
+
`requiresMerge`. `planNoteConsolidation` says what to do about it:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const plan = planNoteConsolidation(unspentNotes, amountRaw, { mint });
|
|
318
|
+
if (plan.ok) {
|
|
319
|
+
for (const { inputs, outputAmount } of plan.steps) {
|
|
320
|
+
// one 2-in-1-out privateTransfer to yourself per step
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Each step is a self-transfer combining two inputs into one, so k notes need k-2
|
|
326
|
+
steps before a final two-input spend. Steps chain — a later step can consume an
|
|
327
|
+
earlier step's output, and inputs are tagged `{ kind: "note" }` or
|
|
328
|
+
`{ kind: "step", step }` so you always know which. The two smallest are merged
|
|
329
|
+
each round, which retires dust first and leaves large notes intact. The plan is
|
|
330
|
+
pure: it computes, it does not execute.
|
|
331
|
+
|
|
286
332
|
### Spend status and private balance
|
|
287
333
|
|
|
288
334
|
Nothing in a note says whether it has been spent — a note is yours until its
|
package/dist/cjs/notes/index.js
CHANGED
|
@@ -21,4 +21,5 @@ __exportStar(require("./mailbox.js"), exports);
|
|
|
21
21
|
__exportStar(require("./recovery.js"), exports);
|
|
22
22
|
__exportStar(require("./spent.js"), exports);
|
|
23
23
|
__exportStar(require("./selection.js"), exports);
|
|
24
|
+
__exportStar(require("./scan.js"), exports);
|
|
24
25
|
__exportStar(require("./amount.js"), exports);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { CompactScanNote, RelayerEncryptedNote } from "../relayer/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Finding your own notes in the public feed.
|
|
4
|
+
*
|
|
5
|
+
* The compact feed is deliberately anonymous: every row is ciphertext plus a
|
|
6
|
+
* one-byte view tag, and nothing on it says who a note belongs to. You find
|
|
7
|
+
* yours by trial decryption. The view tag exists to make that cheap — it
|
|
8
|
+
* rejects ~255/256 of other people's rows with one hash instead of a full
|
|
9
|
+
* decrypt.
|
|
10
|
+
*/
|
|
11
|
+
/** Server-side page cap (notes.constants COMPACT_SCAN_MAX_LIMIT). */
|
|
12
|
+
export declare const COMPACT_SCAN_MAX_LIMIT = 1000;
|
|
13
|
+
/** Server-side cap per fetch-by-commitment request. */
|
|
14
|
+
export declare const MAX_COMMITMENTS_PER_FETCH = 50;
|
|
15
|
+
/** A feed row that decrypted under your key. */
|
|
16
|
+
export interface ScannedNote {
|
|
17
|
+
/** Value in base units. */
|
|
18
|
+
amount: bigint;
|
|
19
|
+
/** 32-byte blinding factor recovered from the cipher. */
|
|
20
|
+
blinding: Uint8Array;
|
|
21
|
+
/** Commitment hex, as stored. */
|
|
22
|
+
commitment: string;
|
|
23
|
+
leafIndex?: number | null;
|
|
24
|
+
treeId?: number | null;
|
|
25
|
+
mint?: string | null;
|
|
26
|
+
timestamp?: number;
|
|
27
|
+
txSignature?: string;
|
|
28
|
+
/** The raw feed row this came from. */
|
|
29
|
+
source: CompactScanNote;
|
|
30
|
+
}
|
|
31
|
+
export interface ScanCompactNotesOptions {
|
|
32
|
+
/** Rows per request. Capped at COMPACT_SCAN_MAX_LIMIT. */
|
|
33
|
+
pageSize?: number;
|
|
34
|
+
/** Resume from a previous run's `nextCursor`. */
|
|
35
|
+
cursor?: string;
|
|
36
|
+
/** Stop after this many pages. Unlimited by default. */
|
|
37
|
+
maxPages?: number;
|
|
38
|
+
/** Called after each page, for progress reporting. Return false to stop. */
|
|
39
|
+
onPage?: (progress: {
|
|
40
|
+
scanned: number;
|
|
41
|
+
matched: number;
|
|
42
|
+
cursor: string | null;
|
|
43
|
+
}) => boolean | void;
|
|
44
|
+
}
|
|
45
|
+
export interface ScanCompactNotesResult {
|
|
46
|
+
/** Rows that decrypted under your key. */
|
|
47
|
+
notes: ScannedNote[];
|
|
48
|
+
/** Feed rows examined. */
|
|
49
|
+
scanned: number;
|
|
50
|
+
/**
|
|
51
|
+
* Cursor to resume from. Null means the feed was exhausted; a string means
|
|
52
|
+
* scanning stopped early (maxPages, or onPage returning false).
|
|
53
|
+
*/
|
|
54
|
+
nextCursor: string | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Walk the compact feed and return the notes that belong to this key.
|
|
58
|
+
*
|
|
59
|
+
* Paginates to exhaustion by default. The X25519 private key is derived once
|
|
60
|
+
* and reused across every row rather than per row — that derivation dominates
|
|
61
|
+
* the cost otherwise.
|
|
62
|
+
*
|
|
63
|
+
* @param walletSecretKey full 64-byte keypair.secretKey
|
|
64
|
+
*/
|
|
65
|
+
export declare function scanCompactNotes(walletSecretKey: Uint8Array, options?: ScanCompactNotesOptions): Promise<ScanCompactNotesResult>;
|
|
66
|
+
/**
|
|
67
|
+
* Look up stored notes by commitment, chunked to the server's limit.
|
|
68
|
+
*
|
|
69
|
+
* Requests are issued concurrently. Commitments the relayer does not know are
|
|
70
|
+
* simply absent from the result — the array is not positional.
|
|
71
|
+
*/
|
|
72
|
+
export declare function fetchNotesByCommitment(commitments: string[], options?: {
|
|
73
|
+
walletPublicKey?: string;
|
|
74
|
+
}): Promise<RelayerEncryptedNote[]>;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_COMMITMENTS_PER_FETCH = exports.COMPACT_SCAN_MAX_LIMIT = void 0;
|
|
4
|
+
exports.scanCompactNotes = scanCompactNotes;
|
|
5
|
+
exports.fetchNotesByCommitment = fetchNotesByCommitment;
|
|
6
|
+
const api_js_1 = require("../relayer/api.js");
|
|
7
|
+
const compactNote_js_1 = require("../compactNote.js");
|
|
8
|
+
/**
|
|
9
|
+
* Finding your own notes in the public feed.
|
|
10
|
+
*
|
|
11
|
+
* The compact feed is deliberately anonymous: every row is ciphertext plus a
|
|
12
|
+
* one-byte view tag, and nothing on it says who a note belongs to. You find
|
|
13
|
+
* yours by trial decryption. The view tag exists to make that cheap — it
|
|
14
|
+
* rejects ~255/256 of other people's rows with one hash instead of a full
|
|
15
|
+
* decrypt.
|
|
16
|
+
*/
|
|
17
|
+
/** Server-side page cap (notes.constants COMPACT_SCAN_MAX_LIMIT). */
|
|
18
|
+
exports.COMPACT_SCAN_MAX_LIMIT = 1000;
|
|
19
|
+
/** Server-side cap per fetch-by-commitment request. */
|
|
20
|
+
exports.MAX_COMMITMENTS_PER_FETCH = 50;
|
|
21
|
+
function decodeBase64(value) {
|
|
22
|
+
if (typeof atob === "function") {
|
|
23
|
+
const binary = atob(value);
|
|
24
|
+
const out = new Uint8Array(binary.length);
|
|
25
|
+
for (let i = 0; i < binary.length; i++)
|
|
26
|
+
out[i] = binary.charCodeAt(i);
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Walk the compact feed and return the notes that belong to this key.
|
|
33
|
+
*
|
|
34
|
+
* Paginates to exhaustion by default. The X25519 private key is derived once
|
|
35
|
+
* and reused across every row rather than per row — that derivation dominates
|
|
36
|
+
* the cost otherwise.
|
|
37
|
+
*
|
|
38
|
+
* @param walletSecretKey full 64-byte keypair.secretKey
|
|
39
|
+
*/
|
|
40
|
+
async function scanCompactNotes(walletSecretKey, options = {}) {
|
|
41
|
+
const pageSize = Math.min(options.pageSize ?? 500, exports.COMPACT_SCAN_MAX_LIMIT);
|
|
42
|
+
const x25519Private = (0, compactNote_js_1.toX25519Private)(walletSecretKey);
|
|
43
|
+
const notes = [];
|
|
44
|
+
let cursor = options.cursor;
|
|
45
|
+
let scanned = 0;
|
|
46
|
+
let pages = 0;
|
|
47
|
+
for (;;) {
|
|
48
|
+
const page = await (0, api_js_1.compactScan)({ limit: pageSize, cursor });
|
|
49
|
+
pages++;
|
|
50
|
+
for (const row of page.notes ?? []) {
|
|
51
|
+
if (!row.compactEphemeralKey || !row.compactBlob)
|
|
52
|
+
continue;
|
|
53
|
+
scanned++;
|
|
54
|
+
const ephemeralPublicKey = decodeBase64(row.compactEphemeralKey);
|
|
55
|
+
// View tag first: one hash rejects almost every row that is not ours.
|
|
56
|
+
const { matches, sharedSecret } = (0, compactNote_js_1.matchesViewTag)(walletSecretKey, ephemeralPublicKey, row.viewTag ?? -1, { x25519Private });
|
|
57
|
+
if (!matches)
|
|
58
|
+
continue;
|
|
59
|
+
const opened = (0, compactNote_js_1.decryptCompactNoteCipher)(walletSecretKey, ephemeralPublicKey, decodeBase64(row.compactBlob), { sharedSecret, x25519Private });
|
|
60
|
+
if (!opened)
|
|
61
|
+
continue; // view-tag collision, ~1 in 256
|
|
62
|
+
notes.push({
|
|
63
|
+
amount: opened.amount,
|
|
64
|
+
blinding: opened.blinding,
|
|
65
|
+
commitment: row.commitment,
|
|
66
|
+
leafIndex: row.leafIndex,
|
|
67
|
+
treeId: row.treeId,
|
|
68
|
+
mint: row.mintAddress,
|
|
69
|
+
timestamp: row.timestamp,
|
|
70
|
+
txSignature: row.txSignature,
|
|
71
|
+
source: row,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
cursor = page.nextCursor ?? undefined;
|
|
75
|
+
const keepGoing = options.onPage?.({
|
|
76
|
+
scanned,
|
|
77
|
+
matched: notes.length,
|
|
78
|
+
cursor: page.nextCursor,
|
|
79
|
+
});
|
|
80
|
+
if (keepGoing === false)
|
|
81
|
+
break;
|
|
82
|
+
if (!page.hasMore || !cursor)
|
|
83
|
+
return { notes, scanned, nextCursor: null };
|
|
84
|
+
if (options.maxPages && pages >= options.maxPages)
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
return { notes, scanned, nextCursor: cursor ?? null };
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Look up stored notes by commitment, chunked to the server's limit.
|
|
91
|
+
*
|
|
92
|
+
* Requests are issued concurrently. Commitments the relayer does not know are
|
|
93
|
+
* simply absent from the result — the array is not positional.
|
|
94
|
+
*/
|
|
95
|
+
async function fetchNotesByCommitment(commitments, options = {}) {
|
|
96
|
+
const unique = [...new Set(commitments.map((c) => c.trim().toLowerCase()))];
|
|
97
|
+
if (unique.length === 0)
|
|
98
|
+
return [];
|
|
99
|
+
const chunks = [];
|
|
100
|
+
for (let i = 0; i < unique.length; i += exports.MAX_COMMITMENTS_PER_FETCH) {
|
|
101
|
+
chunks.push(unique.slice(i, i + exports.MAX_COMMITMENTS_PER_FETCH));
|
|
102
|
+
}
|
|
103
|
+
const pages = await Promise.all(chunks.map((chunk) => (0, api_js_1.fetchNotesByCommitmentPage)({
|
|
104
|
+
commitments: chunk,
|
|
105
|
+
...(options.walletPublicKey
|
|
106
|
+
? { walletPublicKey: options.walletPublicKey }
|
|
107
|
+
: {}),
|
|
108
|
+
})));
|
|
109
|
+
return pages.flatMap((page) => page.notes ?? []);
|
|
110
|
+
}
|
|
@@ -75,3 +75,59 @@ export type NoteSelectionResult<T> = NoteSelection<T> | NoteSelectionFailure;
|
|
|
75
75
|
export declare function selectNotesForAmount<T extends SelectableNote>(notes: T[], amount: bigint | string | number, options?: {
|
|
76
76
|
mint?: string | null;
|
|
77
77
|
}): NoteSelectionResult<T>;
|
|
78
|
+
/**
|
|
79
|
+
* One input to a merge step: an existing note, or the output of an earlier step.
|
|
80
|
+
*
|
|
81
|
+
* Steps chain, so a plan for five notes refers to notes that do not exist yet.
|
|
82
|
+
* Modelling that explicitly beats returning amounts and leaving the caller to
|
|
83
|
+
* work out which is which.
|
|
84
|
+
*/
|
|
85
|
+
export type MergeInput<T> = {
|
|
86
|
+
kind: "note";
|
|
87
|
+
note: T;
|
|
88
|
+
amount: bigint;
|
|
89
|
+
} | {
|
|
90
|
+
kind: "step";
|
|
91
|
+
step: number;
|
|
92
|
+
amount: bigint;
|
|
93
|
+
};
|
|
94
|
+
export interface MergeStep<T> {
|
|
95
|
+
/** 1-based, and the number `{ kind: "step" }` inputs refer to. */
|
|
96
|
+
step: number;
|
|
97
|
+
inputs: [MergeInput<T>, MergeInput<T>];
|
|
98
|
+
/** Value of the single note this step produces. */
|
|
99
|
+
outputAmount: bigint;
|
|
100
|
+
}
|
|
101
|
+
export interface NoteConsolidationPlan<T> {
|
|
102
|
+
ok: true;
|
|
103
|
+
treeId: number;
|
|
104
|
+
/**
|
|
105
|
+
* Merges to run in order, each a 2-in-1-out self-transfer. Empty when the
|
|
106
|
+
* amount is already spendable within MAX_INPUT_NOTES.
|
|
107
|
+
*/
|
|
108
|
+
steps: MergeStep<T>[];
|
|
109
|
+
/** Notes the plan consumes, in the order it consumes them. */
|
|
110
|
+
notes: T[];
|
|
111
|
+
/** Sum of those notes. */
|
|
112
|
+
total: bigint;
|
|
113
|
+
/** total - amount, the change left after the final spend. */
|
|
114
|
+
change: bigint;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Plan how to make `amount` spendable when no one or two notes cover it.
|
|
118
|
+
*
|
|
119
|
+
* `selectNotesForAmount` reports `requiresMerge` but cannot act on it: the
|
|
120
|
+
* circuit takes two inputs, so spending five notes means merging them down
|
|
121
|
+
* first. Each step here is one 2-in-1-out transfer to yourself, and k notes
|
|
122
|
+
* need k-2 of them before a final two-input spend.
|
|
123
|
+
*
|
|
124
|
+
* Merges the two SMALLEST notes each round. That retires dust first and leaves
|
|
125
|
+
* the large notes untouched, so the plan is short and the final spend has the
|
|
126
|
+
* least change.
|
|
127
|
+
*
|
|
128
|
+
* The plan is pure — it performs nothing. Execute the steps with
|
|
129
|
+
* `privateTransfer`, feeding each step's output note into the next.
|
|
130
|
+
*/
|
|
131
|
+
export declare function planNoteConsolidation<T extends SelectableNote>(notes: T[], amount: bigint | string | number, options?: {
|
|
132
|
+
mint?: string | null;
|
|
133
|
+
}): NoteConsolidationPlan<T> | NoteSelectionFailure;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.MAX_INPUT_NOTES = void 0;
|
|
4
4
|
exports.canonicalTreeId = canonicalTreeId;
|
|
5
5
|
exports.selectNotesForAmount = selectNotesForAmount;
|
|
6
|
+
exports.planNoteConsolidation = planNoteConsolidation;
|
|
6
7
|
const web3_js_1 = require("@solana/web3.js");
|
|
7
8
|
const amount_js_1 = require("./amount.js");
|
|
8
9
|
/**
|
|
@@ -191,3 +192,69 @@ function selectWithinTree(bucket, target, treeId) {
|
|
|
191
192
|
requiresMerge: chosen.length > exports.MAX_INPUT_NOTES,
|
|
192
193
|
};
|
|
193
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Plan how to make `amount` spendable when no one or two notes cover it.
|
|
197
|
+
*
|
|
198
|
+
* `selectNotesForAmount` reports `requiresMerge` but cannot act on it: the
|
|
199
|
+
* circuit takes two inputs, so spending five notes means merging them down
|
|
200
|
+
* first. Each step here is one 2-in-1-out transfer to yourself, and k notes
|
|
201
|
+
* need k-2 of them before a final two-input spend.
|
|
202
|
+
*
|
|
203
|
+
* Merges the two SMALLEST notes each round. That retires dust first and leaves
|
|
204
|
+
* the large notes untouched, so the plan is short and the final spend has the
|
|
205
|
+
* least change.
|
|
206
|
+
*
|
|
207
|
+
* The plan is pure — it performs nothing. Execute the steps with
|
|
208
|
+
* `privateTransfer`, feeding each step's output note into the next.
|
|
209
|
+
*/
|
|
210
|
+
function planNoteConsolidation(notes, amount, options = {}) {
|
|
211
|
+
const target = (0, amount_js_1.toBaseUnits)(amount, "amount");
|
|
212
|
+
if (target <= 0n) {
|
|
213
|
+
throw new Error(`amount must be positive, got ${target}`);
|
|
214
|
+
}
|
|
215
|
+
const selection = selectNotesForAmount(notes, target, options);
|
|
216
|
+
if (!selection.ok)
|
|
217
|
+
return selection;
|
|
218
|
+
// Already spendable in one transaction.
|
|
219
|
+
if (!selection.requiresMerge) {
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
treeId: selection.treeId,
|
|
223
|
+
steps: [],
|
|
224
|
+
notes: selection.notes,
|
|
225
|
+
total: selection.total,
|
|
226
|
+
change: selection.change,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Ascending, so the two cheapest merge first.
|
|
230
|
+
const pool = selection.notes
|
|
231
|
+
.map((note, index) => ({
|
|
232
|
+
kind: "note",
|
|
233
|
+
note,
|
|
234
|
+
amount: (0, amount_js_1.toBaseUnits)(note.amount, `notes[${index}].amount`),
|
|
235
|
+
}))
|
|
236
|
+
.sort((a, b) => (a.amount > b.amount ? 1 : a.amount < b.amount ? -1 : 0));
|
|
237
|
+
const steps = [];
|
|
238
|
+
while (pool.length > exports.MAX_INPUT_NOTES) {
|
|
239
|
+
const first = pool.shift();
|
|
240
|
+
const second = pool.shift();
|
|
241
|
+
const outputAmount = first.amount + second.amount;
|
|
242
|
+
const step = steps.length + 1;
|
|
243
|
+
steps.push({ step, inputs: [first, second], outputAmount });
|
|
244
|
+
const produced = { kind: "step", step, amount: outputAmount };
|
|
245
|
+
// Keep the pool ordered so the next round still merges the two smallest.
|
|
246
|
+
const at = pool.findIndex((entry) => entry.amount > outputAmount);
|
|
247
|
+
if (at === -1)
|
|
248
|
+
pool.push(produced);
|
|
249
|
+
else
|
|
250
|
+
pool.splice(at, 0, produced);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
treeId: selection.treeId,
|
|
255
|
+
steps,
|
|
256
|
+
notes: selection.notes,
|
|
257
|
+
total: selection.total,
|
|
258
|
+
change: selection.change,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckNullifiersResponse, ChallengeResponse, CheckUsernameResponse, DeleteUserDataResponse, MerkleRootResponse, MerkleTreeResponse, PreRegisterKeyResponse, PrivateSwapRequest, PrivateSwapResponse, PrivateTransferRequest, PrivateTransferResponse, QueryNotesRequest, QueryNotesResponse, RegisterRequest, RegisterResponse, RestoreRequest, RestoreResponse, SaveEncryptedNoteRequest, SaveEncryptedNoteResponse, VeiloPublicKeyResponse, WithdrawRequest, WithdrawResponse } from "./types.js";
|
|
1
|
+
import type { CheckNullifiersResponse, CompactScanResponse, FetchByCommitmentRequest, FetchByCommitmentResponse, ChallengeResponse, CheckUsernameResponse, DeleteUserDataResponse, MerkleRootResponse, MerkleTreeResponse, PreRegisterKeyResponse, PrivateSwapRequest, PrivateSwapResponse, PrivateTransferRequest, PrivateTransferResponse, QueryNotesRequest, QueryNotesResponse, RegisterRequest, RegisterResponse, RestoreRequest, RestoreResponse, SaveEncryptedNoteRequest, SaveEncryptedNoteResponse, VeiloPublicKeyResponse, WithdrawRequest, WithdrawResponse } from "./types.js";
|
|
2
2
|
/** Request a wallet-authentication challenge from Veilo. */
|
|
3
3
|
export declare function getChallenge(walletPublicKey: string): Promise<ChallengeResponse>;
|
|
4
4
|
/** Register a Veilo account and receive its auth token. */
|
|
@@ -19,6 +19,16 @@ export declare function saveEncryptedNote(data: SaveEncryptedNoteRequest): Promi
|
|
|
19
19
|
export declare function queryEncryptedNotes(authToken: string, params?: QueryNotesRequest): Promise<QueryNotesResponse>;
|
|
20
20
|
/** Delete the authenticated user's stored data without retaining their token. */
|
|
21
21
|
export declare function deleteUserData(authToken: string): Promise<DeleteUserDataResponse>;
|
|
22
|
+
/** One page of the public compact-note feed. Prefer `scanCompactNotes`. */
|
|
23
|
+
export declare function compactScan(params?: {
|
|
24
|
+
limit?: number;
|
|
25
|
+
cursor?: string;
|
|
26
|
+
}): Promise<CompactScanResponse>;
|
|
27
|
+
/**
|
|
28
|
+
* One fetch-by-commitment request, capped at 50 commitments by the server.
|
|
29
|
+
* Prefer `fetchNotesByCommitment` from the notes module, which chunks for you.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fetchNotesByCommitmentPage(data: FetchByCommitmentRequest): Promise<FetchByCommitmentResponse>;
|
|
22
32
|
/** Raw check-batch call. Prefer `checkNullifiersSpent`, which chunks for you. */
|
|
23
33
|
export declare function checkNullifiers(nullifiers: string[]): Promise<CheckNullifiersResponse>;
|
|
24
34
|
export declare function getMerkleRoot(mintAddress?: string, treeId?: number): Promise<MerkleRootResponse>;
|
package/dist/cjs/relayer/api.js
CHANGED
|
@@ -10,6 +10,8 @@ exports.preRegisterKey = preRegisterKey;
|
|
|
10
10
|
exports.saveEncryptedNote = saveEncryptedNote;
|
|
11
11
|
exports.queryEncryptedNotes = queryEncryptedNotes;
|
|
12
12
|
exports.deleteUserData = deleteUserData;
|
|
13
|
+
exports.compactScan = compactScan;
|
|
14
|
+
exports.fetchNotesByCommitmentPage = fetchNotesByCommitmentPage;
|
|
13
15
|
exports.checkNullifiers = checkNullifiers;
|
|
14
16
|
exports.getMerkleRoot = getMerkleRoot;
|
|
15
17
|
exports.getMerkleTree = getMerkleTree;
|
|
@@ -49,6 +51,17 @@ function queryEncryptedNotes(authToken, params = {}) {
|
|
|
49
51
|
function deleteUserData(authToken) {
|
|
50
52
|
return (0, internal_js_1.createInternalRelayerClient)({ authToken }).deleteUserData();
|
|
51
53
|
}
|
|
54
|
+
/** One page of the public compact-note feed. Prefer `scanCompactNotes`. */
|
|
55
|
+
function compactScan(params = {}) {
|
|
56
|
+
return (0, internal_js_1.getInternalRelayerClient)().compactScan(params);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One fetch-by-commitment request, capped at 50 commitments by the server.
|
|
60
|
+
* Prefer `fetchNotesByCommitment` from the notes module, which chunks for you.
|
|
61
|
+
*/
|
|
62
|
+
function fetchNotesByCommitmentPage(data) {
|
|
63
|
+
return (0, internal_js_1.getInternalRelayerClient)().fetchNotesByCommitment(data);
|
|
64
|
+
}
|
|
52
65
|
/** Raw check-batch call. Prefer `checkNullifiersSpent`, which chunks for you. */
|
|
53
66
|
function checkNullifiers(nullifiers) {
|
|
54
67
|
return (0, internal_js_1.getInternalRelayerClient)().checkNullifiers(nullifiers);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckNullifiersResponse, ChallengeResponse, CheckUsernameResponse, DeleteUserDataResponse, MerkleRootResponse, MerkleTreeResponse, PreRegisterKeyResponse, PrivateSwapRequest, PrivateSwapResponse, PrivateTransferRequest, PrivateTransferResponse, QueryNotesRequest, QueryNotesResponse, RegisterRequest, RegisterResponse, RestoreRequest, RestoreResponse, RetryPolicy, SaveEncryptedNoteRequest, SaveEncryptedNoteResponse, VeiloPublicKeyResponse, WithdrawRequest, WithdrawResponse } from "./types.js";
|
|
1
|
+
import type { CheckNullifiersResponse, CompactScanResponse, FetchByCommitmentRequest, FetchByCommitmentResponse, ChallengeResponse, CheckUsernameResponse, DeleteUserDataResponse, MerkleRootResponse, MerkleTreeResponse, PreRegisterKeyResponse, PrivateSwapRequest, PrivateSwapResponse, PrivateTransferRequest, PrivateTransferResponse, QueryNotesRequest, QueryNotesResponse, RegisterRequest, RegisterResponse, RestoreRequest, RestoreResponse, RetryPolicy, SaveEncryptedNoteRequest, SaveEncryptedNoteResponse, VeiloPublicKeyResponse, WithdrawRequest, WithdrawResponse } from "./types.js";
|
|
2
2
|
import { type InternalRelayerClientOptions } from "./internal-config.js";
|
|
3
3
|
import type { ClosePositionRequest, ClosePositionResponse, MergePositionsRequest, MergePositionsResponse, OpenPositionRequest, OpenPositionResponse } from "../positions/types.js";
|
|
4
4
|
import type { JperpCancelTriggerRequest, JperpCancelTriggerResponse, JperpCloseRequest, JperpDecreaseResponse, JperpOpenRequest, JperpOpenResponse, JperpRecoverNativeRequest, JperpRecoverNativeResponse, JperpRecoverSeedRequest, JperpRecoverSeedResponse, JperpReissueRequest, JperpReissueResponse, JperpSetTpslRequest, JperpUpdateTpslRequest } from "../perps/types.js";
|
|
@@ -37,6 +37,19 @@ export declare class VeiloRelayerClient {
|
|
|
37
37
|
/** Query encrypted notes for the authenticated user. Requires auth token. */
|
|
38
38
|
queryEncryptedNotes(params?: QueryNotesRequest): Promise<QueryNotesResponse>;
|
|
39
39
|
/** Delete all stored data for the authenticated user. Requires auth token. */
|
|
40
|
+
/**
|
|
41
|
+
* One page of the public compact-note feed.
|
|
42
|
+
*
|
|
43
|
+
* Unauthenticated by design: rows carry only ciphertext and a one-byte view
|
|
44
|
+
* tag, so scanning reveals nothing without the recipient's key. The feed
|
|
45
|
+
* excludes spent notes and any note already claimed by a wallet.
|
|
46
|
+
*/
|
|
47
|
+
compactScan(params?: {
|
|
48
|
+
limit?: number;
|
|
49
|
+
cursor?: string;
|
|
50
|
+
}): Promise<CompactScanResponse>;
|
|
51
|
+
/** Look up stored notes by commitment. Max 50 per request. */
|
|
52
|
+
fetchNotesByCommitment(data: FetchByCommitmentRequest): Promise<FetchByCommitmentResponse>;
|
|
40
53
|
/**
|
|
41
54
|
* Ask which of these nullifiers the relayer has seen spent.
|
|
42
55
|
*
|
|
@@ -81,6 +81,28 @@ class VeiloRelayerClient {
|
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
/** Delete all stored data for the authenticated user. Requires auth token. */
|
|
84
|
+
/**
|
|
85
|
+
* One page of the public compact-note feed.
|
|
86
|
+
*
|
|
87
|
+
* Unauthenticated by design: rows carry only ciphertext and a one-byte view
|
|
88
|
+
* tag, so scanning reveals nothing without the recipient's key. The feed
|
|
89
|
+
* excludes spent notes and any note already claimed by a wallet.
|
|
90
|
+
*/
|
|
91
|
+
async compactScan(params = {}) {
|
|
92
|
+
const query = new URLSearchParams();
|
|
93
|
+
if (params.limit !== undefined)
|
|
94
|
+
query.set("limit", String(params.limit));
|
|
95
|
+
if (params.cursor)
|
|
96
|
+
query.set("cursor", params.cursor);
|
|
97
|
+
const qs = query.toString();
|
|
98
|
+
return this.transport.request("GET", `/notes/compact-scan${qs ? `?${qs}` : ""}`);
|
|
99
|
+
}
|
|
100
|
+
/** Look up stored notes by commitment. Max 50 per request. */
|
|
101
|
+
async fetchNotesByCommitment(data) {
|
|
102
|
+
return this.transport.request("POST", "/notes/fetch-by-commitment", {
|
|
103
|
+
body: data,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
84
106
|
/**
|
|
85
107
|
* Ask which of these nullifiers the relayer has seen spent.
|
|
86
108
|
*
|
|
@@ -39,6 +39,37 @@ export interface QueryNotesResponse {
|
|
|
39
39
|
limit: number;
|
|
40
40
|
offset: number;
|
|
41
41
|
}
|
|
42
|
+
/** One row from the public compact-note scan feed. */
|
|
43
|
+
export interface CompactScanNote {
|
|
44
|
+
_id: string;
|
|
45
|
+
commitment: string;
|
|
46
|
+
compactEphemeralKey: string | null;
|
|
47
|
+
compactBlob: string | null;
|
|
48
|
+
viewTag: number | null;
|
|
49
|
+
leafIndex?: number | null;
|
|
50
|
+
treeId?: number | null;
|
|
51
|
+
mintAddress?: string | null;
|
|
52
|
+
timestamp?: number;
|
|
53
|
+
txSignature?: string;
|
|
54
|
+
spent?: boolean;
|
|
55
|
+
spentTx?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface CompactScanResponse {
|
|
58
|
+
success: boolean;
|
|
59
|
+
notes: CompactScanNote[];
|
|
60
|
+
/** Opaque cursor for the next page; null when the feed is exhausted. */
|
|
61
|
+
nextCursor: string | null;
|
|
62
|
+
hasMore: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Commitments to look up. Max 50 per request, 64-char hex. */
|
|
65
|
+
export interface FetchByCommitmentRequest {
|
|
66
|
+
commitments: string[];
|
|
67
|
+
walletPublicKey?: string;
|
|
68
|
+
}
|
|
69
|
+
export interface FetchByCommitmentResponse {
|
|
70
|
+
success: boolean;
|
|
71
|
+
notes: RelayerEncryptedNote[];
|
|
72
|
+
}
|
|
42
73
|
/** Nullifiers to test for spend status. Max 200 per request, 64-char hex. */
|
|
43
74
|
export interface CheckNullifiersRequest {
|
|
44
75
|
nullifiers: string[];
|
package/dist/esm/notes/index.js
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { CompactScanNote, RelayerEncryptedNote } from "../relayer/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Finding your own notes in the public feed.
|
|
4
|
+
*
|
|
5
|
+
* The compact feed is deliberately anonymous: every row is ciphertext plus a
|
|
6
|
+
* one-byte view tag, and nothing on it says who a note belongs to. You find
|
|
7
|
+
* yours by trial decryption. The view tag exists to make that cheap — it
|
|
8
|
+
* rejects ~255/256 of other people's rows with one hash instead of a full
|
|
9
|
+
* decrypt.
|
|
10
|
+
*/
|
|
11
|
+
/** Server-side page cap (notes.constants COMPACT_SCAN_MAX_LIMIT). */
|
|
12
|
+
export declare const COMPACT_SCAN_MAX_LIMIT = 1000;
|
|
13
|
+
/** Server-side cap per fetch-by-commitment request. */
|
|
14
|
+
export declare const MAX_COMMITMENTS_PER_FETCH = 50;
|
|
15
|
+
/** A feed row that decrypted under your key. */
|
|
16
|
+
export interface ScannedNote {
|
|
17
|
+
/** Value in base units. */
|
|
18
|
+
amount: bigint;
|
|
19
|
+
/** 32-byte blinding factor recovered from the cipher. */
|
|
20
|
+
blinding: Uint8Array;
|
|
21
|
+
/** Commitment hex, as stored. */
|
|
22
|
+
commitment: string;
|
|
23
|
+
leafIndex?: number | null;
|
|
24
|
+
treeId?: number | null;
|
|
25
|
+
mint?: string | null;
|
|
26
|
+
timestamp?: number;
|
|
27
|
+
txSignature?: string;
|
|
28
|
+
/** The raw feed row this came from. */
|
|
29
|
+
source: CompactScanNote;
|
|
30
|
+
}
|
|
31
|
+
export interface ScanCompactNotesOptions {
|
|
32
|
+
/** Rows per request. Capped at COMPACT_SCAN_MAX_LIMIT. */
|
|
33
|
+
pageSize?: number;
|
|
34
|
+
/** Resume from a previous run's `nextCursor`. */
|
|
35
|
+
cursor?: string;
|
|
36
|
+
/** Stop after this many pages. Unlimited by default. */
|
|
37
|
+
maxPages?: number;
|
|
38
|
+
/** Called after each page, for progress reporting. Return false to stop. */
|
|
39
|
+
onPage?: (progress: {
|
|
40
|
+
scanned: number;
|
|
41
|
+
matched: number;
|
|
42
|
+
cursor: string | null;
|
|
43
|
+
}) => boolean | void;
|
|
44
|
+
}
|
|
45
|
+
export interface ScanCompactNotesResult {
|
|
46
|
+
/** Rows that decrypted under your key. */
|
|
47
|
+
notes: ScannedNote[];
|
|
48
|
+
/** Feed rows examined. */
|
|
49
|
+
scanned: number;
|
|
50
|
+
/**
|
|
51
|
+
* Cursor to resume from. Null means the feed was exhausted; a string means
|
|
52
|
+
* scanning stopped early (maxPages, or onPage returning false).
|
|
53
|
+
*/
|
|
54
|
+
nextCursor: string | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Walk the compact feed and return the notes that belong to this key.
|
|
58
|
+
*
|
|
59
|
+
* Paginates to exhaustion by default. The X25519 private key is derived once
|
|
60
|
+
* and reused across every row rather than per row — that derivation dominates
|
|
61
|
+
* the cost otherwise.
|
|
62
|
+
*
|
|
63
|
+
* @param walletSecretKey full 64-byte keypair.secretKey
|
|
64
|
+
*/
|
|
65
|
+
export declare function scanCompactNotes(walletSecretKey: Uint8Array, options?: ScanCompactNotesOptions): Promise<ScanCompactNotesResult>;
|
|
66
|
+
/**
|
|
67
|
+
* Look up stored notes by commitment, chunked to the server's limit.
|
|
68
|
+
*
|
|
69
|
+
* Requests are issued concurrently. Commitments the relayer does not know are
|
|
70
|
+
* simply absent from the result — the array is not positional.
|
|
71
|
+
*/
|
|
72
|
+
export declare function fetchNotesByCommitment(commitments: string[], options?: {
|
|
73
|
+
walletPublicKey?: string;
|
|
74
|
+
}): Promise<RelayerEncryptedNote[]>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { compactScan, fetchNotesByCommitmentPage, } from "../relayer/api.js";
|
|
2
|
+
import { decryptCompactNoteCipher, matchesViewTag, toX25519Private, } from "../compactNote.js";
|
|
3
|
+
/**
|
|
4
|
+
* Finding your own notes in the public feed.
|
|
5
|
+
*
|
|
6
|
+
* The compact feed is deliberately anonymous: every row is ciphertext plus a
|
|
7
|
+
* one-byte view tag, and nothing on it says who a note belongs to. You find
|
|
8
|
+
* yours by trial decryption. The view tag exists to make that cheap — it
|
|
9
|
+
* rejects ~255/256 of other people's rows with one hash instead of a full
|
|
10
|
+
* decrypt.
|
|
11
|
+
*/
|
|
12
|
+
/** Server-side page cap (notes.constants COMPACT_SCAN_MAX_LIMIT). */
|
|
13
|
+
export const COMPACT_SCAN_MAX_LIMIT = 1000;
|
|
14
|
+
/** Server-side cap per fetch-by-commitment request. */
|
|
15
|
+
export const MAX_COMMITMENTS_PER_FETCH = 50;
|
|
16
|
+
function decodeBase64(value) {
|
|
17
|
+
if (typeof atob === "function") {
|
|
18
|
+
const binary = atob(value);
|
|
19
|
+
const out = new Uint8Array(binary.length);
|
|
20
|
+
for (let i = 0; i < binary.length; i++)
|
|
21
|
+
out[i] = binary.charCodeAt(i);
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Walk the compact feed and return the notes that belong to this key.
|
|
28
|
+
*
|
|
29
|
+
* Paginates to exhaustion by default. The X25519 private key is derived once
|
|
30
|
+
* and reused across every row rather than per row — that derivation dominates
|
|
31
|
+
* the cost otherwise.
|
|
32
|
+
*
|
|
33
|
+
* @param walletSecretKey full 64-byte keypair.secretKey
|
|
34
|
+
*/
|
|
35
|
+
export async function scanCompactNotes(walletSecretKey, options = {}) {
|
|
36
|
+
const pageSize = Math.min(options.pageSize ?? 500, COMPACT_SCAN_MAX_LIMIT);
|
|
37
|
+
const x25519Private = toX25519Private(walletSecretKey);
|
|
38
|
+
const notes = [];
|
|
39
|
+
let cursor = options.cursor;
|
|
40
|
+
let scanned = 0;
|
|
41
|
+
let pages = 0;
|
|
42
|
+
for (;;) {
|
|
43
|
+
const page = await compactScan({ limit: pageSize, cursor });
|
|
44
|
+
pages++;
|
|
45
|
+
for (const row of page.notes ?? []) {
|
|
46
|
+
if (!row.compactEphemeralKey || !row.compactBlob)
|
|
47
|
+
continue;
|
|
48
|
+
scanned++;
|
|
49
|
+
const ephemeralPublicKey = decodeBase64(row.compactEphemeralKey);
|
|
50
|
+
// View tag first: one hash rejects almost every row that is not ours.
|
|
51
|
+
const { matches, sharedSecret } = matchesViewTag(walletSecretKey, ephemeralPublicKey, row.viewTag ?? -1, { x25519Private });
|
|
52
|
+
if (!matches)
|
|
53
|
+
continue;
|
|
54
|
+
const opened = decryptCompactNoteCipher(walletSecretKey, ephemeralPublicKey, decodeBase64(row.compactBlob), { sharedSecret, x25519Private });
|
|
55
|
+
if (!opened)
|
|
56
|
+
continue; // view-tag collision, ~1 in 256
|
|
57
|
+
notes.push({
|
|
58
|
+
amount: opened.amount,
|
|
59
|
+
blinding: opened.blinding,
|
|
60
|
+
commitment: row.commitment,
|
|
61
|
+
leafIndex: row.leafIndex,
|
|
62
|
+
treeId: row.treeId,
|
|
63
|
+
mint: row.mintAddress,
|
|
64
|
+
timestamp: row.timestamp,
|
|
65
|
+
txSignature: row.txSignature,
|
|
66
|
+
source: row,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
cursor = page.nextCursor ?? undefined;
|
|
70
|
+
const keepGoing = options.onPage?.({
|
|
71
|
+
scanned,
|
|
72
|
+
matched: notes.length,
|
|
73
|
+
cursor: page.nextCursor,
|
|
74
|
+
});
|
|
75
|
+
if (keepGoing === false)
|
|
76
|
+
break;
|
|
77
|
+
if (!page.hasMore || !cursor)
|
|
78
|
+
return { notes, scanned, nextCursor: null };
|
|
79
|
+
if (options.maxPages && pages >= options.maxPages)
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
return { notes, scanned, nextCursor: cursor ?? null };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Look up stored notes by commitment, chunked to the server's limit.
|
|
86
|
+
*
|
|
87
|
+
* Requests are issued concurrently. Commitments the relayer does not know are
|
|
88
|
+
* simply absent from the result — the array is not positional.
|
|
89
|
+
*/
|
|
90
|
+
export async function fetchNotesByCommitment(commitments, options = {}) {
|
|
91
|
+
const unique = [...new Set(commitments.map((c) => c.trim().toLowerCase()))];
|
|
92
|
+
if (unique.length === 0)
|
|
93
|
+
return [];
|
|
94
|
+
const chunks = [];
|
|
95
|
+
for (let i = 0; i < unique.length; i += MAX_COMMITMENTS_PER_FETCH) {
|
|
96
|
+
chunks.push(unique.slice(i, i + MAX_COMMITMENTS_PER_FETCH));
|
|
97
|
+
}
|
|
98
|
+
const pages = await Promise.all(chunks.map((chunk) => fetchNotesByCommitmentPage({
|
|
99
|
+
commitments: chunk,
|
|
100
|
+
...(options.walletPublicKey
|
|
101
|
+
? { walletPublicKey: options.walletPublicKey }
|
|
102
|
+
: {}),
|
|
103
|
+
})));
|
|
104
|
+
return pages.flatMap((page) => page.notes ?? []);
|
|
105
|
+
}
|
|
@@ -75,3 +75,59 @@ export type NoteSelectionResult<T> = NoteSelection<T> | NoteSelectionFailure;
|
|
|
75
75
|
export declare function selectNotesForAmount<T extends SelectableNote>(notes: T[], amount: bigint | string | number, options?: {
|
|
76
76
|
mint?: string | null;
|
|
77
77
|
}): NoteSelectionResult<T>;
|
|
78
|
+
/**
|
|
79
|
+
* One input to a merge step: an existing note, or the output of an earlier step.
|
|
80
|
+
*
|
|
81
|
+
* Steps chain, so a plan for five notes refers to notes that do not exist yet.
|
|
82
|
+
* Modelling that explicitly beats returning amounts and leaving the caller to
|
|
83
|
+
* work out which is which.
|
|
84
|
+
*/
|
|
85
|
+
export type MergeInput<T> = {
|
|
86
|
+
kind: "note";
|
|
87
|
+
note: T;
|
|
88
|
+
amount: bigint;
|
|
89
|
+
} | {
|
|
90
|
+
kind: "step";
|
|
91
|
+
step: number;
|
|
92
|
+
amount: bigint;
|
|
93
|
+
};
|
|
94
|
+
export interface MergeStep<T> {
|
|
95
|
+
/** 1-based, and the number `{ kind: "step" }` inputs refer to. */
|
|
96
|
+
step: number;
|
|
97
|
+
inputs: [MergeInput<T>, MergeInput<T>];
|
|
98
|
+
/** Value of the single note this step produces. */
|
|
99
|
+
outputAmount: bigint;
|
|
100
|
+
}
|
|
101
|
+
export interface NoteConsolidationPlan<T> {
|
|
102
|
+
ok: true;
|
|
103
|
+
treeId: number;
|
|
104
|
+
/**
|
|
105
|
+
* Merges to run in order, each a 2-in-1-out self-transfer. Empty when the
|
|
106
|
+
* amount is already spendable within MAX_INPUT_NOTES.
|
|
107
|
+
*/
|
|
108
|
+
steps: MergeStep<T>[];
|
|
109
|
+
/** Notes the plan consumes, in the order it consumes them. */
|
|
110
|
+
notes: T[];
|
|
111
|
+
/** Sum of those notes. */
|
|
112
|
+
total: bigint;
|
|
113
|
+
/** total - amount, the change left after the final spend. */
|
|
114
|
+
change: bigint;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Plan how to make `amount` spendable when no one or two notes cover it.
|
|
118
|
+
*
|
|
119
|
+
* `selectNotesForAmount` reports `requiresMerge` but cannot act on it: the
|
|
120
|
+
* circuit takes two inputs, so spending five notes means merging them down
|
|
121
|
+
* first. Each step here is one 2-in-1-out transfer to yourself, and k notes
|
|
122
|
+
* need k-2 of them before a final two-input spend.
|
|
123
|
+
*
|
|
124
|
+
* Merges the two SMALLEST notes each round. That retires dust first and leaves
|
|
125
|
+
* the large notes untouched, so the plan is short and the final spend has the
|
|
126
|
+
* least change.
|
|
127
|
+
*
|
|
128
|
+
* The plan is pure — it performs nothing. Execute the steps with
|
|
129
|
+
* `privateTransfer`, feeding each step's output note into the next.
|
|
130
|
+
*/
|
|
131
|
+
export declare function planNoteConsolidation<T extends SelectableNote>(notes: T[], amount: bigint | string | number, options?: {
|
|
132
|
+
mint?: string | null;
|
|
133
|
+
}): NoteConsolidationPlan<T> | NoteSelectionFailure;
|
|
@@ -186,3 +186,69 @@ function selectWithinTree(bucket, target, treeId) {
|
|
|
186
186
|
requiresMerge: chosen.length > MAX_INPUT_NOTES,
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Plan how to make `amount` spendable when no one or two notes cover it.
|
|
191
|
+
*
|
|
192
|
+
* `selectNotesForAmount` reports `requiresMerge` but cannot act on it: the
|
|
193
|
+
* circuit takes two inputs, so spending five notes means merging them down
|
|
194
|
+
* first. Each step here is one 2-in-1-out transfer to yourself, and k notes
|
|
195
|
+
* need k-2 of them before a final two-input spend.
|
|
196
|
+
*
|
|
197
|
+
* Merges the two SMALLEST notes each round. That retires dust first and leaves
|
|
198
|
+
* the large notes untouched, so the plan is short and the final spend has the
|
|
199
|
+
* least change.
|
|
200
|
+
*
|
|
201
|
+
* The plan is pure — it performs nothing. Execute the steps with
|
|
202
|
+
* `privateTransfer`, feeding each step's output note into the next.
|
|
203
|
+
*/
|
|
204
|
+
export function planNoteConsolidation(notes, amount, options = {}) {
|
|
205
|
+
const target = toBaseUnits(amount, "amount");
|
|
206
|
+
if (target <= 0n) {
|
|
207
|
+
throw new Error(`amount must be positive, got ${target}`);
|
|
208
|
+
}
|
|
209
|
+
const selection = selectNotesForAmount(notes, target, options);
|
|
210
|
+
if (!selection.ok)
|
|
211
|
+
return selection;
|
|
212
|
+
// Already spendable in one transaction.
|
|
213
|
+
if (!selection.requiresMerge) {
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
treeId: selection.treeId,
|
|
217
|
+
steps: [],
|
|
218
|
+
notes: selection.notes,
|
|
219
|
+
total: selection.total,
|
|
220
|
+
change: selection.change,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Ascending, so the two cheapest merge first.
|
|
224
|
+
const pool = selection.notes
|
|
225
|
+
.map((note, index) => ({
|
|
226
|
+
kind: "note",
|
|
227
|
+
note,
|
|
228
|
+
amount: toBaseUnits(note.amount, `notes[${index}].amount`),
|
|
229
|
+
}))
|
|
230
|
+
.sort((a, b) => (a.amount > b.amount ? 1 : a.amount < b.amount ? -1 : 0));
|
|
231
|
+
const steps = [];
|
|
232
|
+
while (pool.length > MAX_INPUT_NOTES) {
|
|
233
|
+
const first = pool.shift();
|
|
234
|
+
const second = pool.shift();
|
|
235
|
+
const outputAmount = first.amount + second.amount;
|
|
236
|
+
const step = steps.length + 1;
|
|
237
|
+
steps.push({ step, inputs: [first, second], outputAmount });
|
|
238
|
+
const produced = { kind: "step", step, amount: outputAmount };
|
|
239
|
+
// Keep the pool ordered so the next round still merges the two smallest.
|
|
240
|
+
const at = pool.findIndex((entry) => entry.amount > outputAmount);
|
|
241
|
+
if (at === -1)
|
|
242
|
+
pool.push(produced);
|
|
243
|
+
else
|
|
244
|
+
pool.splice(at, 0, produced);
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
treeId: selection.treeId,
|
|
249
|
+
steps,
|
|
250
|
+
notes: selection.notes,
|
|
251
|
+
total: selection.total,
|
|
252
|
+
change: selection.change,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckNullifiersResponse, ChallengeResponse, CheckUsernameResponse, DeleteUserDataResponse, MerkleRootResponse, MerkleTreeResponse, PreRegisterKeyResponse, PrivateSwapRequest, PrivateSwapResponse, PrivateTransferRequest, PrivateTransferResponse, QueryNotesRequest, QueryNotesResponse, RegisterRequest, RegisterResponse, RestoreRequest, RestoreResponse, SaveEncryptedNoteRequest, SaveEncryptedNoteResponse, VeiloPublicKeyResponse, WithdrawRequest, WithdrawResponse } from "./types.js";
|
|
1
|
+
import type { CheckNullifiersResponse, CompactScanResponse, FetchByCommitmentRequest, FetchByCommitmentResponse, ChallengeResponse, CheckUsernameResponse, DeleteUserDataResponse, MerkleRootResponse, MerkleTreeResponse, PreRegisterKeyResponse, PrivateSwapRequest, PrivateSwapResponse, PrivateTransferRequest, PrivateTransferResponse, QueryNotesRequest, QueryNotesResponse, RegisterRequest, RegisterResponse, RestoreRequest, RestoreResponse, SaveEncryptedNoteRequest, SaveEncryptedNoteResponse, VeiloPublicKeyResponse, WithdrawRequest, WithdrawResponse } from "./types.js";
|
|
2
2
|
/** Request a wallet-authentication challenge from Veilo. */
|
|
3
3
|
export declare function getChallenge(walletPublicKey: string): Promise<ChallengeResponse>;
|
|
4
4
|
/** Register a Veilo account and receive its auth token. */
|
|
@@ -19,6 +19,16 @@ export declare function saveEncryptedNote(data: SaveEncryptedNoteRequest): Promi
|
|
|
19
19
|
export declare function queryEncryptedNotes(authToken: string, params?: QueryNotesRequest): Promise<QueryNotesResponse>;
|
|
20
20
|
/** Delete the authenticated user's stored data without retaining their token. */
|
|
21
21
|
export declare function deleteUserData(authToken: string): Promise<DeleteUserDataResponse>;
|
|
22
|
+
/** One page of the public compact-note feed. Prefer `scanCompactNotes`. */
|
|
23
|
+
export declare function compactScan(params?: {
|
|
24
|
+
limit?: number;
|
|
25
|
+
cursor?: string;
|
|
26
|
+
}): Promise<CompactScanResponse>;
|
|
27
|
+
/**
|
|
28
|
+
* One fetch-by-commitment request, capped at 50 commitments by the server.
|
|
29
|
+
* Prefer `fetchNotesByCommitment` from the notes module, which chunks for you.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fetchNotesByCommitmentPage(data: FetchByCommitmentRequest): Promise<FetchByCommitmentResponse>;
|
|
22
32
|
/** Raw check-batch call. Prefer `checkNullifiersSpent`, which chunks for you. */
|
|
23
33
|
export declare function checkNullifiers(nullifiers: string[]): Promise<CheckNullifiersResponse>;
|
|
24
34
|
export declare function getMerkleRoot(mintAddress?: string, treeId?: number): Promise<MerkleRootResponse>;
|
package/dist/esm/relayer/api.js
CHANGED
|
@@ -31,6 +31,17 @@ export function queryEncryptedNotes(authToken, params = {}) {
|
|
|
31
31
|
export function deleteUserData(authToken) {
|
|
32
32
|
return createInternalRelayerClient({ authToken }).deleteUserData();
|
|
33
33
|
}
|
|
34
|
+
/** One page of the public compact-note feed. Prefer `scanCompactNotes`. */
|
|
35
|
+
export function compactScan(params = {}) {
|
|
36
|
+
return getInternalRelayerClient().compactScan(params);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One fetch-by-commitment request, capped at 50 commitments by the server.
|
|
40
|
+
* Prefer `fetchNotesByCommitment` from the notes module, which chunks for you.
|
|
41
|
+
*/
|
|
42
|
+
export function fetchNotesByCommitmentPage(data) {
|
|
43
|
+
return getInternalRelayerClient().fetchNotesByCommitment(data);
|
|
44
|
+
}
|
|
34
45
|
/** Raw check-batch call. Prefer `checkNullifiersSpent`, which chunks for you. */
|
|
35
46
|
export function checkNullifiers(nullifiers) {
|
|
36
47
|
return getInternalRelayerClient().checkNullifiers(nullifiers);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckNullifiersResponse, ChallengeResponse, CheckUsernameResponse, DeleteUserDataResponse, MerkleRootResponse, MerkleTreeResponse, PreRegisterKeyResponse, PrivateSwapRequest, PrivateSwapResponse, PrivateTransferRequest, PrivateTransferResponse, QueryNotesRequest, QueryNotesResponse, RegisterRequest, RegisterResponse, RestoreRequest, RestoreResponse, RetryPolicy, SaveEncryptedNoteRequest, SaveEncryptedNoteResponse, VeiloPublicKeyResponse, WithdrawRequest, WithdrawResponse } from "./types.js";
|
|
1
|
+
import type { CheckNullifiersResponse, CompactScanResponse, FetchByCommitmentRequest, FetchByCommitmentResponse, ChallengeResponse, CheckUsernameResponse, DeleteUserDataResponse, MerkleRootResponse, MerkleTreeResponse, PreRegisterKeyResponse, PrivateSwapRequest, PrivateSwapResponse, PrivateTransferRequest, PrivateTransferResponse, QueryNotesRequest, QueryNotesResponse, RegisterRequest, RegisterResponse, RestoreRequest, RestoreResponse, RetryPolicy, SaveEncryptedNoteRequest, SaveEncryptedNoteResponse, VeiloPublicKeyResponse, WithdrawRequest, WithdrawResponse } from "./types.js";
|
|
2
2
|
import { type InternalRelayerClientOptions } from "./internal-config.js";
|
|
3
3
|
import type { ClosePositionRequest, ClosePositionResponse, MergePositionsRequest, MergePositionsResponse, OpenPositionRequest, OpenPositionResponse } from "../positions/types.js";
|
|
4
4
|
import type { JperpCancelTriggerRequest, JperpCancelTriggerResponse, JperpCloseRequest, JperpDecreaseResponse, JperpOpenRequest, JperpOpenResponse, JperpRecoverNativeRequest, JperpRecoverNativeResponse, JperpRecoverSeedRequest, JperpRecoverSeedResponse, JperpReissueRequest, JperpReissueResponse, JperpSetTpslRequest, JperpUpdateTpslRequest } from "../perps/types.js";
|
|
@@ -37,6 +37,19 @@ export declare class VeiloRelayerClient {
|
|
|
37
37
|
/** Query encrypted notes for the authenticated user. Requires auth token. */
|
|
38
38
|
queryEncryptedNotes(params?: QueryNotesRequest): Promise<QueryNotesResponse>;
|
|
39
39
|
/** Delete all stored data for the authenticated user. Requires auth token. */
|
|
40
|
+
/**
|
|
41
|
+
* One page of the public compact-note feed.
|
|
42
|
+
*
|
|
43
|
+
* Unauthenticated by design: rows carry only ciphertext and a one-byte view
|
|
44
|
+
* tag, so scanning reveals nothing without the recipient's key. The feed
|
|
45
|
+
* excludes spent notes and any note already claimed by a wallet.
|
|
46
|
+
*/
|
|
47
|
+
compactScan(params?: {
|
|
48
|
+
limit?: number;
|
|
49
|
+
cursor?: string;
|
|
50
|
+
}): Promise<CompactScanResponse>;
|
|
51
|
+
/** Look up stored notes by commitment. Max 50 per request. */
|
|
52
|
+
fetchNotesByCommitment(data: FetchByCommitmentRequest): Promise<FetchByCommitmentResponse>;
|
|
40
53
|
/**
|
|
41
54
|
* Ask which of these nullifiers the relayer has seen spent.
|
|
42
55
|
*
|
|
@@ -75,6 +75,28 @@ export class VeiloRelayerClient {
|
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
/** Delete all stored data for the authenticated user. Requires auth token. */
|
|
78
|
+
/**
|
|
79
|
+
* One page of the public compact-note feed.
|
|
80
|
+
*
|
|
81
|
+
* Unauthenticated by design: rows carry only ciphertext and a one-byte view
|
|
82
|
+
* tag, so scanning reveals nothing without the recipient's key. The feed
|
|
83
|
+
* excludes spent notes and any note already claimed by a wallet.
|
|
84
|
+
*/
|
|
85
|
+
async compactScan(params = {}) {
|
|
86
|
+
const query = new URLSearchParams();
|
|
87
|
+
if (params.limit !== undefined)
|
|
88
|
+
query.set("limit", String(params.limit));
|
|
89
|
+
if (params.cursor)
|
|
90
|
+
query.set("cursor", params.cursor);
|
|
91
|
+
const qs = query.toString();
|
|
92
|
+
return this.transport.request("GET", `/notes/compact-scan${qs ? `?${qs}` : ""}`);
|
|
93
|
+
}
|
|
94
|
+
/** Look up stored notes by commitment. Max 50 per request. */
|
|
95
|
+
async fetchNotesByCommitment(data) {
|
|
96
|
+
return this.transport.request("POST", "/notes/fetch-by-commitment", {
|
|
97
|
+
body: data,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
78
100
|
/**
|
|
79
101
|
* Ask which of these nullifiers the relayer has seen spent.
|
|
80
102
|
*
|
|
@@ -39,6 +39,37 @@ export interface QueryNotesResponse {
|
|
|
39
39
|
limit: number;
|
|
40
40
|
offset: number;
|
|
41
41
|
}
|
|
42
|
+
/** One row from the public compact-note scan feed. */
|
|
43
|
+
export interface CompactScanNote {
|
|
44
|
+
_id: string;
|
|
45
|
+
commitment: string;
|
|
46
|
+
compactEphemeralKey: string | null;
|
|
47
|
+
compactBlob: string | null;
|
|
48
|
+
viewTag: number | null;
|
|
49
|
+
leafIndex?: number | null;
|
|
50
|
+
treeId?: number | null;
|
|
51
|
+
mintAddress?: string | null;
|
|
52
|
+
timestamp?: number;
|
|
53
|
+
txSignature?: string;
|
|
54
|
+
spent?: boolean;
|
|
55
|
+
spentTx?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface CompactScanResponse {
|
|
58
|
+
success: boolean;
|
|
59
|
+
notes: CompactScanNote[];
|
|
60
|
+
/** Opaque cursor for the next page; null when the feed is exhausted. */
|
|
61
|
+
nextCursor: string | null;
|
|
62
|
+
hasMore: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Commitments to look up. Max 50 per request, 64-char hex. */
|
|
65
|
+
export interface FetchByCommitmentRequest {
|
|
66
|
+
commitments: string[];
|
|
67
|
+
walletPublicKey?: string;
|
|
68
|
+
}
|
|
69
|
+
export interface FetchByCommitmentResponse {
|
|
70
|
+
success: boolean;
|
|
71
|
+
notes: RelayerEncryptedNote[];
|
|
72
|
+
}
|
|
42
73
|
/** Nullifiers to test for spend status. Max 200 per request, 64-char hex. */
|
|
43
74
|
export interface CheckNullifiersRequest {
|
|
44
75
|
nullifiers: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veilo/sdk-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "TypeScript SDK for the Veilo Privacy Pool - UTXO-based privacy protocol on Solana with ZK-SNARKs",
|
|
5
5
|
"homepage": "https://github.com/VeiloSolana/veilo-sdk#readme",
|
|
6
6
|
"bugs": {
|