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