@toon-protocol/sdk 0.4.0 → 0.5.1
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 +747 -9
- package/dist/index.js +1189 -180
- 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 +27 -18
package/dist/index.js
CHANGED
|
@@ -1,145 +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
|
-
constructor(message, cause) {
|
|
38
|
-
super(message, "VERIFICATION_ERROR", cause);
|
|
39
|
-
this.name = "VerificationError";
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
var PricingError = class extends ToonError {
|
|
43
|
-
constructor(message, cause) {
|
|
44
|
-
super(message, "PRICING_ERROR", cause);
|
|
45
|
-
this.name = "PricingError";
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// src/identity.ts
|
|
50
|
-
function generateMnemonic() {
|
|
51
|
-
return _generateMnemonic(wordlist, 128);
|
|
52
|
-
}
|
|
53
|
-
function fromMnemonic(mnemonic, options) {
|
|
54
|
-
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
55
|
-
throw new IdentityError(
|
|
56
|
-
`Invalid BIP-39 mnemonic: the provided words do not form a valid mnemonic phrase`
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
const accountIndex = options?.accountIndex ?? 0;
|
|
60
|
-
if (!Number.isInteger(accountIndex) || accountIndex < 0 || accountIndex > MAX_BIP32_INDEX) {
|
|
61
|
-
throw new IdentityError(
|
|
62
|
-
`Invalid accountIndex: expected a non-negative integer (0 to ${MAX_BIP32_INDEX}), got ${String(accountIndex)}`
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
const path = `m/44'/1237'/0'/0/${accountIndex}`;
|
|
66
|
-
let seed;
|
|
67
|
-
try {
|
|
68
|
-
seed = mnemonicToSeedSync(mnemonic);
|
|
69
|
-
const hdKey = HDKey.fromMasterSeed(seed).derive(path);
|
|
70
|
-
if (!hdKey.privateKey) {
|
|
71
|
-
throw new IdentityError(`Failed to derive private key at path ${path}`);
|
|
72
|
-
}
|
|
73
|
-
const secretKey = hdKey.privateKey;
|
|
74
|
-
return deriveIdentity(secretKey);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
if (error instanceof IdentityError) {
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
throw new IdentityError(
|
|
80
|
-
`Key derivation failed at path ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
81
|
-
error instanceof Error ? error : void 0
|
|
82
|
-
);
|
|
83
|
-
} finally {
|
|
84
|
-
if (seed) {
|
|
85
|
-
seed.fill(0);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
function fromSecretKey(secretKey) {
|
|
90
|
-
if (!(secretKey instanceof Uint8Array)) {
|
|
91
|
-
throw new IdentityError(
|
|
92
|
-
`Invalid secret key: expected Uint8Array, got ${secretKey === null ? "null" : typeof secretKey}`
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
if (secretKey.length !== 32) {
|
|
96
|
-
throw new IdentityError(
|
|
97
|
-
`Invalid secret key: expected 32 bytes, got ${secretKey.length} bytes`
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
try {
|
|
101
|
-
return deriveIdentity(secretKey);
|
|
102
|
-
} catch (error) {
|
|
103
|
-
if (error instanceof IdentityError) {
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
throw new IdentityError(
|
|
107
|
-
`Invalid secret key: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
|
-
error instanceof Error ? error : void 0
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
var MAX_BIP32_INDEX = 2147483647;
|
|
113
|
-
function deriveIdentity(secretKey) {
|
|
114
|
-
const pubkey = getPublicKey(secretKey);
|
|
115
|
-
const evmAddress = computeEvmAddress(secretKey);
|
|
116
|
-
return { secretKey: new Uint8Array(secretKey), pubkey, evmAddress };
|
|
117
|
-
}
|
|
118
|
-
function computeEvmAddress(secretKey) {
|
|
119
|
-
const uncompressedPubkey = secp256k1.getPublicKey(secretKey, false);
|
|
120
|
-
const pubkeyWithoutPrefix = uncompressedPubkey.slice(1);
|
|
121
|
-
const hash = keccak_256(pubkeyWithoutPrefix);
|
|
122
|
-
const addressBytes = hash.slice(-20);
|
|
123
|
-
const addressHex = bytesToHex(addressBytes);
|
|
124
|
-
return toChecksumAddress(addressHex);
|
|
125
|
-
}
|
|
126
|
-
function toChecksumAddress(addressHex) {
|
|
127
|
-
const lower = addressHex.toLowerCase();
|
|
128
|
-
const hash = bytesToHex(keccak_256(new TextEncoder().encode(lower)));
|
|
129
|
-
let checksummed = "0x";
|
|
130
|
-
for (let i = 0; i < 40; i++) {
|
|
131
|
-
const char = lower[i];
|
|
132
|
-
const hashChar = hash[i];
|
|
133
|
-
if (char === void 0 || hashChar === void 0) {
|
|
134
|
-
throw new IdentityError(
|
|
135
|
-
`Unexpected undefined at index ${i} during checksum computation`
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
const hashNibble = parseInt(hashChar, 16);
|
|
139
|
-
checksummed += hashNibble >= 8 ? char.toUpperCase() : char;
|
|
140
|
-
}
|
|
141
|
-
return checksummed;
|
|
142
|
-
}
|
|
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";
|
|
143
34
|
|
|
144
35
|
// src/handler-context.ts
|
|
145
36
|
function createHandlerContext(options) {
|
|
@@ -208,6 +99,13 @@ var HandlerRegistry = class {
|
|
|
208
99
|
getRegisteredKinds() {
|
|
209
100
|
return [...this.handlers.keys()].sort((a, b) => a - b);
|
|
210
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
|
+
}
|
|
211
109
|
/**
|
|
212
110
|
* Returns registered kinds in the DVM request range (5000-5999), sorted ascending.
|
|
213
111
|
* Uses JOB_REQUEST_KIND_BASE (5000) as the range start.
|
|
@@ -387,7 +285,8 @@ import {
|
|
|
387
285
|
calculateRouteAmount,
|
|
388
286
|
resolveRouteFees,
|
|
389
287
|
buildPrefixClaimEvent,
|
|
390
|
-
validatePrefix
|
|
288
|
+
validatePrefix,
|
|
289
|
+
ilpCodeToSemantic
|
|
391
290
|
} from "@toon-protocol/core";
|
|
392
291
|
import {
|
|
393
292
|
shallowParseToon,
|
|
@@ -396,7 +295,7 @@ import {
|
|
|
396
295
|
} from "@toon-protocol/core/toon";
|
|
397
296
|
|
|
398
297
|
// src/skill-descriptor.ts
|
|
399
|
-
import { ToonError
|
|
298
|
+
import { ToonError } from "@toon-protocol/core";
|
|
400
299
|
var HEX_64_REGEX = /^[0-9a-f]{64}$/;
|
|
401
300
|
function buildSkillDescriptor(registry, config = {}) {
|
|
402
301
|
const dvmKinds = registry.getDvmKinds();
|
|
@@ -405,7 +304,7 @@ function buildSkillDescriptor(registry, config = {}) {
|
|
|
405
304
|
}
|
|
406
305
|
if (config.attestation !== void 0) {
|
|
407
306
|
if (!HEX_64_REGEX.test(config.attestation.eventId)) {
|
|
408
|
-
throw new
|
|
307
|
+
throw new ToonError(
|
|
409
308
|
"Skill descriptor attestation eventId must be a 64-character lowercase hex string",
|
|
410
309
|
"DVM_SKILL_INVALID_ATTESTATION_EVENT_ID"
|
|
411
310
|
);
|
|
@@ -596,6 +495,27 @@ function createNode(config) {
|
|
|
596
495
|
trackerRef.current.processEvent(decoded);
|
|
597
496
|
}
|
|
598
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
|
+
}
|
|
599
519
|
return result;
|
|
600
520
|
} catch (err) {
|
|
601
521
|
const errMsg = err instanceof Error ? err.message : "Unknown error";
|
|
@@ -736,32 +656,36 @@ function createNode(config) {
|
|
|
736
656
|
}
|
|
737
657
|
const hasSettlementAddresses = chainConfig.registryAddress && chainConfig.tokenNetworkAddress;
|
|
738
658
|
const hasChainProviders = config.chainProviders !== void 0 && config.chainProviders.length > 0;
|
|
739
|
-
|
|
659
|
+
const effectiveChainProviders = hasChainProviders ? config.chainProviders : hasSettlementAddresses ? [
|
|
740
660
|
{
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
661
|
+
chainType: "evm",
|
|
662
|
+
chainId: `evm:${chainConfig.chainId ?? "31337"}`,
|
|
663
|
+
rpcUrl: chainConfig.rpcUrl,
|
|
664
|
+
registryAddress: chainConfig.registryAddress,
|
|
665
|
+
tokenAddress: chainConfig.usdcAddress,
|
|
666
|
+
privateKey: settlementPrivateKey
|
|
667
|
+
}
|
|
668
|
+
] : void 0;
|
|
669
|
+
const connectorConfig = {
|
|
670
|
+
nodeId,
|
|
671
|
+
btpServerPort,
|
|
672
|
+
environment: "development",
|
|
673
|
+
deploymentMode: "embedded",
|
|
674
|
+
peers: [],
|
|
675
|
+
routes: [],
|
|
676
|
+
localDelivery: { enabled: false }
|
|
677
|
+
};
|
|
678
|
+
if (effectiveChainProviders) {
|
|
679
|
+
connectorConfig.chainProviders = effectiveChainProviders;
|
|
680
|
+
}
|
|
681
|
+
if (config.transport) {
|
|
682
|
+
connectorConfig.transport = config.transport;
|
|
683
|
+
}
|
|
684
|
+
if (config.nip59) {
|
|
685
|
+
connectorConfig.nip59 = config.nip59;
|
|
686
|
+
}
|
|
687
|
+
autoCreatedConnector = new ConnectorNodeClass(
|
|
688
|
+
connectorConfig,
|
|
765
689
|
connectorLogger
|
|
766
690
|
);
|
|
767
691
|
await autoCreatedConnector.start();
|
|
@@ -1506,7 +1430,7 @@ var WorkflowOrchestrator = class {
|
|
|
1506
1430
|
|
|
1507
1431
|
// src/swarm-coordinator.ts
|
|
1508
1432
|
import {
|
|
1509
|
-
ToonError as
|
|
1433
|
+
ToonError as ToonError2,
|
|
1510
1434
|
parseSwarmRequest,
|
|
1511
1435
|
parseSwarmSelection
|
|
1512
1436
|
} from "@toon-protocol/core";
|
|
@@ -1557,7 +1481,7 @@ var SwarmCoordinator = class {
|
|
|
1557
1481
|
async startSwarm(swarmRequest) {
|
|
1558
1482
|
const parsed = parseSwarmRequest(swarmRequest);
|
|
1559
1483
|
if (!parsed) {
|
|
1560
|
-
throw new
|
|
1484
|
+
throw new ToonError2(
|
|
1561
1485
|
"Invalid swarm request event: missing swarm tag or invalid Kind 5xxx",
|
|
1562
1486
|
"DVM_INVALID_KIND"
|
|
1563
1487
|
);
|
|
@@ -1616,32 +1540,32 @@ var SwarmCoordinator = class {
|
|
|
1616
1540
|
*/
|
|
1617
1541
|
async selectWinner(selectionEvent) {
|
|
1618
1542
|
if (this.state === "settled") {
|
|
1619
|
-
throw new
|
|
1543
|
+
throw new ToonError2(
|
|
1620
1544
|
"Swarm has already been settled; duplicate selection rejected",
|
|
1621
1545
|
"DVM_SWARM_ALREADY_SETTLED"
|
|
1622
1546
|
);
|
|
1623
1547
|
}
|
|
1624
1548
|
if (this.state !== "judging") {
|
|
1625
|
-
throw new
|
|
1549
|
+
throw new ToonError2(
|
|
1626
1550
|
`Cannot select winner in state '${this.state}'; swarm must be in 'judging' state`,
|
|
1627
1551
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1628
1552
|
);
|
|
1629
1553
|
}
|
|
1630
1554
|
if (selectionEvent.pubkey !== this.customerPubkey) {
|
|
1631
|
-
throw new
|
|
1555
|
+
throw new ToonError2(
|
|
1632
1556
|
"Selection event pubkey does not match the swarm request customer pubkey",
|
|
1633
1557
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1634
1558
|
);
|
|
1635
1559
|
}
|
|
1636
1560
|
const parsed = parseSwarmSelection(selectionEvent);
|
|
1637
1561
|
if (!parsed) {
|
|
1638
|
-
throw new
|
|
1562
|
+
throw new ToonError2(
|
|
1639
1563
|
"Invalid swarm selection event: missing winner tag or invalid Kind 7000",
|
|
1640
1564
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1641
1565
|
);
|
|
1642
1566
|
}
|
|
1643
1567
|
if (parsed.swarmRequestEventId !== this.swarmRequestId) {
|
|
1644
|
-
throw new
|
|
1568
|
+
throw new ToonError2(
|
|
1645
1569
|
`Selection event references swarm '${parsed.swarmRequestEventId}' but this swarm is '${this.swarmRequestId}'`,
|
|
1646
1570
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1647
1571
|
);
|
|
@@ -1650,7 +1574,7 @@ var SwarmCoordinator = class {
|
|
|
1650
1574
|
(s) => s.id === parsed.winnerResultEventId
|
|
1651
1575
|
);
|
|
1652
1576
|
if (!winnerSubmission) {
|
|
1653
|
-
throw new
|
|
1577
|
+
throw new ToonError2(
|
|
1654
1578
|
`Winner result event ID '${parsed.winnerResultEventId}' not found in collected submissions`,
|
|
1655
1579
|
"DVM_SWARM_INVALID_SELECTION"
|
|
1656
1580
|
);
|
|
@@ -1661,7 +1585,7 @@ var SwarmCoordinator = class {
|
|
|
1661
1585
|
this.settlementSucceeded = true;
|
|
1662
1586
|
} catch (_err) {
|
|
1663
1587
|
this.settlementSucceeded = false;
|
|
1664
|
-
throw new
|
|
1588
|
+
throw new ToonError2(
|
|
1665
1589
|
"Settlement failed for winning provider; swarm remains in judging state for retry",
|
|
1666
1590
|
"DVM_SWARM_SETTLEMENT_FAILED"
|
|
1667
1591
|
);
|
|
@@ -1825,6 +1749,10 @@ function createArweaveDvmHandler(config) {
|
|
|
1825
1749
|
data: Buffer.from(txId).toString("base64")
|
|
1826
1750
|
};
|
|
1827
1751
|
} catch (error) {
|
|
1752
|
+
console.error(
|
|
1753
|
+
"[ArweaveDvmHandler] Chunked upload failed:",
|
|
1754
|
+
error instanceof Error ? error.stack ?? error.message : error
|
|
1755
|
+
);
|
|
1828
1756
|
const internalMsg = error instanceof Error ? error.message : "Unknown chunk error";
|
|
1829
1757
|
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";
|
|
1830
1758
|
return {
|
|
@@ -1840,7 +1768,11 @@ function createArweaveDvmHandler(config) {
|
|
|
1840
1768
|
accept: true,
|
|
1841
1769
|
data: Buffer.from(txId).toString("base64")
|
|
1842
1770
|
};
|
|
1843
|
-
} catch {
|
|
1771
|
+
} catch (error) {
|
|
1772
|
+
const cause = error instanceof Error ? error.stack ?? error.message : String(error);
|
|
1773
|
+
console.error(
|
|
1774
|
+
`[ArweaveDvmHandler] Arweave upload failed (${parsed.blobData.length} bytes). Cause: ${cause}`
|
|
1775
|
+
);
|
|
1844
1776
|
return {
|
|
1845
1777
|
accept: false,
|
|
1846
1778
|
code: "T00",
|
|
@@ -2062,18 +1994,1076 @@ async function uploadBlobChunked(node, blob, destination, options) {
|
|
|
2062
1994
|
}
|
|
2063
1995
|
return lastResult.eventId;
|
|
2064
1996
|
}
|
|
1997
|
+
|
|
1998
|
+
// src/settlement/evm.ts
|
|
1999
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
2000
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
2001
|
+
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
2002
|
+
|
|
2003
|
+
// src/settlement/hashes.ts
|
|
2004
|
+
import {
|
|
2005
|
+
hexToBytes as hexToBytes2,
|
|
2006
|
+
bigintToBytes32BE,
|
|
2007
|
+
concatBytes,
|
|
2008
|
+
balanceProofHashEvm,
|
|
2009
|
+
balanceProofHashSolana,
|
|
2010
|
+
minaHashToField,
|
|
2011
|
+
balanceProofFieldsMina
|
|
2012
|
+
} from "@toon-protocol/core";
|
|
2013
|
+
|
|
2014
|
+
// src/settlement/evm.ts
|
|
2015
|
+
var EVM_SETTLEMENT_FUNCTION_SIGNATURE = "updateBalance(bytes32,uint256,uint256,address,bytes)";
|
|
2016
|
+
var EVM_SETTLEMENT_EVENT_SIGNATURE = "SettlementSucceeded(bytes32,uint256,uint256,address)";
|
|
2017
|
+
var EVM_SETTLEMENT_FUNCTION_SELECTOR = keccak_256(
|
|
2018
|
+
new TextEncoder().encode(EVM_SETTLEMENT_FUNCTION_SIGNATURE)
|
|
2019
|
+
).slice(0, 4);
|
|
2020
|
+
var EVM_SETTLEMENT_EVENT_TOPIC = "0x" + bytesToHex(
|
|
2021
|
+
keccak_256(new TextEncoder().encode(EVM_SETTLEMENT_EVENT_SIGNATURE))
|
|
2022
|
+
);
|
|
2023
|
+
function recoverEvmSignerAddress(claim) {
|
|
2024
|
+
if (claim.channelId === void 0 || claim.cumulativeAmount === void 0 || claim.nonce === void 0 || claim.recipient === void 0) {
|
|
2025
|
+
throw new SettlementTxError(
|
|
2026
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2027
|
+
"Claim missing channelId/cumulativeAmount/nonce/recipient for EVM signer recovery"
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
if (claim.claimBytes.length !== 65) {
|
|
2031
|
+
throw new SettlementTxError(
|
|
2032
|
+
"INVALID_SIGNATURE_LENGTH",
|
|
2033
|
+
`EVM signature must be 65 bytes (r||s||v), got ${claim.claimBytes.length}`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
const v = claim.claimBytes[64];
|
|
2037
|
+
if (v !== 27 && v !== 28) {
|
|
2038
|
+
throw new SettlementTxError(
|
|
2039
|
+
"INVALID_SIGNATURE_V",
|
|
2040
|
+
`EVM signature v must be 27 or 28, got ${v}`
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
const recovery = v - 27;
|
|
2044
|
+
const compactRS = claim.claimBytes.slice(0, 64);
|
|
2045
|
+
let msgHash;
|
|
2046
|
+
let uncompressedPubkey;
|
|
2047
|
+
try {
|
|
2048
|
+
const channelIdBytes = hexToBytes2(claim.channelId);
|
|
2049
|
+
const recipientBytes = hexToBytes2(claim.recipient);
|
|
2050
|
+
if (channelIdBytes.length !== 32) {
|
|
2051
|
+
throw new Error(
|
|
2052
|
+
`channelId must be 32 bytes (got ${channelIdBytes.length})`
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
if (recipientBytes.length !== 20) {
|
|
2056
|
+
throw new Error(
|
|
2057
|
+
`recipient must be 20 bytes (got ${recipientBytes.length})`
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
msgHash = balanceProofHashEvm(
|
|
2061
|
+
channelIdBytes,
|
|
2062
|
+
BigInt(claim.cumulativeAmount),
|
|
2063
|
+
BigInt(claim.nonce),
|
|
2064
|
+
recipientBytes
|
|
2065
|
+
);
|
|
2066
|
+
const sig = secp256k1.Signature.fromBytes(
|
|
2067
|
+
compactRS,
|
|
2068
|
+
"compact"
|
|
2069
|
+
).addRecoveryBit(recovery);
|
|
2070
|
+
const point = sig.recoverPublicKey(msgHash);
|
|
2071
|
+
uncompressedPubkey = point.toBytes(false);
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
throw new SettlementTxError(
|
|
2074
|
+
"ENCODING_FAILED",
|
|
2075
|
+
`EVM signer recovery failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2076
|
+
{ cause: err }
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
const addrHash = keccak_256(uncompressedPubkey.slice(1));
|
|
2080
|
+
return "0x" + bytesToHex(addrHash.slice(-20)).toLowerCase();
|
|
2081
|
+
}
|
|
2082
|
+
function padLeft32(bytes) {
|
|
2083
|
+
if (bytes.length > 32) {
|
|
2084
|
+
throw new SettlementTxError(
|
|
2085
|
+
"ENCODING_FAILED",
|
|
2086
|
+
`cannot pad bytes of length ${bytes.length} to 32`
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
const out = new Uint8Array(32);
|
|
2090
|
+
out.set(bytes, 32 - bytes.length);
|
|
2091
|
+
return out;
|
|
2092
|
+
}
|
|
2093
|
+
function padRight32(bytes) {
|
|
2094
|
+
const padded = Math.ceil(bytes.length / 32) * 32;
|
|
2095
|
+
const out = new Uint8Array(padded);
|
|
2096
|
+
out.set(bytes, 0);
|
|
2097
|
+
return out;
|
|
2098
|
+
}
|
|
2099
|
+
function encodeUpdateBalanceCallData(channelIdBytes, cumulativeAmount, nonce, recipientBytes, signature) {
|
|
2100
|
+
if (channelIdBytes.length !== 32) {
|
|
2101
|
+
throw new SettlementTxError(
|
|
2102
|
+
"ENCODING_FAILED",
|
|
2103
|
+
`channelId must be 32 bytes (got ${channelIdBytes.length})`
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
if (recipientBytes.length !== 20) {
|
|
2107
|
+
throw new SettlementTxError(
|
|
2108
|
+
"ENCODING_FAILED",
|
|
2109
|
+
`recipient must be 20 bytes (got ${recipientBytes.length})`
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
const channelIdWord = channelIdBytes;
|
|
2113
|
+
const cumulativeWord = bigintToBytes32BE(cumulativeAmount);
|
|
2114
|
+
const nonceWord = bigintToBytes32BE(nonce);
|
|
2115
|
+
const recipientWord = padLeft32(recipientBytes);
|
|
2116
|
+
const offsetWord = bigintToBytes32BE(160n);
|
|
2117
|
+
const sigLenWord = bigintToBytes32BE(BigInt(signature.length));
|
|
2118
|
+
const sigPadded = padRight32(signature);
|
|
2119
|
+
return concatBytes(
|
|
2120
|
+
EVM_SETTLEMENT_FUNCTION_SELECTOR,
|
|
2121
|
+
channelIdWord,
|
|
2122
|
+
cumulativeWord,
|
|
2123
|
+
nonceWord,
|
|
2124
|
+
recipientWord,
|
|
2125
|
+
offsetWord,
|
|
2126
|
+
sigLenWord,
|
|
2127
|
+
sigPadded
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
function rlpEncodeBytes(bytes) {
|
|
2131
|
+
if (bytes.length === 1 && bytes[0] !== void 0 && bytes[0] < 128) {
|
|
2132
|
+
return bytes;
|
|
2133
|
+
}
|
|
2134
|
+
if (bytes.length < 56) {
|
|
2135
|
+
return concatBytes(new Uint8Array([128 + bytes.length]), bytes);
|
|
2136
|
+
}
|
|
2137
|
+
const lenBytes = bigintToMinimalBytes(BigInt(bytes.length));
|
|
2138
|
+
return concatBytes(new Uint8Array([183 + lenBytes.length]), lenBytes, bytes);
|
|
2139
|
+
}
|
|
2140
|
+
function rlpEncodeList(items) {
|
|
2141
|
+
const payload = concatBytes(...items);
|
|
2142
|
+
if (payload.length < 56) {
|
|
2143
|
+
return concatBytes(new Uint8Array([192 + payload.length]), payload);
|
|
2144
|
+
}
|
|
2145
|
+
const lenBytes = bigintToMinimalBytes(BigInt(payload.length));
|
|
2146
|
+
return concatBytes(
|
|
2147
|
+
new Uint8Array([247 + lenBytes.length]),
|
|
2148
|
+
lenBytes,
|
|
2149
|
+
payload
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
function bigintToMinimalBytes(x) {
|
|
2153
|
+
if (x < 0n) {
|
|
2154
|
+
throw new SettlementTxError(
|
|
2155
|
+
"ENCODING_FAILED",
|
|
2156
|
+
"bigintToMinimalBytes: negative input"
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
if (x === 0n) return new Uint8Array(0);
|
|
2160
|
+
let hex = x.toString(16);
|
|
2161
|
+
if (hex.length % 2) hex = "0" + hex;
|
|
2162
|
+
const out = new Uint8Array(hex.length / 2);
|
|
2163
|
+
for (let i = 0; i < out.length; i++) {
|
|
2164
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
2165
|
+
}
|
|
2166
|
+
return out;
|
|
2167
|
+
}
|
|
2168
|
+
function rlpEncodeUint(x) {
|
|
2169
|
+
return rlpEncodeBytes(bigintToMinimalBytes(x));
|
|
2170
|
+
}
|
|
2171
|
+
function rlpEncodeUnsignedTx(params) {
|
|
2172
|
+
return rlpEncodeList([
|
|
2173
|
+
rlpEncodeUint(params.nonce),
|
|
2174
|
+
rlpEncodeUint(params.gasPrice),
|
|
2175
|
+
rlpEncodeUint(params.gasLimit),
|
|
2176
|
+
rlpEncodeBytes(params.to),
|
|
2177
|
+
rlpEncodeUint(params.value),
|
|
2178
|
+
rlpEncodeBytes(params.data),
|
|
2179
|
+
rlpEncodeUint(params.chainId),
|
|
2180
|
+
rlpEncodeUint(0n),
|
|
2181
|
+
rlpEncodeUint(0n)
|
|
2182
|
+
]);
|
|
2183
|
+
}
|
|
2184
|
+
function buildEvmSettlementTx(winner, signer, recipient, selectedClaimIndex, claimsMerged) {
|
|
2185
|
+
if (winner.channelId === void 0 || winner.cumulativeAmount === void 0 || winner.nonce === void 0 || winner.recipient === void 0 || winner.millSignerAddress === void 0) {
|
|
2186
|
+
throw new SettlementTxError(
|
|
2187
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2188
|
+
"EVM winner claim missing settlement-context fields"
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
if (!signer.contractAddress) {
|
|
2192
|
+
throw new SettlementTxError(
|
|
2193
|
+
"INVALID_INPUT",
|
|
2194
|
+
`EVM MillSignerConfig.contractAddress is required for chain ${winner.pair.to.chain}`
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
if (typeof signer.chainId !== "number" || !Number.isInteger(signer.chainId) || signer.chainId <= 0) {
|
|
2198
|
+
throw new SettlementTxError(
|
|
2199
|
+
"INVALID_INPUT",
|
|
2200
|
+
`EVM MillSignerConfig.chainId must be a positive integer, got ${signer.chainId}`
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
const channelIdBytes = hexToBytes2(winner.channelId);
|
|
2204
|
+
const recipientBytes = hexToBytes2(recipient);
|
|
2205
|
+
const contractBytes = hexToBytes2(signer.contractAddress);
|
|
2206
|
+
const calldata = encodeUpdateBalanceCallData(
|
|
2207
|
+
channelIdBytes,
|
|
2208
|
+
BigInt(winner.cumulativeAmount),
|
|
2209
|
+
BigInt(winner.nonce),
|
|
2210
|
+
recipientBytes,
|
|
2211
|
+
winner.claimBytes
|
|
2212
|
+
);
|
|
2213
|
+
const unsignedTxBytes = rlpEncodeUnsignedTx({
|
|
2214
|
+
nonce: 0n,
|
|
2215
|
+
gasPrice: 0n,
|
|
2216
|
+
gasLimit: 0n,
|
|
2217
|
+
to: contractBytes,
|
|
2218
|
+
value: 0n,
|
|
2219
|
+
data: calldata,
|
|
2220
|
+
chainId: BigInt(signer.chainId)
|
|
2221
|
+
});
|
|
2222
|
+
return {
|
|
2223
|
+
chain: winner.pair.to.chain,
|
|
2224
|
+
chainKind: "evm",
|
|
2225
|
+
channelId: winner.channelId,
|
|
2226
|
+
cumulativeAmount: winner.cumulativeAmount,
|
|
2227
|
+
nonce: winner.nonce,
|
|
2228
|
+
recipient,
|
|
2229
|
+
millSignerAddress: winner.millSignerAddress,
|
|
2230
|
+
unsignedTxBytes,
|
|
2231
|
+
expectedEventSignature: EVM_SETTLEMENT_EVENT_TOPIC,
|
|
2232
|
+
claimsMerged,
|
|
2233
|
+
selectedClaimIndex,
|
|
2234
|
+
sourceChain: winner.pair.from.chain,
|
|
2235
|
+
sourceAssetCode: winner.pair.from.assetCode
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
function fillEvmSettlementTxGas(bundle, gas, signer) {
|
|
2239
|
+
if (bundle.chainKind !== "evm") {
|
|
2240
|
+
throw new SettlementTxError(
|
|
2241
|
+
"UNSUPPORTED_CHAIN",
|
|
2242
|
+
`fillEvmSettlementTxGas requires chainKind=evm, got ${bundle.chainKind}`
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
if (!signer.contractAddress) {
|
|
2246
|
+
throw new SettlementTxError(
|
|
2247
|
+
"INVALID_INPUT",
|
|
2248
|
+
"EVM MillSignerConfig.contractAddress is required for gas-fill"
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
if (typeof signer.chainId !== "number" || signer.chainId <= 0) {
|
|
2252
|
+
throw new SettlementTxError(
|
|
2253
|
+
"INVALID_INPUT",
|
|
2254
|
+
"EVM MillSignerConfig.chainId must be a positive integer"
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
const channelIdBytes = hexToBytes2(bundle.channelId);
|
|
2258
|
+
const recipientBytes = hexToBytes2(bundle.recipient);
|
|
2259
|
+
const contractBytes = hexToBytes2(signer.contractAddress);
|
|
2260
|
+
const sig = extractSignatureFromBundle(bundle);
|
|
2261
|
+
const calldata = encodeUpdateBalanceCallData(
|
|
2262
|
+
channelIdBytes,
|
|
2263
|
+
BigInt(bundle.cumulativeAmount),
|
|
2264
|
+
BigInt(bundle.nonce),
|
|
2265
|
+
recipientBytes,
|
|
2266
|
+
sig
|
|
2267
|
+
);
|
|
2268
|
+
return rlpEncodeUnsignedTx({
|
|
2269
|
+
nonce: gas.nonce,
|
|
2270
|
+
gasPrice: gas.gasPrice,
|
|
2271
|
+
gasLimit: gas.gasLimit,
|
|
2272
|
+
to: contractBytes,
|
|
2273
|
+
value: 0n,
|
|
2274
|
+
data: calldata,
|
|
2275
|
+
chainId: BigInt(signer.chainId)
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
function extractSignatureFromBundle(bundle) {
|
|
2279
|
+
const tx = bundle.unsignedTxBytes;
|
|
2280
|
+
const list = rlpDecodeList(tx);
|
|
2281
|
+
if (list.length < 6) {
|
|
2282
|
+
throw new SettlementTxError(
|
|
2283
|
+
"ENCODING_FAILED",
|
|
2284
|
+
"unsignedTxBytes is not a 9-element RLP list"
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
const data = list[5];
|
|
2288
|
+
if (!data) {
|
|
2289
|
+
throw new SettlementTxError(
|
|
2290
|
+
"ENCODING_FAILED",
|
|
2291
|
+
"unsignedTxBytes missing data field"
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
const sigLenOffset = 4 + 5 * 32;
|
|
2295
|
+
if (data.length < sigLenOffset + 32) {
|
|
2296
|
+
throw new SettlementTxError(
|
|
2297
|
+
"ENCODING_FAILED",
|
|
2298
|
+
"calldata too short to contain signature length word"
|
|
2299
|
+
);
|
|
2300
|
+
}
|
|
2301
|
+
const sigLenBytes = data.slice(sigLenOffset, sigLenOffset + 32);
|
|
2302
|
+
let sigLen = 0n;
|
|
2303
|
+
for (const b of sigLenBytes) sigLen = sigLen << 8n | BigInt(b);
|
|
2304
|
+
const sigStart = sigLenOffset + 32;
|
|
2305
|
+
const sigEnd = sigStart + Number(sigLen);
|
|
2306
|
+
if (sigEnd > data.length) {
|
|
2307
|
+
throw new SettlementTxError(
|
|
2308
|
+
"ENCODING_FAILED",
|
|
2309
|
+
"calldata truncated before end of signature bytes"
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
return data.slice(sigStart, sigEnd);
|
|
2313
|
+
}
|
|
2314
|
+
function rlpDecodeList(buf) {
|
|
2315
|
+
if (buf.length === 0) {
|
|
2316
|
+
throw new SettlementTxError("ENCODING_FAILED", "empty RLP input");
|
|
2317
|
+
}
|
|
2318
|
+
const first = buf[0] ?? 0;
|
|
2319
|
+
let offset;
|
|
2320
|
+
let listEnd;
|
|
2321
|
+
if (first >= 192 && first <= 247) {
|
|
2322
|
+
offset = 1;
|
|
2323
|
+
listEnd = 1 + (first - 192);
|
|
2324
|
+
} else if (first >= 248 && first <= 255) {
|
|
2325
|
+
const lenOfLen = first - 247;
|
|
2326
|
+
let listLen = 0;
|
|
2327
|
+
for (let i = 0; i < lenOfLen; i++) {
|
|
2328
|
+
listLen = listLen << 8 | (buf[1 + i] ?? 0);
|
|
2329
|
+
}
|
|
2330
|
+
offset = 1 + lenOfLen;
|
|
2331
|
+
listEnd = offset + listLen;
|
|
2332
|
+
} else {
|
|
2333
|
+
throw new SettlementTxError(
|
|
2334
|
+
"ENCODING_FAILED",
|
|
2335
|
+
`rlpDecodeList: input is not a list (first byte 0x${first.toString(16)})`
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
const items = [];
|
|
2339
|
+
let p = offset;
|
|
2340
|
+
while (p < listEnd) {
|
|
2341
|
+
const b = buf[p] ?? 0;
|
|
2342
|
+
if (b < 128) {
|
|
2343
|
+
items.push(buf.slice(p, p + 1));
|
|
2344
|
+
p += 1;
|
|
2345
|
+
} else if (b <= 183) {
|
|
2346
|
+
const len = b - 128;
|
|
2347
|
+
items.push(buf.slice(p + 1, p + 1 + len));
|
|
2348
|
+
p += 1 + len;
|
|
2349
|
+
} else if (b <= 191) {
|
|
2350
|
+
const lenOfLen = b - 183;
|
|
2351
|
+
let len = 0;
|
|
2352
|
+
for (let i = 0; i < lenOfLen; i++) {
|
|
2353
|
+
len = len << 8 | (buf[p + 1 + i] ?? 0);
|
|
2354
|
+
}
|
|
2355
|
+
items.push(buf.slice(p + 1 + lenOfLen, p + 1 + lenOfLen + len));
|
|
2356
|
+
p += 1 + lenOfLen + len;
|
|
2357
|
+
} else {
|
|
2358
|
+
throw new SettlementTxError(
|
|
2359
|
+
"ENCODING_FAILED",
|
|
2360
|
+
"rlpDecodeList: nested list not supported in minimal decoder"
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
return items;
|
|
2365
|
+
}
|
|
2366
|
+
function verifyEvmClaimSignature(claim, expectedAddress) {
|
|
2367
|
+
const recovered = recoverEvmSignerAddress(claim);
|
|
2368
|
+
const expected = expectedAddress.toLowerCase();
|
|
2369
|
+
return {
|
|
2370
|
+
valid: recovered.toLowerCase() === expected,
|
|
2371
|
+
recovered
|
|
2372
|
+
};
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
// src/settlement/mina.ts
|
|
2376
|
+
var MINA_NETWORK = "mainnet";
|
|
2377
|
+
async function loadMinaSignerClient() {
|
|
2378
|
+
try {
|
|
2379
|
+
const lib = await import("mina-signer");
|
|
2380
|
+
const Ctor = "default" in lib ? lib.default : lib;
|
|
2381
|
+
return new Ctor({ network: MINA_NETWORK });
|
|
2382
|
+
} catch {
|
|
2383
|
+
return void 0;
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
function verifyMinaSignature(claim, expectedSignerAddress, client) {
|
|
2387
|
+
if (claim.channelId === void 0 || claim.cumulativeAmount === void 0 || claim.nonce === void 0 || claim.recipient === void 0) {
|
|
2388
|
+
throw new SettlementTxError(
|
|
2389
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2390
|
+
"Claim missing channelId/cumulativeAmount/nonce/recipient for Mina signature verify"
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
if (claim.claimBytes.length === 0) {
|
|
2394
|
+
throw new SettlementTxError(
|
|
2395
|
+
"INVALID_SIGNATURE_LENGTH",
|
|
2396
|
+
"Mina claimBytes is empty (expected UTF-8 base58 signature string)"
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
const signature = new TextDecoder().decode(claim.claimBytes);
|
|
2400
|
+
const fields = balanceProofFieldsMina(
|
|
2401
|
+
claim.channelId,
|
|
2402
|
+
BigInt(claim.cumulativeAmount),
|
|
2403
|
+
BigInt(claim.nonce),
|
|
2404
|
+
claim.recipient
|
|
2405
|
+
);
|
|
2406
|
+
try {
|
|
2407
|
+
return client.verifyFields({
|
|
2408
|
+
data: fields,
|
|
2409
|
+
signature,
|
|
2410
|
+
publicKey: expectedSignerAddress
|
|
2411
|
+
});
|
|
2412
|
+
} catch {
|
|
2413
|
+
return false;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
function buildMinaSettlementTx(winner, signer, recipient, selectedClaimIndex, claimsMerged) {
|
|
2417
|
+
if (winner.channelId === void 0 || winner.cumulativeAmount === void 0 || winner.nonce === void 0 || winner.recipient === void 0 || winner.millSignerAddress === void 0) {
|
|
2418
|
+
throw new SettlementTxError(
|
|
2419
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2420
|
+
"Mina winner claim missing settlement-context fields"
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
if (!signer.address) {
|
|
2424
|
+
throw new SettlementTxError(
|
|
2425
|
+
"INVALID_INPUT",
|
|
2426
|
+
`Mina MillSignerConfig.address is required for chain ${winner.pair.to.chain}`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
if (winner.claimBytes.length === 0) {
|
|
2430
|
+
throw new SettlementTxError(
|
|
2431
|
+
"INVALID_SIGNATURE_LENGTH",
|
|
2432
|
+
"Mina winner claimBytes is empty (expected UTF-8 base58 signature string)"
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
const unsignedTxBytes = winner.claimBytes;
|
|
2436
|
+
return {
|
|
2437
|
+
chain: winner.pair.to.chain,
|
|
2438
|
+
chainKind: "mina",
|
|
2439
|
+
channelId: winner.channelId,
|
|
2440
|
+
cumulativeAmount: winner.cumulativeAmount,
|
|
2441
|
+
nonce: winner.nonce,
|
|
2442
|
+
recipient,
|
|
2443
|
+
millSignerAddress: winner.millSignerAddress,
|
|
2444
|
+
unsignedTxBytes,
|
|
2445
|
+
claimsMerged,
|
|
2446
|
+
selectedClaimIndex,
|
|
2447
|
+
sourceChain: winner.pair.from.chain,
|
|
2448
|
+
sourceAssetCode: winner.pair.from.assetCode
|
|
2449
|
+
};
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
// src/settlement/solana.ts
|
|
2453
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
2454
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2455
|
+
function verifyEd25519Signature(claim, expectedSignerAddress) {
|
|
2456
|
+
if (claim.channelId === void 0 || claim.cumulativeAmount === void 0 || claim.nonce === void 0 || claim.recipient === void 0) {
|
|
2457
|
+
throw new SettlementTxError(
|
|
2458
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2459
|
+
"Claim missing channelId/cumulativeAmount/nonce/recipient for Solana signature verify"
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
if (claim.claimBytes.length !== 64) {
|
|
2463
|
+
throw new SettlementTxError(
|
|
2464
|
+
"INVALID_SIGNATURE_LENGTH",
|
|
2465
|
+
`Solana signature must be 64 bytes, got ${claim.claimBytes.length}`
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
const msgHash = balanceProofHashSolana(
|
|
2469
|
+
claim.channelId,
|
|
2470
|
+
BigInt(claim.cumulativeAmount),
|
|
2471
|
+
BigInt(claim.nonce),
|
|
2472
|
+
claim.recipient
|
|
2473
|
+
);
|
|
2474
|
+
let pubkeyBytes;
|
|
2475
|
+
try {
|
|
2476
|
+
pubkeyBytes = base58Decode(expectedSignerAddress);
|
|
2477
|
+
} catch (err) {
|
|
2478
|
+
throw new SettlementTxError(
|
|
2479
|
+
"INVALID_INPUT",
|
|
2480
|
+
`Solana expected signer address is not valid base58: ${expectedSignerAddress}`,
|
|
2481
|
+
{ cause: err }
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
if (pubkeyBytes.length !== 32) {
|
|
2485
|
+
throw new SettlementTxError(
|
|
2486
|
+
"INVALID_INPUT",
|
|
2487
|
+
`Solana expected signer pubkey must be 32 bytes, got ${pubkeyBytes.length}`
|
|
2488
|
+
);
|
|
2489
|
+
}
|
|
2490
|
+
try {
|
|
2491
|
+
return ed25519.verify(claim.claimBytes, msgHash, pubkeyBytes);
|
|
2492
|
+
} catch {
|
|
2493
|
+
return false;
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
var SOLANA_UPDATE_BALANCE_DISCRIMINATOR = sha256(
|
|
2497
|
+
new TextEncoder().encode("global:update_balance")
|
|
2498
|
+
).slice(0, 8);
|
|
2499
|
+
function bigintToBytes8LE(x) {
|
|
2500
|
+
if (x < 0n) {
|
|
2501
|
+
throw new SettlementTxError(
|
|
2502
|
+
"ENCODING_FAILED",
|
|
2503
|
+
"bigintToBytes8LE: negative input"
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
if (x > 0xffffffffffffffffn) {
|
|
2507
|
+
throw new SettlementTxError(
|
|
2508
|
+
"ENCODING_FAILED",
|
|
2509
|
+
"bigintToBytes8LE: value exceeds 64 bits"
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
const out = new Uint8Array(8);
|
|
2513
|
+
let v = x;
|
|
2514
|
+
for (let i = 0; i < 8; i++) {
|
|
2515
|
+
out[i] = Number(v & 0xffn);
|
|
2516
|
+
v >>= 8n;
|
|
2517
|
+
}
|
|
2518
|
+
return out;
|
|
2519
|
+
}
|
|
2520
|
+
function buildSolanaSettlementTx(winner, signer, recipient, selectedClaimIndex, claimsMerged) {
|
|
2521
|
+
if (winner.channelId === void 0 || winner.cumulativeAmount === void 0 || winner.nonce === void 0 || winner.recipient === void 0 || winner.millSignerAddress === void 0) {
|
|
2522
|
+
throw new SettlementTxError(
|
|
2523
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2524
|
+
"Solana winner claim missing settlement-context fields"
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
if (!signer.programId) {
|
|
2528
|
+
throw new SettlementTxError(
|
|
2529
|
+
"INVALID_INPUT",
|
|
2530
|
+
`Solana MillSignerConfig.programId is required for chain ${winner.pair.to.chain}`
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
let programIdBytes;
|
|
2534
|
+
let recipientBytes;
|
|
2535
|
+
let millBytes;
|
|
2536
|
+
let channelIdBytes;
|
|
2537
|
+
try {
|
|
2538
|
+
programIdBytes = base58Decode(signer.programId);
|
|
2539
|
+
recipientBytes = base58Decode(recipient);
|
|
2540
|
+
millBytes = base58Decode(winner.millSignerAddress);
|
|
2541
|
+
channelIdBytes = base58Decode(winner.channelId);
|
|
2542
|
+
} catch (err) {
|
|
2543
|
+
throw new SettlementTxError(
|
|
2544
|
+
"ENCODING_FAILED",
|
|
2545
|
+
`Solana settlement tx: base58 decode failed (${err instanceof Error ? err.message : String(err)})`,
|
|
2546
|
+
{ cause: err }
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
if (programIdBytes.length !== 32) {
|
|
2550
|
+
throw new SettlementTxError(
|
|
2551
|
+
"INVALID_INPUT",
|
|
2552
|
+
`Solana programId must be 32 bytes, got ${programIdBytes.length}`
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
if (recipientBytes.length !== 32) {
|
|
2556
|
+
throw new SettlementTxError(
|
|
2557
|
+
"INVALID_INPUT",
|
|
2558
|
+
`Solana recipient must decode to 32 bytes, got ${recipientBytes.length}`
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
if (millBytes.length !== 32) {
|
|
2562
|
+
throw new SettlementTxError(
|
|
2563
|
+
"INVALID_INPUT",
|
|
2564
|
+
`Solana millSignerAddress must decode to 32 bytes, got ${millBytes.length}`
|
|
2565
|
+
);
|
|
2566
|
+
}
|
|
2567
|
+
if (channelIdBytes.length !== 32) {
|
|
2568
|
+
throw new SettlementTxError(
|
|
2569
|
+
"INVALID_INPUT",
|
|
2570
|
+
`Solana channelId must decode to 32 bytes, got ${channelIdBytes.length}`
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
const instructionData = concatBytes(
|
|
2574
|
+
SOLANA_UPDATE_BALANCE_DISCRIMINATOR,
|
|
2575
|
+
bigintToBytes8LE(BigInt(winner.cumulativeAmount)),
|
|
2576
|
+
bigintToBytes8LE(BigInt(winner.nonce)),
|
|
2577
|
+
winner.claimBytes
|
|
2578
|
+
);
|
|
2579
|
+
const accounts = [recipientBytes, millBytes, channelIdBytes, programIdBytes];
|
|
2580
|
+
const header = new Uint8Array([1, 0, 1]);
|
|
2581
|
+
const accountsCountByte = new Uint8Array([accounts.length]);
|
|
2582
|
+
const recentBlockhash = new Uint8Array(32);
|
|
2583
|
+
const instructionsCountByte = new Uint8Array([1]);
|
|
2584
|
+
const programIdIndex = new Uint8Array([3]);
|
|
2585
|
+
const instrAccountsLen = new Uint8Array([3]);
|
|
2586
|
+
const instrAccountIndices = new Uint8Array([0, 1, 2]);
|
|
2587
|
+
if (instructionData.length >= 128) {
|
|
2588
|
+
throw new SettlementTxError(
|
|
2589
|
+
"ENCODING_FAILED",
|
|
2590
|
+
`Solana instruction data too large for simple compact-u16 encoding: ${instructionData.length}`
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
const instrDataLen = new Uint8Array([instructionData.length]);
|
|
2594
|
+
const instruction = concatBytes(
|
|
2595
|
+
programIdIndex,
|
|
2596
|
+
instrAccountsLen,
|
|
2597
|
+
instrAccountIndices,
|
|
2598
|
+
instrDataLen,
|
|
2599
|
+
instructionData
|
|
2600
|
+
);
|
|
2601
|
+
const unsignedTxBytes = concatBytes(
|
|
2602
|
+
header,
|
|
2603
|
+
accountsCountByte,
|
|
2604
|
+
...accounts,
|
|
2605
|
+
recentBlockhash,
|
|
2606
|
+
instructionsCountByte,
|
|
2607
|
+
instruction
|
|
2608
|
+
);
|
|
2609
|
+
return {
|
|
2610
|
+
chain: winner.pair.to.chain,
|
|
2611
|
+
chainKind: "solana",
|
|
2612
|
+
channelId: winner.channelId,
|
|
2613
|
+
cumulativeAmount: winner.cumulativeAmount,
|
|
2614
|
+
nonce: winner.nonce,
|
|
2615
|
+
recipient,
|
|
2616
|
+
millSignerAddress: winner.millSignerAddress,
|
|
2617
|
+
unsignedTxBytes,
|
|
2618
|
+
claimsMerged,
|
|
2619
|
+
selectedClaimIndex,
|
|
2620
|
+
sourceChain: winner.pair.from.chain,
|
|
2621
|
+
sourceAssetCode: winner.pair.from.assetCode
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// src/settlement/build-settlement-tx.ts
|
|
2626
|
+
var EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;
|
|
2627
|
+
function chainKindOf(chain) {
|
|
2628
|
+
if (chain.startsWith("evm:")) return "evm";
|
|
2629
|
+
if (chain.startsWith("solana:")) return "solana";
|
|
2630
|
+
if (chain.startsWith("mina:")) return "mina";
|
|
2631
|
+
return "unknown";
|
|
2632
|
+
}
|
|
2633
|
+
function buildSettlementTx(params) {
|
|
2634
|
+
if (!Array.isArray(params.claims) || params.claims.length === 0) {
|
|
2635
|
+
throw new SettlementTxError(
|
|
2636
|
+
"INVALID_INPUT",
|
|
2637
|
+
"claims array is empty or not an array"
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
if (!params.signers || typeof params.signers !== "object") {
|
|
2641
|
+
throw new SettlementTxError("INVALID_INPUT", "signers map is required");
|
|
2642
|
+
}
|
|
2643
|
+
if (!params.recipients || typeof params.recipients !== "object") {
|
|
2644
|
+
throw new SettlementTxError("INVALID_INPUT", "recipients map is required");
|
|
2645
|
+
}
|
|
2646
|
+
const logger = params.logger;
|
|
2647
|
+
const verifySignatures = params.verifySignatures ?? true;
|
|
2648
|
+
for (let i = 0; i < params.claims.length; i++) {
|
|
2649
|
+
const c = params.claims[i];
|
|
2650
|
+
if (c === void 0) continue;
|
|
2651
|
+
if (c.channelId === void 0 || c.nonce === void 0 || c.cumulativeAmount === void 0 || c.recipient === void 0 || c.millSignerAddress === void 0) {
|
|
2652
|
+
throw new SettlementTxError(
|
|
2653
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2654
|
+
`claims[${i}] missing one or more of { channelId, nonce, cumulativeAmount, recipient, millSignerAddress }`
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
const distinctChains = /* @__PURE__ */ new Set();
|
|
2659
|
+
for (const c of params.claims) distinctChains.add(c.pair.to.chain);
|
|
2660
|
+
for (const chain of distinctChains) {
|
|
2661
|
+
const signer = params.signers[chain];
|
|
2662
|
+
if (!signer) {
|
|
2663
|
+
throw new SettlementTxError(
|
|
2664
|
+
"UNSUPPORTED_CHAIN",
|
|
2665
|
+
`signers map missing entry for chain ${chain}`
|
|
2666
|
+
);
|
|
2667
|
+
}
|
|
2668
|
+
if (!(chain in params.recipients)) {
|
|
2669
|
+
throw new SettlementTxError(
|
|
2670
|
+
"MISSING_RECIPIENT",
|
|
2671
|
+
`recipients map missing entry for chain ${chain}`
|
|
2672
|
+
);
|
|
2673
|
+
}
|
|
2674
|
+
const kind = chainKindOf(chain);
|
|
2675
|
+
if (kind === "evm") {
|
|
2676
|
+
if (!signer.contractAddress || !EVM_ADDRESS_REGEX.test(signer.contractAddress)) {
|
|
2677
|
+
throw new SettlementTxError(
|
|
2678
|
+
"INVALID_INPUT",
|
|
2679
|
+
`EVM signers[${chain}].contractAddress must match 0x + 40 lowercase hex`
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
if (typeof signer.chainId !== "number" || !Number.isInteger(signer.chainId) || signer.chainId <= 0) {
|
|
2683
|
+
throw new SettlementTxError(
|
|
2684
|
+
"INVALID_INPUT",
|
|
2685
|
+
`EVM signers[${chain}].chainId must be a positive integer`
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
} else if (kind === "solana") {
|
|
2689
|
+
if (!signer.programId || signer.programId.length === 0) {
|
|
2690
|
+
throw new SettlementTxError(
|
|
2691
|
+
"INVALID_INPUT",
|
|
2692
|
+
`Solana signers[${chain}].programId is required`
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2695
|
+
let programIdLen;
|
|
2696
|
+
try {
|
|
2697
|
+
programIdLen = base58Decode(signer.programId).length;
|
|
2698
|
+
} catch (err) {
|
|
2699
|
+
throw new SettlementTxError(
|
|
2700
|
+
"INVALID_INPUT",
|
|
2701
|
+
`Solana signers[${chain}].programId is not valid base58`,
|
|
2702
|
+
{ cause: err }
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
if (programIdLen !== 32) {
|
|
2706
|
+
throw new SettlementTxError(
|
|
2707
|
+
"INVALID_INPUT",
|
|
2708
|
+
`Solana signers[${chain}].programId must decode to 32 bytes (got ${programIdLen})`
|
|
2709
|
+
);
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
const rejected = [];
|
|
2714
|
+
const survivors = [];
|
|
2715
|
+
for (const claim of params.claims) {
|
|
2716
|
+
if (!verifySignatures) {
|
|
2717
|
+
survivors.push(claim);
|
|
2718
|
+
continue;
|
|
2719
|
+
}
|
|
2720
|
+
const chain = claim.pair.to.chain;
|
|
2721
|
+
const signer = params.signers[chain];
|
|
2722
|
+
if (!signer) {
|
|
2723
|
+
throw new SettlementTxError(
|
|
2724
|
+
"UNSUPPORTED_CHAIN",
|
|
2725
|
+
`signers map missing entry for chain ${chain} (unreachable \u2014 validated)`
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
const kind = chainKindOf(chain);
|
|
2729
|
+
if (kind === "evm") {
|
|
2730
|
+
try {
|
|
2731
|
+
const { valid, recovered } = verifyEvmClaimSignature(
|
|
2732
|
+
claim,
|
|
2733
|
+
signer.address
|
|
2734
|
+
);
|
|
2735
|
+
if (!valid) {
|
|
2736
|
+
rejected.push({
|
|
2737
|
+
claim,
|
|
2738
|
+
reason: "SIGNER_MISMATCH",
|
|
2739
|
+
details: `recovered=${recovered} expected=${signer.address.toLowerCase()}`
|
|
2740
|
+
});
|
|
2741
|
+
continue;
|
|
2742
|
+
}
|
|
2743
|
+
survivors.push(claim);
|
|
2744
|
+
} catch (err) {
|
|
2745
|
+
rejected.push({
|
|
2746
|
+
claim,
|
|
2747
|
+
reason: "SIGNATURE_INVALID",
|
|
2748
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
} else if (kind === "solana") {
|
|
2752
|
+
try {
|
|
2753
|
+
const ok = verifyEd25519Signature(claim, signer.address);
|
|
2754
|
+
if (!ok) {
|
|
2755
|
+
rejected.push({
|
|
2756
|
+
claim,
|
|
2757
|
+
reason: "SIGNER_MISMATCH",
|
|
2758
|
+
details: `ed25519.verify returned false against ${signer.address}`
|
|
2759
|
+
});
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2762
|
+
survivors.push(claim);
|
|
2763
|
+
} catch (err) {
|
|
2764
|
+
rejected.push({
|
|
2765
|
+
claim,
|
|
2766
|
+
reason: "SIGNATURE_INVALID",
|
|
2767
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
} else if (kind === "mina") {
|
|
2771
|
+
if (!params.minaSignerClient) {
|
|
2772
|
+
rejected.push({
|
|
2773
|
+
claim,
|
|
2774
|
+
reason: "MINA_VERIFICATION_UNSUPPORTED",
|
|
2775
|
+
details: "minaSignerClient not provided \u2014 load mina-signer via loadMinaSignerClient() and pass it in params.minaSignerClient to verify mina:* claims"
|
|
2776
|
+
});
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
try {
|
|
2780
|
+
const ok = verifyMinaSignature(
|
|
2781
|
+
claim,
|
|
2782
|
+
signer.address,
|
|
2783
|
+
params.minaSignerClient
|
|
2784
|
+
);
|
|
2785
|
+
if (!ok) {
|
|
2786
|
+
rejected.push({
|
|
2787
|
+
claim,
|
|
2788
|
+
reason: "SIGNER_MISMATCH",
|
|
2789
|
+
details: `mina-signer verifyFields returned false against ${signer.address}`
|
|
2790
|
+
});
|
|
2791
|
+
continue;
|
|
2792
|
+
}
|
|
2793
|
+
survivors.push(claim);
|
|
2794
|
+
} catch (err) {
|
|
2795
|
+
rejected.push({
|
|
2796
|
+
claim,
|
|
2797
|
+
reason: "SIGNATURE_INVALID",
|
|
2798
|
+
details: err instanceof Error ? err.message : String(err)
|
|
2799
|
+
});
|
|
2800
|
+
}
|
|
2801
|
+
} else {
|
|
2802
|
+
throw new SettlementTxError(
|
|
2803
|
+
"UNSUPPORTED_CHAIN",
|
|
2804
|
+
`Unknown chain kind for ${chain}`
|
|
2805
|
+
);
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
logger?.debug?.({
|
|
2809
|
+
event: "build_settlement_tx.verified",
|
|
2810
|
+
survivorCount: survivors.length,
|
|
2811
|
+
rejectedCount: rejected.length
|
|
2812
|
+
});
|
|
2813
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2814
|
+
const originalIndex = /* @__PURE__ */ new Map();
|
|
2815
|
+
for (let i = 0; i < params.claims.length; i++) {
|
|
2816
|
+
const c = params.claims[i];
|
|
2817
|
+
if (c !== void 0) originalIndex.set(c, i);
|
|
2818
|
+
}
|
|
2819
|
+
for (const claim of survivors) {
|
|
2820
|
+
const chain = claim.pair.to.chain;
|
|
2821
|
+
const channelId = claim.channelId;
|
|
2822
|
+
if (channelId === void 0) {
|
|
2823
|
+
throw new SettlementTxError(
|
|
2824
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2825
|
+
"claim.channelId undefined after validation (unreachable)"
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
const key = `${chain}::${channelId}`;
|
|
2829
|
+
let g = groups.get(key);
|
|
2830
|
+
if (!g) {
|
|
2831
|
+
g = { chain, channelId, claims: [] };
|
|
2832
|
+
groups.set(key, g);
|
|
2833
|
+
}
|
|
2834
|
+
const idx = originalIndex.get(claim);
|
|
2835
|
+
g.claims.push({ claim, originalIndex: idx ?? -1 });
|
|
2836
|
+
}
|
|
2837
|
+
const superseded = [];
|
|
2838
|
+
const bundles = [];
|
|
2839
|
+
for (const g of groups.values()) {
|
|
2840
|
+
if (g.claims.length === 0) continue;
|
|
2841
|
+
const first = g.claims[0];
|
|
2842
|
+
if (!first) continue;
|
|
2843
|
+
const firstRecipient = first.claim.recipient;
|
|
2844
|
+
const firstMillSigner = first.claim.millSignerAddress;
|
|
2845
|
+
if (firstRecipient === void 0 || firstMillSigner === void 0) {
|
|
2846
|
+
throw new SettlementTxError(
|
|
2847
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2848
|
+
"winner claim missing recipient/millSignerAddress (unreachable after validation)"
|
|
2849
|
+
);
|
|
2850
|
+
}
|
|
2851
|
+
for (let i = 1; i < g.claims.length; i++) {
|
|
2852
|
+
const entry = g.claims[i];
|
|
2853
|
+
if (!entry) continue;
|
|
2854
|
+
const c = entry.claim;
|
|
2855
|
+
if (c.recipient !== firstRecipient) {
|
|
2856
|
+
throw new SettlementTxError(
|
|
2857
|
+
"RECIPIENT_MISMATCH",
|
|
2858
|
+
`claims in channel ${g.channelId} disagree on recipient: ${firstRecipient} vs ${String(c.recipient)} (claim indices ${first.originalIndex}, ${entry.originalIndex})`
|
|
2859
|
+
);
|
|
2860
|
+
}
|
|
2861
|
+
if (c.millSignerAddress !== firstMillSigner) {
|
|
2862
|
+
throw new SettlementTxError(
|
|
2863
|
+
"MILL_SIGNER_MISMATCH",
|
|
2864
|
+
`claims in channel ${g.channelId} disagree on millSignerAddress: ${firstMillSigner} vs ${String(c.millSignerAddress)}`
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
const nonceSeen = /* @__PURE__ */ new Set();
|
|
2869
|
+
for (const entry of g.claims) {
|
|
2870
|
+
const nonceStr = entry.claim.nonce;
|
|
2871
|
+
if (nonceStr === void 0) {
|
|
2872
|
+
throw new SettlementTxError(
|
|
2873
|
+
"MISSING_SETTLEMENT_METADATA",
|
|
2874
|
+
"claim.nonce undefined (unreachable after validation)"
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
if (nonceSeen.has(nonceStr)) {
|
|
2878
|
+
throw new SettlementTxError(
|
|
2879
|
+
"DUPLICATE_NONCE",
|
|
2880
|
+
`channel ${g.channelId} has two claims with nonce ${nonceStr}`
|
|
2881
|
+
);
|
|
2882
|
+
}
|
|
2883
|
+
nonceSeen.add(nonceStr);
|
|
2884
|
+
}
|
|
2885
|
+
const sorted = [...g.claims].sort((a, b) => {
|
|
2886
|
+
const an = BigInt(a.claim.nonce ?? "0");
|
|
2887
|
+
const bn = BigInt(b.claim.nonce ?? "0");
|
|
2888
|
+
return an < bn ? -1 : an > bn ? 1 : 0;
|
|
2889
|
+
});
|
|
2890
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
2891
|
+
const prevE = sorted[i - 1];
|
|
2892
|
+
const currE = sorted[i];
|
|
2893
|
+
if (!prevE || !currE) continue;
|
|
2894
|
+
const prev = BigInt(prevE.claim.cumulativeAmount ?? "0");
|
|
2895
|
+
const curr = BigInt(currE.claim.cumulativeAmount ?? "0");
|
|
2896
|
+
if (curr < prev) {
|
|
2897
|
+
throw new SettlementTxError(
|
|
2898
|
+
"NON_MONOTONIC_CUMULATIVE",
|
|
2899
|
+
`channel ${g.channelId} nonce ${String(currE.claim.nonce)} has cumulativeAmount ${curr} < previous nonce ${String(prevE.claim.nonce)} cumulativeAmount ${prev}`
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
const winnerEntry = sorted[sorted.length - 1];
|
|
2904
|
+
if (!winnerEntry) continue;
|
|
2905
|
+
const winner = winnerEntry.claim;
|
|
2906
|
+
if (params.includeSuperseded) {
|
|
2907
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
2908
|
+
const e = sorted[i];
|
|
2909
|
+
if (e) superseded.push(e.claim);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
const signer = params.signers[g.chain];
|
|
2913
|
+
const recipient = params.recipients[g.chain];
|
|
2914
|
+
if (!signer || recipient === void 0) {
|
|
2915
|
+
throw new SettlementTxError(
|
|
2916
|
+
"UNSUPPORTED_CHAIN",
|
|
2917
|
+
`signers/recipients missing entry for ${g.chain} (unreachable after validation)`
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
const kind = chainKindOf(g.chain);
|
|
2921
|
+
let bundle;
|
|
2922
|
+
if (kind === "evm") {
|
|
2923
|
+
if (recipient.toLowerCase() !== firstRecipient.toLowerCase()) {
|
|
2924
|
+
throw new SettlementTxError(
|
|
2925
|
+
"RECIPIENT_MISMATCH",
|
|
2926
|
+
`recipients[${g.chain}] (${recipient}) does not match Mill-reported recipient (${firstRecipient})`
|
|
2927
|
+
);
|
|
2928
|
+
}
|
|
2929
|
+
bundle = buildEvmSettlementTx(
|
|
2930
|
+
winner,
|
|
2931
|
+
signer,
|
|
2932
|
+
recipient.toLowerCase(),
|
|
2933
|
+
winnerEntry.originalIndex,
|
|
2934
|
+
g.claims.length
|
|
2935
|
+
);
|
|
2936
|
+
} else if (kind === "solana") {
|
|
2937
|
+
if (recipient !== firstRecipient) {
|
|
2938
|
+
throw new SettlementTxError(
|
|
2939
|
+
"RECIPIENT_MISMATCH",
|
|
2940
|
+
`recipients[${g.chain}] (${recipient}) does not match Mill-reported recipient (${firstRecipient})`
|
|
2941
|
+
);
|
|
2942
|
+
}
|
|
2943
|
+
bundle = buildSolanaSettlementTx(
|
|
2944
|
+
winner,
|
|
2945
|
+
signer,
|
|
2946
|
+
recipient,
|
|
2947
|
+
winnerEntry.originalIndex,
|
|
2948
|
+
g.claims.length
|
|
2949
|
+
);
|
|
2950
|
+
} else if (kind === "mina") {
|
|
2951
|
+
if (recipient !== firstRecipient) {
|
|
2952
|
+
throw new SettlementTxError(
|
|
2953
|
+
"RECIPIENT_MISMATCH",
|
|
2954
|
+
`recipients[${g.chain}] (${recipient}) does not match Mill-reported recipient (${firstRecipient})`
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
bundle = buildMinaSettlementTx(
|
|
2958
|
+
winner,
|
|
2959
|
+
signer,
|
|
2960
|
+
recipient,
|
|
2961
|
+
winnerEntry.originalIndex,
|
|
2962
|
+
g.claims.length
|
|
2963
|
+
);
|
|
2964
|
+
} else {
|
|
2965
|
+
throw new SettlementTxError(
|
|
2966
|
+
"UNSUPPORTED_CHAIN",
|
|
2967
|
+
`Unknown chain kind for ${g.chain}`
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
bundles.push(bundle);
|
|
2971
|
+
}
|
|
2972
|
+
logger?.info?.({
|
|
2973
|
+
event: "build_settlement_tx.complete",
|
|
2974
|
+
bundleCount: bundles.length,
|
|
2975
|
+
rejectedCount: rejected.length,
|
|
2976
|
+
supersededCount: superseded.length
|
|
2977
|
+
});
|
|
2978
|
+
return { bundles, rejected, superseded };
|
|
2979
|
+
}
|
|
2980
|
+
function verifyAccumulatedClaim(claim, signer, minaSignerClient) {
|
|
2981
|
+
const kind = chainKindOf(claim.pair.to.chain);
|
|
2982
|
+
if (kind === "evm") {
|
|
2983
|
+
try {
|
|
2984
|
+
const { valid, recovered } = verifyEvmClaimSignature(
|
|
2985
|
+
claim,
|
|
2986
|
+
signer.address
|
|
2987
|
+
);
|
|
2988
|
+
if (valid) return { valid: true };
|
|
2989
|
+
return {
|
|
2990
|
+
valid: false,
|
|
2991
|
+
reason: `SIGNER_MISMATCH: recovered=${recovered} expected=${signer.address.toLowerCase()}`
|
|
2992
|
+
};
|
|
2993
|
+
} catch (err) {
|
|
2994
|
+
return {
|
|
2995
|
+
valid: false,
|
|
2996
|
+
reason: `SIGNATURE_INVALID: ${err instanceof Error ? err.message : String(err)}`
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
if (kind === "solana") {
|
|
3001
|
+
try {
|
|
3002
|
+
const ok = verifyEd25519Signature(claim, signer.address);
|
|
3003
|
+
return ok ? { valid: true } : {
|
|
3004
|
+
valid: false,
|
|
3005
|
+
reason: "SIGNER_MISMATCH: ed25519.verify returned false"
|
|
3006
|
+
};
|
|
3007
|
+
} catch (err) {
|
|
3008
|
+
return {
|
|
3009
|
+
valid: false,
|
|
3010
|
+
reason: `SIGNATURE_INVALID: ${err instanceof Error ? err.message : String(err)}`
|
|
3011
|
+
};
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
if (kind === "mina") {
|
|
3015
|
+
if (!minaSignerClient) {
|
|
3016
|
+
return {
|
|
3017
|
+
valid: false,
|
|
3018
|
+
reason: "MINA_VERIFICATION_UNSUPPORTED: pass a mina-signer Client (loadMinaSignerClient()) as minaSignerClient to verify mina:* claims"
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
try {
|
|
3022
|
+
const ok = verifyMinaSignature(claim, signer.address, minaSignerClient);
|
|
3023
|
+
return ok ? { valid: true } : {
|
|
3024
|
+
valid: false,
|
|
3025
|
+
reason: "SIGNER_MISMATCH: mina-signer verifyFields returned false"
|
|
3026
|
+
};
|
|
3027
|
+
} catch (err) {
|
|
3028
|
+
return {
|
|
3029
|
+
valid: false,
|
|
3030
|
+
reason: `SIGNATURE_INVALID: ${err instanceof Error ? err.message : String(err)}`
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
return {
|
|
3035
|
+
valid: false,
|
|
3036
|
+
reason: `UNSUPPORTED_CHAIN: ${claim.pair.to.chain}`
|
|
3037
|
+
};
|
|
3038
|
+
}
|
|
2065
3039
|
export {
|
|
2066
3040
|
ChunkManager,
|
|
3041
|
+
GiftWrapError,
|
|
2067
3042
|
HandlerError,
|
|
2068
3043
|
HandlerRegistry,
|
|
2069
3044
|
IdentityError,
|
|
2070
3045
|
NodeError,
|
|
2071
3046
|
PricingError,
|
|
3047
|
+
SWAP_HANDLER_REJECT_CODES,
|
|
3048
|
+
SWAP_HANDLER_REJECT_MESSAGES,
|
|
3049
|
+
SettlementTxError,
|
|
3050
|
+
StreamSwapError,
|
|
3051
|
+
SwapHandlerError,
|
|
2072
3052
|
SwarmCoordinator,
|
|
2073
3053
|
TurboUploadAdapter,
|
|
2074
3054
|
VerificationError,
|
|
2075
3055
|
WorkflowOrchestrator,
|
|
3056
|
+
__testing as __streamSwapTesting,
|
|
3057
|
+
applyRate,
|
|
3058
|
+
balanceProofFieldsMina,
|
|
3059
|
+
balanceProofHashEvm,
|
|
3060
|
+
balanceProofHashSolana,
|
|
3061
|
+
base58Decode,
|
|
3062
|
+
base58Encode,
|
|
3063
|
+
bigintToBytes32BE,
|
|
3064
|
+
buildSettlementTx,
|
|
2076
3065
|
buildSkillDescriptor,
|
|
3066
|
+
concatBytes,
|
|
2077
3067
|
createArweaveDvmHandler,
|
|
2078
3068
|
createEventStorageHandler,
|
|
2079
3069
|
createHandlerContext,
|
|
@@ -2081,11 +3071,30 @@ export {
|
|
|
2081
3071
|
createPaymentHandlerBridge,
|
|
2082
3072
|
createPrefixClaimHandler,
|
|
2083
3073
|
createPricingValidator,
|
|
3074
|
+
createSwapHandler,
|
|
2084
3075
|
createVerificationPipeline,
|
|
3076
|
+
decryptFulfillClaim,
|
|
3077
|
+
encryptFulfillClaim,
|
|
3078
|
+
fillEvmSettlementTxGas,
|
|
3079
|
+
findSwapPair,
|
|
2085
3080
|
fromMnemonic,
|
|
3081
|
+
fromMnemonicFull,
|
|
2086
3082
|
fromSecretKey,
|
|
2087
3083
|
generateMnemonic,
|
|
3084
|
+
generateSolanaKeypair,
|
|
3085
|
+
hexToBytes2 as hexToBytes,
|
|
3086
|
+
loadMinaSignerClient,
|
|
3087
|
+
minaHashToField,
|
|
3088
|
+
streamSwap,
|
|
3089
|
+
streamSwapControlled,
|
|
3090
|
+
unwrapSwapPacket,
|
|
3091
|
+
unwrapSwapPacketFromToon,
|
|
2088
3092
|
uploadBlob,
|
|
2089
|
-
uploadBlobChunked
|
|
3093
|
+
uploadBlobChunked,
|
|
3094
|
+
verifyAccumulatedClaim,
|
|
3095
|
+
verifyEd25519Signature,
|
|
3096
|
+
verifyMinaSignature,
|
|
3097
|
+
wrapSwapPacket,
|
|
3098
|
+
wrapSwapPacketToToon
|
|
2090
3099
|
};
|
|
2091
3100
|
//# sourceMappingURL=index.js.map
|