@toon-protocol/sdk 2.0.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-Z6S56VPV.js → chunk-RAZRWSJF.js} +1139 -225
- package/dist/chunk-RAZRWSJF.js.map +1 -0
- package/dist/index.d.ts +50 -6
- package/dist/index.js +385 -1
- package/dist/index.js.map +1 -1
- package/dist/swap-PFQTJZA7.d.ts +1244 -0
- package/dist/swap.d.ts +1 -1
- package/dist/swap.js +21 -1
- package/package.json +2 -2
- package/dist/chunk-Z6S56VPV.js.map +0 -1
- package/dist/swap-DF4ghKio.d.ts +0 -484
|
@@ -520,6 +520,313 @@ function decryptFulfillClaim(params) {
|
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
+
// src/stream-receipts.ts
|
|
524
|
+
import { schnorr } from "@noble/curves/secp256k1.js";
|
|
525
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
526
|
+
import { bytesToHex as bytesToHex2, hexToBytes } from "@noble/hashes/utils.js";
|
|
527
|
+
var STREAM_RECEIPT_VERSION = 1;
|
|
528
|
+
var STREAM_RECEIPT_SIGNING_TAG = "toon.stream-receipt.v1";
|
|
529
|
+
var STREAM_NONCE_REGEX = /^[0-9a-f]{32}$/;
|
|
530
|
+
var SIG_REGEX = /^[0-9a-f]{128}$/;
|
|
531
|
+
var HEX64_REGEX = /^[0-9a-f]{64}$/;
|
|
532
|
+
var DECIMAL_UINT_REGEX = /^(0|[1-9]\d*)$/;
|
|
533
|
+
var RATE_REGEX = /^(0|[1-9]\d*)(\.\d+)?$/;
|
|
534
|
+
function isValidStreamNonce(value) {
|
|
535
|
+
return typeof value === "string" && STREAM_NONCE_REGEX.test(value);
|
|
536
|
+
}
|
|
537
|
+
function parseStreamReceipt(value) {
|
|
538
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
539
|
+
throw new Error("receipt must be an object");
|
|
540
|
+
}
|
|
541
|
+
const obj = value;
|
|
542
|
+
if (obj["v"] !== STREAM_RECEIPT_VERSION) {
|
|
543
|
+
throw new Error(
|
|
544
|
+
`receipt.v must be ${STREAM_RECEIPT_VERSION}, got ${String(obj["v"])}`
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
if (!isValidStreamNonce(obj["streamNonce"])) {
|
|
548
|
+
throw new Error("receipt.streamNonce must be 32-char lowercase hex");
|
|
549
|
+
}
|
|
550
|
+
const seq = obj["seq"];
|
|
551
|
+
if (typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 1) {
|
|
552
|
+
throw new Error("receipt.seq must be a positive safe integer");
|
|
553
|
+
}
|
|
554
|
+
const cumulativeDelivered = obj["cumulativeDelivered"];
|
|
555
|
+
if (typeof cumulativeDelivered !== "string" || !DECIMAL_UINT_REGEX.test(cumulativeDelivered)) {
|
|
556
|
+
throw new Error(
|
|
557
|
+
"receipt.cumulativeDelivered must be a non-negative integer decimal string"
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const rate = obj["rate"];
|
|
561
|
+
if (typeof rate !== "string" || !RATE_REGEX.test(rate) || /^0(\.0+)?$/.test(rate)) {
|
|
562
|
+
throw new Error("receipt.rate must be a positive decimal string");
|
|
563
|
+
}
|
|
564
|
+
const rateTimestamp = obj["rateTimestamp"];
|
|
565
|
+
if (typeof rateTimestamp !== "number" || !Number.isInteger(rateTimestamp) || rateTimestamp <= 0) {
|
|
566
|
+
throw new Error(
|
|
567
|
+
"receipt.rateTimestamp must be a positive integer (unix ms)"
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
const sig = obj["sig"];
|
|
571
|
+
if (typeof sig !== "string" || !SIG_REGEX.test(sig)) {
|
|
572
|
+
throw new Error("receipt.sig must be 128-char lowercase hex");
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
v: STREAM_RECEIPT_VERSION,
|
|
576
|
+
streamNonce: obj["streamNonce"],
|
|
577
|
+
seq,
|
|
578
|
+
cumulativeDelivered,
|
|
579
|
+
rate,
|
|
580
|
+
rateTimestamp,
|
|
581
|
+
sig
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function encodeReceiptSigningPayload(fields) {
|
|
585
|
+
if (!isValidStreamNonce(fields.streamNonce)) {
|
|
586
|
+
throw new Error("streamNonce must be 32-char lowercase hex");
|
|
587
|
+
}
|
|
588
|
+
const tag = Buffer.from(STREAM_RECEIPT_SIGNING_TAG, "utf8");
|
|
589
|
+
const cumulative = Buffer.from(fields.cumulativeDelivered, "utf8");
|
|
590
|
+
const rate = Buffer.from(fields.rate, "utf8");
|
|
591
|
+
const buf = Buffer.alloc(
|
|
592
|
+
4 + tag.length + 1 + 16 + 8 + 4 + cumulative.length + 4 + rate.length + 8
|
|
593
|
+
);
|
|
594
|
+
let off = 0;
|
|
595
|
+
buf.writeUInt32BE(tag.length, off);
|
|
596
|
+
off += 4;
|
|
597
|
+
tag.copy(buf, off);
|
|
598
|
+
off += tag.length;
|
|
599
|
+
buf.writeUInt8(fields.v, off);
|
|
600
|
+
off += 1;
|
|
601
|
+
Buffer.from(hexToBytes(fields.streamNonce)).copy(buf, off);
|
|
602
|
+
off += 16;
|
|
603
|
+
buf.writeBigUInt64BE(BigInt(fields.seq), off);
|
|
604
|
+
off += 8;
|
|
605
|
+
buf.writeUInt32BE(cumulative.length, off);
|
|
606
|
+
off += 4;
|
|
607
|
+
cumulative.copy(buf, off);
|
|
608
|
+
off += cumulative.length;
|
|
609
|
+
buf.writeUInt32BE(rate.length, off);
|
|
610
|
+
off += 4;
|
|
611
|
+
rate.copy(buf, off);
|
|
612
|
+
off += rate.length;
|
|
613
|
+
buf.writeBigUInt64BE(BigInt(fields.rateTimestamp), off);
|
|
614
|
+
return new Uint8Array(buf);
|
|
615
|
+
}
|
|
616
|
+
function signStreamReceipt(fields, secretKey) {
|
|
617
|
+
if (!(secretKey instanceof Uint8Array) || secretKey.length !== 32) {
|
|
618
|
+
throw new Error("receipt secretKey must be a 32-byte Uint8Array");
|
|
619
|
+
}
|
|
620
|
+
const digest = sha256(encodeReceiptSigningPayload(fields));
|
|
621
|
+
const sig = bytesToHex2(schnorr.sign(digest, secretKey));
|
|
622
|
+
return { ...fields, sig };
|
|
623
|
+
}
|
|
624
|
+
function verifyStreamReceipt(receipt, makerPubkey) {
|
|
625
|
+
if (typeof makerPubkey !== "string" || !HEX64_REGEX.test(makerPubkey)) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
if (typeof receipt.sig !== "string" || !SIG_REGEX.test(receipt.sig)) {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
try {
|
|
632
|
+
const { sig: _sig, ...fields } = receipt;
|
|
633
|
+
const digest = sha256(encodeReceiptSigningPayload(fields));
|
|
634
|
+
return schnorr.verify(
|
|
635
|
+
hexToBytes(receipt.sig),
|
|
636
|
+
digest,
|
|
637
|
+
hexToBytes(makerPubkey)
|
|
638
|
+
);
|
|
639
|
+
} catch {
|
|
640
|
+
return false;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
var ReceiptChainTracker = class {
|
|
644
|
+
#streamNonce;
|
|
645
|
+
#makerPubkey;
|
|
646
|
+
/** Verified receipts, kept sorted by ascending seq. */
|
|
647
|
+
#receipts = [];
|
|
648
|
+
#seqs = /* @__PURE__ */ new Set();
|
|
649
|
+
constructor(input) {
|
|
650
|
+
if (!isValidStreamNonce(input.streamNonce)) {
|
|
651
|
+
throw new Error("streamNonce must be 32-char lowercase hex");
|
|
652
|
+
}
|
|
653
|
+
if (typeof input.makerPubkey !== "string" || !HEX64_REGEX.test(input.makerPubkey)) {
|
|
654
|
+
throw new Error("makerPubkey must be a 64-char lowercase hex string");
|
|
655
|
+
}
|
|
656
|
+
this.#streamNonce = input.streamNonce;
|
|
657
|
+
this.#makerPubkey = input.makerPubkey;
|
|
658
|
+
}
|
|
659
|
+
get streamNonce() {
|
|
660
|
+
return this.#streamNonce;
|
|
661
|
+
}
|
|
662
|
+
/** Number of verified receipts accumulated so far. */
|
|
663
|
+
get size() {
|
|
664
|
+
return this.#receipts.length;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Verify + accumulate one receipt. On `{ok: false}` the receipt is NOT
|
|
668
|
+
* added and the chain is unchanged.
|
|
669
|
+
*/
|
|
670
|
+
add(receipt) {
|
|
671
|
+
if (receipt.streamNonce !== this.#streamNonce) {
|
|
672
|
+
return {
|
|
673
|
+
ok: false,
|
|
674
|
+
code: "WRONG_STREAM_NONCE",
|
|
675
|
+
message: `receipt streamNonce ${receipt.streamNonce} does not match session ${this.#streamNonce}`
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (!verifyStreamReceipt(receipt, this.#makerPubkey)) {
|
|
679
|
+
return {
|
|
680
|
+
ok: false,
|
|
681
|
+
code: "BAD_SIGNATURE",
|
|
682
|
+
message: `receipt seq ${receipt.seq} signature does not verify against maker key ${this.#makerPubkey}`
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
if (this.#seqs.has(receipt.seq)) {
|
|
686
|
+
return {
|
|
687
|
+
ok: false,
|
|
688
|
+
code: "DUPLICATE_SEQ",
|
|
689
|
+
message: `duplicate receipt seq ${receipt.seq}`
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
let lo = 0;
|
|
693
|
+
let hi = this.#receipts.length;
|
|
694
|
+
while (lo < hi) {
|
|
695
|
+
const mid = lo + hi >>> 1;
|
|
696
|
+
const midReceipt = this.#receipts[mid];
|
|
697
|
+
if (midReceipt.seq < receipt.seq) lo = mid + 1;
|
|
698
|
+
else hi = mid;
|
|
699
|
+
}
|
|
700
|
+
const cumulative = BigInt(receipt.cumulativeDelivered);
|
|
701
|
+
const lower = lo > 0 ? this.#receipts[lo - 1] : void 0;
|
|
702
|
+
const higher = lo < this.#receipts.length ? this.#receipts[lo] : void 0;
|
|
703
|
+
if (lower && BigInt(lower.cumulativeDelivered) > cumulative) {
|
|
704
|
+
return {
|
|
705
|
+
ok: false,
|
|
706
|
+
code: "NON_MONOTONIC",
|
|
707
|
+
message: `receipt seq ${receipt.seq} cumulativeDelivered ${receipt.cumulativeDelivered} is below seq ${lower.seq}'s ${lower.cumulativeDelivered}`
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
if (higher && BigInt(higher.cumulativeDelivered) < cumulative) {
|
|
711
|
+
return {
|
|
712
|
+
ok: false,
|
|
713
|
+
code: "NON_MONOTONIC",
|
|
714
|
+
message: `receipt seq ${receipt.seq} cumulativeDelivered ${receipt.cumulativeDelivered} is above seq ${higher.seq}'s ${higher.cumulativeDelivered}`
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
this.#receipts.splice(lo, 0, receipt);
|
|
718
|
+
this.#seqs.add(receipt.seq);
|
|
719
|
+
return { ok: true };
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Snapshot the accumulated chain: sorted receipts, the superseding latest,
|
|
723
|
+
* the signed total delivered, and any `seq` holes in `[1, latest.seq]`.
|
|
724
|
+
*/
|
|
725
|
+
chain() {
|
|
726
|
+
const receipts = [...this.#receipts];
|
|
727
|
+
const latest = receipts[receipts.length - 1];
|
|
728
|
+
const holes = [];
|
|
729
|
+
if (latest) {
|
|
730
|
+
for (let seq = 1; seq <= latest.seq; seq++) {
|
|
731
|
+
if (!this.#seqs.has(seq)) holes.push(seq);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
streamNonce: this.#streamNonce,
|
|
736
|
+
receipts,
|
|
737
|
+
...latest !== void 0 && { latest },
|
|
738
|
+
totalDelivered: latest?.cumulativeDelivered ?? "0",
|
|
739
|
+
holes
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
function serializeReceiptChain(chain) {
|
|
744
|
+
return JSON.stringify({
|
|
745
|
+
kind: "toon.stream-receipt-chain",
|
|
746
|
+
v: STREAM_RECEIPT_VERSION,
|
|
747
|
+
streamNonce: chain.streamNonce,
|
|
748
|
+
totalDelivered: chain.totalDelivered,
|
|
749
|
+
holes: chain.holes,
|
|
750
|
+
receipts: chain.receipts.map((r) => ({
|
|
751
|
+
v: r.v,
|
|
752
|
+
streamNonce: r.streamNonce,
|
|
753
|
+
seq: r.seq,
|
|
754
|
+
cumulativeDelivered: r.cumulativeDelivered,
|
|
755
|
+
rate: r.rate,
|
|
756
|
+
rateTimestamp: r.rateTimestamp,
|
|
757
|
+
sig: r.sig
|
|
758
|
+
}))
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
var DEFAULT_RECEIPT_SESSIONS_CAP = 1e4;
|
|
762
|
+
var BoundedReceiptSessions = class {
|
|
763
|
+
#map = /* @__PURE__ */ new Map();
|
|
764
|
+
#cap;
|
|
765
|
+
constructor(cap = DEFAULT_RECEIPT_SESSIONS_CAP) {
|
|
766
|
+
if (!Number.isInteger(cap) || cap <= 0) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
`BoundedReceiptSessions cap must be a positive integer, got ${cap}`
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
this.#cap = cap;
|
|
772
|
+
}
|
|
773
|
+
get size() {
|
|
774
|
+
return this.#map.size;
|
|
775
|
+
}
|
|
776
|
+
get cap() {
|
|
777
|
+
return this.#cap;
|
|
778
|
+
}
|
|
779
|
+
get(streamNonce) {
|
|
780
|
+
const state = this.#map.get(streamNonce);
|
|
781
|
+
if (state === void 0) return void 0;
|
|
782
|
+
this.#map.delete(streamNonce);
|
|
783
|
+
this.#map.set(streamNonce, state);
|
|
784
|
+
return state;
|
|
785
|
+
}
|
|
786
|
+
set(streamNonce, state) {
|
|
787
|
+
this.#map.delete(streamNonce);
|
|
788
|
+
this.#map.set(streamNonce, state);
|
|
789
|
+
while (this.#map.size > this.#cap) {
|
|
790
|
+
const first = this.#map.keys().next();
|
|
791
|
+
if (first.done) break;
|
|
792
|
+
this.#map.delete(first.value);
|
|
793
|
+
}
|
|
794
|
+
return this;
|
|
795
|
+
}
|
|
796
|
+
delete(streamNonce) {
|
|
797
|
+
return this.#map.delete(streamNonce);
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
function issueSessionReceipt(input) {
|
|
801
|
+
const { sessions, streamNonce, deliveredAmount, rate, rateTimestamp } = input;
|
|
802
|
+
if (deliveredAmount < 0n) {
|
|
803
|
+
throw new Error(
|
|
804
|
+
`deliveredAmount must be non-negative, got ${deliveredAmount}`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const prev = sessions.get(streamNonce) ?? {
|
|
808
|
+
seq: 0,
|
|
809
|
+
cumulativeDelivered: 0n
|
|
810
|
+
};
|
|
811
|
+
const next = {
|
|
812
|
+
seq: prev.seq + 1,
|
|
813
|
+
cumulativeDelivered: prev.cumulativeDelivered + deliveredAmount
|
|
814
|
+
};
|
|
815
|
+
const receipt = signStreamReceipt(
|
|
816
|
+
{
|
|
817
|
+
v: STREAM_RECEIPT_VERSION,
|
|
818
|
+
streamNonce,
|
|
819
|
+
seq: next.seq,
|
|
820
|
+
cumulativeDelivered: next.cumulativeDelivered.toString(),
|
|
821
|
+
rate,
|
|
822
|
+
rateTimestamp
|
|
823
|
+
},
|
|
824
|
+
input.secretKey
|
|
825
|
+
);
|
|
826
|
+
sessions.set(streamNonce, next);
|
|
827
|
+
return receipt;
|
|
828
|
+
}
|
|
829
|
+
|
|
523
830
|
// src/swap-handler.ts
|
|
524
831
|
import { createHash } from "crypto";
|
|
525
832
|
var DEFAULT_SEEN_PACKET_IDS_CAP = 1e4;
|
|
@@ -611,10 +918,10 @@ var BoundedSeenPacketIds = class {
|
|
|
611
918
|
for (const k of this.#map.keys()) yield [k, k];
|
|
612
919
|
}
|
|
613
920
|
};
|
|
614
|
-
var
|
|
921
|
+
var RATE_REGEX2 = /^(0|[1-9]\d*)(\.\d+)?$/;
|
|
615
922
|
function applyRate(params) {
|
|
616
923
|
const { sourceAmount, fromScale, toScale, rate } = params;
|
|
617
|
-
if (!
|
|
924
|
+
if (!RATE_REGEX2.test(rate)) {
|
|
618
925
|
throw new SwapHandlerError(`Invalid rate format: ${rate}`);
|
|
619
926
|
}
|
|
620
927
|
if (/^0(\.0+)?$/.test(rate)) {
|
|
@@ -713,7 +1020,12 @@ function createSwapHandler(config) {
|
|
|
713
1020
|
"claimIssuer must implement issueClaim(params): Promise<IssueClaimResult>"
|
|
714
1021
|
);
|
|
715
1022
|
}
|
|
1023
|
+
if (config.receiptSecretKey !== void 0 && (!(config.receiptSecretKey instanceof Uint8Array) || config.receiptSecretKey.length !== 32)) {
|
|
1024
|
+
throw new SwapHandlerError("receiptSecretKey must be a 32-byte Uint8Array");
|
|
1025
|
+
}
|
|
716
1026
|
const logger = config.logger ?? NOOP_LOGGER;
|
|
1027
|
+
const receiptSecretKey = config.receiptSecretKey ?? config.recipientSecretKey;
|
|
1028
|
+
const receiptSessions = config.receiptSessions ?? new BoundedReceiptSessions();
|
|
717
1029
|
const seenPacketIds = config.seenPacketIds ?? new BoundedSeenPacketIds();
|
|
718
1030
|
return async (ctx) => {
|
|
719
1031
|
if (ctx.kind !== 1059) {
|
|
@@ -788,8 +1100,20 @@ function createSwapHandler(config) {
|
|
|
788
1100
|
seenPacketIds.delete(packetId);
|
|
789
1101
|
};
|
|
790
1102
|
let rate;
|
|
1103
|
+
let rateTimestamp;
|
|
791
1104
|
try {
|
|
792
|
-
|
|
1105
|
+
const provided = config.rateProvider ? await config.rateProvider(pair) : pair.rate;
|
|
1106
|
+
if (typeof provided === "string") {
|
|
1107
|
+
rate = provided;
|
|
1108
|
+
rateTimestamp = Date.now();
|
|
1109
|
+
} else if (provided !== null && typeof provided === "object" && typeof provided.rate === "string" && typeof provided.rateTimestamp === "number" && Number.isInteger(provided.rateTimestamp) && provided.rateTimestamp > 0) {
|
|
1110
|
+
rate = provided.rate;
|
|
1111
|
+
rateTimestamp = provided.rateTimestamp;
|
|
1112
|
+
} else {
|
|
1113
|
+
throw new SwapHandlerError(
|
|
1114
|
+
"rateProvider must return a decimal-string rate or { rate, rateTimestamp }"
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
793
1117
|
} catch (err) {
|
|
794
1118
|
logger.error({
|
|
795
1119
|
event: "swap_handler.rate_provider_failed",
|
|
@@ -876,6 +1200,32 @@ function createSwapHandler(config) {
|
|
|
876
1200
|
releaseReservation();
|
|
877
1201
|
return ctx.reject("T00", "Internal error");
|
|
878
1202
|
}
|
|
1203
|
+
let receipt;
|
|
1204
|
+
const streamNonceTag = findTagValue(rumor, "stream-nonce");
|
|
1205
|
+
if (streamNonceTag !== void 0) {
|
|
1206
|
+
if (!isValidStreamNonce(streamNonceTag)) {
|
|
1207
|
+
logger.warn({
|
|
1208
|
+
event: "swap_handler.invalid_stream_nonce",
|
|
1209
|
+
destination: ctx.destination
|
|
1210
|
+
});
|
|
1211
|
+
} else {
|
|
1212
|
+
try {
|
|
1213
|
+
receipt = issueSessionReceipt({
|
|
1214
|
+
sessions: receiptSessions,
|
|
1215
|
+
streamNonce: streamNonceTag,
|
|
1216
|
+
deliveredAmount: targetAmount,
|
|
1217
|
+
rate,
|
|
1218
|
+
rateTimestamp,
|
|
1219
|
+
secretKey: receiptSecretKey
|
|
1220
|
+
});
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
logger.error({
|
|
1223
|
+
event: "swap_handler.receipt_issue_failed",
|
|
1224
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
879
1229
|
const claimBase64 = Buffer.from(ciphertext).toString("base64");
|
|
880
1230
|
logger.info({
|
|
881
1231
|
event: "swap_handler.claim_issued",
|
|
@@ -885,9 +1235,12 @@ function createSwapHandler(config) {
|
|
|
885
1235
|
const metadata = {
|
|
886
1236
|
claim: claimBase64,
|
|
887
1237
|
ephemeralPubkey,
|
|
888
|
-
targetAmount: targetAmount.toString()
|
|
1238
|
+
targetAmount: targetAmount.toString(),
|
|
1239
|
+
rate,
|
|
1240
|
+
rateTimestamp
|
|
889
1241
|
};
|
|
890
1242
|
if (claimId !== void 0) metadata["claimId"] = claimId;
|
|
1243
|
+
if (receipt !== void 0) metadata["receipt"] = receipt;
|
|
891
1244
|
if (settlementChannelId !== void 0 && settlementNonce !== void 0 && settlementCumulative !== void 0 && settlementRecipient !== void 0 && settlementSwapSigner !== void 0) {
|
|
892
1245
|
metadata["channelId"] = settlementChannelId;
|
|
893
1246
|
metadata["nonce"] = settlementNonce.toString();
|
|
@@ -914,8 +1267,8 @@ function computePacketId(senderPubkey, sourceAmount, rumor) {
|
|
|
914
1267
|
|
|
915
1268
|
// src/stream-swap.ts
|
|
916
1269
|
import { getPublicKey as getPublicKey3 } from "nostr-tools/pure";
|
|
917
|
-
var
|
|
918
|
-
var
|
|
1270
|
+
var RATE_REGEX3 = /^(0|[1-9]\d*)(\.\d+)?$/;
|
|
1271
|
+
var HEX64_REGEX2 = /^[0-9a-f]{64}$/;
|
|
919
1272
|
var BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
920
1273
|
function isBase64(s) {
|
|
921
1274
|
if (s.length === 0 || s.length % 4 !== 0) return false;
|
|
@@ -951,26 +1304,29 @@ function buildSwapRumor(input) {
|
|
|
951
1304
|
totalPackets,
|
|
952
1305
|
nonce,
|
|
953
1306
|
createdAt,
|
|
954
|
-
chainRecipient
|
|
1307
|
+
chainRecipient,
|
|
1308
|
+
streamNonce
|
|
955
1309
|
} = input;
|
|
1310
|
+
const tags = [
|
|
1311
|
+
["swap-from", `${pair.from.assetCode}:${pair.from.chain}`],
|
|
1312
|
+
["swap-to", `${pair.to.assetCode}:${pair.to.chain}`],
|
|
1313
|
+
["amount", sourceAmount.toString()],
|
|
1314
|
+
["seq", String(packetIndex), String(totalPackets)],
|
|
1315
|
+
["nonce", Buffer.from(nonce).toString("hex")],
|
|
1316
|
+
["chain-recipient", chainRecipient]
|
|
1317
|
+
];
|
|
1318
|
+
if (streamNonce !== void 0) tags.push(["stream-nonce", streamNonce]);
|
|
956
1319
|
return {
|
|
957
1320
|
kind: 20032,
|
|
958
1321
|
pubkey: senderPubkey,
|
|
959
1322
|
content: "",
|
|
960
1323
|
created_at: createdAt,
|
|
961
|
-
tags
|
|
962
|
-
["swap-from", `${pair.from.assetCode}:${pair.from.chain}`],
|
|
963
|
-
["swap-to", `${pair.to.assetCode}:${pair.to.chain}`],
|
|
964
|
-
["amount", sourceAmount.toString()],
|
|
965
|
-
["seq", String(packetIndex), String(totalPackets)],
|
|
966
|
-
["nonce", Buffer.from(nonce).toString("hex")],
|
|
967
|
-
["chain-recipient", chainRecipient]
|
|
968
|
-
]
|
|
1324
|
+
tags
|
|
969
1325
|
};
|
|
970
1326
|
}
|
|
971
1327
|
var EVM_CHANNEL_ID_REGEX = /^0x[0-9a-f]{64}$/;
|
|
972
1328
|
var EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;
|
|
973
|
-
var
|
|
1329
|
+
var DECIMAL_UINT_REGEX2 = /^(0|[1-9]\d*)$/;
|
|
974
1330
|
var BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
|
|
975
1331
|
function validateChainAddress(value, chain, kind) {
|
|
976
1332
|
if (chain.startsWith("evm:")) {
|
|
@@ -992,7 +1348,7 @@ function validateChainAddress(value, chain, kind) {
|
|
|
992
1348
|
}
|
|
993
1349
|
return value.length > 0;
|
|
994
1350
|
}
|
|
995
|
-
function decodeFulfillMetadata(data, chain) {
|
|
1351
|
+
function decodeFulfillMetadata(data, chain, opts) {
|
|
996
1352
|
if (data === void 0 || data === null || data === "") {
|
|
997
1353
|
throw new StreamSwapError("FULFILL_DECODE_FAILED", "FULFILL data missing");
|
|
998
1354
|
}
|
|
@@ -1037,7 +1393,7 @@ function decodeFulfillMetadata(data, chain) {
|
|
|
1037
1393
|
"FULFILL metadata.claim is missing or not base64 string"
|
|
1038
1394
|
);
|
|
1039
1395
|
}
|
|
1040
|
-
if (typeof ephemeralPubkey !== "string" || !
|
|
1396
|
+
if (typeof ephemeralPubkey !== "string" || !HEX64_REGEX2.test(ephemeralPubkey)) {
|
|
1041
1397
|
throw new StreamSwapError(
|
|
1042
1398
|
"FULFILL_DECODE_FAILED",
|
|
1043
1399
|
"FULFILL metadata.ephemeralPubkey is missing or not 64-char hex"
|
|
@@ -1060,16 +1416,58 @@ function decodeFulfillMetadata(data, chain) {
|
|
|
1060
1416
|
}
|
|
1061
1417
|
result.targetAmount = ta;
|
|
1062
1418
|
}
|
|
1419
|
+
const tapeRate = obj["rate"];
|
|
1420
|
+
const tapeTimestamp = obj["rateTimestamp"];
|
|
1421
|
+
const hasRate = tapeRate !== void 0;
|
|
1422
|
+
const hasTimestamp = tapeTimestamp !== void 0;
|
|
1423
|
+
if (hasRate !== hasTimestamp) {
|
|
1424
|
+
throw new StreamSwapError(
|
|
1425
|
+
"FULFILL_DECODE_FAILED",
|
|
1426
|
+
"FULFILL metadata quote tape is malformed: rate and rateTimestamp must travel together"
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1429
|
+
if (hasRate) {
|
|
1430
|
+
if (typeof tapeRate !== "string" || !RATE_REGEX3.test(tapeRate) || /^0(\.0+)?$/.test(tapeRate)) {
|
|
1431
|
+
throw new StreamSwapError(
|
|
1432
|
+
"FULFILL_DECODE_FAILED",
|
|
1433
|
+
"FULFILL metadata.rate must be a positive decimal string"
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
if (typeof tapeTimestamp !== "number" || !Number.isInteger(tapeTimestamp) || tapeTimestamp <= 0) {
|
|
1437
|
+
throw new StreamSwapError(
|
|
1438
|
+
"FULFILL_DECODE_FAILED",
|
|
1439
|
+
"FULFILL metadata.rateTimestamp must be a positive integer (unix ms)"
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
result.rate = tapeRate;
|
|
1443
|
+
result.rateTimestamp = tapeTimestamp;
|
|
1444
|
+
} else if (opts?.requireQuoteTape === true) {
|
|
1445
|
+
throw new StreamSwapError(
|
|
1446
|
+
"FULFILL_DECODE_FAILED",
|
|
1447
|
+
"FULFILL metadata is missing the quote tape (rate + rateTimestamp) required when minExchangeRate is set"
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
if (obj["receipt"] !== void 0) {
|
|
1451
|
+
try {
|
|
1452
|
+
result.receipt = parseStreamReceipt(obj["receipt"]);
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
throw new StreamSwapError(
|
|
1455
|
+
"FULFILL_DECODE_FAILED",
|
|
1456
|
+
`FULFILL metadata.receipt is malformed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1457
|
+
{ cause: err }
|
|
1458
|
+
);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1063
1461
|
const channelId = obj["channelId"];
|
|
1064
1462
|
if (typeof channelId === "string" && (!chain || validateChainAddress(channelId, chain, "channelId"))) {
|
|
1065
1463
|
result.channelId = channelId;
|
|
1066
1464
|
}
|
|
1067
1465
|
const nonce = obj["nonce"];
|
|
1068
|
-
if (typeof nonce === "string" &&
|
|
1466
|
+
if (typeof nonce === "string" && DECIMAL_UINT_REGEX2.test(nonce)) {
|
|
1069
1467
|
result.nonce = nonce;
|
|
1070
1468
|
}
|
|
1071
1469
|
const cumulativeAmount = obj["cumulativeAmount"];
|
|
1072
|
-
if (typeof cumulativeAmount === "string" &&
|
|
1470
|
+
if (typeof cumulativeAmount === "string" && DECIMAL_UINT_REGEX2.test(cumulativeAmount)) {
|
|
1073
1471
|
result.cumulativeAmount = cumulativeAmount;
|
|
1074
1472
|
}
|
|
1075
1473
|
const recipient = obj["recipient"];
|
|
@@ -1082,6 +1480,18 @@ function decodeFulfillMetadata(data, chain) {
|
|
|
1082
1480
|
}
|
|
1083
1481
|
return result;
|
|
1084
1482
|
}
|
|
1483
|
+
function compareDecimalRates(a, b) {
|
|
1484
|
+
const split = (s) => {
|
|
1485
|
+
const dot = s.indexOf(".");
|
|
1486
|
+
return dot === -1 ? { int: s, frac: "" } : { int: s.slice(0, dot), frac: s.slice(dot + 1) };
|
|
1487
|
+
};
|
|
1488
|
+
const pa = split(a);
|
|
1489
|
+
const pb = split(b);
|
|
1490
|
+
const scale = Math.max(pa.frac.length, pb.frac.length);
|
|
1491
|
+
const av = BigInt(pa.int + pa.frac.padEnd(scale, "0"));
|
|
1492
|
+
const bv = BigInt(pb.int + pb.frac.padEnd(scale, "0"));
|
|
1493
|
+
return av < bv ? -1 : av > bv ? 1 : 0;
|
|
1494
|
+
}
|
|
1085
1495
|
var Deferred = class {
|
|
1086
1496
|
promise;
|
|
1087
1497
|
resolve;
|
|
@@ -1110,13 +1520,25 @@ function validateParams(params) {
|
|
|
1110
1520
|
}
|
|
1111
1521
|
const hasCount = params.packetCount !== void 0;
|
|
1112
1522
|
const hasAmounts = params.packetAmounts !== void 0;
|
|
1113
|
-
|
|
1523
|
+
const hasController = params.controller !== void 0;
|
|
1524
|
+
const chunkingModes = [hasCount, hasAmounts, hasController].filter(
|
|
1525
|
+
Boolean
|
|
1526
|
+
).length;
|
|
1527
|
+
if (chunkingModes !== 1) {
|
|
1114
1528
|
throw new StreamSwapError(
|
|
1115
1529
|
"INVALID_CHUNKING",
|
|
1116
|
-
"Exactly one of packetCount or
|
|
1530
|
+
"Exactly one of packetCount, packetAmounts, or controller must be provided"
|
|
1117
1531
|
);
|
|
1118
1532
|
}
|
|
1119
|
-
if (
|
|
1533
|
+
if (hasController) {
|
|
1534
|
+
const c = params.controller;
|
|
1535
|
+
if (typeof c.nextDelta !== "function" || typeof c.observe !== "function" || typeof c.window !== "number") {
|
|
1536
|
+
throw new StreamSwapError(
|
|
1537
|
+
"INVALID_STATE",
|
|
1538
|
+
"controller must implement nextDelta(remaining), observe(observation), and window"
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
} else if (hasCount) {
|
|
1120
1542
|
const c = params.packetCount;
|
|
1121
1543
|
if (!Number.isInteger(c) || c <= 0) {
|
|
1122
1544
|
throw new StreamSwapError(
|
|
@@ -1161,7 +1583,7 @@ function validateParams(params) {
|
|
|
1161
1583
|
"senderSecretKey must be a 32-byte Uint8Array"
|
|
1162
1584
|
);
|
|
1163
1585
|
}
|
|
1164
|
-
if (typeof params.swapPubkey !== "string" || !
|
|
1586
|
+
if (typeof params.swapPubkey !== "string" || !HEX64_REGEX2.test(params.swapPubkey)) {
|
|
1165
1587
|
throw new StreamSwapError(
|
|
1166
1588
|
"INVALID_STATE",
|
|
1167
1589
|
"swapPubkey must be a 64-char lowercase hex string"
|
|
@@ -1182,10 +1604,10 @@ function validateParams(params) {
|
|
|
1182
1604
|
"pair.to must have { assetCode: string, assetScale: number, chain: string }"
|
|
1183
1605
|
);
|
|
1184
1606
|
}
|
|
1185
|
-
if (typeof params.pair.rate !== "string" || !
|
|
1607
|
+
if (typeof params.pair.rate !== "string" || !RATE_REGEX3.test(params.pair.rate)) {
|
|
1186
1608
|
throw new StreamSwapError(
|
|
1187
1609
|
"INVALID_PAIR",
|
|
1188
|
-
`pair.rate must match ${
|
|
1610
|
+
`pair.rate must match ${RATE_REGEX3}, got ${params.pair.rate}`
|
|
1189
1611
|
);
|
|
1190
1612
|
}
|
|
1191
1613
|
try {
|
|
@@ -1208,6 +1630,34 @@ function validateParams(params) {
|
|
|
1208
1630
|
`rateDeviationThreshold must be a non-negative finite number, got ${params.rateDeviationThreshold}`
|
|
1209
1631
|
);
|
|
1210
1632
|
}
|
|
1633
|
+
if (params.minExchangeRate !== void 0) {
|
|
1634
|
+
if (typeof params.minExchangeRate !== "string" || !RATE_REGEX3.test(params.minExchangeRate) || /^0(\.0+)?$/.test(params.minExchangeRate)) {
|
|
1635
|
+
throw new StreamSwapError(
|
|
1636
|
+
"INVALID_STATE",
|
|
1637
|
+
`minExchangeRate must be a positive decimal string matching ${RATE_REGEX3}, got ${String(
|
|
1638
|
+
params.minExchangeRate
|
|
1639
|
+
)}`
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
if (params.receiptPubkey !== void 0 && (typeof params.receiptPubkey !== "string" || !HEX64_REGEX2.test(params.receiptPubkey))) {
|
|
1644
|
+
throw new StreamSwapError(
|
|
1645
|
+
"INVALID_STATE",
|
|
1646
|
+
"receiptPubkey must be a 64-char lowercase hex string"
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
if (params.requireReceipts !== void 0 && typeof params.requireReceipts !== "boolean") {
|
|
1650
|
+
throw new StreamSwapError(
|
|
1651
|
+
"INVALID_STATE",
|
|
1652
|
+
`requireReceipts must be a boolean, got ${String(params.requireReceipts)}`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
if (params.packetExpiryMs !== void 0 && (typeof params.packetExpiryMs !== "number" || !Number.isInteger(params.packetExpiryMs) || params.packetExpiryMs <= 0)) {
|
|
1656
|
+
throw new StreamSwapError(
|
|
1657
|
+
"INVALID_STATE",
|
|
1658
|
+
`packetExpiryMs must be a positive integer (ms), got ${params.packetExpiryMs}`
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1211
1661
|
if (typeof params.chainRecipient !== "string" || params.chainRecipient.length === 0) {
|
|
1212
1662
|
throw new StreamSwapError(
|
|
1213
1663
|
"INVALID_STATE",
|
|
@@ -1231,13 +1681,16 @@ async function streamSwap(params) {
|
|
|
1231
1681
|
function streamSwapControlled(params) {
|
|
1232
1682
|
validateParams(params);
|
|
1233
1683
|
const logger = params.logger ?? NOOP_LOGGER2;
|
|
1234
|
-
const schedule = params.packetAmounts ? [...params.packetAmounts] : chunkAmount(params.totalAmount, params.packetCount);
|
|
1684
|
+
const schedule = params.packetAmounts ? [...params.packetAmounts] : params.packetCount !== void 0 ? chunkAmount(params.totalAmount, params.packetCount) : [];
|
|
1235
1685
|
const frozenPair = Object.freeze({
|
|
1236
1686
|
from: Object.freeze({ ...params.pair.from }),
|
|
1237
1687
|
to: Object.freeze({ ...params.pair.to }),
|
|
1238
1688
|
rate: params.pair.rate
|
|
1239
1689
|
});
|
|
1240
1690
|
const senderPubkey = getPublicKey3(params.senderSecretKey);
|
|
1691
|
+
const streamNonceBytes = new Uint8Array(16);
|
|
1692
|
+
getRandomValues(streamNonceBytes);
|
|
1693
|
+
const streamNonce = Buffer.from(streamNonceBytes).toString("hex");
|
|
1241
1694
|
let streamState = "running";
|
|
1242
1695
|
let resumeDeferred = null;
|
|
1243
1696
|
const controller = {
|
|
@@ -1274,30 +1727,367 @@ function streamSwapControlled(params) {
|
|
|
1274
1727
|
return streamState;
|
|
1275
1728
|
}
|
|
1276
1729
|
};
|
|
1277
|
-
const
|
|
1730
|
+
const getState = () => streamState;
|
|
1731
|
+
const setState = (v) => {
|
|
1732
|
+
streamState = v;
|
|
1733
|
+
};
|
|
1734
|
+
const waitForResumeOrStop = () => {
|
|
1735
|
+
if (streamState !== "paused")
|
|
1736
|
+
return Promise.resolve("resume");
|
|
1737
|
+
if (!resumeDeferred) resumeDeferred = new Deferred();
|
|
1738
|
+
return resumeDeferred.promise;
|
|
1739
|
+
};
|
|
1740
|
+
const result = params.controller ? runAdaptiveLoop(
|
|
1741
|
+
params,
|
|
1742
|
+
frozenPair,
|
|
1743
|
+
senderPubkey,
|
|
1744
|
+
streamNonce,
|
|
1745
|
+
logger,
|
|
1746
|
+
getState,
|
|
1747
|
+
setState,
|
|
1748
|
+
waitForResumeOrStop
|
|
1749
|
+
) : runLoop(
|
|
1278
1750
|
params,
|
|
1279
1751
|
frozenPair,
|
|
1280
1752
|
schedule,
|
|
1281
1753
|
senderPubkey,
|
|
1754
|
+
streamNonce,
|
|
1282
1755
|
logger,
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
},
|
|
1287
|
-
() => {
|
|
1288
|
-
if (streamState !== "paused") return Promise.resolve("resume");
|
|
1289
|
-
if (!resumeDeferred) resumeDeferred = new Deferred();
|
|
1290
|
-
return resumeDeferred.promise;
|
|
1291
|
-
}
|
|
1756
|
+
getState,
|
|
1757
|
+
setState,
|
|
1758
|
+
waitForResumeOrStop
|
|
1292
1759
|
);
|
|
1293
1760
|
return { result, controller };
|
|
1294
1761
|
}
|
|
1295
|
-
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1762
|
+
function buildAndWrapPacket(input) {
|
|
1763
|
+
const {
|
|
1764
|
+
params,
|
|
1765
|
+
pair,
|
|
1766
|
+
senderPubkey,
|
|
1767
|
+
sourceAmount,
|
|
1768
|
+
seq,
|
|
1769
|
+
totalPackets,
|
|
1770
|
+
streamNonce
|
|
1771
|
+
} = input;
|
|
1772
|
+
const nonce = new Uint8Array(16);
|
|
1773
|
+
getRandomValues(nonce);
|
|
1774
|
+
const rumor = buildSwapRumor({
|
|
1775
|
+
senderPubkey,
|
|
1776
|
+
pair,
|
|
1777
|
+
sourceAmount,
|
|
1778
|
+
packetIndex: seq,
|
|
1779
|
+
totalPackets,
|
|
1780
|
+
nonce,
|
|
1781
|
+
createdAt: Math.floor(Date.now() / 1e3),
|
|
1782
|
+
chainRecipient: params.chainRecipient,
|
|
1783
|
+
streamNonce
|
|
1784
|
+
});
|
|
1785
|
+
const packetExpiresAt = params.packetExpiryMs !== void 0 ? new Date(Date.now() + params.packetExpiryMs) : void 0;
|
|
1786
|
+
const wrapped = wrapSwapPacketToToon({
|
|
1787
|
+
rumor,
|
|
1788
|
+
senderSecretKey: params.senderSecretKey,
|
|
1789
|
+
recipientPubkey: params.swapPubkey,
|
|
1790
|
+
destination: params.swapIlpAddress,
|
|
1791
|
+
amount: sourceAmount,
|
|
1792
|
+
...packetExpiresAt !== void 0 && { expiresAt: packetExpiresAt }
|
|
1793
|
+
});
|
|
1794
|
+
const toonData = new Uint8Array(
|
|
1795
|
+
Buffer.from(wrapped.ilpPrepare.data, "base64")
|
|
1796
|
+
);
|
|
1797
|
+
return {
|
|
1798
|
+
toonData,
|
|
1799
|
+
...packetExpiresAt !== void 0 && { packetExpiresAt }
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
async function processAcceptedPacket(ctx, args) {
|
|
1803
|
+
const { params, pair, logger } = ctx;
|
|
1804
|
+
const { packetIndex, totalForProgress, sourceAmount, data } = args;
|
|
1805
|
+
let metadata;
|
|
1806
|
+
try {
|
|
1807
|
+
metadata = decodeFulfillMetadata(data, pair.to.chain, {
|
|
1808
|
+
requireQuoteTape: params.minExchangeRate !== void 0
|
|
1809
|
+
});
|
|
1810
|
+
} catch (err) {
|
|
1811
|
+
logger.error({
|
|
1812
|
+
event: "stream_swap.fulfill_decode_failed",
|
|
1813
|
+
packetIndex,
|
|
1814
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1815
|
+
});
|
|
1816
|
+
ctx.errors.push({
|
|
1817
|
+
packetIndex,
|
|
1818
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
1819
|
+
});
|
|
1820
|
+
return { status: "error" };
|
|
1821
|
+
}
|
|
1822
|
+
const isEvmTarget = pair.to.chain.startsWith("evm:");
|
|
1823
|
+
const recipientMatches = metadata.recipient === void 0 || (isEvmTarget ? metadata.recipient.toLowerCase() === params.chainRecipient.toLowerCase() : metadata.recipient === params.chainRecipient);
|
|
1824
|
+
if (!recipientMatches) {
|
|
1825
|
+
logger.warn({
|
|
1826
|
+
event: "stream_swap.recipient_mismatch",
|
|
1827
|
+
packetIndex,
|
|
1828
|
+
expected: params.chainRecipient,
|
|
1829
|
+
actual: metadata.recipient
|
|
1830
|
+
});
|
|
1831
|
+
ctx.rejections.push({
|
|
1832
|
+
packetIndex,
|
|
1833
|
+
sourceAmount,
|
|
1834
|
+
code: "SWAP_RECIPIENT_MISMATCH",
|
|
1835
|
+
message: `Swap echoed recipient ${metadata.recipient} but sender expected ${params.chainRecipient}`
|
|
1836
|
+
});
|
|
1837
|
+
return { status: "recipient-mismatch" };
|
|
1838
|
+
}
|
|
1839
|
+
if (params.minExchangeRate !== void 0) {
|
|
1840
|
+
const tapeRate = metadata.rate;
|
|
1841
|
+
const floorTargetAmount = applyRate({
|
|
1842
|
+
sourceAmount,
|
|
1843
|
+
fromScale: pair.from.assetScale,
|
|
1844
|
+
toScale: pair.to.assetScale,
|
|
1845
|
+
rate: params.minExchangeRate
|
|
1846
|
+
});
|
|
1847
|
+
const deliveredTargetAmount = metadata.targetAmount !== void 0 ? BigInt(metadata.targetAmount) : applyRate({
|
|
1848
|
+
sourceAmount,
|
|
1849
|
+
fromScale: pair.from.assetScale,
|
|
1850
|
+
toScale: pair.to.assetScale,
|
|
1851
|
+
rate: tapeRate
|
|
1852
|
+
});
|
|
1853
|
+
const tapeBelowFloor = compareDecimalRates(tapeRate, params.minExchangeRate) < 0;
|
|
1854
|
+
if (tapeBelowFloor || deliveredTargetAmount < floorTargetAmount) {
|
|
1855
|
+
logger.warn({
|
|
1856
|
+
event: "stream_swap.below_floor",
|
|
1857
|
+
packetIndex,
|
|
1858
|
+
rate: tapeRate,
|
|
1859
|
+
rateTimestamp: metadata.rateTimestamp,
|
|
1860
|
+
minExchangeRate: params.minExchangeRate,
|
|
1861
|
+
targetAmount: deliveredTargetAmount.toString(),
|
|
1862
|
+
floorTargetAmount: floorTargetAmount.toString()
|
|
1863
|
+
});
|
|
1864
|
+
ctx.rejections.push({
|
|
1865
|
+
packetIndex,
|
|
1866
|
+
sourceAmount,
|
|
1867
|
+
code: "BELOW_FLOOR",
|
|
1868
|
+
message: `Packet fill below minExchangeRate floor: rate ${tapeRate}, targetAmount ${deliveredTargetAmount} < floor ${params.minExchangeRate} (${floorTargetAmount})`
|
|
1869
|
+
});
|
|
1870
|
+
return { status: "below-floor" };
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
let verifiedReceipt;
|
|
1874
|
+
if (metadata.receipt !== void 0) {
|
|
1875
|
+
const receipt = metadata.receipt;
|
|
1876
|
+
let failure;
|
|
1877
|
+
if (metadata.rate !== void 0 && receipt.rate !== metadata.rate) {
|
|
1878
|
+
failure = `receipt.rate ${receipt.rate} does not match tape rate ${metadata.rate}`;
|
|
1879
|
+
} else if (metadata.rateTimestamp !== void 0 && receipt.rateTimestamp !== metadata.rateTimestamp) {
|
|
1880
|
+
failure = `receipt.rateTimestamp ${receipt.rateTimestamp} does not match tape rateTimestamp ${metadata.rateTimestamp}`;
|
|
1881
|
+
} else {
|
|
1882
|
+
const added = ctx.receiptTracker.add(receipt);
|
|
1883
|
+
if (!added.ok) failure = `${added.code}: ${added.message}`;
|
|
1884
|
+
}
|
|
1885
|
+
if (failure !== void 0) {
|
|
1886
|
+
logger.warn({
|
|
1887
|
+
event: "stream_swap.receipt_invalid",
|
|
1888
|
+
packetIndex,
|
|
1889
|
+
seq: receipt.seq,
|
|
1890
|
+
error: failure
|
|
1891
|
+
});
|
|
1892
|
+
ctx.rejections.push({
|
|
1893
|
+
packetIndex,
|
|
1894
|
+
sourceAmount,
|
|
1895
|
+
code: "RECEIPT_INVALID",
|
|
1896
|
+
message: `Stream receipt failed verification: ${failure}`
|
|
1897
|
+
});
|
|
1898
|
+
return { status: "receipt-invalid" };
|
|
1899
|
+
}
|
|
1900
|
+
verifiedReceipt = receipt;
|
|
1901
|
+
} else if (params.requireReceipts === true) {
|
|
1902
|
+
logger.warn({
|
|
1903
|
+
event: "stream_swap.receipt_missing",
|
|
1904
|
+
packetIndex
|
|
1905
|
+
});
|
|
1906
|
+
ctx.rejections.push({
|
|
1907
|
+
packetIndex,
|
|
1908
|
+
sourceAmount,
|
|
1909
|
+
code: "RECEIPT_MISSING",
|
|
1910
|
+
message: "FULFILL carried no stream receipt but requireReceipts is set (legacy maker without rfc-0039 receipt support?)"
|
|
1911
|
+
});
|
|
1912
|
+
return { status: "receipt-invalid" };
|
|
1913
|
+
}
|
|
1914
|
+
let claimBytes;
|
|
1915
|
+
try {
|
|
1916
|
+
const ciphertext = new Uint8Array(Buffer.from(metadata.claim, "base64"));
|
|
1917
|
+
claimBytes = decryptFulfillClaim({
|
|
1918
|
+
ciphertext,
|
|
1919
|
+
ephemeralPubkey: metadata.ephemeralPubkey,
|
|
1920
|
+
recipientSecretKey: params.senderSecretKey
|
|
1921
|
+
});
|
|
1922
|
+
} catch (err) {
|
|
1923
|
+
logger.error({
|
|
1924
|
+
event: "stream_swap.decrypt_failed",
|
|
1925
|
+
packetIndex,
|
|
1926
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1927
|
+
});
|
|
1928
|
+
ctx.errors.push({
|
|
1929
|
+
packetIndex,
|
|
1930
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
1931
|
+
});
|
|
1932
|
+
return { status: "error" };
|
|
1933
|
+
}
|
|
1934
|
+
if (claimBytes.length === 0) {
|
|
1935
|
+
logger.warn({
|
|
1936
|
+
event: "stream_swap.empty_claim_bytes",
|
|
1937
|
+
packetIndex
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
const expectedTargetAmount = applyRate({
|
|
1941
|
+
sourceAmount,
|
|
1942
|
+
fromScale: pair.from.assetScale,
|
|
1943
|
+
toScale: pair.to.assetScale,
|
|
1944
|
+
rate: pair.rate
|
|
1945
|
+
});
|
|
1946
|
+
const targetAmount = metadata.targetAmount !== void 0 ? BigInt(metadata.targetAmount) : expectedTargetAmount;
|
|
1947
|
+
let rateDeviation = 0;
|
|
1948
|
+
if (expectedTargetAmount > 0n) {
|
|
1949
|
+
const diff = targetAmount >= expectedTargetAmount ? targetAmount - expectedTargetAmount : expectedTargetAmount - targetAmount;
|
|
1950
|
+
const scaled = diff * 1000000n / expectedTargetAmount;
|
|
1951
|
+
rateDeviation = Number(scaled) / 1e6;
|
|
1952
|
+
}
|
|
1953
|
+
const advertisedRate = parseFloat(pair.rate);
|
|
1954
|
+
let effectiveRate;
|
|
1955
|
+
if (targetAmount === expectedTargetAmount) {
|
|
1956
|
+
effectiveRate = advertisedRate;
|
|
1957
|
+
} else {
|
|
1958
|
+
const signedDeviation = targetAmount >= expectedTargetAmount ? rateDeviation : -rateDeviation;
|
|
1959
|
+
effectiveRate = advertisedRate * (1 + signedDeviation);
|
|
1960
|
+
}
|
|
1961
|
+
if (!Number.isFinite(effectiveRate)) {
|
|
1962
|
+
effectiveRate = advertisedRate;
|
|
1963
|
+
}
|
|
1964
|
+
ctx.cumulative.source += sourceAmount;
|
|
1965
|
+
ctx.cumulative.target += targetAmount;
|
|
1966
|
+
const accumulated = {
|
|
1967
|
+
packetIndex,
|
|
1968
|
+
sourceAmount,
|
|
1969
|
+
targetAmount,
|
|
1970
|
+
claimBytes,
|
|
1971
|
+
swapEphemeralPubkey: metadata.ephemeralPubkey,
|
|
1972
|
+
pair,
|
|
1973
|
+
receivedAt: Date.now()
|
|
1974
|
+
};
|
|
1975
|
+
if (metadata.claimId !== void 0) accumulated.claimId = metadata.claimId;
|
|
1976
|
+
if (metadata.channelId !== void 0)
|
|
1977
|
+
accumulated.channelId = metadata.channelId;
|
|
1978
|
+
if (metadata.nonce !== void 0) accumulated.nonce = metadata.nonce;
|
|
1979
|
+
if (metadata.cumulativeAmount !== void 0)
|
|
1980
|
+
accumulated.cumulativeAmount = metadata.cumulativeAmount;
|
|
1981
|
+
if (metadata.recipient !== void 0)
|
|
1982
|
+
accumulated.recipient = metadata.recipient;
|
|
1983
|
+
if (metadata.swapSignerAddress !== void 0)
|
|
1984
|
+
accumulated.swapSignerAddress = metadata.swapSignerAddress;
|
|
1985
|
+
if (metadata.rate !== void 0) {
|
|
1986
|
+
accumulated.rate = metadata.rate;
|
|
1987
|
+
accumulated.rateTimestamp = metadata.rateTimestamp;
|
|
1988
|
+
}
|
|
1989
|
+
if (verifiedReceipt !== void 0) accumulated.receipt = verifiedReceipt;
|
|
1990
|
+
ctx.claims.push(accumulated);
|
|
1991
|
+
logger.debug({
|
|
1992
|
+
event: "stream_swap.packet_accepted",
|
|
1993
|
+
packetIndex,
|
|
1994
|
+
sourceAmount: sourceAmount.toString(),
|
|
1995
|
+
targetAmount: targetAmount.toString()
|
|
1996
|
+
});
|
|
1997
|
+
if (params.onPacket) {
|
|
1998
|
+
const progress = Object.freeze({
|
|
1999
|
+
index: packetIndex,
|
|
2000
|
+
total: totalForProgress,
|
|
2001
|
+
sourceAmount,
|
|
2002
|
+
targetAmount,
|
|
2003
|
+
advertisedRate: pair.rate,
|
|
2004
|
+
effectiveRate,
|
|
2005
|
+
rateDeviation,
|
|
2006
|
+
cumulativeSource: ctx.cumulative.source,
|
|
2007
|
+
cumulativeTarget: ctx.cumulative.target,
|
|
2008
|
+
// Issue #82 quote tape: surface the maker's fresh per-packet quote
|
|
2009
|
+
// to the callback (the adaptive-controller seam, toon#83).
|
|
2010
|
+
...metadata.rate !== void 0 && {
|
|
2011
|
+
rate: metadata.rate,
|
|
2012
|
+
rateTimestamp: metadata.rateTimestamp
|
|
2013
|
+
},
|
|
2014
|
+
// Issue #84: surface the verified per-fulfill receipt to the callback.
|
|
2015
|
+
...verifiedReceipt !== void 0 && { receipt: verifiedReceipt },
|
|
2016
|
+
state: ctx.getState() === "paused" ? "paused" : ctx.getState() === "stopped" ? "stopped" : "running"
|
|
2017
|
+
});
|
|
2018
|
+
try {
|
|
2019
|
+
const maybePromise = params.onPacket(progress);
|
|
2020
|
+
if (maybePromise && typeof maybePromise.then === "function") {
|
|
2021
|
+
await maybePromise;
|
|
2022
|
+
}
|
|
2023
|
+
} catch (err) {
|
|
2024
|
+
logger.warn({
|
|
2025
|
+
event: "stream_swap.callback_threw",
|
|
2026
|
+
packetIndex,
|
|
2027
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2028
|
+
});
|
|
2029
|
+
ctx.errors.push({
|
|
2030
|
+
packetIndex,
|
|
2031
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
2032
|
+
});
|
|
2033
|
+
return { status: "callback-throw" };
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
return {
|
|
2037
|
+
status: "accepted",
|
|
2038
|
+
targetAmount,
|
|
2039
|
+
rateDeviation,
|
|
2040
|
+
...metadata.rate !== void 0 && {
|
|
2041
|
+
rate: metadata.rate,
|
|
2042
|
+
rateTimestamp: metadata.rateTimestamp
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
function finalizeResult(input) {
|
|
2047
|
+
const { claims, rejections, errors, cumulative } = input.ctx;
|
|
2048
|
+
let { abortReason } = input;
|
|
2049
|
+
let finalState;
|
|
2050
|
+
if (abortReason === "aborted" || abortReason === "stopped") {
|
|
2051
|
+
finalState = "stopped";
|
|
2052
|
+
} else if (claims.length === 0 && (rejections.length > 0 || errors.length > 0)) {
|
|
2053
|
+
finalState = "failed";
|
|
2054
|
+
if (abortReason === "complete" && rejections.length > 0 && errors.length === 0) {
|
|
2055
|
+
abortReason = "all-rejected";
|
|
2056
|
+
}
|
|
2057
|
+
} else {
|
|
2058
|
+
finalState = "completed";
|
|
2059
|
+
}
|
|
2060
|
+
input.setState(finalState);
|
|
2061
|
+
return {
|
|
2062
|
+
state: finalState,
|
|
2063
|
+
claims,
|
|
2064
|
+
rejections,
|
|
2065
|
+
errors,
|
|
2066
|
+
abortReason,
|
|
2067
|
+
cumulativeSource: cumulative.source,
|
|
2068
|
+
cumulativeTarget: cumulative.target,
|
|
2069
|
+
packetsSent: input.packetsSent,
|
|
2070
|
+
packetsScheduled: input.packetsScheduled,
|
|
2071
|
+
// Issue #84: the verified receipt chain — present on abort too,
|
|
2072
|
+
// covering exactly the packets that filled before the halt.
|
|
2073
|
+
receipts: input.ctx.receiptTracker.chain()
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
async function runLoop(params, pair, schedule, senderPubkey, streamNonce, logger, getState, setState, waitForResumeOrStop) {
|
|
2077
|
+
const ctx = {
|
|
2078
|
+
params,
|
|
2079
|
+
pair,
|
|
2080
|
+
logger,
|
|
2081
|
+
getState,
|
|
2082
|
+
claims: [],
|
|
2083
|
+
rejections: [],
|
|
2084
|
+
errors: [],
|
|
2085
|
+
cumulative: { source: 0n, target: 0n },
|
|
2086
|
+
receiptTracker: new ReceiptChainTracker({
|
|
2087
|
+
streamNonce,
|
|
2088
|
+
makerPubkey: params.receiptPubkey ?? params.swapPubkey
|
|
2089
|
+
})
|
|
2090
|
+
};
|
|
1301
2091
|
let packetsSent = 0;
|
|
1302
2092
|
let abortReason = "complete";
|
|
1303
2093
|
const totalPackets = schedule.length;
|
|
@@ -1329,40 +2119,30 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
|
|
|
1329
2119
|
`schedule[${packetIndex}] is undefined; schedule was mutated mid-stream`
|
|
1330
2120
|
);
|
|
1331
2121
|
}
|
|
1332
|
-
|
|
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;
|
|
2122
|
+
let built;
|
|
1345
2123
|
try {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2124
|
+
built = buildAndWrapPacket({
|
|
2125
|
+
params,
|
|
2126
|
+
pair,
|
|
2127
|
+
senderPubkey,
|
|
2128
|
+
sourceAmount,
|
|
2129
|
+
seq: packetIndex + 1,
|
|
2130
|
+
totalPackets,
|
|
2131
|
+
streamNonce
|
|
1352
2132
|
});
|
|
1353
|
-
toonData = new Uint8Array(Buffer.from(wrapped.ilpPrepare.data, "base64"));
|
|
1354
2133
|
} catch (err) {
|
|
1355
2134
|
logger.error({
|
|
1356
2135
|
event: "stream_swap.wrap_failed",
|
|
1357
2136
|
packetIndex,
|
|
1358
2137
|
error: err instanceof Error ? err.message : String(err)
|
|
1359
2138
|
});
|
|
1360
|
-
errors.push({
|
|
2139
|
+
ctx.errors.push({
|
|
1361
2140
|
packetIndex,
|
|
1362
2141
|
cause: err instanceof Error ? err : new Error(String(err))
|
|
1363
2142
|
});
|
|
1364
2143
|
continue;
|
|
1365
2144
|
}
|
|
2145
|
+
const { toonData, packetExpiresAt } = built;
|
|
1366
2146
|
let sendResult;
|
|
1367
2147
|
try {
|
|
1368
2148
|
sendResult = await params.client.sendSwapPacket({
|
|
@@ -1370,7 +2150,8 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
|
|
|
1370
2150
|
amount: sourceAmount,
|
|
1371
2151
|
toonData,
|
|
1372
2152
|
timeout: params.packetTimeoutMs ?? 3e4,
|
|
1373
|
-
claim: params.claim
|
|
2153
|
+
claim: params.claim,
|
|
2154
|
+
...packetExpiresAt !== void 0 && { expiresAt: packetExpiresAt }
|
|
1374
2155
|
});
|
|
1375
2156
|
packetsSent += 1;
|
|
1376
2157
|
} catch (err) {
|
|
@@ -1379,7 +2160,7 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
|
|
|
1379
2160
|
packetIndex,
|
|
1380
2161
|
error: err instanceof Error ? err.message : String(err)
|
|
1381
2162
|
});
|
|
1382
|
-
errors.push({
|
|
2163
|
+
ctx.errors.push({
|
|
1383
2164
|
packetIndex,
|
|
1384
2165
|
cause: err instanceof Error ? err : new Error(String(err))
|
|
1385
2166
|
});
|
|
@@ -1394,187 +2175,307 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
|
|
|
1394
2175
|
code,
|
|
1395
2176
|
message
|
|
1396
2177
|
});
|
|
1397
|
-
rejections.push({ packetIndex, sourceAmount, code, message });
|
|
2178
|
+
ctx.rejections.push({ packetIndex, sourceAmount, code, message });
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
const outcome = await processAcceptedPacket(ctx, {
|
|
2182
|
+
packetIndex,
|
|
2183
|
+
totalForProgress: totalPackets,
|
|
2184
|
+
sourceAmount,
|
|
2185
|
+
data: sendResult.data
|
|
2186
|
+
});
|
|
2187
|
+
if (outcome.status === "error" || outcome.status === "recipient-mismatch") {
|
|
1398
2188
|
continue;
|
|
1399
2189
|
}
|
|
1400
|
-
|
|
2190
|
+
if (outcome.status === "below-floor") {
|
|
2191
|
+
abortReason = "below-floor";
|
|
2192
|
+
break packetLoop;
|
|
2193
|
+
}
|
|
2194
|
+
if (outcome.status === "receipt-invalid") {
|
|
2195
|
+
abortReason = "receipt-invalid";
|
|
2196
|
+
break packetLoop;
|
|
2197
|
+
}
|
|
2198
|
+
if (outcome.status === "callback-throw") {
|
|
2199
|
+
abortReason = "callback-throw";
|
|
2200
|
+
break packetLoop;
|
|
2201
|
+
}
|
|
2202
|
+
if (isAborted()) {
|
|
2203
|
+
abortReason = "aborted";
|
|
2204
|
+
break;
|
|
2205
|
+
}
|
|
2206
|
+
if (getState() === "stopped") {
|
|
2207
|
+
abortReason = "stopped";
|
|
2208
|
+
break;
|
|
2209
|
+
}
|
|
2210
|
+
if (params.rateDeviationThreshold !== void 0 && outcome.rateDeviation > params.rateDeviationThreshold) {
|
|
2211
|
+
abortReason = "rate-deviation";
|
|
2212
|
+
break;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return finalizeResult({
|
|
2216
|
+
ctx,
|
|
2217
|
+
abortReason,
|
|
2218
|
+
packetsSent,
|
|
2219
|
+
packetsScheduled: totalPackets,
|
|
2220
|
+
setState
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
function classifySendError(err) {
|
|
2224
|
+
return /timeout|timed out|expire/i.test(err.message) ? "timeout" : "error";
|
|
2225
|
+
}
|
|
2226
|
+
function classifyReject(code, message) {
|
|
2227
|
+
if (code === "T99" || /stale[_-]?rate/i.test(message)) return "reject-stale";
|
|
2228
|
+
if (code.startsWith("R") || /timeout|timed out|expire/i.test(message)) {
|
|
2229
|
+
return "timeout";
|
|
2230
|
+
}
|
|
2231
|
+
return "reject";
|
|
2232
|
+
}
|
|
2233
|
+
async function runAdaptiveLoop(params, pair, senderPubkey, streamNonce, logger, getState, setState, waitForResumeOrStop) {
|
|
2234
|
+
const controller = params.controller;
|
|
2235
|
+
const ctx = {
|
|
2236
|
+
params,
|
|
2237
|
+
pair,
|
|
2238
|
+
logger,
|
|
2239
|
+
getState,
|
|
2240
|
+
claims: [],
|
|
2241
|
+
rejections: [],
|
|
2242
|
+
errors: [],
|
|
2243
|
+
cumulative: { source: 0n, target: 0n },
|
|
2244
|
+
receiptTracker: new ReceiptChainTracker({
|
|
2245
|
+
streamNonce,
|
|
2246
|
+
makerPubkey: params.receiptPubkey ?? params.swapPubkey
|
|
2247
|
+
})
|
|
2248
|
+
};
|
|
2249
|
+
let packetsSent = 0;
|
|
2250
|
+
let abortReason = "complete";
|
|
2251
|
+
let halted = false;
|
|
2252
|
+
let remaining = params.totalAmount;
|
|
2253
|
+
let nextIndex = 0;
|
|
2254
|
+
const isAborted = () => params.signal?.aborted === true;
|
|
2255
|
+
const halt = (reason) => {
|
|
2256
|
+
if (!halted) {
|
|
2257
|
+
halted = true;
|
|
2258
|
+
abortReason = reason;
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
const observeSafe = async (obs) => {
|
|
1401
2262
|
try {
|
|
1402
|
-
|
|
2263
|
+
await controller.observe(obs);
|
|
1403
2264
|
} catch (err) {
|
|
1404
|
-
logger.
|
|
1405
|
-
event: "stream_swap.
|
|
1406
|
-
packetIndex,
|
|
2265
|
+
logger.warn({
|
|
2266
|
+
event: "stream_swap.controller_observe_failed",
|
|
1407
2267
|
error: err instanceof Error ? err.message : String(err)
|
|
1408
2268
|
});
|
|
1409
|
-
errors.push({
|
|
1410
|
-
packetIndex,
|
|
1411
|
-
cause: err instanceof Error ? err : new Error(String(err))
|
|
1412
|
-
});
|
|
1413
|
-
continue;
|
|
1414
2269
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
2270
|
+
};
|
|
2271
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
2272
|
+
for (; ; ) {
|
|
2273
|
+
if (!halted && isAborted()) halt("aborted");
|
|
2274
|
+
if (!halted && getState() === "stopped") halt("stopped");
|
|
2275
|
+
if (!halted && getState() === "paused" && inflight.size === 0) {
|
|
2276
|
+
const resumedBy = await waitForResumeOrStop();
|
|
2277
|
+
if (resumedBy === "stop" || getState() === "stopped") halt("stopped");
|
|
2278
|
+
else if (isAborted()) halt("aborted");
|
|
2279
|
+
}
|
|
2280
|
+
if (!halted && getState() === "running") {
|
|
2281
|
+
const rawWindow = controller.window;
|
|
2282
|
+
const window = Number.isFinite(rawWindow) && rawWindow >= 1 ? Math.floor(rawWindow) : 1;
|
|
2283
|
+
while (remaining > 0n && inflight.size < window) {
|
|
2284
|
+
let delta;
|
|
2285
|
+
try {
|
|
2286
|
+
delta = controller.nextDelta(remaining);
|
|
2287
|
+
} catch (err) {
|
|
2288
|
+
logger.error({
|
|
2289
|
+
event: "stream_swap.controller_next_delta_failed",
|
|
2290
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2291
|
+
});
|
|
2292
|
+
ctx.errors.push({
|
|
2293
|
+
packetIndex: nextIndex,
|
|
2294
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
2295
|
+
});
|
|
2296
|
+
halt("callback-throw");
|
|
2297
|
+
break;
|
|
2298
|
+
}
|
|
2299
|
+
if (typeof delta !== "bigint" || delta < 1n) delta = 1n;
|
|
2300
|
+
if (delta > remaining) delta = remaining;
|
|
2301
|
+
const packetIndex = nextIndex;
|
|
2302
|
+
nextIndex += 1;
|
|
2303
|
+
remaining -= delta;
|
|
2304
|
+
let built;
|
|
2305
|
+
try {
|
|
2306
|
+
built = buildAndWrapPacket({
|
|
2307
|
+
params,
|
|
2308
|
+
pair,
|
|
2309
|
+
senderPubkey,
|
|
2310
|
+
sourceAmount: delta,
|
|
2311
|
+
seq: packetIndex + 1,
|
|
2312
|
+
// Adaptive mode: the final packet count is unknown upfront —
|
|
2313
|
+
// `0` in the rumor's `seq` tag total position means "open".
|
|
2314
|
+
totalPackets: 0,
|
|
2315
|
+
streamNonce
|
|
2316
|
+
});
|
|
2317
|
+
} catch (err) {
|
|
2318
|
+
logger.error({
|
|
2319
|
+
event: "stream_swap.wrap_failed",
|
|
2320
|
+
packetIndex,
|
|
2321
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2322
|
+
});
|
|
2323
|
+
ctx.errors.push({
|
|
2324
|
+
packetIndex,
|
|
2325
|
+
cause: err instanceof Error ? err : new Error(String(err))
|
|
2326
|
+
});
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
const { toonData, packetExpiresAt } = built;
|
|
2330
|
+
const sentAt = Date.now();
|
|
2331
|
+
const promise = (async () => {
|
|
2332
|
+
try {
|
|
2333
|
+
const result = await params.client.sendSwapPacket({
|
|
2334
|
+
destination: params.swapIlpAddress,
|
|
2335
|
+
amount: delta,
|
|
2336
|
+
toonData,
|
|
2337
|
+
timeout: params.packetTimeoutMs ?? 3e4,
|
|
2338
|
+
claim: params.claim,
|
|
2339
|
+
...packetExpiresAt !== void 0 && {
|
|
2340
|
+
expiresAt: packetExpiresAt
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
return {
|
|
2344
|
+
index: packetIndex,
|
|
2345
|
+
sourceAmount: delta,
|
|
2346
|
+
rttMs: Date.now() - sentAt,
|
|
2347
|
+
result
|
|
2348
|
+
};
|
|
2349
|
+
} catch (err) {
|
|
2350
|
+
return {
|
|
2351
|
+
index: packetIndex,
|
|
2352
|
+
sourceAmount: delta,
|
|
2353
|
+
rttMs: Date.now() - sentAt,
|
|
2354
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
2355
|
+
};
|
|
2356
|
+
}
|
|
2357
|
+
})();
|
|
2358
|
+
inflight.set(packetIndex, promise);
|
|
2359
|
+
}
|
|
1431
2360
|
}
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
} catch (err) {
|
|
2361
|
+
if (inflight.size === 0) {
|
|
2362
|
+
if (halted || remaining <= 0n) break;
|
|
2363
|
+
if (getState() === "paused") continue;
|
|
2364
|
+
break;
|
|
2365
|
+
}
|
|
2366
|
+
const settled = await Promise.race(inflight.values());
|
|
2367
|
+
inflight.delete(settled.index);
|
|
2368
|
+
if (settled.error) {
|
|
1441
2369
|
logger.error({
|
|
1442
|
-
event: "stream_swap.
|
|
1443
|
-
packetIndex,
|
|
1444
|
-
error:
|
|
2370
|
+
event: "stream_swap.send_failed",
|
|
2371
|
+
packetIndex: settled.index,
|
|
2372
|
+
error: settled.error.message
|
|
1445
2373
|
});
|
|
1446
|
-
errors.push({
|
|
1447
|
-
|
|
1448
|
-
|
|
2374
|
+
ctx.errors.push({ packetIndex: settled.index, cause: settled.error });
|
|
2375
|
+
await observeSafe({
|
|
2376
|
+
resolution: classifySendError(settled.error),
|
|
2377
|
+
rttMs: settled.rttMs,
|
|
2378
|
+
remaining
|
|
1449
2379
|
});
|
|
1450
2380
|
continue;
|
|
1451
2381
|
}
|
|
1452
|
-
|
|
2382
|
+
packetsSent += 1;
|
|
2383
|
+
const sendResult = settled.result;
|
|
2384
|
+
if (!sendResult.accepted) {
|
|
2385
|
+
const code = sendResult.code ?? "F00";
|
|
2386
|
+
const message = sendResult.message ?? "rejected";
|
|
1453
2387
|
logger.warn({
|
|
1454
|
-
event: "stream_swap.
|
|
1455
|
-
packetIndex
|
|
2388
|
+
event: "stream_swap.packet_rejected",
|
|
2389
|
+
packetIndex: settled.index,
|
|
2390
|
+
code,
|
|
2391
|
+
message
|
|
1456
2392
|
});
|
|
2393
|
+
ctx.rejections.push({
|
|
2394
|
+
packetIndex: settled.index,
|
|
2395
|
+
sourceAmount: settled.sourceAmount,
|
|
2396
|
+
code,
|
|
2397
|
+
message
|
|
2398
|
+
});
|
|
2399
|
+
await observeSafe({
|
|
2400
|
+
resolution: classifyReject(code, message),
|
|
2401
|
+
rttMs: settled.rttMs,
|
|
2402
|
+
remaining
|
|
2403
|
+
});
|
|
2404
|
+
continue;
|
|
1457
2405
|
}
|
|
1458
|
-
const
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
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()
|
|
2406
|
+
const outcome = await processAcceptedPacket(ctx, {
|
|
2407
|
+
packetIndex: settled.index,
|
|
2408
|
+
totalForProgress: nextIndex,
|
|
2409
|
+
sourceAmount: settled.sourceAmount,
|
|
2410
|
+
data: sendResult.data
|
|
1509
2411
|
});
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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)
|
|
2412
|
+
switch (outcome.status) {
|
|
2413
|
+
case "error":
|
|
2414
|
+
await observeSafe({
|
|
2415
|
+
resolution: "error",
|
|
2416
|
+
rttMs: settled.rttMs,
|
|
2417
|
+
remaining
|
|
1533
2418
|
});
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
2419
|
+
break;
|
|
2420
|
+
case "recipient-mismatch":
|
|
2421
|
+
await observeSafe({
|
|
2422
|
+
resolution: "reject",
|
|
2423
|
+
rttMs: settled.rttMs,
|
|
2424
|
+
remaining
|
|
2425
|
+
});
|
|
2426
|
+
break;
|
|
2427
|
+
case "below-floor":
|
|
2428
|
+
await observeSafe({
|
|
2429
|
+
resolution: "reject",
|
|
2430
|
+
rttMs: settled.rttMs,
|
|
2431
|
+
remaining
|
|
1537
2432
|
});
|
|
1538
|
-
|
|
1539
|
-
break
|
|
2433
|
+
halt("below-floor");
|
|
2434
|
+
break;
|
|
2435
|
+
case "receipt-invalid":
|
|
2436
|
+
await observeSafe({
|
|
2437
|
+
resolution: "reject",
|
|
2438
|
+
rttMs: settled.rttMs,
|
|
2439
|
+
remaining
|
|
2440
|
+
});
|
|
2441
|
+
halt("receipt-invalid");
|
|
2442
|
+
break;
|
|
2443
|
+
case "callback-throw":
|
|
2444
|
+
await observeSafe({
|
|
2445
|
+
resolution: "fulfill",
|
|
2446
|
+
rttMs: settled.rttMs,
|
|
2447
|
+
remaining
|
|
2448
|
+
});
|
|
2449
|
+
halt("callback-throw");
|
|
2450
|
+
break;
|
|
2451
|
+
case "accepted": {
|
|
2452
|
+
await observeSafe({
|
|
2453
|
+
resolution: "fulfill",
|
|
2454
|
+
rttMs: settled.rttMs,
|
|
2455
|
+
remaining,
|
|
2456
|
+
sourceAmount: settled.sourceAmount,
|
|
2457
|
+
targetAmount: outcome.targetAmount,
|
|
2458
|
+
...outcome.rate !== void 0 && {
|
|
2459
|
+
rate: outcome.rate,
|
|
2460
|
+
rateTimestamp: outcome.rateTimestamp
|
|
2461
|
+
}
|
|
2462
|
+
});
|
|
2463
|
+
if (isAborted()) halt("aborted");
|
|
2464
|
+
else if (getState() === "stopped") halt("stopped");
|
|
2465
|
+
else if (params.rateDeviationThreshold !== void 0 && outcome.rateDeviation > params.rateDeviationThreshold) {
|
|
2466
|
+
halt("rate-deviation");
|
|
2467
|
+
}
|
|
2468
|
+
break;
|
|
1540
2469
|
}
|
|
1541
2470
|
}
|
|
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
2471
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
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,
|
|
2472
|
+
return finalizeResult({
|
|
2473
|
+
ctx,
|
|
1572
2474
|
abortReason,
|
|
1573
|
-
cumulativeSource,
|
|
1574
|
-
cumulativeTarget,
|
|
1575
2475
|
packetsSent,
|
|
1576
|
-
packetsScheduled:
|
|
1577
|
-
|
|
2476
|
+
packetsScheduled: nextIndex,
|
|
2477
|
+
setState
|
|
2478
|
+
});
|
|
1578
2479
|
}
|
|
1579
2480
|
function getRandomValues(buf) {
|
|
1580
2481
|
const g = globalThis;
|
|
@@ -1599,7 +2500,8 @@ function getRandomValues(buf) {
|
|
|
1599
2500
|
var __testing = {
|
|
1600
2501
|
chunkAmount,
|
|
1601
2502
|
decodeFulfillMetadata,
|
|
1602
|
-
buildSwapRumor
|
|
2503
|
+
buildSwapRumor,
|
|
2504
|
+
compareDecimalRates
|
|
1603
2505
|
};
|
|
1604
2506
|
|
|
1605
2507
|
export {
|
|
@@ -1625,6 +2527,18 @@ export {
|
|
|
1625
2527
|
unwrapSwapPacketFromToon,
|
|
1626
2528
|
encryptFulfillClaim,
|
|
1627
2529
|
decryptFulfillClaim,
|
|
2530
|
+
STREAM_RECEIPT_VERSION,
|
|
2531
|
+
STREAM_RECEIPT_SIGNING_TAG,
|
|
2532
|
+
isValidStreamNonce,
|
|
2533
|
+
parseStreamReceipt,
|
|
2534
|
+
encodeReceiptSigningPayload,
|
|
2535
|
+
signStreamReceipt,
|
|
2536
|
+
verifyStreamReceipt,
|
|
2537
|
+
ReceiptChainTracker,
|
|
2538
|
+
serializeReceiptChain,
|
|
2539
|
+
DEFAULT_RECEIPT_SESSIONS_CAP,
|
|
2540
|
+
BoundedReceiptSessions,
|
|
2541
|
+
issueSessionReceipt,
|
|
1628
2542
|
SWAP_HANDLER_REJECT_CODES,
|
|
1629
2543
|
SWAP_HANDLER_REJECT_MESSAGES,
|
|
1630
2544
|
applyRate,
|
|
@@ -1634,4 +2548,4 @@ export {
|
|
|
1634
2548
|
streamSwapControlled,
|
|
1635
2549
|
__testing
|
|
1636
2550
|
};
|
|
1637
|
-
//# sourceMappingURL=chunk-
|
|
2551
|
+
//# sourceMappingURL=chunk-RAZRWSJF.js.map
|