@toon-protocol/sdk 2.1.0 → 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-7LBYFU4L.js → chunk-RAZRWSJF.js} +493 -33
- package/dist/chunk-RAZRWSJF.js.map +1 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/dist/{swap-Oub-0vqU.d.ts → swap-PFQTJZA7.d.ts} +335 -2
- package/dist/swap.d.ts +1 -1
- package/dist/swap.js +21 -1
- package/package.json +1 -1
- package/dist/chunk-7LBYFU4L.js.map +0 -1
|
@@ -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) {
|
|
@@ -888,6 +1200,32 @@ function createSwapHandler(config) {
|
|
|
888
1200
|
releaseReservation();
|
|
889
1201
|
return ctx.reject("T00", "Internal error");
|
|
890
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
|
+
}
|
|
891
1229
|
const claimBase64 = Buffer.from(ciphertext).toString("base64");
|
|
892
1230
|
logger.info({
|
|
893
1231
|
event: "swap_handler.claim_issued",
|
|
@@ -902,6 +1240,7 @@ function createSwapHandler(config) {
|
|
|
902
1240
|
rateTimestamp
|
|
903
1241
|
};
|
|
904
1242
|
if (claimId !== void 0) metadata["claimId"] = claimId;
|
|
1243
|
+
if (receipt !== void 0) metadata["receipt"] = receipt;
|
|
905
1244
|
if (settlementChannelId !== void 0 && settlementNonce !== void 0 && settlementCumulative !== void 0 && settlementRecipient !== void 0 && settlementSwapSigner !== void 0) {
|
|
906
1245
|
metadata["channelId"] = settlementChannelId;
|
|
907
1246
|
metadata["nonce"] = settlementNonce.toString();
|
|
@@ -928,8 +1267,8 @@ function computePacketId(senderPubkey, sourceAmount, rumor) {
|
|
|
928
1267
|
|
|
929
1268
|
// src/stream-swap.ts
|
|
930
1269
|
import { getPublicKey as getPublicKey3 } from "nostr-tools/pure";
|
|
931
|
-
var
|
|
932
|
-
var
|
|
1270
|
+
var RATE_REGEX3 = /^(0|[1-9]\d*)(\.\d+)?$/;
|
|
1271
|
+
var HEX64_REGEX2 = /^[0-9a-f]{64}$/;
|
|
933
1272
|
var BASE64_REGEX = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
934
1273
|
function isBase64(s) {
|
|
935
1274
|
if (s.length === 0 || s.length % 4 !== 0) return false;
|
|
@@ -965,26 +1304,29 @@ function buildSwapRumor(input) {
|
|
|
965
1304
|
totalPackets,
|
|
966
1305
|
nonce,
|
|
967
1306
|
createdAt,
|
|
968
|
-
chainRecipient
|
|
1307
|
+
chainRecipient,
|
|
1308
|
+
streamNonce
|
|
969
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]);
|
|
970
1319
|
return {
|
|
971
1320
|
kind: 20032,
|
|
972
1321
|
pubkey: senderPubkey,
|
|
973
1322
|
content: "",
|
|
974
1323
|
created_at: createdAt,
|
|
975
|
-
tags
|
|
976
|
-
["swap-from", `${pair.from.assetCode}:${pair.from.chain}`],
|
|
977
|
-
["swap-to", `${pair.to.assetCode}:${pair.to.chain}`],
|
|
978
|
-
["amount", sourceAmount.toString()],
|
|
979
|
-
["seq", String(packetIndex), String(totalPackets)],
|
|
980
|
-
["nonce", Buffer.from(nonce).toString("hex")],
|
|
981
|
-
["chain-recipient", chainRecipient]
|
|
982
|
-
]
|
|
1324
|
+
tags
|
|
983
1325
|
};
|
|
984
1326
|
}
|
|
985
1327
|
var EVM_CHANNEL_ID_REGEX = /^0x[0-9a-f]{64}$/;
|
|
986
1328
|
var EVM_ADDRESS_REGEX = /^0x[0-9a-f]{40}$/;
|
|
987
|
-
var
|
|
1329
|
+
var DECIMAL_UINT_REGEX2 = /^(0|[1-9]\d*)$/;
|
|
988
1330
|
var BASE58_REGEX = /^[1-9A-HJ-NP-Za-km-z]+$/;
|
|
989
1331
|
function validateChainAddress(value, chain, kind) {
|
|
990
1332
|
if (chain.startsWith("evm:")) {
|
|
@@ -1051,7 +1393,7 @@ function decodeFulfillMetadata(data, chain, opts) {
|
|
|
1051
1393
|
"FULFILL metadata.claim is missing or not base64 string"
|
|
1052
1394
|
);
|
|
1053
1395
|
}
|
|
1054
|
-
if (typeof ephemeralPubkey !== "string" || !
|
|
1396
|
+
if (typeof ephemeralPubkey !== "string" || !HEX64_REGEX2.test(ephemeralPubkey)) {
|
|
1055
1397
|
throw new StreamSwapError(
|
|
1056
1398
|
"FULFILL_DECODE_FAILED",
|
|
1057
1399
|
"FULFILL metadata.ephemeralPubkey is missing or not 64-char hex"
|
|
@@ -1085,7 +1427,7 @@ function decodeFulfillMetadata(data, chain, opts) {
|
|
|
1085
1427
|
);
|
|
1086
1428
|
}
|
|
1087
1429
|
if (hasRate) {
|
|
1088
|
-
if (typeof tapeRate !== "string" || !
|
|
1430
|
+
if (typeof tapeRate !== "string" || !RATE_REGEX3.test(tapeRate) || /^0(\.0+)?$/.test(tapeRate)) {
|
|
1089
1431
|
throw new StreamSwapError(
|
|
1090
1432
|
"FULFILL_DECODE_FAILED",
|
|
1091
1433
|
"FULFILL metadata.rate must be a positive decimal string"
|
|
@@ -1105,16 +1447,27 @@ function decodeFulfillMetadata(data, chain, opts) {
|
|
|
1105
1447
|
"FULFILL metadata is missing the quote tape (rate + rateTimestamp) required when minExchangeRate is set"
|
|
1106
1448
|
);
|
|
1107
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
|
+
}
|
|
1108
1461
|
const channelId = obj["channelId"];
|
|
1109
1462
|
if (typeof channelId === "string" && (!chain || validateChainAddress(channelId, chain, "channelId"))) {
|
|
1110
1463
|
result.channelId = channelId;
|
|
1111
1464
|
}
|
|
1112
1465
|
const nonce = obj["nonce"];
|
|
1113
|
-
if (typeof nonce === "string" &&
|
|
1466
|
+
if (typeof nonce === "string" && DECIMAL_UINT_REGEX2.test(nonce)) {
|
|
1114
1467
|
result.nonce = nonce;
|
|
1115
1468
|
}
|
|
1116
1469
|
const cumulativeAmount = obj["cumulativeAmount"];
|
|
1117
|
-
if (typeof cumulativeAmount === "string" &&
|
|
1470
|
+
if (typeof cumulativeAmount === "string" && DECIMAL_UINT_REGEX2.test(cumulativeAmount)) {
|
|
1118
1471
|
result.cumulativeAmount = cumulativeAmount;
|
|
1119
1472
|
}
|
|
1120
1473
|
const recipient = obj["recipient"];
|
|
@@ -1230,7 +1583,7 @@ function validateParams(params) {
|
|
|
1230
1583
|
"senderSecretKey must be a 32-byte Uint8Array"
|
|
1231
1584
|
);
|
|
1232
1585
|
}
|
|
1233
|
-
if (typeof params.swapPubkey !== "string" || !
|
|
1586
|
+
if (typeof params.swapPubkey !== "string" || !HEX64_REGEX2.test(params.swapPubkey)) {
|
|
1234
1587
|
throw new StreamSwapError(
|
|
1235
1588
|
"INVALID_STATE",
|
|
1236
1589
|
"swapPubkey must be a 64-char lowercase hex string"
|
|
@@ -1251,10 +1604,10 @@ function validateParams(params) {
|
|
|
1251
1604
|
"pair.to must have { assetCode: string, assetScale: number, chain: string }"
|
|
1252
1605
|
);
|
|
1253
1606
|
}
|
|
1254
|
-
if (typeof params.pair.rate !== "string" || !
|
|
1607
|
+
if (typeof params.pair.rate !== "string" || !RATE_REGEX3.test(params.pair.rate)) {
|
|
1255
1608
|
throw new StreamSwapError(
|
|
1256
1609
|
"INVALID_PAIR",
|
|
1257
|
-
`pair.rate must match ${
|
|
1610
|
+
`pair.rate must match ${RATE_REGEX3}, got ${params.pair.rate}`
|
|
1258
1611
|
);
|
|
1259
1612
|
}
|
|
1260
1613
|
try {
|
|
@@ -1278,15 +1631,27 @@ function validateParams(params) {
|
|
|
1278
1631
|
);
|
|
1279
1632
|
}
|
|
1280
1633
|
if (params.minExchangeRate !== void 0) {
|
|
1281
|
-
if (typeof params.minExchangeRate !== "string" || !
|
|
1634
|
+
if (typeof params.minExchangeRate !== "string" || !RATE_REGEX3.test(params.minExchangeRate) || /^0(\.0+)?$/.test(params.minExchangeRate)) {
|
|
1282
1635
|
throw new StreamSwapError(
|
|
1283
1636
|
"INVALID_STATE",
|
|
1284
|
-
`minExchangeRate must be a positive decimal string matching ${
|
|
1637
|
+
`minExchangeRate must be a positive decimal string matching ${RATE_REGEX3}, got ${String(
|
|
1285
1638
|
params.minExchangeRate
|
|
1286
1639
|
)}`
|
|
1287
1640
|
);
|
|
1288
1641
|
}
|
|
1289
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
|
+
}
|
|
1290
1655
|
if (params.packetExpiryMs !== void 0 && (typeof params.packetExpiryMs !== "number" || !Number.isInteger(params.packetExpiryMs) || params.packetExpiryMs <= 0)) {
|
|
1291
1656
|
throw new StreamSwapError(
|
|
1292
1657
|
"INVALID_STATE",
|
|
@@ -1323,6 +1688,9 @@ function streamSwapControlled(params) {
|
|
|
1323
1688
|
rate: params.pair.rate
|
|
1324
1689
|
});
|
|
1325
1690
|
const senderPubkey = getPublicKey3(params.senderSecretKey);
|
|
1691
|
+
const streamNonceBytes = new Uint8Array(16);
|
|
1692
|
+
getRandomValues(streamNonceBytes);
|
|
1693
|
+
const streamNonce = Buffer.from(streamNonceBytes).toString("hex");
|
|
1326
1694
|
let streamState = "running";
|
|
1327
1695
|
let resumeDeferred = null;
|
|
1328
1696
|
const controller = {
|
|
@@ -1373,6 +1741,7 @@ function streamSwapControlled(params) {
|
|
|
1373
1741
|
params,
|
|
1374
1742
|
frozenPair,
|
|
1375
1743
|
senderPubkey,
|
|
1744
|
+
streamNonce,
|
|
1376
1745
|
logger,
|
|
1377
1746
|
getState,
|
|
1378
1747
|
setState,
|
|
@@ -1382,6 +1751,7 @@ function streamSwapControlled(params) {
|
|
|
1382
1751
|
frozenPair,
|
|
1383
1752
|
schedule,
|
|
1384
1753
|
senderPubkey,
|
|
1754
|
+
streamNonce,
|
|
1385
1755
|
logger,
|
|
1386
1756
|
getState,
|
|
1387
1757
|
setState,
|
|
@@ -1390,7 +1760,15 @@ function streamSwapControlled(params) {
|
|
|
1390
1760
|
return { result, controller };
|
|
1391
1761
|
}
|
|
1392
1762
|
function buildAndWrapPacket(input) {
|
|
1393
|
-
const {
|
|
1763
|
+
const {
|
|
1764
|
+
params,
|
|
1765
|
+
pair,
|
|
1766
|
+
senderPubkey,
|
|
1767
|
+
sourceAmount,
|
|
1768
|
+
seq,
|
|
1769
|
+
totalPackets,
|
|
1770
|
+
streamNonce
|
|
1771
|
+
} = input;
|
|
1394
1772
|
const nonce = new Uint8Array(16);
|
|
1395
1773
|
getRandomValues(nonce);
|
|
1396
1774
|
const rumor = buildSwapRumor({
|
|
@@ -1401,7 +1779,8 @@ function buildAndWrapPacket(input) {
|
|
|
1401
1779
|
totalPackets,
|
|
1402
1780
|
nonce,
|
|
1403
1781
|
createdAt: Math.floor(Date.now() / 1e3),
|
|
1404
|
-
chainRecipient: params.chainRecipient
|
|
1782
|
+
chainRecipient: params.chainRecipient,
|
|
1783
|
+
streamNonce
|
|
1405
1784
|
});
|
|
1406
1785
|
const packetExpiresAt = params.packetExpiryMs !== void 0 ? new Date(Date.now() + params.packetExpiryMs) : void 0;
|
|
1407
1786
|
const wrapped = wrapSwapPacketToToon({
|
|
@@ -1491,6 +1870,47 @@ async function processAcceptedPacket(ctx, args) {
|
|
|
1491
1870
|
return { status: "below-floor" };
|
|
1492
1871
|
}
|
|
1493
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
|
+
}
|
|
1494
1914
|
let claimBytes;
|
|
1495
1915
|
try {
|
|
1496
1916
|
const ciphertext = new Uint8Array(Buffer.from(metadata.claim, "base64"));
|
|
@@ -1566,6 +1986,7 @@ async function processAcceptedPacket(ctx, args) {
|
|
|
1566
1986
|
accumulated.rate = metadata.rate;
|
|
1567
1987
|
accumulated.rateTimestamp = metadata.rateTimestamp;
|
|
1568
1988
|
}
|
|
1989
|
+
if (verifiedReceipt !== void 0) accumulated.receipt = verifiedReceipt;
|
|
1569
1990
|
ctx.claims.push(accumulated);
|
|
1570
1991
|
logger.debug({
|
|
1571
1992
|
event: "stream_swap.packet_accepted",
|
|
@@ -1590,6 +2011,8 @@ async function processAcceptedPacket(ctx, args) {
|
|
|
1590
2011
|
rate: metadata.rate,
|
|
1591
2012
|
rateTimestamp: metadata.rateTimestamp
|
|
1592
2013
|
},
|
|
2014
|
+
// Issue #84: surface the verified per-fulfill receipt to the callback.
|
|
2015
|
+
...verifiedReceipt !== void 0 && { receipt: verifiedReceipt },
|
|
1593
2016
|
state: ctx.getState() === "paused" ? "paused" : ctx.getState() === "stopped" ? "stopped" : "running"
|
|
1594
2017
|
});
|
|
1595
2018
|
try {
|
|
@@ -1644,10 +2067,13 @@ function finalizeResult(input) {
|
|
|
1644
2067
|
cumulativeSource: cumulative.source,
|
|
1645
2068
|
cumulativeTarget: cumulative.target,
|
|
1646
2069
|
packetsSent: input.packetsSent,
|
|
1647
|
-
packetsScheduled: input.packetsScheduled
|
|
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()
|
|
1648
2074
|
};
|
|
1649
2075
|
}
|
|
1650
|
-
async function runLoop(params, pair, schedule, senderPubkey, logger, getState, setState, waitForResumeOrStop) {
|
|
2076
|
+
async function runLoop(params, pair, schedule, senderPubkey, streamNonce, logger, getState, setState, waitForResumeOrStop) {
|
|
1651
2077
|
const ctx = {
|
|
1652
2078
|
params,
|
|
1653
2079
|
pair,
|
|
@@ -1656,7 +2082,11 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
|
|
|
1656
2082
|
claims: [],
|
|
1657
2083
|
rejections: [],
|
|
1658
2084
|
errors: [],
|
|
1659
|
-
cumulative: { source: 0n, target: 0n }
|
|
2085
|
+
cumulative: { source: 0n, target: 0n },
|
|
2086
|
+
receiptTracker: new ReceiptChainTracker({
|
|
2087
|
+
streamNonce,
|
|
2088
|
+
makerPubkey: params.receiptPubkey ?? params.swapPubkey
|
|
2089
|
+
})
|
|
1660
2090
|
};
|
|
1661
2091
|
let packetsSent = 0;
|
|
1662
2092
|
let abortReason = "complete";
|
|
@@ -1697,7 +2127,8 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
|
|
|
1697
2127
|
senderPubkey,
|
|
1698
2128
|
sourceAmount,
|
|
1699
2129
|
seq: packetIndex + 1,
|
|
1700
|
-
totalPackets
|
|
2130
|
+
totalPackets,
|
|
2131
|
+
streamNonce
|
|
1701
2132
|
});
|
|
1702
2133
|
} catch (err) {
|
|
1703
2134
|
logger.error({
|
|
@@ -1760,6 +2191,10 @@ async function runLoop(params, pair, schedule, senderPubkey, logger, getState, s
|
|
|
1760
2191
|
abortReason = "below-floor";
|
|
1761
2192
|
break packetLoop;
|
|
1762
2193
|
}
|
|
2194
|
+
if (outcome.status === "receipt-invalid") {
|
|
2195
|
+
abortReason = "receipt-invalid";
|
|
2196
|
+
break packetLoop;
|
|
2197
|
+
}
|
|
1763
2198
|
if (outcome.status === "callback-throw") {
|
|
1764
2199
|
abortReason = "callback-throw";
|
|
1765
2200
|
break packetLoop;
|
|
@@ -1795,7 +2230,7 @@ function classifyReject(code, message) {
|
|
|
1795
2230
|
}
|
|
1796
2231
|
return "reject";
|
|
1797
2232
|
}
|
|
1798
|
-
async function runAdaptiveLoop(params, pair, senderPubkey, logger, getState, setState, waitForResumeOrStop) {
|
|
2233
|
+
async function runAdaptiveLoop(params, pair, senderPubkey, streamNonce, logger, getState, setState, waitForResumeOrStop) {
|
|
1799
2234
|
const controller = params.controller;
|
|
1800
2235
|
const ctx = {
|
|
1801
2236
|
params,
|
|
@@ -1805,7 +2240,11 @@ async function runAdaptiveLoop(params, pair, senderPubkey, logger, getState, set
|
|
|
1805
2240
|
claims: [],
|
|
1806
2241
|
rejections: [],
|
|
1807
2242
|
errors: [],
|
|
1808
|
-
cumulative: { source: 0n, target: 0n }
|
|
2243
|
+
cumulative: { source: 0n, target: 0n },
|
|
2244
|
+
receiptTracker: new ReceiptChainTracker({
|
|
2245
|
+
streamNonce,
|
|
2246
|
+
makerPubkey: params.receiptPubkey ?? params.swapPubkey
|
|
2247
|
+
})
|
|
1809
2248
|
};
|
|
1810
2249
|
let packetsSent = 0;
|
|
1811
2250
|
let abortReason = "complete";
|
|
@@ -1872,7 +2311,8 @@ async function runAdaptiveLoop(params, pair, senderPubkey, logger, getState, set
|
|
|
1872
2311
|
seq: packetIndex + 1,
|
|
1873
2312
|
// Adaptive mode: the final packet count is unknown upfront —
|
|
1874
2313
|
// `0` in the rumor's `seq` tag total position means "open".
|
|
1875
|
-
totalPackets: 0
|
|
2314
|
+
totalPackets: 0,
|
|
2315
|
+
streamNonce
|
|
1876
2316
|
});
|
|
1877
2317
|
} catch (err) {
|
|
1878
2318
|
logger.error({
|
|
@@ -1992,6 +2432,14 @@ async function runAdaptiveLoop(params, pair, senderPubkey, logger, getState, set
|
|
|
1992
2432
|
});
|
|
1993
2433
|
halt("below-floor");
|
|
1994
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;
|
|
1995
2443
|
case "callback-throw":
|
|
1996
2444
|
await observeSafe({
|
|
1997
2445
|
resolution: "fulfill",
|
|
@@ -2079,6 +2527,18 @@ export {
|
|
|
2079
2527
|
unwrapSwapPacketFromToon,
|
|
2080
2528
|
encryptFulfillClaim,
|
|
2081
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,
|
|
2082
2542
|
SWAP_HANDLER_REJECT_CODES,
|
|
2083
2543
|
SWAP_HANDLER_REJECT_MESSAGES,
|
|
2084
2544
|
applyRate,
|
|
@@ -2088,4 +2548,4 @@ export {
|
|
|
2088
2548
|
streamSwapControlled,
|
|
2089
2549
|
__testing
|
|
2090
2550
|
};
|
|
2091
|
-
//# sourceMappingURL=chunk-
|
|
2551
|
+
//# sourceMappingURL=chunk-RAZRWSJF.js.map
|