@toon-protocol/sdk 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -19
- package/dist/chunk-XTHUXP63.js +1637 -0
- package/dist/chunk-XTHUXP63.js.map +1 -0
- package/dist/index.d.ts +1960 -0
- package/dist/index.js +1187 -301
- package/dist/index.js.map +1 -1
- package/dist/swap-D4Ozr_BM.d.ts +484 -0
- package/dist/swap.d.ts +3 -0
- package/dist/swap.js +14 -0
- package/dist/swap.js.map +1 -0
- package/package.json +23 -18
package/dist/index.js
CHANGED
|
@@ -1,266 +1,36 @@
|
|
|
1
|
-
import "./chunk-UP2VWCW5.js";
|
|
2
|
-
|
|
3
|
-
// src/identity.ts
|
|
4
1
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
var VerificationError = class extends ToonError {
|
|
40
|
-
constructor(message, cause) {
|
|
41
|
-
super(message, "VERIFICATION_ERROR", cause);
|
|
42
|
-
this.name = "VerificationError";
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
var PricingError = class extends ToonError {
|
|
46
|
-
constructor(message, cause) {
|
|
47
|
-
super(message, "PRICING_ERROR", cause);
|
|
48
|
-
this.name = "PricingError";
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// src/identity.ts
|
|
53
|
-
function generateMnemonic() {
|
|
54
|
-
return _generateMnemonic(wordlist, 128);
|
|
55
|
-
}
|
|
56
|
-
function fromMnemonic(mnemonic, options) {
|
|
57
|
-
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
58
|
-
throw new IdentityError(
|
|
59
|
-
`Invalid BIP-39 mnemonic: the provided words do not form a valid mnemonic phrase`
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
const accountIndex = options?.accountIndex ?? 0;
|
|
63
|
-
if (!Number.isInteger(accountIndex) || accountIndex < 0 || accountIndex > MAX_BIP32_INDEX) {
|
|
64
|
-
throw new IdentityError(
|
|
65
|
-
`Invalid accountIndex: expected a non-negative integer (0 to ${MAX_BIP32_INDEX}), got ${String(accountIndex)}`
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
const path = `m/44'/1237'/0'/0/${accountIndex}`;
|
|
69
|
-
let seed;
|
|
70
|
-
try {
|
|
71
|
-
seed = mnemonicToSeedSync(mnemonic);
|
|
72
|
-
const hdKey = HDKey.fromMasterSeed(seed).derive(path);
|
|
73
|
-
if (!hdKey.privateKey) {
|
|
74
|
-
throw new IdentityError(`Failed to derive private key at path ${path}`);
|
|
75
|
-
}
|
|
76
|
-
const secretKey = hdKey.privateKey;
|
|
77
|
-
const base = deriveIdentity(secretKey);
|
|
78
|
-
const solana = deriveSolanaIdentity(seed);
|
|
79
|
-
return { ...base, solana };
|
|
80
|
-
} catch (error) {
|
|
81
|
-
if (error instanceof IdentityError) {
|
|
82
|
-
throw error;
|
|
83
|
-
}
|
|
84
|
-
throw new IdentityError(
|
|
85
|
-
`Key derivation failed at path ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
86
|
-
error instanceof Error ? error : void 0
|
|
87
|
-
);
|
|
88
|
-
} finally {
|
|
89
|
-
if (seed) {
|
|
90
|
-
seed.fill(0);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function fromSecretKey(secretKey) {
|
|
95
|
-
if (!(secretKey instanceof Uint8Array)) {
|
|
96
|
-
throw new IdentityError(
|
|
97
|
-
`Invalid secret key: expected Uint8Array, got ${secretKey === null ? "null" : typeof secretKey}`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
if (secretKey.length !== 32) {
|
|
101
|
-
throw new IdentityError(
|
|
102
|
-
`Invalid secret key: expected 32 bytes, got ${secretKey.length} bytes`
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
try {
|
|
106
|
-
return deriveIdentity(secretKey);
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (error instanceof IdentityError) {
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
throw new IdentityError(
|
|
112
|
-
`Invalid secret key: ${error instanceof Error ? error.message : String(error)}`,
|
|
113
|
-
error instanceof Error ? error : void 0
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
var MAX_BIP32_INDEX = 2147483647;
|
|
118
|
-
function deriveIdentity(secretKey) {
|
|
119
|
-
const pubkey = getPublicKey(secretKey);
|
|
120
|
-
const evmAddress = computeEvmAddress(secretKey);
|
|
121
|
-
return { secretKey: new Uint8Array(secretKey), pubkey, evmAddress };
|
|
122
|
-
}
|
|
123
|
-
function computeEvmAddress(secretKey) {
|
|
124
|
-
const uncompressedPubkey = secp256k1.getPublicKey(secretKey, false);
|
|
125
|
-
const pubkeyWithoutPrefix = uncompressedPubkey.slice(1);
|
|
126
|
-
const hash = keccak_256(pubkeyWithoutPrefix);
|
|
127
|
-
const addressBytes = hash.slice(-20);
|
|
128
|
-
const addressHex = bytesToHex(addressBytes);
|
|
129
|
-
return toChecksumAddress(addressHex);
|
|
130
|
-
}
|
|
131
|
-
function toChecksumAddress(addressHex) {
|
|
132
|
-
const lower = addressHex.toLowerCase();
|
|
133
|
-
const hash = bytesToHex(keccak_256(new TextEncoder().encode(lower)));
|
|
134
|
-
let checksummed = "0x";
|
|
135
|
-
for (let i = 0; i < 40; i++) {
|
|
136
|
-
const char = lower[i];
|
|
137
|
-
const hashChar = hash[i];
|
|
138
|
-
if (char === void 0 || hashChar === void 0) {
|
|
139
|
-
throw new IdentityError(
|
|
140
|
-
`Unexpected undefined at index ${i} during checksum computation`
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
const hashNibble = parseInt(hashChar, 16);
|
|
144
|
-
checksummed += hashNibble >= 8 ? char.toUpperCase() : char;
|
|
145
|
-
}
|
|
146
|
-
return checksummed;
|
|
147
|
-
}
|
|
148
|
-
function slip0010Derive(seed, path) {
|
|
149
|
-
const encoder = new TextEncoder();
|
|
150
|
-
let I = hmac(sha512, encoder.encode("ed25519 seed"), seed);
|
|
151
|
-
let key = I.slice(0, 32);
|
|
152
|
-
let chainCode = I.slice(32);
|
|
153
|
-
for (const index of path) {
|
|
154
|
-
const data = new Uint8Array(37);
|
|
155
|
-
data[0] = 0;
|
|
156
|
-
data.set(key, 1);
|
|
157
|
-
data[33] = index >>> 24 & 255;
|
|
158
|
-
data[34] = index >>> 16 & 255;
|
|
159
|
-
data[35] = index >>> 8 & 255;
|
|
160
|
-
data[36] = index & 255;
|
|
161
|
-
I = hmac(sha512, chainCode, data);
|
|
162
|
-
key = I.slice(0, 32);
|
|
163
|
-
chainCode = I.slice(32);
|
|
164
|
-
}
|
|
165
|
-
return key;
|
|
166
|
-
}
|
|
167
|
-
var SOLANA_PATH = [
|
|
168
|
-
2147483692,
|
|
169
|
-
// 44'
|
|
170
|
-
2147484149,
|
|
171
|
-
// 501'
|
|
172
|
-
2147483648,
|
|
173
|
-
// 0'
|
|
174
|
-
2147483648
|
|
175
|
-
// 0'
|
|
176
|
-
];
|
|
177
|
-
function deriveSolanaIdentity(seed) {
|
|
178
|
-
const privateKey = slip0010Derive(seed, SOLANA_PATH);
|
|
179
|
-
const publicKeyBytes = ed25519.getPublicKey(privateKey);
|
|
180
|
-
const keypair = new Uint8Array(64);
|
|
181
|
-
keypair.set(privateKey, 0);
|
|
182
|
-
keypair.set(publicKeyBytes, 32);
|
|
183
|
-
return { secretKey: keypair, publicKey: base58Encode(publicKeyBytes) };
|
|
184
|
-
}
|
|
185
|
-
async function deriveMinaIdentity(seed) {
|
|
186
|
-
const path = "m/44'/12586'/0'/0/0";
|
|
187
|
-
const hdKey = HDKey.fromMasterSeed(seed).derive(path);
|
|
188
|
-
if (!hdKey.privateKey) {
|
|
189
|
-
throw new IdentityError(`Failed to derive Mina private key at path ${path}`);
|
|
190
|
-
}
|
|
191
|
-
const keyBytes = new Uint8Array(hdKey.privateKey);
|
|
192
|
-
const hexKey = bytesToHex(keyBytes);
|
|
193
|
-
try {
|
|
194
|
-
const MinaSignerLib = await import("mina-signer");
|
|
195
|
-
const Client = "default" in MinaSignerLib ? MinaSignerLib.default : MinaSignerLib;
|
|
196
|
-
const client = new Client({ network: "mainnet" });
|
|
197
|
-
const publicKey = client.derivePublicKey(hexKey);
|
|
198
|
-
return { privateKey: hexKey, publicKey };
|
|
199
|
-
} catch {
|
|
200
|
-
return void 0;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async function fromMnemonicFull(mnemonic, options) {
|
|
204
|
-
const identity = fromMnemonic(mnemonic, options);
|
|
205
|
-
let seed;
|
|
206
|
-
try {
|
|
207
|
-
seed = mnemonicToSeedSync(mnemonic);
|
|
208
|
-
const mina = await deriveMinaIdentity(seed);
|
|
209
|
-
if (mina) {
|
|
210
|
-
return { ...identity, mina };
|
|
211
|
-
}
|
|
212
|
-
} finally {
|
|
213
|
-
if (seed) {
|
|
214
|
-
seed.fill(0);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return identity;
|
|
218
|
-
}
|
|
219
|
-
function generateSolanaKeypair() {
|
|
220
|
-
const privateKey = ed25519.utils.randomSecretKey();
|
|
221
|
-
const publicKeyBytes = ed25519.getPublicKey(privateKey);
|
|
222
|
-
const keypair = new Uint8Array(64);
|
|
223
|
-
keypair.set(privateKey, 0);
|
|
224
|
-
keypair.set(publicKeyBytes, 32);
|
|
225
|
-
return { secretKey: keypair, publicKey: base58Encode(publicKeyBytes) };
|
|
226
|
-
}
|
|
227
|
-
var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
228
|
-
function base58Encode(bytes) {
|
|
229
|
-
let zeros = 0;
|
|
230
|
-
for (let i = 0; i < bytes.length && bytes[i] === 0; i++) zeros++;
|
|
231
|
-
let value = 0n;
|
|
232
|
-
for (const byte of bytes) {
|
|
233
|
-
value = value * 256n + BigInt(byte);
|
|
234
|
-
}
|
|
235
|
-
let result = "";
|
|
236
|
-
while (value > 0n) {
|
|
237
|
-
result = BASE58_ALPHABET[Number(value % 58n)] + result;
|
|
238
|
-
value = value / 58n;
|
|
239
|
-
}
|
|
240
|
-
for (let i = 0; i < zeros; i++) {
|
|
241
|
-
result = "1" + result;
|
|
242
|
-
}
|
|
243
|
-
return result || "1";
|
|
244
|
-
}
|
|
245
|
-
function base58Decode(str) {
|
|
246
|
-
let zeros = 0;
|
|
247
|
-
for (let i = 0; i < str.length && str[i] === "1"; i++) zeros++;
|
|
248
|
-
let value = 0n;
|
|
249
|
-
for (const ch of str) {
|
|
250
|
-
const idx = BASE58_ALPHABET.indexOf(ch);
|
|
251
|
-
if (idx === -1) throw new IdentityError(`Invalid base58 character: ${ch}`);
|
|
252
|
-
value = value * 58n + BigInt(idx);
|
|
253
|
-
}
|
|
254
|
-
const hex = value === 0n ? "" : value.toString(16);
|
|
255
|
-
const hexPadded = hex.length % 2 ? "0" + hex : hex;
|
|
256
|
-
const rawBytes = [];
|
|
257
|
-
for (let i = 0; i < hexPadded.length; i += 2) {
|
|
258
|
-
rawBytes.push(parseInt(hexPadded.slice(i, i + 2), 16));
|
|
259
|
-
}
|
|
260
|
-
const result = new Uint8Array(zeros + rawBytes.length);
|
|
261
|
-
result.set(rawBytes, zeros);
|
|
262
|
-
return result;
|
|
263
|
-
}
|
|
2
|
+
GiftWrapError,
|
|
3
|
+
HandlerError,
|
|
4
|
+
IdentityError,
|
|
5
|
+
NodeError,
|
|
6
|
+
PricingError,
|
|
7
|
+
SWAP_HANDLER_REJECT_CODES,
|
|
8
|
+
SWAP_HANDLER_REJECT_MESSAGES,
|
|
9
|
+
SettlementTxError,
|
|
10
|
+
StreamSwapError,
|
|
11
|
+
SwapHandlerError,
|
|
12
|
+
VerificationError,
|
|
13
|
+
__testing,
|
|
14
|
+
applyRate,
|
|
15
|
+
base58Decode,
|
|
16
|
+
base58Encode,
|
|
17
|
+
createSwapHandler,
|
|
18
|
+
decryptFulfillClaim,
|
|
19
|
+
encryptFulfillClaim,
|
|
20
|
+
findSwapPair,
|
|
21
|
+
fromMnemonic,
|
|
22
|
+
fromMnemonicFull,
|
|
23
|
+
fromSecretKey,
|
|
24
|
+
generateMnemonic,
|
|
25
|
+
generateSolanaKeypair,
|
|
26
|
+
streamSwap,
|
|
27
|
+
streamSwapControlled,
|
|
28
|
+
unwrapSwapPacket,
|
|
29
|
+
unwrapSwapPacketFromToon,
|
|
30
|
+
wrapSwapPacket,
|
|
31
|
+
wrapSwapPacketToToon
|
|
32
|
+
} from "./chunk-XTHUXP63.js";
|
|
33
|
+
import "./chunk-UP2VWCW5.js";
|
|
264
34
|
|
|
265
35
|
// src/handler-context.ts
|
|
266
36
|
function createHandlerContext(options) {
|
|
@@ -329,6 +99,13 @@ var HandlerRegistry = class {
|
|
|
329
99
|
getRegisteredKinds() {
|
|
330
100
|
return [...this.handlers.keys()].sort((a, b) => a - b);
|
|
331
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Returns the handler registered for `kind`, or `undefined` if none.
|
|
104
|
+
* Added for Story 12.7 AC-10 (handler-registration verification).
|
|
105
|
+
*/
|
|
106
|
+
get(kind) {
|
|
107
|
+
return this.handlers.get(kind);
|
|
108
|
+
}
|
|
332
109
|
/**
|
|
333
110
|
* Returns registered kinds in the DVM request range (5000-5999), sorted ascending.
|
|
334
111
|
* Uses JOB_REQUEST_KIND_BASE (5000) as the range start.
|
|
@@ -508,7 +285,8 @@ import {
|
|
|
508
285
|
calculateRouteAmount,
|
|
509
286
|
resolveRouteFees,
|
|
510
287
|
buildPrefixClaimEvent,
|
|
511
|
-
validatePrefix
|
|
288
|
+
validatePrefix,
|
|
289
|
+
ilpCodeToSemantic
|
|
512
290
|
} from "@toon-protocol/core";
|
|
513
291
|
import {
|
|
514
292
|
shallowParseToon,
|
|
@@ -517,7 +295,7 @@ import {
|
|
|
517
295
|
} from "@toon-protocol/core/toon";
|
|
518
296
|
|
|
519
297
|
// src/skill-descriptor.ts
|
|
520
|
-
import { ToonError
|
|
298
|
+
import { ToonError } from "@toon-protocol/core";
|
|
521
299
|
var HEX_64_REGEX = /^[0-9a-f]{64}$/;
|
|
522
300
|
function buildSkillDescriptor(registry, config = {}) {
|
|
523
301
|
const dvmKinds = registry.getDvmKinds();
|
|
@@ -526,7 +304,7 @@ function buildSkillDescriptor(registry, config = {}) {
|
|
|
526
304
|
}
|
|
527
305
|
if (config.attestation !== void 0) {
|
|
528
306
|
if (!HEX_64_REGEX.test(config.attestation.eventId)) {
|
|
529
|
-
throw new
|
|
307
|
+
throw new ToonError(
|
|
530
308
|
"Skill descriptor attestation eventId must be a 64-character lowercase hex string",
|
|
531
309
|
"DVM_SKILL_INVALID_ATTESTATION_EVENT_ID"
|
|
532
310
|
);
|
|
@@ -717,6 +495,27 @@ function createNode(config) {
|
|
|
717
495
|
trackerRef.current.processEvent(decoded);
|
|
718
496
|
}
|
|
719
497
|
}
|
|
498
|
+
if (result.accept && result.metadata && !result.data) {
|
|
499
|
+
try {
|
|
500
|
+
const metadataJson = JSON.stringify(result.metadata);
|
|
501
|
+
result.data = Buffer.from(
|
|
502
|
+
metadataJson,
|
|
503
|
+
"utf8"
|
|
504
|
+
).toString("base64");
|
|
505
|
+
} catch (err) {
|
|
506
|
+
console.error(
|
|
507
|
+
"Failed to serialize handler metadata to FULFILL data:",
|
|
508
|
+
err instanceof Error ? err.message : err
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (!result.accept && !result.rejectReason && result.code) {
|
|
513
|
+
const ilpCode = result.code;
|
|
514
|
+
result.rejectReason = {
|
|
515
|
+
code: ilpCodeToSemantic(ilpCode),
|
|
516
|
+
message: result.message ?? "Payment rejected"
|
|
517
|
+
};
|
|
518
|
+
}
|
|
720
519
|
return result;
|
|
721
520
|
} catch (err) {
|
|
722
521
|
const errMsg = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -769,6 +568,8 @@ function createNode(config) {
|
|
|
769
568
|
ilpAddress: resolvedIlpAddress,
|
|
770
569
|
ilpAddresses: resolvedIlpAddresses,
|
|
771
570
|
btpEndpoint: config.btpEndpoint ?? "",
|
|
571
|
+
...config.httpEndpoint !== void 0 && { httpEndpoint: config.httpEndpoint },
|
|
572
|
+
...config.supportsUpgrade !== void 0 && { supportsUpgrade: config.supportsUpgrade },
|
|
772
573
|
assetCode: config.assetCode ?? "USD",
|
|
773
574
|
assetScale: config.assetScale ?? 6,
|
|
774
575
|
feePerByte: String(config.feePerByte ?? 0n)
|
|
@@ -857,32 +658,36 @@ function createNode(config) {
|
|
|
857
658
|
}
|
|
858
659
|
const hasSettlementAddresses = chainConfig.registryAddress && chainConfig.tokenNetworkAddress;
|
|
859
660
|
const hasChainProviders = config.chainProviders !== void 0 && config.chainProviders.length > 0;
|
|
860
|
-
|
|
661
|
+
const effectiveChainProviders = hasChainProviders ? config.chainProviders : hasSettlementAddresses ? [
|
|
861
662
|
{
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
663
|
+
chainType: "evm",
|
|
664
|
+
chainId: `evm:${chainConfig.chainId ?? "31337"}`,
|
|
665
|
+
rpcUrl: chainConfig.rpcUrl,
|
|
666
|
+
registryAddress: chainConfig.registryAddress,
|
|
667
|
+
tokenAddress: chainConfig.usdcAddress,
|
|
668
|
+
privateKey: settlementPrivateKey
|
|
669
|
+
}
|
|
670
|
+
] : void 0;
|
|
671
|
+
const connectorConfig = {
|
|
672
|
+
nodeId,
|
|
673
|
+
btpServerPort,
|
|
674
|
+
environment: "development",
|
|
675
|
+
deploymentMode: "embedded",
|
|
676
|
+
peers: [],
|
|
677
|
+
routes: [],
|
|
678
|
+
localDelivery: { enabled: false }
|
|
679
|
+
};
|
|
680
|
+
if (effectiveChainProviders) {
|
|
681
|
+
connectorConfig.chainProviders = effectiveChainProviders;
|
|
682
|
+
}
|
|
683
|
+
if (config.transport) {
|
|
684
|
+
connectorConfig.transport = config.transport;
|
|
685
|
+
}
|
|
686
|
+
if (config.nip59) {
|
|
687
|
+
connectorConfig.nip59 = config.nip59;
|
|
688
|
+
}
|
|
689
|
+
autoCreatedConnector = new ConnectorNodeClass(
|
|
690
|
+
connectorConfig,
|
|
886
691
|
connectorLogger
|
|
887
692
|
);
|
|
888
693
|
await autoCreatedConnector.start();
|
|
@@ -1627,7 +1432,7 @@ var WorkflowOrchestrator = class {
|
|
|
1627
1432
|
|
|
1628
1433
|
// src/swarm-coordinator.ts
|
|
1629
1434
|
import {
|
|
1630
|
-
ToonError as
|
|
1435
|
+
ToonError as ToonError2,
|
|
1631
1436
|
parseSwarmRequest,
|
|
1632
1437
|
parseSwarmSelection
|
|
1633
1438
|
} from "@toon-protocol/core";
|
|
@@ -1678,7 +1483,7 @@ var SwarmCoordinator = class {
|
|
|
1678
1483
|
async startSwarm(swarmRequest) {
|
|
1679
1484
|
const parsed = parseSwarmRequest(swarmRequest);
|
|
1680
1485
|
if (!parsed) {
|
|
1681
|
-
throw new
|
|
1486
|
+
throw new ToonError2(
|
|
1682
1487
|
"Invalid swarm request event: missing swarm tag or invalid Kind 5xxx",
|
|
1683
1488
|
"DVM_INVALID_KIND"
|
|
1684
1489
|
);
|
|
@@ -1737,32 +1542,32 @@ var SwarmCoordinator = class {
|
|
|
1737
1542
|
*/
|
|
1738
1543
|
async selectWinner(selectionEvent) {
|
|
1739
1544
|
if (this.state === "settled") {
|
|
1740
|
-
throw new
|
|
1545
|
+
throw new ToonError2(
|
|
1741
1546
|
"Swarm has already been settled; duplicate selection rejected",
|
|
1742
1547
|
"DVM_SWARM_ALREADY_SETTLED"
|
|
1743
1548
|
);
|
|
1744
1549
|
}
|
|
1745
1550
|
if (this.state !== "judging") {
|
|
1746
|
-
throw new
|
|
1551
|
+
throw new ToonError2(
|
|
1747
1552
|
`Cannot select winner in state '${this.state}'; swarm must be in 'judging' state`,
|
|
1748
1553
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1749
1554
|
);
|
|
1750
1555
|
}
|
|
1751
1556
|
if (selectionEvent.pubkey !== this.customerPubkey) {
|
|
1752
|
-
throw new
|
|
1557
|
+
throw new ToonError2(
|
|
1753
1558
|
"Selection event pubkey does not match the swarm request customer pubkey",
|
|
1754
1559
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1755
1560
|
);
|
|
1756
1561
|
}
|
|
1757
1562
|
const parsed = parseSwarmSelection(selectionEvent);
|
|
1758
1563
|
if (!parsed) {
|
|
1759
|
-
throw new
|
|
1564
|
+
throw new ToonError2(
|
|
1760
1565
|
"Invalid swarm selection event: missing winner tag or invalid Kind 7000",
|
|
1761
1566
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1762
1567
|
);
|
|
1763
1568
|
}
|
|
1764
1569
|
if (parsed.swarmRequestEventId !== this.swarmRequestId) {
|
|
1765
|
-
throw new
|
|
1570
|
+
throw new ToonError2(
|
|
1766
1571
|
`Selection event references swarm '${parsed.swarmRequestEventId}' but this swarm is '${this.swarmRequestId}'`,
|
|
1767
1572
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1768
1573
|
);
|
|
@@ -1771,7 +1576,7 @@ var SwarmCoordinator = class {
|
|
|
1771
1576
|
(s) => s.id === parsed.winnerResultEventId
|
|
1772
1577
|
);
|
|
1773
1578
|
if (!winnerSubmission) {
|
|
1774
|
-
throw new
|
|
1579
|
+
throw new ToonError2(
|
|
1775
1580
|
`Winner result event ID '${parsed.winnerResultEventId}' not found in collected submissions`,
|
|
1776
1581
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1777
1582
|
);
|
|
@@ -1782,7 +1587,7 @@ var SwarmCoordinator = class {
|
|
|
1782
1587
|
this.settlementSucceeded = true;
|
|
1783
1588
|
} catch (_err) {
|
|
1784
1589
|
this.settlementSucceeded = false;
|
|
1785
|
-
throw new
|
|
1590
|
+
throw new ToonError2(
|
|
1786
1591
|
"Settlement failed for winning provider; swarm remains in judging state for retry",
|
|
1787
1592
|
"DVM_SWARM_SETTLEMENT_FAILED"
|
|
1788
1593
|
);
|
|
@@ -1946,6 +1751,10 @@ function createArweaveDvmHandler(config) {
|
|
|
1946
1751
|
data: Buffer.from(txId).toString("base64")
|
|
1947
1752
|
};
|
|
1948
1753
|
} catch (error) {
|
|
1754
|
+
console.error(
|
|
1755
|
+
"[ArweaveDvmHandler] Chunked upload failed:",
|
|
1756
|
+
error instanceof Error ? error.stack ?? error.message : error
|
|
1757
|
+
);
|
|
1949
1758
|
const internalMsg = error instanceof Error ? error.message : "Unknown chunk error";
|
|
1950
1759
|
const safeMsg = internalMsg.includes("Duplicate chunkIndex") ? "Duplicate chunk rejected" : internalMsg.includes("Max active uploads") ? "Server busy, too many concurrent uploads" : internalMsg.includes("exceeds max bytes") ? "Upload size limit exceeded" : "Chunk processing error";
|
|
1951
1760
|
return {
|
|
@@ -1961,7 +1770,11 @@ function createArweaveDvmHandler(config) {
|
|
|
1961
1770
|
accept: true,
|
|
1962
1771
|
data: Buffer.from(txId).toString("base64")
|
|
1963
1772
|
};
|
|
1964
|
-
} catch {
|
|
1773
|
+
} catch (error) {
|
|
1774
|
+
const cause = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1775
|
+
console.error(
|
|
1776
|
+
`[ArweaveDvmHandler] Arweave upload failed (${parsed.blobData.length} bytes). Cause: ${cause}`
|
|
1777
|
+
);
|
|
1965
1778
|
return {
|
|
1966
1779
|
accept: false,
|
|
1967
1780
|
code: "T00",
|
|
@@ -2183,20 +1996,1076 @@ async function uploadBlobChunked(node, blob, destination, options) {
|
|
|
2183
1996
|
}
|
|
2184
1997
|
return lastResult.eventId;
|
|
2185
1998
|
}
|
|
1999
|
+
|
|
2000
|
+
// src/settlement/evm.ts
|
|
2001
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
2002
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2003
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
2004
|
+
|
|
2005
|
+
// src/settlement/hashes.ts
|
|
2006
|
+
import {
|
|
2007
|
+
hexToBytes as hexToBytes2,
|
|
2008
|
+
bigintToBytes32BE,
|
|
2009
|
+
concatBytes,
|
|
2010
|
+
balanceProofHashEvm,
|
|
2011
|
+
balanceProofHashSolana,
|
|
2012
|
+
minaHashToField,
|
|
2013
|
+
balanceProofFieldsMina
|
|
2014
|
+
} from "@toon-protocol/core";
|
|
2015
|
+
|
|
2016
|
+
// src/settlement/evm.ts
|
|
2017
|
+
var EVM_SETTLEMENT_FUNCTION_SIGNATURE = "updateBalance(bytes32,uint256,uint256,address,bytes)";
|
|
2018
|
+
var EVM_SETTLEMENT_EVENT_SIGNATURE = "SettlementSucceeded(bytes32,uint256,uint256,address)";
|
|
2019
|
+
var EVM_SETTLEMENT_FUNCTION_SELECTOR = keccak_256(
|
|
2020
|
+
new TextEncoder().encode(EVM_SETTLEMENT_FUNCTION_SIGNATURE)
|
|
2021
|
+
).slice(0, 4);
|
|
2022
|
+
var EVM_SETTLEMENT_EVENT_TOPIC = "0x" + bytesToHex(
|
|
2023
|
+
keccak_256(new TextEncoder().encode(EVM_SETTLEMENT_EVENT_SIGNATURE))
|
|
2024
|
+
);
|
|
2025
|
+
function recoverEvmSignerAddress(claim) {
|
|
2026
|
+
if (claim.channelId === void 0 || claim.cumulativeAmount === void 0 || claim.nonce === void 0 || claim.recipient === void 0) {
|
|
2027
|
+
throw new SettlementTxError(
|
|
2028
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2029
|
+
"Claim missing channelId/cumulativeAmount/nonce/recipient for EVM signer recovery"
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
if (claim.claimBytes.length !== 65) {
|
|
2033
|
+
throw new SettlementTxError(
|
|
2034
|
+
"INVALID_SIGNATURE_LENGTH",
|
|
2035
|
+
`EVM signature must be 65 bytes (r||s||v), got ${claim.claimBytes.length}`
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
const v = claim.claimBytes[64];
|
|
2039
|
+
if (v !== 27 && v !== 28) {
|
|
2040
|
+
throw new SettlementTxError(
|
|
2041
|
+
"INVALID_SIGNATURE_V",
|
|
2042
|
+
`EVM signature v must be 27 or 28, got ${v}`
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
const recovery = v - 27;
|
|
2046
|
+
const compactRS = claim.claimBytes.slice(0, 64);
|
|
2047
|
+
let msgHash;
|
|
2048
|
+
let uncompressedPubkey;
|
|
2049
|
+
try {
|
|
2050
|
+
const channelIdBytes = hexToBytes2(claim.channelId);
|
|
2051
|
+
const recipientBytes = hexToBytes2(claim.recipient);
|
|
2052
|
+
if (channelIdBytes.length !== 32) {
|
|
2053
|
+
throw new Error(
|
|
2054
|
+
`channelId must be 32 bytes (got ${channelIdBytes.length})`
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
if (recipientBytes.length !== 20) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
`recipient must be 20 bytes (got ${recipientBytes.length})`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
msgHash = balanceProofHashEvm(
|
|
2063
|
+
channelIdBytes,
|
|
2064
|
+
BigInt(claim.cumulativeAmount),
|
|
2065
|
+
BigInt(claim.nonce),
|
|
2066
|
+
recipientBytes
|
|
2067
|
+
);
|
|
2068
|
+
const sig = secp256k1.Signature.fromBytes(
|
|
2069
|
+
compactRS,
|
|
2070
|
+
"compact"
|
|
2071
|
+
).addRecoveryBit(recovery);
|
|
2072
|
+
const point = sig.recoverPublicKey(msgHash);
|
|
2073
|
+
uncompressedPubkey = point.toBytes(false);
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
throw new SettlementTxError(
|
|
2076
|
+
"ENCODING_FAILED",
|
|
2077
|
+
`EVM signer recovery failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2078
|
+
{ cause: err }
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
const addrHash = keccak_256(uncompressedPubkey.slice(1));
|
|
2082
|
+
return "0x" + bytesToHex(addrHash.slice(-20)).toLowerCase();
|
|
2083
|
+
}
|
|
2084
|
+
function padLeft32(bytes) {
|
|
2085
|
+
if (bytes.length > 32) {
|
|
2086
|
+
throw new SettlementTxError(
|
|
2087
|
+
"ENCODING_FAILED",
|
|
2088
|
+
`cannot pad bytes of length ${bytes.length} to 32`
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
const out = new Uint8Array(32);
|
|
2092
|
+
out.set(bytes, 32 - bytes.length);
|
|
2093
|
+
return out;
|
|
2094
|
+
}
|
|
2095
|
+
function padRight32(bytes) {
|
|
2096
|
+
const padded = Math.ceil(bytes.length / 32) * 32;
|
|
2097
|
+
const out = new Uint8Array(padded);
|
|
2098
|
+
out.set(bytes, 0);
|
|
2099
|
+
return out;
|
|
2100
|
+
}
|
|
2101
|
+
function encodeUpdateBalanceCallData(channelIdBytes, cumulativeAmount, nonce, recipientBytes, signature) {
|
|
2102
|
+
if (channelIdBytes.length !== 32) {
|
|
2103
|
+
throw new SettlementTxError(
|
|
2104
|
+
"ENCODING_FAILED",
|
|
2105
|
+
`channelId must be 32 bytes (got ${channelIdBytes.length})`
|
|
2106
|
+
);
|
|
2107
|
+
}
|
|
2108
|
+
if (recipientBytes.length !== 20) {
|
|
2109
|
+
throw new SettlementTxError(
|
|
2110
|
+
"ENCODING_FAILED",
|
|
2111
|
+
`recipient must be 20 bytes (got ${recipientBytes.length})`
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
const channelIdWord = channelIdBytes;
|
|
2115
|
+
const cumulativeWord = bigintToBytes32BE(cumulativeAmount);
|
|
2116
|
+
const nonceWord = bigintToBytes32BE(nonce);
|
|
2117
|
+
const recipientWord = padLeft32(recipientBytes);
|
|
2118
|
+
const offsetWord = bigintToBytes32BE(160n);
|
|
2119
|
+
const sigLenWord = bigintToBytes32BE(BigInt(signature.length));
|
|
2120
|
+
const sigPadded = padRight32(signature);
|
|
2121
|
+
return concatBytes(
|
|
2122
|
+
EVM_SETTLEMENT_FUNCTION_SELECTOR,
|
|
2123
|
+
channelIdWord,
|
|
2124
|
+
cumulativeWord,
|
|
2125
|
+
nonceWord,
|
|
2126
|
+
recipientWord,
|
|
2127
|
+
offsetWord,
|
|
2128
|
+
sigLenWord,
|
|
2129
|
+
sigPadded
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
function rlpEncodeBytes(bytes) {
|
|
2133
|
+
if (bytes.length === 1 && bytes[0] !== void 0 && bytes[0] < 128) {
|
|
2134
|
+
return bytes;
|
|
2135
|
+
}
|
|
2136
|
+
if (bytes.length < 56) {
|
|
2137
|
+
return concatBytes(new Uint8Array([128 + bytes.length]), bytes);
|
|
2138
|
+
}
|
|
2139
|
+
const lenBytes = bigintToMinimalBytes(BigInt(bytes.length));
|
|
2140
|
+
return concatBytes(new Uint8Array([183 + lenBytes.length]), lenBytes, bytes);
|
|
2141
|
+
}
|
|
2142
|
+
function rlpEncodeList(items) {
|
|
2143
|
+
const payload = concatBytes(...items);
|
|
2144
|
+
if (payload.length < 56) {
|
|
2145
|
+
return concatBytes(new Uint8Array([192 + payload.length]), payload);
|
|
2146
|
+
}
|
|
2147
|
+
const lenBytes = bigintToMinimalBytes(BigInt(payload.length));
|
|
2148
|
+
return concatBytes(
|
|
2149
|
+
new Uint8Array([247 + lenBytes.length]),
|
|
2150
|
+
lenBytes,
|
|
2151
|
+
payload
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
function bigintToMinimalBytes(x) {
|
|
2155
|
+
if (x < 0n) {
|
|
2156
|
+
throw new SettlementTxError(
|
|
2157
|
+
"ENCODING_FAILED",
|
|
2158
|
+
"bigintToMinimalBytes: negative input"
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
if (x === 0n) return new Uint8Array(0);
|
|
2162
|
+
let hex = x.toString(16);
|
|
2163
|
+
if (hex.length % 2) hex = "0" + hex;
|
|
2164
|
+
const out = new Uint8Array(hex.length / 2);
|
|
2165
|
+
for (let i = 0; i < out.length; i++) {
|
|
2166
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
2167
|
+
}
|
|
2168
|
+
return out;
|
|
2169
|
+
}
|
|
2170
|
+
function rlpEncodeUint(x) {
|
|
2171
|
+
return rlpEncodeBytes(bigintToMinimalBytes(x));
|
|
2172
|
+
}
|
|
2173
|
+
function rlpEncodeUnsignedTx(params) {
|
|
2174
|
+
return rlpEncodeList([
|
|
2175
|
+
rlpEncodeUint(params.nonce),
|
|
2176
|
+
rlpEncodeUint(params.gasPrice),
|
|
2177
|
+
rlpEncodeUint(params.gasLimit),
|
|
2178
|
+
rlpEncodeBytes(params.to),
|
|
2179
|
+
rlpEncodeUint(params.value),
|
|
2180
|
+
rlpEncodeBytes(params.data),
|
|
2181
|
+
rlpEncodeUint(params.chainId),
|
|
2182
|
+
rlpEncodeUint(0n),
|
|
2183
|
+
rlpEncodeUint(0n)
|
|
2184
|
+
]);
|
|
2185
|
+
}
|
|
2186
|
+
function buildEvmSettlementTx(winner, signer, recipient, selectedClaimIndex, claimsMerged) {
|
|
2187
|
+
if (winner.channelId === void 0 || winner.cumulativeAmount === void 0 || winner.nonce === void 0 || winner.recipient === void 0 || winner.millSignerAddress === void 0) {
|
|
2188
|
+
throw new SettlementTxError(
|
|
2189
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2190
|
+
"EVM winner claim missing settlement-context fields"
|
|
2191
|
+
);
|
|
2192
|
+
}
|
|
2193
|
+
if (!signer.contractAddress) {
|
|
2194
|
+
throw new SettlementTxError(
|
|
2195
|
+
"INVALID_INPUT",
|
|
2196
|
+
`EVM MillSignerConfig.contractAddress is required for chain ${winner.pair.to.chain}`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
if (typeof signer.chainId !== "number" || !Number.isInteger(signer.chainId) || signer.chainId <= 0) {
|
|
2200
|
+
throw new SettlementTxError(
|
|
2201
|
+
"INVALID_INPUT",
|
|
2202
|
+
`EVM MillSignerConfig.chainId must be a positive integer, got ${signer.chainId}`
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
const channelIdBytes = hexToBytes2(winner.channelId);
|
|
2206
|
+
const recipientBytes = hexToBytes2(recipient);
|
|
2207
|
+
const contractBytes = hexToBytes2(signer.contractAddress);
|
|
2208
|
+
const calldata = encodeUpdateBalanceCallData(
|
|
2209
|
+
channelIdBytes,
|
|
2210
|
+
BigInt(winner.cumulativeAmount),
|
|
2211
|
+
BigInt(winner.nonce),
|
|
2212
|
+
recipientBytes,
|
|
2213
|
+
winner.claimBytes
|
|
2214
|
+
);
|
|
2215
|
+
const unsignedTxBytes = rlpEncodeUnsignedTx({
|
|
2216
|
+
nonce: 0n,
|
|
2217
|
+
gasPrice: 0n,
|
|
2218
|
+
gasLimit: 0n,
|
|
2219
|
+
to: contractBytes,
|
|
2220
|
+
value: 0n,
|
|
2221
|
+
data: calldata,
|
|
2222
|
+
chainId: BigInt(signer.chainId)
|
|
2223
|
+
});
|
|
2224
|
+
return {
|
|
2225
|
+
chain: winner.pair.to.chain,
|
|
2226
|
+
chainKind: "evm",
|
|
2227
|
+
channelId: winner.channelId,
|
|
2228
|
+
cumulativeAmount: winner.cumulativeAmount,
|
|
2229
|
+
nonce: winner.nonce,
|
|
2230
|
+
recipient,
|
|
2231
|
+
millSignerAddress: winner.millSignerAddress,
|
|
2232
|
+
unsignedTxBytes,
|
|
2233
|
+
expectedEventSignature: EVM_SETTLEMENT_EVENT_TOPIC,
|
|
2234
|
+
claimsMerged,
|
|
2235
|
+
selectedClaimIndex,
|
|
2236
|
+
sourceChain: winner.pair.from.chain,
|
|
2237
|
+
sourceAssetCode: winner.pair.from.assetCode
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
function fillEvmSettlementTxGas(bundle, gas, signer) {
|
|
2241
|
+
if (bundle.chainKind !== "evm") {
|
|
2242
|
+
throw new SettlementTxError(
|
|
2243
|
+
"UNSUPPORTED_CHAIN",
|
|
2244
|
+
`fillEvmSettlementTxGas requires chainKind=evm, got ${bundle.chainKind}`
|
|
2245
|
+
);
|
|
2246
|
+
}
|
|
2247
|
+
if (!signer.contractAddress) {
|
|
2248
|
+
throw new SettlementTxError(
|
|
2249
|
+
"INVALID_INPUT",
|
|
2250
|
+
"EVM MillSignerConfig.contractAddress is required for gas-fill"
|
|
2251
|
+
);
|
|
2252
|
+
}
|
|
2253
|
+
if (typeof signer.chainId !== "number" || signer.chainId <= 0) {
|
|
2254
|
+
throw new SettlementTxError(
|
|
2255
|
+
"INVALID_INPUT",
|
|
2256
|
+
"EVM MillSignerConfig.chainId must be a positive integer"
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
const channelIdBytes = hexToBytes2(bundle.channelId);
|
|
2260
|
+
const recipientBytes = hexToBytes2(bundle.recipient);
|
|
2261
|
+
const contractBytes = hexToBytes2(signer.contractAddress);
|
|
2262
|
+
const sig = extractSignatureFromBundle(bundle);
|
|
2263
|
+
const calldata = encodeUpdateBalanceCallData(
|
|
2264
|
+
channelIdBytes,
|
|
2265
|
+
BigInt(bundle.cumulativeAmount),
|
|
2266
|
+
BigInt(bundle.nonce),
|
|
2267
|
+
recipientBytes,
|
|
2268
|
+
sig
|
|
2269
|
+
);
|
|
2270
|
+
return rlpEncodeUnsignedTx({
|
|
2271
|
+
nonce: gas.nonce,
|
|
2272
|
+
gasPrice: gas.gasPrice,
|
|
2273
|
+
gasLimit: gas.gasLimit,
|
|
2274
|
+
to: contractBytes,
|
|
2275
|
+
value: 0n,
|
|
2276
|
+
data: calldata,
|
|
2277
|
+
chainId: BigInt(signer.chainId)
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
function extractSignatureFromBundle(bundle) {
|
|
2281
|
+
const tx = bundle.unsignedTxBytes;
|
|
2282
|
+
const list = rlpDecodeList(tx);
|
|
2283
|
+
if (list.length < 6) {
|
|
2284
|
+
throw new SettlementTxError(
|
|
2285
|
+
"ENCODING_FAILED",
|
|
2286
|
+
"unsignedTxBytes is not a 9-element RLP list"
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
const data = list[5];
|
|
2290
|
+
if (!data) {
|
|
2291
|
+
throw new SettlementTxError(
|
|
2292
|
+
"ENCODING_FAILED",
|
|
2293
|
+
"unsignedTxBytes missing data field"
|
|
2294
|
+
);
|
|
2295
|
+
}
|
|
2296
|
+
const sigLenOffset = 4 + 5 * 32;
|
|
2297
|
+
if (data.length < sigLenOffset + 32) {
|
|
2298
|
+
throw new SettlementTxError(
|
|
2299
|
+
"ENCODING_FAILED",
|
|
2300
|
+
"calldata too short to contain signature length word"
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
const sigLenBytes = data.slice(sigLenOffset, sigLenOffset + 32);
|
|
2304
|
+
let sigLen = 0n;
|
|
2305
|
+
for (const b of sigLenBytes) sigLen = sigLen << 8n | BigInt(b);
|
|
2306
|
+
const sigStart = sigLenOffset + 32;
|
|
2307
|
+
const sigEnd = sigStart + Number(sigLen);
|
|
2308
|
+
if (sigEnd > data.length) {
|
|
2309
|
+
throw new SettlementTxError(
|
|
2310
|
+
"ENCODING_FAILED",
|
|
2311
|
+
"calldata truncated before end of signature bytes"
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
return data.slice(sigStart, sigEnd);
|
|
2315
|
+
}
|
|
2316
|
+
function rlpDecodeList(buf) {
|
|
2317
|
+
if (buf.length === 0) {
|
|
2318
|
+
throw new SettlementTxError("ENCODING_FAILED", "empty RLP input");
|
|
2319
|
+
}
|
|
2320
|
+
const first = buf[0] ?? 0;
|
|
2321
|
+
let offset;
|
|
2322
|
+
let listEnd;
|
|
2323
|
+
if (first >= 192 && first <= 247) {
|
|
2324
|
+
offset = 1;
|
|
2325
|
+
listEnd = 1 + (first - 192);
|
|
2326
|
+
} else if (first >= 248 && first <= 255) {
|
|
2327
|
+
const lenOfLen = first - 247;
|
|
2328
|
+
let listLen = 0;
|
|
2329
|
+
for (let i = 0; i < lenOfLen; i++) {
|
|
2330
|
+
listLen = listLen << 8 | (buf[1 + i] ?? 0);
|
|
2331
|
+
}
|
|
2332
|
+
offset = 1 + lenOfLen;
|
|
2333
|
+
listEnd = offset + listLen;
|
|
2334
|
+
} else {
|
|
2335
|
+
throw new SettlementTxError(
|
|
2336
|
+
"ENCODING_FAILED",
|
|
2337
|
+
`rlpDecodeList: input is not a list (first byte 0x${first.toString(16)})`
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
2340
|
+
const items = [];
|
|
2341
|
+
let p = offset;
|
|
2342
|
+
while (p < listEnd) {
|
|
2343
|
+
const b = buf[p] ?? 0;
|
|
2344
|
+
if (b < 128) {
|
|
2345
|
+
items.push(buf.slice(p, p + 1));
|
|
2346
|
+
p += 1;
|
|
2347
|
+
} else if (b <= 183) {
|
|
2348
|
+
const len = b - 128;
|
|
2349
|
+
items.push(buf.slice(p + 1, p + 1 + len));
|
|
2350
|
+
p += 1 + len;
|
|
2351
|
+
} else if (b <= 191) {
|
|
2352
|
+
const lenOfLen = b - 183;
|
|
2353
|
+
let len = 0;
|
|
2354
|
+
for (let i = 0; i < lenOfLen; i++) {
|
|
2355
|
+
len = len << 8 | (buf[p + 1 + i] ?? 0);
|
|
2356
|
+
}
|
|
2357
|
+
items.push(buf.slice(p + 1 + lenOfLen, p + 1 + lenOfLen + len));
|
|
2358
|
+
p += 1 + lenOfLen + len;
|
|
2359
|
+
} else {
|
|
2360
|
+
throw new SettlementTxError(
|
|
2361
|
+
"ENCODING_FAILED",
|
|
2362
|
+
"rlpDecodeList: nested list not supported in minimal decoder"
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
return items;
|
|
2367
|
+
}
|
|
2368
|
+
function verifyEvmClaimSignature(claim, expectedAddress) {
|
|
2369
|
+
const recovered = recoverEvmSignerAddress(claim);
|
|
2370
|
+
const expected = expectedAddress.toLowerCase();
|
|
2371
|
+
return {
|
|
2372
|
+
valid: recovered.toLowerCase() === expected,
|
|
2373
|
+
recovered
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
// src/settlement/mina.ts
|
|
2378
|
+
var MINA_NETWORK = "mainnet";
|
|
2379
|
+
async function loadMinaSignerClient() {
|
|
2380
|
+
try {
|
|
2381
|
+
const lib = await import("mina-signer");
|
|
2382
|
+
const Ctor = "default" in lib ? lib.default : lib;
|
|
2383
|
+
return new Ctor({ network: MINA_NETWORK });
|
|
2384
|
+
} catch {
|
|
2385
|
+
return void 0;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function verifyMinaSignature(claim, expectedSignerAddress, client) {
|
|
2389
|
+
if (claim.channelId === void 0 || claim.cumulativeAmount === void 0 || claim.nonce === void 0 || claim.recipient === void 0) {
|
|
2390
|
+
throw new SettlementTxError(
|
|
2391
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2392
|
+
"Claim missing channelId/cumulativeAmount/nonce/recipient for Mina signature verify"
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
if (claim.claimBytes.length === 0) {
|
|
2396
|
+
throw new SettlementTxError(
|
|
2397
|
+
"INVALID_SIGNATURE_LENGTH",
|
|
2398
|
+
"Mina claimBytes is empty (expected UTF-8 base58 signature string)"
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
const signature = new TextDecoder().decode(claim.claimBytes);
|
|
2402
|
+
const fields = balanceProofFieldsMina(
|
|
2403
|
+
claim.channelId,
|
|
2404
|
+
BigInt(claim.cumulativeAmount),
|
|
2405
|
+
BigInt(claim.nonce),
|
|
2406
|
+
claim.recipient
|
|
2407
|
+
);
|
|
2408
|
+
try {
|
|
2409
|
+
return client.verifyFields({
|
|
2410
|
+
data: fields,
|
|
2411
|
+
signature,
|
|
2412
|
+
publicKey: expectedSignerAddress
|
|
2413
|
+
});
|
|
2414
|
+
} catch {
|
|
2415
|
+
return false;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
function buildMinaSettlementTx(winner, signer, recipient, selectedClaimIndex, claimsMerged) {
|
|
2419
|
+
if (winner.channelId === void 0 || winner.cumulativeAmount === void 0 || winner.nonce === void 0 || winner.recipient === void 0 || winner.millSignerAddress === void 0) {
|
|
2420
|
+
throw new SettlementTxError(
|
|
2421
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2422
|
+
"Mina winner claim missing settlement-context fields"
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
if (!signer.address) {
|
|
2426
|
+
throw new SettlementTxError(
|
|
2427
|
+
"INVALID_INPUT",
|
|
2428
|
+
`Mina MillSignerConfig.address is required for chain ${winner.pair.to.chain}`
|
|
2429
|
+
);
|
|
2430
|
+
}
|
|
2431
|
+
if (winner.claimBytes.length === 0) {
|
|
2432
|
+
throw new SettlementTxError(
|
|
2433
|
+
"INVALID_SIGNATURE_LENGTH",
|
|
2434
|
+
"Mina winner claimBytes is empty (expected UTF-8 base58 signature string)"
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
const unsignedTxBytes = winner.claimBytes;
|
|
2438
|
+
return {
|
|
2439
|
+
chain: winner.pair.to.chain,
|
|
2440
|
+
chainKind: "mina",
|
|
2441
|
+
channelId: winner.channelId,
|
|
2442
|
+
cumulativeAmount: winner.cumulativeAmount,
|
|
2443
|
+
nonce: winner.nonce,
|
|
2444
|
+
recipient,
|
|
2445
|
+
millSignerAddress: winner.millSignerAddress,
|
|
2446
|
+
unsignedTxBytes,
|
|
2447
|
+
claimsMerged,
|
|
2448
|
+
selectedClaimIndex,
|
|
2449
|
+
sourceChain: winner.pair.from.chain,
|
|
2450
|
+
sourceAssetCode: winner.pair.from.assetCode
|
|
2451
|
+
};
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
// src/settlement/solana.ts
|
|
2455
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
2456
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2457
|
+
function verifyEd25519Signature(claim, expectedSignerAddress) {
|
|
2458
|
+
if (claim.channelId === void 0 || claim.cumulativeAmount === void 0 || claim.nonce === void 0 || claim.recipient === void 0) {
|
|
2459
|
+
throw new SettlementTxError(
|
|
2460
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2461
|
+
"Claim missing channelId/cumulativeAmount/nonce/recipient for Solana signature verify"
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
if (claim.claimBytes.length !== 64) {
|
|
2465
|
+
throw new SettlementTxError(
|
|
2466
|
+
"INVALID_SIGNATURE_LENGTH",
|
|
2467
|
+
`Solana signature must be 64 bytes, got ${claim.claimBytes.length}`
|
|
2468
|
+
);
|
|
2469
|
+
}
|
|
2470
|
+
const msgHash = balanceProofHashSolana(
|
|
2471
|
+
claim.channelId,
|
|
2472
|
+
BigInt(claim.cumulativeAmount),
|
|
2473
|
+
BigInt(claim.nonce),
|
|
2474
|
+
claim.recipient
|
|
2475
|
+
);
|
|
2476
|
+
let pubkeyBytes;
|
|
2477
|
+
try {
|
|
2478
|
+
pubkeyBytes = base58Decode(expectedSignerAddress);
|
|
2479
|
+
} catch (err) {
|
|
2480
|
+
throw new SettlementTxError(
|
|
2481
|
+
"INVALID_INPUT",
|
|
2482
|
+
`Solana expected signer address is not valid base58: ${expectedSignerAddress}`,
|
|
2483
|
+
{ cause: err }
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
2486
|
+
if (pubkeyBytes.length !== 32) {
|
|
2487
|
+
throw new SettlementTxError(
|
|
2488
|
+
"INVALID_INPUT",
|
|
2489
|
+
`Solana expected signer pubkey must be 32 bytes, got ${pubkeyBytes.length}`
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
try {
|
|
2493
|
+
return ed25519.verify(claim.claimBytes, msgHash, pubkeyBytes);
|
|
2494
|
+
} catch {
|
|
2495
|
+
return false;
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
var SOLANA_UPDATE_BALANCE_DISCRIMINATOR = sha256(
|
|
2499
|
+
new TextEncoder().encode("global:update_balance")
|
|
2500
|
+
).slice(0, 8);
|
|
2501
|
+
function bigintToBytes8LE(x) {
|
|
2502
|
+
if (x < 0n) {
|
|
2503
|
+
throw new SettlementTxError(
|
|
2504
|
+
"ENCODING_FAILED",
|
|
2505
|
+
"bigintToBytes8LE: negative input"
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
if (x > 0xffffffffffffffffn) {
|
|
2509
|
+
throw new SettlementTxError(
|
|
2510
|
+
"ENCODING_FAILED",
|
|
2511
|
+
"bigintToBytes8LE: value exceeds 64 bits"
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
const out = new Uint8Array(8);
|
|
2515
|
+
let v = x;
|
|
2516
|
+
for (let i = 0; i < 8; i++) {
|
|
2517
|
+
out[i] = Number(v & 0xffn);
|
|
2518
|
+
v >>= 8n;
|
|
2519
|
+
}
|
|
2520
|
+
return out;
|
|
2521
|
+
}
|
|
2522
|
+
function buildSolanaSettlementTx(winner, signer, recipient, selectedClaimIndex, claimsMerged) {
|
|
2523
|
+
if (winner.channelId === void 0 || winner.cumulativeAmount === void 0 || winner.nonce === void 0 || winner.recipient === void 0 || winner.millSignerAddress === void 0) {
|
|
2524
|
+
throw new SettlementTxError(
|
|
2525
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2526
|
+
"Solana winner claim missing settlement-context fields"
|
|
2527
|
+
);
|
|
2528
|
+
}
|
|
2529
|
+
if (!signer.programId) {
|
|
2530
|
+
throw new SettlementTxError(
|
|
2531
|
+
"INVALID_INPUT",
|
|
2532
|
+
`Solana MillSignerConfig.programId is required for chain ${winner.pair.to.chain}`
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
let programIdBytes;
|
|
2536
|
+
let recipientBytes;
|
|
2537
|
+
let millBytes;
|
|
2538
|
+
let channelIdBytes;
|
|
2539
|
+
try {
|
|
2540
|
+
programIdBytes = base58Decode(signer.programId);
|
|
2541
|
+
recipientBytes = base58Decode(recipient);
|
|
2542
|
+
millBytes = base58Decode(winner.millSignerAddress);
|
|
2543
|
+
channelIdBytes = base58Decode(winner.channelId);
|
|
2544
|
+
} catch (err) {
|
|
2545
|
+
throw new SettlementTxError(
|
|
2546
|
+
"ENCODING_FAILED",
|
|
2547
|
+
`Solana settlement tx: base58 decode failed (${err instanceof Error ? err.message : String(err)})`,
|
|
2548
|
+
{ cause: err }
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
if (programIdBytes.length !== 32) {
|
|
2552
|
+
throw new SettlementTxError(
|
|
2553
|
+
"INVALID_INPUT",
|
|
2554
|
+
`Solana programId must be 32 bytes, got ${programIdBytes.length}`
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
if (recipientBytes.length !== 32) {
|
|
2558
|
+
throw new SettlementTxError(
|
|
2559
|
+
"INVALID_INPUT",
|
|
2560
|
+
`Solana recipient must decode to 32 bytes, got ${recipientBytes.length}`
|
|
2561
|
+
);
|
|
2562
|
+
}
|
|
2563
|
+
if (millBytes.length !== 32) {
|
|
2564
|
+
throw new SettlementTxError(
|
|
2565
|
+
"INVALID_INPUT",
|
|
2566
|
+
`Solana millSignerAddress must decode to 32 bytes, got ${millBytes.length}`
|
|
2567
|
+
);
|
|
2568
|
+
}
|
|
2569
|
+
if (channelIdBytes.length !== 32) {
|
|
2570
|
+
throw new SettlementTxError(
|
|
2571
|
+
"INVALID_INPUT",
|
|
2572
|
+
`Solana channelId must decode to 32 bytes, got ${channelIdBytes.length}`
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
const instructionData = concatBytes(
|
|
2576
|
+
SOLANA_UPDATE_BALANCE_DISCRIMINATOR,
|
|
2577
|
+
bigintToBytes8LE(BigInt(winner.cumulativeAmount)),
|
|
2578
|
+
bigintToBytes8LE(BigInt(winner.nonce)),
|
|
2579
|
+
winner.claimBytes
|
|
2580
|
+
);
|
|
2581
|
+
const accounts = [recipientBytes, millBytes, channelIdBytes, programIdBytes];
|
|
2582
|
+
const header = new Uint8Array([1, 0, 1]);
|
|
2583
|
+
const accountsCountByte = new Uint8Array([accounts.length]);
|
|
2584
|
+
const recentBlockhash = new Uint8Array(32);
|
|
2585
|
+
const instructionsCountByte = new Uint8Array([1]);
|
|
2586
|
+
const programIdIndex = new Uint8Array([3]);
|
|
2587
|
+
const instrAccountsLen = new Uint8Array([3]);
|
|
2588
|
+
const instrAccountIndices = new Uint8Array([0, 1, 2]);
|
|
2589
|
+
if (instructionData.length >= 128) {
|
|
2590
|
+
throw new SettlementTxError(
|
|
2591
|
+
"ENCODING_FAILED",
|
|
2592
|
+
`Solana instruction data too large for simple compact-u16 encoding: ${instructionData.length}`
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
const instrDataLen = new Uint8Array([instructionData.length]);
|
|
2596
|
+
const instruction = concatBytes(
|
|
2597
|
+
programIdIndex,
|
|
2598
|
+
instrAccountsLen,
|
|
2599
|
+
instrAccountIndices,
|
|
2600
|
+
instrDataLen,
|
|
2601
|
+
instructionData
|
|
2602
|
+
);
|
|
2603
|
+
const unsignedTxBytes = concatBytes(
|
|
2604
|
+
header,
|
|
2605
|
+
accountsCountByte,
|
|
2606
|
+
...accounts,
|
|
2607
|
+
recentBlockhash,
|
|
2608
|
+
instructionsCountByte,
|
|
2609
|
+
instruction
|
|
2610
|
+
);
|
|
2611
|
+
return {
|
|
2612
|
+
chain: winner.pair.to.chain,
|
|
2613
|
+
chainKind: "solana",
|
|
2614
|
+
channelId: winner.channelId,
|
|
2615
|
+
cumulativeAmount: winner.cumulativeAmount,
|
|
2616
|
+
nonce: winner.nonce,
|
|
2617
|
+
recipient,
|
|
2618
|
+
millSignerAddress: winner.millSignerAddress,
|
|
2619
|
+
unsignedTxBytes,
|
|
2620
|
+
claimsMerged,
|
|
2621
|
+
selectedClaimIndex,
|
|
2622
|
+
sourceChain: winner.pair.from.chain,
|
|
2623
|
+
sourceAssetCode: winner.pair.from.assetCode
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
|
|
2627
|
+
// src/settlement/build-settlement-tx.ts
|
|
2628
|
+
var EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;
|
|
2629
|
+
function chainKindOf(chain) {
|
|
2630
|
+
if (chain.startsWith("evm:")) return "evm";
|
|
2631
|
+
if (chain.startsWith("solana:")) return "solana";
|
|
2632
|
+
if (chain.startsWith("mina:")) return "mina";
|
|
2633
|
+
return "unknown";
|
|
2634
|
+
}
|
|
2635
|
+
function buildSettlementTx(params) {
|
|
2636
|
+
if (!Array.isArray(params.claims) || params.claims.length === 0) {
|
|
2637
|
+
throw new SettlementTxError(
|
|
2638
|
+
"INVALID_INPUT",
|
|
2639
|
+
"claims array is empty or not an array"
|
|
2640
|
+
);
|
|
2641
|
+
}
|
|
2642
|
+
if (!params.signers || typeof params.signers !== "object") {
|
|
2643
|
+
throw new SettlementTxError("INVALID_INPUT", "signers map is required");
|
|
2644
|
+
}
|
|
2645
|
+
if (!params.recipients || typeof params.recipients !== "object") {
|
|
2646
|
+
throw new SettlementTxError("INVALID_INPUT", "recipients map is required");
|
|
2647
|
+
}
|
|
2648
|
+
const logger = params.logger;
|
|
2649
|
+
const verifySignatures = params.verifySignatures ?? true;
|
|
2650
|
+
for (let i = 0; i < params.claims.length; i++) {
|
|
2651
|
+
const c = params.claims[i];
|
|
2652
|
+
if (c === void 0) continue;
|
|
2653
|
+
if (c.channelId === void 0 || c.nonce === void 0 || c.cumulativeAmount === void 0 || c.recipient === void 0 || c.millSignerAddress === void 0) {
|
|
2654
|
+
throw new SettlementTxError(
|
|
2655
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2656
|
+
`claims[${i}] missing one or more of { channelId, nonce, cumulativeAmount, recipient, millSignerAddress }`
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
const distinctChains = /* @__PURE__ */ new Set();
|
|
2661
|
+
for (const c of params.claims) distinctChains.add(c.pair.to.chain);
|
|
2662
|
+
for (const chain of distinctChains) {
|
|
2663
|
+
const signer = params.signers[chain];
|
|
2664
|
+
if (!signer) {
|
|
2665
|
+
throw new SettlementTxError(
|
|
2666
|
+
"UNSUPPORTED_CHAIN",
|
|
2667
|
+
`signers map missing entry for chain ${chain}`
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
if (!(chain in params.recipients)) {
|
|
2671
|
+
throw new SettlementTxError(
|
|
2672
|
+
"MISSING_RECIPIENT",
|
|
2673
|
+
`recipients map missing entry for chain ${chain}`
|
|
2674
|
+
);
|
|
2675
|
+
}
|
|
2676
|
+
const kind = chainKindOf(chain);
|
|
2677
|
+
if (kind === "evm") {
|
|
2678
|
+
if (!signer.contractAddress || !EVM_ADDRESS_REGEX.test(signer.contractAddress)) {
|
|
2679
|
+
throw new SettlementTxError(
|
|
2680
|
+
"INVALID_INPUT",
|
|
2681
|
+
`EVM signers[${chain}].contractAddress must match 0x + 40 lowercase hex`
|
|
2682
|
+
);
|
|
2683
|
+
}
|
|
2684
|
+
if (typeof signer.chainId !== "number" || !Number.isInteger(signer.chainId) || signer.chainId <= 0) {
|
|
2685
|
+
throw new SettlementTxError(
|
|
2686
|
+
"INVALID_INPUT",
|
|
2687
|
+
`EVM signers[${chain}].chainId must be a positive integer`
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
} else if (kind === "solana") {
|
|
2691
|
+
if (!signer.programId || signer.programId.length === 0) {
|
|
2692
|
+
throw new SettlementTxError(
|
|
2693
|
+
"INVALID_INPUT",
|
|
2694
|
+
`Solana signers[${chain}].programId is required`
|
|
2695
|
+
);
|
|
2696
|
+
}
|
|
2697
|
+
let programIdLen;
|
|
2698
|
+
try {
|
|
2699
|
+
programIdLen = base58Decode(signer.programId).length;
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
throw new SettlementTxError(
|
|
2702
|
+
"INVALID_INPUT",
|
|
2703
|
+
`Solana signers[${chain}].programId is not valid base58`,
|
|
2704
|
+
{ cause: err }
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
if (programIdLen !== 32) {
|
|
2708
|
+
throw new SettlementTxError(
|
|
2709
|
+
"INVALID_INPUT",
|
|
2710
|
+
`Solana signers[${chain}].programId must decode to 32 bytes (got ${programIdLen})`
|
|
2711
|
+
);
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
const rejected = [];
|
|
2716
|
+
const survivors = [];
|
|
2717
|
+
for (const claim of params.claims) {
|
|
2718
|
+
if (!verifySignatures) {
|
|
2719
|
+
survivors.push(claim);
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
const chain = claim.pair.to.chain;
|
|
2723
|
+
const signer = params.signers[chain];
|
|
2724
|
+
if (!signer) {
|
|
2725
|
+
throw new SettlementTxError(
|
|
2726
|
+
"UNSUPPORTED_CHAIN",
|
|
2727
|
+
`signers map missing entry for chain ${chain} (unreachable \u2014 validated)`
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
const kind = chainKindOf(chain);
|
|
2731
|
+
if (kind === "evm") {
|
|
2732
|
+
try {
|
|
2733
|
+
const { valid, recovered } = verifyEvmClaimSignature(
|
|
2734
|
+
claim,
|
|
2735
|
+
signer.address
|
|
2736
|
+
);
|
|
2737
|
+
if (!valid) {
|
|
2738
|
+
rejected.push({
|
|
2739
|
+
claim,
|
|
2740
|
+
reason: "SIGNER_MISMATCH",
|
|
2741
|
+
details: `recovered=${recovered} expected=${signer.address.toLowerCase()}`
|
|
2742
|
+
});
|
|
2743
|
+
continue;
|
|
2744
|
+
}
|
|
2745
|
+
survivors.push(claim);
|
|
2746
|
+
} catch (err) {
|
|
2747
|
+
rejected.push({
|
|
2748
|
+
claim,
|
|
2749
|
+
reason: "SIGNATURE_INVALID",
|
|
2750
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2751
|
+
});
|
|
2752
|
+
}
|
|
2753
|
+
} else if (kind === "solana") {
|
|
2754
|
+
try {
|
|
2755
|
+
const ok = verifyEd25519Signature(claim, signer.address);
|
|
2756
|
+
if (!ok) {
|
|
2757
|
+
rejected.push({
|
|
2758
|
+
claim,
|
|
2759
|
+
reason: "SIGNER_MISMATCH",
|
|
2760
|
+
details: `ed25519.verify returned false against ${signer.address}`
|
|
2761
|
+
});
|
|
2762
|
+
continue;
|
|
2763
|
+
}
|
|
2764
|
+
survivors.push(claim);
|
|
2765
|
+
} catch (err) {
|
|
2766
|
+
rejected.push({
|
|
2767
|
+
claim,
|
|
2768
|
+
reason: "SIGNATURE_INVALID",
|
|
2769
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
} else if (kind === "mina") {
|
|
2773
|
+
if (!params.minaSignerClient) {
|
|
2774
|
+
rejected.push({
|
|
2775
|
+
claim,
|
|
2776
|
+
reason: "MINA_VERIFICATION_UNSUPPORTED",
|
|
2777
|
+
details: "minaSignerClient not provided \u2014 load mina-signer via loadMinaSignerClient() and pass it in params.minaSignerClient to verify mina:* claims"
|
|
2778
|
+
});
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
try {
|
|
2782
|
+
const ok = verifyMinaSignature(
|
|
2783
|
+
claim,
|
|
2784
|
+
signer.address,
|
|
2785
|
+
params.minaSignerClient
|
|
2786
|
+
);
|
|
2787
|
+
if (!ok) {
|
|
2788
|
+
rejected.push({
|
|
2789
|
+
claim,
|
|
2790
|
+
reason: "SIGNER_MISMATCH",
|
|
2791
|
+
details: `mina-signer verifyFields returned false against ${signer.address}`
|
|
2792
|
+
});
|
|
2793
|
+
continue;
|
|
2794
|
+
}
|
|
2795
|
+
survivors.push(claim);
|
|
2796
|
+
} catch (err) {
|
|
2797
|
+
rejected.push({
|
|
2798
|
+
claim,
|
|
2799
|
+
reason: "SIGNATURE_INVALID",
|
|
2800
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2801
|
+
});
|
|
2802
|
+
}
|
|
2803
|
+
} else {
|
|
2804
|
+
throw new SettlementTxError(
|
|
2805
|
+
"UNSUPPORTED_CHAIN",
|
|
2806
|
+
`Unknown chain kind for ${chain}`
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
logger?.debug?.({
|
|
2811
|
+
event: "build_settlement_tx.verified",
|
|
2812
|
+
survivorCount: survivors.length,
|
|
2813
|
+
rejectedCount: rejected.length
|
|
2814
|
+
});
|
|
2815
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2816
|
+
const originalIndex = /* @__PURE__ */ new Map();
|
|
2817
|
+
for (let i = 0; i < params.claims.length; i++) {
|
|
2818
|
+
const c = params.claims[i];
|
|
2819
|
+
if (c !== void 0) originalIndex.set(c, i);
|
|
2820
|
+
}
|
|
2821
|
+
for (const claim of survivors) {
|
|
2822
|
+
const chain = claim.pair.to.chain;
|
|
2823
|
+
const channelId = claim.channelId;
|
|
2824
|
+
if (channelId === void 0) {
|
|
2825
|
+
throw new SettlementTxError(
|
|
2826
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2827
|
+
"claim.channelId undefined after validation (unreachable)"
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
const key = `${chain}::${channelId}`;
|
|
2831
|
+
let g = groups.get(key);
|
|
2832
|
+
if (!g) {
|
|
2833
|
+
g = { chain, channelId, claims: [] };
|
|
2834
|
+
groups.set(key, g);
|
|
2835
|
+
}
|
|
2836
|
+
const idx = originalIndex.get(claim);
|
|
2837
|
+
g.claims.push({ claim, originalIndex: idx ?? -1 });
|
|
2838
|
+
}
|
|
2839
|
+
const superseded = [];
|
|
2840
|
+
const bundles = [];
|
|
2841
|
+
for (const g of groups.values()) {
|
|
2842
|
+
if (g.claims.length === 0) continue;
|
|
2843
|
+
const first = g.claims[0];
|
|
2844
|
+
if (!first) continue;
|
|
2845
|
+
const firstRecipient = first.claim.recipient;
|
|
2846
|
+
const firstMillSigner = first.claim.millSignerAddress;
|
|
2847
|
+
if (firstRecipient === void 0 || firstMillSigner === void 0) {
|
|
2848
|
+
throw new SettlementTxError(
|
|
2849
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2850
|
+
"winner claim missing recipient/millSignerAddress (unreachable after validation)"
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
for (let i = 1; i < g.claims.length; i++) {
|
|
2854
|
+
const entry = g.claims[i];
|
|
2855
|
+
if (!entry) continue;
|
|
2856
|
+
const c = entry.claim;
|
|
2857
|
+
if (c.recipient !== firstRecipient) {
|
|
2858
|
+
throw new SettlementTxError(
|
|
2859
|
+
"RECIPIENT_MISMATCH",
|
|
2860
|
+
`claims in channel ${g.channelId} disagree on recipient: ${firstRecipient} vs ${String(c.recipient)} (claim indices ${first.originalIndex}, ${entry.originalIndex})`
|
|
2861
|
+
);
|
|
2862
|
+
}
|
|
2863
|
+
if (c.millSignerAddress !== firstMillSigner) {
|
|
2864
|
+
throw new SettlementTxError(
|
|
2865
|
+
"MILL_SIGNER_MISMATCH",
|
|
2866
|
+
`claims in channel ${g.channelId} disagree on millSignerAddress: ${firstMillSigner} vs ${String(c.millSignerAddress)}`
|
|
2867
|
+
);
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
const nonceSeen = /* @__PURE__ */ new Set();
|
|
2871
|
+
for (const entry of g.claims) {
|
|
2872
|
+
const nonceStr = entry.claim.nonce;
|
|
2873
|
+
if (nonceStr === void 0) {
|
|
2874
|
+
throw new SettlementTxError(
|
|
2875
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2876
|
+
"claim.nonce undefined (unreachable after validation)"
|
|
2877
|
+
);
|
|
2878
|
+
}
|
|
2879
|
+
if (nonceSeen.has(nonceStr)) {
|
|
2880
|
+
throw new SettlementTxError(
|
|
2881
|
+
"DUPLICATE_NONCE",
|
|
2882
|
+
`channel ${g.channelId} has two claims with nonce ${nonceStr}`
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
nonceSeen.add(nonceStr);
|
|
2886
|
+
}
|
|
2887
|
+
const sorted = [...g.claims].sort((a, b) => {
|
|
2888
|
+
const an = BigInt(a.claim.nonce ?? "0");
|
|
2889
|
+
const bn = BigInt(b.claim.nonce ?? "0");
|
|
2890
|
+
return an < bn ? -1 : an > bn ? 1 : 0;
|
|
2891
|
+
});
|
|
2892
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
2893
|
+
const prevE = sorted[i - 1];
|
|
2894
|
+
const currE = sorted[i];
|
|
2895
|
+
if (!prevE || !currE) continue;
|
|
2896
|
+
const prev = BigInt(prevE.claim.cumulativeAmount ?? "0");
|
|
2897
|
+
const curr = BigInt(currE.claim.cumulativeAmount ?? "0");
|
|
2898
|
+
if (curr < prev) {
|
|
2899
|
+
throw new SettlementTxError(
|
|
2900
|
+
"NON_MONOTONIC_CUMULATIVE",
|
|
2901
|
+
`channel ${g.channelId} nonce ${String(currE.claim.nonce)} has cumulativeAmount ${curr} < previous nonce ${String(prevE.claim.nonce)} cumulativeAmount ${prev}`
|
|
2902
|
+
);
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
const winnerEntry = sorted[sorted.length - 1];
|
|
2906
|
+
if (!winnerEntry) continue;
|
|
2907
|
+
const winner = winnerEntry.claim;
|
|
2908
|
+
if (params.includeSuperseded) {
|
|
2909
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
2910
|
+
const e = sorted[i];
|
|
2911
|
+
if (e) superseded.push(e.claim);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
const signer = params.signers[g.chain];
|
|
2915
|
+
const recipient = params.recipients[g.chain];
|
|
2916
|
+
if (!signer || recipient === void 0) {
|
|
2917
|
+
throw new SettlementTxError(
|
|
2918
|
+
"UNSUPPORTED_CHAIN",
|
|
2919
|
+
`signers/recipients missing entry for ${g.chain} (unreachable after validation)`
|
|
2920
|
+
);
|
|
2921
|
+
}
|
|
2922
|
+
const kind = chainKindOf(g.chain);
|
|
2923
|
+
let bundle;
|
|
2924
|
+
if (kind === "evm") {
|
|
2925
|
+
if (recipient.toLowerCase() !== firstRecipient.toLowerCase()) {
|
|
2926
|
+
throw new SettlementTxError(
|
|
2927
|
+
"RECIPIENT_MISMATCH",
|
|
2928
|
+
`recipients[${g.chain}] (${recipient}) does not match Mill-reported recipient (${firstRecipient})`
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
bundle = buildEvmSettlementTx(
|
|
2932
|
+
winner,
|
|
2933
|
+
signer,
|
|
2934
|
+
recipient.toLowerCase(),
|
|
2935
|
+
winnerEntry.originalIndex,
|
|
2936
|
+
g.claims.length
|
|
2937
|
+
);
|
|
2938
|
+
} else if (kind === "solana") {
|
|
2939
|
+
if (recipient !== firstRecipient) {
|
|
2940
|
+
throw new SettlementTxError(
|
|
2941
|
+
"RECIPIENT_MISMATCH",
|
|
2942
|
+
`recipients[${g.chain}] (${recipient}) does not match Mill-reported recipient (${firstRecipient})`
|
|
2943
|
+
);
|
|
2944
|
+
}
|
|
2945
|
+
bundle = buildSolanaSettlementTx(
|
|
2946
|
+
winner,
|
|
2947
|
+
signer,
|
|
2948
|
+
recipient,
|
|
2949
|
+
winnerEntry.originalIndex,
|
|
2950
|
+
g.claims.length
|
|
2951
|
+
);
|
|
2952
|
+
} else if (kind === "mina") {
|
|
2953
|
+
if (recipient !== firstRecipient) {
|
|
2954
|
+
throw new SettlementTxError(
|
|
2955
|
+
"RECIPIENT_MISMATCH",
|
|
2956
|
+
`recipients[${g.chain}] (${recipient}) does not match Mill-reported recipient (${firstRecipient})`
|
|
2957
|
+
);
|
|
2958
|
+
}
|
|
2959
|
+
bundle = buildMinaSettlementTx(
|
|
2960
|
+
winner,
|
|
2961
|
+
signer,
|
|
2962
|
+
recipient,
|
|
2963
|
+
winnerEntry.originalIndex,
|
|
2964
|
+
g.claims.length
|
|
2965
|
+
);
|
|
2966
|
+
} else {
|
|
2967
|
+
throw new SettlementTxError(
|
|
2968
|
+
"UNSUPPORTED_CHAIN",
|
|
2969
|
+
`Unknown chain kind for ${g.chain}`
|
|
2970
|
+
);
|
|
2971
|
+
}
|
|
2972
|
+
bundles.push(bundle);
|
|
2973
|
+
}
|
|
2974
|
+
logger?.info?.({
|
|
2975
|
+
event: "build_settlement_tx.complete",
|
|
2976
|
+
bundleCount: bundles.length,
|
|
2977
|
+
rejectedCount: rejected.length,
|
|
2978
|
+
supersededCount: superseded.length
|
|
2979
|
+
});
|
|
2980
|
+
return { bundles, rejected, superseded };
|
|
2981
|
+
}
|
|
2982
|
+
function verifyAccumulatedClaim(claim, signer, minaSignerClient) {
|
|
2983
|
+
const kind = chainKindOf(claim.pair.to.chain);
|
|
2984
|
+
if (kind === "evm") {
|
|
2985
|
+
try {
|
|
2986
|
+
const { valid, recovered } = verifyEvmClaimSignature(
|
|
2987
|
+
claim,
|
|
2988
|
+
signer.address
|
|
2989
|
+
);
|
|
2990
|
+
if (valid) return { valid: true };
|
|
2991
|
+
return {
|
|
2992
|
+
valid: false,
|
|
2993
|
+
reason: `SIGNER_MISMATCH: recovered=${recovered} expected=${signer.address.toLowerCase()}`
|
|
2994
|
+
};
|
|
2995
|
+
} catch (err) {
|
|
2996
|
+
return {
|
|
2997
|
+
valid: false,
|
|
2998
|
+
reason: `SIGNATURE_INVALID: ${err instanceof Error ? err.message : String(err)}`
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
if (kind === "solana") {
|
|
3003
|
+
try {
|
|
3004
|
+
const ok = verifyEd25519Signature(claim, signer.address);
|
|
3005
|
+
return ok ? { valid: true } : {
|
|
3006
|
+
valid: false,
|
|
3007
|
+
reason: "SIGNER_MISMATCH: ed25519.verify returned false"
|
|
3008
|
+
};
|
|
3009
|
+
} catch (err) {
|
|
3010
|
+
return {
|
|
3011
|
+
valid: false,
|
|
3012
|
+
reason: `SIGNATURE_INVALID: ${err instanceof Error ? err.message : String(err)}`
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
if (kind === "mina") {
|
|
3017
|
+
if (!minaSignerClient) {
|
|
3018
|
+
return {
|
|
3019
|
+
valid: false,
|
|
3020
|
+
reason: "MINA_VERIFICATION_UNSUPPORTED: pass a mina-signer Client (loadMinaSignerClient()) as minaSignerClient to verify mina:* claims"
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
try {
|
|
3024
|
+
const ok = verifyMinaSignature(claim, signer.address, minaSignerClient);
|
|
3025
|
+
return ok ? { valid: true } : {
|
|
3026
|
+
valid: false,
|
|
3027
|
+
reason: "SIGNER_MISMATCH: mina-signer verifyFields returned false"
|
|
3028
|
+
};
|
|
3029
|
+
} catch (err) {
|
|
3030
|
+
return {
|
|
3031
|
+
valid: false,
|
|
3032
|
+
reason: `SIGNATURE_INVALID: ${err instanceof Error ? err.message : String(err)}`
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return {
|
|
3037
|
+
valid: false,
|
|
3038
|
+
reason: `UNSUPPORTED_CHAIN: ${claim.pair.to.chain}`
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
2186
3041
|
export {
|
|
2187
3042
|
ChunkManager,
|
|
3043
|
+
GiftWrapError,
|
|
2188
3044
|
HandlerError,
|
|
2189
3045
|
HandlerRegistry,
|
|
2190
3046
|
IdentityError,
|
|
2191
3047
|
NodeError,
|
|
2192
3048
|
PricingError,
|
|
3049
|
+
SWAP_HANDLER_REJECT_CODES,
|
|
3050
|
+
SWAP_HANDLER_REJECT_MESSAGES,
|
|
3051
|
+
SettlementTxError,
|
|
3052
|
+
StreamSwapError,
|
|
3053
|
+
SwapHandlerError,
|
|
2193
3054
|
SwarmCoordinator,
|
|
2194
3055
|
TurboUploadAdapter,
|
|
2195
3056
|
VerificationError,
|
|
2196
3057
|
WorkflowOrchestrator,
|
|
3058
|
+
__testing as __streamSwapTesting,
|
|
3059
|
+
applyRate,
|
|
3060
|
+
balanceProofFieldsMina,
|
|
3061
|
+
balanceProofHashEvm,
|
|
3062
|
+
balanceProofHashSolana,
|
|
2197
3063
|
base58Decode,
|
|
2198
3064
|
base58Encode,
|
|
3065
|
+
bigintToBytes32BE,
|
|
3066
|
+
buildSettlementTx,
|
|
2199
3067
|
buildSkillDescriptor,
|
|
3068
|
+
concatBytes,
|
|
2200
3069
|
createArweaveDvmHandler,
|
|
2201
3070
|
createEventStorageHandler,
|
|
2202
3071
|
createHandlerContext,
|
|
@@ -2204,13 +3073,30 @@ export {
|
|
|
2204
3073
|
createPaymentHandlerBridge,
|
|
2205
3074
|
createPrefixClaimHandler,
|
|
2206
3075
|
createPricingValidator,
|
|
3076
|
+
createSwapHandler,
|
|
2207
3077
|
createVerificationPipeline,
|
|
3078
|
+
decryptFulfillClaim,
|
|
3079
|
+
encryptFulfillClaim,
|
|
3080
|
+
fillEvmSettlementTxGas,
|
|
3081
|
+
findSwapPair,
|
|
2208
3082
|
fromMnemonic,
|
|
2209
3083
|
fromMnemonicFull,
|
|
2210
3084
|
fromSecretKey,
|
|
2211
3085
|
generateMnemonic,
|
|
2212
3086
|
generateSolanaKeypair,
|
|
3087
|
+
hexToBytes2 as hexToBytes,
|
|
3088
|
+
loadMinaSignerClient,
|
|
3089
|
+
minaHashToField,
|
|
3090
|
+
streamSwap,
|
|
3091
|
+
streamSwapControlled,
|
|
3092
|
+
unwrapSwapPacket,
|
|
3093
|
+
unwrapSwapPacketFromToon,
|
|
2213
3094
|
uploadBlob,
|
|
2214
|
-
uploadBlobChunked
|
|
3095
|
+
uploadBlobChunked,
|
|
3096
|
+
verifyAccumulatedClaim,
|
|
3097
|
+
verifyEd25519Signature,
|
|
3098
|
+
verifyMinaSignature,
|
|
3099
|
+
wrapSwapPacket,
|
|
3100
|
+
wrapSwapPacketToToon
|
|
2215
3101
|
};
|
|
2216
3102
|
//# sourceMappingURL=index.js.map
|