@unlink-xyz/sdk 0.0.2-canary.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/dist/index.js ADDED
@@ -0,0 +1,1465 @@
1
+ // src/crypto/poseidon.ts
2
+ import {
3
+ poseidon1,
4
+ poseidon2,
5
+ poseidon3,
6
+ poseidon4,
7
+ poseidon5,
8
+ poseidon6,
9
+ poseidon7,
10
+ poseidon8,
11
+ poseidon9,
12
+ poseidon10,
13
+ poseidon11,
14
+ poseidon12,
15
+ poseidon13,
16
+ poseidon14,
17
+ poseidon15,
18
+ poseidon16
19
+ } from "poseidon-lite";
20
+ var POSEIDON_FNS = [
21
+ void 0,
22
+ poseidon1,
23
+ poseidon2,
24
+ poseidon3,
25
+ poseidon4,
26
+ poseidon5,
27
+ poseidon6,
28
+ poseidon7,
29
+ poseidon8,
30
+ poseidon9,
31
+ poseidon10,
32
+ poseidon11,
33
+ poseidon12,
34
+ poseidon13,
35
+ poseidon14,
36
+ poseidon15,
37
+ poseidon16
38
+ ];
39
+ function poseidon(inputs) {
40
+ const len = inputs.length;
41
+ if (len < 1 || len > 16) {
42
+ throw new Error(`Poseidon supports 1-16 inputs, got ${len}`);
43
+ }
44
+ return POSEIDON_FNS[len](inputs);
45
+ }
46
+
47
+ // src/crypto/eddsa.ts
48
+ async function loadEdDSA() {
49
+ const { createRequire } = await import("module");
50
+ const require2 = createRequire(import.meta.url);
51
+ return require2("@zk-kit/eddsa-poseidon/blake-2b");
52
+ }
53
+ async function eddsaPublicKey(privateKey) {
54
+ const { derivePublicKey } = await loadEdDSA();
55
+ const pk = derivePublicKey(privateKey.toString());
56
+ return [BigInt(pk[0]), BigInt(pk[1])];
57
+ }
58
+ async function eddsaSign(privateKey, message) {
59
+ const { signMessage } = await loadEdDSA();
60
+ const sig = signMessage(privateKey.toString(), message);
61
+ return {
62
+ R8: [BigInt(sig.R8[0]), BigInt(sig.R8[1])],
63
+ S: BigInt(sig.S)
64
+ };
65
+ }
66
+ async function eddsaVerify(message, signature, publicKey) {
67
+ const { verifySignature } = await loadEdDSA();
68
+ return verifySignature(message, signature, publicKey);
69
+ }
70
+
71
+ // src/crypto/ed25519.ts
72
+ import { ed25519 } from "@noble/curves/ed25519.js";
73
+ function viewingPublicKey(privateKey) {
74
+ return ed25519.getPublicKey(privateKey);
75
+ }
76
+ function viewingPublicKeyFromHex(hexPrivateKey) {
77
+ const clean = hexPrivateKey.startsWith("0x") ? hexPrivateKey.slice(2) : hexPrivateKey;
78
+ if (clean.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(clean)) {
79
+ throw new Error(`invalid hex private key: must be 64 hex chars`);
80
+ }
81
+ const bytes = new Uint8Array(32);
82
+ for (let i = 0; i < 32; i++) {
83
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
84
+ }
85
+ return viewingPublicKey(bytes);
86
+ }
87
+
88
+ // src/crypto/field.ts
89
+ var P = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
90
+ function mod(n) {
91
+ return (n % P + P) % P;
92
+ }
93
+ function fromDecimal(s) {
94
+ return mod(BigInt(s));
95
+ }
96
+ function toBytesBE(n) {
97
+ const hex = n.toString(16).padStart(64, "0");
98
+ const bytes = new Uint8Array(32);
99
+ for (let i = 0; i < 32; i++) {
100
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
101
+ }
102
+ return bytes;
103
+ }
104
+ function fromBytesBE(bytes) {
105
+ const n = bytesToBigInt(bytes);
106
+ if (n >= P) {
107
+ throw new Error("value >= BN254 field order");
108
+ }
109
+ return n;
110
+ }
111
+ function fromHex(hex) {
112
+ const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
113
+ const n = BigInt("0x" + clean);
114
+ if (n >= P) {
115
+ throw new Error("value >= BN254 field order");
116
+ }
117
+ return n;
118
+ }
119
+ function toDecimal(n) {
120
+ return n.toString();
121
+ }
122
+ function bytesToBigInt(bytes) {
123
+ let n = 0n;
124
+ for (const b of bytes) {
125
+ n = n << 8n | BigInt(b);
126
+ }
127
+ return n;
128
+ }
129
+
130
+ // src/keys/derive.ts
131
+ import { hmac } from "@noble/hashes/hmac.js";
132
+ import { sha512 } from "@noble/hashes/sha2.js";
133
+
134
+ // src/keys/address.ts
135
+ import { bech32m } from "@scure/base";
136
+ var HRP = "unlink";
137
+ var VERSION = 0;
138
+ function encodeAddress(mpk, viewingPubKey) {
139
+ if (viewingPubKey.length !== 32) {
140
+ throw new Error(
141
+ `viewingPubKey must be 32 bytes, got ${viewingPubKey.length}`
142
+ );
143
+ }
144
+ const payload = new Uint8Array(65);
145
+ payload[0] = VERSION;
146
+ payload.set(toBytesBE(mpk), 1);
147
+ payload.set(viewingPubKey, 33);
148
+ const words = bech32m.toWords(payload);
149
+ return bech32m.encode(HRP, words, 1023);
150
+ }
151
+ function decodeAddress(address) {
152
+ const { prefix, words } = bech32m.decode(
153
+ address,
154
+ 1023
155
+ );
156
+ if (prefix !== HRP)
157
+ throw new Error(`Invalid HRP: expected "${HRP}", got "${prefix}"`);
158
+ const payload = bech32m.fromWords(words);
159
+ if (payload.length !== 65)
160
+ throw new Error(
161
+ `Invalid payload length: expected 65, got ${payload.length}`
162
+ );
163
+ if (payload[0] !== VERSION) {
164
+ throw new Error(
165
+ `unsupported address version: expected ${VERSION}, got ${payload[0]}`
166
+ );
167
+ }
168
+ const mpkBytes = new Uint8Array(payload.slice(1, 33));
169
+ let mpk = 0n;
170
+ for (const b of mpkBytes) {
171
+ mpk = mpk << 8n | BigInt(b);
172
+ }
173
+ if (mpk >= P) {
174
+ throw new Error(
175
+ "non-canonical masterPublicKey: value >= BN254 field order"
176
+ );
177
+ }
178
+ return {
179
+ version: payload[0],
180
+ masterPublicKey: mpk,
181
+ viewingPublicKey: new Uint8Array(payload.slice(33, 65))
182
+ };
183
+ }
184
+
185
+ // src/keys/derive.ts
186
+ var SPENDING_PATH = (index) => [44, 1984, 0, 0, index];
187
+ var VIEWING_PATH = (index) => [420, 1984, 0, 0, index];
188
+ function slip10Master(seed) {
189
+ const I = hmac(sha512, new TextEncoder().encode("ed25519 seed"), seed);
190
+ return { key: I.slice(0, 32), chainCode: I.slice(32) };
191
+ }
192
+ function slip10DeriveHardened(parentKey, parentChainCode, index) {
193
+ const data = new Uint8Array(37);
194
+ data[0] = 0;
195
+ data.set(parentKey, 1);
196
+ new DataView(data.buffer, data.byteOffset, data.byteLength).setUint32(
197
+ 33,
198
+ (index | 2147483648) >>> 0
199
+ );
200
+ const I = hmac(sha512, parentChainCode, data);
201
+ return { key: I.slice(0, 32), chainCode: I.slice(32) };
202
+ }
203
+ function slip10DerivePath(seed, path) {
204
+ let { key, chainCode } = slip10Master(seed);
205
+ for (const index of path) {
206
+ ({ key, chainCode } = slip10DeriveHardened(key, chainCode, index));
207
+ }
208
+ return key;
209
+ }
210
+ async function buildAccountKeys(spendingPrivateKey, viewingPrivateKey) {
211
+ const spendingPublicKey = await eddsaPublicKey(spendingPrivateKey);
212
+ const viewingPubKey = viewingPublicKey(viewingPrivateKey);
213
+ const vpkScalar = mod(bytesToBigInt(viewingPrivateKey));
214
+ const nullifyingKey = poseidon([vpkScalar]);
215
+ const masterPublicKey = poseidon([
216
+ spendingPublicKey[0],
217
+ spendingPublicKey[1],
218
+ nullifyingKey
219
+ ]);
220
+ const address = encodeAddress(masterPublicKey, viewingPubKey);
221
+ return {
222
+ spendingPrivateKey,
223
+ spendingPublicKey,
224
+ viewingPrivateKey,
225
+ viewingPublicKey: viewingPubKey,
226
+ nullifyingKey,
227
+ masterPublicKey,
228
+ address
229
+ };
230
+ }
231
+ async function deriveAccountKeys(seed, index = 0) {
232
+ if (!Number.isInteger(index) || index < 0 || index >= 2 ** 31) {
233
+ throw new Error(
234
+ `index must be a non-negative integer < 2^31, got ${index}`
235
+ );
236
+ }
237
+ const spendingRaw = slip10DerivePath(seed, SPENDING_PATH(index));
238
+ const spendingPrivateKey = mod(bytesToBigInt(spendingRaw));
239
+ const viewingPrivateKey = slip10DerivePath(seed, VIEWING_PATH(index));
240
+ return buildAccountKeys(spendingPrivateKey, viewingPrivateKey);
241
+ }
242
+
243
+ // src/account-provider.ts
244
+ import { mnemonicToSeedSync } from "@scure/bip39";
245
+
246
+ // src/keys/account-export.ts
247
+ var HEX_64_RE = /^0x[0-9a-fA-F]{64}$/;
248
+ function bytesToHex(bytes) {
249
+ let hex = "0x";
250
+ for (const b of bytes) {
251
+ hex += b.toString(16).padStart(2, "0");
252
+ }
253
+ return hex;
254
+ }
255
+ function hexToBytes(hex) {
256
+ const clean = hex.slice(2);
257
+ const bytes = new Uint8Array(clean.length / 2);
258
+ for (let i = 0; i < bytes.length; i++) {
259
+ bytes[i] = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
260
+ }
261
+ return bytes;
262
+ }
263
+ function exportAccountKeys(keys) {
264
+ if (typeof keys !== "object" || keys === null) {
265
+ throw new Error("keys must be a non-null AccountKeys object");
266
+ }
267
+ if (typeof keys.spendingPrivateKey !== "bigint") {
268
+ throw new Error("keys.spendingPrivateKey must be a bigint");
269
+ }
270
+ if (keys.spendingPrivateKey < 0n || keys.spendingPrivateKey >= P) {
271
+ throw new Error(
272
+ "keys.spendingPrivateKey out of range [0, BN254 field order)"
273
+ );
274
+ }
275
+ if (!(keys.viewingPrivateKey instanceof Uint8Array) || keys.viewingPrivateKey.length !== 32) {
276
+ throw new Error("keys.viewingPrivateKey must be a 32-byte Uint8Array");
277
+ }
278
+ return {
279
+ version: 1,
280
+ spendingPrivateKey: "0x" + keys.spendingPrivateKey.toString(16).padStart(64, "0"),
281
+ viewingPrivateKey: bytesToHex(keys.viewingPrivateKey)
282
+ };
283
+ }
284
+ async function importAccountKeys(input) {
285
+ if (typeof input !== "object" || input === null) {
286
+ throw new Error("account export payload must be a non-null object");
287
+ }
288
+ const obj = input;
289
+ if (obj.version !== 1) {
290
+ throw new Error(
291
+ `unsupported account export version: expected 1 (number), got ${JSON.stringify(obj.version)}`
292
+ );
293
+ }
294
+ if (typeof obj.spendingPrivateKey !== "string" || !HEX_64_RE.test(obj.spendingPrivateKey)) {
295
+ throw new Error(
296
+ "spendingPrivateKey must be a 0x-prefixed 64-character hex string"
297
+ );
298
+ }
299
+ if (typeof obj.viewingPrivateKey !== "string" || !HEX_64_RE.test(obj.viewingPrivateKey)) {
300
+ throw new Error(
301
+ "viewingPrivateKey must be a 0x-prefixed 64-character hex string"
302
+ );
303
+ }
304
+ const spendingPrivateKey = fromHex(obj.spendingPrivateKey);
305
+ const viewingPrivateKey = hexToBytes(obj.viewingPrivateKey);
306
+ return buildAccountKeys(spendingPrivateKey, viewingPrivateKey);
307
+ }
308
+
309
+ // src/account-provider.ts
310
+ var DEFAULT_ACCOUNT_INDEX = 0;
311
+ var StaticAccountProvider = class {
312
+ constructor(keys) {
313
+ this.keys = keys;
314
+ }
315
+ async getAccountKeys() {
316
+ return this.keys;
317
+ }
318
+ };
319
+ var DerivedAccountProvider = class {
320
+ constructor(options) {
321
+ this.options = options;
322
+ }
323
+ keysPromise;
324
+ async getAccountKeys() {
325
+ if (!this.keysPromise) {
326
+ this.keysPromise = deriveAccountKeys(
327
+ this.options.seed,
328
+ this.options.accountIndex ?? DEFAULT_ACCOUNT_INDEX
329
+ ).catch((error) => {
330
+ this.keysPromise = void 0;
331
+ throw error;
332
+ });
333
+ }
334
+ return this.keysPromise;
335
+ }
336
+ };
337
+ var unlinkAccount = {
338
+ fromKeys(keys) {
339
+ return new StaticAccountProvider(keys);
340
+ },
341
+ fromSeed(options) {
342
+ return new DerivedAccountProvider(options);
343
+ },
344
+ fromMnemonic(options) {
345
+ return unlinkAccount.fromSeed({
346
+ seed: mnemonicToSeedSync(options.mnemonic),
347
+ accountIndex: options.accountIndex
348
+ });
349
+ },
350
+ export(keys) {
351
+ return exportAccountKeys(keys);
352
+ },
353
+ async import(input) {
354
+ return importAccountKeys(input);
355
+ }
356
+ };
357
+
358
+ // src/client.ts
359
+ import createClient from "openapi-fetch";
360
+ var createUnlinkClient = (baseUrl, apiKey, options) => {
361
+ const opts = typeof options === "function" ? { customFetch: options } : options ?? {};
362
+ const maxRetries = opts.maxRetries ?? 3;
363
+ const baseFetch = opts.customFetch ?? globalThis.fetch;
364
+ const fetchFn = maxRetries > 0 ? createRetryFetch(baseFetch, maxRetries) : baseFetch;
365
+ return createClient({
366
+ baseUrl,
367
+ headers: { Authorization: `Bearer ${apiKey}` },
368
+ fetch: fetchFn
369
+ });
370
+ };
371
+ function createRetryFetch(baseFetch, maxRetries) {
372
+ return async (input, init) => {
373
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
374
+ const response = await baseFetch(input, init);
375
+ if (response.status !== 429 || attempt === maxRetries) {
376
+ return response;
377
+ }
378
+ await response.body?.cancel();
379
+ const retryAfterHeader = response.headers.get("retry-after");
380
+ const retryAfterSecs = retryAfterHeader ? parseInt(retryAfterHeader, 10) || 1 : 1;
381
+ const baseDelay = attempt === 0 ? retryAfterSecs * 1e3 : Math.min(1e3 * 2 ** attempt, 3e4);
382
+ const delayMs = baseDelay * (1 + Math.random() * 0.1);
383
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
384
+ }
385
+ throw new Error("unreachable: retry loop exited without returning");
386
+ };
387
+ }
388
+
389
+ // src/errors.ts
390
+ var UnlinkApiError = class _UnlinkApiError extends Error {
391
+ operation;
392
+ code;
393
+ detail;
394
+ constructor(operation, error) {
395
+ const errObj = typeof error === "object" && error !== null && "error" in error && typeof error.error === "object" && error.error !== null ? error.error : null;
396
+ const code = typeof errObj?.code === "string" ? errObj.code : "UNKNOWN";
397
+ const detail = typeof errObj?.message === "string" ? errObj.message : "Unknown error";
398
+ super(`${operation} failed: ${detail}`);
399
+ this.name = "UnlinkApiError";
400
+ this.operation = operation;
401
+ this.code = code;
402
+ this.detail = detail;
403
+ Object.setPrototypeOf(this, _UnlinkApiError.prototype);
404
+ }
405
+ };
406
+ var UnlinkCapabilityError = class _UnlinkCapabilityError extends Error {
407
+ constructor(message) {
408
+ super(message);
409
+ this.name = "UnlinkCapabilityError";
410
+ Object.setPrototypeOf(this, _UnlinkCapabilityError.prototype);
411
+ }
412
+ };
413
+
414
+ // src/approval.ts
415
+ var APPROVE_SELECTOR = "095ea7b3";
416
+ var MAX_UINT256 = (1n << 256n) - 1n;
417
+ async function getApprovalState(params) {
418
+ const allowanceValue = await params.evm.getErc20Allowance({
419
+ token: params.token,
420
+ owner: params.owner,
421
+ spender: params.spender
422
+ });
423
+ const allowance = normalizeUint256(allowanceValue);
424
+ const amount = normalizeUint256(params.amount);
425
+ return {
426
+ token: params.token,
427
+ owner: params.owner,
428
+ spender: params.spender,
429
+ amount,
430
+ allowance,
431
+ isApproved: BigInt(allowance) >= BigInt(amount)
432
+ };
433
+ }
434
+ function buildApprovalTx(params) {
435
+ return {
436
+ to: params.token,
437
+ data: `0x${APPROVE_SELECTOR}${encodeAddressWord(params.spender)}${encodeUint256Word(MAX_UINT256)}`,
438
+ value: 0n
439
+ };
440
+ }
441
+ async function ensureErc20Approval(params) {
442
+ const state = await getApprovalState(params);
443
+ if (state.isApproved) {
444
+ return {
445
+ status: "already-approved",
446
+ state
447
+ };
448
+ }
449
+ if (!params.evm.sendTransaction) {
450
+ throw new UnlinkCapabilityError(
451
+ "ensureErc20Approval requires an UnlinkEvmProvider with sendTransaction()"
452
+ );
453
+ }
454
+ const transaction = buildApprovalTx(params);
455
+ const txHash = await params.evm.sendTransaction(transaction);
456
+ return {
457
+ status: "submitted",
458
+ state,
459
+ txHash,
460
+ transaction
461
+ };
462
+ }
463
+ function encodeAddressWord(address) {
464
+ return stripHexPrefix(address).padStart(64, "0");
465
+ }
466
+ function encodeUint256Word(value) {
467
+ return toBigInt(value).toString(16).padStart(64, "0");
468
+ }
469
+ function stripHexPrefix(value) {
470
+ return value.startsWith("0x") ? value.slice(2) : value;
471
+ }
472
+ function normalizeUint256(value) {
473
+ return toBigInt(value).toString();
474
+ }
475
+ function toBigInt(value) {
476
+ if (typeof value === "bigint") {
477
+ return value;
478
+ }
479
+ return BigInt(value);
480
+ }
481
+
482
+ // src/permit2-nonce-manager.ts
483
+ function findFirstUnsetBit(bitmap) {
484
+ for (let i = 0; i < 256; i++) {
485
+ if ((bitmap & 1n << BigInt(i)) === 0n) return i;
486
+ }
487
+ return null;
488
+ }
489
+ function setBit(bitmap, index) {
490
+ return bitmap | 1n << BigInt(index);
491
+ }
492
+ function clearBit(bitmap, index) {
493
+ return bitmap & ~(1n << BigInt(index));
494
+ }
495
+ function randomWordPosition() {
496
+ const bytes = crypto.getRandomValues(new Uint8Array(5));
497
+ let n = 0;
498
+ for (const b of bytes) n = n * 256 + b;
499
+ return n;
500
+ }
501
+ var Permit2NonceManager = class {
502
+ client;
503
+ cache = /* @__PURE__ */ new Map();
504
+ locks = /* @__PURE__ */ new Map();
505
+ startWordFn;
506
+ constructor(client, startWordFn) {
507
+ this.client = client;
508
+ this.startWordFn = startWordFn ?? randomWordPosition;
509
+ }
510
+ async getNextNonce(owner) {
511
+ const prev = this.locks.get(owner) ?? Promise.resolve();
512
+ let resolve;
513
+ const next = new Promise((r) => {
514
+ resolve = r;
515
+ });
516
+ this.locks.set(owner, next);
517
+ try {
518
+ await prev;
519
+ return await this.allocateNonce(owner);
520
+ } finally {
521
+ resolve();
522
+ }
523
+ }
524
+ commitNonce(_owner, _nonce) {
525
+ }
526
+ releaseNonce(owner, nonce) {
527
+ const entry = this.cache.get(owner);
528
+ if (!entry) return;
529
+ const nonceNum = Number(nonce);
530
+ const wordPos = Math.floor(nonceNum / 256);
531
+ const bitPos = nonceNum % 256;
532
+ const bitmap = entry.words.get(wordPos);
533
+ if (bitmap != null) {
534
+ entry.words.set(wordPos, clearBit(bitmap, bitPos));
535
+ }
536
+ }
537
+ invalidate(owner) {
538
+ if (owner) {
539
+ this.cache.delete(owner);
540
+ } else {
541
+ this.cache.clear();
542
+ }
543
+ }
544
+ async allocateNonce(owner) {
545
+ let entry = this.cache.get(owner);
546
+ if (!entry) {
547
+ const startWord = this.startWordFn();
548
+ entry = { words: /* @__PURE__ */ new Map(), highWaterMark: startWord };
549
+ this.cache.set(owner, entry);
550
+ await this.fetchWindow(owner, entry, startWord);
551
+ }
552
+ for (; ; ) {
553
+ const result = this.scanForUnsetBit(entry);
554
+ if (result) return result;
555
+ await this.fetchWindow(owner, entry, entry.highWaterMark);
556
+ }
557
+ }
558
+ scanForUnsetBit(entry) {
559
+ for (const [wordPos, bitmap] of entry.words) {
560
+ const bitIndex = findFirstUnsetBit(bitmap);
561
+ if (bitIndex != null) {
562
+ entry.words.set(wordPos, setBit(bitmap, bitIndex));
563
+ return String(wordPos * 256 + bitIndex);
564
+ }
565
+ entry.words.delete(wordPos);
566
+ }
567
+ return null;
568
+ }
569
+ async fetchWindow(owner, entry, startWord) {
570
+ const { data, error } = await this.client.GET(
571
+ "/info/permit2/nonces/{owner}",
572
+ {
573
+ params: {
574
+ path: { owner },
575
+ query: { start_word: startWord }
576
+ }
577
+ }
578
+ );
579
+ if (error) {
580
+ const msg = typeof error === "object" && error !== null && "error" in error ? error.error.message : "Unknown error";
581
+ throw new Error(`Permit2 nonce fetch failed: ${msg}`);
582
+ }
583
+ const words = data.data.bitmap_words;
584
+ if (words.length === 0) {
585
+ throw new Error(
586
+ "All Permit2 nonces exhausted in scanned range. Call invalidate() to re-fetch."
587
+ );
588
+ }
589
+ const previousHighWaterMark = entry.highWaterMark;
590
+ let maxWordPos = startWord - 1;
591
+ for (const word of words) {
592
+ const pos = word.word_position;
593
+ if (!entry.words.has(pos)) {
594
+ entry.words.set(pos, BigInt(word.bitmap));
595
+ }
596
+ if (pos > maxWordPos) maxWordPos = pos;
597
+ }
598
+ entry.highWaterMark = maxWordPos + 1;
599
+ if (entry.highWaterMark <= previousHighWaterMark) {
600
+ throw new Error(
601
+ `Permit2 nonce fetch failed: non-advancing cursor at word ${startWord}`
602
+ );
603
+ }
604
+ }
605
+ };
606
+
607
+ // src/queries.ts
608
+ async function getEnvironment(client) {
609
+ const { data, error } = await client.GET("/info/environment");
610
+ if (error) throw new UnlinkApiError("getEnvironment", error);
611
+ return data.data;
612
+ }
613
+ async function getTransaction(client, txId) {
614
+ const { data, error } = await client.GET("/transactions/{tx_id}", {
615
+ params: { path: { tx_id: txId } }
616
+ });
617
+ if (error) throw new UnlinkApiError("getTransaction", error);
618
+ return data.data;
619
+ }
620
+ async function getUserTransactions(client, address, options) {
621
+ const { data, error } = await client.GET("/users/{address}/transactions", {
622
+ params: {
623
+ path: { address },
624
+ query: options
625
+ }
626
+ });
627
+ if (error) throw new UnlinkApiError("getUserTransactions", error);
628
+ return data.data;
629
+ }
630
+
631
+ // src/transactions/permit2.ts
632
+ function buildPermit2TypedData(params) {
633
+ return {
634
+ domain: {
635
+ name: "Permit2",
636
+ chainId: params.chainId,
637
+ verifyingContract: params.permit2Address
638
+ },
639
+ types: {
640
+ PermitTransferFrom: [
641
+ { name: "permitted", type: "TokenPermissions" },
642
+ { name: "spender", type: "address" },
643
+ { name: "nonce", type: "uint256" },
644
+ { name: "deadline", type: "uint256" }
645
+ ],
646
+ TokenPermissions: [
647
+ { name: "token", type: "address" },
648
+ { name: "amount", type: "uint256" }
649
+ ]
650
+ },
651
+ primaryType: "PermitTransferFrom",
652
+ value: {
653
+ permitted: { token: params.token, amount: params.amount },
654
+ spender: params.spender,
655
+ nonce: params.nonce,
656
+ deadline: params.deadline
657
+ }
658
+ };
659
+ }
660
+ function buildPermit2WitnessTypedData(params) {
661
+ return {
662
+ domain: {
663
+ name: "Permit2",
664
+ chainId: params.chainId,
665
+ verifyingContract: params.permit2Address
666
+ },
667
+ types: {
668
+ PermitWitnessTransferFrom: [
669
+ { name: "permitted", type: "TokenPermissions" },
670
+ { name: "spender", type: "address" },
671
+ { name: "nonce", type: "uint256" },
672
+ { name: "deadline", type: "uint256" },
673
+ { name: "witness", type: "DepositWitness" }
674
+ ],
675
+ TokenPermissions: [
676
+ { name: "token", type: "address" },
677
+ { name: "amount", type: "uint256" }
678
+ ],
679
+ DepositWitness: [{ name: "notesHash", type: "bytes32" }]
680
+ },
681
+ primaryType: "PermitWitnessTransferFrom",
682
+ value: {
683
+ permitted: { token: params.token, amount: params.amount },
684
+ spender: params.spender,
685
+ nonce: params.nonce,
686
+ deadline: params.deadline,
687
+ witness: { notesHash: params.notesHash }
688
+ }
689
+ };
690
+ }
691
+ async function getPermit2Nonce(client, owner) {
692
+ let startWord = 0;
693
+ for (; ; ) {
694
+ const { data, error } = await client.GET("/info/permit2/nonces/{owner}", {
695
+ params: { path: { owner }, query: { start_word: startWord } }
696
+ });
697
+ if (error) {
698
+ throw new UnlinkApiError("permit2.getNonce", error);
699
+ }
700
+ if (data.data.next_available_nonce != null) {
701
+ return String(data.data.next_available_nonce);
702
+ }
703
+ if (data.data.next_word_position == null || data.data.next_word_position <= startWord) {
704
+ throw new Error(
705
+ "No available Permit2 nonce in scanned window. Try Permit2NonceManager for pagination."
706
+ );
707
+ }
708
+ startWord = data.data.next_word_position;
709
+ }
710
+ }
711
+
712
+ // src/transactions/deposit.ts
713
+ async function deposit(client, params) {
714
+ const autoNonce = params.nonce == null;
715
+ if (autoNonce && !params.nonceManager) {
716
+ throw new Error("Either nonce or nonceManager must be provided");
717
+ }
718
+ const nonce = autoNonce ? await params.nonceManager.getNextNonce(params.evmAddress) : params.nonce;
719
+ const prepareResp = await client.POST("/transactions/deposit/prepare", {
720
+ body: {
721
+ unlink_address: params.unlinkAddress,
722
+ evm_address: params.evmAddress,
723
+ token: params.token,
724
+ amount: params.amount,
725
+ environment: params.environment
726
+ }
727
+ });
728
+ if (prepareResp.error) {
729
+ if (autoNonce) params.nonceManager.releaseNonce(params.evmAddress, nonce);
730
+ throw new UnlinkApiError("deposit.prepare", prepareResp.error);
731
+ }
732
+ const { tx_id: txId, notes_hash: notesHash } = prepareResp.data.data;
733
+ const typedData = buildPermit2WitnessTypedData({
734
+ token: params.token,
735
+ amount: params.amount,
736
+ spender: params.poolAddress,
737
+ nonce,
738
+ deadline: String(params.deadline),
739
+ chainId: params.chainId,
740
+ permit2Address: params.permit2Address,
741
+ notesHash
742
+ });
743
+ let signature;
744
+ try {
745
+ signature = await params.signTypedData(typedData);
746
+ } catch (err) {
747
+ if (autoNonce) params.nonceManager.releaseNonce(params.evmAddress, nonce);
748
+ throw err;
749
+ }
750
+ const submitResp = await client.POST("/transactions/deposit/{tx_id}/submit", {
751
+ params: { path: { tx_id: txId } },
752
+ body: {
753
+ permit2_signature: signature,
754
+ permit2_nonce: nonce,
755
+ permit2_deadline: params.deadline
756
+ }
757
+ });
758
+ if (submitResp.error) {
759
+ if (autoNonce) params.nonceManager.releaseNonce(params.evmAddress, nonce);
760
+ throw new UnlinkApiError("deposit.submit", submitResp.error);
761
+ }
762
+ return {
763
+ txId: submitResp.data.data.tx_id,
764
+ status: submitResp.data.data.status
765
+ };
766
+ }
767
+
768
+ // src/transactions/sign-submit.ts
769
+ async function signAndSubmit(client, params) {
770
+ const signature = await eddsaSign(
771
+ params.spendingPrivateKey,
772
+ BigInt(params.messageHash)
773
+ );
774
+ const { data: submitData, error: submitError } = await client.POST(
775
+ "/transactions/{tx_id}/submit",
776
+ {
777
+ params: { path: { tx_id: params.txId } },
778
+ body: {
779
+ signature: [
780
+ signature.R8[0].toString(),
781
+ signature.R8[1].toString(),
782
+ signature.S.toString()
783
+ ]
784
+ }
785
+ }
786
+ );
787
+ if (submitError) {
788
+ throw new UnlinkApiError(
789
+ `${params.operationLabel.toLowerCase()}.submit`,
790
+ submitError
791
+ );
792
+ }
793
+ return { txId: submitData.data.tx_id, status: submitData.data.status };
794
+ }
795
+
796
+ // src/transactions/transfer.ts
797
+ async function transfer(client, params) {
798
+ const { data: prepareData, error: prepareError } = await client.POST(
799
+ "/transactions/prepare/transfer",
800
+ {
801
+ body: {
802
+ unlink_address: params.senderKeys.address,
803
+ transfers: params.transfers.map((t) => ({
804
+ unlink_address: t.recipientAddress,
805
+ token: t.token,
806
+ amount: t.amount
807
+ })),
808
+ environment: params.environment
809
+ }
810
+ }
811
+ );
812
+ if (prepareError) {
813
+ throw new UnlinkApiError("transfer.prepare", prepareError);
814
+ }
815
+ const { tx_id, signing_request } = prepareData.data;
816
+ return signAndSubmit(client, {
817
+ txId: tx_id,
818
+ messageHash: signing_request.message_hash,
819
+ spendingPrivateKey: params.senderKeys.spendingPrivateKey,
820
+ operationLabel: "Transfer"
821
+ });
822
+ }
823
+
824
+ // src/transactions/withdraw.ts
825
+ async function withdraw(client, params) {
826
+ const { data: prepareData, error: prepareError } = await client.POST(
827
+ "/transactions/prepare/withdraw",
828
+ {
829
+ body: {
830
+ unlink_address: params.senderKeys.address,
831
+ evm_address: params.recipientEvmAddress,
832
+ token: params.token,
833
+ amount: params.amount,
834
+ environment: params.environment
835
+ }
836
+ }
837
+ );
838
+ if (prepareError) {
839
+ throw new UnlinkApiError("withdraw.prepare", prepareError);
840
+ }
841
+ const { tx_id, signing_request } = prepareData.data;
842
+ return signAndSubmit(client, {
843
+ txId: tx_id,
844
+ messageHash: signing_request.message_hash,
845
+ spendingPrivateKey: params.senderKeys.spendingPrivateKey,
846
+ operationLabel: "Withdraw"
847
+ });
848
+ }
849
+
850
+ // src/users.ts
851
+ async function createUser(client, keys) {
852
+ const { data, error } = await client.POST("/users", {
853
+ body: {
854
+ public_key: [
855
+ keys.spendingPublicKey[0].toString(),
856
+ keys.spendingPublicKey[1].toString()
857
+ ],
858
+ viewing_private_key: Array.from(
859
+ keys.viewingPrivateKey,
860
+ (b) => b.toString(16).padStart(2, "0")
861
+ ).join(""),
862
+ nullifying_key: keys.nullifyingKey.toString()
863
+ }
864
+ });
865
+ if (error) throw new UnlinkApiError("createUser", error);
866
+ return data.data;
867
+ }
868
+ async function getUser(client, address) {
869
+ const { data, error } = await client.GET("/users/{address}", {
870
+ params: { path: { address } }
871
+ });
872
+ if (error) throw new UnlinkApiError("getUser", error);
873
+ return data.data;
874
+ }
875
+
876
+ // src/unlink.ts
877
+ var DEFAULT_DEPOSIT_DEADLINE_SECONDS = 60 * 60;
878
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
879
+ var DEFAULT_POLL_TIMEOUT_MS = 6e4;
880
+ var TERMINAL_TRANSACTION_STATUSES = /* @__PURE__ */ new Set([
881
+ "relayed",
882
+ "processed",
883
+ "failed"
884
+ ]);
885
+ var REGISTERED_USER_ERROR_CODES = /* @__PURE__ */ new Set(["ALREADY_EXISTS", "CONFLICT"]);
886
+ var TenantUnlinkClient = class {
887
+ client;
888
+ account;
889
+ evm;
890
+ nonceManager;
891
+ environmentInfo;
892
+ environmentInfoPromise;
893
+ accountKeysPromise;
894
+ isRegistered = false;
895
+ registerPromise;
896
+ constructor(options) {
897
+ this.client = createUnlinkClient(
898
+ options.engineUrl,
899
+ options.apiKey,
900
+ options.customFetch
901
+ );
902
+ this.account = options.account;
903
+ this.evm = options.evm;
904
+ this.nonceManager = new Permit2NonceManager(this.client);
905
+ }
906
+ async getAddress() {
907
+ const keys = await this.getAccountKeys();
908
+ return keys.address;
909
+ }
910
+ async getPublicKey() {
911
+ const keys = await this.getAccountKeys();
912
+ return keys.spendingPublicKey;
913
+ }
914
+ async ensureRegistered() {
915
+ if (this.isRegistered) return;
916
+ if (!this.registerPromise) {
917
+ this.registerPromise = this.getAccountKeys().then((keys) => registerUser(this.client, keys)).then(() => {
918
+ this.isRegistered = true;
919
+ }).finally(() => {
920
+ this.registerPromise = void 0;
921
+ });
922
+ }
923
+ await this.registerPromise;
924
+ }
925
+ async deposit(params) {
926
+ const evm = this.resolveEvmProvider(params.evm);
927
+ await this.ensureRegistered();
928
+ const [keys, environmentInfo, evmAddress] = await Promise.all([
929
+ this.getAccountKeys(),
930
+ this.getEnvironmentInfo(),
931
+ evm.getAddress()
932
+ ]);
933
+ return deposit(this.client, {
934
+ unlinkAddress: keys.address,
935
+ evmAddress,
936
+ token: params.token,
937
+ amount: params.amount,
938
+ environment: environmentInfo.name,
939
+ nonce: params.nonce,
940
+ deadline: params.deadline ?? getDefaultPermitDeadline(),
941
+ chainId: environmentInfo.chain_id,
942
+ permit2Address: environmentInfo.permit2_address,
943
+ poolAddress: environmentInfo.pool_address,
944
+ signTypedData: (typedData) => evm.signTypedData(typedData),
945
+ nonceManager: this.nonceManager
946
+ });
947
+ }
948
+ async transfer(params) {
949
+ const transfers = normalizeTransfers(params);
950
+ await this.ensureRegistered();
951
+ const [keys, environmentInfo] = await Promise.all([
952
+ this.getAccountKeys(),
953
+ this.getEnvironmentInfo()
954
+ ]);
955
+ return transfer(this.client, {
956
+ senderKeys: keys,
957
+ transfers,
958
+ environment: environmentInfo.name
959
+ });
960
+ }
961
+ async withdraw(params) {
962
+ await this.ensureRegistered();
963
+ const [keys, environmentInfo] = await Promise.all([
964
+ this.getAccountKeys(),
965
+ this.getEnvironmentInfo()
966
+ ]);
967
+ return withdraw(this.client, {
968
+ senderKeys: keys,
969
+ recipientEvmAddress: params.recipientEvmAddress,
970
+ token: params.token,
971
+ amount: params.amount,
972
+ environment: environmentInfo.name
973
+ });
974
+ }
975
+ async getBalances(params = {}) {
976
+ const address = await this.getAddress();
977
+ const { data, error } = await this.client.GET("/users/{address}/balances", {
978
+ params: {
979
+ path: { address },
980
+ query: params.token ? { token: params.token } : {}
981
+ }
982
+ });
983
+ if (error) throw new UnlinkApiError("getUserBalances", error);
984
+ return data.data;
985
+ }
986
+ async getTransactions(params = {}) {
987
+ const address = await this.getAddress();
988
+ return getUserTransactions(this.client, address, params);
989
+ }
990
+ async pollTransactionStatus(txId, options = {}) {
991
+ const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
992
+ const timeoutMs = options.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
993
+ const deadlineAt = Date.now() + timeoutMs;
994
+ for (; ; ) {
995
+ const transaction = await getTransaction(this.client, txId);
996
+ if (TERMINAL_TRANSACTION_STATUSES.has(transaction.status)) {
997
+ return {
998
+ txId: transaction.id,
999
+ status: transaction.status
1000
+ };
1001
+ }
1002
+ if (Date.now() >= deadlineAt) {
1003
+ throw new Error(
1004
+ `Timed out waiting for transaction ${txId} after ${timeoutMs}ms`
1005
+ );
1006
+ }
1007
+ await sleep(intervalMs);
1008
+ }
1009
+ }
1010
+ async getApprovalState(params) {
1011
+ const evm = this.resolveEvmProvider(params.evm);
1012
+ if (!evm.getErc20Allowance) {
1013
+ throw new UnlinkCapabilityError(
1014
+ "getApprovalState requires an UnlinkEvmProvider with getErc20Allowance()"
1015
+ );
1016
+ }
1017
+ const [environmentInfo, owner] = await Promise.all([
1018
+ this.getEnvironmentInfo(),
1019
+ evm.getAddress()
1020
+ ]);
1021
+ return getApprovalState({
1022
+ evm: { getErc20Allowance: evm.getErc20Allowance },
1023
+ token: params.token,
1024
+ owner,
1025
+ spender: environmentInfo.permit2_address,
1026
+ amount: params.amount
1027
+ });
1028
+ }
1029
+ async buildApprovalTx(params) {
1030
+ const environmentInfo = await this.getEnvironmentInfo();
1031
+ return buildApprovalTx({
1032
+ token: params.token,
1033
+ spender: environmentInfo.permit2_address,
1034
+ amount: params.amount
1035
+ });
1036
+ }
1037
+ async ensureErc20Approval(params) {
1038
+ const evm = this.resolveEvmProvider(params.evm);
1039
+ if (!evm.getErc20Allowance) {
1040
+ throw new UnlinkCapabilityError(
1041
+ "ensureErc20Approval requires an UnlinkEvmProvider with getErc20Allowance()"
1042
+ );
1043
+ }
1044
+ const [environmentInfo, owner] = await Promise.all([
1045
+ this.getEnvironmentInfo(),
1046
+ evm.getAddress()
1047
+ ]);
1048
+ return ensureErc20Approval({
1049
+ evm: {
1050
+ getErc20Allowance: evm.getErc20Allowance,
1051
+ ...evm.sendTransaction && { sendTransaction: evm.sendTransaction }
1052
+ },
1053
+ token: params.token,
1054
+ owner,
1055
+ spender: environmentInfo.permit2_address,
1056
+ amount: params.amount
1057
+ });
1058
+ }
1059
+ resolveEvmProvider(override) {
1060
+ const evm = override ?? this.evm;
1061
+ if (!evm) {
1062
+ throw new UnlinkCapabilityError(
1063
+ "This operation requires an UnlinkEvmProvider"
1064
+ );
1065
+ }
1066
+ return evm;
1067
+ }
1068
+ async getAccountKeys() {
1069
+ if (!this.accountKeysPromise) {
1070
+ this.accountKeysPromise = this.account.getAccountKeys().catch((error) => {
1071
+ this.accountKeysPromise = void 0;
1072
+ throw error;
1073
+ });
1074
+ }
1075
+ return this.accountKeysPromise;
1076
+ }
1077
+ async getEnvironmentInfo() {
1078
+ if (this.environmentInfo) return this.environmentInfo;
1079
+ if (!this.environmentInfoPromise) {
1080
+ this.environmentInfoPromise = getEnvironment(this.client).then((environmentInfo) => {
1081
+ this.environmentInfo = environmentInfo;
1082
+ return environmentInfo;
1083
+ }).finally(() => {
1084
+ this.environmentInfoPromise = void 0;
1085
+ });
1086
+ }
1087
+ return this.environmentInfoPromise;
1088
+ }
1089
+ };
1090
+ function createUnlink(options) {
1091
+ return new TenantUnlinkClient(options);
1092
+ }
1093
+ function normalizeTransfers(params) {
1094
+ const hasTransfers = isMultiTransferParams(params);
1095
+ const hasSingleRecipient = isSingleTransferParams(params);
1096
+ if (hasTransfers && hasSingleRecipient) {
1097
+ throw new Error(
1098
+ "transfer accepts either transfers or recipientAddress/amount, not both"
1099
+ );
1100
+ }
1101
+ let transfers;
1102
+ if (hasTransfers) {
1103
+ transfers = params.transfers;
1104
+ } else {
1105
+ if (!hasSingleRecipient) {
1106
+ throw new Error("transfer requires recipientAddress and amount");
1107
+ }
1108
+ transfers = [
1109
+ {
1110
+ recipientAddress: params.recipientAddress,
1111
+ amount: params.amount
1112
+ }
1113
+ ];
1114
+ }
1115
+ if (transfers.length === 0) {
1116
+ throw new Error("transfer requires at least one recipient");
1117
+ }
1118
+ return transfers.map((transferInstruction) => ({
1119
+ recipientAddress: transferInstruction.recipientAddress,
1120
+ token: params.token,
1121
+ amount: transferInstruction.amount
1122
+ }));
1123
+ }
1124
+ function isMultiTransferParams(params) {
1125
+ return Array.isArray(params.transfers);
1126
+ }
1127
+ function isSingleTransferParams(params) {
1128
+ return typeof params.recipientAddress === "string" && typeof params.amount === "string";
1129
+ }
1130
+ async function registerUser(client, keys) {
1131
+ try {
1132
+ await createUser(client, keys);
1133
+ } catch (error) {
1134
+ if (error instanceof UnlinkApiError && REGISTERED_USER_ERROR_CODES.has(error.code)) {
1135
+ return;
1136
+ }
1137
+ throw error;
1138
+ }
1139
+ }
1140
+ function getDefaultPermitDeadline() {
1141
+ return Math.floor(Date.now() / 1e3) + DEFAULT_DEPOSIT_DEADLINE_SECONDS;
1142
+ }
1143
+ function sleep(ms) {
1144
+ return new Promise((resolve) => {
1145
+ setTimeout(resolve, ms);
1146
+ });
1147
+ }
1148
+
1149
+ // src/evm-provider.ts
1150
+ var ERC20_ALLOWANCE_ABI = [
1151
+ {
1152
+ type: "function",
1153
+ name: "allowance",
1154
+ stateMutability: "view",
1155
+ inputs: [
1156
+ { name: "owner", type: "address" },
1157
+ { name: "spender", type: "address" }
1158
+ ],
1159
+ outputs: [{ name: "", type: "uint256" }]
1160
+ }
1161
+ ];
1162
+ var ALLOWANCE_SELECTOR = "dd62ed3e";
1163
+ var unlinkEvm = {
1164
+ fromSigner(options) {
1165
+ return {
1166
+ async getAddress() {
1167
+ return options.address;
1168
+ },
1169
+ signTypedData: options.signTypedData,
1170
+ ...options.getErc20Allowance && {
1171
+ getErc20Allowance: options.getErc20Allowance
1172
+ },
1173
+ ...options.sendTransaction && {
1174
+ sendTransaction: options.sendTransaction
1175
+ }
1176
+ };
1177
+ },
1178
+ fromViem(options) {
1179
+ const walletClient = options.walletClient;
1180
+ const publicClient = options.publicClient;
1181
+ const walletAccount = resolveViemAccount(
1182
+ walletClient.account,
1183
+ options.address
1184
+ );
1185
+ const walletSendTransaction = walletClient.sendTransaction?.bind(walletClient);
1186
+ return {
1187
+ async getAddress() {
1188
+ return resolveViemAddress(walletAccount);
1189
+ },
1190
+ async signTypedData(typedData) {
1191
+ return walletClient.signTypedData({
1192
+ account: walletAccount,
1193
+ domain: typedData.domain,
1194
+ types: typedData.types,
1195
+ primaryType: typedData.primaryType,
1196
+ message: typedData.value
1197
+ });
1198
+ },
1199
+ ...publicClient && {
1200
+ async getErc20Allowance(params) {
1201
+ const result = await publicClient.readContract({
1202
+ address: params.token,
1203
+ abi: ERC20_ALLOWANCE_ABI,
1204
+ functionName: "allowance",
1205
+ args: [params.owner, params.spender]
1206
+ });
1207
+ return BigInt(result);
1208
+ }
1209
+ },
1210
+ ...walletSendTransaction && {
1211
+ async sendTransaction(tx) {
1212
+ return walletSendTransaction({
1213
+ account: walletAccount,
1214
+ to: tx.to,
1215
+ data: tx.data,
1216
+ value: tx.value
1217
+ });
1218
+ }
1219
+ }
1220
+ };
1221
+ },
1222
+ fromEthers(options) {
1223
+ const signer = options.signer;
1224
+ const provider = options.provider ?? signer.provider ?? void 0;
1225
+ const signerSendTransaction = signer.sendTransaction?.bind(signer);
1226
+ return {
1227
+ async getAddress() {
1228
+ return signer.getAddress();
1229
+ },
1230
+ async signTypedData(typedData) {
1231
+ return signer.signTypedData(
1232
+ typedData.domain,
1233
+ typedData.types,
1234
+ typedData.value
1235
+ );
1236
+ },
1237
+ ...provider && {
1238
+ async getErc20Allowance(params) {
1239
+ const result = await provider.call({
1240
+ to: params.token,
1241
+ data: buildAllowanceCalldata(params.owner, params.spender)
1242
+ });
1243
+ return decodeUint256Hex(result);
1244
+ }
1245
+ },
1246
+ ...signerSendTransaction && {
1247
+ async sendTransaction(tx) {
1248
+ const response = await signerSendTransaction({
1249
+ to: tx.to,
1250
+ data: tx.data,
1251
+ value: tx.value
1252
+ });
1253
+ return response.hash;
1254
+ }
1255
+ }
1256
+ };
1257
+ }
1258
+ };
1259
+ function resolveViemAccount(account, fallback) {
1260
+ if (account !== void 0) {
1261
+ return account;
1262
+ }
1263
+ if (fallback) {
1264
+ return fallback;
1265
+ }
1266
+ throw new UnlinkCapabilityError(
1267
+ "unlinkEvm.fromViem requires walletClient.account or an explicit address"
1268
+ );
1269
+ }
1270
+ function resolveViemAddress(account) {
1271
+ if (typeof account === "string") {
1272
+ return account;
1273
+ }
1274
+ return account.address;
1275
+ }
1276
+ function buildAllowanceCalldata(owner, spender) {
1277
+ return `0x${ALLOWANCE_SELECTOR}${encodeAddressWord2(owner)}${encodeAddressWord2(spender)}`;
1278
+ }
1279
+ function decodeUint256Hex(value) {
1280
+ if (value === "0x" || value === "") {
1281
+ return 0n;
1282
+ }
1283
+ return BigInt(value);
1284
+ }
1285
+ function encodeAddressWord2(address) {
1286
+ const stripped = address.startsWith("0x") ? address.slice(2) : address;
1287
+ return stripped.padStart(64, "0");
1288
+ }
1289
+
1290
+ // src/burner.ts
1291
+ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
1292
+ var MemoryBurnerStorage = class {
1293
+ keys = /* @__PURE__ */ new Map();
1294
+ async save(address, privateKey) {
1295
+ this.keys.set(address.toLowerCase(), privateKey);
1296
+ }
1297
+ async load(address) {
1298
+ return this.keys.get(address.toLowerCase()) ?? null;
1299
+ }
1300
+ async delete(address) {
1301
+ this.keys.delete(address.toLowerCase());
1302
+ }
1303
+ };
1304
+ var BurnerWallet = class _BurnerWallet {
1305
+ address;
1306
+ account;
1307
+ storage;
1308
+ constructor(account, storage) {
1309
+ this.account = account;
1310
+ this.address = account.address;
1311
+ this.storage = storage;
1312
+ }
1313
+ /** Generate a new burner with a fresh keypair. */
1314
+ static async create(storage) {
1315
+ const store = storage ?? new MemoryBurnerStorage();
1316
+ const privateKey = generatePrivateKey();
1317
+ const account = privateKeyToAccount(privateKey);
1318
+ await store.save(account.address, privateKey);
1319
+ return new _BurnerWallet(account, store);
1320
+ }
1321
+ /** Restore a burner from storage by address. Returns null if not found. */
1322
+ static async restore(address, storage) {
1323
+ const privateKey = await storage.load(address);
1324
+ if (!privateKey) return null;
1325
+ const account = privateKeyToAccount(privateKey);
1326
+ return new _BurnerWallet(account, storage);
1327
+ }
1328
+ /** Return a viem-compatible Account for use with WalletClient. */
1329
+ toViemAccount() {
1330
+ return this.account;
1331
+ }
1332
+ /**
1333
+ * Fund this burner by withdrawing from the Unlink pool.
1334
+ *
1335
+ * Calls POST /burner/create, signs the withdrawal with EdDSA, and submits.
1336
+ * After the withdrawal confirms on-chain, the relayer sends ETH for gas.
1337
+ */
1338
+ async fundFromPool(client, params) {
1339
+ const { data, error } = await client.POST("/burner/create", {
1340
+ body: {
1341
+ unlink_address: params.senderKeys.address,
1342
+ burner_address: this.address,
1343
+ token: params.token,
1344
+ amount: params.amount,
1345
+ environment: params.environment
1346
+ }
1347
+ });
1348
+ if (error) {
1349
+ throw new UnlinkApiError("burner.create", error);
1350
+ }
1351
+ const { tx_id, signing_request } = data.data;
1352
+ return signAndSubmit(client, {
1353
+ txId: tx_id,
1354
+ messageHash: signing_request.message_hash,
1355
+ spendingPrivateKey: params.senderKeys.spendingPrivateKey,
1356
+ operationLabel: "BurnerFund"
1357
+ });
1358
+ }
1359
+ /**
1360
+ * Deposit tokens back to the Unlink pool from this burner.
1361
+ *
1362
+ * The caller must ensure the burner has approved Permit2 for the token
1363
+ * before calling this method. This uses the standard deposit flow with
1364
+ * the burner signing the Permit2 typed data.
1365
+ */
1366
+ async depositToPool(client, params) {
1367
+ const signTypedData = async (typedData) => {
1368
+ return this.account.signTypedData({
1369
+ domain: {
1370
+ ...typedData.domain,
1371
+ verifyingContract: typedData.domain.verifyingContract
1372
+ },
1373
+ types: typedData.types,
1374
+ primaryType: typedData.primaryType,
1375
+ message: typedData.value
1376
+ });
1377
+ };
1378
+ return deposit(client, {
1379
+ unlinkAddress: params.unlinkAddress,
1380
+ evmAddress: this.address,
1381
+ token: params.token,
1382
+ amount: params.amount,
1383
+ environment: params.environment,
1384
+ nonce: params.nonce,
1385
+ deadline: params.deadline,
1386
+ chainId: params.chainId,
1387
+ permit2Address: params.permit2Address,
1388
+ poolAddress: params.poolAddress,
1389
+ signTypedData,
1390
+ nonceManager: params.nonceManager
1391
+ });
1392
+ }
1393
+ /** Check the burner's lifecycle status. */
1394
+ async getStatus(client) {
1395
+ const { data, error } = await client.GET(
1396
+ "/burner/{burner_address}/status",
1397
+ { params: { path: { burner_address: this.address } } }
1398
+ );
1399
+ if (error) {
1400
+ throw new UnlinkApiError("burner.getStatus", error);
1401
+ }
1402
+ return data.data;
1403
+ }
1404
+ /** Mark this burner as disposed, optionally linking a deposit-back tx. */
1405
+ async dispose(client, depositBackTxId) {
1406
+ const { error } = await client.POST("/burner/{burner_address}/dispose", {
1407
+ params: { path: { burner_address: this.address } },
1408
+ body: { deposit_back_tx_id: depositBackTxId }
1409
+ });
1410
+ if (error) {
1411
+ throw new UnlinkApiError("burner.dispose", error);
1412
+ }
1413
+ }
1414
+ /** Delete the burner key from storage. Call after dispose. */
1415
+ async deleteKey() {
1416
+ await this.storage.delete(this.address);
1417
+ }
1418
+ /** Get chain configuration for burner operations. */
1419
+ static async getInfo(client) {
1420
+ const { data, error } = await client.GET("/burner/info");
1421
+ if (error) {
1422
+ throw new UnlinkApiError("burner.getInfo", error);
1423
+ }
1424
+ return data.data;
1425
+ }
1426
+ };
1427
+ export {
1428
+ P as BN254_FIELD_ORDER,
1429
+ BurnerWallet,
1430
+ Permit2NonceManager,
1431
+ UnlinkApiError,
1432
+ UnlinkCapabilityError,
1433
+ buildApprovalTx,
1434
+ buildPermit2TypedData,
1435
+ createUnlink,
1436
+ createUnlinkClient,
1437
+ createUser,
1438
+ decodeAddress,
1439
+ deposit,
1440
+ deriveAccountKeys,
1441
+ eddsaPublicKey,
1442
+ eddsaSign,
1443
+ eddsaVerify,
1444
+ encodeAddress,
1445
+ ensureErc20Approval,
1446
+ fromBytesBE,
1447
+ fromDecimal,
1448
+ fromHex,
1449
+ getApprovalState,
1450
+ getEnvironment,
1451
+ getPermit2Nonce,
1452
+ getTransaction,
1453
+ getUser,
1454
+ getUserTransactions,
1455
+ mod,
1456
+ poseidon,
1457
+ toBytesBE,
1458
+ toDecimal,
1459
+ transfer,
1460
+ unlinkAccount,
1461
+ unlinkEvm,
1462
+ viewingPublicKey,
1463
+ viewingPublicKeyFromHex,
1464
+ withdraw
1465
+ };