@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.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(refusal, message) {
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
- "status"
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 refusal = patchRefusal(current, defined);
746
- if (refusal !== void 0) {
747
- return refused(refusal);
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 judgeSetMarker(records, orderId, expectedVersion, marker) {
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
- if (nextState === "ended-unpaid" && current.marker?.rail === "tempo" && current.marker.bundleId !== void 0) {
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({ ...current, state: nextState, version: current.version + 1 });
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(judged.orderId, judged.version, previous.attemptId, marker)
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.rpc !== void 0) {
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