@elisym/commerce 0.3.0 → 0.4.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/buyer.d.ts +203 -17
- package/dist/buyer.js +492 -24
- package/dist/buyer.js.map +1 -1
- package/package.json +2 -2
package/dist/buyer.js
CHANGED
|
@@ -2,8 +2,9 @@ import { MAX_FUTURE_SKEW_SECS, isPublicHostname, KIND_INBOX_RELAYS, decodeProduc
|
|
|
2
2
|
import { verifyEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
|
|
3
3
|
import { getTransactionDecoder, partiallySignTransaction, getTransactionEncoder, isAddress, address, getCompiledTransactionMessageDecoder, verifySignature, getPublicKeyFromAddress, getBase64Decoder, getBase58Decoder, createNoopSigner, pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, appendTransactionMessageInstructions, compileTransaction } from '@solana/kit';
|
|
4
4
|
import { bytesToHex, hexToBytes, normalizeURL } from 'nostr-tools/utils';
|
|
5
|
-
import { parsePaymentRequest, composeSolanaPaymentRequest, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, directInstructionsFromCompiledMessage, boundTransferAmount, buildPaymentInstructions, getProtocolProgramId, verifyDirectSolanaPayment } from '@elisym/pay-core';
|
|
5
|
+
import { parsePaymentRequest, composeSolanaPaymentRequest, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, directInstructionsFromCompiledMessage, boundTransferAmount, PaymentRequestV2Schema, buildPaymentInstructions, getProtocolProgramId, verifyDirectSolanaPayment } from '@elisym/pay-core';
|
|
6
6
|
import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda } from '@solana-program/token';
|
|
7
|
+
import { readFinalizedBlock, MIN_PAY_WINDOW_SECS, buildTempoPaymentCalls, readTxHash, verifyTempoPayment, EVM_LATE_PAYMENT_GRACE_SECS, checkEvmChain, checkTempoReceivePolicies, composeTempoPaymentRequest } from '@elisym/pay-core/evm';
|
|
7
8
|
import { SimplePool } from 'nostr-tools/pool';
|
|
8
9
|
|
|
9
10
|
// src/buyer/constants.ts
|
|
@@ -172,8 +173,8 @@ function isPageOrigin(value) {
|
|
|
172
173
|
function isSnapshotStale(snapshotAt, now = nowSecs()) {
|
|
173
174
|
return now - snapshotAt > OFFER_SNAPSHOT_MAX_AGE_SECS;
|
|
174
175
|
}
|
|
175
|
-
function refuse(
|
|
176
|
-
return { ok: false, refusal, message };
|
|
176
|
+
function refuse(refusal2, message) {
|
|
177
|
+
return { ok: false, refusal: refusal2, message };
|
|
177
178
|
}
|
|
178
179
|
async function loadOffer(naddr, options) {
|
|
179
180
|
if (!isPageOrigin(options.pageOrigin)) {
|
|
@@ -617,7 +618,10 @@ var PATCH_KEYS = [
|
|
|
617
618
|
"inboxRelays",
|
|
618
619
|
"acknowledgedRelays",
|
|
619
620
|
"paidTx",
|
|
620
|
-
"
|
|
621
|
+
"paidAt",
|
|
622
|
+
"status",
|
|
623
|
+
"endedBy",
|
|
624
|
+
"confirmedOverIds"
|
|
621
625
|
];
|
|
622
626
|
var STATUS_SUCCESSORS = {
|
|
623
627
|
pending: ["pending", "confirmed", "completed", "cancelled"],
|
|
@@ -653,6 +657,20 @@ function patchRefusal(current, patch) {
|
|
|
653
657
|
if (patch.paidTx !== void 0 && (current.paidTx !== void 0 || !PAID_STATES.includes(nextState))) {
|
|
654
658
|
return "not_ready";
|
|
655
659
|
}
|
|
660
|
+
if (patch.paidAt !== void 0 && patch.paidTx === void 0) {
|
|
661
|
+
return "not_ready";
|
|
662
|
+
}
|
|
663
|
+
const tempo = railOf(current) === "tempo";
|
|
664
|
+
const endsUnpaid = patch.state === "ended-unpaid" && current.state !== "ended-unpaid";
|
|
665
|
+
if (patch.endedBy !== void 0 && (!tempo || !endsUnpaid || patch.endedBy !== "nothing")) {
|
|
666
|
+
return "not_ready";
|
|
667
|
+
}
|
|
668
|
+
if (tempo && endsUnpaid && patch.endedBy === void 0) {
|
|
669
|
+
return "not_ready";
|
|
670
|
+
}
|
|
671
|
+
if (patch.confirmedOverIds !== void 0 && !(current.confirmedOverIds ?? []).every((id) => patch.confirmedOverIds?.includes(id))) {
|
|
672
|
+
return "not_ready";
|
|
673
|
+
}
|
|
656
674
|
if (patch.receiptWrap !== void 0 && current.receiptWrap !== void 0 && current.state !== "paying") {
|
|
657
675
|
return "not_ready";
|
|
658
676
|
}
|
|
@@ -742,13 +760,21 @@ function judgeUpdate(records, orderId, expectedVersion, patch) {
|
|
|
742
760
|
Object.assign(defined, { [key]: patch[key] });
|
|
743
761
|
}
|
|
744
762
|
}
|
|
745
|
-
const
|
|
746
|
-
if (
|
|
747
|
-
return refused(
|
|
763
|
+
const refusal2 = patchRefusal(current, defined);
|
|
764
|
+
if (refusal2 !== void 0) {
|
|
765
|
+
return refused(refusal2);
|
|
748
766
|
}
|
|
749
767
|
return accepted({ ...current, ...defined, version: current.version + 1 });
|
|
750
768
|
}
|
|
751
|
-
function
|
|
769
|
+
function unconfirmedOver(records, current, now) {
|
|
770
|
+
return records.filter(
|
|
771
|
+
(sibling) => sibling.orderId !== current.orderId && railOf(sibling) === "tempo" && sibling.state === "ended-unpaid" && (sibling.endedBy === "over" || sibling.endedBy === void 0) && (now === void 0 || now - sibling.createdAt <= MERCHANT_CATCH_UP_SECS + MAX_CLOCK_SKEW_SECS) && !(current.confirmedOverIds ?? []).includes(sibling.orderId)
|
|
772
|
+
).map((sibling) => sibling.orderId);
|
|
773
|
+
}
|
|
774
|
+
function needsConfirmation(unconfirmed) {
|
|
775
|
+
return { result: { ok: false, reason: "needs_confirmation", unconfirmed } };
|
|
776
|
+
}
|
|
777
|
+
function judgeSetMarker(records, orderId, expectedVersion, marker, now) {
|
|
752
778
|
const current = judgedRecord(records, orderId, expectedVersion);
|
|
753
779
|
if (isJudged(current)) {
|
|
754
780
|
return current;
|
|
@@ -760,9 +786,13 @@ function judgeSetMarker(records, orderId, expectedVersion, marker) {
|
|
|
760
786
|
if (holder !== void 0) {
|
|
761
787
|
return refused("exclusion", holder.orderId);
|
|
762
788
|
}
|
|
789
|
+
const unconfirmed = unconfirmedOver(records, current, now);
|
|
790
|
+
if (unconfirmed.length > 0) {
|
|
791
|
+
return needsConfirmation(unconfirmed);
|
|
792
|
+
}
|
|
763
793
|
return accepted({ ...current, marker, state: "paying", version: current.version + 1 });
|
|
764
794
|
}
|
|
765
|
-
function judgeUpdateMarker(records, orderId, expectedVersion, attemptId, next) {
|
|
795
|
+
function judgeUpdateMarker(records, orderId, expectedVersion, attemptId, next, now) {
|
|
766
796
|
const current = records.find((record) => record.orderId === orderId);
|
|
767
797
|
if (current === void 0) {
|
|
768
798
|
return refused("missing");
|
|
@@ -786,10 +816,14 @@ function judgeUpdateMarker(records, orderId, expectedVersion, attemptId, next) {
|
|
|
786
816
|
if (holder !== void 0) {
|
|
787
817
|
return refused("exclusion", holder.orderId);
|
|
788
818
|
}
|
|
819
|
+
const unconfirmed = unconfirmedOver(records, current, now);
|
|
820
|
+
if (unconfirmed.length > 0) {
|
|
821
|
+
return needsConfirmation(unconfirmed);
|
|
822
|
+
}
|
|
789
823
|
}
|
|
790
824
|
return accepted({ ...current, marker: next, version: current.version + 1 });
|
|
791
825
|
}
|
|
792
|
-
function judgeClearMarker(records, orderId, expectedVersion, attemptId, nextState) {
|
|
826
|
+
function judgeClearMarker(records, orderId, expectedVersion, attemptId, nextState, endedBy) {
|
|
793
827
|
const current = records.find((record) => record.orderId === orderId);
|
|
794
828
|
if (current === void 0) {
|
|
795
829
|
return refused("missing");
|
|
@@ -800,7 +834,16 @@ function judgeClearMarker(records, orderId, expectedVersion, attemptId, nextStat
|
|
|
800
834
|
if (current.state !== "paying") {
|
|
801
835
|
return refused("not_ready");
|
|
802
836
|
}
|
|
803
|
-
|
|
837
|
+
const tempo = current.marker?.rail === "tempo";
|
|
838
|
+
if (nextState === "ended-unpaid" && tempo) {
|
|
839
|
+
if (current.marker !== void 0 && holdsEvidence(current.marker)) {
|
|
840
|
+
return refused("not_ready");
|
|
841
|
+
}
|
|
842
|
+
if (endedBy === void 0) {
|
|
843
|
+
return refused("not_ready");
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
if (endedBy !== void 0 && (!tempo || nextState !== "ended-unpaid")) {
|
|
804
847
|
return refused("not_ready");
|
|
805
848
|
}
|
|
806
849
|
if (nextState === "ordered" && current.marker !== void 0 && holdsEvidence(current.marker)) {
|
|
@@ -810,7 +853,12 @@ function judgeClearMarker(records, orderId, expectedVersion, attemptId, nextStat
|
|
|
810
853
|
const { marker: _removed, ...rest } = current;
|
|
811
854
|
return accepted({ ...rest, state: nextState, version: current.version + 1 });
|
|
812
855
|
}
|
|
813
|
-
return accepted({
|
|
856
|
+
return accepted({
|
|
857
|
+
...current,
|
|
858
|
+
state: nextState,
|
|
859
|
+
...endedBy === void 0 ? {} : { endedBy },
|
|
860
|
+
version: current.version + 1
|
|
861
|
+
});
|
|
814
862
|
}
|
|
815
863
|
function mergePins(current, storePubkey, ownerPubkey, payouts) {
|
|
816
864
|
if (current !== void 0 && current.pinnedOwnerPubkey !== ownerPubkey) {
|
|
@@ -861,24 +909,24 @@ var OrderStore = class {
|
|
|
861
909
|
return this.judge(orderId, (records) => judgeUpdate(records, orderId, expectedVersion, patch));
|
|
862
910
|
}
|
|
863
911
|
/** See `judgeSetMarker`. */
|
|
864
|
-
setMarker(orderId, expectedVersion, marker) {
|
|
912
|
+
setMarker(orderId, expectedVersion, marker, now) {
|
|
865
913
|
return this.judge(
|
|
866
914
|
orderId,
|
|
867
|
-
(records) => judgeSetMarker(records, orderId, expectedVersion, marker)
|
|
915
|
+
(records) => judgeSetMarker(records, orderId, expectedVersion, marker, now)
|
|
868
916
|
);
|
|
869
917
|
}
|
|
870
918
|
/** See `judgeUpdateMarker`. */
|
|
871
|
-
updateMarker(orderId, expectedVersion, attemptId, next) {
|
|
919
|
+
updateMarker(orderId, expectedVersion, attemptId, next, now) {
|
|
872
920
|
return this.judge(
|
|
873
921
|
orderId,
|
|
874
|
-
(records) => judgeUpdateMarker(records, orderId, expectedVersion, attemptId, next)
|
|
922
|
+
(records) => judgeUpdateMarker(records, orderId, expectedVersion, attemptId, next, now)
|
|
875
923
|
);
|
|
876
924
|
}
|
|
877
925
|
/** See `judgeClearMarker`. */
|
|
878
|
-
clearMarker(orderId, expectedVersion, attemptId, nextState) {
|
|
926
|
+
clearMarker(orderId, expectedVersion, attemptId, nextState, endedBy) {
|
|
879
927
|
return this.judge(
|
|
880
928
|
orderId,
|
|
881
|
-
(records) => judgeClearMarker(records, orderId, expectedVersion, attemptId, nextState)
|
|
929
|
+
(records) => judgeClearMarker(records, orderId, expectedVersion, attemptId, nextState, endedBy)
|
|
882
930
|
);
|
|
883
931
|
}
|
|
884
932
|
pins(storePubkey) {
|
|
@@ -1272,6 +1320,14 @@ function storeRefusal(written, record) {
|
|
|
1272
1320
|
...written.holder === void 0 ? {} : { holder: written.holder }
|
|
1273
1321
|
};
|
|
1274
1322
|
}
|
|
1323
|
+
if (written.reason === "needs_confirmation") {
|
|
1324
|
+
return {
|
|
1325
|
+
ok: false,
|
|
1326
|
+
reason: "needs_confirmation",
|
|
1327
|
+
record,
|
|
1328
|
+
unconfirmed: written.unconfirmed ?? []
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1275
1331
|
return { ok: false, reason: written.reason === "conflict" ? "conflict" : "not_payable", record };
|
|
1276
1332
|
}
|
|
1277
1333
|
async function withReleaseOnThrow(attemptId, deps, write) {
|
|
@@ -1324,7 +1380,7 @@ async function payWithSolana(record, wallet, input, deps) {
|
|
|
1324
1380
|
const marked = await withReleaseOnThrow(
|
|
1325
1381
|
marker.attemptId,
|
|
1326
1382
|
deps,
|
|
1327
|
-
() => deps.store.setMarker(record.orderId, record.version, marker)
|
|
1383
|
+
() => deps.store.setMarker(record.orderId, record.version, marker, (deps.now ?? nowSecs)())
|
|
1328
1384
|
);
|
|
1329
1385
|
if (!marked.ok) {
|
|
1330
1386
|
deps.release?.(marker.attemptId);
|
|
@@ -1427,7 +1483,8 @@ async function recordPaid(record, signature, deps) {
|
|
|
1427
1483
|
}
|
|
1428
1484
|
const written = await deps.store.update(current.orderId, current.version, {
|
|
1429
1485
|
state: "paid",
|
|
1430
|
-
paidTx: signature
|
|
1486
|
+
paidTx: signature,
|
|
1487
|
+
paidAt: (deps.now ?? nowSecs)()
|
|
1431
1488
|
});
|
|
1432
1489
|
if (written.ok) {
|
|
1433
1490
|
return written.record;
|
|
@@ -1527,7 +1584,13 @@ async function retryWithSolana(record, wallet, input, deps) {
|
|
|
1527
1584
|
const replaced = await withReleaseOnThrow(
|
|
1528
1585
|
marker.attemptId,
|
|
1529
1586
|
deps,
|
|
1530
|
-
() => deps.store.updateMarker(
|
|
1587
|
+
() => deps.store.updateMarker(
|
|
1588
|
+
judged.orderId,
|
|
1589
|
+
judged.version,
|
|
1590
|
+
previous.attemptId,
|
|
1591
|
+
marker,
|
|
1592
|
+
(deps.now ?? nowSecs)()
|
|
1593
|
+
)
|
|
1531
1594
|
);
|
|
1532
1595
|
if (!replaced.ok) {
|
|
1533
1596
|
deps.release?.(marker.attemptId);
|
|
@@ -1557,6 +1620,406 @@ async function endSolanaOrder(record, deps) {
|
|
|
1557
1620
|
);
|
|
1558
1621
|
return written.ok ? { ended: true, record: written.record } : { ended: false, record: watch.record };
|
|
1559
1622
|
}
|
|
1623
|
+
var BALANCE_OF_SELECTOR = "0x70a08231";
|
|
1624
|
+
var EVM_ADDRESS_RE = /^0x[0-9a-f]{40}$/;
|
|
1625
|
+
var UINT256_RE = /^0x[0-9a-fA-F]{64}$/;
|
|
1626
|
+
function tempoFeeMargin(decimals) {
|
|
1627
|
+
return decimals > 2 ? 10n ** BigInt(decimals - 2) : 1n;
|
|
1628
|
+
}
|
|
1629
|
+
function tempoTargetOf(record) {
|
|
1630
|
+
const caip19 = parseCaip19(record.payout.caip19);
|
|
1631
|
+
const mint = caip19?.asset.mint;
|
|
1632
|
+
if (caip19 === void 0 || caip19.chain.family !== "evm" || mint === void 0) {
|
|
1633
|
+
return void 0;
|
|
1634
|
+
}
|
|
1635
|
+
return { chain: caip19.chain, asset: caip19.asset, token: mint.toLowerCase() };
|
|
1636
|
+
}
|
|
1637
|
+
function storedTempoRequest(record) {
|
|
1638
|
+
if (record.paymentRequest === void 0) {
|
|
1639
|
+
return void 0;
|
|
1640
|
+
}
|
|
1641
|
+
try {
|
|
1642
|
+
const parsed = PaymentRequestV2Schema.safeParse(JSON.parse(record.paymentRequest));
|
|
1643
|
+
return parsed.success ? parsed.data : void 0;
|
|
1644
|
+
} catch {
|
|
1645
|
+
return void 0;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function tempoRequestMatches(request, record, target) {
|
|
1649
|
+
return request.chain === target.chain.caip2 && request.asset.toLowerCase() === `${target.chain.caip2}/erc20:${target.token}` && request.recipient === record.payout.address.toLowerCase() && request.memo === record.reference && BigInt(request.amount) === BigInt(record.amount) && request.fee_address === void 0 && (request.fee_amount === void 0 || BigInt(request.fee_amount) === 0n);
|
|
1650
|
+
}
|
|
1651
|
+
async function readTempoChainTime(client) {
|
|
1652
|
+
const head = await readFinalizedBlock(client);
|
|
1653
|
+
if (head === null) {
|
|
1654
|
+
throw new Error("The Tempo chain could not be read.");
|
|
1655
|
+
}
|
|
1656
|
+
return head.timestamp;
|
|
1657
|
+
}
|
|
1658
|
+
async function tokenBalance2(client, token, owner) {
|
|
1659
|
+
try {
|
|
1660
|
+
const answer = await client.request({
|
|
1661
|
+
method: "eth_call",
|
|
1662
|
+
params: [
|
|
1663
|
+
{ to: token, data: `${BALANCE_OF_SELECTOR}${owner.slice(2).padStart(64, "0")}` },
|
|
1664
|
+
"latest"
|
|
1665
|
+
]
|
|
1666
|
+
});
|
|
1667
|
+
return typeof answer === "string" && UINT256_RE.test(answer) ? BigInt(answer) : null;
|
|
1668
|
+
} catch {
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
function refusal(reason, record) {
|
|
1673
|
+
return record === void 0 ? { ok: false, reason } : { ok: false, reason, record };
|
|
1674
|
+
}
|
|
1675
|
+
async function endNothingRequested(record, deps) {
|
|
1676
|
+
const written = await deps.store.update(record.orderId, record.version, {
|
|
1677
|
+
state: "ended-unpaid",
|
|
1678
|
+
endedBy: "nothing"
|
|
1679
|
+
});
|
|
1680
|
+
return written.ok ? written.record : record;
|
|
1681
|
+
}
|
|
1682
|
+
function storeRefusal2(written, record) {
|
|
1683
|
+
if (written.reason === "exclusion") {
|
|
1684
|
+
return {
|
|
1685
|
+
...refusal("exclusion", record),
|
|
1686
|
+
...written.holder === void 0 ? {} : { holder: written.holder }
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1689
|
+
if (written.reason === "needs_confirmation") {
|
|
1690
|
+
return { ...refusal("needs_confirmation", record), unconfirmed: written.unconfirmed ?? [] };
|
|
1691
|
+
}
|
|
1692
|
+
return refusal(written.reason === "conflict" ? "conflict" : "not_payable", record);
|
|
1693
|
+
}
|
|
1694
|
+
async function checkBeforeTempoPaying(record, payer, fresh, target, head, deps) {
|
|
1695
|
+
if (record.state !== "ordered" || record.marker !== void 0) {
|
|
1696
|
+
return refusal("not_payable", record);
|
|
1697
|
+
}
|
|
1698
|
+
if (isSnapshotStale(fresh.snapshotAt, (deps.now ?? nowSecs)())) {
|
|
1699
|
+
return refusal("stale_offer", record);
|
|
1700
|
+
}
|
|
1701
|
+
const match = fresh.payouts.find(
|
|
1702
|
+
(payout) => payout.target.caip19.id === record.payout.caip19 && payout.target.address === record.payout.address
|
|
1703
|
+
);
|
|
1704
|
+
if (fresh.productAddress !== record.productAddress || !isOfferPayout(fresh.offer, record.payout.caip19, record.payout.address) || match === void 0 || match.amount !== BigInt(record.amount)) {
|
|
1705
|
+
return refusal("offer_changed", record);
|
|
1706
|
+
}
|
|
1707
|
+
if (head.timestamp > record.createdAt + MERCHANT_CATCH_UP_SECS - PAY_CUTOFF_SECS) {
|
|
1708
|
+
return refusal("too_late", record);
|
|
1709
|
+
}
|
|
1710
|
+
const recipient = record.payout.address.toLowerCase();
|
|
1711
|
+
if (payer === recipient) {
|
|
1712
|
+
return refusal("self_payment", record);
|
|
1713
|
+
}
|
|
1714
|
+
const named = await checkEvmChain(deps.client, target.chain).catch(() => "wrong");
|
|
1715
|
+
if (named === "wrong") {
|
|
1716
|
+
return refusal("wrong_chain", record);
|
|
1717
|
+
}
|
|
1718
|
+
if (named === null) {
|
|
1719
|
+
return refusal("rpc_error", record);
|
|
1720
|
+
}
|
|
1721
|
+
const policy = await checkTempoReceivePolicies(deps.client, {
|
|
1722
|
+
chain: target.chain,
|
|
1723
|
+
token: target.token,
|
|
1724
|
+
payer,
|
|
1725
|
+
recipient
|
|
1726
|
+
});
|
|
1727
|
+
if (!policy.ok) {
|
|
1728
|
+
return refusal(policy.reason === "blocked" ? "policy_blocked" : "rpc_error", record);
|
|
1729
|
+
}
|
|
1730
|
+
const balance = await tokenBalance2(deps.client, target.token, payer);
|
|
1731
|
+
if (balance === null) {
|
|
1732
|
+
return refusal("rpc_error", record);
|
|
1733
|
+
}
|
|
1734
|
+
const needed = BigInt(record.amount) + tempoFeeMargin(target.asset.decimals);
|
|
1735
|
+
if (balance < needed) {
|
|
1736
|
+
return { ...refusal("insufficient_token", record), needed, available: balance };
|
|
1737
|
+
}
|
|
1738
|
+
return void 0;
|
|
1739
|
+
}
|
|
1740
|
+
async function requestFor(record, target, head, deps) {
|
|
1741
|
+
if (record.paymentRequest !== void 0) {
|
|
1742
|
+
const stored = storedTempoRequest(record);
|
|
1743
|
+
return stored !== void 0 && tempoRequestMatches(stored, record, target) ? { request: stored, record } : refusal("not_payable", record);
|
|
1744
|
+
}
|
|
1745
|
+
let request;
|
|
1746
|
+
try {
|
|
1747
|
+
request = composeTempoPaymentRequest({
|
|
1748
|
+
chain: target.chain,
|
|
1749
|
+
asset: target.asset,
|
|
1750
|
+
recipient: record.payout.address.toLowerCase(),
|
|
1751
|
+
amount: BigInt(record.amount),
|
|
1752
|
+
feeBps: 0,
|
|
1753
|
+
treasury: record.payout.address.toLowerCase(),
|
|
1754
|
+
memo: record.reference,
|
|
1755
|
+
createdAt: head.timestamp
|
|
1756
|
+
});
|
|
1757
|
+
} catch {
|
|
1758
|
+
return refusal("unpayable", await endNothingRequested(record, deps));
|
|
1759
|
+
}
|
|
1760
|
+
const written = await deps.store.update(record.orderId, record.version, {
|
|
1761
|
+
paymentRequest: JSON.stringify(request)
|
|
1762
|
+
});
|
|
1763
|
+
if (!written.ok) {
|
|
1764
|
+
return storeRefusal2(written, record);
|
|
1765
|
+
}
|
|
1766
|
+
return { request, record: written.record };
|
|
1767
|
+
}
|
|
1768
|
+
function isRefusal(value) {
|
|
1769
|
+
return "ok" in value;
|
|
1770
|
+
}
|
|
1771
|
+
async function saveTempoHash(record, attemptId, hash, deps) {
|
|
1772
|
+
let current = record;
|
|
1773
|
+
for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS && current !== void 0; attempt += 1) {
|
|
1774
|
+
const marker = current.marker;
|
|
1775
|
+
if (marker?.rail !== "tempo" || marker.attemptId !== attemptId) {
|
|
1776
|
+
return { saved: false, record: current, stopped: true };
|
|
1777
|
+
}
|
|
1778
|
+
if (marker.txHash === hash) {
|
|
1779
|
+
return { saved: true, record: current };
|
|
1780
|
+
}
|
|
1781
|
+
const written = await deps.store.updateMarker(current.orderId, current.version, attemptId, {
|
|
1782
|
+
...marker,
|
|
1783
|
+
txHash: hash
|
|
1784
|
+
});
|
|
1785
|
+
if (written.ok) {
|
|
1786
|
+
return { saved: true, record: written.record };
|
|
1787
|
+
}
|
|
1788
|
+
if (written.reason === "not_ready") {
|
|
1789
|
+
return { saved: false, record: current, stopped: true };
|
|
1790
|
+
}
|
|
1791
|
+
current = await deps.store.get(record.orderId);
|
|
1792
|
+
}
|
|
1793
|
+
return { saved: false, record: current ?? record };
|
|
1794
|
+
}
|
|
1795
|
+
async function endRejected(record, attemptId, deps) {
|
|
1796
|
+
let current = record;
|
|
1797
|
+
for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS && current !== void 0; attempt += 1) {
|
|
1798
|
+
if (current.marker?.attemptId !== attemptId || current.state !== "paying") {
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
const ended = await deps.store.clearMarker(
|
|
1802
|
+
current.orderId,
|
|
1803
|
+
current.version,
|
|
1804
|
+
attemptId,
|
|
1805
|
+
"ended-unpaid",
|
|
1806
|
+
"rejected"
|
|
1807
|
+
);
|
|
1808
|
+
if (ended.ok) {
|
|
1809
|
+
return refusal("rejected", ended.record);
|
|
1810
|
+
}
|
|
1811
|
+
if (ended.reason !== "conflict") {
|
|
1812
|
+
break;
|
|
1813
|
+
}
|
|
1814
|
+
current = await deps.store.get(record.orderId);
|
|
1815
|
+
}
|
|
1816
|
+
return refusal("conflict", current ?? record);
|
|
1817
|
+
}
|
|
1818
|
+
function isUserRejection(error) {
|
|
1819
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === 4001;
|
|
1820
|
+
}
|
|
1821
|
+
async function payWithTempo(record, wallet, fresh, deps) {
|
|
1822
|
+
const target = tempoTargetOf(record);
|
|
1823
|
+
const payer = wallet.address.toLowerCase();
|
|
1824
|
+
if (target === void 0 || !EVM_ADDRESS_RE.test(payer)) {
|
|
1825
|
+
return refusal("not_payable", record);
|
|
1826
|
+
}
|
|
1827
|
+
const head = await readFinalizedBlock(deps.client);
|
|
1828
|
+
if (head === null) {
|
|
1829
|
+
return refusal("rpc_error", record);
|
|
1830
|
+
}
|
|
1831
|
+
const failed = await checkBeforeTempoPaying(record, payer, fresh, target, head, deps);
|
|
1832
|
+
if (failed !== void 0) {
|
|
1833
|
+
return failed;
|
|
1834
|
+
}
|
|
1835
|
+
const composed = await requestFor(record, target, head, deps);
|
|
1836
|
+
if (isRefusal(composed)) {
|
|
1837
|
+
return composed;
|
|
1838
|
+
}
|
|
1839
|
+
const { request } = composed;
|
|
1840
|
+
let current = composed.record;
|
|
1841
|
+
if (head.timestamp + MIN_PAY_WINDOW_SECS > request.created_at + request.expiry_secs) {
|
|
1842
|
+
return refusal("too_late", await endNothingRequested(current, deps));
|
|
1843
|
+
}
|
|
1844
|
+
let calls;
|
|
1845
|
+
try {
|
|
1846
|
+
calls = buildTempoPaymentCalls(request, payer);
|
|
1847
|
+
} catch {
|
|
1848
|
+
return refusal("unpayable", await endNothingRequested(current, deps));
|
|
1849
|
+
}
|
|
1850
|
+
const call = calls.calls[0];
|
|
1851
|
+
if (calls.calls.length !== 1 || call === void 0) {
|
|
1852
|
+
return refusal("not_payable", current);
|
|
1853
|
+
}
|
|
1854
|
+
const now = (deps.now ?? nowSecs)();
|
|
1855
|
+
const attemptId = (deps.newAttemptId ?? (() => crypto.randomUUID()))();
|
|
1856
|
+
const marked = await deps.store.setMarker(
|
|
1857
|
+
current.orderId,
|
|
1858
|
+
current.version,
|
|
1859
|
+
{ rail: "tempo", attemptId, setAt: now, floorBlock: String(head.number) },
|
|
1860
|
+
now
|
|
1861
|
+
);
|
|
1862
|
+
if (!marked.ok) {
|
|
1863
|
+
return storeRefusal2(marked, current);
|
|
1864
|
+
}
|
|
1865
|
+
current = marked.record;
|
|
1866
|
+
const walletChain = await wallet.chainId().catch(() => void 0);
|
|
1867
|
+
if (walletChain !== target.chain.evmChainId) {
|
|
1868
|
+
const cleared = await deps.store.clearMarker(
|
|
1869
|
+
current.orderId,
|
|
1870
|
+
current.version,
|
|
1871
|
+
attemptId,
|
|
1872
|
+
"ordered"
|
|
1873
|
+
);
|
|
1874
|
+
return refusal("wrong_chain", cleared.ok ? cleared.record : current);
|
|
1875
|
+
}
|
|
1876
|
+
let returned;
|
|
1877
|
+
try {
|
|
1878
|
+
returned = await wallet.sendCall({
|
|
1879
|
+
from: payer,
|
|
1880
|
+
to: call.to,
|
|
1881
|
+
data: call.data,
|
|
1882
|
+
chainId: calls.chainId
|
|
1883
|
+
});
|
|
1884
|
+
} catch (error) {
|
|
1885
|
+
if (isUserRejection(error)) {
|
|
1886
|
+
return await endRejected(current, attemptId, deps);
|
|
1887
|
+
}
|
|
1888
|
+
return refusal("wallet_failed", current);
|
|
1889
|
+
}
|
|
1890
|
+
const hash = readTxHash(returned);
|
|
1891
|
+
if (hash === null) {
|
|
1892
|
+
return refusal("wallet_failed", current);
|
|
1893
|
+
}
|
|
1894
|
+
const saved = await saveTempoHash(current, attemptId, hash, deps);
|
|
1895
|
+
current = saved.record;
|
|
1896
|
+
if (!saved.saved) {
|
|
1897
|
+
return { ok: true, record: current, hash, hashUnsaved: true };
|
|
1898
|
+
}
|
|
1899
|
+
const receipt = await sendReceipt(current, hash, (deps.now ?? nowSecs)(), deps);
|
|
1900
|
+
return { ok: true, record: receipt.ok ? receipt.record : current, hash };
|
|
1901
|
+
}
|
|
1902
|
+
async function recordTempoPaid(record, hash, deps) {
|
|
1903
|
+
let current = record;
|
|
1904
|
+
if (current.receiptWrap === void 0) {
|
|
1905
|
+
const receipt = await sendReceipt(current, hash, (deps.now ?? nowSecs)(), deps);
|
|
1906
|
+
if (receipt.ok) {
|
|
1907
|
+
current = receipt.record;
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
for (let attempt = 0; attempt < STORE_WRITE_ATTEMPTS && current !== void 0; attempt += 1) {
|
|
1911
|
+
if (current.paidTx !== void 0 || isTerminal(current)) {
|
|
1912
|
+
return current;
|
|
1913
|
+
}
|
|
1914
|
+
const written = await deps.store.update(current.orderId, current.version, {
|
|
1915
|
+
state: "paid",
|
|
1916
|
+
paidTx: hash,
|
|
1917
|
+
paidAt: (deps.now ?? nowSecs)()
|
|
1918
|
+
});
|
|
1919
|
+
if (written.ok) {
|
|
1920
|
+
return written.record;
|
|
1921
|
+
}
|
|
1922
|
+
if (written.reason !== "conflict") {
|
|
1923
|
+
return current;
|
|
1924
|
+
}
|
|
1925
|
+
current = await deps.store.get(record.orderId);
|
|
1926
|
+
}
|
|
1927
|
+
return current ?? record;
|
|
1928
|
+
}
|
|
1929
|
+
async function recordBlocked(record, deps) {
|
|
1930
|
+
const written = await deps.store.update(record.orderId, record.version, { state: "blocked" });
|
|
1931
|
+
return written.ok ? written.record : record;
|
|
1932
|
+
}
|
|
1933
|
+
async function watchTempoPayment(record, deps, options = {}) {
|
|
1934
|
+
if (isTerminal(record)) {
|
|
1935
|
+
return { state: "closed", record };
|
|
1936
|
+
}
|
|
1937
|
+
if (record.paidTx !== void 0) {
|
|
1938
|
+
return { state: "paid", record: await recordTempoPaid(record, record.paidTx, deps) };
|
|
1939
|
+
}
|
|
1940
|
+
const request = storedTempoRequest(record);
|
|
1941
|
+
if (record.marker?.rail !== "tempo" || request === void 0) {
|
|
1942
|
+
return { state: "waiting", record };
|
|
1943
|
+
}
|
|
1944
|
+
let current = record;
|
|
1945
|
+
if (options.pendingHash !== void 0 && record.marker.txHash === void 0 && record.state === "paying") {
|
|
1946
|
+
current = (await saveTempoHash(record, record.marker.attemptId, options.pendingHash, deps)).record;
|
|
1947
|
+
}
|
|
1948
|
+
const marker = current.marker?.rail === "tempo" ? current.marker : record.marker;
|
|
1949
|
+
const hash = marker.txHash ?? options.pendingHash;
|
|
1950
|
+
const result = await verifyTempoPayment(deps.client, request, {
|
|
1951
|
+
...hash === void 0 ? {} : { txSignature: hash },
|
|
1952
|
+
fromBlock: Number(marker.floorBlock),
|
|
1953
|
+
pollBudgetMs: 0
|
|
1954
|
+
});
|
|
1955
|
+
switch (result.outcome) {
|
|
1956
|
+
case "verified":
|
|
1957
|
+
return {
|
|
1958
|
+
state: "paid",
|
|
1959
|
+
record: await recordTempoPaid(current, result.providerLeg.transactionHash, deps)
|
|
1960
|
+
};
|
|
1961
|
+
case "none": {
|
|
1962
|
+
const held = hash !== void 0 || options.callPending === true || marker.bundleId !== void 0;
|
|
1963
|
+
if (!held && current.state === "paying") {
|
|
1964
|
+
return { state: "over", record: current };
|
|
1965
|
+
}
|
|
1966
|
+
return { state: "waiting", record: current, pastDeadline: hash !== void 0 };
|
|
1967
|
+
}
|
|
1968
|
+
case "refused":
|
|
1969
|
+
switch (result.code) {
|
|
1970
|
+
case "provider_leg_blocked":
|
|
1971
|
+
return { state: "blocked", record: await recordBlocked(current, deps) };
|
|
1972
|
+
case "wrong_chain":
|
|
1973
|
+
case "unknown_asset":
|
|
1974
|
+
case "unreadable_receipt":
|
|
1975
|
+
case "reverted":
|
|
1976
|
+
case "no_provider_leg":
|
|
1977
|
+
case "fee_leg_missing":
|
|
1978
|
+
case "fee_leg_blocked":
|
|
1979
|
+
return { state: "unsure", record: current };
|
|
1980
|
+
}
|
|
1981
|
+
return { state: "unsure", record: current };
|
|
1982
|
+
case "inconclusive":
|
|
1983
|
+
switch (result.reason) {
|
|
1984
|
+
case "not_yet_due":
|
|
1985
|
+
case "not_finalized":
|
|
1986
|
+
case "no_receipt":
|
|
1987
|
+
case "chain_unreadable":
|
|
1988
|
+
return { state: "waiting", record: current };
|
|
1989
|
+
case "incomplete_scan":
|
|
1990
|
+
case "control_failed":
|
|
1991
|
+
return { state: "unsure", record: current };
|
|
1992
|
+
}
|
|
1993
|
+
return { state: "unsure", record: current };
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
function tempoLateDeadline(request) {
|
|
1997
|
+
return request.created_at + request.expiry_secs + EVM_LATE_PAYMENT_GRACE_SECS;
|
|
1998
|
+
}
|
|
1999
|
+
async function endTempoOrder(record, deps, options = {}) {
|
|
2000
|
+
if (record.state === "ordered" && record.marker === void 0) {
|
|
2001
|
+
const ended = await endNothingRequested(record, deps);
|
|
2002
|
+
return { ended: ended.state === "ended-unpaid", record: ended };
|
|
2003
|
+
}
|
|
2004
|
+
if (record.state !== "paying" || record.marker?.rail !== "tempo") {
|
|
2005
|
+
return { ended: false, record };
|
|
2006
|
+
}
|
|
2007
|
+
const watch = await watchTempoPayment(record, deps, options);
|
|
2008
|
+
if (watch.state !== "over" || watch.record.marker === void 0) {
|
|
2009
|
+
return { ended: false, record: watch.record };
|
|
2010
|
+
}
|
|
2011
|
+
const written = await deps.store.clearMarker(
|
|
2012
|
+
watch.record.orderId,
|
|
2013
|
+
watch.record.version,
|
|
2014
|
+
watch.record.marker.attemptId,
|
|
2015
|
+
"ended-unpaid",
|
|
2016
|
+
"over"
|
|
2017
|
+
);
|
|
2018
|
+
return written.ok ? { ended: true, record: written.record } : { ended: false, record: watch.record };
|
|
2019
|
+
}
|
|
2020
|
+
function mayStillBePaid(record, now) {
|
|
2021
|
+
return record.state === "ended-unpaid" && record.payout.caip19.startsWith("eip155:") && (record.endedBy === "over" || record.endedBy === void 0) && now - record.createdAt <= MERCHANT_CATCH_UP_SECS + MAX_CLOCK_SKEW_SECS;
|
|
2022
|
+
}
|
|
1560
2023
|
|
|
1561
2024
|
// src/buyer/purchase.ts
|
|
1562
2025
|
function cancelledUnpaid(record) {
|
|
@@ -1572,12 +2035,17 @@ async function endOrder(record, deps) {
|
|
|
1572
2035
|
if (record.state === "created") {
|
|
1573
2036
|
return { ended: true, record };
|
|
1574
2037
|
}
|
|
1575
|
-
if (deps.
|
|
2038
|
+
if (record.payout.caip19.startsWith("eip155:") && deps.tempo !== void 0) {
|
|
2039
|
+
return await endTempoOrder(record, { ...deps, client: deps.tempo });
|
|
2040
|
+
}
|
|
2041
|
+
if (deps.rpc !== void 0 && !record.payout.caip19.startsWith("eip155:")) {
|
|
1576
2042
|
return await endSolanaOrder(record, { ...deps, rpc: deps.rpc });
|
|
1577
2043
|
}
|
|
1578
2044
|
if (record.state === "ordered" && record.marker === void 0) {
|
|
1579
2045
|
const written = await deps.store.update(record.orderId, record.version, {
|
|
1580
|
-
state: "ended-unpaid"
|
|
2046
|
+
state: "ended-unpaid",
|
|
2047
|
+
// Nothing was requested: no Tempo prompt can be open for it.
|
|
2048
|
+
...record.payout.caip19.startsWith("eip155:") ? { endedBy: "nothing" } : {}
|
|
1581
2049
|
});
|
|
1582
2050
|
return written.ok ? { ended: true, record: written.record } : { ended: false, record };
|
|
1583
2051
|
}
|
|
@@ -1794,6 +2262,6 @@ function createRelayClient(options = {}) {
|
|
|
1794
2262
|
};
|
|
1795
2263
|
}
|
|
1796
2264
|
|
|
1797
|
-
export { CONFIRM_WARNINGS, DEFAULT_RELAYS, MAX_CLOCK_SKEW_SECS, MAX_RELAY_URL_LENGTH, MERCHANT_CATCH_UP_SECS, MemoryOrderBackend, OFFER_SNAPSHOT_MAX_AGE_SECS, ORDER_ACK_TARGET, OrderStore, PAYMENT_SCAN_MARGIN_SECS, PAY_CUTOFF_SECS, RELAY_CONNECT_MAX_WAIT_MS, RELAY_PUBLISH_DEADLINE_MS, RELAY_QUERY_DEADLINE_MS, RELAY_QUERY_MAX_WAIT_MS, SOLANA_COMPUTE_UNIT_LIMIT, STORE_RELAY_CAP, STORE_WRITE_ATTEMPTS, SUBSCRIBE_RETRY_MAX_MS, SUBSCRIBE_RETRY_MS, SUBSCRIBE_STABLE_MS, TERMINAL_STATES, WRAP_BACKDATE_SECS, applyStatus, cancelledUnpaid, checkBeforePaying, checkSignedTransaction, clockAgrees, compareOffers, composeOrderPayment, createPool, createRelayClient, deliveryLink, endOrder, endSolanaOrder, gone, holdsPayExclusion, isEventShaped, isGenuineEvent, isPageOrigin, isSnapshotStale, isTerminal, judgeAdd, judgeClearMarker, judgeSetMarker, judgeUpdate, judgeUpdateMarker, listenForStatus, loadOffer, loadOfferForAgent, localSolanaWallet, mediumOf, mergePins, newestGenuine, newestStoreInbox, normalizeRelayUrl, nowSecs, onOtherTerms, payWithSolana, placeOrder, readChainTime, readRelays, readStoreInbox, recordToShow, relayHost, resumeOrder, retryWithSolana, sendReceipt, statusFor, storeRelays, storedSolanaRequest, uniqueRelays, watchSolanaPayment };
|
|
2265
|
+
export { CONFIRM_WARNINGS, DEFAULT_RELAYS, MAX_CLOCK_SKEW_SECS, MAX_RELAY_URL_LENGTH, MERCHANT_CATCH_UP_SECS, MemoryOrderBackend, OFFER_SNAPSHOT_MAX_AGE_SECS, ORDER_ACK_TARGET, OrderStore, PAYMENT_SCAN_MARGIN_SECS, PAY_CUTOFF_SECS, RELAY_CONNECT_MAX_WAIT_MS, RELAY_PUBLISH_DEADLINE_MS, RELAY_QUERY_DEADLINE_MS, RELAY_QUERY_MAX_WAIT_MS, SOLANA_COMPUTE_UNIT_LIMIT, STORE_RELAY_CAP, STORE_WRITE_ATTEMPTS, SUBSCRIBE_RETRY_MAX_MS, SUBSCRIBE_RETRY_MS, SUBSCRIBE_STABLE_MS, TERMINAL_STATES, WRAP_BACKDATE_SECS, applyStatus, cancelledUnpaid, checkBeforePaying, checkSignedTransaction, clockAgrees, compareOffers, composeOrderPayment, createPool, createRelayClient, deliveryLink, endOrder, endSolanaOrder, endTempoOrder, gone, holdsPayExclusion, isEventShaped, isGenuineEvent, isPageOrigin, isSnapshotStale, isTerminal, judgeAdd, judgeClearMarker, judgeSetMarker, judgeUpdate, judgeUpdateMarker, listenForStatus, loadOffer, loadOfferForAgent, localSolanaWallet, mayStillBePaid, mediumOf, mergePins, newestGenuine, newestStoreInbox, normalizeRelayUrl, nowSecs, onOtherTerms, payWithSolana, payWithTempo, placeOrder, readChainTime, readRelays, readStoreInbox, readTempoChainTime, recordToShow, relayHost, resumeOrder, retryWithSolana, saveTempoHash, sendReceipt, statusFor, storeRelays, storedSolanaRequest, storedTempoRequest, tempoFeeMargin, tempoLateDeadline, uniqueRelays, watchSolanaPayment, watchTempoPayment };
|
|
1798
2266
|
//# sourceMappingURL=buyer.js.map
|
|
1799
2267
|
//# sourceMappingURL=buyer.js.map
|