@toon-protocol/client 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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, finalizeEvent } 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 {
@@ -13,6 +21,7 @@ var ToonClientError = class extends Error {
13
21
  this.code = code;
14
22
  this.name = "ToonClientError";
15
23
  }
24
+ code;
16
25
  };
17
26
  var NetworkError = class extends ToonClientError {
18
27
  constructor(message, cause) {
@@ -51,7 +60,209 @@ var PeerAlreadyExistsError = class extends ToonClientError {
51
60
  }
52
61
  };
53
62
 
63
+ // src/keys/KeyDerivation.ts
64
+ import { generateSecretKey, getPublicKey } from "nostr-tools/pure";
65
+ import { privateKeyToAccount } from "viem/accounts";
66
+ import { toHex } from "viem";
67
+ import {
68
+ generateMnemonic as _genMnemonic,
69
+ validateMnemonic as _validateMnemonic,
70
+ mnemonicToSeedSync
71
+ } from "@scure/bip39";
72
+ import { wordlist as english } from "@scure/bip39/wordlists/english";
73
+ import { HDKey } from "@scure/bip32";
74
+ import { hexToMinaBase58PrivateKey } from "@toon-protocol/core";
75
+ function generateMnemonic() {
76
+ return _genMnemonic(english, 128);
77
+ }
78
+ function validateMnemonic(mnemonic) {
79
+ return _validateMnemonic(mnemonic, english);
80
+ }
81
+ var MAX_BIP32_INDEX = 2147483647;
82
+ function assertValidAccountIndex(accountIndex) {
83
+ if (!Number.isInteger(accountIndex) || accountIndex < 0 || accountIndex > MAX_BIP32_INDEX) {
84
+ throw new Error(
85
+ `Invalid accountIndex: expected a non-negative integer (0 to ${MAX_BIP32_INDEX}), got ${String(accountIndex)}`
86
+ );
87
+ }
88
+ }
89
+ function deriveNostrKey(seed, accountIndex = 0) {
90
+ const master = HDKey.fromMasterSeed(seed);
91
+ const child = master.derive(`m/44'/1237'/0'/0/${accountIndex}`);
92
+ if (!child.privateKey) {
93
+ throw new Error("Failed to derive Nostr private key from seed");
94
+ }
95
+ const secretKey = new Uint8Array(child.privateKey);
96
+ const pubkey = getPublicKey(secretKey);
97
+ return { secretKey, pubkey };
98
+ }
99
+ function deriveEvmIdentity(secretKey) {
100
+ const account = privateKeyToAccount(toHex(secretKey));
101
+ return {
102
+ privateKey: secretKey,
103
+ address: account.address
104
+ };
105
+ }
106
+ async function deriveSolanaKey(seed, accountIndex = 0) {
107
+ const { hmac } = await import("@noble/hashes/hmac");
108
+ const { sha512 } = await import("@noble/hashes/sha512");
109
+ const { ed25519: ed255194 } = await import("@noble/curves/ed25519.js");
110
+ const encoder = new TextEncoder();
111
+ let I = hmac(sha512, encoder.encode("ed25519 seed"), seed);
112
+ let key = I.slice(0, 32);
113
+ let chainCode = I.slice(32);
114
+ const indices = [
115
+ 2147483692,
116
+ // 44'
117
+ 2147484149,
118
+ // 501'
119
+ 2147483648 + accountIndex >>> 0,
120
+ // {accountIndex}'
121
+ 2147483648
122
+ // 0'
123
+ ];
124
+ for (const index of indices) {
125
+ const data = new Uint8Array(37);
126
+ data[0] = 0;
127
+ data.set(key, 1);
128
+ data[33] = index >>> 24 & 255;
129
+ data[34] = index >>> 16 & 255;
130
+ data[35] = index >>> 8 & 255;
131
+ data[36] = index & 255;
132
+ I = hmac(sha512, chainCode, data);
133
+ key = I.slice(0, 32);
134
+ chainCode = I.slice(32);
135
+ }
136
+ const publicKeyBytes = ed255194.getPublicKey(key);
137
+ const keypair = new Uint8Array(64);
138
+ keypair.set(key, 0);
139
+ keypair.set(publicKeyBytes, 32);
140
+ const publicKey = toBase58(publicKeyBytes);
141
+ return { secretKey: keypair, publicKey };
142
+ }
143
+ async function deriveMinaKey(seed, accountIndex = 0) {
144
+ const master = HDKey.fromMasterSeed(seed);
145
+ const child = master.derive(`m/44'/12586'/${accountIndex}'/0/0`);
146
+ if (!child.privateKey) {
147
+ throw new Error("Failed to derive Mina private key from seed");
148
+ }
149
+ const keyBytes = new Uint8Array(child.privateKey);
150
+ keyBytes[0] = (keyBytes[0] ?? 0) & 63;
151
+ try {
152
+ const MinaSignerLib = await import("mina-signer");
153
+ const Client = "default" in MinaSignerLib ? MinaSignerLib.default : MinaSignerLib;
154
+ const client = new Client({ network: "mainnet" });
155
+ const hexKey = Array.from(keyBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
156
+ const minaPrivateKey = hexToMinaBase58PrivateKey(hexKey);
157
+ const publicKey = client.derivePublicKey(minaPrivateKey);
158
+ return {
159
+ // Store the clamped big-endian hex scalar; consumers (e.g. the client's
160
+ // MinaSigner) re-convert to base58check via hexToMinaBase58PrivateKey.
161
+ privateKey: hexKey,
162
+ publicKey
163
+ };
164
+ } catch {
165
+ throw new Error(
166
+ "mina-signer is required for Mina key derivation. Install it as an optional dependency."
167
+ );
168
+ }
169
+ }
170
+ function deriveNostrKeyFromMnemonic(mnemonic, accountIndex = 0) {
171
+ assertValidAccountIndex(accountIndex);
172
+ const seed = mnemonicToSeedSync(mnemonic);
173
+ const result = deriveNostrKey(seed, accountIndex);
174
+ seed.fill(0);
175
+ return result;
176
+ }
177
+ async function deriveFullIdentity(mnemonic, accountIndex = 0) {
178
+ assertValidAccountIndex(accountIndex);
179
+ const seed = mnemonicToSeedSync(mnemonic);
180
+ const nostr = deriveNostrKey(seed, accountIndex);
181
+ const evm = deriveEvmIdentity(nostr.secretKey);
182
+ let solana;
183
+ try {
184
+ solana = await deriveSolanaKey(seed, accountIndex);
185
+ } catch {
186
+ solana = { secretKey: new Uint8Array(64), publicKey: "" };
187
+ }
188
+ let mina;
189
+ try {
190
+ mina = await deriveMinaKey(seed, accountIndex);
191
+ } catch {
192
+ mina = { privateKey: "", publicKey: "" };
193
+ }
194
+ seed.fill(0);
195
+ return { nostr, evm, solana, mina };
196
+ }
197
+ function deriveFromNsec(secretKey) {
198
+ const keyCopy = new Uint8Array(secretKey);
199
+ const pubkey = getPublicKey(keyCopy);
200
+ const evm = deriveEvmIdentity(keyCopy);
201
+ return {
202
+ nostr: { secretKey: keyCopy, pubkey },
203
+ evm,
204
+ solana: { secretKey: new Uint8Array(64), publicKey: "" },
205
+ mina: { privateKey: "", publicKey: "" }
206
+ };
207
+ }
208
+ function generateRandomIdentity() {
209
+ const secretKey = generateSecretKey();
210
+ return deriveFromNsec(secretKey);
211
+ }
212
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
213
+ function toBase58(bytes) {
214
+ let num = BigInt(0);
215
+ for (const b of bytes) num = num * 256n + BigInt(b);
216
+ let result = "";
217
+ while (num > 0n) {
218
+ result = BASE58_ALPHABET[Number(num % 58n)] + result;
219
+ num = num / 58n;
220
+ }
221
+ for (const b of bytes) {
222
+ if (b === 0) result = "1" + result;
223
+ else break;
224
+ }
225
+ return result;
226
+ }
227
+
54
228
  // src/config.ts
229
+ function applyNetworkPresets(config) {
230
+ const { network } = config;
231
+ if (!network || network === "custom") return config;
232
+ const presets = resolveClientNetwork(network);
233
+ const mergeRecord = (explicit, preset) => ({ ...preset, ...explicit });
234
+ const supportedChains = config.supportedChains ? Array.from(
235
+ /* @__PURE__ */ new Set([...presets.supportedChains, ...config.supportedChains])
236
+ ) : presets.supportedChains;
237
+ return {
238
+ ...config,
239
+ supportedChains,
240
+ chainRpcUrls: mergeRecord(config.chainRpcUrls, presets.chainRpcUrls),
241
+ preferredTokens: mergeRecord(
242
+ config.preferredTokens,
243
+ presets.preferredTokens
244
+ ),
245
+ tokenNetworks: mergeRecord(config.tokenNetworks, presets.tokenNetworks),
246
+ // settlementAddresses are identity-derived (per-client), so they have no
247
+ // preset; pass any explicit value through unchanged.
248
+ ...config.settlementAddresses && {
249
+ settlementAddresses: config.settlementAddresses
250
+ },
251
+ // Channel params: preset fills the deployed programId/zkApp + URLs unless
252
+ // the caller supplied their own (explicit object wins wholesale).
253
+ ...presets.solanaChannel && {
254
+ solanaChannel: config.solanaChannel ?? presets.solanaChannel
255
+ },
256
+ ...presets.minaChannel && {
257
+ minaChannel: config.minaChannel ?? presets.minaChannel
258
+ }
259
+ };
260
+ }
261
+ function getNetworkStatus(config) {
262
+ const { network } = config;
263
+ if (!network || network === "custom") return void 0;
264
+ return resolveClientNetwork(network).status;
265
+ }
55
266
  function validateConfig(config) {
56
267
  if (config.connector !== void 0) {
57
268
  throw new ValidationError(
@@ -80,6 +291,24 @@ function validateConfig(config) {
80
291
  );
81
292
  }
82
293
  }
294
+ if (config.mnemonic !== void 0) {
295
+ if (config.secretKey !== void 0) {
296
+ throw new ValidationError(
297
+ "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.)"
298
+ );
299
+ }
300
+ if (typeof config.mnemonic !== "string" || !validateMnemonic(config.mnemonic)) {
301
+ throw new ValidationError("mnemonic must be a valid BIP-39 phrase");
302
+ }
303
+ }
304
+ if (config.mnemonicAccountIndex !== void 0) {
305
+ const idx = config.mnemonicAccountIndex;
306
+ if (!Number.isInteger(idx) || idx < 0 || idx > 2147483647) {
307
+ throw new ValidationError(
308
+ "mnemonicAccountIndex must be a non-negative integer (0 to 2147483647)"
309
+ );
310
+ }
311
+ }
83
312
  if (!config.ilpInfo?.ilpAddress) {
84
313
  throw new ValidationError("ilpInfo.ilpAddress is required");
85
314
  }
@@ -117,6 +346,25 @@ function validateConfig(config) {
117
346
  );
118
347
  }
119
348
  }
349
+ if (config.transport) {
350
+ if (config.transport.type === "socks5") {
351
+ if (!config.transport.socksProxy?.startsWith("socks5h://")) {
352
+ throw new ValidationError(
353
+ 'transport.socksProxy must use socks5h:// scheme to prevent DNS leaks. The "h" suffix ensures .anyone hostnames are resolved by the proxy, not locally.'
354
+ );
355
+ }
356
+ } else if (config.transport.type === "gateway") {
357
+ if (!config.transport.gatewayUrl) {
358
+ throw new ValidationError(
359
+ "transport.gatewayUrl is required for gateway transport"
360
+ );
361
+ }
362
+ } else if (config.transport.type !== "direct") {
363
+ throw new ValidationError(
364
+ `Unknown transport type: "${config.transport.type}"`
365
+ );
366
+ }
367
+ }
120
368
  if (config.chainRpcUrls && config.supportedChains) {
121
369
  for (const chain of Object.keys(config.chainRpcUrls)) {
122
370
  if (!config.supportedChains.includes(chain)) {
@@ -127,8 +375,12 @@ function validateConfig(config) {
127
375
  }
128
376
  }
129
377
  }
130
- function applyDefaults(config) {
131
- const secretKey = config.secretKey ?? generateSecretKey();
378
+ function applyDefaults(rawConfig) {
379
+ const config = applyNetworkPresets(rawConfig);
380
+ const secretKey = config.secretKey ?? (config.mnemonic ? deriveNostrKeyFromMnemonic(
381
+ config.mnemonic,
382
+ config.mnemonicAccountIndex ?? 0
383
+ ).secretKey : generateSecretKey2());
132
384
  let btpUrl = config.btpUrl;
133
385
  if (!btpUrl && config.connectorUrl) {
134
386
  try {
@@ -175,7 +427,8 @@ function applyDefaults(config) {
175
427
  // Always set by logic above
176
428
  };
177
429
  }
178
- function buildSettlementInfo(config) {
430
+ function buildSettlementInfo(rawConfig) {
431
+ const config = applyNetworkPresets(rawConfig);
179
432
  if (!config.supportedChains?.length && !config.settlementAddresses && !config.preferredTokens && !config.tokenNetworks) {
180
433
  return void 0;
181
434
  }
@@ -210,7 +463,7 @@ function fromBase64(base64) {
210
463
  }
211
464
  return bytes;
212
465
  }
213
- function toHex(bytes) {
466
+ function toHex2(bytes) {
214
467
  let hex = "";
215
468
  for (const byte of bytes) {
216
469
  hex += byte.toString(16).padStart(2, "0");
@@ -220,6 +473,9 @@ function toHex(bytes) {
220
473
  function encodeUtf8(str) {
221
474
  return new TextEncoder().encode(str);
222
475
  }
476
+ function decodeUtf8(bytes) {
477
+ return new TextDecoder().decode(bytes);
478
+ }
223
479
  function isBase64(str) {
224
480
  return /^[A-Za-z0-9+/]*={0,2}$/.test(str);
225
481
  }
@@ -665,7 +921,7 @@ var IsomorphicBtpClient = class {
665
921
  if (this._isConnected) return;
666
922
  return new Promise((resolve, reject) => {
667
923
  try {
668
- this.ws = new WebSocket(this.config.url);
924
+ this.ws = this.config.createWebSocket ? this.config.createWebSocket(this.config.url) : new WebSocket(this.config.url);
669
925
  this.ws.binaryType = "arraybuffer";
670
926
  } catch (err) {
671
927
  reject(
@@ -689,8 +945,14 @@ var IsomorphicBtpClient = class {
689
945
  this.ws.onmessage = (event) => {
690
946
  this.handleMessage(event.data);
691
947
  };
692
- this.ws.onerror = () => {
693
- reject(new BtpConnectionError("WebSocket connection error"));
948
+ this.ws.onerror = (event) => {
949
+ const underlying = event?.error ?? event?.message;
950
+ const detail = underlying instanceof Error ? underlying.message : typeof underlying === "string" ? underlying : null;
951
+ reject(
952
+ new BtpConnectionError(
953
+ detail ? `WebSocket connection error: ${detail}` : "WebSocket connection error"
954
+ )
955
+ );
694
956
  };
695
957
  this.ws.onclose = () => {
696
958
  this._isConnected = false;
@@ -746,6 +1008,30 @@ var IsomorphicBtpClient = class {
746
1008
  this.pendingRequests.set(requestId, { resolve, reject, timeoutId });
747
1009
  });
748
1010
  }
1011
+ /**
1012
+ * Send a fire-and-forget BTP MESSAGE carrying only protocol data (no ILP
1013
+ * packet). Used for out-of-band claim notifications that the connector's
1014
+ * ClaimReceiver consumes via `handleClaimMessage` — there is no RESPONSE
1015
+ * frame, so we resolve immediately after the WebSocket buffers the bytes.
1016
+ *
1017
+ * Mirrors `sendPacket` wire-format construction but uses an empty ILP
1018
+ * payload and does not enroll a pending request.
1019
+ */
1020
+ async sendProtocolData(protocolName, contentType, data) {
1021
+ if (!this._isConnected || !this.ws) {
1022
+ throw new BtpConnectionError("Not connected");
1023
+ }
1024
+ const requestId = this.nextRequestId();
1025
+ const btpMessage = serializeBtpMessage({
1026
+ type: BTPMessageType.MESSAGE,
1027
+ requestId,
1028
+ data: {
1029
+ protocolData: [{ protocolName, contentType, data }],
1030
+ ilpPacket: new Uint8Array(0)
1031
+ }
1032
+ });
1033
+ this.ws.send(btpMessage);
1034
+ }
749
1035
  // ─── Private ────────────────────────────────────────────────────────────
750
1036
  async authenticate() {
751
1037
  if (!this.ws) throw new BtpAuthError("WebSocket not connected");
@@ -789,7 +1075,9 @@ var IsomorphicBtpClient = class {
789
1075
  if (message.type === BTPMessageType.ERROR) {
790
1076
  const errData = message.data;
791
1077
  reject(
792
- new BtpAuthError(`Authentication failed: ${errData.code}`)
1078
+ new BtpAuthError(
1079
+ `Authentication failed: ${errData.code} msg=${errData.message ?? ""} trigger=${errData.triggeredBy ?? ""}`
1080
+ )
793
1081
  );
794
1082
  } else if (message.type === BTPMessageType.RESPONSE) {
795
1083
  resolve();
@@ -901,7 +1189,8 @@ var BtpRuntimeClient = class {
901
1189
  this.btpClient = new IsomorphicBtpClient({
902
1190
  url: this.config.btpUrl,
903
1191
  peerId: this.config.peerId,
904
- authToken: this.config.authToken
1192
+ authToken: this.config.authToken,
1193
+ createWebSocket: this.config.createWebSocket
905
1194
  });
906
1195
  await this.btpClient.connect();
907
1196
  this._isConnected = true;
@@ -963,6 +1252,36 @@ var BtpRuntimeClient = class {
963
1252
  }
964
1253
  });
965
1254
  }
1255
+ /**
1256
+ * Send a standalone `payment-channel-claim` BTP MESSAGE (no ILP packet
1257
+ * attached). The connector's ClaimReceiver consumes this fire-and-forget
1258
+ * to register cumulative claim state independently of the per-packet
1259
+ * forwarding path. Auto-reconnects on connection errors.
1260
+ */
1261
+ async sendClaimMessage(claim) {
1262
+ return withRetry(() => this._sendClaimMessageOnce(claim), {
1263
+ maxRetries: this.config.maxRetries ?? 3,
1264
+ retryDelay: this.config.retryDelay ?? 1e3,
1265
+ shouldRetry: (error) => {
1266
+ if (!isConnectionError(error)) return false;
1267
+ this._isConnected = false;
1268
+ return true;
1269
+ }
1270
+ });
1271
+ }
1272
+ async _sendClaimMessageOnce(claim) {
1273
+ if (!this._isConnected) {
1274
+ await this.reconnect();
1275
+ }
1276
+ if (!this.btpClient) {
1277
+ throw new BtpConnectionError("BTP client not connected");
1278
+ }
1279
+ await this.btpClient.sendProtocolData(
1280
+ "payment-channel-claim",
1281
+ 1,
1282
+ encodeUtf8(JSON.stringify(claim))
1283
+ );
1284
+ }
966
1285
  /**
967
1286
  * Single-attempt ILP packet send. Reconnects if not connected.
968
1287
  */
@@ -1034,63 +1353,860 @@ var BtpRuntimeClient = class {
1034
1353
  }
1035
1354
  };
1036
1355
 
1037
- // src/channel/OnChainChannelClient.ts
1038
- import {
1039
- createPublicClient,
1040
- createWalletClient,
1041
- http,
1042
- maxUint256,
1043
- decodeEventLog,
1044
- defineChain
1045
- } 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
- ]
1091
- }
1092
- ];
1093
- var ERC20_ABI = [
1356
+ // src/adapters/HttpIlpClient.ts
1357
+ var ILP_CLAIM_HEADER = "ILP-Payment-Channel-Claim";
1358
+ var ILP_CLAIM_WRAPPED_HEADER = "ILP-Payment-Channel-Claim-Wrapped";
1359
+ var ILP_PEER_ID_HEADER = "ILP-Peer-Id";
1360
+ var HttpIlpClient = class {
1361
+ httpEndpoint;
1362
+ peerId;
1363
+ authToken;
1364
+ timeout;
1365
+ retryConfig;
1366
+ httpClient;
1367
+ createWebSocket;
1368
+ constructor(config) {
1369
+ this.httpEndpoint = config.httpEndpoint;
1370
+ this.peerId = config.peerId;
1371
+ this.authToken = config.authToken;
1372
+ this.timeout = config.timeout ?? 3e4;
1373
+ this.retryConfig = {
1374
+ maxRetries: config.maxRetries ?? 3,
1375
+ retryDelay: config.retryDelay ?? 1e3
1376
+ };
1377
+ this.httpClient = config.httpClient ?? fetch;
1378
+ this.createWebSocket = config.createWebSocket;
1379
+ }
1380
+ /**
1381
+ * Send an ILP PREPARE via `POST /ilp` WITHOUT a claim. The connector accepts
1382
+ * this only on free/zero-amount routes; paid writes must use
1383
+ * {@link sendIlpPacketWithClaim}. Satisfies the IlpClient interface.
1384
+ */
1385
+ async sendIlpPacket(params) {
1386
+ return withRetry(() => this.postPrepare(params), {
1387
+ maxRetries: this.retryConfig.maxRetries,
1388
+ retryDelay: this.retryConfig.retryDelay,
1389
+ exponentialBackoff: true,
1390
+ shouldRetry: (error) => error instanceof NetworkError
1391
+ });
1392
+ }
1393
+ /**
1394
+ * Send an ILP PREPARE via `POST /ilp` with the payment-channel claim attached
1395
+ * as the `ILP-Payment-Channel-Claim` header. `claim` is the SAME JSON object
1396
+ * the BTP path attaches as the `payment-channel-claim` protocolData entry —
1397
+ * we base64(JSON.stringify(claim)) it, byte-for-byte identical to BTP.
1398
+ */
1399
+ async sendIlpPacketWithClaim(params, claim) {
1400
+ return withRetry(() => this.postPrepare(params, claim), {
1401
+ maxRetries: this.retryConfig.maxRetries,
1402
+ retryDelay: this.retryConfig.retryDelay,
1403
+ exponentialBackoff: true,
1404
+ shouldRetry: (error) => error instanceof NetworkError
1405
+ });
1406
+ }
1407
+ /**
1408
+ * Upgrade to a duplex BTP session over the SAME endpoint.
1409
+ *
1410
+ * Derives the `ws(s)://` URL from `httpEndpoint`, opens a WebSocket with
1411
+ * `Sec-WebSocket-Protocol: btp` and the same `ILP-Peer-Id` + `Authorization`
1412
+ * headers, and returns a connected {@link BtpRuntimeClient}. When auth headers
1413
+ * are present the connector pre-authenticates the session (no in-band auth
1414
+ * frame); without them the BtpRuntimeClient falls back to the normal BTP
1415
+ * auth-frame flow.
1416
+ *
1417
+ * NOTE: passing per-connection headers + a subprotocol to a WebSocket is
1418
+ * Node-only (the `ws` package). Browsers cannot set arbitrary request headers
1419
+ * on a WebSocket handshake, so a browser consumer must use the gateway
1420
+ * transport or BTP-with-auth-frame instead.
1421
+ */
1422
+ async upgradeToBtp() {
1423
+ const btpUrl = httpEndpointToBtpUrl(this.httpEndpoint);
1424
+ const createWebSocket = this.createWebSocket ?? await makeBtpWebSocketFactory(this.authHeaders());
1425
+ const client = new BtpRuntimeClient({
1426
+ btpUrl,
1427
+ // BtpRuntimeClient sends an auth frame using these; when the connector
1428
+ // pre-authenticated via Upgrade headers it accepts the (redundant) frame.
1429
+ peerId: this.peerId ?? "client",
1430
+ authToken: this.authToken ?? "",
1431
+ createWebSocket
1432
+ });
1433
+ await client.connect();
1434
+ return client;
1435
+ }
1436
+ // ─── Private ──────────────────────────────────────────────────────────────
1437
+ authHeaders() {
1438
+ const headers = {};
1439
+ if (this.peerId) headers[ILP_PEER_ID_HEADER] = this.peerId;
1440
+ if (this.authToken) headers["Authorization"] = `Bearer ${this.authToken}`;
1441
+ return headers;
1442
+ }
1443
+ /**
1444
+ * Single attempt: serialize the PREPARE, POST it, and map the response.
1445
+ * @throws {NetworkError} On connection/timeout failures (retried).
1446
+ * @throws {ConnectorError} On non-retryable transport errors (5xx / unexpected).
1447
+ */
1448
+ async postPrepare(params, claim) {
1449
+ const requestTimeout = params.timeout ?? this.timeout;
1450
+ const prepare = serializeIlpPrepare({
1451
+ type: ILPPacketType.PREPARE,
1452
+ amount: BigInt(params.amount),
1453
+ destination: params.destination,
1454
+ executionCondition: new Uint8Array(32),
1455
+ expiresAt: new Date(Date.now() + requestTimeout),
1456
+ data: fromBase64(params.data)
1457
+ });
1458
+ const headers = {
1459
+ "Content-Type": "application/octet-stream",
1460
+ ...this.authHeaders()
1461
+ };
1462
+ if (claim !== void 0) {
1463
+ headers[ILP_CLAIM_HEADER] = toBase64(
1464
+ encodeUtf8(JSON.stringify(claim))
1465
+ );
1466
+ }
1467
+ const controller = new AbortController();
1468
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeout);
1469
+ try {
1470
+ const response = await this.httpClient(this.httpEndpoint, {
1471
+ method: "POST",
1472
+ headers,
1473
+ // Copy into a fresh ArrayBuffer so fetch sees a clean body, not a view.
1474
+ body: prepare.slice(),
1475
+ signal: controller.signal
1476
+ });
1477
+ clearTimeout(timeoutId);
1478
+ return await this.mapResponse(response);
1479
+ } catch (error) {
1480
+ clearTimeout(timeoutId);
1481
+ throw this.mapTransportError(error, requestTimeout);
1482
+ }
1483
+ }
1484
+ /**
1485
+ * Map a `200 OK` body (OER FULFILL/REJECT) to an IlpSendResult; map a non-2xx
1486
+ * to a transport error. Per the wire contract, ILP-level rejects arrive as a
1487
+ * 200 + REJECT body — only HTTP non-2xx means a transport-layer failure.
1488
+ */
1489
+ async mapResponse(response) {
1490
+ if (response.ok) {
1491
+ const buf = new Uint8Array(await response.arrayBuffer());
1492
+ if (buf.length === 0) {
1493
+ throw new ConnectorError("Empty 200 body from /ilp (expected OER ILP response)");
1494
+ }
1495
+ const ilp = deserializeIlpPacket(buf);
1496
+ if (ilp.type === ILPPacketType.FULFILL) {
1497
+ return {
1498
+ accepted: true,
1499
+ data: ilp.data.length > 0 ? toBase64(ilp.data) : void 0
1500
+ };
1501
+ }
1502
+ return {
1503
+ accepted: false,
1504
+ code: ilp.code,
1505
+ message: ilp.message,
1506
+ data: ilp.data.length > 0 ? toBase64(ilp.data) : void 0
1507
+ };
1508
+ }
1509
+ const body = await response.text().catch(() => "");
1510
+ const detail = body ? `: ${body}` : "";
1511
+ if (response.status >= 500) {
1512
+ throw new ConnectorError(
1513
+ `Connector transport error (${response.status} ${response.statusText})${detail}`
1514
+ );
1515
+ }
1516
+ throw new ConnectorError(
1517
+ `ILP-over-HTTP request rejected (${response.status} ${response.statusText})${detail}`
1518
+ );
1519
+ }
1520
+ mapTransportError(error, requestTimeout) {
1521
+ if (error instanceof ConnectorError || error instanceof NetworkError) {
1522
+ return error;
1523
+ }
1524
+ if (error instanceof Error && error.name === "AbortError") {
1525
+ return new NetworkError(`Request timeout after ${requestTimeout}ms`, error);
1526
+ }
1527
+ if (error instanceof TypeError && (error.message.includes("fetch failed") || error.message.includes("ECONNREFUSED") || error.message.includes("ECONNRESET") || error.message.includes("ETIMEDOUT") || error.message.includes("network"))) {
1528
+ return new NetworkError(`Network connection failed: ${error.message}`, error);
1529
+ }
1530
+ return new ConnectorError(
1531
+ `Unexpected error during ILP-over-HTTP request: ${error instanceof Error ? error.message : String(error)}`,
1532
+ error instanceof Error ? error : void 0
1533
+ );
1534
+ }
1535
+ };
1536
+ function httpEndpointToBtpUrl(httpEndpoint) {
1537
+ return httpEndpoint.replace(/^https:\/\//i, "wss://").replace(/^http:\/\//i, "ws://");
1538
+ }
1539
+ async function makeBtpWebSocketFactory(headers) {
1540
+ const { createRequire } = await import("module");
1541
+ const require2 = createRequire(import.meta.url);
1542
+ const WS = require2("ws");
1543
+ const ws = WS;
1544
+ const WSClass = typeof ws === "function" ? ws : typeof ws.default === "function" ? ws.default : typeof ws.WebSocket === "function" ? ws.WebSocket : null;
1545
+ if (WSClass === null) {
1546
+ throw new Error(
1547
+ "makeBtpWebSocketFactory: require('ws') did not yield a constructor on .default, .WebSocket, or the module root."
1548
+ );
1549
+ }
1550
+ return (url) => (
1551
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1552
+ new WSClass(url, "btp", { headers })
1553
+ );
1554
+ }
1555
+
1556
+ // src/adapters/selectIlpTransport.ts
1557
+ function readDiscoveredIlpPeer(peer) {
1558
+ const p = peer ?? {};
1559
+ return {
1560
+ btpEndpoint: typeof p["btpEndpoint"] === "string" ? p["btpEndpoint"] : void 0,
1561
+ httpEndpoint: typeof p["httpEndpoint"] === "string" ? p["httpEndpoint"] : void 0,
1562
+ supportsUpgrade: typeof p["supportsUpgrade"] === "boolean" ? p["supportsUpgrade"] : void 0
1563
+ };
1564
+ }
1565
+ function selectIlpTransport(peer, options = {}) {
1566
+ const needsDuplex = options.needsDuplex ?? false;
1567
+ const http2 = peer.httpEndpoint?.trim() || void 0;
1568
+ const btp = peer.btpEndpoint?.trim() || void 0;
1569
+ const canUpgrade = peer.supportsUpgrade === true;
1570
+ if (needsDuplex) {
1571
+ if (btp) return { kind: "btp", btpEndpoint: btp };
1572
+ if (http2 && canUpgrade) return { kind: "http-upgradable", httpEndpoint: http2 };
1573
+ throw new Error(
1574
+ "Duplex transport required but peer exposes neither a btpEndpoint nor an upgradable httpEndpoint"
1575
+ );
1576
+ }
1577
+ if (http2) return { kind: "http", httpEndpoint: http2, canUpgrade };
1578
+ if (btp) return { kind: "btp", btpEndpoint: btp };
1579
+ throw new Error("Peer exposes neither an httpEndpoint nor a btpEndpoint");
1580
+ }
1581
+
1582
+ // src/channel/OnChainChannelClient.ts
1583
+ import {
1584
+ createPublicClient,
1585
+ createWalletClient,
1586
+ http,
1587
+ maxUint256,
1588
+ decodeEventLog,
1589
+ defineChain
1590
+ } from "viem";
1591
+ import { ed25519 as ed255192 } from "@noble/curves/ed25519.js";
1592
+ import { base58Encode as base58Encode2 } from "@toon-protocol/core";
1593
+
1594
+ // src/channel/solana-payment-channel.ts
1595
+ import { ed25519 } from "@noble/curves/ed25519.js";
1596
+ import { sha256 } from "@noble/hashes/sha2.js";
1597
+ import { base58Encode, base58Decode } from "@toon-protocol/core";
1598
+ var TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
1599
+ var SYSTEM_PROGRAM_ID = "11111111111111111111111111111111";
1600
+ var RENT_SYSVAR_ID = "SysvarRent111111111111111111111111111111111";
1601
+ var IX_INITIALIZE_CHANNEL = new Uint8Array([1, 0, 0, 0, 0, 0, 0, 0]);
1602
+ var IX_DEPOSIT = new Uint8Array([2, 0, 0, 0, 0, 0, 0, 0]);
1603
+ var CHANNEL_DISCRIMINATOR = new Uint8Array([
1604
+ 112,
1605
+ 99,
1606
+ 104,
1607
+ 97,
1608
+ 110,
1609
+ 110,
1610
+ 101,
1611
+ 108
1612
+ ]);
1613
+ var CHANNEL_ACCOUNT_SIZE = 178;
1614
+ var MAX_U64 = (1n << 64n) - 1n;
1615
+ function writeU64LE(buf, offset, value) {
1616
+ if (value < 0n || value > MAX_U64) {
1617
+ throw new RangeError(`Value ${value} outside u64 range [0, 2^64-1]`);
1618
+ }
1619
+ for (let i = 0; i < 8; i++) {
1620
+ buf[offset + i] = Number(value >> BigInt(i * 8) & 0xffn);
1621
+ }
1622
+ }
1623
+ function padTo32(bytes) {
1624
+ if (bytes.length === 32) return bytes;
1625
+ if (bytes.length > 32) return bytes.slice(bytes.length - 32);
1626
+ const padded = new Uint8Array(32);
1627
+ padded.set(bytes, 32 - bytes.length);
1628
+ return padded;
1629
+ }
1630
+ function sortPubkeys(a, b) {
1631
+ for (let i = 0; i < 32; i++) {
1632
+ const ai = a[i] ?? 0;
1633
+ const bi = b[i] ?? 0;
1634
+ if (ai < bi) return [a, b];
1635
+ if (ai > bi) return [b, a];
1636
+ }
1637
+ return [a, b];
1638
+ }
1639
+ function modPow(base, exp, mod) {
1640
+ let result = 1n;
1641
+ base = (base % mod + mod) % mod;
1642
+ while (exp > 0n) {
1643
+ if (exp & 1n) result = result * base % mod;
1644
+ exp >>= 1n;
1645
+ base = base * base % mod;
1646
+ }
1647
+ return result;
1648
+ }
1649
+ function modInverse(a, m) {
1650
+ return modPow((a % m + m) % m, m - 2n, m);
1651
+ }
1652
+ function isOnCurve(bytes) {
1653
+ const P = (1n << 255n) - 19n;
1654
+ const yBytes = new Uint8Array(32);
1655
+ yBytes.set(bytes);
1656
+ yBytes[31] = (yBytes[31] ?? 0) & 127;
1657
+ let y = 0n;
1658
+ for (let i = 0; i < 32; i++) {
1659
+ y |= BigInt(yBytes[i] ?? 0) << BigInt(i * 8);
1660
+ }
1661
+ if (y >= P) return true;
1662
+ const y2 = y * y % P;
1663
+ const D = (P - 121665n * modInverse(121666n, P) % P + P) % P;
1664
+ const numerator = (y2 - 1n + P) % P;
1665
+ const denominator = (D * y2 + 1n) % P;
1666
+ const x2 = numerator * modInverse(denominator, P) % P;
1667
+ if (x2 === 0n) return true;
1668
+ return modPow(x2, (P - 1n) / 2n, P) === 1n;
1669
+ }
1670
+ function findProgramAddress(seeds, programId) {
1671
+ const PDA_MARKER = new TextEncoder().encode("ProgramDerivedAddress");
1672
+ for (let bump = 255; bump >= 0; bump--) {
1673
+ const allSeeds = [...seeds, new Uint8Array([bump])];
1674
+ let totalLen = programId.length + PDA_MARKER.length;
1675
+ for (const s of allSeeds) totalLen += s.length;
1676
+ const input = new Uint8Array(totalLen);
1677
+ let offset = 0;
1678
+ for (const s of allSeeds) {
1679
+ input.set(s, offset);
1680
+ offset += s.length;
1681
+ }
1682
+ input.set(programId, offset);
1683
+ offset += programId.length;
1684
+ input.set(PDA_MARKER, offset);
1685
+ const hash = sha256(input);
1686
+ if (!isOnCurve(hash)) return { pda: hash, bump };
1687
+ }
1688
+ throw new Error("Could not find a viable PDA bump seed");
1689
+ }
1690
+ function deriveChannelPDA(participantA, participantB, tokenMint, programId) {
1691
+ const a = padTo32(base58Decode(participantA));
1692
+ const b = padTo32(base58Decode(participantB));
1693
+ const mint = padTo32(base58Decode(tokenMint));
1694
+ const program = padTo32(base58Decode(programId));
1695
+ const [min, max] = sortPubkeys(a, b);
1696
+ const seeds = [new TextEncoder().encode("channel"), min, max, mint];
1697
+ const { pda, bump } = findProgramAddress(seeds, program);
1698
+ return { pda: base58Encode(pda), bump };
1699
+ }
1700
+ function deriveVaultPDA(channelPDA, programId) {
1701
+ const channel = padTo32(base58Decode(channelPDA));
1702
+ const program = padTo32(base58Decode(programId));
1703
+ const seeds = [new TextEncoder().encode("vault"), channel];
1704
+ const { pda, bump } = findProgramAddress(seeds, program);
1705
+ return { pda: base58Encode(pda), bump };
1706
+ }
1707
+ function buildBalanceProofMessage(channelPDA, nonce, transferredAmount) {
1708
+ const message = new Uint8Array(48);
1709
+ message.set(padTo32(base58Decode(channelPDA)), 0);
1710
+ writeU64LE(message, 32, nonce);
1711
+ writeU64LE(message, 40, transferredAmount);
1712
+ return message;
1713
+ }
1714
+ var rpcIdCounter = 1;
1715
+ async function solanaRpc(rpcUrl, method, params = []) {
1716
+ const res = await fetch(rpcUrl, {
1717
+ method: "POST",
1718
+ headers: { "Content-Type": "application/json" },
1719
+ body: JSON.stringify({
1720
+ jsonrpc: "2.0",
1721
+ method,
1722
+ params,
1723
+ id: rpcIdCounter++
1724
+ }),
1725
+ signal: AbortSignal.timeout(3e4)
1726
+ });
1727
+ const json = await res.json();
1728
+ if (json.error) {
1729
+ throw new Error(
1730
+ `Solana RPC error [${method}]: ${json.error.message} (code ${json.error.code})`
1731
+ );
1732
+ }
1733
+ return json.result;
1734
+ }
1735
+ async function getLatestBlockhash(rpcUrl) {
1736
+ const result = await solanaRpc(rpcUrl, "getLatestBlockhash", [
1737
+ { commitment: "confirmed" }
1738
+ ]);
1739
+ return result.value.blockhash;
1740
+ }
1741
+ async function getAccountInfo(rpcUrl, pubkey) {
1742
+ const result = await solanaRpc(rpcUrl, "getAccountInfo", [
1743
+ pubkey,
1744
+ { encoding: "base64", commitment: "confirmed" }
1745
+ ]);
1746
+ return result.value;
1747
+ }
1748
+ async function waitForConfirmation(rpcUrl, signature, timeoutMs = 3e4) {
1749
+ const start = Date.now();
1750
+ while (Date.now() - start < timeoutMs) {
1751
+ const result = await solanaRpc(rpcUrl, "getSignatureStatuses", [
1752
+ [signature]
1753
+ ]);
1754
+ const status = result.value[0];
1755
+ if (status?.confirmationStatus === "confirmed" || status?.confirmationStatus === "finalized") {
1756
+ if (status.err) {
1757
+ throw new Error(
1758
+ `Transaction ${signature} failed: ${JSON.stringify(status.err)}`
1759
+ );
1760
+ }
1761
+ return;
1762
+ }
1763
+ await new Promise((r) => setTimeout(r, 500));
1764
+ }
1765
+ throw new Error(
1766
+ `Transaction ${signature} not confirmed within ${timeoutMs}ms`
1767
+ );
1768
+ }
1769
+ function compactU16Size(value) {
1770
+ if (value > 65535) {
1771
+ throw new RangeError(`compact-u16 value ${value} exceeds u16 max (0xFFFF)`);
1772
+ }
1773
+ return value < 128 ? 1 : value < 16384 ? 2 : 3;
1774
+ }
1775
+ function writeCompactU16(buf, offset, value) {
1776
+ if (value < 128) {
1777
+ buf[offset++] = value;
1778
+ } else if (value < 16384) {
1779
+ buf[offset++] = value & 127 | 128;
1780
+ buf[offset++] = value >> 7;
1781
+ } else {
1782
+ buf[offset++] = value & 127 | 128;
1783
+ buf[offset++] = value >> 7 & 127 | 128;
1784
+ buf[offset++] = value >> 14;
1785
+ }
1786
+ return offset;
1787
+ }
1788
+ async function buildAndSendTransaction(rpcUrl, feePayer, instructions, additionalSigners = []) {
1789
+ const blockhash = await getLatestBlockhash(rpcUrl);
1790
+ const feePayerPubkey = base58Encode(feePayer.publicKey);
1791
+ const accountMap = /* @__PURE__ */ new Map();
1792
+ accountMap.set(feePayerPubkey, {
1793
+ pubkey: feePayerPubkey,
1794
+ isSigner: true,
1795
+ isWritable: true
1796
+ });
1797
+ for (const ix of instructions) {
1798
+ for (const key of ix.keys) {
1799
+ const existing = accountMap.get(key.pubkey);
1800
+ if (existing) {
1801
+ existing.isSigner = existing.isSigner || key.isSigner;
1802
+ existing.isWritable = existing.isWritable || key.isWritable;
1803
+ } else {
1804
+ accountMap.set(key.pubkey, { ...key });
1805
+ }
1806
+ }
1807
+ if (!accountMap.has(ix.programId)) {
1808
+ accountMap.set(ix.programId, {
1809
+ pubkey: ix.programId,
1810
+ isSigner: false,
1811
+ isWritable: false
1812
+ });
1813
+ }
1814
+ }
1815
+ const accounts = [...accountMap.values()].sort((a, b) => {
1816
+ if (a.pubkey === feePayerPubkey) return -1;
1817
+ if (b.pubkey === feePayerPubkey) return 1;
1818
+ const aScore = (a.isSigner ? 2 : 0) + (a.isWritable ? 1 : 0);
1819
+ const bScore = (b.isSigner ? 2 : 0) + (b.isWritable ? 1 : 0);
1820
+ return bScore - aScore;
1821
+ });
1822
+ const numSigners = accounts.filter((a) => a.isSigner).length;
1823
+ const numReadonlySigners = accounts.filter(
1824
+ (a) => a.isSigner && !a.isWritable
1825
+ ).length;
1826
+ const numReadonlyNonSigners = accounts.filter(
1827
+ (a) => !a.isSigner && !a.isWritable
1828
+ ).length;
1829
+ const accountIndexMap = /* @__PURE__ */ new Map();
1830
+ accounts.forEach((a, i) => accountIndexMap.set(a.pubkey, i));
1831
+ const compiled = instructions.map((ix) => ({
1832
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- programId added to accountMap above
1833
+ programIdIndex: accountIndexMap.get(ix.programId),
1834
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- every key added to accountMap above
1835
+ accountIndices: ix.keys.map((k) => accountIndexMap.get(k.pubkey)),
1836
+ data: ix.data
1837
+ }));
1838
+ const blockhashBytes = base58Decode(blockhash);
1839
+ let instructionSize = compactU16Size(compiled.length);
1840
+ for (const ix of compiled) {
1841
+ instructionSize += 1;
1842
+ instructionSize += compactU16Size(ix.accountIndices.length) + ix.accountIndices.length;
1843
+ instructionSize += compactU16Size(ix.data.length) + ix.data.length;
1844
+ }
1845
+ const messageSize = 3 + compactU16Size(accounts.length) + 32 * accounts.length + 32 + instructionSize;
1846
+ const message = new Uint8Array(messageSize);
1847
+ let offset = 0;
1848
+ message[offset++] = numSigners;
1849
+ message[offset++] = numReadonlySigners;
1850
+ message[offset++] = numReadonlyNonSigners;
1851
+ offset = writeCompactU16(message, offset, accounts.length);
1852
+ for (const acct of accounts) {
1853
+ message.set(padTo32(base58Decode(acct.pubkey)), offset);
1854
+ offset += 32;
1855
+ }
1856
+ message.set(padTo32(blockhashBytes), offset);
1857
+ offset += 32;
1858
+ offset = writeCompactU16(message, offset, compiled.length);
1859
+ for (const ix of compiled) {
1860
+ message[offset++] = ix.programIdIndex;
1861
+ offset = writeCompactU16(message, offset, ix.accountIndices.length);
1862
+ for (const idx of ix.accountIndices) message[offset++] = idx;
1863
+ offset = writeCompactU16(message, offset, ix.data.length);
1864
+ message.set(ix.data, offset);
1865
+ offset += ix.data.length;
1866
+ }
1867
+ const finalMessage = message.slice(0, offset);
1868
+ const allSigners = [feePayer, ...additionalSigners];
1869
+ const signerPubkeys = accounts.filter((a) => a.isSigner).map((a) => a.pubkey);
1870
+ const signatures = [];
1871
+ for (const signerPubkey of signerPubkeys) {
1872
+ const signer = allSigners.find(
1873
+ (s) => base58Encode(s.publicKey) === signerPubkey
1874
+ );
1875
+ if (!signer) throw new Error(`Missing signer for ${signerPubkey}`);
1876
+ signatures.push(ed25519.sign(finalMessage, signer.privateKey));
1877
+ }
1878
+ const txSize = compactU16Size(signatures.length) + signatures.length * 64 + finalMessage.length;
1879
+ const tx = new Uint8Array(txSize);
1880
+ let txOffset = 0;
1881
+ txOffset = writeCompactU16(tx, txOffset, signatures.length);
1882
+ for (const sig of signatures) {
1883
+ tx.set(sig, txOffset);
1884
+ txOffset += 64;
1885
+ }
1886
+ tx.set(finalMessage, txOffset);
1887
+ const txBase64 = Buffer.from(tx).toString("base64");
1888
+ const txSig = await solanaRpc(rpcUrl, "sendTransaction", [
1889
+ txBase64,
1890
+ {
1891
+ encoding: "base64",
1892
+ skipPreflight: false,
1893
+ preflightCommitment: "confirmed"
1894
+ }
1895
+ ]);
1896
+ await waitForConfirmation(rpcUrl, txSig);
1897
+ return txSig;
1898
+ }
1899
+ var STATE_MAP = ["opened", "closed", "settled"];
1900
+ async function getChannelAccountState(rpcUrl, channelPDA) {
1901
+ const info = await getAccountInfo(rpcUrl, channelPDA);
1902
+ if (!info) return { exists: false };
1903
+ const data = new Uint8Array(Buffer.from(info.data[0], "base64"));
1904
+ if (data.length < CHANNEL_ACCOUNT_SIZE) return { exists: false };
1905
+ for (let i = 0; i < 8; i++) {
1906
+ if (data[i] !== CHANNEL_DISCRIMINATOR[i]) return { exists: false };
1907
+ }
1908
+ return {
1909
+ exists: true,
1910
+ state: STATE_MAP[data[160] ?? 0] ?? "opened",
1911
+ participantA: base58Encode(data.slice(8, 40)),
1912
+ participantB: base58Encode(data.slice(40, 72))
1913
+ };
1914
+ }
1915
+ async function openSolanaChannel(params) {
1916
+ const {
1917
+ rpcUrl,
1918
+ programId,
1919
+ tokenMint,
1920
+ payerSeed,
1921
+ payerPubkey,
1922
+ peerPubkey,
1923
+ challengeDuration
1924
+ } = params;
1925
+ const { pda: channelPDA } = deriveChannelPDA(
1926
+ payerPubkey,
1927
+ peerPubkey,
1928
+ tokenMint,
1929
+ programId
1930
+ );
1931
+ const existing = await getChannelAccountState(rpcUrl, channelPDA);
1932
+ if (existing.exists) {
1933
+ return { channelPDA, opened: false };
1934
+ }
1935
+ const payerPublicKey = padTo32(base58Decode(payerPubkey));
1936
+ const payer = { publicKey: payerPublicKey, privateKey: payerSeed };
1937
+ const { pda: vaultPDA } = deriveVaultPDA(channelPDA, programId);
1938
+ const initData = new Uint8Array(16);
1939
+ initData.set(IX_INITIALIZE_CHANNEL, 0);
1940
+ writeU64LE(initData, 8, challengeDuration);
1941
+ const initTxSignature = await buildAndSendTransaction(rpcUrl, payer, [
1942
+ {
1943
+ programId,
1944
+ keys: [
1945
+ { pubkey: payerPubkey, isSigner: true, isWritable: true },
1946
+ { pubkey: payerPubkey, isSigner: false, isWritable: false },
1947
+ // participant A
1948
+ { pubkey: peerPubkey, isSigner: false, isWritable: false },
1949
+ // participant B
1950
+ { pubkey: tokenMint, isSigner: false, isWritable: false },
1951
+ { pubkey: channelPDA, isSigner: false, isWritable: true },
1952
+ { pubkey: vaultPDA, isSigner: false, isWritable: true },
1953
+ { pubkey: SYSTEM_PROGRAM_ID, isSigner: false, isWritable: false },
1954
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
1955
+ { pubkey: RENT_SYSVAR_ID, isSigner: false, isWritable: false }
1956
+ ],
1957
+ data: initData
1958
+ }
1959
+ ]);
1960
+ let depositTxSignature;
1961
+ if (params.deposit && params.deposit.amount > 0n) {
1962
+ const depositData = new Uint8Array(16);
1963
+ depositData.set(IX_DEPOSIT, 0);
1964
+ writeU64LE(depositData, 8, params.deposit.amount);
1965
+ depositTxSignature = await buildAndSendTransaction(rpcUrl, payer, [
1966
+ {
1967
+ programId,
1968
+ keys: [
1969
+ { pubkey: payerPubkey, isSigner: true, isWritable: false },
1970
+ {
1971
+ pubkey: params.deposit.payerTokenAccount,
1972
+ isSigner: false,
1973
+ isWritable: true
1974
+ },
1975
+ { pubkey: vaultPDA, isSigner: false, isWritable: true },
1976
+ { pubkey: channelPDA, isSigner: false, isWritable: true },
1977
+ { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }
1978
+ ],
1979
+ data: depositData
1980
+ }
1981
+ ]);
1982
+ }
1983
+ return { channelPDA, opened: true, initTxSignature, depositTxSignature };
1984
+ }
1985
+
1986
+ // src/channel/mina-channel-open.ts
1987
+ import { hexToMinaBase58PrivateKey as hexToMinaBase58PrivateKey2 } from "@toon-protocol/core";
1988
+ var cachedO1js = null;
1989
+ var cachedPaymentChannel = null;
1990
+ var compiledContract = null;
1991
+ var runtimeOverride = null;
1992
+ async function loadMinaRuntime() {
1993
+ if (cachedO1js && cachedPaymentChannel) {
1994
+ return { o1js: cachedO1js, PaymentChannel: cachedPaymentChannel };
1995
+ }
1996
+ if (runtimeOverride) {
1997
+ const injected = await runtimeOverride();
1998
+ cachedO1js = injected.o1js;
1999
+ cachedPaymentChannel = injected.PaymentChannel;
2000
+ return injected;
2001
+ }
2002
+ const { createRequire } = await import("module");
2003
+ const nodePath = await import("path");
2004
+ const requireHere = createRequire(import.meta.url);
2005
+ const mzkPkgPath = requireHere.resolve(
2006
+ "@toon-protocol/mina-zkapp/package.json"
2007
+ );
2008
+ const requireFromMzk = createRequire(mzkPkgPath);
2009
+ const o1js = requireFromMzk("o1js");
2010
+ const mzkPkgJson = requireFromMzk(mzkPkgPath);
2011
+ const mzkDir = nodePath.dirname(mzkPkgPath);
2012
+ const mzkEntry = nodePath.join(mzkDir, mzkPkgJson.main ?? "dist/index.js");
2013
+ const mzk = requireFromMzk(mzkEntry);
2014
+ const PaymentChannel = mzk.PaymentChannel ?? mzk.default?.PaymentChannel;
2015
+ if (!PaymentChannel) {
2016
+ throw new Error(
2017
+ "@toon-protocol/mina-zkapp does not export PaymentChannel \u2014 cannot open a Mina channel"
2018
+ );
2019
+ }
2020
+ cachedO1js = o1js;
2021
+ cachedPaymentChannel = PaymentChannel;
2022
+ return { o1js, PaymentChannel };
2023
+ }
2024
+ async function getO1js() {
2025
+ return (await loadMinaRuntime()).o1js;
2026
+ }
2027
+ async function getCompiledPaymentChannel() {
2028
+ const { PaymentChannel } = await loadMinaRuntime();
2029
+ if (!compiledContract) {
2030
+ await PaymentChannel.compile();
2031
+ compiledContract = PaymentChannel;
2032
+ }
2033
+ return compiledContract;
2034
+ }
2035
+ var MINA_CHANNEL_STATE_OPEN = 1n;
2036
+ var MINA_CHANNEL_STATE_UNINITIALIZED = 0n;
2037
+ async function openMinaChannelOnChain(params) {
2038
+ const { Mina, PrivateKey, PublicKey, Field, AccountUpdate, fetchAccount } = await getO1js();
2039
+ const network = Mina.Network(params.graphqlUrl);
2040
+ Mina.setActiveInstance(network);
2041
+ const txFee = params.feeNanomina ?? 100000000n;
2042
+ const payerKeyBase58 = hexToMinaBase58PrivateKey2(params.payerPrivateKey);
2043
+ const payerPrivateKey = PrivateKey.fromBase58(payerKeyBase58);
2044
+ const payerPublicKey = payerPrivateKey.toPublicKey();
2045
+ const zkAppPublicKey = PublicKey.fromBase58(params.zkAppAddress);
2046
+ const readChannelState = async () => {
2047
+ const res = await fetchAccount({ publicKey: zkAppPublicKey });
2048
+ if (res.error || !res.account) {
2049
+ throw new Error(
2050
+ `Mina zkApp account ${params.zkAppAddress} not found on-chain (${String(
2051
+ res.error
2052
+ )}) \u2014 deploy the PaymentChannel zkApp before opening a channel`
2053
+ );
2054
+ }
2055
+ const appState = res.account.zkapp?.appState;
2056
+ const raw = appState?.[3]?.toString() ?? "0";
2057
+ return BigInt(raw);
2058
+ };
2059
+ const readDepositTotal = async () => {
2060
+ const res = await fetchAccount({ publicKey: zkAppPublicKey });
2061
+ if (res.error || !res.account) {
2062
+ throw new Error(
2063
+ `Mina zkApp account ${params.zkAppAddress} not found on-chain (${String(
2064
+ res.error
2065
+ )}) \u2014 deploy the PaymentChannel zkApp before opening a channel`
2066
+ );
2067
+ }
2068
+ const appState = res.account.zkapp?.appState;
2069
+ const raw = appState?.[4]?.toString() ?? "0";
2070
+ return BigInt(raw);
2071
+ };
2072
+ const currentState = await readChannelState();
2073
+ await fetchAccount({ publicKey: payerPublicKey });
2074
+ let opened = false;
2075
+ let initTxHash;
2076
+ let zkApp;
2077
+ const getZkApp = async () => {
2078
+ if (!zkApp) {
2079
+ const PaymentChannel = await getCompiledPaymentChannel();
2080
+ zkApp = new PaymentChannel(zkAppPublicKey);
2081
+ }
2082
+ return zkApp;
2083
+ };
2084
+ if (currentState === MINA_CHANNEL_STATE_UNINITIALIZED) {
2085
+ const channel = await getZkApp();
2086
+ const participantA = payerPublicKey;
2087
+ const participantB = params.peerPublicKey ? PublicKey.fromBase58(params.peerPublicKey) : payerPublicKey;
2088
+ const nonce = Field(0);
2089
+ const timeoutField = Field((params.timeout ?? 86400n).toString());
2090
+ const tokenIdField = Field(params.tokenId ?? "1");
2091
+ await fetchAccount({ publicKey: zkAppPublicKey });
2092
+ await fetchAccount({ publicKey: payerPublicKey });
2093
+ const initTx = await Mina.transaction(
2094
+ { sender: payerPublicKey, fee: Number(txFee) },
2095
+ async () => {
2096
+ await channel.initializeChannel(
2097
+ participantA,
2098
+ participantB,
2099
+ nonce,
2100
+ timeoutField,
2101
+ tokenIdField
2102
+ );
2103
+ }
2104
+ );
2105
+ await initTx.prove();
2106
+ const sentInit = await initTx.sign([payerPrivateKey]).send();
2107
+ initTxHash = sentInit.hash ?? void 0;
2108
+ opened = true;
2109
+ await sentInit.wait();
2110
+ await fetchAccount({ publicKey: zkAppPublicKey });
2111
+ await fetchAccount({ publicKey: payerPublicKey });
2112
+ } else if (currentState !== MINA_CHANNEL_STATE_OPEN) {
2113
+ throw new Error(
2114
+ `Mina channel ${params.zkAppAddress} is in state ${currentState} (not UNINITIALIZED/OPEN) \u2014 cannot open`
2115
+ );
2116
+ }
2117
+ let depositTxHash;
2118
+ if (params.deposit && params.deposit.amount > 0n) {
2119
+ const channel = await getZkApp();
2120
+ await fetchAccount({ publicKey: zkAppPublicKey });
2121
+ const amountField = Field(params.deposit.amount.toString());
2122
+ const depositTx = await Mina.transaction(
2123
+ { sender: payerPublicKey, fee: Number(txFee) },
2124
+ async () => {
2125
+ await channel.deposit(amountField, payerPublicKey);
2126
+ }
2127
+ );
2128
+ await depositTx.prove();
2129
+ const sentDeposit = await depositTx.sign([payerPrivateKey]).send();
2130
+ depositTxHash = sentDeposit.hash ?? void 0;
2131
+ await sentDeposit.wait();
2132
+ await fetchAccount({ publicKey: zkAppPublicKey });
2133
+ await fetchAccount({ publicKey: payerPublicKey });
2134
+ }
2135
+ let finalState;
2136
+ try {
2137
+ finalState = Number(await readChannelState());
2138
+ } catch {
2139
+ finalState = opened ? Number(MINA_CHANNEL_STATE_OPEN) : Number(currentState);
2140
+ }
2141
+ if (opened && finalState === Number(MINA_CHANNEL_STATE_UNINITIALIZED)) {
2142
+ finalState = Number(MINA_CHANNEL_STATE_OPEN);
2143
+ }
2144
+ void AccountUpdate;
2145
+ let depositTotal;
2146
+ try {
2147
+ depositTotal = await readDepositTotal();
2148
+ } catch {
2149
+ depositTotal = 0n;
2150
+ }
2151
+ return {
2152
+ zkAppAddress: params.zkAppAddress,
2153
+ opened,
2154
+ initTxHash,
2155
+ depositTxHash,
2156
+ channelState: finalState,
2157
+ depositTotal
2158
+ };
2159
+ }
2160
+
2161
+ // src/channel/OnChainChannelClient.ts
2162
+ var TOKEN_NETWORK_ABI = [
2163
+ {
2164
+ name: "openChannel",
2165
+ type: "function",
2166
+ stateMutability: "nonpayable",
2167
+ inputs: [
2168
+ { name: "participant2", type: "address" },
2169
+ { name: "settlementTimeout", type: "uint256" }
2170
+ ],
2171
+ outputs: [{ type: "bytes32" }]
2172
+ },
2173
+ {
2174
+ name: "setTotalDeposit",
2175
+ type: "function",
2176
+ stateMutability: "nonpayable",
2177
+ inputs: [
2178
+ { name: "channelId", type: "bytes32" },
2179
+ { name: "participant", type: "address" },
2180
+ { name: "totalDeposit", type: "uint256" }
2181
+ ],
2182
+ outputs: []
2183
+ },
2184
+ {
2185
+ name: "channels",
2186
+ type: "function",
2187
+ stateMutability: "view",
2188
+ inputs: [{ type: "bytes32" }],
2189
+ outputs: [
2190
+ { name: "settlementTimeout", type: "uint256" },
2191
+ { name: "state", type: "uint8" },
2192
+ { name: "closedAt", type: "uint256" },
2193
+ { name: "openedAt", type: "uint256" },
2194
+ { name: "participant1", type: "address" },
2195
+ { name: "participant2", type: "address" }
2196
+ ]
2197
+ },
2198
+ {
2199
+ name: "ChannelOpened",
2200
+ type: "event",
2201
+ inputs: [
2202
+ { name: "channelId", type: "bytes32", indexed: true },
2203
+ { name: "participant1", type: "address", indexed: true },
2204
+ { name: "participant2", type: "address", indexed: true },
2205
+ { name: "settlementTimeout", type: "uint256", indexed: false }
2206
+ ]
2207
+ }
2208
+ ];
2209
+ var ERC20_ABI = [
1094
2210
  {
1095
2211
  name: "approve",
1096
2212
  type: "function",
@@ -1112,7 +2228,7 @@ var ERC20_ABI = [
1112
2228
  outputs: [{ type: "uint256" }]
1113
2229
  }
1114
2230
  ];
1115
- var STATE_MAP = {
2231
+ var STATE_MAP2 = {
1116
2232
  0: "settled",
1117
2233
  1: "open",
1118
2234
  2: "closed",
@@ -1130,6 +2246,29 @@ var OnChainChannelClient = class {
1130
2246
  this.solanaConfig = config.solanaConfig;
1131
2247
  this.minaConfig = config.minaConfig;
1132
2248
  }
2249
+ /**
2250
+ * Late-binds the Solana channel config.
2251
+ *
2252
+ * `ToonClient.start()` derives the Solana Ed25519 keypair from the client's
2253
+ * mnemonic asynchronously (after this client is constructed), so the keypair
2254
+ * is injected here rather than at construction. Same keypair as the
2255
+ * registered Solana signer — guarantees the channel-open key and the
2256
+ * claim-signing key match.
2257
+ */
2258
+ setSolanaConfig(config) {
2259
+ this.solanaConfig = config;
2260
+ }
2261
+ /**
2262
+ * Late-binds the Mina channel config.
2263
+ *
2264
+ * Parallel to `setSolanaConfig`: `ToonClient.start()` derives the Mina private
2265
+ * key from the client's mnemonic asynchronously (after this client is
2266
+ * constructed), so the key is injected here rather than at construction. Same
2267
+ * key as the registered Mina signer.
2268
+ */
2269
+ setMinaConfig(config) {
2270
+ this.minaConfig = config;
2271
+ }
1133
2272
  /**
1134
2273
  * Parse chain identifier to extract chainId.
1135
2274
  * Format: "evm:{network}:{chainId}" e.g., "evm:anvil:31337"
@@ -1196,7 +2335,19 @@ var OnChainChannelClient = class {
1196
2335
  return this.openEvmChannel(params);
1197
2336
  }
1198
2337
  /**
1199
- * Opens a Solana payment channel (PDA creation).
2338
+ * Opens a REAL on-chain Solana payment channel.
2339
+ *
2340
+ * Derives the connector-parity channel PDA
2341
+ * (`[b"channel", min_pubkey, max_pubkey, token_mint]`), submits the
2342
+ * `initialize_channel` instruction (+ optional `deposit`) to the deployed
2343
+ * payment-channel program, and returns the base58 PDA as the channel id. That
2344
+ * PDA is what the claim carries as `channelAccount`, and the on-chain channel
2345
+ * is what the connector's `verifySolanaClaim` reads via
2346
+ * `provider.getChannelState` before accepting the claim.
2347
+ *
2348
+ * Mirrors `openEvmChannel`'s open(+deposit) structure. Idempotent: if the
2349
+ * channel account already exists on-chain, returns its PDA without
2350
+ * re-initializing.
1200
2351
  */
1201
2352
  async openSolanaChannel(params) {
1202
2353
  if (!this.solanaConfig) {
@@ -1204,23 +2355,72 @@ var OnChainChannelClient = class {
1204
2355
  "Solana channel config not provided \u2014 cannot open Solana channel"
1205
2356
  );
1206
2357
  }
1207
- const encoder = new TextEncoder();
1208
- const channelSeed = encoder.encode(
1209
- `channel:${toHex(this.solanaConfig.keypair).slice(0, 32)}:${params.peerAddress}:${Date.now()}`
2358
+ const cfg = this.solanaConfig;
2359
+ const payerSeed = cfg.keypair.slice(0, 32);
2360
+ const payerPubkey = base58Encode2(
2361
+ new Uint8Array(ed255192.getPublicKey(payerSeed))
1210
2362
  );
1211
- const channelIdBytes = new Uint8Array(
1212
- await crypto.subtle.digest("SHA-256", channelSeed)
2363
+ const tokenMint = params.token ?? cfg.tokenMint;
2364
+ if (!tokenMint) {
2365
+ throw new Error(
2366
+ "Solana channel requires a token mint (OpenChannelParams.token or solanaConfig.tokenMint)"
2367
+ );
2368
+ }
2369
+ if (!params.peerAddress) {
2370
+ throw new Error(
2371
+ "Solana channel requires peerAddress (apex settlement pubkey, base58)"
2372
+ );
2373
+ }
2374
+ const challengeDuration = BigInt(
2375
+ cfg.challengeDuration ?? params.settlementTimeout ?? 86400
1213
2376
  );
1214
- const channelId = "0x" + toHex(channelIdBytes);
1215
- this.channelContext.set(channelId, {
2377
+ const deposit = cfg.deposit ? {
2378
+ amount: BigInt(cfg.deposit.amount),
2379
+ payerTokenAccount: cfg.deposit.payerTokenAccount
2380
+ } : void 0;
2381
+ const { channelPDA } = await openSolanaChannel({
2382
+ rpcUrl: cfg.rpcUrl,
2383
+ programId: cfg.programId,
2384
+ tokenMint,
2385
+ payerSeed,
2386
+ payerPubkey,
2387
+ peerPubkey: params.peerAddress,
2388
+ challengeDuration,
2389
+ deposit
2390
+ });
2391
+ this.channelContext.set(channelPDA, {
1216
2392
  chain: params.chain,
1217
- tokenNetworkAddress: this.solanaConfig.programId
2393
+ tokenNetworkAddress: cfg.programId
1218
2394
  });
1219
- return { channelId, status: "opening" };
2395
+ return { channelId: channelPDA, status: "opening" };
1220
2396
  }
1221
2397
  /**
1222
- * Opens a Mina payment channel (zkApp state transition).
1223
- * Dynamically imports o1js to avoid bundle bloat.
2398
+ * Opens a REAL on-chain Mina payment channel on the deployed `PaymentChannel`
2399
+ * zkApp.
2400
+ *
2401
+ * The zkApp is deployed out-of-band (the operator/e2e harness deploys it
2402
+ * deterministically and advertises its B62 address). This client then calls
2403
+ * `initializeChannel` on that zkApp so its on-chain `channelState` becomes
2404
+ * `OPEN` — which is what the connector's `MinaPaymentChannelSDK.getChannelState`
2405
+ * reads to return status `'opened'` (claim verification otherwise fails with
2406
+ * `mina_claim_verification_failed`). The deployed zkApp address IS the channel
2407
+ * id: `MinaClaimMessage.zkAppAddress` is both the claim's channel identifier
2408
+ * AND the channel-hash preimage the off-chain proof binds to (see
2409
+ * `mina-payment-channel.ts`), so the channel-open id and the claim's channel id
2410
+ * are guaranteed identical.
2411
+ *
2412
+ * This is the Mina analog of `openSolanaChannel` (connector#105): the client
2413
+ * opens its own per-channel on-chain state (initialize + optional deposit). The
2414
+ * heavyweight o1js + `@toon-protocol/mina-zkapp` proof work is lazily imported
2415
+ * inside `openMinaChannelOnChain` so npm consumers who never open a Mina
2416
+ * channel don't pay the o1js cost.
2417
+ *
2418
+ * Idempotent: if the on-chain channel is already `OPEN`, the opener returns
2419
+ * without re-initializing.
2420
+ *
2421
+ * NOTE: full on-chain Mina SETTLE remains gated by the connector-side
2422
+ * settlement-executor (the same blocker that stops the Solana SETTLE); reaching
2423
+ * `opened` + a stored claim is parity with Solana.
1224
2424
  */
1225
2425
  async openMinaChannel(params) {
1226
2426
  if (!this.minaConfig) {
@@ -1228,19 +2428,41 @@ var OnChainChannelClient = class {
1228
2428
  "Mina channel config not provided \u2014 cannot open Mina channel"
1229
2429
  );
1230
2430
  }
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)
2431
+ const zkAppAddress = this.minaConfig.zkAppAddress;
2432
+ if (!zkAppAddress) {
2433
+ throw new Error(
2434
+ "Mina channel requires a deployed zkAppAddress (minaConfig.zkAppAddress)"
2435
+ );
2436
+ }
2437
+ if (!params.peerAddress) {
2438
+ throw new Error(
2439
+ "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"
2440
+ );
2441
+ }
2442
+ const timeout = BigInt(
2443
+ this.minaConfig.challengeDuration ?? params.settlementTimeout ?? 86400
1237
2444
  );
1238
- const channelId = "0x" + toHex(channelIdBytes);
1239
- this.channelContext.set(channelId, {
2445
+ const deposit = this.minaConfig.deposit ? { amount: BigInt(this.minaConfig.deposit.amount) } : void 0;
2446
+ const openResult = await openMinaChannelOnChain({
2447
+ graphqlUrl: this.minaConfig.graphqlUrl,
2448
+ zkAppAddress,
2449
+ payerPrivateKey: this.minaConfig.privateKey,
2450
+ // params.peerAddress is the apex Mina settlement B62 pubkey (participantB).
2451
+ peerPublicKey: params.peerAddress,
2452
+ timeout,
2453
+ tokenId: this.minaConfig.tokenId,
2454
+ deposit,
2455
+ networkId: this.minaConfig.networkId
2456
+ });
2457
+ this.channelContext.set(zkAppAddress, {
1240
2458
  chain: params.chain,
1241
- tokenNetworkAddress: this.minaConfig.zkAppAddress
2459
+ tokenNetworkAddress: zkAppAddress
1242
2460
  });
1243
- return { channelId, status: "opening" };
2461
+ return {
2462
+ channelId: zkAppAddress,
2463
+ status: "opening",
2464
+ depositTotal: openResult.depositTotal
2465
+ };
1244
2466
  }
1245
2467
  /**
1246
2468
  * Opens an EVM payment channel on-chain.
@@ -1336,6 +2558,17 @@ var OnChainChannelClient = class {
1336
2558
  `No context for channel "${channelId}". Channel must be opened via this client first.`
1337
2559
  );
1338
2560
  }
2561
+ if (context.chain.split(":")[0] === "mina") {
2562
+ return { channelId, status: "open", chain: context.chain };
2563
+ }
2564
+ if (context.chain.split(":")[0] === "solana" && this.solanaConfig) {
2565
+ const account = await getChannelAccountState(
2566
+ this.solanaConfig.rpcUrl,
2567
+ channelId
2568
+ );
2569
+ const status2 = !account.exists ? "settled" : account.state === "opened" ? "open" : account.state === "closed" ? "closed" : "settled";
2570
+ return { channelId, status: status2, chain: context.chain };
2571
+ }
1339
2572
  const { publicClient } = this.createClients(context.chain);
1340
2573
  const result = await publicClient.readContract({
1341
2574
  address: context.tokenNetworkAddress,
@@ -1344,7 +2577,7 @@ var OnChainChannelClient = class {
1344
2577
  args: [channelId]
1345
2578
  });
1346
2579
  const [, state] = result;
1347
- const status = STATE_MAP[state] ?? "settled";
2580
+ const status = STATE_MAP2[state] ?? "settled";
1348
2581
  return {
1349
2582
  channelId,
1350
2583
  status,
@@ -1354,8 +2587,8 @@ var OnChainChannelClient = class {
1354
2587
  };
1355
2588
 
1356
2589
  // src/signing/evm-signer.ts
1357
- import { privateKeyToAccount } from "viem/accounts";
1358
- import { toHex as toHex2 } from "viem";
2590
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
2591
+ import { toHex as toHex3 } from "viem";
1359
2592
  function getBalanceProofDomain(chainId, tokenNetworkAddress) {
1360
2593
  return {
1361
2594
  name: "TokenNetwork",
@@ -1382,11 +2615,11 @@ var EvmSigner = class {
1382
2615
  constructor(privateKey) {
1383
2616
  let hexKey;
1384
2617
  if (privateKey instanceof Uint8Array) {
1385
- hexKey = toHex2(privateKey);
2618
+ hexKey = toHex3(privateKey);
1386
2619
  } else {
1387
2620
  hexKey = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
1388
2621
  }
1389
- this._account = privateKeyToAccount(hexKey);
2622
+ this._account = privateKeyToAccount2(hexKey);
1390
2623
  }
1391
2624
  /** Derived 0x EVM address */
1392
2625
  get address() {
@@ -1465,24 +2698,118 @@ var EvmSigner = class {
1465
2698
  }
1466
2699
  };
1467
2700
 
2701
+ // src/transport/index.ts
2702
+ function isAnyoneHost(url) {
2703
+ if (!url) return false;
2704
+ try {
2705
+ const withScheme = /:\/\//.test(url) ? url : `ws://${url}`;
2706
+ const host = new URL(withScheme).hostname.toLowerCase();
2707
+ return host.endsWith(".anyone");
2708
+ } catch {
2709
+ return false;
2710
+ }
2711
+ }
2712
+ async function resolveTransport(transport, originalBtpUrl, originalConnectorUrl, managedProxyOptions) {
2713
+ const hasExplicitProxy = !!transport && (transport.type === "socks5" || transport.type === "gateway");
2714
+ const envProxy = process.env["ANYONE_PROXY_URLS"];
2715
+ if (!hasExplicitProxy && managedProxyOptions?.managedAnonProxy !== false && !envProxy && isAnyoneHost(originalBtpUrl)) {
2716
+ const { startManagedAnonProxy: startManagedAnonProxy2 } = await import("./anon-proxy-W3KMM7GU.js");
2717
+ const { createSocks5WebSocketFactory, createSocks5Fetch } = await import("./socks5-WTJBYGME.js");
2718
+ const proxy = await startManagedAnonProxy2({
2719
+ ...managedProxyOptions?.managedAnonSocksPort !== void 0 ? { socksPort: managedProxyOptions.managedAnonSocksPort } : {}
2720
+ });
2721
+ try {
2722
+ return {
2723
+ createWebSocket: createSocks5WebSocketFactory(proxy.socksProxy),
2724
+ httpClient: createSocks5Fetch(proxy.socksProxy),
2725
+ stopManagedProxy: proxy.stop
2726
+ };
2727
+ } catch (err) {
2728
+ await proxy.stop();
2729
+ throw err;
2730
+ }
2731
+ }
2732
+ if (!transport || transport.type === "direct") {
2733
+ return {};
2734
+ }
2735
+ if (transport.type === "socks5") {
2736
+ const {
2737
+ createSocks5WebSocketFactory,
2738
+ createSocks5Fetch,
2739
+ probeSocks5Proxy
2740
+ } = await import("./socks5-WTJBYGME.js");
2741
+ await probeSocks5Proxy(transport.socksProxy);
2742
+ return {
2743
+ createWebSocket: createSocks5WebSocketFactory(transport.socksProxy),
2744
+ httpClient: createSocks5Fetch(transport.socksProxy)
2745
+ };
2746
+ }
2747
+ if (transport.type === "gateway") {
2748
+ const { rewriteUrlsForGateway } = await import("./gateway-QOK47RKS.js");
2749
+ const rewritten = rewriteUrlsForGateway(
2750
+ transport.gatewayUrl,
2751
+ originalBtpUrl,
2752
+ originalConnectorUrl
2753
+ );
2754
+ return {
2755
+ btpUrl: rewritten.btpUrl,
2756
+ connectorUrl: rewritten.connectorUrl
2757
+ };
2758
+ }
2759
+ throw new Error(
2760
+ `Unknown transport type: "${transport.type}"`
2761
+ );
2762
+ }
2763
+
1468
2764
  // src/modes/http.ts
1469
2765
  async function initializeHttpMode(config) {
1470
- const connectorUrl = config.connectorUrl;
2766
+ const transport = await resolveTransport(
2767
+ config.transport,
2768
+ config.btpUrl,
2769
+ config.connectorUrl,
2770
+ {
2771
+ ...config.managedAnonProxy !== void 0 ? { managedAnonProxy: config.managedAnonProxy } : {},
2772
+ ...config.managedAnonSocksPort !== void 0 ? { managedAnonSocksPort: config.managedAnonSocksPort } : {}
2773
+ }
2774
+ );
2775
+ const effectiveBtpUrl = transport.btpUrl ?? config.btpUrl;
2776
+ const effectiveConnectorUrl = transport.connectorUrl ?? config.connectorUrl;
1471
2777
  const settlementInfo = buildSettlementInfo(config);
2778
+ const discoveredPeer = readDiscoveredIlpPeer({
2779
+ btpEndpoint: effectiveBtpUrl,
2780
+ httpEndpoint: config.connectorHttpEndpoint,
2781
+ supportsUpgrade: config.connectorSupportsUpgrade
2782
+ });
2783
+ const transportChoice = discoveredPeer.httpEndpoint || discoveredPeer.btpEndpoint ? selectIlpTransport(discoveredPeer, { needsDuplex: false }) : null;
1472
2784
  let btpClient = null;
1473
- if (config.btpUrl) {
2785
+ if (effectiveBtpUrl) {
1474
2786
  btpClient = new BtpRuntimeClient({
1475
- btpUrl: config.btpUrl,
2787
+ btpUrl: effectiveBtpUrl,
1476
2788
  peerId: config.btpPeerId ?? `client`,
1477
- authToken: config.btpAuthToken ?? ""
2789
+ authToken: config.btpAuthToken ?? "",
2790
+ createWebSocket: transport.createWebSocket
1478
2791
  });
1479
2792
  await btpClient.connect();
1480
2793
  }
1481
- const runtimeClient = btpClient ?? new HttpRuntimeClient({
1482
- connectorUrl,
2794
+ let httpIlpClient = null;
2795
+ if (transportChoice && (transportChoice.kind === "http" || transportChoice.kind === "http-upgradable")) {
2796
+ httpIlpClient = new HttpIlpClient({
2797
+ httpEndpoint: transportChoice.httpEndpoint,
2798
+ ...config.btpPeerId !== void 0 ? { peerId: config.btpPeerId } : {},
2799
+ ...config.btpAuthToken !== void 0 ? { authToken: config.btpAuthToken } : {},
2800
+ timeout: config.queryTimeout,
2801
+ maxRetries: config.maxRetries,
2802
+ retryDelay: config.retryDelay,
2803
+ ...transport.httpClient !== void 0 ? { httpClient: transport.httpClient } : {},
2804
+ ...transport.createWebSocket !== void 0 ? { createWebSocket: transport.createWebSocket } : {}
2805
+ });
2806
+ }
2807
+ const runtimeClient = httpIlpClient ?? btpClient ?? new HttpRuntimeClient({
2808
+ connectorUrl: effectiveConnectorUrl,
1483
2809
  timeout: config.queryTimeout,
1484
2810
  maxRetries: config.maxRetries,
1485
- retryDelay: config.retryDelay
2811
+ retryDelay: config.retryDelay,
2812
+ httpClient: transport.httpClient
1486
2813
  });
1487
2814
  let onChainChannelClient = null;
1488
2815
  if (config.chainRpcUrls) {
@@ -1517,19 +2844,395 @@ async function initializeHttpMode(config) {
1517
2844
  if (onChainChannelClient) {
1518
2845
  bootstrapService.setChannelClient(onChainChannelClient);
1519
2846
  }
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
- }
2847
+ const discoveryTracker = createDiscoveryTracker({
2848
+ secretKey: config.secretKey,
2849
+ settlementInfo
2850
+ });
2851
+ return {
2852
+ bootstrapService,
2853
+ discoveryTracker,
2854
+ runtimeClient,
2855
+ adminClient: null,
2856
+ btpClient,
2857
+ onChainChannelClient,
2858
+ // Teardown handle for a managed `anon` proxy this init STARTED (undefined
2859
+ // for explicit-proxy/direct/gateway). ToonClient.stop() invokes it.
2860
+ stopManagedProxy: transport.stopManagedProxy
2861
+ };
2862
+ }
2863
+
2864
+ // src/signing/solana-signer.ts
2865
+ import { ed25519 as ed255193 } from "@noble/curves/ed25519.js";
2866
+ import { base58Encode as base58Encode3 } from "@toon-protocol/core";
2867
+ var SolanaSigner = class {
2868
+ chainType = "solana";
2869
+ /** 32-byte Ed25519 seed. */
2870
+ privateKey;
2871
+ pubkeyBase58Cache;
2872
+ /**
2873
+ * @param privateKey - 32-byte Ed25519 seed (e.g. `identity.solana.secretKey.slice(0, 32)`).
2874
+ * @param publicKeyBase58 - Optional base58 public key (e.g. `identity.solana.publicKey`).
2875
+ * When omitted it is derived lazily from `privateKey`.
2876
+ */
2877
+ constructor(privateKey, publicKeyBase58) {
2878
+ if (privateKey.length !== 32) {
2879
+ throw new Error(
2880
+ `SolanaSigner requires a 32-byte Ed25519 seed, got ${privateKey.length} bytes`
2881
+ );
2882
+ }
2883
+ this.privateKey = privateKey;
2884
+ this.pubkeyBase58Cache = publicKeyBase58;
2885
+ }
2886
+ ensurePublicKey() {
2887
+ if (this.pubkeyBase58Cache) return this.pubkeyBase58Cache;
2888
+ const pk = ed255193.getPublicKey(this.privateKey);
2889
+ this.pubkeyBase58Cache = base58Encode3(new Uint8Array(pk));
2890
+ return this.pubkeyBase58Cache;
2891
+ }
2892
+ get signerIdentifier() {
2893
+ return this.pubkeyBase58Cache ?? "uninitialized";
2894
+ }
2895
+ async signBalanceProof(params) {
2896
+ if (params.metadata.chainType !== "solana") {
2897
+ throw new Error(
2898
+ `SolanaSigner cannot sign for chain type: ${params.metadata.chainType}`
2899
+ );
2900
+ }
2901
+ const base58 = this.ensurePublicKey();
2902
+ const message = buildBalanceProofMessage(
2903
+ params.channelId,
2904
+ BigInt(params.nonce),
2905
+ params.transferredAmount
2906
+ );
2907
+ const signature = ed255193.sign(message, this.privateKey);
2908
+ const signatureHex = "0x" + toHex2(new Uint8Array(signature));
2909
+ return {
2910
+ channelId: params.channelId,
2911
+ nonce: params.nonce,
2912
+ transferredAmount: params.transferredAmount,
2913
+ lockedAmount: params.lockedAmount,
2914
+ locksRoot: params.locksRoot,
2915
+ signature: signatureHex,
2916
+ signerAddress: base58,
2917
+ chainId: 0,
2918
+ tokenNetworkAddress: params.metadata.programId,
2919
+ recipient: params.recipient
2920
+ };
2921
+ }
2922
+ buildClaimMessage(proof, senderId) {
2923
+ const sigHex = proof.signature.startsWith("0x") ? proof.signature.slice(2) : proof.signature;
2924
+ const sigBytes = Uint8Array.from(
2925
+ sigHex.match(/.{1,2}/g)?.map((b) => parseInt(b, 16)) ?? []
2926
+ );
2927
+ const signatureBase64 = Buffer.from(sigBytes).toString("base64");
2928
+ const claim = {
2929
+ version: "1.0",
2930
+ blockchain: "solana",
2931
+ messageId: crypto.randomUUID(),
2932
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, ".000Z"),
2933
+ senderId,
2934
+ // channelId IS the base58 channel PDA -> connector's channelAccount.
2935
+ channelAccount: proof.channelId,
2936
+ nonce: proof.nonce,
2937
+ transferredAmount: proof.transferredAmount.toString(),
2938
+ signature: signatureBase64,
2939
+ signerPublicKey: this.pubkeyBase58Cache ?? proof.signerAddress,
2940
+ programId: proof.tokenNetworkAddress
2941
+ };
2942
+ return claim;
2943
+ }
2944
+ };
2945
+
2946
+ // src/signing/mina-signer.ts
2947
+ import { hexToMinaBase58PrivateKey as hexToMinaBase58PrivateKey3 } from "@toon-protocol/core";
2948
+ import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
2949
+ import { bytesToHex } from "@noble/hashes/utils.js";
2950
+
2951
+ // src/channel/mina-payment-channel.ts
2952
+ var cachedBindings = null;
2953
+ async function loadMinaPaymentChannelBindings() {
2954
+ if (cachedBindings) return cachedBindings;
2955
+ const specifier = "mina-signer";
2956
+ const lib = await import(
2957
+ /* @vite-ignore */
2958
+ specifier
2959
+ );
2960
+ const Client = "default" in lib ? lib.default : lib;
2961
+ const resolveFn = import.meta.resolve;
2962
+ let mainUrl;
2963
+ if (typeof resolveFn === "function") {
2964
+ mainUrl = resolveFn(specifier);
2965
+ } else {
2966
+ const { createRequire } = await import("module");
2967
+ const { pathToFileURL } = await import("url");
2968
+ mainUrl = pathToFileURL(
2969
+ createRequire(import.meta.url).resolve(specifier)
2970
+ ).href;
2971
+ }
2972
+ const minaSignerDir = new URL("./", mainUrl);
2973
+ const poseidonUrl = new URL("../bindings/crypto/poseidon.js", minaSignerDir).href;
2974
+ const signatureUrl = new URL("./src/signature.js", minaSignerDir).href;
2975
+ const curveUrl = new URL("./src/curve-bigint.js", minaSignerDir).href;
2976
+ const [poseidonMod, signatureMod, curveMod] = await Promise.all([
2977
+ import(
2978
+ /* @vite-ignore */
2979
+ poseidonUrl
2980
+ ),
2981
+ import(
2982
+ /* @vite-ignore */
2983
+ signatureUrl
2984
+ ),
2985
+ import(
2986
+ /* @vite-ignore */
2987
+ curveUrl
2988
+ )
2989
+ ]);
2990
+ cachedBindings = {
2991
+ Client,
2992
+ Poseidon: poseidonMod.Poseidon,
2993
+ Signature: signatureMod.Signature,
2994
+ PublicKey: curveMod.PublicKey
2995
+ };
2996
+ return cachedBindings;
2997
+ }
2998
+ function minaBalanceCommitment(poseidon, balanceA, balanceB, salt) {
2999
+ return poseidon.hash([balanceA, balanceB, salt]);
3000
+ }
3001
+ function minaChannelHashField(poseidon, publicKeyCodec, zkAppAddress) {
3002
+ const zkAppPubKey = publicKeyCodec.fromBase58(zkAppAddress);
3003
+ return poseidon.hash([zkAppPubKey.x]);
3004
+ }
3005
+ function minaParticipantChannelHashField(poseidon, publicKeyCodec, participantA_B62, participantB_B62, channelNonce) {
3006
+ const a = publicKeyCodec.fromBase58(participantA_B62);
3007
+ const b = publicKeyCodec.fromBase58(participantB_B62);
3008
+ return poseidon.hash([a.x, b.x, channelNonce]);
3009
+ }
3010
+ async function buildMinaPaymentChannelProof(params) {
3011
+ const { Client, Poseidon, Signature, PublicKey } = await loadMinaPaymentChannelBindings();
3012
+ const client = new Client({ network: "devnet" });
3013
+ const signerPublicKey = params.signerPublicKey ?? client.derivePublicKey(params.minaPrivateKeyBase58);
3014
+ const commitment = minaBalanceCommitment(
3015
+ Poseidon,
3016
+ params.balanceA,
3017
+ params.balanceB,
3018
+ params.salt
3019
+ );
3020
+ const channelHashField = params.participantA && params.participantB ? minaParticipantChannelHashField(
3021
+ Poseidon,
3022
+ PublicKey,
3023
+ params.participantA,
3024
+ params.participantB,
3025
+ params.channelNonce ?? 0n
3026
+ ) : minaChannelHashField(Poseidon, PublicKey, params.zkAppAddress);
3027
+ const message = [commitment, params.nonce, channelHashField];
3028
+ const signed = client.signFields(message, params.minaPrivateKeyBase58);
3029
+ const { r, s } = Signature.fromBase58(signed.signature);
3030
+ const proofObject = {
3031
+ commitment: commitment.toString(),
3032
+ signature: { r: r.toString(), s: s.toString() },
3033
+ nonce: params.nonce.toString(),
3034
+ signerPublicKey
3035
+ };
3036
+ const proofJson = JSON.stringify(proofObject);
3037
+ const encoding = params.proofEncoding ?? "base64";
3038
+ const proof = encoding === "base64" ? Buffer.from(proofJson, "utf8").toString("base64") : proofJson;
3039
+ return {
3040
+ balanceCommitment: commitment.toString(),
3041
+ proof,
3042
+ salt: params.salt.toString(),
3043
+ signerPublicKey
3044
+ };
3045
+ }
3046
+
3047
+ // src/channel/mina-deposit.ts
3048
+ var DEPOSIT_TOTAL_STATE_INDEX = 4;
3049
+ async function readMinaDepositTotal(graphqlUrl, zkAppAddress, fetchImpl = fetch) {
3050
+ const query = "query($pk:String!){account(publicKey:$pk){zkappState}}";
3051
+ const res = await fetchImpl(graphqlUrl, {
3052
+ method: "POST",
3053
+ headers: { "content-type": "application/json" },
3054
+ body: JSON.stringify({ query, variables: { pk: zkAppAddress } })
3055
+ });
3056
+ if (!res.ok) {
3057
+ throw new Error(`Mina GraphQL request failed: HTTP ${res.status}`);
3058
+ }
3059
+ const json = await res.json();
3060
+ if (json.errors && json.errors.length > 0) {
3061
+ throw new Error(
3062
+ `Mina GraphQL error: ${json.errors[0]?.message ?? "unknown"}`
3063
+ );
3064
+ }
3065
+ const state = json.data?.account?.zkappState;
3066
+ if (!state || state.length <= DEPOSIT_TOTAL_STATE_INDEX) {
3067
+ throw new Error(
3068
+ `Mina zkApp ${zkAppAddress} has no readable zkappState (account not found or not a zkApp)`
3069
+ );
3070
+ }
3071
+ return BigInt(state[DEPOSIT_TOTAL_STATE_INDEX]);
3072
+ }
3073
+
3074
+ // src/signing/mina-signer.ts
3075
+ var DEFAULT_MINA_TOKEN_ID = "MINA";
3076
+ var MINA_CLAIM_NETWORK = "devnet";
3077
+ function deriveMinaSalt(zkAppAddress, nonce) {
3078
+ const digestHex = bytesToHex(
3079
+ sha2562(new TextEncoder().encode(`mina-pc-salt:${zkAppAddress}:${nonce}`))
3080
+ );
3081
+ const salt = BigInt("0x" + digestHex.slice(0, 60));
3082
+ return salt === 0n ? 1n : salt;
3083
+ }
3084
+ var MinaSigner = class {
3085
+ chainType = "mina";
3086
+ /** Big-endian hex scalar (or already-`EK…` base58) Mina private key. */
3087
+ privateKey;
3088
+ publicKeyBase58;
3089
+ depositReader;
3090
+ /** Per-zkApp `depositTotal` cache (deposits are rare; the connector re-reads). */
3091
+ depositCache = /* @__PURE__ */ new Map();
3092
+ /**
3093
+ * @param privateKey - Mina private key as big-endian hex scalar (the form
3094
+ * `deriveFullIdentity()` emits, `identity.mina.privateKey`) or an `EK…`
3095
+ * base58 key. Converted to the base58check form mina-signer requires.
3096
+ * @param publicKeyBase58 - Optional base58 public key (e.g.
3097
+ * `identity.mina.publicKey`). When omitted it is derived during signing.
3098
+ * @param options - Optional on-chain `depositTotal` resolution (graphqlUrl or
3099
+ * an injected reader) so claims conserve balances on funded zkApps.
3100
+ */
3101
+ constructor(privateKey, publicKeyBase58, options) {
3102
+ this.privateKey = privateKey;
3103
+ this.publicKeyBase58 = publicKeyBase58;
3104
+ if (options?.depositReader) {
3105
+ this.depositReader = options.depositReader;
3106
+ } else if (options?.graphqlUrl) {
3107
+ const url = options.graphqlUrl;
3108
+ this.depositReader = (zkAppAddress) => readMinaDepositTotal(url, zkAppAddress);
3109
+ }
3110
+ }
3111
+ /**
3112
+ * Resolve the channel's on-chain `depositTotal`, caching per zkApp. Returns
3113
+ * `undefined` when no reader is configured or the read fails — callers then
3114
+ * fall back to the legacy `balanceB = 0` commitment.
3115
+ */
3116
+ async resolveDepositTotal(zkAppAddress) {
3117
+ if (this.depositCache.has(zkAppAddress)) {
3118
+ return this.depositCache.get(zkAppAddress);
3119
+ }
3120
+ if (!this.depositReader) return void 0;
3121
+ try {
3122
+ const depositTotal = await this.depositReader(zkAppAddress);
3123
+ this.depositCache.set(zkAppAddress, depositTotal);
3124
+ return depositTotal;
3125
+ } catch {
3126
+ return void 0;
3127
+ }
3128
+ }
3129
+ get signerIdentifier() {
3130
+ return this.publicKeyBase58 ?? "uninitialized";
3131
+ }
3132
+ /** Derive this signer's B62 public key from its (base58) private key. */
3133
+ async deriveOwnPublicKey(minaPrivateKeyBase58) {
3134
+ const { Client } = await loadMinaPaymentChannelBindings();
3135
+ return new Client({ network: MINA_CLAIM_NETWORK }).derivePublicKey(
3136
+ minaPrivateKeyBase58
3137
+ );
3138
+ }
3139
+ async signBalanceProof(params) {
3140
+ if (params.metadata.chainType !== "mina") {
3141
+ throw new Error(
3142
+ `MinaSigner cannot sign for chain type: ${params.metadata.chainType}`
3143
+ );
3144
+ }
3145
+ const zkAppAddress = params.channelId || params.metadata.zkAppAddress;
3146
+ if (!zkAppAddress) {
3147
+ throw new Error(
3148
+ "MinaSigner requires a zkAppAddress (channel id) to sign a balance proof"
3149
+ );
3150
+ }
3151
+ const minaPrivateKey = hexToMinaBase58PrivateKey3(this.privateKey);
3152
+ const tokenId = params.metadata.tokenId ?? DEFAULT_MINA_TOKEN_ID;
3153
+ const salt = deriveMinaSalt(zkAppAddress, params.nonce);
3154
+ const clientPubKey = this.publicKeyBase58 ?? await this.deriveOwnPublicKey(minaPrivateKey);
3155
+ this.publicKeyBase58 = clientPubKey;
3156
+ const apexPubKey = params.recipient && /^B62[a-zA-Z0-9]{40,60}$/.test(params.recipient) ? params.recipient : void 0;
3157
+ const depositTotal = params.depositTotal ?? await this.resolveDepositTotal(zkAppAddress);
3158
+ let balanceB = 0n;
3159
+ if (depositTotal != null && depositTotal > 0n) {
3160
+ if (params.transferredAmount > depositTotal) {
3161
+ throw new Error(
3162
+ `Mina claim balanceA (${params.transferredAmount}) exceeds on-chain depositTotal (${depositTotal}) \u2014 cannot conserve balances`
3163
+ );
3164
+ }
3165
+ balanceB = depositTotal - params.transferredAmount;
3166
+ }
3167
+ const built = await buildMinaPaymentChannelProof({
3168
+ zkAppAddress,
3169
+ minaPrivateKeyBase58: minaPrivateKey,
3170
+ signerPublicKey: clientPubKey,
3171
+ // Recipient-credit (unidirectional): party A carries the cumulative amount;
3172
+ // party B carries the funder's remaining balance (depositTotal − balanceA)
3173
+ // so the signed commitment conserves and the on-chain claimFromChannel
3174
+ // signatureA check passes. `signatureB` remains apex-co-signed downstream.
3175
+ balanceA: params.transferredAmount,
3176
+ balanceB,
3177
+ salt,
3178
+ nonce: BigInt(params.nonce),
3179
+ // Participant-form channelHash (on-chain-settleable) when the apex pubkey
3180
+ // is known; otherwise the legacy zkApp-x form (off-chain-store only).
3181
+ ...apexPubKey ? { participantA: clientPubKey, participantB: apexPubKey } : {}
3182
+ });
3183
+ this.publicKeyBase58 = built.signerPublicKey;
3184
+ return {
3185
+ channelId: zkAppAddress,
3186
+ nonce: params.nonce,
3187
+ transferredAmount: params.transferredAmount,
3188
+ lockedAmount: params.lockedAmount,
3189
+ locksRoot: params.locksRoot,
3190
+ // `signature` is unused on the Mina wire (the proof carries the Schnorr
3191
+ // signature); keep the base64 proof here too for symmetry / debugging.
3192
+ signature: built.proof,
3193
+ signerAddress: built.signerPublicKey,
3194
+ chainId: 0,
3195
+ tokenNetworkAddress: zkAppAddress,
3196
+ recipient: params.recipient,
3197
+ mina: {
3198
+ balanceCommitment: built.balanceCommitment,
3199
+ proof: built.proof,
3200
+ salt: built.salt,
3201
+ tokenId
3202
+ }
3203
+ };
3204
+ }
3205
+ buildClaimMessage(proof, senderId) {
3206
+ if (!proof.mina) {
3207
+ throw new Error(
3208
+ "MinaSigner.buildClaimMessage requires a Mina-signed proof (missing `mina` fields)"
3209
+ );
3210
+ }
3211
+ const claim = {
3212
+ version: "1.0",
3213
+ blockchain: "mina",
3214
+ messageId: crypto.randomUUID(),
3215
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, ".000Z"),
3216
+ senderId,
3217
+ zkAppAddress: proof.channelId,
3218
+ tokenId: proof.mina.tokenId,
3219
+ balanceCommitment: proof.mina.balanceCommitment,
3220
+ nonce: proof.nonce,
3221
+ proof: proof.mina.proof,
3222
+ salt: proof.mina.salt,
3223
+ transferredAmount: proof.transferredAmount.toString(),
3224
+ // Surface the signer's Mina pubkey top-level (it is also embedded in the
3225
+ // base64 `proof`). The connector's SettlementExecutor reads
3226
+ // `latestClaim.signerPublicKey` to resolve participant keys for the
3227
+ // on-chain claimFromChannel on an inbound/externally-opened channel;
3228
+ // without it the Mina SDK throws ACCOUNT_NOT_FOUND. `signerAddress`
3229
+ // carries the B62 base58 pubkey for Mina proofs (see MinaSigner.sign*).
3230
+ signerPublicKey: proof.signerAddress,
3231
+ network: MINA_CLAIM_NETWORK
3232
+ };
3233
+ return claim;
3234
+ }
3235
+ };
1533
3236
 
1534
3237
  // src/channel/ChannelManager.ts
1535
3238
  var ChannelManager = class {
@@ -1629,7 +3332,11 @@ var ChannelManager = class {
1629
3332
  chainType: negotiation.chainType,
1630
3333
  chainId: typeof negotiation.chainId === "number" ? negotiation.chainId : 0,
1631
3334
  tokenNetworkAddress: negotiation.tokenNetwork ?? "",
1632
- tokenAddress: negotiation.tokenAddress
3335
+ tokenAddress: negotiation.tokenAddress,
3336
+ recipient: negotiation.settlementAddress,
3337
+ // On-chain depositTotal (Mina only) — needed so the Mina signer binds
3338
+ // balanceB = depositTotal − balanceA (connector#133).
3339
+ depositTotal: result.depositTotal
1633
3340
  });
1634
3341
  this.peerChannels.set(peerId, result.channelId);
1635
3342
  return result.channelId;
@@ -1667,7 +3374,9 @@ var ChannelManager = class {
1667
3374
  chainType: chainContext?.chainType ?? "evm",
1668
3375
  chainId: cId,
1669
3376
  tokenNetworkAddress: tnAddr,
1670
- tokenAddress: chainContext?.tokenAddress
3377
+ tokenAddress: chainContext?.tokenAddress,
3378
+ recipient: chainContext?.recipient,
3379
+ depositTotal: chainContext?.depositTotal
1671
3380
  });
1672
3381
  return;
1673
3382
  }
@@ -1678,7 +3387,9 @@ var ChannelManager = class {
1678
3387
  chainType: chainContext?.chainType ?? "evm",
1679
3388
  chainId: cId,
1680
3389
  tokenNetworkAddress: tnAddr,
1681
- tokenAddress: chainContext?.tokenAddress
3390
+ tokenAddress: chainContext?.tokenAddress,
3391
+ recipient: chainContext?.recipient,
3392
+ depositTotal: chainContext?.depositTotal
1682
3393
  });
1683
3394
  }
1684
3395
  /**
@@ -1708,6 +3419,11 @@ var ChannelManager = class {
1708
3419
  }
1709
3420
  const signer = this.chainSigners.get(tracking.chainType);
1710
3421
  if (signer && tracking.chainType !== "evm") {
3422
+ if (!tracking.recipient) {
3423
+ throw new Error(
3424
+ `Channel "${channelId}" (${tracking.chainType}) has no recipient settlement address; cannot sign a Solana/Mina balance proof. Ensure the peer negotiation supplied a settlementAddress.`
3425
+ );
3426
+ }
1711
3427
  const metadata = this.buildMetadata(tracking);
1712
3428
  return signer.signBalanceProof({
1713
3429
  channelId,
@@ -1715,7 +3431,13 @@ var ChannelManager = class {
1715
3431
  transferredAmount: tracking.cumulativeAmount,
1716
3432
  lockedAmount: 0n,
1717
3433
  locksRoot: "0x0000000000000000000000000000000000000000000000000000000000000000",
1718
- metadata
3434
+ recipient: tracking.recipient,
3435
+ metadata,
3436
+ // On-chain depositTotal captured at open time (#220) — the Mina signer
3437
+ // binds balanceB = depositTotal − balanceA (connector#133); the Solana
3438
+ // signer ignores it. When undefined (resume / idempotent re-open) the
3439
+ // Mina signer self-resolves it from chain (#223).
3440
+ depositTotal: tracking.depositTotal
1719
3441
  });
1720
3442
  }
1721
3443
  if (!this.evmSigner) {
@@ -1828,11 +3550,91 @@ var JsonFileChannelStore = class {
1828
3550
  }
1829
3551
  };
1830
3552
 
3553
+ // src/blob-storage.ts
3554
+ import { buildBlobStorageRequest } from "@toon-protocol/core";
3555
+ var ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;
3556
+ async function requestBlobStorage(client, secretKey, params) {
3557
+ const bid = params.bid ?? (params.ilpAmount !== void 0 ? String(params.ilpAmount) : void 0);
3558
+ if (bid === void 0 || bid === "") {
3559
+ return {
3560
+ success: false,
3561
+ error: "requestBlobStorage requires a bid (or ilpAmount to derive it)"
3562
+ };
3563
+ }
3564
+ const blobBuffer = Buffer.from(
3565
+ params.blobData.buffer,
3566
+ params.blobData.byteOffset,
3567
+ params.blobData.byteLength
3568
+ );
3569
+ let event;
3570
+ try {
3571
+ event = buildBlobStorageRequest(
3572
+ {
3573
+ blobData: blobBuffer,
3574
+ contentType: params.contentType,
3575
+ bid
3576
+ },
3577
+ secretKey
3578
+ );
3579
+ } catch (error) {
3580
+ return {
3581
+ success: false,
3582
+ error: error instanceof Error ? error.message : String(error)
3583
+ };
3584
+ }
3585
+ const result = await client.publishEvent(event, {
3586
+ destination: params.destination,
3587
+ claim: params.claim,
3588
+ ilpAmount: params.ilpAmount
3589
+ });
3590
+ if (!result.success) {
3591
+ return {
3592
+ success: false,
3593
+ eventId: result.eventId ?? event.id,
3594
+ error: result.error ?? "Blob storage request rejected"
3595
+ };
3596
+ }
3597
+ if (!result.data) {
3598
+ return {
3599
+ success: false,
3600
+ eventId: event.id,
3601
+ error: "FULFILL contained no data; expected base64-encoded Arweave tx ID"
3602
+ };
3603
+ }
3604
+ const txId = decodeUtf8(fromBase64(result.data));
3605
+ if (!ARWEAVE_TX_ID_REGEX.test(txId)) {
3606
+ return {
3607
+ success: false,
3608
+ eventId: event.id,
3609
+ error: `Decoded FULFILL data is not a valid Arweave tx ID: "${txId}"`
3610
+ };
3611
+ }
3612
+ return {
3613
+ success: true,
3614
+ txId,
3615
+ eventId: event.id
3616
+ };
3617
+ }
3618
+
1831
3619
  // src/ToonClient.ts
1832
3620
  var ToonClient = class {
1833
3621
  config;
1834
3622
  state = null;
1835
3623
  evmSigner;
3624
+ solanaSigner;
3625
+ /**
3626
+ * Ed25519 signing seed (32 bytes) derived from the mnemonic for the Solana
3627
+ * identity. Retained so `start()` can inject it into the on-chain channel
3628
+ * client's Solana config (same key as `solanaSigner`).
3629
+ */
3630
+ solanaSeed;
3631
+ minaSigner;
3632
+ /**
3633
+ * Mina private key (big-endian hex scalar, as `deriveFullIdentity` emits)
3634
+ * derived from the mnemonic. Retained so `start()` can inject it into the
3635
+ * on-chain channel client's Mina config (same key as `minaSigner`).
3636
+ */
3637
+ minaPrivateKey;
1836
3638
  channelManager;
1837
3639
  peerNegotiations = /* @__PURE__ */ new Map();
1838
3640
  /**
@@ -1854,8 +3656,8 @@ var ToonClient = class {
1854
3656
  * @returns Object with secretKey (Uint8Array) and pubkey (hex string)
1855
3657
  */
1856
3658
  static generateKeypair() {
1857
- const secretKey = generateSecretKey2();
1858
- const pubkey = getPublicKey(secretKey);
3659
+ const secretKey = generateSecretKey3();
3660
+ const pubkey = getPublicKey2(secretKey);
1859
3661
  return { secretKey, pubkey };
1860
3662
  }
1861
3663
  /**
@@ -1863,7 +3665,37 @@ var ToonClient = class {
1863
3665
  * Works before start() is called.
1864
3666
  */
1865
3667
  getPublicKey() {
1866
- return getPublicKey(this.config.secretKey);
3668
+ return getPublicKey2(this.config.secretKey);
3669
+ }
3670
+ /**
3671
+ * Sign an unsigned Nostr event template with the client's Nostr secret key,
3672
+ * returning a fully-signed event (id + pubkey + sig).
3673
+ *
3674
+ * This is the key primitive behind the daemon's sign-and-publish path: a UI
3675
+ * or agent supplies only `{ kind, content, tags, created_at }` and never holds
3676
+ * the private key — signing happens here, inside the key owner.
3677
+ */
3678
+ signEvent(template) {
3679
+ return finalizeEvent(template, this.config.secretKey);
3680
+ }
3681
+ /**
3682
+ * Upload bytes to Arweave via the kind:5094 blob-storage DVM (single-packet),
3683
+ * signing the request with this client's Nostr key and paying through its
3684
+ * existing channel. Returns the Arweave tx id on success.
3685
+ *
3686
+ * Backs the daemon's `upload-media` path: the key and claim/channel plumbing
3687
+ * stay inside the client; callers pass only the bytes.
3688
+ */
3689
+ async uploadBlob(params) {
3690
+ return requestBlobStorage(this, this.config.secretKey, params);
3691
+ }
3692
+ /**
3693
+ * Per-chain settlement readiness for the configured `network` tier, mirroring
3694
+ * the townhouse node's status. Returns `undefined` when no named `network` is
3695
+ * set (or `network: 'custom'`), since there is no preset tier to report on.
3696
+ */
3697
+ getNetworkStatus() {
3698
+ return getNetworkStatus(this.config);
1867
3699
  }
1868
3700
  /**
1869
3701
  * Gets the EVM address derived from the Nostr secret key (or explicit evmPrivateKey override).
@@ -1871,6 +3703,46 @@ var ToonClient = class {
1871
3703
  getEvmAddress() {
1872
3704
  return this.evmSigner?.address;
1873
3705
  }
3706
+ /**
3707
+ * Gets the Solana (base58) address, when the client was constructed from a
3708
+ * `mnemonic`. Available only AFTER `start()` (Solana keys are derived
3709
+ * asynchronously). Returns undefined otherwise.
3710
+ */
3711
+ getSolanaAddress() {
3712
+ return this.solanaSigner?.signerIdentifier;
3713
+ }
3714
+ /**
3715
+ * Gets the Mina (base58) address, when the client was constructed from a
3716
+ * `mnemonic` AND `mina-signer` is installed. Available only AFTER `start()`.
3717
+ * Returns undefined otherwise.
3718
+ */
3719
+ getMinaAddress() {
3720
+ return this.minaSigner?.signerIdentifier;
3721
+ }
3722
+ /**
3723
+ * Derive the Solana/Mina keys from the mnemonic and register their signers on
3724
+ * the ChannelManager. Mirrors how the EVM signer is wired, but for the
3725
+ * non-secp256k1 chains. Skips any chain whose optional dependency is missing.
3726
+ */
3727
+ async registerMnemonicChainSigners(mnemonic, accountIndex = 0) {
3728
+ if (!this.channelManager) return;
3729
+ const identity = await deriveFullIdentity(mnemonic, accountIndex);
3730
+ if (identity.solana.publicKey) {
3731
+ const seed = identity.solana.secretKey.slice(0, 32);
3732
+ this.solanaSeed = seed;
3733
+ this.solanaSigner = new SolanaSigner(seed, identity.solana.publicKey);
3734
+ this.channelManager.registerChainSigner("solana", this.solanaSigner);
3735
+ }
3736
+ if (identity.mina.publicKey) {
3737
+ this.minaPrivateKey = identity.mina.privateKey;
3738
+ this.minaSigner = new MinaSigner(
3739
+ identity.mina.privateKey,
3740
+ identity.mina.publicKey,
3741
+ this.config.minaChannel?.graphqlUrl ? { graphqlUrl: this.config.minaChannel.graphqlUrl } : void 0
3742
+ );
3743
+ this.channelManager.registerChainSigner("mina", this.minaSigner);
3744
+ }
3745
+ }
1874
3746
  /**
1875
3747
  * Starts the ToonClient.
1876
3748
  *
@@ -1891,9 +3763,21 @@ var ToonClient = class {
1891
3763
  if (this.evmSigner) {
1892
3764
  const store = this.config.channelStorePath ? new JsonFileChannelStore(this.config.channelStorePath) : void 0;
1893
3765
  this.channelManager = new ChannelManager(this.evmSigner, store);
3766
+ if (this.config.mnemonic) {
3767
+ await this.registerMnemonicChainSigners(
3768
+ this.config.mnemonic,
3769
+ this.config.mnemonicAccountIndex ?? 0
3770
+ );
3771
+ }
1894
3772
  }
1895
3773
  const initialization = await initializeHttpMode(this.config);
1896
- const { bootstrapService, discoveryTracker, runtimeClient, btpClient } = initialization;
3774
+ const {
3775
+ bootstrapService,
3776
+ discoveryTracker,
3777
+ runtimeClient,
3778
+ btpClient,
3779
+ stopManagedProxy
3780
+ } = initialization;
1897
3781
  if (this.channelManager) {
1898
3782
  const cm = this.channelManager;
1899
3783
  const nostrPubkey = this.getPublicKey();
@@ -1904,7 +3788,8 @@ var ToonClient = class {
1904
3788
  cm.trackChannel(channelId, defaultChainCtx);
1905
3789
  }
1906
3790
  const proof = await cm.signBalanceProof(channelId, amount);
1907
- return EvmSigner.buildClaimMessage(proof, nostrPubkey);
3791
+ const signer = cm.getSignerForChannel(channelId);
3792
+ return signer.buildClaimMessage(proof, nostrPubkey);
1908
3793
  }
1909
3794
  );
1910
3795
  }
@@ -1953,13 +3838,35 @@ var ToonClient = class {
1953
3838
  this.channelManager.setChannelClient(
1954
3839
  initialization.onChainChannelClient
1955
3840
  );
3841
+ if (this.config.solanaChannel && this.solanaSeed) {
3842
+ initialization.onChainChannelClient.setSolanaConfig({
3843
+ rpcUrl: this.config.solanaChannel.rpcUrl,
3844
+ programId: this.config.solanaChannel.programId,
3845
+ tokenMint: this.config.solanaChannel.tokenMint,
3846
+ challengeDuration: this.config.solanaChannel.challengeDuration,
3847
+ deposit: this.config.solanaChannel.deposit,
3848
+ keypair: this.solanaSeed
3849
+ });
3850
+ }
3851
+ if (this.config.minaChannel && this.minaPrivateKey) {
3852
+ initialization.onChainChannelClient.setMinaConfig({
3853
+ graphqlUrl: this.config.minaChannel.graphqlUrl,
3854
+ zkAppAddress: this.config.minaChannel.zkAppAddress,
3855
+ privateKey: this.minaPrivateKey,
3856
+ ...this.config.minaChannel.challengeDuration !== void 0 ? { challengeDuration: this.config.minaChannel.challengeDuration } : {},
3857
+ ...this.config.minaChannel.tokenId !== void 0 ? { tokenId: this.config.minaChannel.tokenId } : {},
3858
+ ...this.config.minaChannel.deposit !== void 0 ? { deposit: this.config.minaChannel.deposit } : {},
3859
+ ...this.config.minaChannel.networkId !== void 0 ? { networkId: this.config.minaChannel.networkId } : {}
3860
+ });
3861
+ }
1956
3862
  }
1957
3863
  this.state = {
1958
3864
  bootstrapService,
1959
3865
  discoveryTracker,
1960
3866
  runtimeClient,
1961
3867
  peersDiscovered: bootstrapResults.length,
1962
- btpClient: btpClient ?? void 0
3868
+ btpClient: btpClient ?? void 0,
3869
+ ...stopManagedProxy ? { stopManagedProxy } : {}
1963
3870
  };
1964
3871
  return {
1965
3872
  peersDiscovered: bootstrapResults.length,
@@ -1994,7 +3901,7 @@ var ToonClient = class {
1994
3901
  try {
1995
3902
  const toonData = this.config.toonEncoder(event);
1996
3903
  const basePricePerByte = 10n;
1997
- const amount = String(BigInt(toonData.length) * basePricePerByte);
3904
+ const amount = options?.ilpAmount !== void 0 ? String(options.ilpAmount) : String(BigInt(toonData.length) * basePricePerByte);
1998
3905
  const destination = options?.destination ?? this.config.destinationAddress;
1999
3906
  if (!this.state.btpClient) {
2000
3907
  throw new ToonClientError(
@@ -2004,10 +3911,7 @@ var ToonClient = class {
2004
3911
  }
2005
3912
  let claimMessage;
2006
3913
  if (options?.claim) {
2007
- claimMessage = EvmSigner.buildClaimMessage(
2008
- options.claim,
2009
- this.getPublicKey()
2010
- );
3914
+ claimMessage = this.buildClaimMessageForProof(options.claim);
2011
3915
  } else if (this.channelManager) {
2012
3916
  const peerId = this.resolvePeerId(destination);
2013
3917
  const negotiation = this.peerNegotiations.get(peerId);
@@ -2067,6 +3971,112 @@ var ToonClient = class {
2067
3971
  );
2068
3972
  }
2069
3973
  }
3974
+ /**
3975
+ * Sends a raw swap ILP packet (Story 12.5) to a Mill peer with an attached
3976
+ * balance-proof claim. This is a lower-level surface than `publishEvent`:
3977
+ * it forwards the raw `IlpSendResult` so the sender (`streamSwap()`) can
3978
+ * decode FULFILL metadata itself.
3979
+ *
3980
+ * Claim resolution mirrors `publishEvent`:
3981
+ * (a) explicit `params.claim` -> use it,
3982
+ * (b) `channelManager` present -> auto-open + auto-sign for the peer
3983
+ * matching `destination`,
3984
+ * (c) neither -> throw MISSING_CLAIM.
3985
+ *
3986
+ * @throws {ToonClientError} INVALID_STATE / NO_BTP_CLIENT / MISSING_CLAIM
3987
+ */
3988
+ async sendSwapPacket(params) {
3989
+ if (!this.state) {
3990
+ throw new ToonClientError(
3991
+ "Client not started. Call start() first.",
3992
+ "INVALID_STATE"
3993
+ );
3994
+ }
3995
+ if (!this.state.btpClient) {
3996
+ throw new ToonClientError(
3997
+ "BTP client required for sending swap packets. Configure btpUrl.",
3998
+ "NO_BTP_CLIENT"
3999
+ );
4000
+ }
4001
+ const claimMessage = await this.resolveClaimForDestination(
4002
+ params.destination,
4003
+ params.amount,
4004
+ params.claim
4005
+ );
4006
+ return this.state.btpClient.sendIlpPacketWithClaim(
4007
+ {
4008
+ destination: params.destination,
4009
+ amount: String(params.amount),
4010
+ data: toBase64(params.toonData),
4011
+ timeout: params.timeout ?? 3e4
4012
+ },
4013
+ claimMessage
4014
+ );
4015
+ }
4016
+ /**
4017
+ * Build a BTP claim message from a pre-signed balance proof using the
4018
+ * CHAIN-APPROPRIATE signer.
4019
+ *
4020
+ * The explicit-claim path (caller signs the balance proof, then passes
4021
+ * `{ claim }`) must wrap the proof with the signer matching the channel's
4022
+ * chain. Hardcoding `EvmSigner.buildClaimMessage` here produced an EVM
4023
+ * `BTPClaimMessage` for a Solana/Mina balance proof — no `blockchain`
4024
+ * discriminator and the base58 channel account placed in the EVM
4025
+ * `channelId` field — which the connector's inbound validator classifies
4026
+ * as EVM and rejects with F06 (`Invalid channelId format`).
4027
+ *
4028
+ * When the proof's `channelId` is tracked we use
4029
+ * `getSignerForChannel(channelId).buildClaimMessage`, which emits the
4030
+ * correct per-chain envelope (e.g. `blockchain:'solana'` + base58
4031
+ * `channelAccount`). When it is not tracked we fall back to the EVM signer
4032
+ * to preserve prior behavior for lightweight/EVM-only callers.
4033
+ *
4034
+ * EVM output is byte-identical to the previous hardcoded path (the EVM
4035
+ * adapter in `getSignerForChannel` delegates to the same
4036
+ * `EvmSigner.buildClaimMessage`).
4037
+ */
4038
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- claim message is opaque forwarded type
4039
+ buildClaimMessageForProof(claim) {
4040
+ if (this.channelManager?.isTracking(claim.channelId)) {
4041
+ const signer = this.channelManager.getSignerForChannel(claim.channelId);
4042
+ return signer.buildClaimMessage(claim, this.getPublicKey());
4043
+ }
4044
+ return EvmSigner.buildClaimMessage(claim, this.getPublicKey());
4045
+ }
4046
+ /**
4047
+ * Shared claim-resolution logic used by `publishEvent` and `sendSwapPacket`.
4048
+ * TODO(12.5 followup): also factor `publishEvent`'s inline claim resolution
4049
+ * to call this helper. Kept duplicated for now to minimize regression risk.
4050
+ */
4051
+ async resolveClaimForDestination(destination, amount, explicitClaim) {
4052
+ if (explicitClaim) {
4053
+ return this.buildClaimMessageForProof(explicitClaim);
4054
+ }
4055
+ if (this.channelManager) {
4056
+ const peerId = this.resolvePeerId(destination);
4057
+ const negotiation = this.peerNegotiations.get(peerId);
4058
+ if (!negotiation) {
4059
+ throw new ToonClientError(
4060
+ `No negotiation metadata for peer "${peerId}" \u2014 was bootstrap completed?`,
4061
+ "PEER_NOT_NEGOTIATED"
4062
+ );
4063
+ }
4064
+ const channelId = await this.channelManager.ensureChannel(
4065
+ peerId,
4066
+ negotiation
4067
+ );
4068
+ const proof = await this.channelManager.signBalanceProof(
4069
+ channelId,
4070
+ amount
4071
+ );
4072
+ const signer = this.channelManager.getSignerForChannel(channelId);
4073
+ return signer.buildClaimMessage(proof, this.getPublicKey());
4074
+ }
4075
+ throw new ToonClientError(
4076
+ "No claim provided and no channel manager configured",
4077
+ "MISSING_CLAIM"
4078
+ );
4079
+ }
2070
4080
  /**
2071
4081
  * Signs a balance proof for the given channel with the specified amount.
2072
4082
  * Delegates to ChannelManager which auto-increments nonce and tracks cumulative amount.
@@ -2085,6 +4095,51 @@ var ToonClient = class {
2085
4095
  }
2086
4096
  return this.channelManager.signBalanceProof(channelId, amount);
2087
4097
  }
4098
+ /**
4099
+ * Eagerly open (or return existing) payment channel for the given destination.
4100
+ *
4101
+ * Channels are normally opened lazily on the first `publishEvent()` /
4102
+ * `sendSwapPacket()` call. This method exposes the lazy-open path so
4103
+ * callers (and E2E tests) that need a tracked `channelId` BEFORE publishing
4104
+ * can force the open. Idempotent — returns the existing channel ID for the
4105
+ * peer if one is already open.
4106
+ *
4107
+ * @param destination - Optional ILP destination address. Defaults to
4108
+ * `config.destinationAddress`.
4109
+ * @returns The channel ID of the (now) open channel.
4110
+ * @throws {ToonClientError} If client not started, no channel manager
4111
+ * configured, or peer negotiation metadata missing.
4112
+ */
4113
+ async openChannel(destination) {
4114
+ if (!this.state) {
4115
+ throw new ToonClientError(
4116
+ "Client not started. Call start() first.",
4117
+ "INVALID_STATE"
4118
+ );
4119
+ }
4120
+ if (!this.channelManager) {
4121
+ throw new ToonClientError(
4122
+ "No channel manager configured. Provide evmPrivateKey in config.",
4123
+ "NO_EVM_SIGNER"
4124
+ );
4125
+ }
4126
+ const dest = destination ?? this.config.destinationAddress;
4127
+ if (!dest) {
4128
+ throw new ToonClientError(
4129
+ "No destination provided and no default destinationAddress configured.",
4130
+ "NO_DESTINATION"
4131
+ );
4132
+ }
4133
+ const peerId = this.resolvePeerId(dest);
4134
+ const negotiation = this.peerNegotiations.get(peerId);
4135
+ if (!negotiation) {
4136
+ throw new ToonClientError(
4137
+ `No negotiation metadata for peer "${peerId}" \u2014 was bootstrap completed?`,
4138
+ "PEER_NOT_NEGOTIATED"
4139
+ );
4140
+ }
4141
+ return this.channelManager.ensureChannel(peerId, negotiation);
4142
+ }
2088
4143
  /**
2089
4144
  * Gets list of tracked payment channel IDs.
2090
4145
  */
@@ -2182,10 +4237,7 @@ var ToonClient = class {
2182
4237
  "NO_BTP_CLIENT"
2183
4238
  );
2184
4239
  }
2185
- const claimMessage = EvmSigner.buildClaimMessage(
2186
- params.claim,
2187
- this.getPublicKey()
2188
- );
4240
+ const claimMessage = this.buildClaimMessageForProof(params.claim);
2189
4241
  return this.state.btpClient.sendIlpPacketWithClaim(
2190
4242
  ilpParams,
2191
4243
  claimMessage
@@ -2204,10 +4256,14 @@ var ToonClient = class {
2204
4256
  if (!this.state) {
2205
4257
  throw new ToonClientError("Client not started", "INVALID_STATE");
2206
4258
  }
4259
+ const stopManagedProxy = this.state.stopManagedProxy;
2207
4260
  try {
2208
4261
  if (this.state.btpClient) {
2209
4262
  await this.state.btpClient.disconnect();
2210
4263
  }
4264
+ if (stopManagedProxy) {
4265
+ await stopManagedProxy();
4266
+ }
2211
4267
  this.state = null;
2212
4268
  } catch (error) {
2213
4269
  throw new ToonClientError(
@@ -2255,6 +4311,26 @@ var ToonClient = class {
2255
4311
  }
2256
4312
  };
2257
4313
 
4314
+ // src/transport/hs-hostname.ts
4315
+ var HS_HOSTNAME_REGEX = /^[a-z2-7]+\.anyone$/;
4316
+ var HS_HOSTNAME_MAX_LENGTH = 80;
4317
+ function isRoutableHsHostname(s) {
4318
+ return typeof s === "string" && s.length <= HS_HOSTNAME_MAX_LENGTH && HS_HOSTNAME_REGEX.test(s);
4319
+ }
4320
+ function assertRoutableHsHostname(hostname) {
4321
+ if (typeof hostname === "string" && /\.anon$/.test(hostname)) {
4322
+ throw new Error(
4323
+ `"${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).`
4324
+ );
4325
+ }
4326
+ if (!isRoutableHsHostname(hostname)) {
4327
+ throw new Error(
4328
+ `Invalid hidden-service hostname: ${JSON.stringify(hostname)}. Expected a base32 .anyone address matching ${HS_HOSTNAME_REGEX}.`
4329
+ );
4330
+ }
4331
+ return hostname;
4332
+ }
4333
+
2258
4334
  // src/adapters/HttpConnectorAdmin.ts
2259
4335
  var HttpConnectorAdmin = class {
2260
4336
  adminUrl;
@@ -2551,324 +4627,452 @@ var HttpConnectorAdmin = class {
2551
4627
  throw new PeerAlreadyExistsError(
2552
4628
  `Peer already exists: "${peerId}" (${endpoint}): ${statusText}${errorMessage}`
2553
4629
  );
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;
4630
+ default:
4631
+ if (status >= 500) {
4632
+ throw new ConnectorError(
4633
+ `Connector admin API error (${endpoint}): ${status} ${statusText}${errorMessage}`
4634
+ );
4635
+ }
4636
+ throw new ConnectorError(
4637
+ `Admin API error (${endpoint}): ${status} ${statusText}${errorMessage}`
4638
+ );
4639
+ }
2654
4640
  }
2655
4641
  };
2656
4642
 
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;
4643
+ // src/pet/filterPetDvmProviders.ts
4644
+ import { parseServiceDiscovery } from "@toon-protocol/core";
4645
+ import { PET_INTERACTION_REQUEST_KIND } from "@toon-protocol/core";
4646
+ function filterPetDvmProviders(events) {
4647
+ const providers = [];
4648
+ for (const event of events) {
4649
+ let parsed;
4650
+ try {
4651
+ parsed = parseServiceDiscovery(event);
4652
+ } catch {
4653
+ continue;
4654
+ }
4655
+ if (!parsed) continue;
4656
+ const skill = parsed.skill;
4657
+ if (!skill) continue;
4658
+ if (!skill.kinds.includes(PET_INTERACTION_REQUEST_KIND)) continue;
4659
+ const pricing = skill.pricing[String(PET_INTERACTION_REQUEST_KIND)] ?? "0";
4660
+ providers.push({
4661
+ ilpAddress: parsed.ilpAddress,
4662
+ pricing,
4663
+ pubkey: event.pubkey,
4664
+ features: skill.features
4665
+ });
2667
4666
  }
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;
4667
+ providers.sort((a, b) => {
4668
+ const priceA = Number(a.pricing) || 0;
4669
+ const priceB = Number(b.pricing) || 0;
4670
+ return priceA - priceB;
4671
+ });
4672
+ return providers;
4673
+ }
4674
+
4675
+ // src/pet/buildPetInteractionRequest.ts
4676
+ import { PET_INTERACTION_REQUEST_KIND as PET_INTERACTION_REQUEST_KIND2 } from "@toon-protocol/core";
4677
+ var MAX_ACTION_TYPE = 10;
4678
+ function buildPetInteractionRequest(params) {
4679
+ const { blobbiId, actionType, itemId, tokenCost, isSleeping } = params;
4680
+ if (!blobbiId || blobbiId.trim() === "") {
4681
+ throw new ValidationError("blobbiId must be a non-empty string");
4682
+ }
4683
+ if (!Number.isInteger(actionType) || actionType < 0 || actionType > MAX_ACTION_TYPE) {
4684
+ throw new ValidationError(
4685
+ `actionType must be an integer between 0 and ${MAX_ACTION_TYPE}, got ${actionType}`
4686
+ );
2674
4687
  }
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)
4688
+ if (!Number.isInteger(itemId) || itemId < 0) {
4689
+ throw new ValidationError(
4690
+ `itemId must be a non-negative integer, got ${itemId}`
2685
4691
  );
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
4692
  }
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;
4693
+ if (!Number.isFinite(tokenCost) || tokenCost < 0) {
4694
+ throw new ValidationError(
4695
+ `tokenCost must be a non-negative number, got ${tokenCost}`
4696
+ );
2721
4697
  }
2722
- };
2723
-
2724
- // src/keys/KeyManager.ts
2725
- import { finalizeEvent } from "nostr-tools/pure";
2726
- import { nip19 } from "nostr-tools";
2727
-
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);
4698
+ return {
4699
+ kind: PET_INTERACTION_REQUEST_KIND2,
4700
+ created_at: Math.floor(Date.now() / 1e3),
4701
+ tags: [
4702
+ ["d", blobbiId],
4703
+ ["action", String(actionType)],
4704
+ ["item", String(itemId)],
4705
+ ["cost", String(tokenCost)],
4706
+ ["sleeping", String(isSleeping)]
4707
+ ],
4708
+ content: ""
4709
+ };
2741
4710
  }
2742
- function validateMnemonic(mnemonic) {
2743
- return _validateMnemonic(mnemonic, english);
4711
+
4712
+ // src/pet/parsePetInteractionResult.ts
4713
+ var STAT_FIELDS = [
4714
+ "hunger",
4715
+ "happiness",
4716
+ "health",
4717
+ "hygiene",
4718
+ "energy"
4719
+ ];
4720
+ var HEX_64_RE = /^[0-9a-f]{64}$/i;
4721
+ function isValidStats(obj) {
4722
+ if (typeof obj !== "object" || obj === null) return false;
4723
+ const record = obj;
4724
+ return STAT_FIELDS.every(
4725
+ (field) => typeof record[field] === "number" && Number.isFinite(record[field])
4726
+ );
2744
4727
  }
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");
4728
+ function parsePetInteractionResult(data) {
4729
+ if (!data) return null;
4730
+ let json;
4731
+ try {
4732
+ json = atob(data);
4733
+ } catch {
4734
+ return null;
2750
4735
  }
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));
4736
+ let parsed;
4737
+ try {
4738
+ parsed = JSON.parse(json);
4739
+ } catch {
4740
+ return null;
4741
+ }
4742
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
4743
+ return null;
4744
+ }
4745
+ const record = parsed;
4746
+ if (!isValidStats(record["stats"])) return null;
4747
+ const stage = record["stage"];
4748
+ if (typeof stage !== "number" || !Number.isInteger(stage) || stage < 0 || stage > 2) {
4749
+ return null;
4750
+ }
4751
+ const cycle = record["cycle"];
4752
+ if (typeof cycle !== "number" || !Number.isInteger(cycle) || cycle < 0) {
4753
+ return null;
4754
+ }
4755
+ const lastInteraction = record["lastInteraction"];
4756
+ if (typeof lastInteraction !== "number" || !Number.isFinite(lastInteraction)) {
4757
+ return null;
4758
+ }
4759
+ const brainHash = record["brainHash"];
4760
+ if (typeof brainHash !== "string" || !HEX_64_RE.test(brainHash)) {
4761
+ return null;
4762
+ }
4763
+ const cooldownTimestamps = record["cooldownTimestamps"];
4764
+ if (!Array.isArray(cooldownTimestamps)) return null;
4765
+ if (!cooldownTimestamps.every(
4766
+ (t) => typeof t === "number" && Number.isFinite(t)
4767
+ )) {
4768
+ return null;
4769
+ }
4770
+ const validatedStats = record["stats"];
4771
+ const stats = {
4772
+ hunger: validatedStats.hunger,
4773
+ happiness: validatedStats.happiness,
4774
+ health: validatedStats.health,
4775
+ hygiene: validatedStats.hygiene,
4776
+ energy: validatedStats.energy
4777
+ };
2757
4778
  return {
2758
- privateKey: secretKey,
2759
- address: account.address
4779
+ stats,
4780
+ stage,
4781
+ cycle,
4782
+ lastInteraction,
4783
+ brainHash,
4784
+ cooldownTimestamps: [...cooldownTimestamps]
2760
4785
  };
2761
4786
  }
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);
4787
+
4788
+ // src/pet/parsePetInteractionEvent.ts
4789
+ function getTagValue(tags, name) {
4790
+ for (const tag of tags) {
4791
+ if (tag[0] === name) {
4792
+ return tag[1];
4793
+ }
2791
4794
  }
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 };
4795
+ return void 0;
2798
4796
  }
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);
4797
+ function isStatLike(obj) {
4798
+ if (typeof obj !== "object" || obj === null) return false;
4799
+ const r = obj;
4800
+ 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"]);
4801
+ }
4802
+ function cleanStats(obj) {
4803
+ return {
4804
+ hunger: obj["hunger"],
4805
+ happiness: obj["happiness"],
4806
+ health: obj["health"],
4807
+ hygiene: obj["hygiene"],
4808
+ energy: obj["energy"]
4809
+ };
4810
+ }
4811
+ function parseContent(content) {
2806
4812
  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);
4813
+ const parsed = JSON.parse(content);
4814
+ if (typeof parsed !== "object" || parsed === null) return null;
4815
+ if (!isStatLike(parsed.priorStats) || !isStatLike(parsed.decayedStats) || !isStatLike(parsed.finalStats)) {
4816
+ return null;
4817
+ }
4818
+ if (typeof parsed.cycle !== "number" || typeof parsed.stage !== "number" || typeof parsed.tokenCost !== "number") {
4819
+ return null;
4820
+ }
2812
4821
  return {
2813
- privateKey: hexKey,
2814
- publicKey: keypair
4822
+ priorStats: cleanStats(parsed.priorStats),
4823
+ decayedStats: cleanStats(parsed.decayedStats),
4824
+ finalStats: cleanStats(parsed.finalStats),
4825
+ cycle: parsed.cycle,
4826
+ stage: parsed.stage,
4827
+ tokenCost: parsed.tokenCost
2815
4828
  };
2816
4829
  } catch {
2817
- throw new Error(
2818
- "mina-signer is required for Mina key derivation. Install it as an optional dependency."
2819
- );
4830
+ return null;
2820
4831
  }
2821
4832
  }
2822
- async function deriveFullIdentity(mnemonic) {
2823
- const seed = mnemonicToSeedSync(mnemonic);
2824
- const nostr = deriveNostrKey(seed);
2825
- const evm = deriveEvmIdentity(nostr.secretKey);
2826
- let solana;
2827
- try {
2828
- solana = await deriveSolanaKey(seed);
2829
- } catch {
2830
- solana = { secretKey: new Uint8Array(64), publicKey: "" };
4833
+ function parsePetInteractionEvent(event) {
4834
+ const tags = event.tags;
4835
+ const blobbiId = getTagValue(tags, "d");
4836
+ if (!blobbiId) return null;
4837
+ const actionStr = getTagValue(tags, "action");
4838
+ if (!actionStr) return null;
4839
+ const actionType = Number(actionStr);
4840
+ if (!Number.isFinite(actionType)) return null;
4841
+ const itemStr = getTagValue(tags, "item");
4842
+ if (!itemStr) return null;
4843
+ const itemId = Number(itemStr);
4844
+ if (!Number.isFinite(itemId)) return null;
4845
+ const costStr = getTagValue(tags, "cost");
4846
+ if (!costStr) return null;
4847
+ const tokenCost = Number(costStr);
4848
+ if (!Number.isFinite(tokenCost)) return null;
4849
+ const cycleStr = getTagValue(tags, "cycle");
4850
+ if (!cycleStr) return null;
4851
+ const cycle = Number(cycleStr);
4852
+ if (!Number.isFinite(cycle)) return null;
4853
+ const stageStr = getTagValue(tags, "stage");
4854
+ if (!stageStr) return null;
4855
+ const stage = Number(stageStr);
4856
+ if (!Number.isFinite(stage)) return null;
4857
+ const brainHash = getTagValue(tags, "brain_hash");
4858
+ if (!brainHash) return null;
4859
+ const proof = getTagValue(tags, "proof");
4860
+ const minaTx = getTagValue(tags, "mina_tx");
4861
+ const proofStatus = proof && minaTx ? "proven" : "optimistic";
4862
+ const content = parseContent(event.content);
4863
+ const result = {
4864
+ blobbiId,
4865
+ actionType,
4866
+ itemId,
4867
+ tokenCost,
4868
+ cycle,
4869
+ stage,
4870
+ brainHash,
4871
+ proofStatus,
4872
+ content
4873
+ };
4874
+ if (proof) result.proof = proof;
4875
+ if (minaTx) result.minaTx = minaTx;
4876
+ return result;
4877
+ }
4878
+
4879
+ // src/pet/buildPetListingEvent.ts
4880
+ var PET_LISTING_KIND = 30402;
4881
+ var STAGE_NAMES = {
4882
+ 0: "Egg",
4883
+ 1: "Baby",
4884
+ 2: "Adult"
4885
+ };
4886
+ function buildPetListingEvent(params) {
4887
+ const {
4888
+ blobbiId,
4889
+ askPriceUsdc,
4890
+ lifecycleHash,
4891
+ totalSpent,
4892
+ stage,
4893
+ stats,
4894
+ sellerPubkey,
4895
+ relayUrl,
4896
+ expiresAt
4897
+ } = params;
4898
+ const stageName = STAGE_NAMES[stage] ?? "Unknown";
4899
+ const summary = `${stageName} pet for sale \u2014 ${totalSpent} PET tokens spent (verified biography)`;
4900
+ return {
4901
+ kind: PET_LISTING_KIND,
4902
+ created_at: Math.floor(Date.now() / 1e3),
4903
+ tags: [
4904
+ ["d", blobbiId],
4905
+ ["title", `Pet ${blobbiId} for sale`],
4906
+ ["price", String(askPriceUsdc), "USDC", ""],
4907
+ ["summary", summary],
4908
+ ["t", "pet"],
4909
+ ["t", "toon-pet"],
4910
+ ["lifecycle_hash", lifecycleHash],
4911
+ ["total_spent", totalSpent],
4912
+ ["stage", String(stage)],
4913
+ ["expiration", String(expiresAt)],
4914
+ ["relay", relayUrl],
4915
+ ["p", sellerPubkey]
4916
+ ],
4917
+ content: JSON.stringify(stats)
4918
+ };
4919
+ }
4920
+
4921
+ // src/pet/parsePetListing.ts
4922
+ var HEX_64_RE2 = /^[0-9a-f]{64}$/i;
4923
+ function getTagValue2(tags, name) {
4924
+ for (const tag of tags) {
4925
+ if (tag[0] === name) {
4926
+ return tag[1];
4927
+ }
2831
4928
  }
2832
- let mina;
4929
+ return void 0;
4930
+ }
4931
+ var DEFAULT_STATS = {
4932
+ hunger: 0,
4933
+ happiness: 0,
4934
+ health: 0,
4935
+ hygiene: 0,
4936
+ energy: 0
4937
+ };
4938
+ function parseStats(content) {
2833
4939
  try {
2834
- mina = await deriveMinaKey(seed);
4940
+ const parsed = JSON.parse(content);
4941
+ if (typeof parsed !== "object" || parsed === null) return DEFAULT_STATS;
4942
+ const r = parsed;
4943
+ if (typeof r["hunger"] === "number" && typeof r["happiness"] === "number" && typeof r["health"] === "number" && typeof r["hygiene"] === "number" && typeof r["energy"] === "number") {
4944
+ return {
4945
+ hunger: r["hunger"],
4946
+ happiness: r["happiness"],
4947
+ health: r["health"],
4948
+ hygiene: r["hygiene"],
4949
+ energy: r["energy"]
4950
+ };
4951
+ }
4952
+ return DEFAULT_STATS;
2835
4953
  } catch {
2836
- mina = { privateKey: "", publicKey: "" };
4954
+ return DEFAULT_STATS;
2837
4955
  }
2838
- seed.fill(0);
2839
- return { nostr, evm, solana, mina };
2840
4956
  }
2841
- function deriveFromNsec(secretKey) {
2842
- const keyCopy = new Uint8Array(secretKey);
2843
- const pubkey = getPublicKey2(keyCopy);
2844
- const evm = deriveEvmIdentity(keyCopy);
4957
+ function parsePetListing(event) {
4958
+ if (event.kind !== 30402) return null;
4959
+ const { tags } = event;
4960
+ const blobbiId = getTagValue2(tags, "d");
4961
+ if (!blobbiId || blobbiId.trim() === "") return null;
4962
+ let askPriceUsdc = 0;
4963
+ let foundPrice = false;
4964
+ for (const tag of tags) {
4965
+ if (tag[0] === "price") {
4966
+ const priceStr = tag[1];
4967
+ if (priceStr === void 0) return null;
4968
+ const parsed = Number(priceStr);
4969
+ if (!Number.isFinite(parsed) || parsed <= 0) return null;
4970
+ askPriceUsdc = parsed;
4971
+ foundPrice = true;
4972
+ break;
4973
+ }
4974
+ }
4975
+ if (!foundPrice) return null;
4976
+ const lifecycleHash = getTagValue2(tags, "lifecycle_hash");
4977
+ if (!lifecycleHash) return null;
4978
+ if (!HEX_64_RE2.test(lifecycleHash)) return null;
4979
+ const totalSpent = getTagValue2(tags, "total_spent");
4980
+ if (totalSpent === void 0 || totalSpent === "") return null;
4981
+ const totalSpentNum = Number(totalSpent);
4982
+ if (!Number.isFinite(totalSpentNum) || totalSpentNum < 0) return null;
4983
+ const stageStr = getTagValue2(tags, "stage");
4984
+ if (stageStr === void 0) return null;
4985
+ const stage = Number(stageStr);
4986
+ if (!Number.isFinite(stage)) return null;
4987
+ const sellerPubkey = getTagValue2(tags, "p") ?? "";
4988
+ const relayUrl = getTagValue2(tags, "relay") ?? "";
4989
+ const expiresAtStr = getTagValue2(tags, "expiration");
4990
+ const expiresAt = expiresAtStr !== void 0 ? Number(expiresAtStr) : 0;
4991
+ const stats = parseStats(event.content);
2845
4992
  return {
2846
- nostr: { secretKey: keyCopy, pubkey },
2847
- evm,
2848
- solana: { secretKey: new Uint8Array(64), publicKey: "" },
2849
- mina: { privateKey: "", publicKey: "" }
4993
+ blobbiId,
4994
+ askPriceUsdc,
4995
+ lifecycleHash,
4996
+ totalSpent,
4997
+ stage,
4998
+ stats,
4999
+ sellerPubkey,
5000
+ relayUrl,
5001
+ expiresAt,
5002
+ eventId: event.id,
5003
+ createdAt: event.created_at
2850
5004
  };
2851
5005
  }
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;
5006
+
5007
+ // src/pet/filterPetListings.ts
5008
+ function compareNumericStrings(a, b) {
5009
+ if (a === b) return 0;
5010
+ try {
5011
+ const bigA = BigInt(a);
5012
+ const bigB = BigInt(b);
5013
+ if (bigA < bigB) return -1;
5014
+ if (bigA > bigB) return 1;
5015
+ return 0;
5016
+ } catch {
5017
+ const fa = Number(a);
5018
+ const fb = Number(b);
5019
+ if (!Number.isFinite(fa) && !Number.isFinite(fb)) return 0;
5020
+ if (!Number.isFinite(fa)) return -1;
5021
+ if (!Number.isFinite(fb)) return 1;
5022
+ return fa - fb;
2864
5023
  }
2865
- for (const b of bytes) {
2866
- if (b === 0) result = "1" + result;
2867
- else break;
5024
+ }
5025
+ function filterPetListings(events, options) {
5026
+ const now = Math.floor(Date.now() / 1e3);
5027
+ const listings = [];
5028
+ for (const event of events) {
5029
+ const listing = parsePetListing(event);
5030
+ if (listing === null) continue;
5031
+ if (listing.expiresAt > 0 && listing.expiresAt < now) continue;
5032
+ if (options?.minStage !== void 0 && listing.stage < options.minStage) {
5033
+ continue;
5034
+ }
5035
+ if (options?.maxAskPriceUsdc !== void 0 && listing.askPriceUsdc > options.maxAskPriceUsdc) {
5036
+ continue;
5037
+ }
5038
+ if (options?.minTotalSpent !== void 0) {
5039
+ if (compareNumericStrings(listing.totalSpent, options.minTotalSpent) < 0) {
5040
+ continue;
5041
+ }
5042
+ }
5043
+ if (options?.sellerPubkey !== void 0 && listing.sellerPubkey !== options.sellerPubkey) {
5044
+ continue;
5045
+ }
5046
+ listings.push(listing);
2868
5047
  }
2869
- return result;
5048
+ listings.sort((a, b) => compareNumericStrings(b.totalSpent, a.totalSpent));
5049
+ return listings;
2870
5050
  }
2871
5051
 
5052
+ // src/pet/buildPetPurchaseRequest.ts
5053
+ import { PET_INTERACTION_REQUEST_KIND as PET_INTERACTION_REQUEST_KIND3 } from "@toon-protocol/core";
5054
+ var TRANSFER_OWNERSHIP_ACTION = 9;
5055
+ function buildPetPurchaseRequest(params) {
5056
+ const { blobbiId, listingEventId, buyerPubkey, tokenCost, sellerPubkey } = params;
5057
+ return {
5058
+ kind: PET_INTERACTION_REQUEST_KIND3,
5059
+ created_at: Math.floor(Date.now() / 1e3),
5060
+ tags: [
5061
+ ["action", String(TRANSFER_OWNERSHIP_ACTION)],
5062
+ ["i", blobbiId],
5063
+ ["listing", listingEventId],
5064
+ ["buyer", buyerPubkey],
5065
+ ["p", sellerPubkey],
5066
+ ["cost", String(tokenCost)]
5067
+ ],
5068
+ content: ""
5069
+ };
5070
+ }
5071
+
5072
+ // src/keys/KeyManager.ts
5073
+ import { finalizeEvent as finalizeEvent2 } from "nostr-tools/pure";
5074
+ import { nip19 } from "nostr-tools";
5075
+
2872
5076
  // src/keys/PasskeyAuth.ts
2873
5077
  async function registerPasskey(params) {
2874
5078
  const { rpId, rpName, userId, userName, prfSalt } = params;
@@ -3003,7 +5207,7 @@ function hexToBytes(hex) {
3003
5207
  }
3004
5208
  return bytes;
3005
5209
  }
3006
- function bytesToHex(bytes) {
5210
+ function bytesToHex2(bytes) {
3007
5211
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3008
5212
  }
3009
5213
 
@@ -3377,7 +5581,7 @@ var KeyManager = class {
3377
5581
  "Passkey did not return a userHandle. Cannot determine Nostr pubkey for recovery."
3378
5582
  );
3379
5583
  }
3380
- const pubkey = bytesToHex(discovery.userHandle);
5584
+ const pubkey = bytesToHex2(discovery.userHandle);
3381
5585
  const vault = await fetchBackupFromRelays(pubkey, this.config.relayUrls);
3382
5586
  if (!vault) {
3383
5587
  throw new Error(
@@ -3457,7 +5661,7 @@ var KeyManager = class {
3457
5661
  });
3458
5662
  const kek = await deriveKek(registration.prfOutput);
3459
5663
  const credIdHash = await hashCredentialId(registration.credentialId);
3460
- const hexKey = bytesToHex(secretKey);
5664
+ const hexKey = bytesToHex2(secretKey);
3461
5665
  this.vault = await createVault(hexKey, kek, credIdHash, prfSalt);
3462
5666
  this.activeCredentialIdHash = credIdHash;
3463
5667
  await this.saveToLocalStorage();
@@ -3652,7 +5856,7 @@ var KeyManager = class {
3652
5856
  this.vault,
3653
5857
  this.identity.nostr.secretKey
3654
5858
  );
3655
- const signedEvent = finalizeEvent(
5859
+ const signedEvent = finalizeEvent2(
3656
5860
  eventTemplate,
3657
5861
  this.identity.nostr.secretKey
3658
5862
  );
@@ -3769,31 +5973,202 @@ function openDb(name) {
3769
5973
  request.onerror = () => reject(request.error);
3770
5974
  });
3771
5975
  }
5976
+
5977
+ // src/keys/keystore-node.ts
5978
+ import {
5979
+ scryptSync,
5980
+ createCipheriv,
5981
+ createDecipheriv,
5982
+ randomBytes
5983
+ } from "crypto";
5984
+ import { writeFileSync as writeFileSync2, readFileSync as readFileSync2 } from "fs";
5985
+ var SCRYPT_N = 2 ** 17;
5986
+ var SCRYPT_R = 8;
5987
+ var SCRYPT_P = 1;
5988
+ var SCRYPT_KEY_LEN = 32;
5989
+ var SCRYPT_MAXMEM = SCRYPT_N * SCRYPT_R * 256 + 32 * 1024 * 1024;
5990
+ var SALT_LEN = 32;
5991
+ var IV_LEN = 12;
5992
+ var AUTH_TAG_LEN = 16;
5993
+ function assertNode() {
5994
+ const versions = globalThis.process?.versions;
5995
+ if (!versions?.node) {
5996
+ throw new Error(
5997
+ "keystore-node is Node.js-only and cannot run in a browser. Use the Passkey/IndexedDB KeyManager for browser key storage."
5998
+ );
5999
+ }
6000
+ }
6001
+ function encryptMnemonic2(mnemonic, password) {
6002
+ assertNode();
6003
+ if (typeof mnemonic !== "string" || mnemonic.length === 0) {
6004
+ throw new Error("encryptMnemonic: mnemonic must be a non-empty string");
6005
+ }
6006
+ if (typeof password !== "string" || password.length === 0) {
6007
+ throw new Error("encryptMnemonic: password must be a non-empty string");
6008
+ }
6009
+ const salt = randomBytes(SALT_LEN);
6010
+ const iv = randomBytes(IV_LEN);
6011
+ const key = scryptSync(password, salt, SCRYPT_KEY_LEN, {
6012
+ N: SCRYPT_N,
6013
+ r: SCRYPT_R,
6014
+ p: SCRYPT_P,
6015
+ maxmem: SCRYPT_MAXMEM
6016
+ });
6017
+ try {
6018
+ const cipher = createCipheriv("aes-256-gcm", key, iv, {
6019
+ authTagLength: AUTH_TAG_LEN
6020
+ });
6021
+ const ciphertext = Buffer.concat([
6022
+ cipher.update(mnemonic, "utf8"),
6023
+ cipher.final()
6024
+ ]);
6025
+ const tag = cipher.getAuthTag();
6026
+ return {
6027
+ salt: salt.toString("base64"),
6028
+ iv: iv.toString("base64"),
6029
+ ciphertext: ciphertext.toString("base64"),
6030
+ tag: tag.toString("base64"),
6031
+ version: 1
6032
+ };
6033
+ } finally {
6034
+ key.fill(0);
6035
+ }
6036
+ }
6037
+ function decryptMnemonic2(encrypted, password) {
6038
+ assertNode();
6039
+ if (typeof password !== "string" || password.length === 0) {
6040
+ throw new Error("decryptMnemonic: password must be a non-empty string");
6041
+ }
6042
+ if (!encrypted || typeof encrypted.salt !== "string" || typeof encrypted.iv !== "string" || typeof encrypted.ciphertext !== "string" || typeof encrypted.tag !== "string") {
6043
+ throw new Error("decryptMnemonic: malformed keystore envelope");
6044
+ }
6045
+ const salt = Buffer.from(encrypted.salt, "base64");
6046
+ const iv = Buffer.from(encrypted.iv, "base64");
6047
+ const ciphertext = Buffer.from(encrypted.ciphertext, "base64");
6048
+ const tag = Buffer.from(encrypted.tag, "base64");
6049
+ const key = scryptSync(password, salt, SCRYPT_KEY_LEN, {
6050
+ N: SCRYPT_N,
6051
+ r: SCRYPT_R,
6052
+ p: SCRYPT_P,
6053
+ maxmem: SCRYPT_MAXMEM
6054
+ });
6055
+ try {
6056
+ const decipher = createDecipheriv("aes-256-gcm", key, iv, {
6057
+ authTagLength: AUTH_TAG_LEN
6058
+ });
6059
+ decipher.setAuthTag(tag);
6060
+ try {
6061
+ const plaintext = Buffer.concat([
6062
+ decipher.update(ciphertext),
6063
+ decipher.final()
6064
+ ]);
6065
+ return plaintext.toString("utf8");
6066
+ } catch {
6067
+ throw new Error(
6068
+ "Decryption failed: wrong password or corrupted keystore file"
6069
+ );
6070
+ }
6071
+ } finally {
6072
+ key.fill(0);
6073
+ }
6074
+ }
6075
+ function generateKeystore(path, password) {
6076
+ assertNode();
6077
+ const mnemonic = generateMnemonic();
6078
+ const keystore = encryptMnemonic2(mnemonic, password);
6079
+ writeKeystoreFile(path, keystore);
6080
+ return { mnemonic, keystore };
6081
+ }
6082
+ function importKeystore(path, mnemonic, password) {
6083
+ assertNode();
6084
+ if (!validateMnemonic(mnemonic)) {
6085
+ throw new Error(
6086
+ "Invalid BIP-39 mnemonic: checksum or word-list validation failed"
6087
+ );
6088
+ }
6089
+ const keystore = encryptMnemonic2(mnemonic, password);
6090
+ writeKeystoreFile(path, keystore);
6091
+ return keystore;
6092
+ }
6093
+ function loadKeystore(path, password) {
6094
+ assertNode();
6095
+ const raw = readFileSync2(path, "utf8");
6096
+ let parsed;
6097
+ try {
6098
+ parsed = JSON.parse(raw);
6099
+ } catch {
6100
+ throw new Error(`Keystore file at ${path} is not valid JSON`);
6101
+ }
6102
+ return decryptMnemonic2(parsed, password);
6103
+ }
6104
+ function writeKeystoreFile(path, keystore) {
6105
+ assertNode();
6106
+ writeFileSync2(path, JSON.stringify(keystore, null, 2), {
6107
+ encoding: "utf8",
6108
+ mode: 384
6109
+ });
6110
+ }
3772
6111
  export {
6112
+ ANON_ASSETS,
6113
+ ANON_VERSION,
3773
6114
  BtpRuntimeClient,
3774
6115
  ChannelManager,
3775
6116
  ConnectorError,
3776
6117
  EvmSigner,
6118
+ HS_HOSTNAME_MAX_LENGTH,
6119
+ HS_HOSTNAME_REGEX,
3777
6120
  HttpConnectorAdmin,
6121
+ HttpIlpClient,
3778
6122
  HttpRuntimeClient,
6123
+ ILP_CLAIM_HEADER,
6124
+ ILP_CLAIM_WRAPPED_HEADER,
6125
+ ILP_PEER_ID_HEADER,
3779
6126
  KeyManager,
6127
+ MinaSigner,
3780
6128
  NetworkError,
3781
6129
  OnChainChannelClient,
6130
+ SolanaSigner,
3782
6131
  ToonClient,
3783
6132
  ToonClientError,
3784
6133
  ValidationError,
3785
6134
  applyDefaults,
6135
+ applyNetworkPresets,
6136
+ assertRoutableHsHostname,
3786
6137
  buildBackupEvent,
3787
6138
  buildBackupFilter,
6139
+ buildPetInteractionRequest,
6140
+ buildPetListingEvent,
6141
+ buildPetPurchaseRequest,
3788
6142
  buildSettlementInfo,
6143
+ decryptMnemonic2 as decryptMnemonic,
3789
6144
  deriveFromNsec,
3790
6145
  deriveFullIdentity,
6146
+ deriveNostrKeyFromMnemonic,
6147
+ encryptMnemonic2 as encryptMnemonic,
6148
+ filterPetDvmProviders,
6149
+ filterPetListings,
6150
+ generateKeystore,
3791
6151
  generateMnemonic,
3792
6152
  generateRandomIdentity,
6153
+ getNetworkStatus,
6154
+ httpEndpointToBtpUrl,
6155
+ importKeystore,
3793
6156
  isPrfSupported,
6157
+ isRoutableHsHostname,
6158
+ loadKeystore,
3794
6159
  parseBackupPayload,
6160
+ parsePetInteractionEvent,
6161
+ parsePetInteractionResult,
6162
+ parsePetListing,
6163
+ readDiscoveredIlpPeer,
6164
+ readMinaDepositTotal,
6165
+ requestBlobStorage,
6166
+ selectAnonAsset,
6167
+ selectIlpTransport,
6168
+ startManagedAnonProxy,
3795
6169
  validateConfig,
3796
6170
  validateMnemonic,
3797
- withRetry
6171
+ withRetry,
6172
+ writeKeystoreFile
3798
6173
  };
3799
6174
  //# sourceMappingURL=index.js.map