@toon-protocol/client 0.9.0 → 0.9.2

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 CHANGED
@@ -1,10 +1,18 @@
1
- import "./chunk-5WRI5ZAA.js";
1
+ import {
2
+ ANON_ASSETS,
3
+ ANON_VERSION,
4
+ selectAnonAsset,
5
+ startManagedAnonProxy
6
+ } from "./chunk-WHAEQLIW.js";
2
7
 
3
8
  // src/ToonClient.ts
4
- import { generateSecretKey as generateSecretKey2, getPublicKey } from "nostr-tools/pure";
9
+ import { generateSecretKey as generateSecretKey3, getPublicKey as getPublicKey2 } from "nostr-tools/pure";
5
10
 
6
11
  // src/config.ts
7
- import { generateSecretKey } from "nostr-tools/pure";
12
+ import { generateSecretKey as generateSecretKey2 } from "nostr-tools/pure";
13
+ import {
14
+ resolveClientNetwork
15
+ } from "@toon-protocol/core";
8
16
 
9
17
  // src/errors.ts
10
18
  var ToonClientError = class extends Error {
@@ -51,7 +59,209 @@ var PeerAlreadyExistsError = class extends ToonClientError {
51
59
  }
52
60
  };
53
61
 
62
+ // src/keys/KeyDerivation.ts
63
+ import { generateSecretKey, getPublicKey } from "nostr-tools/pure";
64
+ import { privateKeyToAccount } from "viem/accounts";
65
+ import { toHex } from "viem";
66
+ import {
67
+ generateMnemonic as _genMnemonic,
68
+ validateMnemonic as _validateMnemonic,
69
+ mnemonicToSeedSync
70
+ } from "@scure/bip39";
71
+ import { wordlist as english } from "@scure/bip39/wordlists/english";
72
+ import { HDKey } from "@scure/bip32";
73
+ import { hexToMinaBase58PrivateKey } from "@toon-protocol/core";
74
+ function generateMnemonic() {
75
+ return _genMnemonic(english, 128);
76
+ }
77
+ function validateMnemonic(mnemonic) {
78
+ return _validateMnemonic(mnemonic, english);
79
+ }
80
+ var MAX_BIP32_INDEX = 2147483647;
81
+ function assertValidAccountIndex(accountIndex) {
82
+ if (!Number.isInteger(accountIndex) || accountIndex < 0 || accountIndex > MAX_BIP32_INDEX) {
83
+ throw new Error(
84
+ `Invalid accountIndex: expected a non-negative integer (0 to ${MAX_BIP32_INDEX}), got ${String(accountIndex)}`
85
+ );
86
+ }
87
+ }
88
+ function deriveNostrKey(seed, accountIndex = 0) {
89
+ const master = HDKey.fromMasterSeed(seed);
90
+ const child = master.derive(`m/44'/1237'/0'/0/${accountIndex}`);
91
+ if (!child.privateKey) {
92
+ throw new Error("Failed to derive Nostr private key from seed");
93
+ }
94
+ const secretKey = new Uint8Array(child.privateKey);
95
+ const pubkey = getPublicKey(secretKey);
96
+ return { secretKey, pubkey };
97
+ }
98
+ function deriveEvmIdentity(secretKey) {
99
+ const account = privateKeyToAccount(toHex(secretKey));
100
+ return {
101
+ privateKey: secretKey,
102
+ address: account.address
103
+ };
104
+ }
105
+ async function deriveSolanaKey(seed, accountIndex = 0) {
106
+ const { hmac } = await import("@noble/hashes/hmac");
107
+ const { sha512 } = await import("@noble/hashes/sha512");
108
+ const { ed25519: ed255194 } = await import("@noble/curves/ed25519.js");
109
+ const encoder = new TextEncoder();
110
+ let I = hmac(sha512, encoder.encode("ed25519 seed"), seed);
111
+ let key = I.slice(0, 32);
112
+ let chainCode = I.slice(32);
113
+ const indices = [
114
+ 2147483692,
115
+ // 44'
116
+ 2147484149,
117
+ // 501'
118
+ 2147483648 + accountIndex >>> 0,
119
+ // {accountIndex}'
120
+ 2147483648
121
+ // 0'
122
+ ];
123
+ for (const index of indices) {
124
+ const data = new Uint8Array(37);
125
+ data[0] = 0;
126
+ data.set(key, 1);
127
+ data[33] = index >>> 24 & 255;
128
+ data[34] = index >>> 16 & 255;
129
+ data[35] = index >>> 8 & 255;
130
+ data[36] = index & 255;
131
+ I = hmac(sha512, chainCode, data);
132
+ key = I.slice(0, 32);
133
+ chainCode = I.slice(32);
134
+ }
135
+ const publicKeyBytes = ed255194.getPublicKey(key);
136
+ const keypair = new Uint8Array(64);
137
+ keypair.set(key, 0);
138
+ keypair.set(publicKeyBytes, 32);
139
+ const publicKey = toBase58(publicKeyBytes);
140
+ return { secretKey: keypair, publicKey };
141
+ }
142
+ async function deriveMinaKey(seed, accountIndex = 0) {
143
+ const master = HDKey.fromMasterSeed(seed);
144
+ const child = master.derive(`m/44'/12586'/${accountIndex}'/0/0`);
145
+ if (!child.privateKey) {
146
+ throw new Error("Failed to derive Mina private key from seed");
147
+ }
148
+ const keyBytes = new Uint8Array(child.privateKey);
149
+ keyBytes[0] = (keyBytes[0] ?? 0) & 63;
150
+ try {
151
+ const MinaSignerLib = await import("mina-signer");
152
+ const Client = "default" in MinaSignerLib ? MinaSignerLib.default : MinaSignerLib;
153
+ const client = new Client({ network: "mainnet" });
154
+ const hexKey = Array.from(keyBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
155
+ const minaPrivateKey = hexToMinaBase58PrivateKey(hexKey);
156
+ const publicKey = client.derivePublicKey(minaPrivateKey);
157
+ return {
158
+ // Store the clamped big-endian hex scalar; consumers (e.g. the client's
159
+ // MinaSigner) re-convert to base58check via hexToMinaBase58PrivateKey.
160
+ privateKey: hexKey,
161
+ publicKey
162
+ };
163
+ } catch {
164
+ throw new Error(
165
+ "mina-signer is required for Mina key derivation. Install it as an optional dependency."
166
+ );
167
+ }
168
+ }
169
+ function deriveNostrKeyFromMnemonic(mnemonic, accountIndex = 0) {
170
+ assertValidAccountIndex(accountIndex);
171
+ const seed = mnemonicToSeedSync(mnemonic);
172
+ const result = deriveNostrKey(seed, accountIndex);
173
+ seed.fill(0);
174
+ return result;
175
+ }
176
+ async function deriveFullIdentity(mnemonic, accountIndex = 0) {
177
+ assertValidAccountIndex(accountIndex);
178
+ const seed = mnemonicToSeedSync(mnemonic);
179
+ const nostr = deriveNostrKey(seed, accountIndex);
180
+ const evm = deriveEvmIdentity(nostr.secretKey);
181
+ let solana;
182
+ try {
183
+ solana = await deriveSolanaKey(seed, accountIndex);
184
+ } catch {
185
+ solana = { secretKey: new Uint8Array(64), publicKey: "" };
186
+ }
187
+ let mina;
188
+ try {
189
+ mina = await deriveMinaKey(seed, accountIndex);
190
+ } catch {
191
+ mina = { privateKey: "", publicKey: "" };
192
+ }
193
+ seed.fill(0);
194
+ return { nostr, evm, solana, mina };
195
+ }
196
+ function deriveFromNsec(secretKey) {
197
+ const keyCopy = new Uint8Array(secretKey);
198
+ const pubkey = getPublicKey(keyCopy);
199
+ const evm = deriveEvmIdentity(keyCopy);
200
+ return {
201
+ nostr: { secretKey: keyCopy, pubkey },
202
+ evm,
203
+ solana: { secretKey: new Uint8Array(64), publicKey: "" },
204
+ mina: { privateKey: "", publicKey: "" }
205
+ };
206
+ }
207
+ function generateRandomIdentity() {
208
+ const secretKey = generateSecretKey();
209
+ return deriveFromNsec(secretKey);
210
+ }
211
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
212
+ function toBase58(bytes) {
213
+ let num = BigInt(0);
214
+ for (const b of bytes) num = num * 256n + BigInt(b);
215
+ let result = "";
216
+ while (num > 0n) {
217
+ result = BASE58_ALPHABET[Number(num % 58n)] + result;
218
+ num = num / 58n;
219
+ }
220
+ for (const b of bytes) {
221
+ if (b === 0) result = "1" + result;
222
+ else break;
223
+ }
224
+ return result;
225
+ }
226
+
54
227
  // src/config.ts
228
+ function applyNetworkPresets(config) {
229
+ const { network } = config;
230
+ if (!network || network === "custom") return config;
231
+ const presets = resolveClientNetwork(network);
232
+ const mergeRecord = (explicit, preset) => ({ ...preset, ...explicit });
233
+ const supportedChains = config.supportedChains ? Array.from(
234
+ /* @__PURE__ */ new Set([...presets.supportedChains, ...config.supportedChains])
235
+ ) : presets.supportedChains;
236
+ return {
237
+ ...config,
238
+ supportedChains,
239
+ chainRpcUrls: mergeRecord(config.chainRpcUrls, presets.chainRpcUrls),
240
+ preferredTokens: mergeRecord(
241
+ config.preferredTokens,
242
+ presets.preferredTokens
243
+ ),
244
+ tokenNetworks: mergeRecord(config.tokenNetworks, presets.tokenNetworks),
245
+ // settlementAddresses are identity-derived (per-client), so they have no
246
+ // preset; pass any explicit value through unchanged.
247
+ ...config.settlementAddresses && {
248
+ settlementAddresses: config.settlementAddresses
249
+ },
250
+ // Channel params: preset fills the deployed programId/zkApp + URLs unless
251
+ // the caller supplied their own (explicit object wins wholesale).
252
+ ...presets.solanaChannel && {
253
+ solanaChannel: config.solanaChannel ?? presets.solanaChannel
254
+ },
255
+ ...presets.minaChannel && {
256
+ minaChannel: config.minaChannel ?? presets.minaChannel
257
+ }
258
+ };
259
+ }
260
+ function getNetworkStatus(config) {
261
+ const { network } = config;
262
+ if (!network || network === "custom") return void 0;
263
+ return resolveClientNetwork(network).status;
264
+ }
55
265
  function validateConfig(config) {
56
266
  if (config.connector !== void 0) {
57
267
  throw new ValidationError(
@@ -80,6 +290,24 @@ function validateConfig(config) {
80
290
  );
81
291
  }
82
292
  }
293
+ if (config.mnemonic !== void 0) {
294
+ if (config.secretKey !== void 0) {
295
+ throw new ValidationError(
296
+ "Provide either `mnemonic` or `secretKey`, not both \u2014 the mnemonic derives the Nostr key, so a separate secretKey would yield an inconsistent cross-chain identity. (An `evmPrivateKey` override is allowed.)"
297
+ );
298
+ }
299
+ if (typeof config.mnemonic !== "string" || !validateMnemonic(config.mnemonic)) {
300
+ throw new ValidationError("mnemonic must be a valid BIP-39 phrase");
301
+ }
302
+ }
303
+ if (config.mnemonicAccountIndex !== void 0) {
304
+ const idx = config.mnemonicAccountIndex;
305
+ if (!Number.isInteger(idx) || idx < 0 || idx > 2147483647) {
306
+ throw new ValidationError(
307
+ "mnemonicAccountIndex must be a non-negative integer (0 to 2147483647)"
308
+ );
309
+ }
310
+ }
83
311
  if (!config.ilpInfo?.ilpAddress) {
84
312
  throw new ValidationError("ilpInfo.ilpAddress is required");
85
313
  }
@@ -117,6 +345,25 @@ function validateConfig(config) {
117
345
  );
118
346
  }
119
347
  }
348
+ if (config.transport) {
349
+ if (config.transport.type === "socks5") {
350
+ if (!config.transport.socksProxy?.startsWith("socks5h://")) {
351
+ throw new ValidationError(
352
+ 'transport.socksProxy must use socks5h:// scheme to prevent DNS leaks. The "h" suffix ensures .anyone hostnames are resolved by the proxy, not locally.'
353
+ );
354
+ }
355
+ } else if (config.transport.type === "gateway") {
356
+ if (!config.transport.gatewayUrl) {
357
+ throw new ValidationError(
358
+ "transport.gatewayUrl is required for gateway transport"
359
+ );
360
+ }
361
+ } else if (config.transport.type !== "direct") {
362
+ throw new ValidationError(
363
+ `Unknown transport type: "${config.transport.type}"`
364
+ );
365
+ }
366
+ }
120
367
  if (config.chainRpcUrls && config.supportedChains) {
121
368
  for (const chain of Object.keys(config.chainRpcUrls)) {
122
369
  if (!config.supportedChains.includes(chain)) {
@@ -127,8 +374,12 @@ function validateConfig(config) {
127
374
  }
128
375
  }
129
376
  }
130
- function applyDefaults(config) {
131
- const secretKey = config.secretKey ?? generateSecretKey();
377
+ function applyDefaults(rawConfig) {
378
+ const config = applyNetworkPresets(rawConfig);
379
+ const secretKey = config.secretKey ?? (config.mnemonic ? deriveNostrKeyFromMnemonic(
380
+ config.mnemonic,
381
+ config.mnemonicAccountIndex ?? 0
382
+ ).secretKey : generateSecretKey2());
132
383
  let btpUrl = config.btpUrl;
133
384
  if (!btpUrl && config.connectorUrl) {
134
385
  try {
@@ -175,7 +426,8 @@ function applyDefaults(config) {
175
426
  // Always set by logic above
176
427
  };
177
428
  }
178
- function buildSettlementInfo(config) {
429
+ function buildSettlementInfo(rawConfig) {
430
+ const config = applyNetworkPresets(rawConfig);
179
431
  if (!config.supportedChains?.length && !config.settlementAddresses && !config.preferredTokens && !config.tokenNetworks) {
180
432
  return void 0;
181
433
  }
@@ -210,7 +462,7 @@ function fromBase64(base64) {
210
462
  }
211
463
  return bytes;
212
464
  }
213
- function toHex(bytes) {
465
+ function toHex2(bytes) {
214
466
  let hex = "";
215
467
  for (const byte of bytes) {
216
468
  hex += byte.toString(16).padStart(2, "0");
@@ -220,6 +472,9 @@ function toHex(bytes) {
220
472
  function encodeUtf8(str) {
221
473
  return new TextEncoder().encode(str);
222
474
  }
475
+ function decodeUtf8(bytes) {
476
+ return new TextDecoder().decode(bytes);
477
+ }
223
478
  function isBase64(str) {
224
479
  return /^[A-Za-z0-9+/]*={0,2}$/.test(str);
225
480
  }
@@ -665,7 +920,7 @@ var IsomorphicBtpClient = class {
665
920
  if (this._isConnected) return;
666
921
  return new Promise((resolve, reject) => {
667
922
  try {
668
- this.ws = new WebSocket(this.config.url);
923
+ this.ws = this.config.createWebSocket ? this.config.createWebSocket(this.config.url) : new WebSocket(this.config.url);
669
924
  this.ws.binaryType = "arraybuffer";
670
925
  } catch (err) {
671
926
  reject(
@@ -689,8 +944,14 @@ var IsomorphicBtpClient = class {
689
944
  this.ws.onmessage = (event) => {
690
945
  this.handleMessage(event.data);
691
946
  };
692
- this.ws.onerror = () => {
693
- reject(new BtpConnectionError("WebSocket connection error"));
947
+ this.ws.onerror = (event) => {
948
+ const underlying = event?.error ?? event?.message;
949
+ const detail = underlying instanceof Error ? underlying.message : typeof underlying === "string" ? underlying : null;
950
+ reject(
951
+ new BtpConnectionError(
952
+ detail ? `WebSocket connection error: ${detail}` : "WebSocket connection error"
953
+ )
954
+ );
694
955
  };
695
956
  this.ws.onclose = () => {
696
957
  this._isConnected = false;
@@ -746,6 +1007,30 @@ var IsomorphicBtpClient = class {
746
1007
  this.pendingRequests.set(requestId, { resolve, reject, timeoutId });
747
1008
  });
748
1009
  }
1010
+ /**
1011
+ * Send a fire-and-forget BTP MESSAGE carrying only protocol data (no ILP
1012
+ * packet). Used for out-of-band claim notifications that the connector's
1013
+ * ClaimReceiver consumes via `handleClaimMessage` — there is no RESPONSE
1014
+ * frame, so we resolve immediately after the WebSocket buffers the bytes.
1015
+ *
1016
+ * Mirrors `sendPacket` wire-format construction but uses an empty ILP
1017
+ * payload and does not enroll a pending request.
1018
+ */
1019
+ async sendProtocolData(protocolName, contentType, data) {
1020
+ if (!this._isConnected || !this.ws) {
1021
+ throw new BtpConnectionError("Not connected");
1022
+ }
1023
+ const requestId = this.nextRequestId();
1024
+ const btpMessage = serializeBtpMessage({
1025
+ type: BTPMessageType.MESSAGE,
1026
+ requestId,
1027
+ data: {
1028
+ protocolData: [{ protocolName, contentType, data }],
1029
+ ilpPacket: new Uint8Array(0)
1030
+ }
1031
+ });
1032
+ this.ws.send(btpMessage);
1033
+ }
749
1034
  // ─── Private ────────────────────────────────────────────────────────────
750
1035
  async authenticate() {
751
1036
  if (!this.ws) throw new BtpAuthError("WebSocket not connected");
@@ -789,7 +1074,9 @@ var IsomorphicBtpClient = class {
789
1074
  if (message.type === BTPMessageType.ERROR) {
790
1075
  const errData = message.data;
791
1076
  reject(
792
- new BtpAuthError(`Authentication failed: ${errData.code}`)
1077
+ new BtpAuthError(
1078
+ `Authentication failed: ${errData.code} msg=${errData.message ?? ""} trigger=${errData.triggeredBy ?? ""}`
1079
+ )
793
1080
  );
794
1081
  } else if (message.type === BTPMessageType.RESPONSE) {
795
1082
  resolve();
@@ -901,7 +1188,8 @@ var BtpRuntimeClient = class {
901
1188
  this.btpClient = new IsomorphicBtpClient({
902
1189
  url: this.config.btpUrl,
903
1190
  peerId: this.config.peerId,
904
- authToken: this.config.authToken
1191
+ authToken: this.config.authToken,
1192
+ createWebSocket: this.config.createWebSocket
905
1193
  });
906
1194
  await this.btpClient.connect();
907
1195
  this._isConnected = true;
@@ -963,6 +1251,36 @@ var BtpRuntimeClient = class {
963
1251
  }
964
1252
  });
965
1253
  }
1254
+ /**
1255
+ * Send a standalone `payment-channel-claim` BTP MESSAGE (no ILP packet
1256
+ * attached). The connector's ClaimReceiver consumes this fire-and-forget
1257
+ * to register cumulative claim state independently of the per-packet
1258
+ * forwarding path. Auto-reconnects on connection errors.
1259
+ */
1260
+ async sendClaimMessage(claim) {
1261
+ return withRetry(() => this._sendClaimMessageOnce(claim), {
1262
+ maxRetries: this.config.maxRetries ?? 3,
1263
+ retryDelay: this.config.retryDelay ?? 1e3,
1264
+ shouldRetry: (error) => {
1265
+ if (!isConnectionError(error)) return false;
1266
+ this._isConnected = false;
1267
+ return true;
1268
+ }
1269
+ });
1270
+ }
1271
+ async _sendClaimMessageOnce(claim) {
1272
+ if (!this._isConnected) {
1273
+ await this.reconnect();
1274
+ }
1275
+ if (!this.btpClient) {
1276
+ throw new BtpConnectionError("BTP client not connected");
1277
+ }
1278
+ await this.btpClient.sendProtocolData(
1279
+ "payment-channel-claim",
1280
+ 1,
1281
+ encodeUtf8(JSON.stringify(claim))
1282
+ );
1283
+ }
966
1284
  /**
967
1285
  * Single-attempt ILP packet send. Reconnects if not connected.
968
1286
  */
@@ -1043,62 +1361,633 @@ import {
1043
1361
  decodeEventLog,
1044
1362
  defineChain
1045
1363
  } from "viem";
1046
- var TOKEN_NETWORK_ABI = [
1047
- {
1048
- name: "openChannel",
1049
- type: "function",
1050
- stateMutability: "nonpayable",
1051
- inputs: [
1052
- { name: "participant2", type: "address" },
1053
- { name: "settlementTimeout", type: "uint256" }
1054
- ],
1055
- outputs: [{ type: "bytes32" }]
1056
- },
1057
- {
1058
- name: "setTotalDeposit",
1059
- type: "function",
1060
- stateMutability: "nonpayable",
1061
- inputs: [
1062
- { name: "channelId", type: "bytes32" },
1063
- { name: "participant", type: "address" },
1064
- { name: "totalDeposit", type: "uint256" }
1065
- ],
1066
- outputs: []
1067
- },
1068
- {
1069
- name: "channels",
1070
- type: "function",
1071
- stateMutability: "view",
1072
- inputs: [{ type: "bytes32" }],
1073
- outputs: [
1074
- { name: "settlementTimeout", type: "uint256" },
1075
- { name: "state", type: "uint8" },
1076
- { name: "closedAt", type: "uint256" },
1077
- { name: "openedAt", type: "uint256" },
1078
- { name: "participant1", type: "address" },
1079
- { name: "participant2", type: "address" }
1080
- ]
1081
- },
1082
- {
1083
- name: "ChannelOpened",
1084
- type: "event",
1085
- inputs: [
1086
- { name: "channelId", type: "bytes32", indexed: true },
1087
- { name: "participant1", type: "address", indexed: true },
1088
- { name: "participant2", type: "address", indexed: true },
1089
- { name: "settlementTimeout", type: "uint256", indexed: false }
1090
- ]
1364
+ import { ed25519 as ed255192 } from "@noble/curves/ed25519.js";
1365
+ import { base58Encode as base58Encode2 } from "@toon-protocol/core";
1366
+
1367
+ // src/channel/solana-payment-channel.ts
1368
+ import { ed25519 } from "@noble/curves/ed25519.js";
1369
+ import { sha256 } from "@noble/hashes/sha2.js";
1370
+ import { base58Encode, base58Decode } from "@toon-protocol/core";
1371
+ var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
1372
+ var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
1373
+ var RENT_SYSVAR_ID = "SysvarRent111111111111111111111111111111111";
1374
+ var IX_INITIALIZE_CHANNEL = new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0]);
1375
+ var IX_DEPOSIT = new Uint8Array([2, 0, 0, 0, 0, 0, 0, 0]);
1376
+ var CHANNEL_DISCRIMINATOR = new Uint8Array([
1377
+ 112,
1378
+ 99,
1379
+ 104,
1380
+ 97,
1381
+ 110,
1382
+ 110,
1383
+ 101,
1384
+ 108
1385
+ ]);
1386
+ var CHANNEL_ACCOUNT_SIZE = 178;
1387
+ var MAX_U64 = (1n << 64n) - 1n;
1388
+ function writeU64LE(buf, offset, value) {
1389
+ if (value < 0n || value > MAX_U64) {
1390
+ throw new RangeError(`Value ${value} outside u64 range [0, 2^64-1]`);
1391
+ }
1392
+ for (let i = 0; i < 8; i++) {
1393
+ buf[offset + i] = Number(value >> BigInt(i * 8) & 0xffn);
1091
1394
  }
1092
- ];
1093
- var ERC20_ABI = [
1094
- {
1095
- name: "approve",
1096
- type: "function",
1097
- stateMutability: "nonpayable",
1098
- inputs: [
1099
- { name: "spender", type: "address" },
1100
- { name: "amount", type: "uint256" }
1101
- ],
1395
+ }
1396
+ function padTo32(bytes) {
1397
+ if (bytes.length === 32) return bytes;
1398
+ if (bytes.length > 32) return bytes.slice(bytes.length - 32);
1399
+ const padded = new Uint8Array(32);
1400
+ padded.set(bytes, 32 - bytes.length);
1401
+ return padded;
1402
+ }
1403
+ function sortPubkeys(a, b) {
1404
+ for (let i = 0; i < 32; i++) {
1405
+ const ai = a[i] ?? 0;
1406
+ const bi = b[i] ?? 0;
1407
+ if (ai < bi) return [a, b];
1408
+ if (ai > bi) return [b, a];
1409
+ }
1410
+ return [a, b];
1411
+ }
1412
+ function modPow(base, exp, mod) {
1413
+ let result = 1n;
1414
+ base = (base % mod + mod) % mod;
1415
+ while (exp > 0n) {
1416
+ if (exp & 1n) result = result * base % mod;
1417
+ exp >>= 1n;
1418
+ base = base * base % mod;
1419
+ }
1420
+ return result;
1421
+ }
1422
+ function modInverse(a, m) {
1423
+ return modPow((a % m + m) % m, m - 2n, m);
1424
+ }
1425
+ function isOnCurve(bytes) {
1426
+ const P = (1n << 255n) - 19n;
1427
+ const yBytes = new Uint8Array(32);
1428
+ yBytes.set(bytes);
1429
+ yBytes[31] = (yBytes[31] ?? 0) & 127;
1430
+ let y = 0n;
1431
+ for (let i = 0; i < 32; i++) {
1432
+ y |= BigInt(yBytes[i] ?? 0) << BigInt(i * 8);
1433
+ }
1434
+ if (y >= P) return true;
1435
+ const y2 = y * y % P;
1436
+ const D = (P - 121665n * modInverse(121666n, P) % P + P) % P;
1437
+ const numerator = (y2 - 1n + P) % P;
1438
+ const denominator = (D * y2 + 1n) % P;
1439
+ const x2 = numerator * modInverse(denominator, P) % P;
1440
+ if (x2 === 0n) return true;
1441
+ return modPow(x2, (P - 1n) / 2n, P) === 1n;
1442
+ }
1443
+ function findProgramAddress(seeds, programId) {
1444
+ const PDA_MARKER = new TextEncoder().encode("ProgramDerivedAddress");
1445
+ for (let bump = 255; bump >= 0; bump--) {
1446
+ const allSeeds = [...seeds, new Uint8Array([bump])];
1447
+ let totalLen = programId.length + PDA_MARKER.length;
1448
+ for (const s of allSeeds) totalLen += s.length;
1449
+ const input = new Uint8Array(totalLen);
1450
+ let offset = 0;
1451
+ for (const s of allSeeds) {
1452
+ input.set(s, offset);
1453
+ offset += s.length;
1454
+ }
1455
+ input.set(programId, offset);
1456
+ offset += programId.length;
1457
+ input.set(PDA_MARKER, offset);
1458
+ const hash = sha256(input);
1459
+ if (!isOnCurve(hash)) return { pda: hash, bump };
1460
+ }
1461
+ throw new Error("Could not find a viable PDA bump seed");
1462
+ }
1463
+ function deriveChannelPDA(participantA, participantB, tokenMint, programId) {
1464
+ const a = padTo32(base58Decode(participantA));
1465
+ const b = padTo32(base58Decode(participantB));
1466
+ const mint = padTo32(base58Decode(tokenMint));
1467
+ const program = padTo32(base58Decode(programId));
1468
+ const [min, max] = sortPubkeys(a, b);
1469
+ const seeds = [new TextEncoder().encode("channel"), min, max, mint];
1470
+ const { pda, bump } = findProgramAddress(seeds, program);
1471
+ return { pda: base58Encode(pda), bump };
1472
+ }
1473
+ function deriveVaultPDA(channelPDA, programId) {
1474
+ const channel = padTo32(base58Decode(channelPDA));
1475
+ const program = padTo32(base58Decode(programId));
1476
+ const seeds = [new TextEncoder().encode("vault"), channel];
1477
+ const { pda, bump } = findProgramAddress(seeds, program);
1478
+ return { pda: base58Encode(pda), bump };
1479
+ }
1480
+ function buildBalanceProofMessage(channelPDA, nonce, transferredAmount) {
1481
+ const message = new Uint8Array(48);
1482
+ message.set(padTo32(base58Decode(channelPDA)), 0);
1483
+ writeU64LE(message, 32, nonce);
1484
+ writeU64LE(message, 40, transferredAmount);
1485
+ return message;
1486
+ }
1487
+ var rpcIdCounter = 1;
1488
+ async function solanaRpc(rpcUrl, method, params = []) {
1489
+ const res = await fetch(rpcUrl, {
1490
+ method: "POST",
1491
+ headers: { "Content-Type": "application/json" },
1492
+ body: JSON.stringify({
1493
+ jsonrpc: "2.0",
1494
+ method,
1495
+ params,
1496
+ id: rpcIdCounter++
1497
+ }),
1498
+ signal: AbortSignal.timeout(3e4)
1499
+ });
1500
+ const json = await res.json();
1501
+ if (json.error) {
1502
+ throw new Error(
1503
+ `Solana RPC error [${method}]: ${json.error.message} (code ${json.error.code})`
1504
+ );
1505
+ }
1506
+ return json.result;
1507
+ }
1508
+ async function getLatestBlockhash(rpcUrl) {
1509
+ const result = await solanaRpc(rpcUrl, "getLatestBlockhash", [
1510
+ { commitment: "confirmed" }
1511
+ ]);
1512
+ return result.value.blockhash;
1513
+ }
1514
+ async function getAccountInfo(rpcUrl, pubkey) {
1515
+ const result = await solanaRpc(rpcUrl, "getAccountInfo", [
1516
+ pubkey,
1517
+ { encoding: "base64", commitment: "confirmed" }
1518
+ ]);
1519
+ return result.value;
1520
+ }
1521
+ async function waitForConfirmation(rpcUrl, signature, timeoutMs = 3e4) {
1522
+ const start = Date.now();
1523
+ while (Date.now() - start < timeoutMs) {
1524
+ const result = await solanaRpc(rpcUrl, "getSignatureStatuses", [
1525
+ [signature]
1526
+ ]);
1527
+ const status = result.value[0];
1528
+ if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
1529
+ if (status.err) {
1530
+ throw new Error(
1531
+ `Transaction ${signature} failed: ${JSON.stringify(status.err)}`
1532
+ );
1533
+ }
1534
+ return;
1535
+ }
1536
+ await new Promise((r) => setTimeout(r, 500));
1537
+ }
1538
+ throw new Error(
1539
+ `Transaction ${signature} not confirmed within ${timeoutMs}ms`
1540
+ );
1541
+ }
1542
+ function compactU16Size(value) {
1543
+ if (value > 65535) {
1544
+ throw new RangeError(`compact-u16 value ${value} exceeds u16 max (0xFFFF)`);
1545
+ }
1546
+ return value < 128 ? 1 : value < 16384 ? 2 : 3;
1547
+ }
1548
+ function writeCompactU16(buf, offset, value) {
1549
+ if (value < 128) {
1550
+ buf[offset++] = value;
1551
+ } else if (value < 16384) {
1552
+ buf[offset++] = value & 127 | 128;
1553
+ buf[offset++] = value >> 7;
1554
+ } else {
1555
+ buf[offset++] = value & 127 | 128;
1556
+ buf[offset++] = value >> 7 & 127 | 128;
1557
+ buf[offset++] = value >> 14;
1558
+ }
1559
+ return offset;
1560
+ }
1561
+ async function buildAndSendTransaction(rpcUrl, feePayer, instructions, additionalSigners = []) {
1562
+ const blockhash = await getLatestBlockhash(rpcUrl);
1563
+ const feePayerPubkey = base58Encode(feePayer.publicKey);
1564
+ const accountMap = /* @__PURE__ */ new Map();
1565
+ accountMap.set(feePayerPubkey, {
1566
+ pubkey: feePayerPubkey,
1567
+ isSigner: true,
1568
+ isWritable: true
1569
+ });
1570
+ for (const ix of instructions) {
1571
+ for (const key of ix.keys) {
1572
+ const existing = accountMap.get(key.pubkey);
1573
+ if (existing) {
1574
+ existing.isSigner = existing.isSigner || key.isSigner;
1575
+ existing.isWritable = existing.isWritable || key.isWritable;
1576
+ } else {
1577
+ accountMap.set(key.pubkey, { ...key });
1578
+ }
1579
+ }
1580
+ if (!accountMap.has(ix.programId)) {
1581
+ accountMap.set(ix.programId, {
1582
+ pubkey: ix.programId,
1583
+ isSigner: false,
1584
+ isWritable: false
1585
+ });
1586
+ }
1587
+ }
1588
+ const accounts = [...accountMap.values()].sort((a, b) => {
1589
+ if (a.pubkey === feePayerPubkey) return -1;
1590
+ if (b.pubkey === feePayerPubkey) return 1;
1591
+ const aScore = (a.isSigner ? 2 : 0) + (a.isWritable ? 1 : 0);
1592
+ const bScore = (b.isSigner ? 2 : 0) + (b.isWritable ? 1 : 0);
1593
+ return bScore - aScore;
1594
+ });
1595
+ const numSigners = accounts.filter((a) => a.isSigner).length;
1596
+ const numReadonlySigners = accounts.filter(
1597
+ (a) => a.isSigner && !a.isWritable
1598
+ ).length;
1599
+ const numReadonlyNonSigners = accounts.filter(
1600
+ (a) => !a.isSigner && !a.isWritable
1601
+ ).length;
1602
+ const accountIndexMap = /* @__PURE__ */ new Map();
1603
+ accounts.forEach((a, i) => accountIndexMap.set(a.pubkey, i));
1604
+ const compiled = instructions.map((ix) => ({
1605
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- programId added to accountMap above
1606
+ programIdIndex: accountIndexMap.get(ix.programId),
1607
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- every key added to accountMap above
1608
+ accountIndices: ix.keys.map((k) => accountIndexMap.get(k.pubkey)),
1609
+ data: ix.data
1610
+ }));
1611
+ const blockhashBytes = base58Decode(blockhash);
1612
+ let instructionSize = compactU16Size(compiled.length);
1613
+ for (const ix of compiled) {
1614
+ instructionSize += 1;
1615
+ instructionSize += compactU16Size(ix.accountIndices.length) + ix.accountIndices.length;
1616
+ instructionSize += compactU16Size(ix.data.length) + ix.data.length;
1617
+ }
1618
+ const messageSize = 3 + compactU16Size(accounts.length) + 32 * accounts.length + 32 + instructionSize;
1619
+ const message = new Uint8Array(messageSize);
1620
+ let offset = 0;
1621
+ message[offset++] = numSigners;
1622
+ message[offset++] = numReadonlySigners;
1623
+ message[offset++] = numReadonlyNonSigners;
1624
+ offset = writeCompactU16(message, offset, accounts.length);
1625
+ for (const acct of accounts) {
1626
+ message.set(padTo32(base58Decode(acct.pubkey)), offset);
1627
+ offset += 32;
1628
+ }
1629
+ message.set(padTo32(blockhashBytes), offset);
1630
+ offset += 32;
1631
+ offset = writeCompactU16(message, offset, compiled.length);
1632
+ for (const ix of compiled) {
1633
+ message[offset++] = ix.programIdIndex;
1634
+ offset = writeCompactU16(message, offset, ix.accountIndices.length);
1635
+ for (const idx of ix.accountIndices) message[offset++] = idx;
1636
+ offset = writeCompactU16(message, offset, ix.data.length);
1637
+ message.set(ix.data, offset);
1638
+ offset += ix.data.length;
1639
+ }
1640
+ const finalMessage = message.slice(0, offset);
1641
+ const allSigners = [feePayer, ...additionalSigners];
1642
+ const signerPubkeys = accounts.filter((a) => a.isSigner).map((a) => a.pubkey);
1643
+ const signatures = [];
1644
+ for (const signerPubkey of signerPubkeys) {
1645
+ const signer = allSigners.find(
1646
+ (s) => base58Encode(s.publicKey) === signerPubkey
1647
+ );
1648
+ if (!signer) throw new Error(`Missing signer for ${signerPubkey}`);
1649
+ signatures.push(ed25519.sign(finalMessage, signer.privateKey));
1650
+ }
1651
+ const txSize = compactU16Size(signatures.length) + signatures.length * 64 + finalMessage.length;
1652
+ const tx = new Uint8Array(txSize);
1653
+ let txOffset = 0;
1654
+ txOffset = writeCompactU16(tx, txOffset, signatures.length);
1655
+ for (const sig of signatures) {
1656
+ tx.set(sig, txOffset);
1657
+ txOffset += 64;
1658
+ }
1659
+ tx.set(finalMessage, txOffset);
1660
+ const txBase64 = Buffer.from(tx).toString("base64");
1661
+ const txSig = await solanaRpc(rpcUrl, "sendTransaction", [
1662
+ txBase64,
1663
+ {
1664
+ encoding: "base64",
1665
+ skipPreflight: false,
1666
+ preflightCommitment: "confirmed"
1667
+ }
1668
+ ]);
1669
+ await waitForConfirmation(rpcUrl, txSig);
1670
+ return txSig;
1671
+ }
1672
+ var STATE_MAP = ["opened", "closed", "settled"];
1673
+ async function getChannelAccountState(rpcUrl, channelPDA) {
1674
+ const info = await getAccountInfo(rpcUrl, channelPDA);
1675
+ if (!info) return { exists: false };
1676
+ const data = new Uint8Array(Buffer.from(info.data[0], "base64"));
1677
+ if (data.length < CHANNEL_ACCOUNT_SIZE) return { exists: false };
1678
+ for (let i = 0; i < 8; i++) {
1679
+ if (data[i] !== CHANNEL_DISCRIMINATOR[i]) return { exists: false };
1680
+ }
1681
+ return {
1682
+ exists: true,
1683
+ state: STATE_MAP[data[160] ?? 0] ?? "opened",
1684
+ participantA: base58Encode(data.slice(8, 40)),
1685
+ participantB: base58Encode(data.slice(40, 72))
1686
+ };
1687
+ }
1688
+ async function openSolanaChannel(params) {
1689
+ const {
1690
+ rpcUrl,
1691
+ programId,
1692
+ tokenMint,
1693
+ payerSeed,
1694
+ payerPubkey,
1695
+ peerPubkey,
1696
+ challengeDuration
1697
+ } = params;
1698
+ const { pda: channelPDA } = deriveChannelPDA(
1699
+ payerPubkey,
1700
+ peerPubkey,
1701
+ tokenMint,
1702
+ programId
1703
+ );
1704
+ const existing = await getChannelAccountState(rpcUrl, channelPDA);
1705
+ if (existing.exists) {
1706
+ return { channelPDA, opened: false };
1707
+ }
1708
+ const payerPublicKey = padTo32(base58Decode(payerPubkey));
1709
+ const payer = { publicKey: payerPublicKey, privateKey: payerSeed };
1710
+ const { pda: vaultPDA } = deriveVaultPDA(channelPDA, programId);
1711
+ const initData = new Uint8Array(16);
1712
+ initData.set(IX_INITIALIZE_CHANNEL, 0);
1713
+ writeU64LE(initData, 8, challengeDuration);
1714
+ const initTxSignature = await buildAndSendTransaction(rpcUrl, payer, [
1715
+ {
1716
+ programId,
1717
+ keys: [
1718
+ { pubkey: payerPubkey, isSigner: true, isWritable: true },
1719
+ { pubkey: payerPubkey, isSigner: false, isWritable: false },
1720
+ // participant A
1721
+ { pubkey: peerPubkey, isSigner: false, isWritable: false },
1722
+ // participant B
1723
+ { pubkey: tokenMint, isSigner: false, isWritable: false },
1724
+ { pubkey: channelPDA, isSigner: false, isWritable: true },
1725
+ { pubkey: vaultPDA, isSigner: false, isWritable: true },
1726
+ { pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false },
1727
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1728
+ { pubkey: RENT_SYSVAR_ID, isSigner: false, isWritable: false }
1729
+ ],
1730
+ data: initData
1731
+ }
1732
+ ]);
1733
+ let depositTxSignature;
1734
+ if (params.deposit && params.deposit.amount > 0n) {
1735
+ const depositData = new Uint8Array(16);
1736
+ depositData.set(IX_DEPOSIT, 0);
1737
+ writeU64LE(depositData, 8, params.deposit.amount);
1738
+ depositTxSignature = await buildAndSendTransaction(rpcUrl, payer, [
1739
+ {
1740
+ programId,
1741
+ keys: [
1742
+ { pubkey: payerPubkey, isSigner: true, isWritable: false },
1743
+ {
1744
+ pubkey: params.deposit.payerTokenAccount,
1745
+ isSigner: false,
1746
+ isWritable: true
1747
+ },
1748
+ { pubkey: vaultPDA, isSigner: false, isWritable: true },
1749
+ { pubkey: channelPDA, isSigner: false, isWritable: true },
1750
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }
1751
+ ],
1752
+ data: depositData
1753
+ }
1754
+ ]);
1755
+ }
1756
+ return { channelPDA, opened: true, initTxSignature, depositTxSignature };
1757
+ }
1758
+
1759
+ // src/channel/mina-channel-open.ts
1760
+ import { hexToMinaBase58PrivateKey as hexToMinaBase58PrivateKey2 } from "@toon-protocol/core";
1761
+ var cachedO1js = null;
1762
+ var cachedPaymentChannel = null;
1763
+ var compiledContract = null;
1764
+ var runtimeOverride = null;
1765
+ async function loadMinaRuntime() {
1766
+ if (cachedO1js && cachedPaymentChannel) {
1767
+ return { o1js: cachedO1js, PaymentChannel: cachedPaymentChannel };
1768
+ }
1769
+ if (runtimeOverride) {
1770
+ const injected = await runtimeOverride();
1771
+ cachedO1js = injected.o1js;
1772
+ cachedPaymentChannel = injected.PaymentChannel;
1773
+ return injected;
1774
+ }
1775
+ const { createRequire } = await import("module");
1776
+ const nodePath = await import("path");
1777
+ const requireHere = createRequire(import.meta.url);
1778
+ const mzkPkgPath = requireHere.resolve(
1779
+ "@toon-protocol/mina-zkapp/package.json"
1780
+ );
1781
+ const requireFromMzk = createRequire(mzkPkgPath);
1782
+ const o1js = requireFromMzk("o1js");
1783
+ const mzkPkgJson = requireFromMzk(mzkPkgPath);
1784
+ const mzkDir = nodePath.dirname(mzkPkgPath);
1785
+ const mzkEntry = nodePath.join(mzkDir, mzkPkgJson.main ?? "dist/index.js");
1786
+ const mzk = requireFromMzk(mzkEntry);
1787
+ const PaymentChannel = mzk.PaymentChannel ?? mzk.default?.PaymentChannel;
1788
+ if (!PaymentChannel) {
1789
+ throw new Error(
1790
+ "@toon-protocol/mina-zkapp does not export PaymentChannel \u2014 cannot open a Mina channel"
1791
+ );
1792
+ }
1793
+ cachedO1js = o1js;
1794
+ cachedPaymentChannel = PaymentChannel;
1795
+ return { o1js, PaymentChannel };
1796
+ }
1797
+ async function getO1js() {
1798
+ return (await loadMinaRuntime()).o1js;
1799
+ }
1800
+ async function getCompiledPaymentChannel() {
1801
+ const { PaymentChannel } = await loadMinaRuntime();
1802
+ if (!compiledContract) {
1803
+ await PaymentChannel.compile();
1804
+ compiledContract = PaymentChannel;
1805
+ }
1806
+ return compiledContract;
1807
+ }
1808
+ var MINA_CHANNEL_STATE_OPEN = 1n;
1809
+ var MINA_CHANNEL_STATE_UNINITIALIZED = 0n;
1810
+ async function openMinaChannelOnChain(params) {
1811
+ const { Mina, PrivateKey, PublicKey, Field, AccountUpdate, fetchAccount } = await getO1js();
1812
+ const network = Mina.Network(params.graphqlUrl);
1813
+ Mina.setActiveInstance(network);
1814
+ const txFee = params.feeNanomina ?? 100000000n;
1815
+ const payerKeyBase58 = hexToMinaBase58PrivateKey2(params.payerPrivateKey);
1816
+ const payerPrivateKey = PrivateKey.fromBase58(payerKeyBase58);
1817
+ const payerPublicKey = payerPrivateKey.toPublicKey();
1818
+ const zkAppPublicKey = PublicKey.fromBase58(params.zkAppAddress);
1819
+ const readChannelState = async () => {
1820
+ const res = await fetchAccount({ publicKey: zkAppPublicKey });
1821
+ if (res.error || !res.account) {
1822
+ throw new Error(
1823
+ `Mina zkApp account ${params.zkAppAddress} not found on-chain (${String(
1824
+ res.error
1825
+ )}) \u2014 deploy the PaymentChannel zkApp before opening a channel`
1826
+ );
1827
+ }
1828
+ const appState = res.account.zkapp?.appState;
1829
+ const raw = appState?.[3]?.toString() ?? "0";
1830
+ return BigInt(raw);
1831
+ };
1832
+ const readDepositTotal = async () => {
1833
+ const res = await fetchAccount({ publicKey: zkAppPublicKey });
1834
+ if (res.error || !res.account) {
1835
+ throw new Error(
1836
+ `Mina zkApp account ${params.zkAppAddress} not found on-chain (${String(
1837
+ res.error
1838
+ )}) \u2014 deploy the PaymentChannel zkApp before opening a channel`
1839
+ );
1840
+ }
1841
+ const appState = res.account.zkapp?.appState;
1842
+ const raw = appState?.[4]?.toString() ?? "0";
1843
+ return BigInt(raw);
1844
+ };
1845
+ const currentState = await readChannelState();
1846
+ await fetchAccount({ publicKey: payerPublicKey });
1847
+ let opened = false;
1848
+ let initTxHash;
1849
+ let zkApp;
1850
+ const getZkApp = async () => {
1851
+ if (!zkApp) {
1852
+ const PaymentChannel = await getCompiledPaymentChannel();
1853
+ zkApp = new PaymentChannel(zkAppPublicKey);
1854
+ }
1855
+ return zkApp;
1856
+ };
1857
+ if (currentState === MINA_CHANNEL_STATE_UNINITIALIZED) {
1858
+ const channel = await getZkApp();
1859
+ const participantA = payerPublicKey;
1860
+ const participantB = params.peerPublicKey ? PublicKey.fromBase58(params.peerPublicKey) : payerPublicKey;
1861
+ const nonce = Field(0);
1862
+ const timeoutField = Field((params.timeout ?? 86400n).toString());
1863
+ const tokenIdField = Field(params.tokenId ?? "1");
1864
+ await fetchAccount({ publicKey: zkAppPublicKey });
1865
+ await fetchAccount({ publicKey: payerPublicKey });
1866
+ const initTx = await Mina.transaction(
1867
+ { sender: payerPublicKey, fee: Number(txFee) },
1868
+ async () => {
1869
+ await channel.initializeChannel(
1870
+ participantA,
1871
+ participantB,
1872
+ nonce,
1873
+ timeoutField,
1874
+ tokenIdField
1875
+ );
1876
+ }
1877
+ );
1878
+ await initTx.prove();
1879
+ const sentInit = await initTx.sign([payerPrivateKey]).send();
1880
+ initTxHash = sentInit.hash ?? void 0;
1881
+ opened = true;
1882
+ await sentInit.wait();
1883
+ await fetchAccount({ publicKey: zkAppPublicKey });
1884
+ await fetchAccount({ publicKey: payerPublicKey });
1885
+ } else if (currentState !== MINA_CHANNEL_STATE_OPEN) {
1886
+ throw new Error(
1887
+ `Mina channel ${params.zkAppAddress} is in state ${currentState} (not UNINITIALIZED/OPEN) \u2014 cannot open`
1888
+ );
1889
+ }
1890
+ let depositTxHash;
1891
+ if (params.deposit && params.deposit.amount > 0n) {
1892
+ const channel = await getZkApp();
1893
+ await fetchAccount({ publicKey: zkAppPublicKey });
1894
+ const amountField = Field(params.deposit.amount.toString());
1895
+ const depositTx = await Mina.transaction(
1896
+ { sender: payerPublicKey, fee: Number(txFee) },
1897
+ async () => {
1898
+ await channel.deposit(amountField, payerPublicKey);
1899
+ }
1900
+ );
1901
+ await depositTx.prove();
1902
+ const sentDeposit = await depositTx.sign([payerPrivateKey]).send();
1903
+ depositTxHash = sentDeposit.hash ?? void 0;
1904
+ await sentDeposit.wait();
1905
+ await fetchAccount({ publicKey: zkAppPublicKey });
1906
+ await fetchAccount({ publicKey: payerPublicKey });
1907
+ }
1908
+ let finalState;
1909
+ try {
1910
+ finalState = Number(await readChannelState());
1911
+ } catch {
1912
+ finalState = opened ? Number(MINA_CHANNEL_STATE_OPEN) : Number(currentState);
1913
+ }
1914
+ if (opened && finalState === Number(MINA_CHANNEL_STATE_UNINITIALIZED)) {
1915
+ finalState = Number(MINA_CHANNEL_STATE_OPEN);
1916
+ }
1917
+ void AccountUpdate;
1918
+ let depositTotal;
1919
+ try {
1920
+ depositTotal = await readDepositTotal();
1921
+ } catch {
1922
+ depositTotal = 0n;
1923
+ }
1924
+ return {
1925
+ zkAppAddress: params.zkAppAddress,
1926
+ opened,
1927
+ initTxHash,
1928
+ depositTxHash,
1929
+ channelState: finalState,
1930
+ depositTotal
1931
+ };
1932
+ }
1933
+
1934
+ // src/channel/OnChainChannelClient.ts
1935
+ var TOKEN_NETWORK_ABI = [
1936
+ {
1937
+ name: "openChannel",
1938
+ type: "function",
1939
+ stateMutability: "nonpayable",
1940
+ inputs: [
1941
+ { name: "participant2", type: "address" },
1942
+ { name: "settlementTimeout", type: "uint256" }
1943
+ ],
1944
+ outputs: [{ type: "bytes32" }]
1945
+ },
1946
+ {
1947
+ name: "setTotalDeposit",
1948
+ type: "function",
1949
+ stateMutability: "nonpayable",
1950
+ inputs: [
1951
+ { name: "channelId", type: "bytes32" },
1952
+ { name: "participant", type: "address" },
1953
+ { name: "totalDeposit", type: "uint256" }
1954
+ ],
1955
+ outputs: []
1956
+ },
1957
+ {
1958
+ name: "channels",
1959
+ type: "function",
1960
+ stateMutability: "view",
1961
+ inputs: [{ type: "bytes32" }],
1962
+ outputs: [
1963
+ { name: "settlementTimeout", type: "uint256" },
1964
+ { name: "state", type: "uint8" },
1965
+ { name: "closedAt", type: "uint256" },
1966
+ { name: "openedAt", type: "uint256" },
1967
+ { name: "participant1", type: "address" },
1968
+ { name: "participant2", type: "address" }
1969
+ ]
1970
+ },
1971
+ {
1972
+ name: "ChannelOpened",
1973
+ type: "event",
1974
+ inputs: [
1975
+ { name: "channelId", type: "bytes32", indexed: true },
1976
+ { name: "participant1", type: "address", indexed: true },
1977
+ { name: "participant2", type: "address", indexed: true },
1978
+ { name: "settlementTimeout", type: "uint256", indexed: false }
1979
+ ]
1980
+ }
1981
+ ];
1982
+ var ERC20_ABI = [
1983
+ {
1984
+ name: "approve",
1985
+ type: "function",
1986
+ stateMutability: "nonpayable",
1987
+ inputs: [
1988
+ { name: "spender", type: "address" },
1989
+ { name: "amount", type: "uint256" }
1990
+ ],
1102
1991
  outputs: [{ type: "bool" }]
1103
1992
  },
1104
1993
  {
@@ -1112,7 +2001,7 @@ var ERC20_ABI = [
1112
2001
  outputs: [{ type: "uint256" }]
1113
2002
  }
1114
2003
  ];
1115
- var STATE_MAP = {
2004
+ var STATE_MAP2 = {
1116
2005
  0: "settled",
1117
2006
  1: "open",
1118
2007
  2: "closed",
@@ -1130,6 +2019,29 @@ var OnChainChannelClient = class {
1130
2019
  this.solanaConfig = config.solanaConfig;
1131
2020
  this.minaConfig = config.minaConfig;
1132
2021
  }
2022
+ /**
2023
+ * Late-binds the Solana channel config.
2024
+ *
2025
+ * `ToonClient.start()` derives the Solana Ed25519 keypair from the client's
2026
+ * mnemonic asynchronously (after this client is constructed), so the keypair
2027
+ * is injected here rather than at construction. Same keypair as the
2028
+ * registered Solana signer — guarantees the channel-open key and the
2029
+ * claim-signing key match.
2030
+ */
2031
+ setSolanaConfig(config) {
2032
+ this.solanaConfig = config;
2033
+ }
2034
+ /**
2035
+ * Late-binds the Mina channel config.
2036
+ *
2037
+ * Parallel to `setSolanaConfig`: `ToonClient.start()` derives the Mina private
2038
+ * key from the client's mnemonic asynchronously (after this client is
2039
+ * constructed), so the key is injected here rather than at construction. Same
2040
+ * key as the registered Mina signer.
2041
+ */
2042
+ setMinaConfig(config) {
2043
+ this.minaConfig = config;
2044
+ }
1133
2045
  /**
1134
2046
  * Parse chain identifier to extract chainId.
1135
2047
  * Format: "evm:{network}:{chainId}" e.g., "evm:anvil:31337"
@@ -1196,7 +2108,19 @@ var OnChainChannelClient = class {
1196
2108
  return this.openEvmChannel(params);
1197
2109
  }
1198
2110
  /**
1199
- * Opens a Solana payment channel (PDA creation).
2111
+ * Opens a REAL on-chain Solana payment channel.
2112
+ *
2113
+ * Derives the connector-parity channel PDA
2114
+ * (`[b"channel", min_pubkey, max_pubkey, token_mint]`), submits the
2115
+ * `initialize_channel` instruction (+ optional `deposit`) to the deployed
2116
+ * payment-channel program, and returns the base58 PDA as the channel id. That
2117
+ * PDA is what the claim carries as `channelAccount`, and the on-chain channel
2118
+ * is what the connector's `verifySolanaClaim` reads via
2119
+ * `provider.getChannelState` before accepting the claim.
2120
+ *
2121
+ * Mirrors `openEvmChannel`'s open(+deposit) structure. Idempotent: if the
2122
+ * channel account already exists on-chain, returns its PDA without
2123
+ * re-initializing.
1200
2124
  */
1201
2125
  async openSolanaChannel(params) {
1202
2126
  if (!this.solanaConfig) {
@@ -1204,23 +2128,72 @@ var OnChainChannelClient = class {
1204
2128
  "Solana channel config not provided \u2014 cannot open Solana channel"
1205
2129
  );
1206
2130
  }
1207
- const encoder = new TextEncoder();
1208
- const channelSeed = encoder.encode(
1209
- `channel:${toHex(this.solanaConfig.keypair).slice(0, 32)}:${params.peerAddress}:${Date.now()}`
2131
+ const cfg = this.solanaConfig;
2132
+ const payerSeed = cfg.keypair.slice(0, 32);
2133
+ const payerPubkey = base58Encode2(
2134
+ new Uint8Array(ed255192.getPublicKey(payerSeed))
1210
2135
  );
1211
- const channelIdBytes = new Uint8Array(
1212
- await crypto.subtle.digest("SHA-256", channelSeed)
2136
+ const tokenMint = params.token ?? cfg.tokenMint;
2137
+ if (!tokenMint) {
2138
+ throw new Error(
2139
+ "Solana channel requires a token mint (OpenChannelParams.token or solanaConfig.tokenMint)"
2140
+ );
2141
+ }
2142
+ if (!params.peerAddress) {
2143
+ throw new Error(
2144
+ "Solana channel requires peerAddress (apex settlement pubkey, base58)"
2145
+ );
2146
+ }
2147
+ const challengeDuration = BigInt(
2148
+ cfg.challengeDuration ?? params.settlementTimeout ?? 86400
1213
2149
  );
1214
- const channelId = "0x" + toHex(channelIdBytes);
1215
- this.channelContext.set(channelId, {
2150
+ const deposit = cfg.deposit ? {
2151
+ amount: BigInt(cfg.deposit.amount),
2152
+ payerTokenAccount: cfg.deposit.payerTokenAccount
2153
+ } : void 0;
2154
+ const { channelPDA } = await openSolanaChannel({
2155
+ rpcUrl: cfg.rpcUrl,
2156
+ programId: cfg.programId,
2157
+ tokenMint,
2158
+ payerSeed,
2159
+ payerPubkey,
2160
+ peerPubkey: params.peerAddress,
2161
+ challengeDuration,
2162
+ deposit
2163
+ });
2164
+ this.channelContext.set(channelPDA, {
1216
2165
  chain: params.chain,
1217
- tokenNetworkAddress: this.solanaConfig.programId
2166
+ tokenNetworkAddress: cfg.programId
1218
2167
  });
1219
- return { channelId, status: "opening" };
2168
+ return { channelId: channelPDA, status: "opening" };
1220
2169
  }
1221
2170
  /**
1222
- * Opens a Mina payment channel (zkApp state transition).
1223
- * Dynamically imports o1js to avoid bundle bloat.
2171
+ * Opens a REAL on-chain Mina payment channel on the deployed `PaymentChannel`
2172
+ * zkApp.
2173
+ *
2174
+ * The zkApp is deployed out-of-band (the operator/e2e harness deploys it
2175
+ * deterministically and advertises its B62 address). This client then calls
2176
+ * `initializeChannel` on that zkApp so its on-chain `channelState` becomes
2177
+ * `OPEN` — which is what the connector's `MinaPaymentChannelSDK.getChannelState`
2178
+ * reads to return status `'opened'` (claim verification otherwise fails with
2179
+ * `mina_claim_verification_failed`). The deployed zkApp address IS the channel
2180
+ * id: `MinaClaimMessage.zkAppAddress` is both the claim's channel identifier
2181
+ * AND the channel-hash preimage the off-chain proof binds to (see
2182
+ * `mina-payment-channel.ts`), so the channel-open id and the claim's channel id
2183
+ * are guaranteed identical.
2184
+ *
2185
+ * This is the Mina analog of `openSolanaChannel` (connector#105): the client
2186
+ * opens its own per-channel on-chain state (initialize + optional deposit). The
2187
+ * heavyweight o1js + `@toon-protocol/mina-zkapp` proof work is lazily imported
2188
+ * inside `openMinaChannelOnChain` so npm consumers who never open a Mina
2189
+ * channel don't pay the o1js cost.
2190
+ *
2191
+ * Idempotent: if the on-chain channel is already `OPEN`, the opener returns
2192
+ * without re-initializing.
2193
+ *
2194
+ * NOTE: full on-chain Mina SETTLE remains gated by the connector-side
2195
+ * settlement-executor (the same blocker that stops the Solana SETTLE); reaching
2196
+ * `opened` + a stored claim is parity with Solana.
1224
2197
  */
1225
2198
  async openMinaChannel(params) {
1226
2199
  if (!this.minaConfig) {
@@ -1228,19 +2201,41 @@ var OnChainChannelClient = class {
1228
2201
  "Mina channel config not provided \u2014 cannot open Mina channel"
1229
2202
  );
1230
2203
  }
1231
- const encoder = new TextEncoder();
1232
- const channelSeed = encoder.encode(
1233
- `channel:${this.minaConfig.privateKey.slice(0, 16)}:${params.peerAddress}:${Date.now()}`
1234
- );
1235
- const channelIdBytes = new Uint8Array(
1236
- await crypto.subtle.digest("SHA-256", channelSeed)
2204
+ const zkAppAddress = this.minaConfig.zkAppAddress;
2205
+ if (!zkAppAddress) {
2206
+ throw new Error(
2207
+ "Mina channel requires a deployed zkAppAddress (minaConfig.zkAppAddress)"
2208
+ );
2209
+ }
2210
+ if (!params.peerAddress) {
2211
+ throw new Error(
2212
+ "Mina channel requires peerAddress (apex Mina settlement B62) so the on-chain channel is opened two-party \u2014 the participant-form claim cannot settle against a single-party channel"
2213
+ );
2214
+ }
2215
+ const timeout = BigInt(
2216
+ this.minaConfig.challengeDuration ?? params.settlementTimeout ?? 86400
1237
2217
  );
1238
- const channelId = "0x" + toHex(channelIdBytes);
1239
- this.channelContext.set(channelId, {
2218
+ const deposit = this.minaConfig.deposit ? { amount: BigInt(this.minaConfig.deposit.amount) } : void 0;
2219
+ const openResult = await openMinaChannelOnChain({
2220
+ graphqlUrl: this.minaConfig.graphqlUrl,
2221
+ zkAppAddress,
2222
+ payerPrivateKey: this.minaConfig.privateKey,
2223
+ // params.peerAddress is the apex Mina settlement B62 pubkey (participantB).
2224
+ peerPublicKey: params.peerAddress,
2225
+ timeout,
2226
+ tokenId: this.minaConfig.tokenId,
2227
+ deposit,
2228
+ networkId: this.minaConfig.networkId
2229
+ });
2230
+ this.channelContext.set(zkAppAddress, {
1240
2231
  chain: params.chain,
1241
- tokenNetworkAddress: this.minaConfig.zkAppAddress
2232
+ tokenNetworkAddress: zkAppAddress
1242
2233
  });
1243
- return { channelId, status: "opening" };
2234
+ return {
2235
+ channelId: zkAppAddress,
2236
+ status: "opening",
2237
+ depositTotal: openResult.depositTotal
2238
+ };
1244
2239
  }
1245
2240
  /**
1246
2241
  * Opens an EVM payment channel on-chain.
@@ -1336,6 +2331,17 @@ var OnChainChannelClient = class {
1336
2331
  `No context for channel "${channelId}". Channel must be opened via this client first.`
1337
2332
  );
1338
2333
  }
2334
+ if (context.chain.split(":")[0] === "mina") {
2335
+ return { channelId, status: "open", chain: context.chain };
2336
+ }
2337
+ if (context.chain.split(":")[0] === "solana" && this.solanaConfig) {
2338
+ const account = await getChannelAccountState(
2339
+ this.solanaConfig.rpcUrl,
2340
+ channelId
2341
+ );
2342
+ const status2 = !account.exists ? "settled" : account.state === "opened" ? "open" : account.state === "closed" ? "closed" : "settled";
2343
+ return { channelId, status: status2, chain: context.chain };
2344
+ }
1339
2345
  const { publicClient } = this.createClients(context.chain);
1340
2346
  const result = await publicClient.readContract({
1341
2347
  address: context.tokenNetworkAddress,
@@ -1344,7 +2350,7 @@ var OnChainChannelClient = class {
1344
2350
  args: [channelId]
1345
2351
  });
1346
2352
  const [, state] = result;
1347
- const status = STATE_MAP[state] ?? "settled";
2353
+ const status = STATE_MAP2[state] ?? "settled";
1348
2354
  return {
1349
2355
  channelId,
1350
2356
  status,
@@ -1354,8 +2360,8 @@ var OnChainChannelClient = class {
1354
2360
  };
1355
2361
 
1356
2362
  // src/signing/evm-signer.ts
1357
- import { privateKeyToAccount } from "viem/accounts";
1358
- import { toHex as toHex2 } from "viem";
2363
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
2364
+ import { toHex as toHex3 } from "viem";
1359
2365
  function getBalanceProofDomain(chainId, tokenNetworkAddress) {
1360
2366
  return {
1361
2367
  name: "TokenNetwork",
@@ -1382,11 +2388,11 @@ var EvmSigner = class {
1382
2388
  constructor(privateKey) {
1383
2389
  let hexKey;
1384
2390
  if (privateKey instanceof Uint8Array) {
1385
- hexKey = toHex2(privateKey);
2391
+ hexKey = toHex3(privateKey);
1386
2392
  } else {
1387
2393
  hexKey = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
1388
2394
  }
1389
- this._account = privateKeyToAccount(hexKey);
2395
+ this._account = privateKeyToAccount2(hexKey);
1390
2396
  }
1391
2397
  /** Derived 0x EVM address */
1392
2398
  get address() {
@@ -1465,24 +2471,99 @@ var EvmSigner = class {
1465
2471
  }
1466
2472
  };
1467
2473
 
2474
+ // src/transport/index.ts
2475
+ function isAnyoneHost(url) {
2476
+ if (!url) return false;
2477
+ try {
2478
+ const withScheme = /:\/\//.test(url) ? url : `ws://${url}`;
2479
+ const host = new URL(withScheme).hostname.toLowerCase();
2480
+ return host.endsWith(".anyone");
2481
+ } catch {
2482
+ return false;
2483
+ }
2484
+ }
2485
+ async function resolveTransport(transport, originalBtpUrl, originalConnectorUrl, managedProxyOptions) {
2486
+ const hasExplicitProxy = !!transport && (transport.type === "socks5" || transport.type === "gateway");
2487
+ const envProxy = process.env["ANYONE_PROXY_URLS"];
2488
+ if (!hasExplicitProxy && managedProxyOptions?.managedAnonProxy !== false && !envProxy && isAnyoneHost(originalBtpUrl)) {
2489
+ const { startManagedAnonProxy: startManagedAnonProxy2 } = await import("./anon-proxy-W3KMM7GU.js");
2490
+ const { createSocks5WebSocketFactory, createSocks5Fetch } = await import("./socks5-WTJBYGME.js");
2491
+ const proxy = await startManagedAnonProxy2({
2492
+ ...managedProxyOptions?.managedAnonSocksPort !== void 0 ? { socksPort: managedProxyOptions.managedAnonSocksPort } : {}
2493
+ });
2494
+ try {
2495
+ return {
2496
+ createWebSocket: createSocks5WebSocketFactory(proxy.socksProxy),
2497
+ httpClient: createSocks5Fetch(proxy.socksProxy),
2498
+ stopManagedProxy: proxy.stop
2499
+ };
2500
+ } catch (err) {
2501
+ await proxy.stop();
2502
+ throw err;
2503
+ }
2504
+ }
2505
+ if (!transport || transport.type === "direct") {
2506
+ return {};
2507
+ }
2508
+ if (transport.type === "socks5") {
2509
+ const {
2510
+ createSocks5WebSocketFactory,
2511
+ createSocks5Fetch,
2512
+ probeSocks5Proxy
2513
+ } = await import("./socks5-WTJBYGME.js");
2514
+ await probeSocks5Proxy(transport.socksProxy);
2515
+ return {
2516
+ createWebSocket: createSocks5WebSocketFactory(transport.socksProxy),
2517
+ httpClient: createSocks5Fetch(transport.socksProxy)
2518
+ };
2519
+ }
2520
+ if (transport.type === "gateway") {
2521
+ const { rewriteUrlsForGateway } = await import("./gateway-QOK47RKS.js");
2522
+ const rewritten = rewriteUrlsForGateway(
2523
+ transport.gatewayUrl,
2524
+ originalBtpUrl,
2525
+ originalConnectorUrl
2526
+ );
2527
+ return {
2528
+ btpUrl: rewritten.btpUrl,
2529
+ connectorUrl: rewritten.connectorUrl
2530
+ };
2531
+ }
2532
+ throw new Error(
2533
+ `Unknown transport type: "${transport.type}"`
2534
+ );
2535
+ }
2536
+
1468
2537
  // src/modes/http.ts
1469
2538
  async function initializeHttpMode(config) {
1470
- const connectorUrl = config.connectorUrl;
2539
+ const transport = await resolveTransport(
2540
+ config.transport,
2541
+ config.btpUrl,
2542
+ config.connectorUrl,
2543
+ {
2544
+ ...config.managedAnonProxy !== void 0 ? { managedAnonProxy: config.managedAnonProxy } : {},
2545
+ ...config.managedAnonSocksPort !== void 0 ? { managedAnonSocksPort: config.managedAnonSocksPort } : {}
2546
+ }
2547
+ );
2548
+ const effectiveBtpUrl = transport.btpUrl ?? config.btpUrl;
2549
+ const effectiveConnectorUrl = transport.connectorUrl ?? config.connectorUrl;
1471
2550
  const settlementInfo = buildSettlementInfo(config);
1472
2551
  let btpClient = null;
1473
- if (config.btpUrl) {
2552
+ if (effectiveBtpUrl) {
1474
2553
  btpClient = new BtpRuntimeClient({
1475
- btpUrl: config.btpUrl,
2554
+ btpUrl: effectiveBtpUrl,
1476
2555
  peerId: config.btpPeerId ?? `client`,
1477
- authToken: config.btpAuthToken ?? ""
2556
+ authToken: config.btpAuthToken ?? "",
2557
+ createWebSocket: transport.createWebSocket
1478
2558
  });
1479
2559
  await btpClient.connect();
1480
2560
  }
1481
2561
  const runtimeClient = btpClient ?? new HttpRuntimeClient({
1482
- connectorUrl,
2562
+ connectorUrl: effectiveConnectorUrl,
1483
2563
  timeout: config.queryTimeout,
1484
2564
  maxRetries: config.maxRetries,
1485
- retryDelay: config.retryDelay
2565
+ retryDelay: config.retryDelay,
2566
+ httpClient: transport.httpClient
1486
2567
  });
1487
2568
  let onChainChannelClient = null;
1488
2569
  if (config.chainRpcUrls) {
@@ -1517,19 +2598,395 @@ async function initializeHttpMode(config) {
1517
2598
  if (onChainChannelClient) {
1518
2599
  bootstrapService.setChannelClient(onChainChannelClient);
1519
2600
  }
1520
- const discoveryTracker = createDiscoveryTracker({
1521
- secretKey: config.secretKey,
1522
- settlementInfo
1523
- });
1524
- return {
1525
- bootstrapService,
1526
- discoveryTracker,
1527
- runtimeClient,
1528
- adminClient: null,
1529
- btpClient,
1530
- onChainChannelClient
1531
- };
1532
- }
2601
+ const discoveryTracker = createDiscoveryTracker({
2602
+ secretKey: config.secretKey,
2603
+ settlementInfo
2604
+ });
2605
+ return {
2606
+ bootstrapService,
2607
+ discoveryTracker,
2608
+ runtimeClient,
2609
+ adminClient: null,
2610
+ btpClient,
2611
+ onChainChannelClient,
2612
+ // Teardown handle for a managed `anon` proxy this init STARTED (undefined
2613
+ // for explicit-proxy/direct/gateway). ToonClient.stop() invokes it.
2614
+ stopManagedProxy: transport.stopManagedProxy
2615
+ };
2616
+ }
2617
+
2618
+ // src/signing/solana-signer.ts
2619
+ import { ed25519 as ed255193 } from "@noble/curves/ed25519.js";
2620
+ import { base58Encode as base58Encode3 } from "@toon-protocol/core";
2621
+ var SolanaSigner = class {
2622
+ chainType = "solana";
2623
+ /** 32-byte Ed25519 seed. */
2624
+ privateKey;
2625
+ pubkeyBase58Cache;
2626
+ /**
2627
+ * @param privateKey - 32-byte Ed25519 seed (e.g. `identity.solana.secretKey.slice(0, 32)`).
2628
+ * @param publicKeyBase58 - Optional base58 public key (e.g. `identity.solana.publicKey`).
2629
+ * When omitted it is derived lazily from `privateKey`.
2630
+ */
2631
+ constructor(privateKey, publicKeyBase58) {
2632
+ if (privateKey.length !== 32) {
2633
+ throw new Error(
2634
+ `SolanaSigner requires a 32-byte Ed25519 seed, got ${privateKey.length} bytes`
2635
+ );
2636
+ }
2637
+ this.privateKey = privateKey;
2638
+ this.pubkeyBase58Cache = publicKeyBase58;
2639
+ }
2640
+ ensurePublicKey() {
2641
+ if (this.pubkeyBase58Cache) return this.pubkeyBase58Cache;
2642
+ const pk = ed255193.getPublicKey(this.privateKey);
2643
+ this.pubkeyBase58Cache = base58Encode3(new Uint8Array(pk));
2644
+ return this.pubkeyBase58Cache;
2645
+ }
2646
+ get signerIdentifier() {
2647
+ return this.pubkeyBase58Cache ?? "uninitialized";
2648
+ }
2649
+ async signBalanceProof(params) {
2650
+ if (params.metadata.chainType !== "solana") {
2651
+ throw new Error(
2652
+ `SolanaSigner cannot sign for chain type: ${params.metadata.chainType}`
2653
+ );
2654
+ }
2655
+ const base58 = this.ensurePublicKey();
2656
+ const message = buildBalanceProofMessage(
2657
+ params.channelId,
2658
+ BigInt(params.nonce),
2659
+ params.transferredAmount
2660
+ );
2661
+ const signature = ed255193.sign(message, this.privateKey);
2662
+ const signatureHex = "0x" + toHex2(new Uint8Array(signature));
2663
+ return {
2664
+ channelId: params.channelId,
2665
+ nonce: params.nonce,
2666
+ transferredAmount: params.transferredAmount,
2667
+ lockedAmount: params.lockedAmount,
2668
+ locksRoot: params.locksRoot,
2669
+ signature: signatureHex,
2670
+ signerAddress: base58,
2671
+ chainId: 0,
2672
+ tokenNetworkAddress: params.metadata.programId,
2673
+ recipient: params.recipient
2674
+ };
2675
+ }
2676
+ buildClaimMessage(proof, senderId) {
2677
+ const sigHex = proof.signature.startsWith("0x") ? proof.signature.slice(2) : proof.signature;
2678
+ const sigBytes = Uint8Array.from(
2679
+ sigHex.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? []
2680
+ );
2681
+ const signatureBase64 = Buffer.from(sigBytes).toString("base64");
2682
+ const claim = {
2683
+ version: "1.0",
2684
+ blockchain: "solana",
2685
+ messageId: crypto.randomUUID(),
2686
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, ".000Z"),
2687
+ senderId,
2688
+ // channelId IS the base58 channel PDA -> connector's channelAccount.
2689
+ channelAccount: proof.channelId,
2690
+ nonce: proof.nonce,
2691
+ transferredAmount: proof.transferredAmount.toString(),
2692
+ signature: signatureBase64,
2693
+ signerPublicKey: this.pubkeyBase58Cache ?? proof.signerAddress,
2694
+ programId: proof.tokenNetworkAddress
2695
+ };
2696
+ return claim;
2697
+ }
2698
+ };
2699
+
2700
+ // src/signing/mina-signer.ts
2701
+ import { hexToMinaBase58PrivateKey as hexToMinaBase58PrivateKey3 } from "@toon-protocol/core";
2702
+ import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
2703
+ import { bytesToHex } from "@noble/hashes/utils.js";
2704
+
2705
+ // src/channel/mina-payment-channel.ts
2706
+ var cachedBindings = null;
2707
+ async function loadMinaPaymentChannelBindings() {
2708
+ if (cachedBindings) return cachedBindings;
2709
+ const specifier = "mina-signer";
2710
+ const lib = await import(
2711
+ /* @vite-ignore */
2712
+ specifier
2713
+ );
2714
+ const Client = "default" in lib ? lib.default : lib;
2715
+ const resolveFn = import.meta.resolve;
2716
+ let mainUrl;
2717
+ if (typeof resolveFn === "function") {
2718
+ mainUrl = resolveFn(specifier);
2719
+ } else {
2720
+ const { createRequire } = await import("module");
2721
+ const { pathToFileURL } = await import("url");
2722
+ mainUrl = pathToFileURL(
2723
+ createRequire(import.meta.url).resolve(specifier)
2724
+ ).href;
2725
+ }
2726
+ const minaSignerDir = new URL("./", mainUrl);
2727
+ const poseidonUrl = new URL("../bindings/crypto/poseidon.js", minaSignerDir).href;
2728
+ const signatureUrl = new URL("./src/signature.js", minaSignerDir).href;
2729
+ const curveUrl = new URL("./src/curve-bigint.js", minaSignerDir).href;
2730
+ const [poseidonMod, signatureMod, curveMod] = await Promise.all([
2731
+ import(
2732
+ /* @vite-ignore */
2733
+ poseidonUrl
2734
+ ),
2735
+ import(
2736
+ /* @vite-ignore */
2737
+ signatureUrl
2738
+ ),
2739
+ import(
2740
+ /* @vite-ignore */
2741
+ curveUrl
2742
+ )
2743
+ ]);
2744
+ cachedBindings = {
2745
+ Client,
2746
+ Poseidon: poseidonMod.Poseidon,
2747
+ Signature: signatureMod.Signature,
2748
+ PublicKey: curveMod.PublicKey
2749
+ };
2750
+ return cachedBindings;
2751
+ }
2752
+ function minaBalanceCommitment(poseidon, balanceA, balanceB, salt) {
2753
+ return poseidon.hash([balanceA, balanceB, salt]);
2754
+ }
2755
+ function minaChannelHashField(poseidon, publicKeyCodec, zkAppAddress) {
2756
+ const zkAppPubKey = publicKeyCodec.fromBase58(zkAppAddress);
2757
+ return poseidon.hash([zkAppPubKey.x]);
2758
+ }
2759
+ function minaParticipantChannelHashField(poseidon, publicKeyCodec, participantA_B62, participantB_B62, channelNonce) {
2760
+ const a = publicKeyCodec.fromBase58(participantA_B62);
2761
+ const b = publicKeyCodec.fromBase58(participantB_B62);
2762
+ return poseidon.hash([a.x, b.x, channelNonce]);
2763
+ }
2764
+ async function buildMinaPaymentChannelProof(params) {
2765
+ const { Client, Poseidon, Signature, PublicKey } = await loadMinaPaymentChannelBindings();
2766
+ const client = new Client({ network: "devnet" });
2767
+ const signerPublicKey = params.signerPublicKey ?? client.derivePublicKey(params.minaPrivateKeyBase58);
2768
+ const commitment = minaBalanceCommitment(
2769
+ Poseidon,
2770
+ params.balanceA,
2771
+ params.balanceB,
2772
+ params.salt
2773
+ );
2774
+ const channelHashField = params.participantA && params.participantB ? minaParticipantChannelHashField(
2775
+ Poseidon,
2776
+ PublicKey,
2777
+ params.participantA,
2778
+ params.participantB,
2779
+ params.channelNonce ?? 0n
2780
+ ) : minaChannelHashField(Poseidon, PublicKey, params.zkAppAddress);
2781
+ const message = [commitment, params.nonce, channelHashField];
2782
+ const signed = client.signFields(message, params.minaPrivateKeyBase58);
2783
+ const { r, s } = Signature.fromBase58(signed.signature);
2784
+ const proofObject = {
2785
+ commitment: commitment.toString(),
2786
+ signature: { r: r.toString(), s: s.toString() },
2787
+ nonce: params.nonce.toString(),
2788
+ signerPublicKey
2789
+ };
2790
+ const proofJson = JSON.stringify(proofObject);
2791
+ const encoding = params.proofEncoding ?? "base64";
2792
+ const proof = encoding === "base64" ? Buffer.from(proofJson, "utf8").toString("base64") : proofJson;
2793
+ return {
2794
+ balanceCommitment: commitment.toString(),
2795
+ proof,
2796
+ salt: params.salt.toString(),
2797
+ signerPublicKey
2798
+ };
2799
+ }
2800
+
2801
+ // src/channel/mina-deposit.ts
2802
+ var DEPOSIT_TOTAL_STATE_INDEX = 4;
2803
+ async function readMinaDepositTotal(graphqlUrl, zkAppAddress, fetchImpl = fetch) {
2804
+ const query = "query($pk:String!){account(publicKey:$pk){zkappState}}";
2805
+ const res = await fetchImpl(graphqlUrl, {
2806
+ method: "POST",
2807
+ headers: { "content-type": "application/json" },
2808
+ body: JSON.stringify({ query, variables: { pk: zkAppAddress } })
2809
+ });
2810
+ if (!res.ok) {
2811
+ throw new Error(`Mina GraphQL request failed: HTTP ${res.status}`);
2812
+ }
2813
+ const json = await res.json();
2814
+ if (json.errors && json.errors.length > 0) {
2815
+ throw new Error(
2816
+ `Mina GraphQL error: ${json.errors[0]?.message ?? "unknown"}`
2817
+ );
2818
+ }
2819
+ const state = json.data?.account?.zkappState;
2820
+ if (!state || state.length <= DEPOSIT_TOTAL_STATE_INDEX) {
2821
+ throw new Error(
2822
+ `Mina zkApp ${zkAppAddress} has no readable zkappState (account not found or not a zkApp)`
2823
+ );
2824
+ }
2825
+ return BigInt(state[DEPOSIT_TOTAL_STATE_INDEX]);
2826
+ }
2827
+
2828
+ // src/signing/mina-signer.ts
2829
+ var DEFAULT_MINA_TOKEN_ID = "MINA";
2830
+ var MINA_CLAIM_NETWORK = "devnet";
2831
+ function deriveMinaSalt(zkAppAddress, nonce) {
2832
+ const digestHex = bytesToHex(
2833
+ sha2562(new TextEncoder().encode(`mina-pc-salt:${zkAppAddress}:${nonce}`))
2834
+ );
2835
+ const salt = BigInt("0x" + digestHex.slice(0, 60));
2836
+ return salt === 0n ? 1n : salt;
2837
+ }
2838
+ var MinaSigner = class {
2839
+ chainType = "mina";
2840
+ /** Big-endian hex scalar (or already-`EK…` base58) Mina private key. */
2841
+ privateKey;
2842
+ publicKeyBase58;
2843
+ depositReader;
2844
+ /** Per-zkApp `depositTotal` cache (deposits are rare; the connector re-reads). */
2845
+ depositCache = /* @__PURE__ */ new Map();
2846
+ /**
2847
+ * @param privateKey - Mina private key as big-endian hex scalar (the form
2848
+ * `deriveFullIdentity()` emits, `identity.mina.privateKey`) or an `EK…`
2849
+ * base58 key. Converted to the base58check form mina-signer requires.
2850
+ * @param publicKeyBase58 - Optional base58 public key (e.g.
2851
+ * `identity.mina.publicKey`). When omitted it is derived during signing.
2852
+ * @param options - Optional on-chain `depositTotal` resolution (graphqlUrl or
2853
+ * an injected reader) so claims conserve balances on funded zkApps.
2854
+ */
2855
+ constructor(privateKey, publicKeyBase58, options) {
2856
+ this.privateKey = privateKey;
2857
+ this.publicKeyBase58 = publicKeyBase58;
2858
+ if (options?.depositReader) {
2859
+ this.depositReader = options.depositReader;
2860
+ } else if (options?.graphqlUrl) {
2861
+ const url = options.graphqlUrl;
2862
+ this.depositReader = (zkAppAddress) => readMinaDepositTotal(url, zkAppAddress);
2863
+ }
2864
+ }
2865
+ /**
2866
+ * Resolve the channel's on-chain `depositTotal`, caching per zkApp. Returns
2867
+ * `undefined` when no reader is configured or the read fails — callers then
2868
+ * fall back to the legacy `balanceB = 0` commitment.
2869
+ */
2870
+ async resolveDepositTotal(zkAppAddress) {
2871
+ if (this.depositCache.has(zkAppAddress)) {
2872
+ return this.depositCache.get(zkAppAddress);
2873
+ }
2874
+ if (!this.depositReader) return void 0;
2875
+ try {
2876
+ const depositTotal = await this.depositReader(zkAppAddress);
2877
+ this.depositCache.set(zkAppAddress, depositTotal);
2878
+ return depositTotal;
2879
+ } catch {
2880
+ return void 0;
2881
+ }
2882
+ }
2883
+ get signerIdentifier() {
2884
+ return this.publicKeyBase58 ?? "uninitialized";
2885
+ }
2886
+ /** Derive this signer's B62 public key from its (base58) private key. */
2887
+ async deriveOwnPublicKey(minaPrivateKeyBase58) {
2888
+ const { Client } = await loadMinaPaymentChannelBindings();
2889
+ return new Client({ network: MINA_CLAIM_NETWORK }).derivePublicKey(
2890
+ minaPrivateKeyBase58
2891
+ );
2892
+ }
2893
+ async signBalanceProof(params) {
2894
+ if (params.metadata.chainType !== "mina") {
2895
+ throw new Error(
2896
+ `MinaSigner cannot sign for chain type: ${params.metadata.chainType}`
2897
+ );
2898
+ }
2899
+ const zkAppAddress = params.channelId || params.metadata.zkAppAddress;
2900
+ if (!zkAppAddress) {
2901
+ throw new Error(
2902
+ "MinaSigner requires a zkAppAddress (channel id) to sign a balance proof"
2903
+ );
2904
+ }
2905
+ const minaPrivateKey = hexToMinaBase58PrivateKey3(this.privateKey);
2906
+ const tokenId = params.metadata.tokenId ?? DEFAULT_MINA_TOKEN_ID;
2907
+ const salt = deriveMinaSalt(zkAppAddress, params.nonce);
2908
+ const clientPubKey = this.publicKeyBase58 ?? await this.deriveOwnPublicKey(minaPrivateKey);
2909
+ this.publicKeyBase58 = clientPubKey;
2910
+ const apexPubKey = params.recipient && /^B62[a-zA-Z0-9]{40,60}$/.test(params.recipient) ? params.recipient : void 0;
2911
+ const depositTotal = params.depositTotal ?? await this.resolveDepositTotal(zkAppAddress);
2912
+ let balanceB = 0n;
2913
+ if (depositTotal != null && depositTotal > 0n) {
2914
+ if (params.transferredAmount > depositTotal) {
2915
+ throw new Error(
2916
+ `Mina claim balanceA (${params.transferredAmount}) exceeds on-chain depositTotal (${depositTotal}) \u2014 cannot conserve balances`
2917
+ );
2918
+ }
2919
+ balanceB = depositTotal - params.transferredAmount;
2920
+ }
2921
+ const built = await buildMinaPaymentChannelProof({
2922
+ zkAppAddress,
2923
+ minaPrivateKeyBase58: minaPrivateKey,
2924
+ signerPublicKey: clientPubKey,
2925
+ // Recipient-credit (unidirectional): party A carries the cumulative amount;
2926
+ // party B carries the funder's remaining balance (depositTotal − balanceA)
2927
+ // so the signed commitment conserves and the on-chain claimFromChannel
2928
+ // signatureA check passes. `signatureB` remains apex-co-signed downstream.
2929
+ balanceA: params.transferredAmount,
2930
+ balanceB,
2931
+ salt,
2932
+ nonce: BigInt(params.nonce),
2933
+ // Participant-form channelHash (on-chain-settleable) when the apex pubkey
2934
+ // is known; otherwise the legacy zkApp-x form (off-chain-store only).
2935
+ ...apexPubKey ? { participantA: clientPubKey, participantB: apexPubKey } : {}
2936
+ });
2937
+ this.publicKeyBase58 = built.signerPublicKey;
2938
+ return {
2939
+ channelId: zkAppAddress,
2940
+ nonce: params.nonce,
2941
+ transferredAmount: params.transferredAmount,
2942
+ lockedAmount: params.lockedAmount,
2943
+ locksRoot: params.locksRoot,
2944
+ // `signature` is unused on the Mina wire (the proof carries the Schnorr
2945
+ // signature); keep the base64 proof here too for symmetry / debugging.
2946
+ signature: built.proof,
2947
+ signerAddress: built.signerPublicKey,
2948
+ chainId: 0,
2949
+ tokenNetworkAddress: zkAppAddress,
2950
+ recipient: params.recipient,
2951
+ mina: {
2952
+ balanceCommitment: built.balanceCommitment,
2953
+ proof: built.proof,
2954
+ salt: built.salt,
2955
+ tokenId
2956
+ }
2957
+ };
2958
+ }
2959
+ buildClaimMessage(proof, senderId) {
2960
+ if (!proof.mina) {
2961
+ throw new Error(
2962
+ "MinaSigner.buildClaimMessage requires a Mina-signed proof (missing `mina` fields)"
2963
+ );
2964
+ }
2965
+ const claim = {
2966
+ version: "1.0",
2967
+ blockchain: "mina",
2968
+ messageId: crypto.randomUUID(),
2969
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, ".000Z"),
2970
+ senderId,
2971
+ zkAppAddress: proof.channelId,
2972
+ tokenId: proof.mina.tokenId,
2973
+ balanceCommitment: proof.mina.balanceCommitment,
2974
+ nonce: proof.nonce,
2975
+ proof: proof.mina.proof,
2976
+ salt: proof.mina.salt,
2977
+ transferredAmount: proof.transferredAmount.toString(),
2978
+ // Surface the signer's Mina pubkey top-level (it is also embedded in the
2979
+ // base64 `proof`). The connector's SettlementExecutor reads
2980
+ // `latestClaim.signerPublicKey` to resolve participant keys for the
2981
+ // on-chain claimFromChannel on an inbound/externally-opened channel;
2982
+ // without it the Mina SDK throws ACCOUNT_NOT_FOUND. `signerAddress`
2983
+ // carries the B62 base58 pubkey for Mina proofs (see MinaSigner.sign*).
2984
+ signerPublicKey: proof.signerAddress,
2985
+ network: MINA_CLAIM_NETWORK
2986
+ };
2987
+ return claim;
2988
+ }
2989
+ };
1533
2990
 
1534
2991
  // src/channel/ChannelManager.ts
1535
2992
  var ChannelManager = class {
@@ -1629,7 +3086,11 @@ var ChannelManager = class {
1629
3086
  chainType: negotiation.chainType,
1630
3087
  chainId: typeof negotiation.chainId === "number" ? negotiation.chainId : 0,
1631
3088
  tokenNetworkAddress: negotiation.tokenNetwork ?? "",
1632
- tokenAddress: negotiation.tokenAddress
3089
+ tokenAddress: negotiation.tokenAddress,
3090
+ recipient: negotiation.settlementAddress,
3091
+ // On-chain depositTotal (Mina only) — needed so the Mina signer binds
3092
+ // balanceB = depositTotal − balanceA (connector#133).
3093
+ depositTotal: result.depositTotal
1633
3094
  });
1634
3095
  this.peerChannels.set(peerId, result.channelId);
1635
3096
  return result.channelId;
@@ -1667,7 +3128,9 @@ var ChannelManager = class {
1667
3128
  chainType: chainContext?.chainType ?? "evm",
1668
3129
  chainId: cId,
1669
3130
  tokenNetworkAddress: tnAddr,
1670
- tokenAddress: chainContext?.tokenAddress
3131
+ tokenAddress: chainContext?.tokenAddress,
3132
+ recipient: chainContext?.recipient,
3133
+ depositTotal: chainContext?.depositTotal
1671
3134
  });
1672
3135
  return;
1673
3136
  }
@@ -1678,7 +3141,9 @@ var ChannelManager = class {
1678
3141
  chainType: chainContext?.chainType ?? "evm",
1679
3142
  chainId: cId,
1680
3143
  tokenNetworkAddress: tnAddr,
1681
- tokenAddress: chainContext?.tokenAddress
3144
+ tokenAddress: chainContext?.tokenAddress,
3145
+ recipient: chainContext?.recipient,
3146
+ depositTotal: chainContext?.depositTotal
1682
3147
  });
1683
3148
  }
1684
3149
  /**
@@ -1708,6 +3173,11 @@ var ChannelManager = class {
1708
3173
  }
1709
3174
  const signer = this.chainSigners.get(tracking.chainType);
1710
3175
  if (signer && tracking.chainType !== "evm") {
3176
+ if (!tracking.recipient) {
3177
+ throw new Error(
3178
+ `Channel "${channelId}" (${tracking.chainType}) has no recipient settlement address; cannot sign a Solana/Mina balance proof. Ensure the peer negotiation supplied a settlementAddress.`
3179
+ );
3180
+ }
1711
3181
  const metadata = this.buildMetadata(tracking);
1712
3182
  return signer.signBalanceProof({
1713
3183
  channelId,
@@ -1715,7 +3185,13 @@ var ChannelManager = class {
1715
3185
  transferredAmount: tracking.cumulativeAmount,
1716
3186
  lockedAmount: 0n,
1717
3187
  locksRoot: "0x0000000000000000000000000000000000000000000000000000000000000000",
1718
- metadata
3188
+ recipient: tracking.recipient,
3189
+ metadata,
3190
+ // On-chain depositTotal captured at open time (#220) — the Mina signer
3191
+ // binds balanceB = depositTotal − balanceA (connector#133); the Solana
3192
+ // signer ignores it. When undefined (resume / idempotent re-open) the
3193
+ // Mina signer self-resolves it from chain (#223).
3194
+ depositTotal: tracking.depositTotal
1719
3195
  });
1720
3196
  }
1721
3197
  if (!this.evmSigner) {
@@ -1833,6 +3309,20 @@ var ToonClient = class {
1833
3309
  config;
1834
3310
  state = null;
1835
3311
  evmSigner;
3312
+ solanaSigner;
3313
+ /**
3314
+ * Ed25519 signing seed (32 bytes) derived from the mnemonic for the Solana
3315
+ * identity. Retained so `start()` can inject it into the on-chain channel
3316
+ * client's Solana config (same key as `solanaSigner`).
3317
+ */
3318
+ solanaSeed;
3319
+ minaSigner;
3320
+ /**
3321
+ * Mina private key (big-endian hex scalar, as `deriveFullIdentity` emits)
3322
+ * derived from the mnemonic. Retained so `start()` can inject it into the
3323
+ * on-chain channel client's Mina config (same key as `minaSigner`).
3324
+ */
3325
+ minaPrivateKey;
1836
3326
  channelManager;
1837
3327
  peerNegotiations = /* @__PURE__ */ new Map();
1838
3328
  /**
@@ -1854,8 +3344,8 @@ var ToonClient = class {
1854
3344
  * @returns Object with secretKey (Uint8Array) and pubkey (hex string)
1855
3345
  */
1856
3346
  static generateKeypair() {
1857
- const secretKey = generateSecretKey2();
1858
- const pubkey = getPublicKey(secretKey);
3347
+ const secretKey = generateSecretKey3();
3348
+ const pubkey = getPublicKey2(secretKey);
1859
3349
  return { secretKey, pubkey };
1860
3350
  }
1861
3351
  /**
@@ -1863,7 +3353,15 @@ var ToonClient = class {
1863
3353
  * Works before start() is called.
1864
3354
  */
1865
3355
  getPublicKey() {
1866
- return getPublicKey(this.config.secretKey);
3356
+ return getPublicKey2(this.config.secretKey);
3357
+ }
3358
+ /**
3359
+ * Per-chain settlement readiness for the configured `network` tier, mirroring
3360
+ * the townhouse node's status. Returns `undefined` when no named `network` is
3361
+ * set (or `network: 'custom'`), since there is no preset tier to report on.
3362
+ */
3363
+ getNetworkStatus() {
3364
+ return getNetworkStatus(this.config);
1867
3365
  }
1868
3366
  /**
1869
3367
  * Gets the EVM address derived from the Nostr secret key (or explicit evmPrivateKey override).
@@ -1871,6 +3369,46 @@ var ToonClient = class {
1871
3369
  getEvmAddress() {
1872
3370
  return this.evmSigner?.address;
1873
3371
  }
3372
+ /**
3373
+ * Gets the Solana (base58) address, when the client was constructed from a
3374
+ * `mnemonic`. Available only AFTER `start()` (Solana keys are derived
3375
+ * asynchronously). Returns undefined otherwise.
3376
+ */
3377
+ getSolanaAddress() {
3378
+ return this.solanaSigner?.signerIdentifier;
3379
+ }
3380
+ /**
3381
+ * Gets the Mina (base58) address, when the client was constructed from a
3382
+ * `mnemonic` AND `mina-signer` is installed. Available only AFTER `start()`.
3383
+ * Returns undefined otherwise.
3384
+ */
3385
+ getMinaAddress() {
3386
+ return this.minaSigner?.signerIdentifier;
3387
+ }
3388
+ /**
3389
+ * Derive the Solana/Mina keys from the mnemonic and register their signers on
3390
+ * the ChannelManager. Mirrors how the EVM signer is wired, but for the
3391
+ * non-secp256k1 chains. Skips any chain whose optional dependency is missing.
3392
+ */
3393
+ async registerMnemonicChainSigners(mnemonic, accountIndex = 0) {
3394
+ if (!this.channelManager) return;
3395
+ const identity = await deriveFullIdentity(mnemonic, accountIndex);
3396
+ if (identity.solana.publicKey) {
3397
+ const seed = identity.solana.secretKey.slice(0, 32);
3398
+ this.solanaSeed = seed;
3399
+ this.solanaSigner = new SolanaSigner(seed, identity.solana.publicKey);
3400
+ this.channelManager.registerChainSigner("solana", this.solanaSigner);
3401
+ }
3402
+ if (identity.mina.publicKey) {
3403
+ this.minaPrivateKey = identity.mina.privateKey;
3404
+ this.minaSigner = new MinaSigner(
3405
+ identity.mina.privateKey,
3406
+ identity.mina.publicKey,
3407
+ this.config.minaChannel?.graphqlUrl ? { graphqlUrl: this.config.minaChannel.graphqlUrl } : void 0
3408
+ );
3409
+ this.channelManager.registerChainSigner("mina", this.minaSigner);
3410
+ }
3411
+ }
1874
3412
  /**
1875
3413
  * Starts the ToonClient.
1876
3414
  *
@@ -1891,9 +3429,21 @@ var ToonClient = class {
1891
3429
  if (this.evmSigner) {
1892
3430
  const store = this.config.channelStorePath ? new JsonFileChannelStore(this.config.channelStorePath) : void 0;
1893
3431
  this.channelManager = new ChannelManager(this.evmSigner, store);
3432
+ if (this.config.mnemonic) {
3433
+ await this.registerMnemonicChainSigners(
3434
+ this.config.mnemonic,
3435
+ this.config.mnemonicAccountIndex ?? 0
3436
+ );
3437
+ }
1894
3438
  }
1895
3439
  const initialization = await initializeHttpMode(this.config);
1896
- const { bootstrapService, discoveryTracker, runtimeClient, btpClient } = initialization;
3440
+ const {
3441
+ bootstrapService,
3442
+ discoveryTracker,
3443
+ runtimeClient,
3444
+ btpClient,
3445
+ stopManagedProxy
3446
+ } = initialization;
1897
3447
  if (this.channelManager) {
1898
3448
  const cm = this.channelManager;
1899
3449
  const nostrPubkey = this.getPublicKey();
@@ -1904,7 +3454,8 @@ var ToonClient = class {
1904
3454
  cm.trackChannel(channelId, defaultChainCtx);
1905
3455
  }
1906
3456
  const proof = await cm.signBalanceProof(channelId, amount);
1907
- return EvmSigner.buildClaimMessage(proof, nostrPubkey);
3457
+ const signer = cm.getSignerForChannel(channelId);
3458
+ return signer.buildClaimMessage(proof, nostrPubkey);
1908
3459
  }
1909
3460
  );
1910
3461
  }
@@ -1953,13 +3504,35 @@ var ToonClient = class {
1953
3504
  this.channelManager.setChannelClient(
1954
3505
  initialization.onChainChannelClient
1955
3506
  );
3507
+ if (this.config.solanaChannel && this.solanaSeed) {
3508
+ initialization.onChainChannelClient.setSolanaConfig({
3509
+ rpcUrl: this.config.solanaChannel.rpcUrl,
3510
+ programId: this.config.solanaChannel.programId,
3511
+ tokenMint: this.config.solanaChannel.tokenMint,
3512
+ challengeDuration: this.config.solanaChannel.challengeDuration,
3513
+ deposit: this.config.solanaChannel.deposit,
3514
+ keypair: this.solanaSeed
3515
+ });
3516
+ }
3517
+ if (this.config.minaChannel && this.minaPrivateKey) {
3518
+ initialization.onChainChannelClient.setMinaConfig({
3519
+ graphqlUrl: this.config.minaChannel.graphqlUrl,
3520
+ zkAppAddress: this.config.minaChannel.zkAppAddress,
3521
+ privateKey: this.minaPrivateKey,
3522
+ ...this.config.minaChannel.challengeDuration !== void 0 ? { challengeDuration: this.config.minaChannel.challengeDuration } : {},
3523
+ ...this.config.minaChannel.tokenId !== void 0 ? { tokenId: this.config.minaChannel.tokenId } : {},
3524
+ ...this.config.minaChannel.deposit !== void 0 ? { deposit: this.config.minaChannel.deposit } : {},
3525
+ ...this.config.minaChannel.networkId !== void 0 ? { networkId: this.config.minaChannel.networkId } : {}
3526
+ });
3527
+ }
1956
3528
  }
1957
3529
  this.state = {
1958
3530
  bootstrapService,
1959
3531
  discoveryTracker,
1960
3532
  runtimeClient,
1961
3533
  peersDiscovered: bootstrapResults.length,
1962
- btpClient: btpClient ?? void 0
3534
+ btpClient: btpClient ?? void 0,
3535
+ ...stopManagedProxy ? { stopManagedProxy } : {}
1963
3536
  };
1964
3537
  return {
1965
3538
  peersDiscovered: bootstrapResults.length,
@@ -1994,7 +3567,7 @@ var ToonClient = class {
1994
3567
  try {
1995
3568
  const toonData = this.config.toonEncoder(event);
1996
3569
  const basePricePerByte = 10n;
1997
- const amount = String(BigInt(toonData.length) * basePricePerByte);
3570
+ const amount = options?.ilpAmount !== void 0 ? String(options.ilpAmount) : String(BigInt(toonData.length) * basePricePerByte);
1998
3571
  const destination = options?.destination ?? this.config.destinationAddress;
1999
3572
  if (!this.state.btpClient) {
2000
3573
  throw new ToonClientError(
@@ -2004,10 +3577,7 @@ var ToonClient = class {
2004
3577
  }
2005
3578
  let claimMessage;
2006
3579
  if (options?.claim) {
2007
- claimMessage = EvmSigner.buildClaimMessage(
2008
- options.claim,
2009
- this.getPublicKey()
2010
- );
3580
+ claimMessage = this.buildClaimMessageForProof(options.claim);
2011
3581
  } else if (this.channelManager) {
2012
3582
  const peerId = this.resolvePeerId(destination);
2013
3583
  const negotiation = this.peerNegotiations.get(peerId);
@@ -2067,6 +3637,112 @@ var ToonClient = class {
2067
3637
  );
2068
3638
  }
2069
3639
  }
3640
+ /**
3641
+ * Sends a raw swap ILP packet (Story 12.5) to a Mill peer with an attached
3642
+ * balance-proof claim. This is a lower-level surface than `publishEvent`:
3643
+ * it forwards the raw `IlpSendResult` so the sender (`streamSwap()`) can
3644
+ * decode FULFILL metadata itself.
3645
+ *
3646
+ * Claim resolution mirrors `publishEvent`:
3647
+ * (a) explicit `params.claim` -> use it,
3648
+ * (b) `channelManager` present -> auto-open + auto-sign for the peer
3649
+ * matching `destination`,
3650
+ * (c) neither -> throw MISSING_CLAIM.
3651
+ *
3652
+ * @throws {ToonClientError} INVALID_STATE / NO_BTP_CLIENT / MISSING_CLAIM
3653
+ */
3654
+ async sendSwapPacket(params) {
3655
+ if (!this.state) {
3656
+ throw new ToonClientError(
3657
+ "Client not started. Call start() first.",
3658
+ "INVALID_STATE"
3659
+ );
3660
+ }
3661
+ if (!this.state.btpClient) {
3662
+ throw new ToonClientError(
3663
+ "BTP client required for sending swap packets. Configure btpUrl.",
3664
+ "NO_BTP_CLIENT"
3665
+ );
3666
+ }
3667
+ const claimMessage = await this.resolveClaimForDestination(
3668
+ params.destination,
3669
+ params.amount,
3670
+ params.claim
3671
+ );
3672
+ return this.state.btpClient.sendIlpPacketWithClaim(
3673
+ {
3674
+ destination: params.destination,
3675
+ amount: String(params.amount),
3676
+ data: toBase64(params.toonData),
3677
+ timeout: params.timeout ?? 3e4
3678
+ },
3679
+ claimMessage
3680
+ );
3681
+ }
3682
+ /**
3683
+ * Build a BTP claim message from a pre-signed balance proof using the
3684
+ * CHAIN-APPROPRIATE signer.
3685
+ *
3686
+ * The explicit-claim path (caller signs the balance proof, then passes
3687
+ * `{ claim }`) must wrap the proof with the signer matching the channel's
3688
+ * chain. Hardcoding `EvmSigner.buildClaimMessage` here produced an EVM
3689
+ * `BTPClaimMessage` for a Solana/Mina balance proof — no `blockchain`
3690
+ * discriminator and the base58 channel account placed in the EVM
3691
+ * `channelId` field — which the connector's inbound validator classifies
3692
+ * as EVM and rejects with F06 (`Invalid channelId format`).
3693
+ *
3694
+ * When the proof's `channelId` is tracked we use
3695
+ * `getSignerForChannel(channelId).buildClaimMessage`, which emits the
3696
+ * correct per-chain envelope (e.g. `blockchain:'solana'` + base58
3697
+ * `channelAccount`). When it is not tracked we fall back to the EVM signer
3698
+ * to preserve prior behavior for lightweight/EVM-only callers.
3699
+ *
3700
+ * EVM output is byte-identical to the previous hardcoded path (the EVM
3701
+ * adapter in `getSignerForChannel` delegates to the same
3702
+ * `EvmSigner.buildClaimMessage`).
3703
+ */
3704
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- claim message is opaque forwarded type
3705
+ buildClaimMessageForProof(claim) {
3706
+ if (this.channelManager?.isTracking(claim.channelId)) {
3707
+ const signer = this.channelManager.getSignerForChannel(claim.channelId);
3708
+ return signer.buildClaimMessage(claim, this.getPublicKey());
3709
+ }
3710
+ return EvmSigner.buildClaimMessage(claim, this.getPublicKey());
3711
+ }
3712
+ /**
3713
+ * Shared claim-resolution logic used by `publishEvent` and `sendSwapPacket`.
3714
+ * TODO(12.5 followup): also factor `publishEvent`'s inline claim resolution
3715
+ * to call this helper. Kept duplicated for now to minimize regression risk.
3716
+ */
3717
+ async resolveClaimForDestination(destination, amount, explicitClaim) {
3718
+ if (explicitClaim) {
3719
+ return this.buildClaimMessageForProof(explicitClaim);
3720
+ }
3721
+ if (this.channelManager) {
3722
+ const peerId = this.resolvePeerId(destination);
3723
+ const negotiation = this.peerNegotiations.get(peerId);
3724
+ if (!negotiation) {
3725
+ throw new ToonClientError(
3726
+ `No negotiation metadata for peer "${peerId}" \u2014 was bootstrap completed?`,
3727
+ "PEER_NOT_NEGOTIATED"
3728
+ );
3729
+ }
3730
+ const channelId = await this.channelManager.ensureChannel(
3731
+ peerId,
3732
+ negotiation
3733
+ );
3734
+ const proof = await this.channelManager.signBalanceProof(
3735
+ channelId,
3736
+ amount
3737
+ );
3738
+ const signer = this.channelManager.getSignerForChannel(channelId);
3739
+ return signer.buildClaimMessage(proof, this.getPublicKey());
3740
+ }
3741
+ throw new ToonClientError(
3742
+ "No claim provided and no channel manager configured",
3743
+ "MISSING_CLAIM"
3744
+ );
3745
+ }
2070
3746
  /**
2071
3747
  * Signs a balance proof for the given channel with the specified amount.
2072
3748
  * Delegates to ChannelManager which auto-increments nonce and tracks cumulative amount.
@@ -2085,6 +3761,51 @@ var ToonClient = class {
2085
3761
  }
2086
3762
  return this.channelManager.signBalanceProof(channelId, amount);
2087
3763
  }
3764
+ /**
3765
+ * Eagerly open (or return existing) payment channel for the given destination.
3766
+ *
3767
+ * Channels are normally opened lazily on the first `publishEvent()` /
3768
+ * `sendSwapPacket()` call. This method exposes the lazy-open path so
3769
+ * callers (and E2E tests) that need a tracked `channelId` BEFORE publishing
3770
+ * can force the open. Idempotent — returns the existing channel ID for the
3771
+ * peer if one is already open.
3772
+ *
3773
+ * @param destination - Optional ILP destination address. Defaults to
3774
+ * `config.destinationAddress`.
3775
+ * @returns The channel ID of the (now) open channel.
3776
+ * @throws {ToonClientError} If client not started, no channel manager
3777
+ * configured, or peer negotiation metadata missing.
3778
+ */
3779
+ async openChannel(destination) {
3780
+ if (!this.state) {
3781
+ throw new ToonClientError(
3782
+ "Client not started. Call start() first.",
3783
+ "INVALID_STATE"
3784
+ );
3785
+ }
3786
+ if (!this.channelManager) {
3787
+ throw new ToonClientError(
3788
+ "No channel manager configured. Provide evmPrivateKey in config.",
3789
+ "NO_EVM_SIGNER"
3790
+ );
3791
+ }
3792
+ const dest = destination ?? this.config.destinationAddress;
3793
+ if (!dest) {
3794
+ throw new ToonClientError(
3795
+ "No destination provided and no default destinationAddress configured.",
3796
+ "NO_DESTINATION"
3797
+ );
3798
+ }
3799
+ const peerId = this.resolvePeerId(dest);
3800
+ const negotiation = this.peerNegotiations.get(peerId);
3801
+ if (!negotiation) {
3802
+ throw new ToonClientError(
3803
+ `No negotiation metadata for peer "${peerId}" \u2014 was bootstrap completed?`,
3804
+ "PEER_NOT_NEGOTIATED"
3805
+ );
3806
+ }
3807
+ return this.channelManager.ensureChannel(peerId, negotiation);
3808
+ }
2088
3809
  /**
2089
3810
  * Gets list of tracked payment channel IDs.
2090
3811
  */
@@ -2182,10 +3903,7 @@ var ToonClient = class {
2182
3903
  "NO_BTP_CLIENT"
2183
3904
  );
2184
3905
  }
2185
- const claimMessage = EvmSigner.buildClaimMessage(
2186
- params.claim,
2187
- this.getPublicKey()
2188
- );
3906
+ const claimMessage = this.buildClaimMessageForProof(params.claim);
2189
3907
  return this.state.btpClient.sendIlpPacketWithClaim(
2190
3908
  ilpParams,
2191
3909
  claimMessage
@@ -2204,10 +3922,14 @@ var ToonClient = class {
2204
3922
  if (!this.state) {
2205
3923
  throw new ToonClientError("Client not started", "INVALID_STATE");
2206
3924
  }
3925
+ const stopManagedProxy = this.state.stopManagedProxy;
2207
3926
  try {
2208
3927
  if (this.state.btpClient) {
2209
3928
  await this.state.btpClient.disconnect();
2210
3929
  }
3930
+ if (stopManagedProxy) {
3931
+ await stopManagedProxy();
3932
+ }
2211
3933
  this.state = null;
2212
3934
  } catch (error) {
2213
3935
  throw new ToonClientError(
@@ -2255,6 +3977,26 @@ var ToonClient = class {
2255
3977
  }
2256
3978
  };
2257
3979
 
3980
+ // src/transport/hs-hostname.ts
3981
+ var HS_HOSTNAME_REGEX = /^[a-z2-7]+\.anyone$/;
3982
+ var HS_HOSTNAME_MAX_LENGTH = 80;
3983
+ function isRoutableHsHostname(s) {
3984
+ return typeof s === "string" && s.length <= HS_HOSTNAME_MAX_LENGTH && HS_HOSTNAME_REGEX.test(s);
3985
+ }
3986
+ function assertRoutableHsHostname(hostname) {
3987
+ if (typeof hostname === "string" && /\.anon$/.test(hostname)) {
3988
+ throw new Error(
3989
+ `"${hostname}" is not a routable hidden-service address; use the .anyone TLD (e.g. "${hostname.replace(/\.anon$/, ".anyone")}"). The anon daemon only resolves hidden services under .anyone \u2014 a .anon name is treated as a clearnet address and fails (HostUnreachable).`
3990
+ );
3991
+ }
3992
+ if (!isRoutableHsHostname(hostname)) {
3993
+ throw new Error(
3994
+ `Invalid hidden-service hostname: ${JSON.stringify(hostname)}. Expected a base32 .anyone address matching ${HS_HOSTNAME_REGEX}.`
3995
+ );
3996
+ }
3997
+ return hostname;
3998
+ }
3999
+
2258
4000
  // src/adapters/HttpConnectorAdmin.ts
2259
4001
  var HttpConnectorAdmin = class {
2260
4002
  adminUrl;
@@ -2544,331 +4286,525 @@ var HttpConnectorAdmin = class {
2544
4286
  `Admin API authentication failed for ${endpoint}: ${statusText}${errorMessage}`
2545
4287
  );
2546
4288
  case 404:
2547
- throw new PeerNotFoundError(
2548
- `Peer not found: "${peerId}" (${endpoint}): ${statusText}${errorMessage}`
2549
- );
2550
- case 409:
2551
- throw new PeerAlreadyExistsError(
2552
- `Peer already exists: "${peerId}" (${endpoint}): ${statusText}${errorMessage}`
2553
- );
2554
- default:
2555
- if (status >= 500) {
2556
- throw new ConnectorError(
2557
- `Connector admin API error (${endpoint}): ${status} ${statusText}${errorMessage}`
2558
- );
2559
- }
2560
- throw new ConnectorError(
2561
- `Admin API error (${endpoint}): ${status} ${statusText}${errorMessage}`
2562
- );
2563
- }
2564
- }
2565
- };
2566
-
2567
- // src/signing/solana-signer.ts
2568
- var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2569
- function toBase58(bytes) {
2570
- let num = BigInt(0);
2571
- for (const b of bytes) num = num * 256n + BigInt(b);
2572
- let result = "";
2573
- while (num > 0n) {
2574
- result = BASE58_ALPHABET[Number(num % 58n)] + result;
2575
- num = num / 58n;
2576
- }
2577
- for (const b of bytes) {
2578
- if (b === 0) result = "1" + result;
2579
- else break;
2580
- }
2581
- return result;
2582
- }
2583
- var _ed25519 = null;
2584
- async function getEd25519() {
2585
- if (!_ed25519) {
2586
- const mod = await import("@noble/curves/ed25519");
2587
- _ed25519 = mod.ed25519;
2588
- }
2589
- return _ed25519;
2590
- }
2591
- var SolanaSigner = class {
2592
- chainType = "solana";
2593
- privateKey;
2594
- publicKey;
2595
- pubkeyBase58Cache;
2596
- constructor(privateKey) {
2597
- this.privateKey = privateKey;
2598
- }
2599
- async ensurePublicKey() {
2600
- if (this.publicKey && this.pubkeyBase58Cache) {
2601
- return { publicKey: this.publicKey, base58: this.pubkeyBase58Cache };
2602
- }
2603
- const ed = await getEd25519();
2604
- const pk = ed.getPublicKey(this.privateKey);
2605
- const b58 = toBase58(pk);
2606
- this.publicKey = pk;
2607
- this.pubkeyBase58Cache = b58;
2608
- return { publicKey: pk, base58: b58 };
2609
- }
2610
- get signerIdentifier() {
2611
- return this.pubkeyBase58Cache ?? "uninitialized";
2612
- }
2613
- async signBalanceProof(params) {
2614
- if (params.metadata.chainType !== "solana") {
2615
- throw new Error(
2616
- `SolanaSigner cannot sign for chain type: ${params.metadata.chainType}`
2617
- );
2618
- }
2619
- const ed = await getEd25519();
2620
- const { base58 } = await this.ensurePublicKey();
2621
- const encoder = new TextEncoder();
2622
- const message = encoder.encode(
2623
- `${params.channelId}:${params.nonce}:${params.transferredAmount}:${params.lockedAmount}:${params.locksRoot}`
2624
- );
2625
- const signature = ed.sign(message, this.privateKey);
2626
- const signatureHex = "0x" + toHex(new Uint8Array(signature));
2627
- return {
2628
- channelId: params.channelId,
2629
- nonce: params.nonce,
2630
- transferredAmount: params.transferredAmount,
2631
- lockedAmount: params.lockedAmount,
2632
- locksRoot: params.locksRoot,
2633
- signature: signatureHex,
2634
- signerAddress: base58,
2635
- chainId: 0,
2636
- tokenNetworkAddress: params.metadata.programId
2637
- };
2638
- }
2639
- buildClaimMessage(proof, senderId) {
2640
- const claim = {
2641
- version: "1.0",
2642
- blockchain: "solana",
2643
- messageId: crypto.randomUUID(),
2644
- timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, ".000Z"),
2645
- senderId,
2646
- channelId: proof.channelId,
2647
- nonce: proof.nonce,
2648
- transferredAmount: proof.transferredAmount.toString(),
2649
- signature: proof.signature,
2650
- signerAddress: this.pubkeyBase58Cache ?? proof.signerAddress,
2651
- programId: proof.tokenNetworkAddress
2652
- };
2653
- return claim;
2654
- }
2655
- };
2656
-
2657
- // src/signing/mina-signer.ts
2658
- var MinaSigner = class {
2659
- chainType = "mina";
2660
- privateKeyBase58;
2661
- publicKeyBase58 = "uninitialized";
2662
- constructor(privateKeyBase58) {
2663
- this.privateKeyBase58 = privateKeyBase58;
2664
- }
2665
- get signerIdentifier() {
2666
- return this.publicKeyBase58;
2667
- }
2668
- async ensurePublicKey() {
2669
- if (this.publicKeyBase58 !== "uninitialized") return this.publicKeyBase58;
2670
- const o1js = await import("o1js");
2671
- const pk = o1js.PrivateKey.fromBase58(this.privateKeyBase58);
2672
- this.publicKeyBase58 = pk.toPublicKey().toBase58();
2673
- return this.publicKeyBase58;
2674
- }
2675
- async signBalanceProof(params) {
2676
- if (params.metadata.chainType !== "mina") {
2677
- throw new Error(
2678
- `MinaSigner cannot sign for chain type: ${params.metadata.chainType}`
2679
- );
2680
- }
2681
- const o1js = await import("o1js");
2682
- const pubkey = await this.ensurePublicKey();
2683
- const channelIdNum = BigInt(
2684
- "0x" + params.channelId.replace(/^0x/, "").slice(0, 16)
2685
- );
2686
- const commitment = o1js.Poseidon.hash([
2687
- o1js.Field(channelIdNum),
2688
- o1js.Field(params.nonce),
2689
- o1js.Field(params.transferredAmount),
2690
- o1js.Field(params.lockedAmount)
2691
- ]);
2692
- const pk = o1js.PrivateKey.fromBase58(this.privateKeyBase58);
2693
- const signature = o1js.Signature.create(pk, [commitment]);
2694
- return {
2695
- channelId: params.channelId,
2696
- nonce: params.nonce,
2697
- transferredAmount: params.transferredAmount,
2698
- lockedAmount: params.lockedAmount,
2699
- locksRoot: params.locksRoot,
2700
- signature: signature.toBase58(),
2701
- signerAddress: pubkey,
2702
- chainId: 0,
2703
- tokenNetworkAddress: params.metadata.zkAppAddress
2704
- };
2705
- }
2706
- buildClaimMessage(proof, senderId) {
2707
- const claim = {
2708
- version: "1.0",
2709
- blockchain: "mina",
2710
- messageId: crypto.randomUUID(),
2711
- timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, ".000Z"),
2712
- senderId,
2713
- channelId: proof.channelId,
2714
- nonce: proof.nonce,
2715
- transferredAmount: proof.transferredAmount.toString(),
2716
- commitment: proof.signature,
2717
- signerAddress: proof.signerAddress,
2718
- zkAppAddress: proof.tokenNetworkAddress
2719
- };
2720
- return claim;
4289
+ throw new PeerNotFoundError(
4290
+ `Peer not found: "${peerId}" (${endpoint}): ${statusText}${errorMessage}`
4291
+ );
4292
+ case 409:
4293
+ throw new PeerAlreadyExistsError(
4294
+ `Peer already exists: "${peerId}" (${endpoint}): ${statusText}${errorMessage}`
4295
+ );
4296
+ default:
4297
+ if (status >= 500) {
4298
+ throw new ConnectorError(
4299
+ `Connector admin API error (${endpoint}): ${status} ${statusText}${errorMessage}`
4300
+ );
4301
+ }
4302
+ throw new ConnectorError(
4303
+ `Admin API error (${endpoint}): ${status} ${statusText}${errorMessage}`
4304
+ );
4305
+ }
2721
4306
  }
2722
4307
  };
2723
4308
 
2724
- // src/keys/KeyManager.ts
2725
- import { finalizeEvent } from "nostr-tools/pure";
2726
- import { nip19 } from "nostr-tools";
4309
+ // src/pet/filterPetDvmProviders.ts
4310
+ import { parseServiceDiscovery } from "@toon-protocol/core";
4311
+ import { PET_INTERACTION_REQUEST_KIND } from "@toon-protocol/core";
4312
+ function filterPetDvmProviders(events) {
4313
+ const providers = [];
4314
+ for (const event of events) {
4315
+ let parsed;
4316
+ try {
4317
+ parsed = parseServiceDiscovery(event);
4318
+ } catch {
4319
+ continue;
4320
+ }
4321
+ if (!parsed) continue;
4322
+ const skill = parsed.skill;
4323
+ if (!skill) continue;
4324
+ if (!skill.kinds.includes(PET_INTERACTION_REQUEST_KIND)) continue;
4325
+ const pricing = skill.pricing[String(PET_INTERACTION_REQUEST_KIND)] ?? "0";
4326
+ providers.push({
4327
+ ilpAddress: parsed.ilpAddress,
4328
+ pricing,
4329
+ pubkey: event.pubkey,
4330
+ features: skill.features
4331
+ });
4332
+ }
4333
+ providers.sort((a, b) => {
4334
+ const priceA = Number(a.pricing) || 0;
4335
+ const priceB = Number(b.pricing) || 0;
4336
+ return priceA - priceB;
4337
+ });
4338
+ return providers;
4339
+ }
2727
4340
 
2728
- // src/keys/KeyDerivation.ts
2729
- import { generateSecretKey as generateSecretKey3, getPublicKey as getPublicKey2 } from "nostr-tools/pure";
2730
- import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
2731
- import { toHex as toHex3 } from "viem";
2732
- import {
2733
- generateMnemonic as _genMnemonic,
2734
- validateMnemonic as _validateMnemonic,
2735
- mnemonicToSeedSync
2736
- } from "@scure/bip39";
2737
- import { wordlist as english } from "@scure/bip39/wordlists/english";
2738
- import { HDKey } from "@scure/bip32";
2739
- function generateMnemonic() {
2740
- return _genMnemonic(english, 128);
4341
+ // src/pet/buildPetInteractionRequest.ts
4342
+ import { PET_INTERACTION_REQUEST_KIND as PET_INTERACTION_REQUEST_KIND2 } from "@toon-protocol/core";
4343
+ var MAX_ACTION_TYPE = 10;
4344
+ function buildPetInteractionRequest(params) {
4345
+ const { blobbiId, actionType, itemId, tokenCost, isSleeping } = params;
4346
+ if (!blobbiId || blobbiId.trim() === "") {
4347
+ throw new ValidationError("blobbiId must be a non-empty string");
4348
+ }
4349
+ if (!Number.isInteger(actionType) || actionType < 0 || actionType > MAX_ACTION_TYPE) {
4350
+ throw new ValidationError(
4351
+ `actionType must be an integer between 0 and ${MAX_ACTION_TYPE}, got ${actionType}`
4352
+ );
4353
+ }
4354
+ if (!Number.isInteger(itemId) || itemId < 0) {
4355
+ throw new ValidationError(
4356
+ `itemId must be a non-negative integer, got ${itemId}`
4357
+ );
4358
+ }
4359
+ if (!Number.isFinite(tokenCost) || tokenCost < 0) {
4360
+ throw new ValidationError(
4361
+ `tokenCost must be a non-negative number, got ${tokenCost}`
4362
+ );
4363
+ }
4364
+ return {
4365
+ kind: PET_INTERACTION_REQUEST_KIND2,
4366
+ created_at: Math.floor(Date.now() / 1e3),
4367
+ tags: [
4368
+ ["d", blobbiId],
4369
+ ["action", String(actionType)],
4370
+ ["item", String(itemId)],
4371
+ ["cost", String(tokenCost)],
4372
+ ["sleeping", String(isSleeping)]
4373
+ ],
4374
+ content: ""
4375
+ };
2741
4376
  }
2742
- function validateMnemonic(mnemonic) {
2743
- return _validateMnemonic(mnemonic, english);
4377
+
4378
+ // src/pet/parsePetInteractionResult.ts
4379
+ var STAT_FIELDS = [
4380
+ "hunger",
4381
+ "happiness",
4382
+ "health",
4383
+ "hygiene",
4384
+ "energy"
4385
+ ];
4386
+ var HEX_64_RE = /^[0-9a-f]{64}$/i;
4387
+ function isValidStats(obj) {
4388
+ if (typeof obj !== "object" || obj === null) return false;
4389
+ const record = obj;
4390
+ return STAT_FIELDS.every(
4391
+ (field) => typeof record[field] === "number" && Number.isFinite(record[field])
4392
+ );
2744
4393
  }
2745
- function deriveNostrKey(seed) {
2746
- const master = HDKey.fromMasterSeed(seed);
2747
- const child = master.derive("m/44'/1237'/0'/0/0");
2748
- if (!child.privateKey) {
2749
- throw new Error("Failed to derive Nostr private key from seed");
4394
+ function parsePetInteractionResult(data) {
4395
+ if (!data) return null;
4396
+ let json;
4397
+ try {
4398
+ json = atob(data);
4399
+ } catch {
4400
+ return null;
2750
4401
  }
2751
- const secretKey = new Uint8Array(child.privateKey);
2752
- const pubkey = getPublicKey2(secretKey);
2753
- return { secretKey, pubkey };
2754
- }
2755
- function deriveEvmIdentity(secretKey) {
2756
- const account = privateKeyToAccount2(toHex3(secretKey));
4402
+ let parsed;
4403
+ try {
4404
+ parsed = JSON.parse(json);
4405
+ } catch {
4406
+ return null;
4407
+ }
4408
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4409
+ return null;
4410
+ }
4411
+ const record = parsed;
4412
+ if (!isValidStats(record["stats"])) return null;
4413
+ const stage = record["stage"];
4414
+ if (typeof stage !== "number" || !Number.isInteger(stage) || stage < 0 || stage > 2) {
4415
+ return null;
4416
+ }
4417
+ const cycle = record["cycle"];
4418
+ if (typeof cycle !== "number" || !Number.isInteger(cycle) || cycle < 0) {
4419
+ return null;
4420
+ }
4421
+ const lastInteraction = record["lastInteraction"];
4422
+ if (typeof lastInteraction !== "number" || !Number.isFinite(lastInteraction)) {
4423
+ return null;
4424
+ }
4425
+ const brainHash = record["brainHash"];
4426
+ if (typeof brainHash !== "string" || !HEX_64_RE.test(brainHash)) {
4427
+ return null;
4428
+ }
4429
+ const cooldownTimestamps = record["cooldownTimestamps"];
4430
+ if (!Array.isArray(cooldownTimestamps)) return null;
4431
+ if (!cooldownTimestamps.every(
4432
+ (t) => typeof t === "number" && Number.isFinite(t)
4433
+ )) {
4434
+ return null;
4435
+ }
4436
+ const validatedStats = record["stats"];
4437
+ const stats = {
4438
+ hunger: validatedStats.hunger,
4439
+ happiness: validatedStats.happiness,
4440
+ health: validatedStats.health,
4441
+ hygiene: validatedStats.hygiene,
4442
+ energy: validatedStats.energy
4443
+ };
2757
4444
  return {
2758
- privateKey: secretKey,
2759
- address: account.address
4445
+ stats,
4446
+ stage,
4447
+ cycle,
4448
+ lastInteraction,
4449
+ brainHash,
4450
+ cooldownTimestamps: [...cooldownTimestamps]
2760
4451
  };
2761
4452
  }
2762
- async function deriveSolanaKey(seed) {
2763
- const { hmac } = await import("@noble/hashes/hmac");
2764
- const { sha512 } = await import("@noble/hashes/sha512");
2765
- const { ed25519 } = await import("@noble/curves/ed25519");
2766
- const encoder = new TextEncoder();
2767
- let I = hmac(sha512, encoder.encode("ed25519 seed"), seed);
2768
- let key = I.slice(0, 32);
2769
- let chainCode = I.slice(32);
2770
- const indices = [
2771
- 2147483692,
2772
- // 44'
2773
- 2147484149,
2774
- // 501'
2775
- 2147483648,
2776
- // 0'
2777
- 2147483648
2778
- // 0'
2779
- ];
2780
- for (const index of indices) {
2781
- const data = new Uint8Array(37);
2782
- data[0] = 0;
2783
- data.set(key, 1);
2784
- data[33] = index >>> 24 & 255;
2785
- data[34] = index >>> 16 & 255;
2786
- data[35] = index >>> 8 & 255;
2787
- data[36] = index & 255;
2788
- I = hmac(sha512, chainCode, data);
2789
- key = I.slice(0, 32);
2790
- chainCode = I.slice(32);
4453
+
4454
+ // src/pet/parsePetInteractionEvent.ts
4455
+ function getTagValue(tags, name) {
4456
+ for (const tag of tags) {
4457
+ if (tag[0] === name) {
4458
+ return tag[1];
4459
+ }
2791
4460
  }
2792
- const publicKeyBytes = ed25519.getPublicKey(key);
2793
- const keypair = new Uint8Array(64);
2794
- keypair.set(key, 0);
2795
- keypair.set(publicKeyBytes, 32);
2796
- const publicKey = toBase582(publicKeyBytes);
2797
- return { secretKey: keypair, publicKey };
4461
+ return void 0;
2798
4462
  }
2799
- async function deriveMinaKey(seed) {
2800
- const master = HDKey.fromMasterSeed(seed);
2801
- const child = master.derive("m/44'/12586'/0'/0/0");
2802
- if (!child.privateKey) {
2803
- throw new Error("Failed to derive Mina private key from seed");
2804
- }
2805
- const keyBytes = new Uint8Array(child.privateKey);
4463
+ function isStatLike(obj) {
4464
+ if (typeof obj !== "object" || obj === null) return false;
4465
+ const r = obj;
4466
+ return typeof r["hunger"] === "number" && Number.isFinite(r["hunger"]) && typeof r["happiness"] === "number" && Number.isFinite(r["happiness"]) && typeof r["health"] === "number" && Number.isFinite(r["health"]) && typeof r["hygiene"] === "number" && Number.isFinite(r["hygiene"]) && typeof r["energy"] === "number" && Number.isFinite(r["energy"]);
4467
+ }
4468
+ function cleanStats(obj) {
4469
+ return {
4470
+ hunger: obj["hunger"],
4471
+ happiness: obj["happiness"],
4472
+ health: obj["health"],
4473
+ hygiene: obj["hygiene"],
4474
+ energy: obj["energy"]
4475
+ };
4476
+ }
4477
+ function parseContent(content) {
2806
4478
  try {
2807
- const MinaSignerLib = await import("./mina-signer-J7GFWOGO.js");
2808
- const Client = "default" in MinaSignerLib ? MinaSignerLib.default : MinaSignerLib;
2809
- const client = new Client({ network: "mainnet" });
2810
- const hexKey = Array.from(keyBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2811
- const keypair = client.derivePublicKey(hexKey);
4479
+ const parsed = JSON.parse(content);
4480
+ if (typeof parsed !== "object" || parsed === null) return null;
4481
+ if (!isStatLike(parsed.priorStats) || !isStatLike(parsed.decayedStats) || !isStatLike(parsed.finalStats)) {
4482
+ return null;
4483
+ }
4484
+ if (typeof parsed.cycle !== "number" || typeof parsed.stage !== "number" || typeof parsed.tokenCost !== "number") {
4485
+ return null;
4486
+ }
2812
4487
  return {
2813
- privateKey: hexKey,
2814
- publicKey: keypair
4488
+ priorStats: cleanStats(parsed.priorStats),
4489
+ decayedStats: cleanStats(parsed.decayedStats),
4490
+ finalStats: cleanStats(parsed.finalStats),
4491
+ cycle: parsed.cycle,
4492
+ stage: parsed.stage,
4493
+ tokenCost: parsed.tokenCost
2815
4494
  };
2816
4495
  } catch {
2817
- throw new Error(
2818
- "mina-signer is required for Mina key derivation. Install it as an optional dependency."
2819
- );
4496
+ return null;
2820
4497
  }
2821
4498
  }
2822
- async function deriveFullIdentity(mnemonic) {
2823
- const seed = mnemonicToSeedSync(mnemonic);
2824
- const nostr = deriveNostrKey(seed);
2825
- const evm = deriveEvmIdentity(nostr.secretKey);
2826
- let solana;
4499
+ function parsePetInteractionEvent(event) {
4500
+ const tags = event.tags;
4501
+ const blobbiId = getTagValue(tags, "d");
4502
+ if (!blobbiId) return null;
4503
+ const actionStr = getTagValue(tags, "action");
4504
+ if (!actionStr) return null;
4505
+ const actionType = Number(actionStr);
4506
+ if (!Number.isFinite(actionType)) return null;
4507
+ const itemStr = getTagValue(tags, "item");
4508
+ if (!itemStr) return null;
4509
+ const itemId = Number(itemStr);
4510
+ if (!Number.isFinite(itemId)) return null;
4511
+ const costStr = getTagValue(tags, "cost");
4512
+ if (!costStr) return null;
4513
+ const tokenCost = Number(costStr);
4514
+ if (!Number.isFinite(tokenCost)) return null;
4515
+ const cycleStr = getTagValue(tags, "cycle");
4516
+ if (!cycleStr) return null;
4517
+ const cycle = Number(cycleStr);
4518
+ if (!Number.isFinite(cycle)) return null;
4519
+ const stageStr = getTagValue(tags, "stage");
4520
+ if (!stageStr) return null;
4521
+ const stage = Number(stageStr);
4522
+ if (!Number.isFinite(stage)) return null;
4523
+ const brainHash = getTagValue(tags, "brain_hash");
4524
+ if (!brainHash) return null;
4525
+ const proof = getTagValue(tags, "proof");
4526
+ const minaTx = getTagValue(tags, "mina_tx");
4527
+ const proofStatus = proof && minaTx ? "proven" : "optimistic";
4528
+ const content = parseContent(event.content);
4529
+ const result = {
4530
+ blobbiId,
4531
+ actionType,
4532
+ itemId,
4533
+ tokenCost,
4534
+ cycle,
4535
+ stage,
4536
+ brainHash,
4537
+ proofStatus,
4538
+ content
4539
+ };
4540
+ if (proof) result.proof = proof;
4541
+ if (minaTx) result.minaTx = minaTx;
4542
+ return result;
4543
+ }
4544
+
4545
+ // src/pet/buildPetListingEvent.ts
4546
+ var PET_LISTING_KIND = 30402;
4547
+ var STAGE_NAMES = {
4548
+ 0: "Egg",
4549
+ 1: "Baby",
4550
+ 2: "Adult"
4551
+ };
4552
+ function buildPetListingEvent(params) {
4553
+ const {
4554
+ blobbiId,
4555
+ askPriceUsdc,
4556
+ lifecycleHash,
4557
+ totalSpent,
4558
+ stage,
4559
+ stats,
4560
+ sellerPubkey,
4561
+ relayUrl,
4562
+ expiresAt
4563
+ } = params;
4564
+ const stageName = STAGE_NAMES[stage] ?? "Unknown";
4565
+ const summary = `${stageName} pet for sale \u2014 ${totalSpent} PET tokens spent (verified biography)`;
4566
+ return {
4567
+ kind: PET_LISTING_KIND,
4568
+ created_at: Math.floor(Date.now() / 1e3),
4569
+ tags: [
4570
+ ["d", blobbiId],
4571
+ ["title", `Pet ${blobbiId} for sale`],
4572
+ ["price", String(askPriceUsdc), "USDC", ""],
4573
+ ["summary", summary],
4574
+ ["t", "pet"],
4575
+ ["t", "toon-pet"],
4576
+ ["lifecycle_hash", lifecycleHash],
4577
+ ["total_spent", totalSpent],
4578
+ ["stage", String(stage)],
4579
+ ["expiration", String(expiresAt)],
4580
+ ["relay", relayUrl],
4581
+ ["p", sellerPubkey]
4582
+ ],
4583
+ content: JSON.stringify(stats)
4584
+ };
4585
+ }
4586
+
4587
+ // src/pet/parsePetListing.ts
4588
+ var HEX_64_RE2 = /^[0-9a-f]{64}$/i;
4589
+ function getTagValue2(tags, name) {
4590
+ for (const tag of tags) {
4591
+ if (tag[0] === name) {
4592
+ return tag[1];
4593
+ }
4594
+ }
4595
+ return void 0;
4596
+ }
4597
+ var DEFAULT_STATS = {
4598
+ hunger: 0,
4599
+ happiness: 0,
4600
+ health: 0,
4601
+ hygiene: 0,
4602
+ energy: 0
4603
+ };
4604
+ function parseStats(content) {
2827
4605
  try {
2828
- solana = await deriveSolanaKey(seed);
4606
+ const parsed = JSON.parse(content);
4607
+ if (typeof parsed !== "object" || parsed === null) return DEFAULT_STATS;
4608
+ const r = parsed;
4609
+ if (typeof r["hunger"] === "number" && typeof r["happiness"] === "number" && typeof r["health"] === "number" && typeof r["hygiene"] === "number" && typeof r["energy"] === "number") {
4610
+ return {
4611
+ hunger: r["hunger"],
4612
+ happiness: r["happiness"],
4613
+ health: r["health"],
4614
+ hygiene: r["hygiene"],
4615
+ energy: r["energy"]
4616
+ };
4617
+ }
4618
+ return DEFAULT_STATS;
2829
4619
  } catch {
2830
- solana = { secretKey: new Uint8Array(64), publicKey: "" };
4620
+ return DEFAULT_STATS;
2831
4621
  }
2832
- let mina;
4622
+ }
4623
+ function parsePetListing(event) {
4624
+ if (event.kind !== 30402) return null;
4625
+ const { tags } = event;
4626
+ const blobbiId = getTagValue2(tags, "d");
4627
+ if (!blobbiId || blobbiId.trim() === "") return null;
4628
+ let askPriceUsdc = 0;
4629
+ let foundPrice = false;
4630
+ for (const tag of tags) {
4631
+ if (tag[0] === "price") {
4632
+ const priceStr = tag[1];
4633
+ if (priceStr === void 0) return null;
4634
+ const parsed = Number(priceStr);
4635
+ if (!Number.isFinite(parsed) || parsed <= 0) return null;
4636
+ askPriceUsdc = parsed;
4637
+ foundPrice = true;
4638
+ break;
4639
+ }
4640
+ }
4641
+ if (!foundPrice) return null;
4642
+ const lifecycleHash = getTagValue2(tags, "lifecycle_hash");
4643
+ if (!lifecycleHash) return null;
4644
+ if (!HEX_64_RE2.test(lifecycleHash)) return null;
4645
+ const totalSpent = getTagValue2(tags, "total_spent");
4646
+ if (totalSpent === void 0 || totalSpent === "") return null;
4647
+ const totalSpentNum = Number(totalSpent);
4648
+ if (!Number.isFinite(totalSpentNum) || totalSpentNum < 0) return null;
4649
+ const stageStr = getTagValue2(tags, "stage");
4650
+ if (stageStr === void 0) return null;
4651
+ const stage = Number(stageStr);
4652
+ if (!Number.isFinite(stage)) return null;
4653
+ const sellerPubkey = getTagValue2(tags, "p") ?? "";
4654
+ const relayUrl = getTagValue2(tags, "relay") ?? "";
4655
+ const expiresAtStr = getTagValue2(tags, "expiration");
4656
+ const expiresAt = expiresAtStr !== void 0 ? Number(expiresAtStr) : 0;
4657
+ const stats = parseStats(event.content);
4658
+ return {
4659
+ blobbiId,
4660
+ askPriceUsdc,
4661
+ lifecycleHash,
4662
+ totalSpent,
4663
+ stage,
4664
+ stats,
4665
+ sellerPubkey,
4666
+ relayUrl,
4667
+ expiresAt,
4668
+ eventId: event.id,
4669
+ createdAt: event.created_at
4670
+ };
4671
+ }
4672
+
4673
+ // src/pet/filterPetListings.ts
4674
+ function compareNumericStrings(a, b) {
4675
+ if (a === b) return 0;
2833
4676
  try {
2834
- mina = await deriveMinaKey(seed);
4677
+ const bigA = BigInt(a);
4678
+ const bigB = BigInt(b);
4679
+ if (bigA < bigB) return -1;
4680
+ if (bigA > bigB) return 1;
4681
+ return 0;
2835
4682
  } catch {
2836
- mina = { privateKey: "", publicKey: "" };
4683
+ const fa = Number(a);
4684
+ const fb = Number(b);
4685
+ if (!Number.isFinite(fa) && !Number.isFinite(fb)) return 0;
4686
+ if (!Number.isFinite(fa)) return -1;
4687
+ if (!Number.isFinite(fb)) return 1;
4688
+ return fa - fb;
2837
4689
  }
2838
- seed.fill(0);
2839
- return { nostr, evm, solana, mina };
2840
4690
  }
2841
- function deriveFromNsec(secretKey) {
2842
- const keyCopy = new Uint8Array(secretKey);
2843
- const pubkey = getPublicKey2(keyCopy);
2844
- const evm = deriveEvmIdentity(keyCopy);
4691
+ function filterPetListings(events, options) {
4692
+ const now = Math.floor(Date.now() / 1e3);
4693
+ const listings = [];
4694
+ for (const event of events) {
4695
+ const listing = parsePetListing(event);
4696
+ if (listing === null) continue;
4697
+ if (listing.expiresAt > 0 && listing.expiresAt < now) continue;
4698
+ if (options?.minStage !== void 0 && listing.stage < options.minStage) {
4699
+ continue;
4700
+ }
4701
+ if (options?.maxAskPriceUsdc !== void 0 && listing.askPriceUsdc > options.maxAskPriceUsdc) {
4702
+ continue;
4703
+ }
4704
+ if (options?.minTotalSpent !== void 0) {
4705
+ if (compareNumericStrings(listing.totalSpent, options.minTotalSpent) < 0) {
4706
+ continue;
4707
+ }
4708
+ }
4709
+ if (options?.sellerPubkey !== void 0 && listing.sellerPubkey !== options.sellerPubkey) {
4710
+ continue;
4711
+ }
4712
+ listings.push(listing);
4713
+ }
4714
+ listings.sort((a, b) => compareNumericStrings(b.totalSpent, a.totalSpent));
4715
+ return listings;
4716
+ }
4717
+
4718
+ // src/pet/buildPetPurchaseRequest.ts
4719
+ import { PET_INTERACTION_REQUEST_KIND as PET_INTERACTION_REQUEST_KIND3 } from "@toon-protocol/core";
4720
+ var TRANSFER_OWNERSHIP_ACTION = 9;
4721
+ function buildPetPurchaseRequest(params) {
4722
+ const { blobbiId, listingEventId, buyerPubkey, tokenCost, sellerPubkey } = params;
2845
4723
  return {
2846
- nostr: { secretKey: keyCopy, pubkey },
2847
- evm,
2848
- solana: { secretKey: new Uint8Array(64), publicKey: "" },
2849
- mina: { privateKey: "", publicKey: "" }
4724
+ kind: PET_INTERACTION_REQUEST_KIND3,
4725
+ created_at: Math.floor(Date.now() / 1e3),
4726
+ tags: [
4727
+ ["action", String(TRANSFER_OWNERSHIP_ACTION)],
4728
+ ["i", blobbiId],
4729
+ ["listing", listingEventId],
4730
+ ["buyer", buyerPubkey],
4731
+ ["p", sellerPubkey],
4732
+ ["cost", String(tokenCost)]
4733
+ ],
4734
+ content: ""
2850
4735
  };
2851
4736
  }
2852
- function generateRandomIdentity() {
2853
- const secretKey = generateSecretKey3();
2854
- return deriveFromNsec(secretKey);
2855
- }
2856
- var BASE58_ALPHABET2 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2857
- function toBase582(bytes) {
2858
- let num = BigInt(0);
2859
- for (const b of bytes) num = num * 256n + BigInt(b);
2860
- let result = "";
2861
- while (num > 0n) {
2862
- result = BASE58_ALPHABET2[Number(num % 58n)] + result;
2863
- num = num / 58n;
4737
+
4738
+ // src/blob-storage.ts
4739
+ import { buildBlobStorageRequest } from "@toon-protocol/core";
4740
+ var ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;
4741
+ async function requestBlobStorage(client, secretKey, params) {
4742
+ const bid = params.bid ?? (params.ilpAmount !== void 0 ? String(params.ilpAmount) : void 0);
4743
+ if (bid === void 0 || bid === "") {
4744
+ return {
4745
+ success: false,
4746
+ error: "requestBlobStorage requires a bid (or ilpAmount to derive it)"
4747
+ };
2864
4748
  }
2865
- for (const b of bytes) {
2866
- if (b === 0) result = "1" + result;
2867
- else break;
4749
+ const blobBuffer = Buffer.from(
4750
+ params.blobData.buffer,
4751
+ params.blobData.byteOffset,
4752
+ params.blobData.byteLength
4753
+ );
4754
+ let event;
4755
+ try {
4756
+ event = buildBlobStorageRequest(
4757
+ {
4758
+ blobData: blobBuffer,
4759
+ contentType: params.contentType,
4760
+ bid
4761
+ },
4762
+ secretKey
4763
+ );
4764
+ } catch (error) {
4765
+ return {
4766
+ success: false,
4767
+ error: error instanceof Error ? error.message : String(error)
4768
+ };
2868
4769
  }
2869
- return result;
4770
+ const result = await client.publishEvent(event, {
4771
+ destination: params.destination,
4772
+ claim: params.claim,
4773
+ ilpAmount: params.ilpAmount
4774
+ });
4775
+ if (!result.success) {
4776
+ return {
4777
+ success: false,
4778
+ eventId: result.eventId ?? event.id,
4779
+ error: result.error ?? "Blob storage request rejected"
4780
+ };
4781
+ }
4782
+ if (!result.data) {
4783
+ return {
4784
+ success: false,
4785
+ eventId: event.id,
4786
+ error: "FULFILL contained no data; expected base64-encoded Arweave tx ID"
4787
+ };
4788
+ }
4789
+ const txId = decodeUtf8(fromBase64(result.data));
4790
+ if (!ARWEAVE_TX_ID_REGEX.test(txId)) {
4791
+ return {
4792
+ success: false,
4793
+ eventId: event.id,
4794
+ error: `Decoded FULFILL data is not a valid Arweave tx ID: "${txId}"`
4795
+ };
4796
+ }
4797
+ return {
4798
+ success: true,
4799
+ txId,
4800
+ eventId: event.id
4801
+ };
2870
4802
  }
2871
4803
 
4804
+ // src/keys/KeyManager.ts
4805
+ import { finalizeEvent } from "nostr-tools/pure";
4806
+ import { nip19 } from "nostr-tools";
4807
+
2872
4808
  // src/keys/PasskeyAuth.ts
2873
4809
  async function registerPasskey(params) {
2874
4810
  const { rpId, rpName, userId, userName, prfSalt } = params;
@@ -3003,7 +4939,7 @@ function hexToBytes(hex) {
3003
4939
  }
3004
4940
  return bytes;
3005
4941
  }
3006
- function bytesToHex(bytes) {
4942
+ function bytesToHex2(bytes) {
3007
4943
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3008
4944
  }
3009
4945
 
@@ -3377,7 +5313,7 @@ var KeyManager = class {
3377
5313
  "Passkey did not return a userHandle. Cannot determine Nostr pubkey for recovery."
3378
5314
  );
3379
5315
  }
3380
- const pubkey = bytesToHex(discovery.userHandle);
5316
+ const pubkey = bytesToHex2(discovery.userHandle);
3381
5317
  const vault = await fetchBackupFromRelays(pubkey, this.config.relayUrls);
3382
5318
  if (!vault) {
3383
5319
  throw new Error(
@@ -3457,7 +5393,7 @@ var KeyManager = class {
3457
5393
  });
3458
5394
  const kek = await deriveKek(registration.prfOutput);
3459
5395
  const credIdHash = await hashCredentialId(registration.credentialId);
3460
- const hexKey = bytesToHex(secretKey);
5396
+ const hexKey = bytesToHex2(secretKey);
3461
5397
  this.vault = await createVault(hexKey, kek, credIdHash, prfSalt);
3462
5398
  this.activeCredentialIdHash = credIdHash;
3463
5399
  await this.saveToLocalStorage();
@@ -3769,31 +5705,195 @@ function openDb(name) {
3769
5705
  request.onerror = () => reject(request.error);
3770
5706
  });
3771
5707
  }
5708
+
5709
+ // src/keys/keystore-node.ts
5710
+ import {
5711
+ scryptSync,
5712
+ createCipheriv,
5713
+ createDecipheriv,
5714
+ randomBytes
5715
+ } from "crypto";
5716
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
5717
+ var SCRYPT_N = 2 ** 17;
5718
+ var SCRYPT_R = 8;
5719
+ var SCRYPT_P = 1;
5720
+ var SCRYPT_KEY_LEN = 32;
5721
+ var SCRYPT_MAXMEM = SCRYPT_N * SCRYPT_R * 256 + 32 * 1024 * 1024;
5722
+ var SALT_LEN = 32;
5723
+ var IV_LEN = 12;
5724
+ var AUTH_TAG_LEN = 16;
5725
+ function assertNode() {
5726
+ const versions = globalThis.process?.versions;
5727
+ if (!versions?.node) {
5728
+ throw new Error(
5729
+ "keystore-node is Node.js-only and cannot run in a browser. Use the Passkey/IndexedDB KeyManager for browser key storage."
5730
+ );
5731
+ }
5732
+ }
5733
+ function encryptMnemonic2(mnemonic, password) {
5734
+ assertNode();
5735
+ if (typeof mnemonic !== "string" || mnemonic.length === 0) {
5736
+ throw new Error("encryptMnemonic: mnemonic must be a non-empty string");
5737
+ }
5738
+ if (typeof password !== "string" || password.length === 0) {
5739
+ throw new Error("encryptMnemonic: password must be a non-empty string");
5740
+ }
5741
+ const salt = randomBytes(SALT_LEN);
5742
+ const iv = randomBytes(IV_LEN);
5743
+ const key = scryptSync(password, salt, SCRYPT_KEY_LEN, {
5744
+ N: SCRYPT_N,
5745
+ r: SCRYPT_R,
5746
+ p: SCRYPT_P,
5747
+ maxmem: SCRYPT_MAXMEM
5748
+ });
5749
+ try {
5750
+ const cipher = createCipheriv("aes-256-gcm", key, iv, {
5751
+ authTagLength: AUTH_TAG_LEN
5752
+ });
5753
+ const ciphertext = Buffer.concat([
5754
+ cipher.update(mnemonic, "utf8"),
5755
+ cipher.final()
5756
+ ]);
5757
+ const tag = cipher.getAuthTag();
5758
+ return {
5759
+ salt: salt.toString("base64"),
5760
+ iv: iv.toString("base64"),
5761
+ ciphertext: ciphertext.toString("base64"),
5762
+ tag: tag.toString("base64"),
5763
+ version: 1
5764
+ };
5765
+ } finally {
5766
+ key.fill(0);
5767
+ }
5768
+ }
5769
+ function decryptMnemonic2(encrypted, password) {
5770
+ assertNode();
5771
+ if (typeof password !== "string" || password.length === 0) {
5772
+ throw new Error("decryptMnemonic: password must be a non-empty string");
5773
+ }
5774
+ if (!encrypted || typeof encrypted.salt !== "string" || typeof encrypted.iv !== "string" || typeof encrypted.ciphertext !== "string" || typeof encrypted.tag !== "string") {
5775
+ throw new Error("decryptMnemonic: malformed keystore envelope");
5776
+ }
5777
+ const salt = Buffer.from(encrypted.salt, "base64");
5778
+ const iv = Buffer.from(encrypted.iv, "base64");
5779
+ const ciphertext = Buffer.from(encrypted.ciphertext, "base64");
5780
+ const tag = Buffer.from(encrypted.tag, "base64");
5781
+ const key = scryptSync(password, salt, SCRYPT_KEY_LEN, {
5782
+ N: SCRYPT_N,
5783
+ r: SCRYPT_R,
5784
+ p: SCRYPT_P,
5785
+ maxmem: SCRYPT_MAXMEM
5786
+ });
5787
+ try {
5788
+ const decipher = createDecipheriv("aes-256-gcm", key, iv, {
5789
+ authTagLength: AUTH_TAG_LEN
5790
+ });
5791
+ decipher.setAuthTag(tag);
5792
+ try {
5793
+ const plaintext = Buffer.concat([
5794
+ decipher.update(ciphertext),
5795
+ decipher.final()
5796
+ ]);
5797
+ return plaintext.toString("utf8");
5798
+ } catch {
5799
+ throw new Error(
5800
+ "Decryption failed: wrong password or corrupted keystore file"
5801
+ );
5802
+ }
5803
+ } finally {
5804
+ key.fill(0);
5805
+ }
5806
+ }
5807
+ function generateKeystore(path, password) {
5808
+ assertNode();
5809
+ const mnemonic = generateMnemonic();
5810
+ const keystore = encryptMnemonic2(mnemonic, password);
5811
+ writeKeystoreFile(path, keystore);
5812
+ return { mnemonic, keystore };
5813
+ }
5814
+ function importKeystore(path, mnemonic, password) {
5815
+ assertNode();
5816
+ if (!validateMnemonic(mnemonic)) {
5817
+ throw new Error(
5818
+ "Invalid BIP-39 mnemonic: checksum or word-list validation failed"
5819
+ );
5820
+ }
5821
+ const keystore = encryptMnemonic2(mnemonic, password);
5822
+ writeKeystoreFile(path, keystore);
5823
+ return keystore;
5824
+ }
5825
+ function loadKeystore(path, password) {
5826
+ assertNode();
5827
+ const raw = readFileSync2(path, "utf8");
5828
+ let parsed;
5829
+ try {
5830
+ parsed = JSON.parse(raw);
5831
+ } catch {
5832
+ throw new Error(`Keystore file at ${path} is not valid JSON`);
5833
+ }
5834
+ return decryptMnemonic2(parsed, password);
5835
+ }
5836
+ function writeKeystoreFile(path, keystore) {
5837
+ assertNode();
5838
+ writeFileSync2(path, JSON.stringify(keystore, null, 2), {
5839
+ encoding: "utf8",
5840
+ mode: 384
5841
+ });
5842
+ }
3772
5843
  export {
5844
+ ANON_ASSETS,
5845
+ ANON_VERSION,
3773
5846
  BtpRuntimeClient,
3774
5847
  ChannelManager,
3775
5848
  ConnectorError,
3776
5849
  EvmSigner,
5850
+ HS_HOSTNAME_MAX_LENGTH,
5851
+ HS_HOSTNAME_REGEX,
3777
5852
  HttpConnectorAdmin,
3778
5853
  HttpRuntimeClient,
3779
5854
  KeyManager,
5855
+ MinaSigner,
3780
5856
  NetworkError,
3781
5857
  OnChainChannelClient,
5858
+ SolanaSigner,
3782
5859
  ToonClient,
3783
5860
  ToonClientError,
3784
5861
  ValidationError,
3785
5862
  applyDefaults,
5863
+ applyNetworkPresets,
5864
+ assertRoutableHsHostname,
3786
5865
  buildBackupEvent,
3787
5866
  buildBackupFilter,
5867
+ buildPetInteractionRequest,
5868
+ buildPetListingEvent,
5869
+ buildPetPurchaseRequest,
3788
5870
  buildSettlementInfo,
5871
+ decryptMnemonic2 as decryptMnemonic,
3789
5872
  deriveFromNsec,
3790
5873
  deriveFullIdentity,
5874
+ deriveNostrKeyFromMnemonic,
5875
+ encryptMnemonic2 as encryptMnemonic,
5876
+ filterPetDvmProviders,
5877
+ filterPetListings,
5878
+ generateKeystore,
3791
5879
  generateMnemonic,
3792
5880
  generateRandomIdentity,
5881
+ getNetworkStatus,
5882
+ importKeystore,
3793
5883
  isPrfSupported,
5884
+ isRoutableHsHostname,
5885
+ loadKeystore,
3794
5886
  parseBackupPayload,
5887
+ parsePetInteractionEvent,
5888
+ parsePetInteractionResult,
5889
+ parsePetListing,
5890
+ readMinaDepositTotal,
5891
+ requestBlobStorage,
5892
+ selectAnonAsset,
5893
+ startManagedAnonProxy,
3795
5894
  validateConfig,
3796
5895
  validateMnemonic,
3797
- withRetry
5896
+ withRetry,
5897
+ writeKeystoreFile
3798
5898
  };
3799
5899
  //# sourceMappingURL=index.js.map