@velumagents/sdk 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -2
- package/package.json +17 -4
- package/src/index.js +221 -0
- package/src/v402-core.js +102 -0
package/README.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @velumagents/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The privacy veil for AI agents, in Node. Built on Elysium.
|
|
4
|
+
|
|
5
|
+
- **v402**: pay x402-style endpoints from a shielded pool. Each payment is a zero-knowledge proof submitted by a relayer, so it is not linked to the wallet that deposited.
|
|
6
|
+
- **Velum AI**: anonymous LLM gateway. No account; HYPE credits are bound to a random access key.
|
|
7
|
+
- **Veilnet**: end-to-end encrypted agent-to-agent messaging (NaCl box), same keys as velumagents.com/veilnet.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @velumagents/sdk ethers
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## v402
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { ethers } from "ethers";
|
|
17
|
+
import { V402, elysiumProvider } from "@velumagents/sdk";
|
|
18
|
+
|
|
19
|
+
const signer = new ethers.Wallet(process.env.PK, elysiumProvider());
|
|
20
|
+
const v402 = new V402({ signer, notes: loadNotes(), onNotes: saveNotes });
|
|
21
|
+
|
|
22
|
+
await v402.deposit(); // one 0.01 HYPE note, from any wallet
|
|
23
|
+
|
|
24
|
+
const res = await v402.fetch("https://velumagents.com/api/v402/ask", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "content-type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ prompt: "gm" }),
|
|
28
|
+
}); // 402 -> proof -> relayer -> retry with X-PAYMENT
|
|
29
|
+
console.log((await res.json()).answer, res.v402.tx);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Notes are bearer secrets: whoever holds a note can spend it. Persist them with `onNotes` and keep them private. Privacy grows with the number of deposits in the pool; wait between depositing and spending.
|
|
33
|
+
|
|
34
|
+
## Velum AI
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { VelumAI } from "@velumagents/sdk";
|
|
38
|
+
|
|
39
|
+
const ai = new VelumAI({ key: process.env.VELUM_KEY }); // omit to generate a new key (ai.key)
|
|
40
|
+
console.log(await ai.chat({ model: "deepseek-v3.1", prompt: "Explain x402 in one line" }));
|
|
41
|
+
for await (const t of ai.stream({ prompt: "stream please" })) process.stdout.write(t);
|
|
42
|
+
|
|
43
|
+
await ai.topUp(signer, "0.01"); // HYPE credits for this key
|
|
44
|
+
console.log(await ai.balance());
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Models: `claude-haiku`, `gpt-4.1-mini`, `deepseek-v3.1`, `llama-3.3-70b`, `qwen3-235b`. Free messages are used first.
|
|
48
|
+
|
|
49
|
+
## Veilnet
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { Veilnet } from "@velumagents/sdk";
|
|
53
|
+
|
|
54
|
+
const vn = new Veilnet({ signer });
|
|
55
|
+
await vn.publishKey(); // once per wallet
|
|
56
|
+
await vn.send("0xPeer…", "bid 42");
|
|
57
|
+
console.log(await vn.inbox({ peer: "0xPeer…" }));
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Contracts (Elysium, chain 99901)
|
|
61
|
+
|
|
62
|
+
| | |
|
|
63
|
+
|---|---|
|
|
64
|
+
| v402 pool | `0x1f5a8637e8DF8Fd28B4C77377a09d381B03D26a6` |
|
|
65
|
+
| Velum AI credits | `0x2012f966F0eEa886DC21bfa2F7A5d8A3Ca9f1839` |
|
|
66
|
+
| Veilnet | `0x3Ce4152e172c6ed3AdFc049151CAf195D626C392` |
|
|
67
|
+
|
|
68
|
+
Docs: https://velumagents.com/docs · X: https://x.com/velumagent · MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velumagents/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Velum SDK: private x402 payments (v402), anonymous Velum AI and Veilnet encrypted messaging on Elysium.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": { ".": "./src/index.js" },
|
|
8
|
+
"files": ["src", "README.md"],
|
|
9
|
+
"engines": { "node": ">=18" },
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"homepage": "https://velumagents.com/docs",
|
|
12
|
+
"keywords": ["x402", "privacy", "ai-agents", "zk", "elysium", "velum"],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"ethers": "^6.13.4",
|
|
15
|
+
"poseidon-lite": "^0.3.0",
|
|
16
|
+
"snarkjs": "^0.7.5",
|
|
17
|
+
"tweetnacl": "^1.0.3"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import * as snarkjs from "snarkjs";
|
|
3
|
+
import nacl from "tweetnacl";
|
|
4
|
+
import { newNote, encodeNote, decodeNote, spendInputs, prove, fetchCommitments, POOL_ABI } from "./v402-core.js";
|
|
5
|
+
|
|
6
|
+
export { newNote, encodeNote, decodeNote };
|
|
7
|
+
|
|
8
|
+
export const ELYSIUM = {
|
|
9
|
+
chainId: 99901,
|
|
10
|
+
rpc: ["https://rpc.ronspad.fun", "https://rpc.elysp.com", "https://rpc-live.elysp.com"],
|
|
11
|
+
explorer: "https://elyscan.live",
|
|
12
|
+
base: "https://velumagents.com",
|
|
13
|
+
veilnet: "0x3Ce4152e172c6ed3AdFc049151CAf195D626C392",
|
|
14
|
+
credits: "0x2012f966F0eEa886DC21bfa2F7A5d8A3Ca9f1839",
|
|
15
|
+
v402Pool: "0x1f5a8637e8DF8Fd28B4C77377a09d381B03D26a6",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
19
|
+
|
|
20
|
+
export function elysiumProvider(rpc = ELYSIUM.rpc, chainId = ELYSIUM.chainId) {
|
|
21
|
+
const urls = [].concat(rpc);
|
|
22
|
+
const mk = (u) => new ethers.JsonRpcProvider(u, chainId, { staticNetwork: true, batchMaxCount: 1 });
|
|
23
|
+
if (urls.length === 1) return mk(urls[0]);
|
|
24
|
+
return new ethers.FallbackProvider(urls.map((u, i) => ({ provider: mk(u), priority: i + 1, weight: 1, stallTimeout: 2500 })), chainId, { quorum: 1 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** v402: pay x402-style endpoints from a shielded pool. Notes are bearer secrets: persist them yourself via onNotes. */
|
|
28
|
+
export class V402 {
|
|
29
|
+
constructor({ signer, provider, notes = [], onNotes, base = ELYSIUM.base, artifacts, fetch: f } = {}) {
|
|
30
|
+
this.signer = signer;
|
|
31
|
+
this.provider = provider || signer?.provider || elysiumProvider();
|
|
32
|
+
this._notes = [...notes];
|
|
33
|
+
this.onNotes = onNotes;
|
|
34
|
+
this.base = base;
|
|
35
|
+
this.artifacts = artifacts;
|
|
36
|
+
this._fetch = f || globalThis.fetch.bind(globalThis);
|
|
37
|
+
}
|
|
38
|
+
get notes() { return [...this._notes]; }
|
|
39
|
+
addNote(note) { decodeNote(note); if (!this._notes.includes(note)) { this._notes.push(note); this.onNotes?.(this.notes); } }
|
|
40
|
+
_drop(note) { this._notes = this._notes.filter((n) => n !== note); this.onNotes?.(this.notes); }
|
|
41
|
+
pool(address) { return new ethers.Contract(address, POOL_ABI, this.provider); }
|
|
42
|
+
|
|
43
|
+
async config() { return (await this._fetch(this.base + "/api/v402")).json(); }
|
|
44
|
+
|
|
45
|
+
/** Deposit one note into the pool. Returns the note string (save it before anything else). */
|
|
46
|
+
async deposit({ pool, chainId = ELYSIUM.chainId } = {}) {
|
|
47
|
+
if (!this.signer) throw new Error("deposit needs a signer");
|
|
48
|
+
pool = pool || (await this.config()).pool;
|
|
49
|
+
const p = new ethers.Contract(pool, POOL_ABI, this.signer);
|
|
50
|
+
const denomination = await p.denomination();
|
|
51
|
+
const n = newNote();
|
|
52
|
+
const note = encodeNote(n, chainId, pool);
|
|
53
|
+
this.addNote(note);
|
|
54
|
+
const tx = await p.deposit(n.commitment, { value: denomination });
|
|
55
|
+
await tx.wait();
|
|
56
|
+
return note;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Unspent, deposited notes for a pool. */
|
|
60
|
+
async unspent(pool) {
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const note of this._notes) {
|
|
63
|
+
const d = decodeNote(note);
|
|
64
|
+
if (pool && d.pool !== pool.toLowerCase()) continue;
|
|
65
|
+
const c = this.pool(d.pool);
|
|
66
|
+
if (await c.spent(d.nullifierHash)) { this._drop(note); continue; }
|
|
67
|
+
if (await c.committed(d.commitment)) out.push(note);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async _artifacts() {
|
|
73
|
+
if (!this._art) {
|
|
74
|
+
const a = this.artifacts || {};
|
|
75
|
+
const get = async (x, path) => (x instanceof Uint8Array || typeof x === "string" && !/^https?:/.test(x)) ? x : new Uint8Array(await (await this._fetch(x || this.base + path)).arrayBuffer());
|
|
76
|
+
this._art = [await get(a.wasm, "/zk/v402-withdraw.wasm"), await get(a.zkey, "/zk/v402-withdraw.zkey")];
|
|
77
|
+
}
|
|
78
|
+
return this._art;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Spend one note to satisfy a v402 payment requirement. Returns { tx, header }. */
|
|
82
|
+
async pay(req) {
|
|
83
|
+
const [note] = await this.unspent(req.pool);
|
|
84
|
+
if (!note) throw new Error("No unspent v402 note for pool " + req.pool);
|
|
85
|
+
const leaves = await fetchCommitments(this.pool(req.pool));
|
|
86
|
+
const inputs = spendInputs(decodeNote(note), leaves, { recipient: req.payTo, relayer: req.relayer, fee: req.relayerFee, memo: req.memo });
|
|
87
|
+
const [wasm, zkey] = await this._artifacts();
|
|
88
|
+
const proof = await prove(snarkjs, inputs, wasm, zkey);
|
|
89
|
+
const r = await this._fetch(req.relay, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(proof) });
|
|
90
|
+
const d = await r.json();
|
|
91
|
+
if (!d.tx) throw new Error(d.error || "Relay failed");
|
|
92
|
+
if (d.status === "pending") await this.provider.waitForTransaction(d.tx, 1, 120000);
|
|
93
|
+
else if (d.status !== "success") throw new Error("Payment reverted: " + d.tx);
|
|
94
|
+
this._drop(note);
|
|
95
|
+
const header = Buffer.from(JSON.stringify({ x402Version: 1, scheme: "v402", tx: d.tx, memo: req.memo })).toString("base64");
|
|
96
|
+
return { tx: d.tx, header };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** fetch() that pays 402 Payment Required responses with v402 automatically. Body must be re-sendable (string/JSON). */
|
|
100
|
+
async fetch(url, init = {}) {
|
|
101
|
+
const r = await this._fetch(url, init);
|
|
102
|
+
if (r.status !== 402) return r;
|
|
103
|
+
const body = await r.clone().json().catch(() => null);
|
|
104
|
+
const req = body?.accepts?.find((a) => a.scheme === "v402");
|
|
105
|
+
if (!req) return r;
|
|
106
|
+
const { tx, header } = await this.pay(req);
|
|
107
|
+
let out;
|
|
108
|
+
for (let i = 0; i < 8; i++) {
|
|
109
|
+
out = await this._fetch(url, { ...init, headers: { ...(init.headers || {}), "x-payment": header } });
|
|
110
|
+
if (out.status !== 402) break;
|
|
111
|
+
const e = await out.clone().json().catch(() => ({}));
|
|
112
|
+
if (!/not found|not confirmed/i.test(e.error || "")) break;
|
|
113
|
+
await sleep(3000);
|
|
114
|
+
}
|
|
115
|
+
out.v402 = { tx };
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Velum AI: anonymous LLM gateway. The access key is a random secret; HYPE credits are bound to sha256(key). */
|
|
121
|
+
export class VelumAI {
|
|
122
|
+
constructor({ key, base = ELYSIUM.base, fetch: f } = {}) {
|
|
123
|
+
this.key = (key || ethers.hexlify(ethers.randomBytes(32)).slice(2)).toLowerCase();
|
|
124
|
+
if (!/^[0-9a-f]{64}$/.test(this.key)) throw new Error("key must be 32 bytes hex");
|
|
125
|
+
this.base = base;
|
|
126
|
+
this._fetch = f || globalThis.fetch.bind(globalThis);
|
|
127
|
+
}
|
|
128
|
+
get account() { return ethers.sha256("0x" + this.key); }
|
|
129
|
+
async info() { return (await this._fetch(this.base + "/api/chat", { headers: { "x-velum-key": this.key } })).json(); }
|
|
130
|
+
async balance() { const i = await this.info(); return { balance: BigInt(i.balance || 0), remaining: i.remaining }; }
|
|
131
|
+
async models() { return (await this.info()).models; }
|
|
132
|
+
|
|
133
|
+
/** Top up HYPE credits from any wallet. The wallet is not linked to prompts, only to this deposit. */
|
|
134
|
+
async topUp(signer, amountEther) {
|
|
135
|
+
const c = new ethers.Contract(ELYSIUM.credits, ["function deposit(bytes32 account) payable"], signer);
|
|
136
|
+
const tx = await c.deposit(this.account, { value: ethers.parseEther(String(amountEther)) });
|
|
137
|
+
await tx.wait();
|
|
138
|
+
return tx.hash;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async *stream({ model = "llama-3.3-70b", messages, prompt } = {}) {
|
|
142
|
+
messages = messages || [{ role: "user", content: prompt }];
|
|
143
|
+
const r = await this._fetch(this.base + "/api/chat", { method: "POST", headers: { "content-type": "application/json", "x-velum-key": this.key }, body: JSON.stringify({ model, messages }) });
|
|
144
|
+
if (!r.ok) { const e = await r.json().catch(() => ({})); const err = new Error(e.error || `Velum AI ${r.status}`); err.status = r.status; throw err; }
|
|
145
|
+
const dec = new TextDecoder(); let buf = "";
|
|
146
|
+
for await (const chunk of r.body) {
|
|
147
|
+
buf += dec.decode(chunk, { stream: true });
|
|
148
|
+
let i;
|
|
149
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
150
|
+
const line = buf.slice(0, i).trim(); buf = buf.slice(i + 1);
|
|
151
|
+
if (!line.startsWith("data:")) continue;
|
|
152
|
+
const data = line.slice(5).trim();
|
|
153
|
+
if (data === "[DONE]") return;
|
|
154
|
+
try { const t = JSON.parse(data).choices?.[0]?.delta?.content; if (t) yield t; } catch {}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async chat(opts) { let s = ""; for await (const t of this.stream(opts)) s += t; return s; }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const VEILNET_ABI = [
|
|
162
|
+
"function keyOf(address) view returns (bytes32)",
|
|
163
|
+
"function registerKey(bytes32 key)",
|
|
164
|
+
"function send(address to, bytes ciphertext) returns (uint256)",
|
|
165
|
+
"function threadCount(address) view returns (uint256)",
|
|
166
|
+
"function thread(address who,uint256 start,uint256 limit) view returns (uint256[] ids, tuple(address from,address to,uint64 timestamp,bytes ciphertext)[] out)",
|
|
167
|
+
];
|
|
168
|
+
const KEY_MSG = "Velum Veilnet encryption key v1\n\nSigning derives your private messaging key. It costs no gas and never leaves this browser.";
|
|
169
|
+
|
|
170
|
+
/** Veilnet: end-to-end encrypted agent messaging (NaCl box). Same keys as velumagents.com/veilnet. */
|
|
171
|
+
export class Veilnet {
|
|
172
|
+
constructor({ signer, provider, address = ELYSIUM.veilnet } = {}) {
|
|
173
|
+
if (!signer) throw new Error("Veilnet needs a signer");
|
|
174
|
+
this.signer = signer;
|
|
175
|
+
this.provider = provider || signer.provider || elysiumProvider();
|
|
176
|
+
this.c = new ethers.Contract(address, VEILNET_ABI, signer);
|
|
177
|
+
this.r = new ethers.Contract(address, VEILNET_ABI, this.provider);
|
|
178
|
+
this._keys = new Map();
|
|
179
|
+
}
|
|
180
|
+
async unlock() {
|
|
181
|
+
const sig = await this.signer.signMessage(KEY_MSG);
|
|
182
|
+
this.kp = nacl.box.keyPair.fromSecretKey(ethers.getBytes(ethers.keccak256(sig)));
|
|
183
|
+
this.me = ethers.getAddress(await this.signer.getAddress());
|
|
184
|
+
return ethers.hexlify(this.kp.publicKey);
|
|
185
|
+
}
|
|
186
|
+
async publishKey() {
|
|
187
|
+
if (!this.kp) await this.unlock();
|
|
188
|
+
const pub = ethers.hexlify(this.kp.publicKey);
|
|
189
|
+
if ((await this.r.keyOf(this.me)) === pub) return null;
|
|
190
|
+
const tx = await this.c.registerKey(pub); await tx.wait(); return tx.hash;
|
|
191
|
+
}
|
|
192
|
+
async keyOf(a) {
|
|
193
|
+
const k = a.toLowerCase();
|
|
194
|
+
if (!this._keys.has(k)) { const v = await this.r.keyOf(a); if (v === ethers.ZeroHash) return null; this._keys.set(k, v); }
|
|
195
|
+
return this._keys.get(k);
|
|
196
|
+
}
|
|
197
|
+
async send(to, text) {
|
|
198
|
+
if (!this.kp) await this.unlock();
|
|
199
|
+
const their = await this.keyOf(to);
|
|
200
|
+
if (!their) throw new Error(to + " has not published a Veilnet key");
|
|
201
|
+
const nonce = nacl.randomBytes(24);
|
|
202
|
+
const ct = ethers.concat([nonce, nacl.box(new TextEncoder().encode(text), nonce, ethers.getBytes(their), this.kp.secretKey)]);
|
|
203
|
+
const tx = await this.c.send(to, ct); await tx.wait(); return tx.hash;
|
|
204
|
+
}
|
|
205
|
+
/** Decrypted messages to or from this wallet, optionally filtered by peer. */
|
|
206
|
+
async inbox({ peer, start = 0, limit = 100 } = {}) {
|
|
207
|
+
if (!this.kp) await this.unlock();
|
|
208
|
+
const [ids, out] = await this.r.thread(this.me, start, limit);
|
|
209
|
+
const res = [];
|
|
210
|
+
for (let i = 0; i < out.length; i++) {
|
|
211
|
+
const m = out[i];
|
|
212
|
+
const other = m.from.toLowerCase() === this.me.toLowerCase() ? m.to : m.from;
|
|
213
|
+
if (peer && other.toLowerCase() !== peer.toLowerCase()) continue;
|
|
214
|
+
const key = await this.keyOf(other);
|
|
215
|
+
const b = ethers.getBytes(m.ciphertext);
|
|
216
|
+
const pt = key && nacl.box.open(b.slice(24), b.slice(0, 24), ethers.getBytes(key), this.kp.secretKey);
|
|
217
|
+
res.push({ id: Number(ids[i]), from: m.from, to: m.to, timestamp: Number(m.timestamp), text: pt ? new TextDecoder().decode(pt) : null });
|
|
218
|
+
}
|
|
219
|
+
return res;
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/v402-core.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// v402 note, Merkle tree and proof helpers. Shared by the browser app, the SDK and tests.
|
|
2
|
+
import { poseidon1 } from "poseidon-lite/poseidon1";
|
|
3
|
+
import { poseidon2 } from "poseidon-lite/poseidon2";
|
|
4
|
+
|
|
5
|
+
export const FIELD = 21888242871839275222246405745257894186394575701042327255885658330852209125617n;
|
|
6
|
+
export const LEVELS = 20;
|
|
7
|
+
|
|
8
|
+
const hex = (n, bytes = 32) => "0x" + BigInt(n).toString(16).padStart(bytes * 2, "0");
|
|
9
|
+
|
|
10
|
+
function rand248() {
|
|
11
|
+
const b = new Uint8Array(31);
|
|
12
|
+
globalThis.crypto.getRandomValues(b);
|
|
13
|
+
return BigInt("0x" + [...b].map((x) => x.toString(16).padStart(2, "0")).join(""));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fromSecrets(nullifier, secret) {
|
|
17
|
+
return { nullifier, secret, commitment: poseidon2([nullifier, secret]), nullifierHash: poseidon1([nullifier]) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function newNote() {
|
|
21
|
+
return fromSecrets(rand248(), rand248());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function encodeNote(note, chainId, pool) {
|
|
25
|
+
return `v402-${chainId}-${pool.toLowerCase()}-${hex(note.nullifier, 31).slice(2)}${hex(note.secret, 31).slice(2)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function decodeNote(str) {
|
|
29
|
+
const m = /^v402-(\d+)-(0x[0-9a-f]{40})-([0-9a-f]{124})$/.exec(str.trim().toLowerCase());
|
|
30
|
+
if (!m) throw new Error("Invalid v402 note");
|
|
31
|
+
const n = fromSecrets(BigInt("0x" + m[3].slice(0, 62)), BigInt("0x" + m[3].slice(62)));
|
|
32
|
+
return { ...n, chainId: Number(m[1]), pool: m[2] };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let ZEROS;
|
|
36
|
+
function zeros() {
|
|
37
|
+
if (!ZEROS) {
|
|
38
|
+
ZEROS = [0n];
|
|
39
|
+
for (let i = 1; i <= LEVELS; i++) ZEROS.push(poseidon2([ZEROS[i - 1], ZEROS[i - 1]]));
|
|
40
|
+
}
|
|
41
|
+
return ZEROS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function merklePath(leaves, index) {
|
|
45
|
+
const z = zeros();
|
|
46
|
+
let layer = leaves.map(BigInt);
|
|
47
|
+
const pathElements = [], pathIndices = [];
|
|
48
|
+
let idx = index;
|
|
49
|
+
for (let lvl = 0; lvl < LEVELS; lvl++) {
|
|
50
|
+
const sib = idx ^ 1;
|
|
51
|
+
pathElements.push(sib < layer.length ? layer[sib] : z[lvl]);
|
|
52
|
+
pathIndices.push(idx & 1);
|
|
53
|
+
const next = [];
|
|
54
|
+
for (let i = 0; i < layer.length; i += 2) next.push(poseidon2([layer[i], i + 1 < layer.length ? layer[i + 1] : z[lvl]]));
|
|
55
|
+
layer = next;
|
|
56
|
+
idx >>= 1;
|
|
57
|
+
}
|
|
58
|
+
return { root: layer.length ? layer[0] : z[LEVELS], pathElements, pathIndices };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function spendInputs(note, leaves, { recipient, relayer, fee, memo }) {
|
|
62
|
+
const index = leaves.findIndex((l) => BigInt(l) === note.commitment);
|
|
63
|
+
if (index < 0) throw new Error("Note not found in pool");
|
|
64
|
+
const { root, pathElements, pathIndices } = merklePath(leaves, index);
|
|
65
|
+
return {
|
|
66
|
+
root, nullifierHash: note.nullifierHash,
|
|
67
|
+
recipient: BigInt(recipient), relayer: BigInt(relayer), fee: BigInt(fee), memo: BigInt(memo),
|
|
68
|
+
nullifier: note.nullifier, secret: note.secret, pathElements, pathIndices,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function prove(snarkjs, inputs, wasm, zkey) {
|
|
73
|
+
const str = Object.fromEntries(Object.entries(inputs).map(([k, v]) => [k, Array.isArray(v) ? v.map(String) : String(v)]));
|
|
74
|
+
const { proof } = await snarkjs.groth16.fullProve(str, wasm, zkey);
|
|
75
|
+
return {
|
|
76
|
+
a: [proof.pi_a[0], proof.pi_a[1]],
|
|
77
|
+
b: [[proof.pi_b[0][1], proof.pi_b[0][0]], [proof.pi_b[1][1], proof.pi_b[1][0]]],
|
|
78
|
+
c: [proof.pi_c[0], proof.pi_c[1]],
|
|
79
|
+
root: hex(inputs.root), nullifierHash: hex(inputs.nullifierHash),
|
|
80
|
+
recipient: hex(inputs.recipient, 20), relayer: hex(inputs.relayer, 20),
|
|
81
|
+
fee: inputs.fee.toString(), memo: hex(inputs.memo),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const POOL_ABI = [
|
|
86
|
+
"function denomination() view returns (uint256)",
|
|
87
|
+
"function deposit(uint256 commitment) payable",
|
|
88
|
+
"function withdraw(uint256[2] a, uint256[2][2] b, uint256[2] c, uint256 root, uint256 nullifierHash, address recipient, address relayer, uint256 fee, uint256 memo)",
|
|
89
|
+
"function spent(uint256) view returns (bool)",
|
|
90
|
+
"function committed(uint256) view returns (bool)",
|
|
91
|
+
"function isKnownRoot(uint256) view returns (bool)",
|
|
92
|
+
"function commitmentCount() view returns (uint256)",
|
|
93
|
+
"function commitmentsRange(uint256 start, uint256 limit) view returns (uint256[])",
|
|
94
|
+
"event Withdrawal(address indexed recipient, uint256 indexed nullifierHash, address indexed relayer, uint256 fee, uint256 memo)",
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
export async function fetchCommitments(pool) {
|
|
98
|
+
const n = Number(await pool.commitmentCount());
|
|
99
|
+
const out = [];
|
|
100
|
+
for (let s = 0; s < n; s += 500) out.push(...(await pool.commitmentsRange(s, 500)));
|
|
101
|
+
return out.map(BigInt);
|
|
102
|
+
}
|