@toon-protocol/sdk 2.0.1 → 2.1.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.
@@ -788,8 +788,20 @@ function createSwapHandler(config) {
788
788
  seenPacketIds.delete(packetId);
789
789
  };
790
790
  let rate;
791
+ let rateTimestamp;
791
792
  try {
792
- rate = config.rateProvider ? await config.rateProvider(pair) : pair.rate;
793
+ const provided = config.rateProvider ? await config.rateProvider(pair) : pair.rate;
794
+ if (typeof provided === "string") {
795
+ rate = provided;
796
+ rateTimestamp = Date.now();
797
+ } else if (provided !== null && typeof provided === "object" && typeof provided.rate === "string" && typeof provided.rateTimestamp === "number" && Number.isInteger(provided.rateTimestamp) && provided.rateTimestamp > 0) {
798
+ rate = provided.rate;
799
+ rateTimestamp = provided.rateTimestamp;
800
+ } else {
801
+ throw new SwapHandlerError(
802
+ "rateProvider must return a decimal-string rate or { rate, rateTimestamp }"
803
+ );
804
+ }
793
805
  } catch (err) {
794
806
  logger.error({
795
807
  event: "swap_handler.rate_provider_failed",
@@ -885,7 +897,9 @@ function createSwapHandler(config) {
885
897
  const metadata = {
886
898
  claim: claimBase64,
887
899
  ephemeralPubkey,
888
- targetAmount: targetAmount.toString()
900
+ targetAmount: targetAmount.toString(),
901
+ rate,
902
+ rateTimestamp
889
903
  };
890
904
  if (claimId !== void 0) metadata["claimId"] = claimId;
891
905
  if (settlementChannelId !== void 0 && settlementNonce !== void 0 && settlementCumulative !== void 0 && settlementRecipient !== void 0 && settlementSwapSigner !== void 0) {
@@ -992,7 +1006,7 @@ function validateChainAddress(value, chain, kind) {
992
1006
  }
993
1007
  return value.length > 0;
994
1008
  }
995
- function decodeFulfillMetadata(data, chain) {
1009
+ function decodeFulfillMetadata(data, chain, opts) {
996
1010
  if (data === void 0 || data === null || data === "") {
997
1011
  throw new StreamSwapError("FULFILL_DECODE_FAILED", "FULFILL data missing");
998
1012
  }
@@ -1060,6 +1074,37 @@ function decodeFulfillMetadata(data, chain) {
1060
1074
  }
1061
1075
  result.targetAmount = ta;
1062
1076
  }
1077
+ const tapeRate = obj["rate"];
1078
+ const tapeTimestamp = obj["rateTimestamp"];
1079
+ const hasRate = tapeRate !== void 0;
1080
+ const hasTimestamp = tapeTimestamp !== void 0;
1081
+ if (hasRate !== hasTimestamp) {
1082
+ throw new StreamSwapError(
1083
+ "FULFILL_DECODE_FAILED",
1084
+ "FULFILL metadata quote tape is malformed: rate and rateTimestamp must travel together"
1085
+ );
1086
+ }
1087
+ if (hasRate) {
1088
+ if (typeof tapeRate !== "string" || !RATE_REGEX2.test(tapeRate) || /^0(\.0+)?$/.test(tapeRate)) {
1089
+ throw new StreamSwapError(
1090
+ "FULFILL_DECODE_FAILED",
1091
+ "FULFILL metadata.rate must be a positive decimal string"
1092
+ );
1093
+ }
1094
+ if (typeof tapeTimestamp !== "number" || !Number.isInteger(tapeTimestamp) || tapeTimestamp <= 0) {
1095
+ throw new StreamSwapError(
1096
+ "FULFILL_DECODE_FAILED",
1097
+ "FULFILL metadata.rateTimestamp must be a positive integer (unix ms)"
1098
+ );
1099
+ }
1100
+ result.rate = tapeRate;
1101
+ result.rateTimestamp = tapeTimestamp;
1102
+ } else if (opts?.requireQuoteTape === true) {
1103
+ throw new StreamSwapError(
1104
+ "FULFILL_DECODE_FAILED",
1105
+ "FULFILL metadata is missing the quote tape (rate + rateTimestamp) required when minExchangeRate is set"
1106
+ );
1107
+ }
1063
1108
  const channelId = obj["channelId"];
1064
1109
  if (typeof channelId === "string" && (!chain || validateChainAddress(channelId, chain, "channelId"))) {
1065
1110
  result.channelId = channelId;
@@ -1082,6 +1127,18 @@ function decodeFulfillMetadata(data, chain) {
1082
1127
  }
1083
1128
  return result;
1084
1129
  }
1130
+ function compareDecimalRates(a, b) {
1131
+ const split = (s) => {
1132
+ const dot = s.indexOf(".");
1133
+ return dot === -1 ? { int: s, frac: "" } : { int: s.slice(0, dot), frac: s.slice(dot + 1) };
1134
+ };
1135
+ const pa = split(a);
1136
+ const pb = split(b);
1137
+ const scale = Math.max(pa.frac.length, pb.frac.length);
1138
+ const av = BigInt(pa.int + pa.frac.padEnd(scale, "0"));
1139
+ const bv = BigInt(pb.int + pb.frac.padEnd(scale, "0"));
1140
+ return av < bv ? -1 : av > bv ? 1 : 0;
1141
+ }
1085
1142
  var Deferred = class {
1086
1143
  promise;
1087
1144
  resolve;
@@ -1110,13 +1167,25 @@ function validateParams(params) {
1110
1167
  }
1111
1168
  const hasCount = params.packetCount !== void 0;
1112
1169
  const hasAmounts = params.packetAmounts !== void 0;
1113
- if (hasCount === hasAmounts) {
1170
+ const hasController = params.controller !== void 0;
1171
+ const chunkingModes = [hasCount, hasAmounts, hasController].filter(
1172
+ Boolean
1173
+ ).length;
1174
+ if (chunkingModes !== 1) {
1114
1175
  throw new StreamSwapError(
1115
1176
  "INVALID_CHUNKING",
1116
- "Exactly one of packetCount or packetAmounts must be provided"
1177
+ "Exactly one of packetCount, packetAmounts, or controller must be provided"
1117
1178
  );
1118
1179
  }
1119
- if (hasCount) {
1180
+ if (hasController) {
1181
+ const c = params.controller;
1182
+ if (typeof c.nextDelta !== "function" || typeof c.observe !== "function" || typeof c.window !== "number") {
1183
+ throw new StreamSwapError(
1184
+ "INVALID_STATE",
1185
+ "controller must implement nextDelta(remaining), observe(observation), and window"
1186
+ );
1187
+ }
1188
+ } else if (hasCount) {
1120
1189
  const c = params.packetCount;
1121
1190
  if (!Number.isInteger(c) || c <= 0) {
1122
1191
  throw new StreamSwapError(
@@ -1208,6 +1277,22 @@ function validateParams(params) {
1208
1277
  `rateDeviationThreshold must be a non-negative finite number, got ${params.rateDeviationThreshold}`
1209
1278
  );
1210
1279
  }
1280
+ if (params.minExchangeRate !== void 0) {
1281
+ if (typeof params.minExchangeRate !== "string" || !RATE_REGEX2.test(params.minExchangeRate) || /^0(\.0+)?$/.test(params.minExchangeRate)) {
1282
+ throw new StreamSwapError(
1283
+ "INVALID_STATE",
1284
+ `minExchangeRate must be a positive decimal string matching ${RATE_REGEX2}, got ${String(
1285
+ params.minExchangeRate
1286
+ )}`
1287
+ );
1288
+ }
1289
+ }
1290
+ if (params.packetExpiryMs !== void 0 && (typeof params.packetExpiryMs !== "number" || !Number.isInteger(params.packetExpiryMs) || params.packetExpiryMs <= 0)) {
1291
+ throw new StreamSwapError(
1292
+ "INVALID_STATE",
1293
+ `packetExpiryMs must be a positive integer (ms), got ${params.packetExpiryMs}`
1294
+ );
1295
+ }
1211
1296
  if (typeof params.chainRecipient !== "string" || params.chainRecipient.length === 0) {
1212
1297
  throw new StreamSwapError(
1213
1298
  "INVALID_STATE",
@@ -1231,7 +1316,7 @@ async function streamSwap(params) {
1231
1316
  function streamSwapControlled(params) {
1232
1317
  validateParams(params);
1233
1318
  const logger = params.logger ?? NOOP_LOGGER2;
1234
- const schedule = params.packetAmounts ? [...params.packetAmounts] : chunkAmount(params.totalAmount, params.packetCount);
1319
+ const schedule = params.packetAmounts ? [...params.packetAmounts] : params.packetCount !== void 0 ? chunkAmount(params.totalAmount, params.packetCount) : [];
1235
1320
  const frozenPair = Object.freeze({
1236
1321
  from: Object.freeze({ ...params.pair.from }),
1237
1322
  to: Object.freeze({ ...params.pair.to }),
@@ -1274,30 +1359,305 @@ function streamSwapControlled(params) {
1274
1359
  return streamState;
1275
1360
  }
1276
1361
  };
1277
- const result = runLoop(
1362
+ const getState = () => streamState;
1363
+ const setState = (v) => {
1364
+ streamState = v;
1365
+ };
1366
+ const waitForResumeOrStop = () => {
1367
+ if (streamState !== "paused")
1368
+ return Promise.resolve("resume");
1369
+ if (!resumeDeferred) resumeDeferred = new Deferred();
1370
+ return resumeDeferred.promise;
1371
+ };
1372
+ const result = params.controller ? runAdaptiveLoop(
1373
+ params,
1374
+ frozenPair,
1375
+ senderPubkey,
1376
+ logger,
1377
+ getState,
1378
+ setState,
1379
+ waitForResumeOrStop
1380
+ ) : runLoop(
1278
1381
  params,
1279
1382
  frozenPair,
1280
1383
  schedule,
1281
1384
  senderPubkey,
1282
1385
  logger,
1283
- () => streamState,
1284
- (v) => {
1285
- streamState = v;
1286
- },
1287
- () => {
1288
- if (streamState !== "paused") return Promise.resolve("resume");
1289
- if (!resumeDeferred) resumeDeferred = new Deferred();
1290
- return resumeDeferred.promise;
1291
- }
1386
+ getState,
1387
+ setState,
1388
+ waitForResumeOrStop
1292
1389
  );
1293
1390
  return { result, controller };
1294
1391
  }
1392
+ function buildAndWrapPacket(input) {
1393
+ const { params, pair, senderPubkey, sourceAmount, seq, totalPackets } = input;
1394
+ const nonce = new Uint8Array(16);
1395
+ getRandomValues(nonce);
1396
+ const rumor = buildSwapRumor({
1397
+ senderPubkey,
1398
+ pair,
1399
+ sourceAmount,
1400
+ packetIndex: seq,
1401
+ totalPackets,
1402
+ nonce,
1403
+ createdAt: Math.floor(Date.now() / 1e3),
1404
+ chainRecipient: params.chainRecipient
1405
+ });
1406
+ const packetExpiresAt = params.packetExpiryMs !== void 0 ? new Date(Date.now() + params.packetExpiryMs) : void 0;
1407
+ const wrapped = wrapSwapPacketToToon({
1408
+ rumor,
1409
+ senderSecretKey: params.senderSecretKey,
1410
+ recipientPubkey: params.swapPubkey,
1411
+ destination: params.swapIlpAddress,
1412
+ amount: sourceAmount,
1413
+ ...packetExpiresAt !== void 0 && { expiresAt: packetExpiresAt }
1414
+ });
1415
+ const toonData = new Uint8Array(
1416
+ Buffer.from(wrapped.ilpPrepare.data, "base64")
1417
+ );
1418
+ return {
1419
+ toonData,
1420
+ ...packetExpiresAt !== void 0 && { packetExpiresAt }
1421
+ };
1422
+ }
1423
+ async function processAcceptedPacket(ctx, args) {
1424
+ const { params, pair, logger } = ctx;
1425
+ const { packetIndex, totalForProgress, sourceAmount, data } = args;
1426
+ let metadata;
1427
+ try {
1428
+ metadata = decodeFulfillMetadata(data, pair.to.chain, {
1429
+ requireQuoteTape: params.minExchangeRate !== void 0
1430
+ });
1431
+ } catch (err) {
1432
+ logger.error({
1433
+ event: "stream_swap.fulfill_decode_failed",
1434
+ packetIndex,
1435
+ error: err instanceof Error ? err.message : String(err)
1436
+ });
1437
+ ctx.errors.push({
1438
+ packetIndex,
1439
+ cause: err instanceof Error ? err : new Error(String(err))
1440
+ });
1441
+ return { status: "error" };
1442
+ }
1443
+ const isEvmTarget = pair.to.chain.startsWith("evm:");
1444
+ const recipientMatches = metadata.recipient === void 0 || (isEvmTarget ? metadata.recipient.toLowerCase() === params.chainRecipient.toLowerCase() : metadata.recipient === params.chainRecipient);
1445
+ if (!recipientMatches) {
1446
+ logger.warn({
1447
+ event: "stream_swap.recipient_mismatch",
1448
+ packetIndex,
1449
+ expected: params.chainRecipient,
1450
+ actual: metadata.recipient
1451
+ });
1452
+ ctx.rejections.push({
1453
+ packetIndex,
1454
+ sourceAmount,
1455
+ code: "SWAP_RECIPIENT_MISMATCH",
1456
+ message: `Swap echoed recipient ${metadata.recipient} but sender expected ${params.chainRecipient}`
1457
+ });
1458
+ return { status: "recipient-mismatch" };
1459
+ }
1460
+ if (params.minExchangeRate !== void 0) {
1461
+ const tapeRate = metadata.rate;
1462
+ const floorTargetAmount = applyRate({
1463
+ sourceAmount,
1464
+ fromScale: pair.from.assetScale,
1465
+ toScale: pair.to.assetScale,
1466
+ rate: params.minExchangeRate
1467
+ });
1468
+ const deliveredTargetAmount = metadata.targetAmount !== void 0 ? BigInt(metadata.targetAmount) : applyRate({
1469
+ sourceAmount,
1470
+ fromScale: pair.from.assetScale,
1471
+ toScale: pair.to.assetScale,
1472
+ rate: tapeRate
1473
+ });
1474
+ const tapeBelowFloor = compareDecimalRates(tapeRate, params.minExchangeRate) < 0;
1475
+ if (tapeBelowFloor || deliveredTargetAmount < floorTargetAmount) {
1476
+ logger.warn({
1477
+ event: "stream_swap.below_floor",
1478
+ packetIndex,
1479
+ rate: tapeRate,
1480
+ rateTimestamp: metadata.rateTimestamp,
1481
+ minExchangeRate: params.minExchangeRate,
1482
+ targetAmount: deliveredTargetAmount.toString(),
1483
+ floorTargetAmount: floorTargetAmount.toString()
1484
+ });
1485
+ ctx.rejections.push({
1486
+ packetIndex,
1487
+ sourceAmount,
1488
+ code: "BELOW_FLOOR",
1489
+ message: `Packet fill below minExchangeRate floor: rate ${tapeRate}, targetAmount ${deliveredTargetAmount} < floor ${params.minExchangeRate} (${floorTargetAmount})`
1490
+ });
1491
+ return { status: "below-floor" };
1492
+ }
1493
+ }
1494
+ let claimBytes;
1495
+ try {
1496
+ const ciphertext = new Uint8Array(Buffer.from(metadata.claim, "base64"));
1497
+ claimBytes = decryptFulfillClaim({
1498
+ ciphertext,
1499
+ ephemeralPubkey: metadata.ephemeralPubkey,
1500
+ recipientSecretKey: params.senderSecretKey
1501
+ });
1502
+ } catch (err) {
1503
+ logger.error({
1504
+ event: "stream_swap.decrypt_failed",
1505
+ packetIndex,
1506
+ error: err instanceof Error ? err.message : String(err)
1507
+ });
1508
+ ctx.errors.push({
1509
+ packetIndex,
1510
+ cause: err instanceof Error ? err : new Error(String(err))
1511
+ });
1512
+ return { status: "error" };
1513
+ }
1514
+ if (claimBytes.length === 0) {
1515
+ logger.warn({
1516
+ event: "stream_swap.empty_claim_bytes",
1517
+ packetIndex
1518
+ });
1519
+ }
1520
+ const expectedTargetAmount = applyRate({
1521
+ sourceAmount,
1522
+ fromScale: pair.from.assetScale,
1523
+ toScale: pair.to.assetScale,
1524
+ rate: pair.rate
1525
+ });
1526
+ const targetAmount = metadata.targetAmount !== void 0 ? BigInt(metadata.targetAmount) : expectedTargetAmount;
1527
+ let rateDeviation = 0;
1528
+ if (expectedTargetAmount > 0n) {
1529
+ const diff = targetAmount >= expectedTargetAmount ? targetAmount - expectedTargetAmount : expectedTargetAmount - targetAmount;
1530
+ const scaled = diff * 1000000n / expectedTargetAmount;
1531
+ rateDeviation = Number(scaled) / 1e6;
1532
+ }
1533
+ const advertisedRate = parseFloat(pair.rate);
1534
+ let effectiveRate;
1535
+ if (targetAmount === expectedTargetAmount) {
1536
+ effectiveRate = advertisedRate;
1537
+ } else {
1538
+ const signedDeviation = targetAmount >= expectedTargetAmount ? rateDeviation : -rateDeviation;
1539
+ effectiveRate = advertisedRate * (1 + signedDeviation);
1540
+ }
1541
+ if (!Number.isFinite(effectiveRate)) {
1542
+ effectiveRate = advertisedRate;
1543
+ }
1544
+ ctx.cumulative.source += sourceAmount;
1545
+ ctx.cumulative.target += targetAmount;
1546
+ const accumulated = {
1547
+ packetIndex,
1548
+ sourceAmount,
1549
+ targetAmount,
1550
+ claimBytes,
1551
+ swapEphemeralPubkey: metadata.ephemeralPubkey,
1552
+ pair,
1553
+ receivedAt: Date.now()
1554
+ };
1555
+ if (metadata.claimId !== void 0) accumulated.claimId = metadata.claimId;
1556
+ if (metadata.channelId !== void 0)
1557
+ accumulated.channelId = metadata.channelId;
1558
+ if (metadata.nonce !== void 0) accumulated.nonce = metadata.nonce;
1559
+ if (metadata.cumulativeAmount !== void 0)
1560
+ accumulated.cumulativeAmount = metadata.cumulativeAmount;
1561
+ if (metadata.recipient !== void 0)
1562
+ accumulated.recipient = metadata.recipient;
1563
+ if (metadata.swapSignerAddress !== void 0)
1564
+ accumulated.swapSignerAddress = metadata.swapSignerAddress;
1565
+ if (metadata.rate !== void 0) {
1566
+ accumulated.rate = metadata.rate;
1567
+ accumulated.rateTimestamp = metadata.rateTimestamp;
1568
+ }
1569
+ ctx.claims.push(accumulated);
1570
+ logger.debug({
1571
+ event: "stream_swap.packet_accepted",
1572
+ packetIndex,
1573
+ sourceAmount: sourceAmount.toString(),
1574
+ targetAmount: targetAmount.toString()
1575
+ });
1576
+ if (params.onPacket) {
1577
+ const progress = Object.freeze({
1578
+ index: packetIndex,
1579
+ total: totalForProgress,
1580
+ sourceAmount,
1581
+ targetAmount,
1582
+ advertisedRate: pair.rate,
1583
+ effectiveRate,
1584
+ rateDeviation,
1585
+ cumulativeSource: ctx.cumulative.source,
1586
+ cumulativeTarget: ctx.cumulative.target,
1587
+ // Issue #82 quote tape: surface the maker's fresh per-packet quote
1588
+ // to the callback (the adaptive-controller seam, toon#83).
1589
+ ...metadata.rate !== void 0 && {
1590
+ rate: metadata.rate,
1591
+ rateTimestamp: metadata.rateTimestamp
1592
+ },
1593
+ state: ctx.getState() === "paused" ? "paused" : ctx.getState() === "stopped" ? "stopped" : "running"
1594
+ });
1595
+ try {
1596
+ const maybePromise = params.onPacket(progress);
1597
+ if (maybePromise && typeof maybePromise.then === "function") {
1598
+ await maybePromise;
1599
+ }
1600
+ } catch (err) {
1601
+ logger.warn({
1602
+ event: "stream_swap.callback_threw",
1603
+ packetIndex,
1604
+ error: err instanceof Error ? err.message : String(err)
1605
+ });
1606
+ ctx.errors.push({
1607
+ packetIndex,
1608
+ cause: err instanceof Error ? err : new Error(String(err))
1609
+ });
1610
+ return { status: "callback-throw" };
1611
+ }
1612
+ }
1613
+ return {
1614
+ status: "accepted",
1615
+ targetAmount,
1616
+ rateDeviation,
1617
+ ...metadata.rate !== void 0 && {
1618
+ rate: metadata.rate,
1619
+ rateTimestamp: metadata.rateTimestamp
1620
+ }
1621
+ };
1622
+ }
1623
+ function finalizeResult(input) {
1624
+ const { claims, rejections, errors, cumulative } = input.ctx;
1625
+ let { abortReason } = input;
1626
+ let finalState;
1627
+ if (abortReason === "aborted" || abortReason === "stopped") {
1628
+ finalState = "stopped";
1629
+ } else if (claims.length === 0 && (rejections.length > 0 || errors.length > 0)) {
1630
+ finalState = "failed";
1631
+ if (abortReason === "complete" && rejections.length > 0 && errors.length === 0) {
1632
+ abortReason = "all-rejected";
1633
+ }
1634
+ } else {
1635
+ finalState = "completed";
1636
+ }
1637
+ input.setState(finalState);
1638
+ return {
1639
+ state: finalState,
1640
+ claims,
1641
+ rejections,
1642
+ errors,
1643
+ abortReason,
1644
+ cumulativeSource: cumulative.source,
1645
+ cumulativeTarget: cumulative.target,
1646
+ packetsSent: input.packetsSent,
1647
+ packetsScheduled: input.packetsScheduled
1648
+ };
1649
+ }
1295
1650
  async function runLoop(params, pair, schedule, senderPubkey, logger, getState, setState, waitForResumeOrStop) {
1296
- const claims = [];
1297
- const rejections = [];
1298
- const errors = [];
1299
- let cumulativeSource = 0n;
1300
- let cumulativeTarget = 0n;
1651
+ const ctx = {
1652
+ params,
1653
+ pair,
1654
+ logger,
1655
+ getState,
1656
+ claims: [],
1657
+ rejections: [],
1658
+ errors: [],
1659
+ cumulative: { source: 0n, target: 0n }
1660
+ };
1301
1661
  let packetsSent = 0;
1302
1662
  let abortReason = "complete";
1303
1663
  const totalPackets = schedule.length;
@@ -1329,40 +1689,29 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
1329
1689
  `schedule[${packetIndex}] is undefined; schedule was mutated mid-stream`
1330
1690
  );
1331
1691
  }
1332
- const nonce = new Uint8Array(16);
1333
- getRandomValues(nonce);
1334
- const rumor = buildSwapRumor({
1335
- senderPubkey,
1336
- pair,
1337
- sourceAmount,
1338
- packetIndex: packetIndex + 1,
1339
- totalPackets,
1340
- nonce,
1341
- createdAt: Math.floor(Date.now() / 1e3),
1342
- chainRecipient: params.chainRecipient
1343
- });
1344
- let toonData;
1692
+ let built;
1345
1693
  try {
1346
- const wrapped = wrapSwapPacketToToon({
1347
- rumor,
1348
- senderSecretKey: params.senderSecretKey,
1349
- recipientPubkey: params.swapPubkey,
1350
- destination: params.swapIlpAddress,
1351
- amount: sourceAmount
1694
+ built = buildAndWrapPacket({
1695
+ params,
1696
+ pair,
1697
+ senderPubkey,
1698
+ sourceAmount,
1699
+ seq: packetIndex + 1,
1700
+ totalPackets
1352
1701
  });
1353
- toonData = new Uint8Array(Buffer.from(wrapped.ilpPrepare.data, "base64"));
1354
1702
  } catch (err) {
1355
1703
  logger.error({
1356
1704
  event: "stream_swap.wrap_failed",
1357
1705
  packetIndex,
1358
1706
  error: err instanceof Error ? err.message : String(err)
1359
1707
  });
1360
- errors.push({
1708
+ ctx.errors.push({
1361
1709
  packetIndex,
1362
1710
  cause: err instanceof Error ? err : new Error(String(err))
1363
1711
  });
1364
1712
  continue;
1365
1713
  }
1714
+ const { toonData, packetExpiresAt } = built;
1366
1715
  let sendResult;
1367
1716
  try {
1368
1717
  sendResult = await params.client.sendSwapPacket({
@@ -1370,7 +1719,8 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
1370
1719
  amount: sourceAmount,
1371
1720
  toonData,
1372
1721
  timeout: params.packetTimeoutMs ?? 3e4,
1373
- claim: params.claim
1722
+ claim: params.claim,
1723
+ ...packetExpiresAt !== void 0 && { expiresAt: packetExpiresAt }
1374
1724
  });
1375
1725
  packetsSent += 1;
1376
1726
  } catch (err) {
@@ -1379,7 +1729,7 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
1379
1729
  packetIndex,
1380
1730
  error: err instanceof Error ? err.message : String(err)
1381
1731
  });
1382
- errors.push({
1732
+ ctx.errors.push({
1383
1733
  packetIndex,
1384
1734
  cause: err instanceof Error ? err : new Error(String(err))
1385
1735
  });
@@ -1394,187 +1744,290 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
1394
1744
  code,
1395
1745
  message
1396
1746
  });
1397
- rejections.push({ packetIndex, sourceAmount, code, message });
1747
+ ctx.rejections.push({ packetIndex, sourceAmount, code, message });
1748
+ continue;
1749
+ }
1750
+ const outcome = await processAcceptedPacket(ctx, {
1751
+ packetIndex,
1752
+ totalForProgress: totalPackets,
1753
+ sourceAmount,
1754
+ data: sendResult.data
1755
+ });
1756
+ if (outcome.status === "error" || outcome.status === "recipient-mismatch") {
1398
1757
  continue;
1399
1758
  }
1400
- let metadata;
1759
+ if (outcome.status === "below-floor") {
1760
+ abortReason = "below-floor";
1761
+ break packetLoop;
1762
+ }
1763
+ if (outcome.status === "callback-throw") {
1764
+ abortReason = "callback-throw";
1765
+ break packetLoop;
1766
+ }
1767
+ if (isAborted()) {
1768
+ abortReason = "aborted";
1769
+ break;
1770
+ }
1771
+ if (getState() === "stopped") {
1772
+ abortReason = "stopped";
1773
+ break;
1774
+ }
1775
+ if (params.rateDeviationThreshold !== void 0 && outcome.rateDeviation > params.rateDeviationThreshold) {
1776
+ abortReason = "rate-deviation";
1777
+ break;
1778
+ }
1779
+ }
1780
+ return finalizeResult({
1781
+ ctx,
1782
+ abortReason,
1783
+ packetsSent,
1784
+ packetsScheduled: totalPackets,
1785
+ setState
1786
+ });
1787
+ }
1788
+ function classifySendError(err) {
1789
+ return /timeout|timed out|expire/i.test(err.message) ? "timeout" : "error";
1790
+ }
1791
+ function classifyReject(code, message) {
1792
+ if (code === "T99" || /stale[_-]?rate/i.test(message)) return "reject-stale";
1793
+ if (code.startsWith("R") || /timeout|timed out|expire/i.test(message)) {
1794
+ return "timeout";
1795
+ }
1796
+ return "reject";
1797
+ }
1798
+ async function runAdaptiveLoop(params, pair, senderPubkey, logger, getState, setState, waitForResumeOrStop) {
1799
+ const controller = params.controller;
1800
+ const ctx = {
1801
+ params,
1802
+ pair,
1803
+ logger,
1804
+ getState,
1805
+ claims: [],
1806
+ rejections: [],
1807
+ errors: [],
1808
+ cumulative: { source: 0n, target: 0n }
1809
+ };
1810
+ let packetsSent = 0;
1811
+ let abortReason = "complete";
1812
+ let halted = false;
1813
+ let remaining = params.totalAmount;
1814
+ let nextIndex = 0;
1815
+ const isAborted = () => params.signal?.aborted === true;
1816
+ const halt = (reason) => {
1817
+ if (!halted) {
1818
+ halted = true;
1819
+ abortReason = reason;
1820
+ }
1821
+ };
1822
+ const observeSafe = async (obs) => {
1401
1823
  try {
1402
- metadata = decodeFulfillMetadata(sendResult.data, pair.to.chain);
1824
+ await controller.observe(obs);
1403
1825
  } catch (err) {
1404
- logger.error({
1405
- event: "stream_swap.fulfill_decode_failed",
1406
- packetIndex,
1826
+ logger.warn({
1827
+ event: "stream_swap.controller_observe_failed",
1407
1828
  error: err instanceof Error ? err.message : String(err)
1408
1829
  });
1409
- errors.push({
1410
- packetIndex,
1411
- cause: err instanceof Error ? err : new Error(String(err))
1412
- });
1413
- continue;
1414
1830
  }
1415
- const isEvmTarget = pair.to.chain.startsWith("evm:");
1416
- const recipientMatches = metadata.recipient === void 0 || (isEvmTarget ? metadata.recipient.toLowerCase() === params.chainRecipient.toLowerCase() : metadata.recipient === params.chainRecipient);
1417
- if (!recipientMatches) {
1418
- logger.warn({
1419
- event: "stream_swap.recipient_mismatch",
1420
- packetIndex,
1421
- expected: params.chainRecipient,
1422
- actual: metadata.recipient
1423
- });
1424
- rejections.push({
1425
- packetIndex,
1426
- sourceAmount,
1427
- code: "SWAP_RECIPIENT_MISMATCH",
1428
- message: `Swap echoed recipient ${metadata.recipient} but sender expected ${params.chainRecipient}`
1429
- });
1430
- continue;
1831
+ };
1832
+ const inflight = /* @__PURE__ */ new Map();
1833
+ for (; ; ) {
1834
+ if (!halted && isAborted()) halt("aborted");
1835
+ if (!halted && getState() === "stopped") halt("stopped");
1836
+ if (!halted && getState() === "paused" && inflight.size === 0) {
1837
+ const resumedBy = await waitForResumeOrStop();
1838
+ if (resumedBy === "stop" || getState() === "stopped") halt("stopped");
1839
+ else if (isAborted()) halt("aborted");
1840
+ }
1841
+ if (!halted && getState() === "running") {
1842
+ const rawWindow = controller.window;
1843
+ const window = Number.isFinite(rawWindow) && rawWindow >= 1 ? Math.floor(rawWindow) : 1;
1844
+ while (remaining > 0n && inflight.size < window) {
1845
+ let delta;
1846
+ try {
1847
+ delta = controller.nextDelta(remaining);
1848
+ } catch (err) {
1849
+ logger.error({
1850
+ event: "stream_swap.controller_next_delta_failed",
1851
+ error: err instanceof Error ? err.message : String(err)
1852
+ });
1853
+ ctx.errors.push({
1854
+ packetIndex: nextIndex,
1855
+ cause: err instanceof Error ? err : new Error(String(err))
1856
+ });
1857
+ halt("callback-throw");
1858
+ break;
1859
+ }
1860
+ if (typeof delta !== "bigint" || delta < 1n) delta = 1n;
1861
+ if (delta > remaining) delta = remaining;
1862
+ const packetIndex = nextIndex;
1863
+ nextIndex += 1;
1864
+ remaining -= delta;
1865
+ let built;
1866
+ try {
1867
+ built = buildAndWrapPacket({
1868
+ params,
1869
+ pair,
1870
+ senderPubkey,
1871
+ sourceAmount: delta,
1872
+ seq: packetIndex + 1,
1873
+ // Adaptive mode: the final packet count is unknown upfront —
1874
+ // `0` in the rumor's `seq` tag total position means "open".
1875
+ totalPackets: 0
1876
+ });
1877
+ } catch (err) {
1878
+ logger.error({
1879
+ event: "stream_swap.wrap_failed",
1880
+ packetIndex,
1881
+ error: err instanceof Error ? err.message : String(err)
1882
+ });
1883
+ ctx.errors.push({
1884
+ packetIndex,
1885
+ cause: err instanceof Error ? err : new Error(String(err))
1886
+ });
1887
+ continue;
1888
+ }
1889
+ const { toonData, packetExpiresAt } = built;
1890
+ const sentAt = Date.now();
1891
+ const promise = (async () => {
1892
+ try {
1893
+ const result = await params.client.sendSwapPacket({
1894
+ destination: params.swapIlpAddress,
1895
+ amount: delta,
1896
+ toonData,
1897
+ timeout: params.packetTimeoutMs ?? 3e4,
1898
+ claim: params.claim,
1899
+ ...packetExpiresAt !== void 0 && {
1900
+ expiresAt: packetExpiresAt
1901
+ }
1902
+ });
1903
+ return {
1904
+ index: packetIndex,
1905
+ sourceAmount: delta,
1906
+ rttMs: Date.now() - sentAt,
1907
+ result
1908
+ };
1909
+ } catch (err) {
1910
+ return {
1911
+ index: packetIndex,
1912
+ sourceAmount: delta,
1913
+ rttMs: Date.now() - sentAt,
1914
+ error: err instanceof Error ? err : new Error(String(err))
1915
+ };
1916
+ }
1917
+ })();
1918
+ inflight.set(packetIndex, promise);
1919
+ }
1431
1920
  }
1432
- let claimBytes;
1433
- try {
1434
- const ciphertext = new Uint8Array(Buffer.from(metadata.claim, "base64"));
1435
- claimBytes = decryptFulfillClaim({
1436
- ciphertext,
1437
- ephemeralPubkey: metadata.ephemeralPubkey,
1438
- recipientSecretKey: params.senderSecretKey
1439
- });
1440
- } catch (err) {
1921
+ if (inflight.size === 0) {
1922
+ if (halted || remaining <= 0n) break;
1923
+ if (getState() === "paused") continue;
1924
+ break;
1925
+ }
1926
+ const settled = await Promise.race(inflight.values());
1927
+ inflight.delete(settled.index);
1928
+ if (settled.error) {
1441
1929
  logger.error({
1442
- event: "stream_swap.decrypt_failed",
1443
- packetIndex,
1444
- error: err instanceof Error ? err.message : String(err)
1930
+ event: "stream_swap.send_failed",
1931
+ packetIndex: settled.index,
1932
+ error: settled.error.message
1445
1933
  });
1446
- errors.push({
1447
- packetIndex,
1448
- cause: err instanceof Error ? err : new Error(String(err))
1934
+ ctx.errors.push({ packetIndex: settled.index, cause: settled.error });
1935
+ await observeSafe({
1936
+ resolution: classifySendError(settled.error),
1937
+ rttMs: settled.rttMs,
1938
+ remaining
1449
1939
  });
1450
1940
  continue;
1451
1941
  }
1452
- if (claimBytes.length === 0) {
1942
+ packetsSent += 1;
1943
+ const sendResult = settled.result;
1944
+ if (!sendResult.accepted) {
1945
+ const code = sendResult.code ?? "F00";
1946
+ const message = sendResult.message ?? "rejected";
1453
1947
  logger.warn({
1454
- event: "stream_swap.empty_claim_bytes",
1455
- packetIndex
1948
+ event: "stream_swap.packet_rejected",
1949
+ packetIndex: settled.index,
1950
+ code,
1951
+ message
1456
1952
  });
1953
+ ctx.rejections.push({
1954
+ packetIndex: settled.index,
1955
+ sourceAmount: settled.sourceAmount,
1956
+ code,
1957
+ message
1958
+ });
1959
+ await observeSafe({
1960
+ resolution: classifyReject(code, message),
1961
+ rttMs: settled.rttMs,
1962
+ remaining
1963
+ });
1964
+ continue;
1457
1965
  }
1458
- const expectedTargetAmount = applyRate({
1459
- sourceAmount,
1460
- fromScale: pair.from.assetScale,
1461
- toScale: pair.to.assetScale,
1462
- rate: pair.rate
1463
- });
1464
- const targetAmount = metadata.targetAmount !== void 0 ? BigInt(metadata.targetAmount) : expectedTargetAmount;
1465
- let rateDeviation = 0;
1466
- if (expectedTargetAmount > 0n) {
1467
- const diff = targetAmount >= expectedTargetAmount ? targetAmount - expectedTargetAmount : expectedTargetAmount - targetAmount;
1468
- const scaled = diff * 1000000n / expectedTargetAmount;
1469
- rateDeviation = Number(scaled) / 1e6;
1470
- }
1471
- const advertisedRate = parseFloat(pair.rate);
1472
- let effectiveRate;
1473
- if (targetAmount === expectedTargetAmount) {
1474
- effectiveRate = advertisedRate;
1475
- } else {
1476
- const signedDeviation = targetAmount >= expectedTargetAmount ? rateDeviation : -rateDeviation;
1477
- effectiveRate = advertisedRate * (1 + signedDeviation);
1478
- }
1479
- if (!Number.isFinite(effectiveRate)) {
1480
- effectiveRate = advertisedRate;
1481
- }
1482
- cumulativeSource += sourceAmount;
1483
- cumulativeTarget += targetAmount;
1484
- const accumulated = {
1485
- packetIndex,
1486
- sourceAmount,
1487
- targetAmount,
1488
- claimBytes,
1489
- swapEphemeralPubkey: metadata.ephemeralPubkey,
1490
- pair,
1491
- receivedAt: Date.now()
1492
- };
1493
- if (metadata.claimId !== void 0) accumulated.claimId = metadata.claimId;
1494
- if (metadata.channelId !== void 0)
1495
- accumulated.channelId = metadata.channelId;
1496
- if (metadata.nonce !== void 0) accumulated.nonce = metadata.nonce;
1497
- if (metadata.cumulativeAmount !== void 0)
1498
- accumulated.cumulativeAmount = metadata.cumulativeAmount;
1499
- if (metadata.recipient !== void 0)
1500
- accumulated.recipient = metadata.recipient;
1501
- if (metadata.swapSignerAddress !== void 0)
1502
- accumulated.swapSignerAddress = metadata.swapSignerAddress;
1503
- claims.push(accumulated);
1504
- logger.debug({
1505
- event: "stream_swap.packet_accepted",
1506
- packetIndex,
1507
- sourceAmount: sourceAmount.toString(),
1508
- targetAmount: targetAmount.toString()
1966
+ const outcome = await processAcceptedPacket(ctx, {
1967
+ packetIndex: settled.index,
1968
+ totalForProgress: nextIndex,
1969
+ sourceAmount: settled.sourceAmount,
1970
+ data: sendResult.data
1509
1971
  });
1510
- if (params.onPacket) {
1511
- const progress = Object.freeze({
1512
- index: packetIndex,
1513
- total: totalPackets,
1514
- sourceAmount,
1515
- targetAmount,
1516
- advertisedRate: pair.rate,
1517
- effectiveRate,
1518
- rateDeviation,
1519
- cumulativeSource,
1520
- cumulativeTarget,
1521
- state: getState() === "paused" ? "paused" : getState() === "stopped" ? "stopped" : "running"
1522
- });
1523
- try {
1524
- const maybePromise = params.onPacket(progress);
1525
- if (maybePromise && typeof maybePromise.then === "function") {
1526
- await maybePromise;
1527
- }
1528
- } catch (err) {
1529
- logger.warn({
1530
- event: "stream_swap.callback_threw",
1531
- packetIndex,
1532
- error: err instanceof Error ? err.message : String(err)
1972
+ switch (outcome.status) {
1973
+ case "error":
1974
+ await observeSafe({
1975
+ resolution: "error",
1976
+ rttMs: settled.rttMs,
1977
+ remaining
1978
+ });
1979
+ break;
1980
+ case "recipient-mismatch":
1981
+ await observeSafe({
1982
+ resolution: "reject",
1983
+ rttMs: settled.rttMs,
1984
+ remaining
1985
+ });
1986
+ break;
1987
+ case "below-floor":
1988
+ await observeSafe({
1989
+ resolution: "reject",
1990
+ rttMs: settled.rttMs,
1991
+ remaining
1533
1992
  });
1534
- errors.push({
1535
- packetIndex,
1536
- cause: err instanceof Error ? err : new Error(String(err))
1993
+ halt("below-floor");
1994
+ break;
1995
+ case "callback-throw":
1996
+ await observeSafe({
1997
+ resolution: "fulfill",
1998
+ rttMs: settled.rttMs,
1999
+ remaining
1537
2000
  });
1538
- abortReason = "callback-throw";
1539
- break packetLoop;
2001
+ halt("callback-throw");
2002
+ break;
2003
+ case "accepted": {
2004
+ await observeSafe({
2005
+ resolution: "fulfill",
2006
+ rttMs: settled.rttMs,
2007
+ remaining,
2008
+ sourceAmount: settled.sourceAmount,
2009
+ targetAmount: outcome.targetAmount,
2010
+ ...outcome.rate !== void 0 && {
2011
+ rate: outcome.rate,
2012
+ rateTimestamp: outcome.rateTimestamp
2013
+ }
2014
+ });
2015
+ if (isAborted()) halt("aborted");
2016
+ else if (getState() === "stopped") halt("stopped");
2017
+ else if (params.rateDeviationThreshold !== void 0 && outcome.rateDeviation > params.rateDeviationThreshold) {
2018
+ halt("rate-deviation");
2019
+ }
2020
+ break;
1540
2021
  }
1541
2022
  }
1542
- if (isAborted()) {
1543
- abortReason = "aborted";
1544
- break;
1545
- }
1546
- if (getState() === "stopped") {
1547
- abortReason = "stopped";
1548
- break;
1549
- }
1550
- if (params.rateDeviationThreshold !== void 0 && rateDeviation > params.rateDeviationThreshold) {
1551
- abortReason = "rate-deviation";
1552
- break;
1553
- }
1554
2023
  }
1555
- let finalState;
1556
- if (abortReason === "aborted" || abortReason === "stopped") {
1557
- finalState = "stopped";
1558
- } else if (claims.length === 0 && (rejections.length > 0 || errors.length > 0)) {
1559
- finalState = "failed";
1560
- if (abortReason === "complete" && rejections.length > 0 && errors.length === 0) {
1561
- abortReason = "all-rejected";
1562
- }
1563
- } else {
1564
- finalState = "completed";
1565
- }
1566
- setState(finalState);
1567
- return {
1568
- state: finalState,
1569
- claims,
1570
- rejections,
1571
- errors,
2024
+ return finalizeResult({
2025
+ ctx,
1572
2026
  abortReason,
1573
- cumulativeSource,
1574
- cumulativeTarget,
1575
2027
  packetsSent,
1576
- packetsScheduled: totalPackets
1577
- };
2028
+ packetsScheduled: nextIndex,
2029
+ setState
2030
+ });
1578
2031
  }
1579
2032
  function getRandomValues(buf) {
1580
2033
  const g = globalThis;
@@ -1599,7 +2052,8 @@ function getRandomValues(buf) {
1599
2052
  var __testing = {
1600
2053
  chunkAmount,
1601
2054
  decodeFulfillMetadata,
1602
- buildSwapRumor
2055
+ buildSwapRumor,
2056
+ compareDecimalRates
1603
2057
  };
1604
2058
 
1605
2059
  export {
@@ -1634,4 +2088,4 @@ export {
1634
2088
  streamSwapControlled,
1635
2089
  __testing
1636
2090
  };
1637
- //# sourceMappingURL=chunk-Z6S56VPV.js.map
2091
+ //# sourceMappingURL=chunk-7LBYFU4L.js.map