@piprail/sdk 2.8.0 → 2.9.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/CHANGELOG.md +62 -0
- package/README.md +5 -3
- package/dist/{algorand-WJL6VRXW.js → algorand-AA3WXKW4.js} +1 -1
- package/dist/{algorand-EKD34T4Q.cjs → algorand-FCEECDG6.cjs} +32 -32
- package/dist/{aptos-CVHLVMAF.cjs → aptos-GHJPO6JJ.cjs} +31 -31
- package/dist/{aptos-WMPFEASY.js → aptos-LY67Q6QF.js} +1 -1
- package/dist/{chunk-VPENSVOI.cjs → chunk-MWBT7MCE.cjs} +7 -3
- package/dist/chunk-O4UQOZ4Z.cjs +14 -0
- package/dist/{chunk-W3HOMXOF.js → chunk-SC2ZYDHD.js} +6 -2
- package/dist/chunk-SK3CB7UA.js +14 -0
- package/dist/index.cjs +748 -237
- package/dist/index.d.cts +2384 -2577
- package/dist/index.d.ts +2384 -2577
- package/dist/index.js +589 -78
- package/dist/ledger-BtzrfO-3.d.cts +658 -0
- package/dist/ledger-BtzrfO-3.d.ts +658 -0
- package/dist/{near-NYFMX2KA.cjs → near-6KAQVVG2.cjs} +26 -26
- package/dist/{near-QV4IOZNX.js → near-FZBUICCS.js} +1 -1
- package/dist/node.cjs +38 -0
- package/dist/node.d.cts +16 -0
- package/dist/node.d.ts +16 -0
- package/dist/node.js +38 -0
- package/dist/{solana-Z34OWXMO.js → solana-ELUWO6N5.js} +1 -1
- package/dist/{solana-L342QCSV.cjs → solana-MYF4HBO4.cjs} +32 -32
- package/dist/{stellar-PWC4LETJ.js → stellar-BEMT7UYF.js} +1 -1
- package/dist/{stellar-YEXRVR3W.cjs → stellar-SUKASK4N.cjs} +20 -20
- package/dist/{sui-VEOGHBYE.js → sui-F5JQ2N6I.js} +1 -1
- package/dist/{sui-EFRSCDWN.cjs → sui-VE5LT7BL.cjs} +16 -16
- package/dist/{ton-N7KSPSAR.js → ton-7GKCTC5H.js} +1 -1
- package/dist/{ton-LZKJWYHV.cjs → ton-AOR3EURW.cjs} +16 -16
- package/dist/{tron-DQU4WTGQ.cjs → tron-EMFXDFHW.cjs} +24 -24
- package/dist/{tron-WKDUQVZR.js → tron-ZZZS3FNN.js} +1 -1
- package/dist/{xrpl-6B4IYRWI.js → xrpl-DD7TJL5L.js} +1 -1
- package/dist/{xrpl-K3U3SBF4.cjs → xrpl-Y6SQNYLC.cjs} +20 -20
- package/package.json +11 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
memorySpendStore
|
|
3
|
+
} from "./chunk-SK3CB7UA.js";
|
|
1
4
|
import {
|
|
2
5
|
ConfirmationTimeoutError,
|
|
3
6
|
InsufficientFundsError,
|
|
4
7
|
InvalidEnvelopeError,
|
|
8
|
+
MAX_DECIMALS,
|
|
5
9
|
MaxRetriesExceededError,
|
|
6
10
|
MissingDriverError,
|
|
7
11
|
NoCompatibleAcceptError,
|
|
@@ -24,7 +28,7 @@ import {
|
|
|
24
28
|
parseUnits,
|
|
25
29
|
rejectForeignToken,
|
|
26
30
|
toInsufficientFundsError
|
|
27
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-SC2ZYDHD.js";
|
|
28
32
|
|
|
29
33
|
// src/drivers/registry.ts
|
|
30
34
|
var byFamily = /* @__PURE__ */ new Map();
|
|
@@ -1840,7 +1844,7 @@ var loaders = {
|
|
|
1840
1844
|
solana: async () => {
|
|
1841
1845
|
let mod;
|
|
1842
1846
|
try {
|
|
1843
|
-
mod = await import("./solana-
|
|
1847
|
+
mod = await import("./solana-ELUWO6N5.js");
|
|
1844
1848
|
} catch (cause) {
|
|
1845
1849
|
throw new MissingDriverError(
|
|
1846
1850
|
`Solana selected, but its packages aren't installed. Run: npm install @solana/web3.js @solana/spl-token bs58`,
|
|
@@ -1852,7 +1856,7 @@ var loaders = {
|
|
|
1852
1856
|
ton: async () => {
|
|
1853
1857
|
let mod;
|
|
1854
1858
|
try {
|
|
1855
|
-
mod = await import("./ton-
|
|
1859
|
+
mod = await import("./ton-7GKCTC5H.js");
|
|
1856
1860
|
} catch (cause) {
|
|
1857
1861
|
throw new MissingDriverError(
|
|
1858
1862
|
`TON selected, but its packages aren't installed. Run: npm install @ton/ton @ton/core @ton/crypto`,
|
|
@@ -1864,7 +1868,7 @@ var loaders = {
|
|
|
1864
1868
|
stellar: async () => {
|
|
1865
1869
|
let mod;
|
|
1866
1870
|
try {
|
|
1867
|
-
mod = await import("./stellar-
|
|
1871
|
+
mod = await import("./stellar-BEMT7UYF.js");
|
|
1868
1872
|
} catch (cause) {
|
|
1869
1873
|
throw new MissingDriverError(
|
|
1870
1874
|
`Stellar selected, but its package isn't installed. Run: npm install @stellar/stellar-sdk`,
|
|
@@ -1876,7 +1880,7 @@ var loaders = {
|
|
|
1876
1880
|
xrpl: async () => {
|
|
1877
1881
|
let mod;
|
|
1878
1882
|
try {
|
|
1879
|
-
mod = await import("./xrpl-
|
|
1883
|
+
mod = await import("./xrpl-DD7TJL5L.js");
|
|
1880
1884
|
} catch (cause) {
|
|
1881
1885
|
throw new MissingDriverError(
|
|
1882
1886
|
`XRPL selected, but its package isn't installed. Run: npm install xrpl`,
|
|
@@ -1888,7 +1892,7 @@ var loaders = {
|
|
|
1888
1892
|
tron: async () => {
|
|
1889
1893
|
let mod;
|
|
1890
1894
|
try {
|
|
1891
|
-
mod = await import("./tron-
|
|
1895
|
+
mod = await import("./tron-ZZZS3FNN.js");
|
|
1892
1896
|
} catch (cause) {
|
|
1893
1897
|
throw new MissingDriverError(
|
|
1894
1898
|
`Tron selected, but its package isn't installed. Run: npm install tronweb`,
|
|
@@ -1900,7 +1904,7 @@ var loaders = {
|
|
|
1900
1904
|
sui: async () => {
|
|
1901
1905
|
let mod;
|
|
1902
1906
|
try {
|
|
1903
|
-
mod = await import("./sui-
|
|
1907
|
+
mod = await import("./sui-F5JQ2N6I.js");
|
|
1904
1908
|
} catch (cause) {
|
|
1905
1909
|
throw new MissingDriverError(
|
|
1906
1910
|
`Sui selected, but its package isn't installed. Run: npm install @mysten/sui`,
|
|
@@ -1912,7 +1916,7 @@ var loaders = {
|
|
|
1912
1916
|
near: async () => {
|
|
1913
1917
|
let mod;
|
|
1914
1918
|
try {
|
|
1915
|
-
mod = await import("./near-
|
|
1919
|
+
mod = await import("./near-FZBUICCS.js");
|
|
1916
1920
|
} catch (cause) {
|
|
1917
1921
|
throw new MissingDriverError(
|
|
1918
1922
|
`NEAR selected, but its package isn't installed. Run: npm install near-api-js`,
|
|
@@ -1924,7 +1928,7 @@ var loaders = {
|
|
|
1924
1928
|
aptos: async () => {
|
|
1925
1929
|
let mod;
|
|
1926
1930
|
try {
|
|
1927
|
-
mod = await import("./aptos-
|
|
1931
|
+
mod = await import("./aptos-LY67Q6QF.js");
|
|
1928
1932
|
} catch (cause) {
|
|
1929
1933
|
throw new MissingDriverError(
|
|
1930
1934
|
`Aptos selected, but its package isn't installed. Run: npm install @aptos-labs/ts-sdk`,
|
|
@@ -1936,7 +1940,7 @@ var loaders = {
|
|
|
1936
1940
|
algorand: async () => {
|
|
1937
1941
|
let mod;
|
|
1938
1942
|
try {
|
|
1939
|
-
mod = await import("./algorand-
|
|
1943
|
+
mod = await import("./algorand-AA3WXKW4.js");
|
|
1940
1944
|
} catch (cause) {
|
|
1941
1945
|
throw new MissingDriverError(
|
|
1942
1946
|
`Algorand selected, but its package isn't installed. Run: npm install algosdk`,
|
|
@@ -2582,6 +2586,53 @@ function encodeBase642(str) {
|
|
|
2582
2586
|
}
|
|
2583
2587
|
|
|
2584
2588
|
// src/policy.ts
|
|
2589
|
+
var DENOM_PRECISION = MAX_DECIMALS;
|
|
2590
|
+
var BUILTIN_DENOMS = {
|
|
2591
|
+
USDC: "USD",
|
|
2592
|
+
USDT: "USD",
|
|
2593
|
+
USD1: "USD",
|
|
2594
|
+
FDUSD: "USD",
|
|
2595
|
+
RLUSD: "USD",
|
|
2596
|
+
EURC: "EUR"
|
|
2597
|
+
};
|
|
2598
|
+
function denomOf(symbol, asset, policy) {
|
|
2599
|
+
if (asset === "native") return void 0;
|
|
2600
|
+
const norm = (v) => typeof v === "string" && v.trim() !== "" ? v.trim().toUpperCase() : void 0;
|
|
2601
|
+
const override = policy?.denomFor;
|
|
2602
|
+
if (override && typeof override === "object") {
|
|
2603
|
+
if (asset && asset in override) {
|
|
2604
|
+
const d = norm(override[asset]);
|
|
2605
|
+
if (d) return d;
|
|
2606
|
+
}
|
|
2607
|
+
if (symbol) {
|
|
2608
|
+
const hit = Object.entries(override).find(([k]) => k.trim().toUpperCase() === symbol.trim().toUpperCase());
|
|
2609
|
+
if (hit) {
|
|
2610
|
+
const d = norm(hit[1]);
|
|
2611
|
+
if (d) return d;
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
if (symbol) {
|
|
2616
|
+
const builtin = BUILTIN_DENOMS[symbol.trim().toUpperCase()];
|
|
2617
|
+
if (builtin) return builtin;
|
|
2618
|
+
}
|
|
2619
|
+
return void 0;
|
|
2620
|
+
}
|
|
2621
|
+
function scaleToDenom(amountBase, decimals) {
|
|
2622
|
+
if (!Number.isSafeInteger(decimals) || decimals < 0 || decimals > DENOM_PRECISION) return null;
|
|
2623
|
+
return amountBase * 10n ** BigInt(DENOM_PRECISION - decimals);
|
|
2624
|
+
}
|
|
2625
|
+
function capForDenom(caps, denom) {
|
|
2626
|
+
if (!caps || typeof caps !== "object") return void 0;
|
|
2627
|
+
const target = denom.trim().toUpperCase();
|
|
2628
|
+
let best;
|
|
2629
|
+
for (const [k, v] of Object.entries(caps)) {
|
|
2630
|
+
if (typeof k !== "string" || typeof v !== "string") continue;
|
|
2631
|
+
if (k.trim().toUpperCase() !== target) continue;
|
|
2632
|
+
if (best === void 0 || Number(v) < Number(best)) best = v;
|
|
2633
|
+
}
|
|
2634
|
+
return best;
|
|
2635
|
+
}
|
|
2585
2636
|
var ALLOW = { allowed: true };
|
|
2586
2637
|
var deny = (code, reason) => ({
|
|
2587
2638
|
allowed: false,
|
|
@@ -2665,6 +2716,34 @@ function evaluatePolicy(intent, policy, spentForAssetBase, ctx) {
|
|
|
2665
2716
|
);
|
|
2666
2717
|
}
|
|
2667
2718
|
}
|
|
2719
|
+
if (ctx && policy.maxTotalPerDenom) {
|
|
2720
|
+
const denom = denomOf(intent.symbol, intent.asset, policy);
|
|
2721
|
+
const capStr = denom ? capForDenom(policy.maxTotalPerDenom, denom) : void 0;
|
|
2722
|
+
if (denom && capStr !== void 0) {
|
|
2723
|
+
const thisScaled = scaleToDenom(intent.amountBase, intent.decimals);
|
|
2724
|
+
if (thisScaled === null) {
|
|
2725
|
+
return deny(
|
|
2726
|
+
"MAX_TOTAL_DENOM",
|
|
2727
|
+
`token for ${denom} has more than ${DENOM_PRECISION} decimals \u2014 refusing to count it toward policy.maxTotalPerDenom.${denom} on trust (it can't be summed safely).`
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
const capScaled = floorUnits(capStr, DENOM_PRECISION);
|
|
2731
|
+
if ((ctx.spentInDenomScaled ?? 0n) + thisScaled > capScaled) {
|
|
2732
|
+
return deny(
|
|
2733
|
+
"MAX_TOTAL_DENOM",
|
|
2734
|
+
`this payment would push total ${denom} spend past policy.maxTotalPerDenom.${denom} (${capStr}) \u2014 summed across every ${denom} token and chain.`
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
if (ctx && policy.maxPayments !== void 0) {
|
|
2740
|
+
if ((ctx.paymentCount ?? 0) + 1 > policy.maxPayments) {
|
|
2741
|
+
return deny(
|
|
2742
|
+
"MAX_PAYMENTS",
|
|
2743
|
+
`this payment would exceed policy.maxPayments (${policy.maxPayments}); already settled ${ctx.paymentCount ?? 0}.`
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2668
2747
|
if (ctx && policy.windowTotal !== void 0 && policy.windowSeconds !== void 0) {
|
|
2669
2748
|
const cap = floorUnits(policy.windowTotal, intent.decimals);
|
|
2670
2749
|
if (ctx.spentInWindowBase + intent.amountBase > cap) {
|
|
@@ -2674,6 +2753,14 @@ function evaluatePolicy(intent, policy, spentForAssetBase, ctx) {
|
|
|
2674
2753
|
);
|
|
2675
2754
|
}
|
|
2676
2755
|
}
|
|
2756
|
+
if (ctx && policy.maxPaymentsPerWindow !== void 0 && policy.windowSeconds !== void 0) {
|
|
2757
|
+
if ((ctx.paymentCountInWindow ?? 0) + 1 > policy.maxPaymentsPerWindow) {
|
|
2758
|
+
return deny(
|
|
2759
|
+
"WINDOW_COUNT",
|
|
2760
|
+
`this payment would exceed policy.maxPaymentsPerWindow (${policy.maxPaymentsPerWindow}) within the last ${policy.windowSeconds}s.`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2677
2764
|
return ALLOW;
|
|
2678
2765
|
}
|
|
2679
2766
|
|
|
@@ -2682,37 +2769,135 @@ var keyFor = (network, asset) => `${network}|${asset}`;
|
|
|
2682
2769
|
var SpendLedger = class {
|
|
2683
2770
|
records = [];
|
|
2684
2771
|
buckets = /* @__PURE__ */ new Map();
|
|
2772
|
+
/** Per-denomination running total, scaled to {@link DENOM_PRECISION}. Keyed by the
|
|
2773
|
+
* UPPERCASE denomination so lookups are case-insensitive. */
|
|
2774
|
+
denomTotals = /* @__PURE__ */ new Map();
|
|
2775
|
+
/** Threshold keys already warned (`warnAtFraction` fires once per crossing per cap).
|
|
2776
|
+
* Lives on the LEDGER — not the client — so clients SHARING one (a cross-chain
|
|
2777
|
+
* MultiChainPayer) dedupe together: a denomination/count threshold fires once across the
|
|
2778
|
+
* whole shared budget, not once per chain. */
|
|
2779
|
+
warned = /* @__PURE__ */ new Set();
|
|
2780
|
+
store;
|
|
2685
2781
|
/**
|
|
2686
2782
|
* Session clock origin (epoch-ms) — process/session start = ledger
|
|
2687
2783
|
* construction. In-memory; a new process is a new session. The client reads it
|
|
2688
2784
|
* to compute the `ttlSeconds` deadline and the rolling-window slice.
|
|
2689
2785
|
*/
|
|
2690
2786
|
sessionStart = Date.now();
|
|
2691
|
-
/**
|
|
2692
|
-
*
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2787
|
+
/**
|
|
2788
|
+
* @param store Optional durable {@link SpendStore}. When supplied, the ledger
|
|
2789
|
+
* HYDRATES from `store.load()` here (so prior spend resumes after a restart) and
|
|
2790
|
+
* `append()`s every settled payment. A throwing/absent store fails SAFE to an
|
|
2791
|
+
* empty in-memory ledger — it never blocks construction (ERRORS.md: never throw).
|
|
2792
|
+
*/
|
|
2793
|
+
constructor(store) {
|
|
2794
|
+
this.store = store;
|
|
2795
|
+
if (store) {
|
|
2796
|
+
let seed = [];
|
|
2797
|
+
try {
|
|
2798
|
+
seed = store.load() ?? [];
|
|
2799
|
+
} catch {
|
|
2800
|
+
seed = [];
|
|
2801
|
+
}
|
|
2802
|
+
for (const r of seed) this.ingest(r, r?.decimals ?? 0, r?.denom);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
/**
|
|
2806
|
+
* A record is safe to tally iff its `amountBase` is a non-negative integer STRING and its
|
|
2807
|
+
* `decimals` is an integer in `[0, MAX_DECIMALS]`. The live `record()` path always passes
|
|
2808
|
+
* (the client validated the quote), but a hydrated record comes from an UNTRUSTED store
|
|
2809
|
+
* (a tampered/corrupt/future-version JSONL line), so we gate it here — a bad record is
|
|
2810
|
+
* dropped rather than allowed to throw `BigInt(...)`/`formatUnits(...)` later and brick a
|
|
2811
|
+
* read or the constructor.
|
|
2812
|
+
*/
|
|
2813
|
+
isTallyable(r, decimals) {
|
|
2814
|
+
return !!r && typeof r.amountBase === "string" && /^\d+$/.test(r.amountBase) && Number.isInteger(decimals) && decimals >= 0 && decimals <= MAX_DECIMALS;
|
|
2815
|
+
}
|
|
2816
|
+
/** Apply a record to the in-memory tallies (records + per-asset bucket + denom total).
|
|
2817
|
+
* Shared by {@link record} and constructor hydration; does NOT persist. Returns the
|
|
2818
|
+
* stored record, or `null` when the record is corrupt and was skipped. */
|
|
2819
|
+
ingest(r, decimals, denom) {
|
|
2820
|
+
if (!this.isTallyable(r, decimals)) return null;
|
|
2821
|
+
const denomStr = typeof denom === "string" && denom.trim() !== "" ? denom : void 0;
|
|
2822
|
+
const rec = { ...r, decimals, ...denomStr ? { denom: denomStr } : {} };
|
|
2823
|
+
this.records.push(rec);
|
|
2824
|
+
const key = keyFor(rec.network, rec.asset);
|
|
2696
2825
|
const bucket = this.buckets.get(key);
|
|
2697
2826
|
if (bucket) {
|
|
2698
|
-
bucket.total += BigInt(
|
|
2827
|
+
bucket.total += BigInt(rec.amountBase);
|
|
2699
2828
|
bucket.count += 1;
|
|
2700
|
-
if (!bucket.symbol &&
|
|
2829
|
+
if (!bucket.symbol && rec.symbol) bucket.symbol = rec.symbol;
|
|
2701
2830
|
} else {
|
|
2702
2831
|
this.buckets.set(key, {
|
|
2703
|
-
network:
|
|
2704
|
-
asset:
|
|
2705
|
-
symbol:
|
|
2832
|
+
network: rec.network,
|
|
2833
|
+
asset: rec.asset,
|
|
2834
|
+
symbol: rec.symbol,
|
|
2706
2835
|
decimals,
|
|
2707
|
-
total: BigInt(
|
|
2836
|
+
total: BigInt(rec.amountBase),
|
|
2708
2837
|
count: 1
|
|
2709
2838
|
});
|
|
2710
2839
|
}
|
|
2840
|
+
if (denomStr) {
|
|
2841
|
+
const scaled = scaleToDenom(BigInt(rec.amountBase), decimals);
|
|
2842
|
+
if (scaled !== null) {
|
|
2843
|
+
const k = denomStr.toUpperCase();
|
|
2844
|
+
this.denomTotals.set(k, (this.denomTotals.get(k) ?? 0n) + scaled);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
return rec;
|
|
2848
|
+
}
|
|
2849
|
+
/** Record a settled payment. `decimals` is the TRUE token decimals (for the
|
|
2850
|
+
* per-asset running total + the formatted summary). `denom` is the unit-of-account
|
|
2851
|
+
* the payment counts toward in the grand total (or omit for none). Persists to the
|
|
2852
|
+
* {@link SpendStore} when one is configured (a failed append never throws). */
|
|
2853
|
+
record(r, decimals, denom) {
|
|
2854
|
+
const rec = this.ingest(r, decimals, denom);
|
|
2855
|
+
if (rec && this.store) {
|
|
2856
|
+
try {
|
|
2857
|
+
this.store.append(rec);
|
|
2858
|
+
} catch {
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2711
2861
|
}
|
|
2712
2862
|
/** Running total (base units) already spent on this (network, asset). */
|
|
2713
2863
|
totalFor(network, asset) {
|
|
2714
2864
|
return this.buckets.get(keyFor(network, asset))?.total ?? 0n;
|
|
2715
2865
|
}
|
|
2866
|
+
/**
|
|
2867
|
+
* Running grand total for a DENOMINATION, scaled to {@link DENOM_PRECISION} (so
|
|
2868
|
+
* tokens of different decimals add up exactly). Summed across every token of that
|
|
2869
|
+
* denomination and every chain this ledger has seen. Powers `maxTotalPerDenom`.
|
|
2870
|
+
* `0n` for a denomination never spent on. Case-insensitive.
|
|
2871
|
+
*/
|
|
2872
|
+
totalForDenom(denom) {
|
|
2873
|
+
return this.denomTotals.get(denom.toUpperCase()) ?? 0n;
|
|
2874
|
+
}
|
|
2875
|
+
/** Total number of settled payments (across every chain + token). Powers `maxPayments`. */
|
|
2876
|
+
count() {
|
|
2877
|
+
return this.records.length;
|
|
2878
|
+
}
|
|
2879
|
+
/** Mark a `warnAtFraction` threshold key as fired; returns `true` the FIRST time (so the
|
|
2880
|
+
* caller emits the `budget-threshold` event once) and `false` thereafter. Shared across
|
|
2881
|
+
* every client on this ledger, so a cross-chain threshold fires once for the whole budget. */
|
|
2882
|
+
markWarned(key) {
|
|
2883
|
+
if (this.warned.has(key)) return false;
|
|
2884
|
+
this.warned.add(key);
|
|
2885
|
+
return true;
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Number of settled payments whose `at` (ISO) is at or after `sinceMs` (epoch-ms),
|
|
2889
|
+
* across every chain + token. Backs the rolling payment-count cap
|
|
2890
|
+
* (`maxPaymentsPerWindow`, `sinceMs = now - windowSeconds*1000`). Linear scan —
|
|
2891
|
+
* negligible at agent-session cardinality and only when a window count cap is set.
|
|
2892
|
+
*/
|
|
2893
|
+
countSince(sinceMs) {
|
|
2894
|
+
let n = 0;
|
|
2895
|
+
for (const r of this.records) {
|
|
2896
|
+
const t = Date.parse(r.at);
|
|
2897
|
+
if (Number.isNaN(t) || t >= sinceMs) n += 1;
|
|
2898
|
+
}
|
|
2899
|
+
return n;
|
|
2900
|
+
}
|
|
2716
2901
|
/**
|
|
2717
2902
|
* Sum of base-unit amounts for (network, asset) whose record `at` (ISO
|
|
2718
2903
|
* timestamp) is at or after `sinceMs` (epoch-ms). Backs the rolling window
|
|
@@ -2723,9 +2908,9 @@ var SpendLedger = class {
|
|
|
2723
2908
|
totalSince(network, asset, sinceMs) {
|
|
2724
2909
|
let sum = 0n;
|
|
2725
2910
|
for (const r of this.records) {
|
|
2726
|
-
if (r.network
|
|
2727
|
-
|
|
2728
|
-
|
|
2911
|
+
if (r.network !== network || r.asset !== asset) continue;
|
|
2912
|
+
const t = Date.parse(r.at);
|
|
2913
|
+
if (Number.isNaN(t) || t >= sinceMs) sum += BigInt(r.amountBase);
|
|
2729
2914
|
}
|
|
2730
2915
|
return sum;
|
|
2731
2916
|
}
|
|
@@ -2745,6 +2930,14 @@ var SpendLedger = class {
|
|
|
2745
2930
|
totalBase: b.total
|
|
2746
2931
|
}));
|
|
2747
2932
|
}
|
|
2933
|
+
/**
|
|
2934
|
+
* The per-denomination grand totals, as read-only tuples — `denom` and the running
|
|
2935
|
+
* `totalScaled` (at {@link DENOM_PRECISION}). Lets the client compose the grand-total
|
|
2936
|
+
* budget view; the cap math lives in the client. A never-spent denomination is absent.
|
|
2937
|
+
*/
|
|
2938
|
+
denomBuckets() {
|
|
2939
|
+
return [...this.denomTotals.entries()].map(([denom, totalScaled]) => ({ denom, totalScaled }));
|
|
2940
|
+
}
|
|
2748
2941
|
/** An immutable snapshot of all spend so far. */
|
|
2749
2942
|
summary() {
|
|
2750
2943
|
return {
|
|
@@ -2758,6 +2951,12 @@ var SpendLedger = class {
|
|
|
2758
2951
|
totalFormatted: formatUnits(b.total, b.decimals),
|
|
2759
2952
|
count: b.count
|
|
2760
2953
|
})),
|
|
2954
|
+
byDenom: [...this.denomTotals.entries()].map(([denom, scaled]) => ({
|
|
2955
|
+
denom,
|
|
2956
|
+
totalScaled: scaled.toString(),
|
|
2957
|
+
totalFormatted: formatUnits(scaled, DENOM_PRECISION),
|
|
2958
|
+
count: this.records.filter((r) => r.denom?.toUpperCase() === denom).length
|
|
2959
|
+
})),
|
|
2761
2960
|
records: [...this.records]
|
|
2762
2961
|
};
|
|
2763
2962
|
}
|
|
@@ -2776,9 +2975,14 @@ var PipRailClient = class {
|
|
|
2776
2975
|
maxRetries;
|
|
2777
2976
|
retryTimeoutMs;
|
|
2778
2977
|
onEvent;
|
|
2779
|
-
// Per-asset tally of everything this client has paid
|
|
2780
|
-
//
|
|
2781
|
-
|
|
2978
|
+
// Per-asset (+ per-denomination + count) tally of everything this client has paid —
|
|
2979
|
+
// powers spent()/budget() and the maxTotal/maxTotalPerDenom/maxPayments caps. Either
|
|
2980
|
+
// its own (optionally store-backed) ledger, or a SHARED one injected so the grand
|
|
2981
|
+
// total + count caps span several chains (MultiChainPayer.fromWallets).
|
|
2982
|
+
ledger;
|
|
2983
|
+
// `warnAtFraction` threshold dedup lives on the LEDGER (`ledger.markWarned`), so clients
|
|
2984
|
+
// sharing one ledger (a cross-chain MultiChainPayer) fire each threshold once for the whole
|
|
2985
|
+
// shared budget — not once per chain.
|
|
2782
2986
|
// Resolved lazily on first request — this is what lets Solana (and future
|
|
2783
2987
|
// families) auto-mount with no setup call.
|
|
2784
2988
|
bound;
|
|
@@ -2787,8 +2991,15 @@ var PipRailClient = class {
|
|
|
2787
2991
|
this.maxRetries = Math.max(1, opts.maxPaymentRetries ?? 3);
|
|
2788
2992
|
this.retryTimeoutMs = opts.retryTimeoutMs ?? 3e4;
|
|
2789
2993
|
this.onEvent = opts.onEvent ?? (() => void 0);
|
|
2994
|
+
if (opts.ledger && opts.spendStore) {
|
|
2995
|
+
throw new TypeError(
|
|
2996
|
+
"Pass either `ledger` (a shared SpendLedger) or `spendStore`, not both \u2014 a shared ledger already owns its store."
|
|
2997
|
+
);
|
|
2998
|
+
}
|
|
2999
|
+
this.ledger = opts.ledger ?? new SpendLedger(opts.spendStore);
|
|
2790
3000
|
this.assertPolicyAmountCaps(opts.policy);
|
|
2791
3001
|
this.assertPolicyTimeOptions(opts.policy);
|
|
3002
|
+
this.assertPolicySpendControls(opts.policy);
|
|
2792
3003
|
}
|
|
2793
3004
|
/**
|
|
2794
3005
|
* Fail LOUDLY at construction on a malformed amount cap — a security boundary
|
|
@@ -2809,6 +3020,65 @@ var PipRailClient = class {
|
|
|
2809
3020
|
);
|
|
2810
3021
|
}
|
|
2811
3022
|
}
|
|
3023
|
+
if (policy.maxTotalPerDenom !== void 0) {
|
|
3024
|
+
const m = policy.maxTotalPerDenom;
|
|
3025
|
+
const proto = m && typeof m === "object" ? Object.getPrototypeOf(m) : false;
|
|
3026
|
+
if (!m || typeof m !== "object" || Array.isArray(m) || proto !== Object.prototype && proto !== null) {
|
|
3027
|
+
throw new TypeError(
|
|
3028
|
+
`policy.maxTotalPerDenom must be a plain { DENOM: amount } object (e.g. { USD: '20.00' }); got ${JSON.stringify(policy.maxTotalPerDenom)}.`
|
|
3029
|
+
);
|
|
3030
|
+
}
|
|
3031
|
+
for (const [denom, v] of Object.entries(policy.maxTotalPerDenom)) {
|
|
3032
|
+
if (denom.trim() === "") {
|
|
3033
|
+
throw new TypeError("policy.maxTotalPerDenom has a blank denomination key.");
|
|
3034
|
+
}
|
|
3035
|
+
if (typeof v !== "string" || !/^\d+(\.\d+)?$/.test(v)) {
|
|
3036
|
+
throw new TypeError(
|
|
3037
|
+
`policy.maxTotalPerDenom.${denom} must be a non-negative decimal string (e.g. '20.00'); got ${JSON.stringify(v)}.`
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (policy.denomFor !== void 0) {
|
|
3043
|
+
const m = policy.denomFor;
|
|
3044
|
+
if (!m || typeof m !== "object" || Array.isArray(m)) {
|
|
3045
|
+
throw new TypeError(`policy.denomFor must be a { token: DENOM } object; got ${JSON.stringify(m)}.`);
|
|
3046
|
+
}
|
|
3047
|
+
for (const [k, v] of Object.entries(policy.denomFor)) {
|
|
3048
|
+
if (typeof v !== "string" || v.trim() === "") {
|
|
3049
|
+
throw new TypeError(`policy.denomFor.${k} must be a non-empty denomination string; got ${JSON.stringify(v)}.`);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
/**
|
|
3055
|
+
* Fail LOUDLY at construction on a misconfigured count / threshold control (a
|
|
3056
|
+
* programmer error → `TypeError`): payment counts must be positive safe integers,
|
|
3057
|
+
* `maxPaymentsPerWindow` needs `windowSeconds` (it shares the window), and
|
|
3058
|
+
* `warnAtFraction` must be in (0, 1].
|
|
3059
|
+
*/
|
|
3060
|
+
assertPolicySpendControls(policy) {
|
|
3061
|
+
if (!policy) return;
|
|
3062
|
+
for (const field of ["maxPayments", "maxPaymentsPerWindow"]) {
|
|
3063
|
+
const v = policy[field];
|
|
3064
|
+
if (v === void 0) continue;
|
|
3065
|
+
if (!Number.isSafeInteger(v) || v <= 0) {
|
|
3066
|
+
throw new TypeError(`policy.${field} must be a positive integer; got ${JSON.stringify(v)}.`);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
if (policy.maxPaymentsPerWindow !== void 0 && policy.windowSeconds === void 0) {
|
|
3070
|
+
throw new TypeError(
|
|
3071
|
+
"policy.maxPaymentsPerWindow needs policy.windowSeconds (the rolling window it counts within) \u2014 set both, or neither."
|
|
3072
|
+
);
|
|
3073
|
+
}
|
|
3074
|
+
if (policy.warnAtFraction !== void 0) {
|
|
3075
|
+
const f = policy.warnAtFraction;
|
|
3076
|
+
if (typeof f !== "number" || !(f > 0 && f <= 1)) {
|
|
3077
|
+
throw new TypeError(
|
|
3078
|
+
`policy.warnAtFraction must be a number in (0, 1] (e.g. 0.8); got ${JSON.stringify(f)}.`
|
|
3079
|
+
);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
2812
3082
|
}
|
|
2813
3083
|
/**
|
|
2814
3084
|
* Fail LOUDLY at construction on a misconfigured time policy — a security
|
|
@@ -2823,11 +3093,17 @@ var PipRailClient = class {
|
|
|
2823
3093
|
if (!policy) return;
|
|
2824
3094
|
const hasWindowTotal = policy.windowTotal !== void 0;
|
|
2825
3095
|
const hasWindowSeconds = policy.windowSeconds !== void 0;
|
|
2826
|
-
|
|
3096
|
+
const hasWindowCount = policy.maxPaymentsPerWindow !== void 0;
|
|
3097
|
+
if (hasWindowTotal && !hasWindowSeconds) {
|
|
2827
3098
|
throw new TypeError(
|
|
2828
3099
|
"policy.windowTotal and policy.windowSeconds must be set together \u2014 a rolling-window cap can't be half-armed (set both, or neither)."
|
|
2829
3100
|
);
|
|
2830
3101
|
}
|
|
3102
|
+
if (hasWindowSeconds && !hasWindowTotal && !hasWindowCount) {
|
|
3103
|
+
throw new TypeError(
|
|
3104
|
+
"policy.windowSeconds must be set together with policy.windowTotal and/or policy.maxPaymentsPerWindow \u2014 a window width alone bounds nothing."
|
|
3105
|
+
);
|
|
3106
|
+
}
|
|
2831
3107
|
if (hasWindowSeconds && !(Number.isSafeInteger(policy.windowSeconds) && policy.windowSeconds > 0)) {
|
|
2832
3108
|
throw new TypeError("policy.windowSeconds must be a positive integer number of seconds.");
|
|
2833
3109
|
}
|
|
@@ -2839,6 +3115,14 @@ var PipRailClient = class {
|
|
|
2839
3115
|
);
|
|
2840
3116
|
}
|
|
2841
3117
|
}
|
|
3118
|
+
if (policy.expiresAt !== void 0) {
|
|
3119
|
+
const at = policy.expiresAt;
|
|
3120
|
+
if (!Number.isSafeInteger(at) || Math.abs(at) > 864e13) {
|
|
3121
|
+
throw new TypeError(
|
|
3122
|
+
`policy.expiresAt must be an absolute epoch-MILLISECONDS integer (like Date.now()); got ${JSON.stringify(at)}.`
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
2842
3126
|
}
|
|
2843
3127
|
/** Emit an observability event, never letting a throwing handler break the
|
|
2844
3128
|
* payment flow (mirrors the server gate's `onPaid` isolation). */
|
|
@@ -2932,15 +3216,25 @@ var PipRailClient = class {
|
|
|
2932
3216
|
return { quote, cost };
|
|
2933
3217
|
}
|
|
2934
3218
|
/** Aggregated snapshot of every payment this client has settled — total
|
|
2935
|
-
* count, cumulative spend per token,
|
|
3219
|
+
* count, cumulative spend per token, cumulative spend per denomination (the
|
|
3220
|
+
* cross-token grand total), and the individual records. */
|
|
2936
3221
|
spent() {
|
|
2937
3222
|
return this.ledger.summary();
|
|
2938
3223
|
}
|
|
3224
|
+
/** The CONFIGURED spend policy, read back unchanged — so an agent can self-check
|
|
3225
|
+
* its WHOLE leash (caps, allowlists, time, denom + count limits) without hitting a
|
|
3226
|
+
* decline. `undefined` when no policy is set. Pure; never throws. */
|
|
3227
|
+
policy() {
|
|
3228
|
+
return this.opts.policy;
|
|
3229
|
+
}
|
|
2939
3230
|
/**
|
|
2940
3231
|
* Read-only budget + time leash for a Mode-A (headless) agent — the policy IS
|
|
2941
3232
|
* the consent, and this is how the agent SEES what's left of it before paying.
|
|
2942
|
-
* Composes the
|
|
2943
|
-
*
|
|
3233
|
+
* Composes the ledger with the configured policy: the per-asset money leash
|
|
3234
|
+
* (`byAsset`), the cross-token GRAND TOTAL per denomination (`byDenom`, present
|
|
3235
|
+
* from the start), and the payment-COUNT leash (`counts`). Never throws, moves no
|
|
3236
|
+
* funds. The money/count figures persist if a `spendStore` is set; the time
|
|
3237
|
+
* envelope is process-scoped (see {@link SessionBudget}).
|
|
2944
3238
|
*/
|
|
2945
3239
|
budget() {
|
|
2946
3240
|
const view = this.sessionView();
|
|
@@ -2951,9 +3245,54 @@ var PipRailClient = class {
|
|
|
2951
3245
|
expiresAt: view?.expiresAt != null ? new Date(view.expiresAt).toISOString() : null,
|
|
2952
3246
|
secondsRemaining: view?.secondsRemaining ?? null
|
|
2953
3247
|
},
|
|
2954
|
-
byAsset: this.remaining()
|
|
3248
|
+
byAsset: this.remaining(),
|
|
3249
|
+
byDenom: this.denomRemaining(),
|
|
3250
|
+
counts: this.countStatus()
|
|
2955
3251
|
};
|
|
2956
3252
|
}
|
|
3253
|
+
/**
|
|
3254
|
+
* The cross-token GRAND-TOTAL leash — one row per denomination capped by
|
|
3255
|
+
* `policy.maxTotalPerDenom`. Unlike `remaining()`, rows exist from the START
|
|
3256
|
+
* (the cap is a single declared number, so headroom is previewable before any
|
|
3257
|
+
* spend). `[]` when no `maxTotalPerDenom` is set. Pure; never throws; never a
|
|
3258
|
+
* price-converted figure (tokens grouped as one unit, each 1:1).
|
|
3259
|
+
*/
|
|
3260
|
+
denomRemaining() {
|
|
3261
|
+
const caps = this.opts.policy?.maxTotalPerDenom;
|
|
3262
|
+
if (!caps) return [];
|
|
3263
|
+
return Object.entries(caps).map(([rawDenom, capStr]) => {
|
|
3264
|
+
const denom = rawDenom.toUpperCase();
|
|
3265
|
+
const spentScaled = this.ledger.totalForDenom(denom);
|
|
3266
|
+
const capScaled = floorUnits(capStr, DENOM_PRECISION);
|
|
3267
|
+
const remainingScaled = capScaled > spentScaled ? capScaled - spentScaled : 0n;
|
|
3268
|
+
const fraction = capScaled === 0n ? spentScaled > 0n ? 1 : 0 : Number(spentScaled * 1000000n / capScaled) / 1e6;
|
|
3269
|
+
return {
|
|
3270
|
+
denom,
|
|
3271
|
+
spentFormatted: formatUnits(spentScaled, DENOM_PRECISION),
|
|
3272
|
+
capFormatted: formatUnits(capScaled, DENOM_PRECISION),
|
|
3273
|
+
remainingFormatted: formatUnits(remainingScaled, DENOM_PRECISION),
|
|
3274
|
+
fraction
|
|
3275
|
+
};
|
|
3276
|
+
});
|
|
3277
|
+
}
|
|
3278
|
+
/** The payment-COUNT leash — settled count so far + the configured lifetime/window
|
|
3279
|
+
* caps and what's left. Pure; never throws. */
|
|
3280
|
+
countStatus() {
|
|
3281
|
+
const policy = this.opts.policy;
|
|
3282
|
+
const settled = this.ledger.count();
|
|
3283
|
+
const out = { settled };
|
|
3284
|
+
if (policy?.maxPayments !== void 0) {
|
|
3285
|
+
out.lifetimeCap = policy.maxPayments;
|
|
3286
|
+
out.lifetimeRemaining = Math.max(0, policy.maxPayments - settled);
|
|
3287
|
+
}
|
|
3288
|
+
if (policy?.maxPaymentsPerWindow !== void 0 && policy.windowSeconds !== void 0) {
|
|
3289
|
+
const windowSettled = this.ledger.countSince(Date.now() - policy.windowSeconds * 1e3);
|
|
3290
|
+
out.windowCap = policy.maxPaymentsPerWindow;
|
|
3291
|
+
out.windowSettled = windowSettled;
|
|
3292
|
+
out.windowRemaining = Math.max(0, policy.maxPaymentsPerWindow - windowSettled);
|
|
3293
|
+
}
|
|
3294
|
+
return out;
|
|
3295
|
+
}
|
|
2957
3296
|
/**
|
|
2958
3297
|
* Per-(network, asset) remaining budget — ONE row per pair the ledger already
|
|
2959
3298
|
* holds (decimals are known only after the first spend), so a fresh client with
|
|
@@ -3371,7 +3710,7 @@ var PipRailClient = class {
|
|
|
3371
3710
|
const shortfall = {};
|
|
3372
3711
|
if (!quote.withinPolicy) {
|
|
3373
3712
|
blockers.push(
|
|
3374
|
-
quote.policyCode === "SESSION_EXPIRED" || quote.policyCode === "WINDOW_TOTAL" ? "OUTSIDE_WINDOW" : "OUTSIDE_POLICY"
|
|
3713
|
+
quote.policyCode === "SESSION_EXPIRED" || quote.policyCode === "WINDOW_TOTAL" || quote.policyCode === "WINDOW_COUNT" ? "OUTSIDE_WINDOW" : "OUTSIDE_POLICY"
|
|
3375
3714
|
);
|
|
3376
3715
|
}
|
|
3377
3716
|
if (quote.symbolMismatch) warnings.push("SYMBOL_MISMATCH");
|
|
@@ -3450,6 +3789,11 @@ var PipRailClient = class {
|
|
|
3450
3789
|
`challenge for ${accept.asset} on ${accept.network} states no valid decimals and the SDK doesn't recognise the token \u2014 refusing to price it.`
|
|
3451
3790
|
);
|
|
3452
3791
|
}
|
|
3792
|
+
if (decimals > MAX_DECIMALS) {
|
|
3793
|
+
throw new InvalidEnvelopeError(
|
|
3794
|
+
`challenge for ${accept.asset} on ${accept.network} states ${decimals} decimals (> ${MAX_DECIMALS}) \u2014 refusing to price it (no real token is that deep).`
|
|
3795
|
+
);
|
|
3796
|
+
}
|
|
3453
3797
|
const symbol = described?.symbol ?? accept.extra?.symbol;
|
|
3454
3798
|
const amountFormatted = formatUnits(amountBase, decimals);
|
|
3455
3799
|
const intent = {
|
|
@@ -3464,9 +3808,14 @@ var PipRailClient = class {
|
|
|
3464
3808
|
};
|
|
3465
3809
|
const policy = this.opts.policy;
|
|
3466
3810
|
const hasWindow = !!policy && policy.windowTotal != null && policy.windowSeconds != null;
|
|
3811
|
+
const hasWindowCount = !!policy && policy.maxPaymentsPerWindow != null && policy.windowSeconds != null;
|
|
3812
|
+
const hasDenomCap = !!policy && policy.maxTotalPerDenom != null && Object.keys(policy.maxTotalPerDenom).length > 0;
|
|
3813
|
+
const hasCountCap = !!policy && policy.maxPayments != null;
|
|
3467
3814
|
const hasTimePolicy = !!policy && (policy.ttlSeconds != null || policy.expiresAt != null || hasWindow);
|
|
3815
|
+
const needsCtx = hasTimePolicy || hasWindowCount || hasDenomCap || hasCountCap;
|
|
3816
|
+
const denom = hasDenomCap ? denomOf(intent.symbol, intent.asset, policy) : void 0;
|
|
3468
3817
|
const now = Date.now();
|
|
3469
|
-
const ctx =
|
|
3818
|
+
const ctx = needsCtx ? {
|
|
3470
3819
|
now,
|
|
3471
3820
|
sessionStart: this.ledger.sessionStart,
|
|
3472
3821
|
// Window slice ONLY when BOTH fields are set — never a `?? 0` width.
|
|
@@ -3474,7 +3823,12 @@ var PipRailClient = class {
|
|
|
3474
3823
|
accept.network,
|
|
3475
3824
|
accept.asset,
|
|
3476
3825
|
now - policy.windowSeconds * 1e3
|
|
3477
|
-
) : 0n
|
|
3826
|
+
) : 0n,
|
|
3827
|
+
// Running grand total on this intent's denomination, across the (shared) ledger.
|
|
3828
|
+
spentInDenomScaled: denom ? this.ledger.totalForDenom(denom) : 0n,
|
|
3829
|
+
// Settled-payment counts (lifetime + window) for the count caps.
|
|
3830
|
+
paymentCount: hasCountCap ? this.ledger.count() : 0,
|
|
3831
|
+
paymentCountInWindow: hasWindowCount ? this.ledger.countSince(now - policy.windowSeconds * 1e3) : 0
|
|
3478
3832
|
} : void 0;
|
|
3479
3833
|
const decision = evaluatePolicy(
|
|
3480
3834
|
intent,
|
|
@@ -3510,9 +3864,11 @@ var PipRailClient = class {
|
|
|
3510
3864
|
async authorize(quote) {
|
|
3511
3865
|
if (!quote.withinPolicy) {
|
|
3512
3866
|
const reason = `Payment refused by policy: ${quote.policyReason ?? "not allowed"}`;
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3867
|
+
this.refuse(reason, {
|
|
3868
|
+
reasonCode: reasonCodeForPolicy(quote.policyCode),
|
|
3869
|
+
policyCode: quote.policyCode,
|
|
3870
|
+
quote
|
|
3871
|
+
});
|
|
3516
3872
|
}
|
|
3517
3873
|
const hook = this.opts.onBeforePay;
|
|
3518
3874
|
if (!hook) return;
|
|
@@ -3520,32 +3876,125 @@ var PipRailClient = class {
|
|
|
3520
3876
|
try {
|
|
3521
3877
|
approved = await hook(quote);
|
|
3522
3878
|
} catch (err) {
|
|
3523
|
-
|
|
3524
|
-
this.safeEmit({ kind: "payment-failed", reason, code: "APPROVAL" });
|
|
3525
|
-
throw new PaymentDeclinedError(reason, { cause: err, reasonCode: "APPROVAL" });
|
|
3879
|
+
this.refuse("onBeforePay threw \u2014 refusing to pay.", { reasonCode: "APPROVAL", quote, cause: err });
|
|
3526
3880
|
}
|
|
3527
3881
|
if (!approved) {
|
|
3528
3882
|
const reason = `onBeforePay declined ${quote.amountFormatted} ${quote.symbol ?? ""}`.trimEnd() + ` on ${quote.network}.`;
|
|
3529
|
-
this.
|
|
3530
|
-
throw new PaymentDeclinedError(reason, { reasonCode: "APPROVAL" });
|
|
3883
|
+
this.refuse(reason, { reasonCode: "APPROVAL", quote });
|
|
3531
3884
|
}
|
|
3532
3885
|
}
|
|
3533
|
-
/**
|
|
3886
|
+
/**
|
|
3887
|
+
* Refuse a payment BEFORE any send: emit BOTH the legacy `payment-failed` (so existing
|
|
3888
|
+
* `onEvent` consumers are unaffected) AND the richer, dedicated `payment-declined`
|
|
3889
|
+
* (typed reasonCode + fine PolicyDenyCode + the quote + a budget snapshot), then throw
|
|
3890
|
+
* the typed {@link PaymentDeclinedError}. Returns `never` so callers' control flow is
|
|
3891
|
+
* exhaustive.
|
|
3892
|
+
*/
|
|
3893
|
+
refuse(reason, opts) {
|
|
3894
|
+
this.safeEmit({ kind: "payment-failed", reason, ...opts.reasonCode ? { code: opts.reasonCode } : {} });
|
|
3895
|
+
this.safeEmit({
|
|
3896
|
+
kind: "payment-declined",
|
|
3897
|
+
reason,
|
|
3898
|
+
...opts.reasonCode ? { reasonCode: opts.reasonCode } : {},
|
|
3899
|
+
...opts.policyCode ? { code: opts.policyCode } : {},
|
|
3900
|
+
...opts.quote ? { quote: opts.quote } : {},
|
|
3901
|
+
budget: this.budget()
|
|
3902
|
+
});
|
|
3903
|
+
throw new PaymentDeclinedError(reason, {
|
|
3904
|
+
...opts.reasonCode ? { reasonCode: opts.reasonCode } : {},
|
|
3905
|
+
...opts.cause !== void 0 ? { cause: opts.cause } : {}
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
/** Record a settled payment in the ledger (TRUE decimals for the running total + the
|
|
3909
|
+
* denomination it counts toward in the grand total). Then fire the `onSpend` callback
|
|
3910
|
+
* with the record + the post-payment budget, and emit any `warnAtFraction` thresholds
|
|
3911
|
+
* this payment just crossed. All observability is isolated — a throwing hook never
|
|
3912
|
+
* affects the (already-settled) payment. */
|
|
3534
3913
|
recordSpend(quote, ref) {
|
|
3535
|
-
this.
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3914
|
+
const denom = denomOf(quote.symbol, quote.asset, this.opts.policy);
|
|
3915
|
+
const record = {
|
|
3916
|
+
url: quote.url,
|
|
3917
|
+
host: hostOf2(quote.url),
|
|
3918
|
+
network: quote.network,
|
|
3919
|
+
asset: quote.asset,
|
|
3920
|
+
amountBase: quote.amount,
|
|
3921
|
+
amountFormatted: quote.amountFormatted,
|
|
3922
|
+
...quote.symbol ? { symbol: quote.symbol } : {},
|
|
3923
|
+
decimals: quote.decimals,
|
|
3924
|
+
...denom ? { denom } : {},
|
|
3925
|
+
ref,
|
|
3926
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3927
|
+
};
|
|
3928
|
+
this.ledger.record(record, quote.decimals, denom);
|
|
3929
|
+
const budget = this.budget();
|
|
3930
|
+
if (this.opts.onSpend) {
|
|
3931
|
+
try {
|
|
3932
|
+
this.opts.onSpend(record, budget);
|
|
3933
|
+
} catch {
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
this.emitThresholds(budget);
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Emit a `budget-threshold` event for each cap whose used-fraction just reached
|
|
3940
|
+
* `policy.warnAtFraction` — the early warning before a hard decline. Fires ONCE per
|
|
3941
|
+
* crossing per cap (deduped on the shared ledger via `markWarned`, so a cross-chain
|
|
3942
|
+
* threshold fires once for the whole budget, not once per chain). No-op when no
|
|
3943
|
+
* `warnAtFraction` is set. Reads the just-computed {@link SessionBudget}; isolated (safeEmit).
|
|
3944
|
+
*/
|
|
3945
|
+
emitThresholds(budget) {
|
|
3946
|
+
const frac = this.opts.policy?.warnAtFraction;
|
|
3947
|
+
if (frac === void 0) return;
|
|
3948
|
+
const fire = (scope, label, spentFormatted, capFormatted, fraction) => {
|
|
3949
|
+
if (fraction < frac) return;
|
|
3950
|
+
if (!this.ledger.markWarned(`${scope}:${label}`)) return;
|
|
3951
|
+
this.safeEmit({ kind: "budget-threshold", scope, label, spentFormatted, capFormatted, fraction });
|
|
3952
|
+
};
|
|
3953
|
+
for (const r of budget.byAsset) {
|
|
3954
|
+
if (r.capBase === void 0) continue;
|
|
3955
|
+
const cap = BigInt(r.capBase);
|
|
3956
|
+
const spent = BigInt(r.spentBase);
|
|
3957
|
+
const fraction = cap === 0n ? spent > 0n ? 1 : 0 : Number(spent * 1000000n / cap) / 1e6;
|
|
3958
|
+
fire(
|
|
3959
|
+
"asset",
|
|
3960
|
+
`${r.symbol ?? r.asset} on ${r.network}`,
|
|
3961
|
+
formatUnits(spent, r.decimals),
|
|
3962
|
+
formatUnits(cap, r.decimals),
|
|
3963
|
+
fraction
|
|
3964
|
+
);
|
|
3965
|
+
}
|
|
3966
|
+
for (const d of budget.byDenom) {
|
|
3967
|
+
fire("denom", d.denom, d.spentFormatted, d.capFormatted, d.fraction);
|
|
3968
|
+
}
|
|
3969
|
+
const policy = this.opts.policy;
|
|
3970
|
+
if (policy?.windowTotal !== void 0 && policy.windowSeconds !== void 0) {
|
|
3971
|
+
const since = Date.now() - policy.windowSeconds * 1e3;
|
|
3972
|
+
for (const r of budget.byAsset) {
|
|
3973
|
+
const cap = floorUnits(policy.windowTotal, r.decimals);
|
|
3974
|
+
const spent = this.ledger.totalSince(r.network, r.asset, since);
|
|
3975
|
+
const fraction = cap === 0n ? spent > 0n ? 1 : 0 : Number(spent * 1000000n / cap) / 1e6;
|
|
3976
|
+
fire(
|
|
3977
|
+
"window",
|
|
3978
|
+
`${r.symbol ?? r.asset} on ${r.network}`,
|
|
3979
|
+
formatUnits(spent, r.decimals),
|
|
3980
|
+
formatUnits(cap, r.decimals),
|
|
3981
|
+
fraction
|
|
3982
|
+
);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
const c = budget.counts;
|
|
3986
|
+
if (c.lifetimeCap !== void 0 && c.lifetimeCap > 0) {
|
|
3987
|
+
fire("count", "maxPayments", String(c.settled), String(c.lifetimeCap), c.settled / c.lifetimeCap);
|
|
3988
|
+
}
|
|
3989
|
+
if (c.windowCap !== void 0 && c.windowCap > 0 && c.windowSettled !== void 0) {
|
|
3990
|
+
fire(
|
|
3991
|
+
"window-count",
|
|
3992
|
+
"maxPaymentsPerWindow",
|
|
3993
|
+
String(c.windowSettled),
|
|
3994
|
+
String(c.windowCap),
|
|
3995
|
+
c.windowSettled / c.windowCap
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3549
3998
|
}
|
|
3550
3999
|
async payAndConfirm(net, wallet, accept) {
|
|
3551
4000
|
if (!this.supportsNetwork(net, accept.network)) {
|
|
@@ -3843,8 +4292,11 @@ function reasonCodeForPolicy(code) {
|
|
|
3843
4292
|
case "SESSION_EXPIRED":
|
|
3844
4293
|
return "SESSION_EXPIRED";
|
|
3845
4294
|
case "WINDOW_TOTAL":
|
|
4295
|
+
case "WINDOW_COUNT":
|
|
3846
4296
|
return "OUTSIDE_WINDOW";
|
|
3847
4297
|
case "MAX_TOTAL":
|
|
4298
|
+
case "MAX_TOTAL_DENOM":
|
|
4299
|
+
case "MAX_PAYMENTS":
|
|
3848
4300
|
return "BUDGET";
|
|
3849
4301
|
case void 0:
|
|
3850
4302
|
return void 0;
|
|
@@ -3902,6 +4354,10 @@ async function readInvalidReason(response) {
|
|
|
3902
4354
|
// src/payer.ts
|
|
3903
4355
|
var MultiChainPayer = class _MultiChainPayer {
|
|
3904
4356
|
_clients;
|
|
4357
|
+
/** True when every client shares ONE ledger (the `fromWallets` path) — so spend +
|
|
4358
|
+
* budget are read from that single source, never concatenated (which would
|
|
4359
|
+
* double-count a shared ledger). */
|
|
4360
|
+
_shared;
|
|
3905
4361
|
/**
|
|
3906
4362
|
* Wrap an explicit, ordered set of single-chain clients — use this when a client
|
|
3907
4363
|
* needs full control (e.g. a custom EVM chain configured by a viem `Chain`). The
|
|
@@ -3909,12 +4365,18 @@ var MultiChainPayer = class _MultiChainPayer {
|
|
|
3909
4365
|
* at MOST one client per chain — two clients on the SAME network would double-count in
|
|
3910
4366
|
* `spent()`/`budget()` and waste a plan round-trip (`fromWallets` can't produce this).
|
|
3911
4367
|
* For the common case, prefer {@link MultiChainPayer.fromWallets}.
|
|
4368
|
+
*
|
|
4369
|
+
* For a cross-chain grand total / count cap via this path, build the clients with a
|
|
4370
|
+
* SHARED `ledger` (`new SpendLedger(store?)` passed to each client's `ledger` option)
|
|
4371
|
+
* and set `opts.shared = true` so reads come from that one ledger. `fromWallets` does
|
|
4372
|
+
* this for you.
|
|
3912
4373
|
*/
|
|
3913
|
-
constructor(clients) {
|
|
4374
|
+
constructor(clients, opts = {}) {
|
|
3914
4375
|
if (clients.length === 0) {
|
|
3915
4376
|
throw new TypeError("MultiChainPayer needs at least one PipRailClient.");
|
|
3916
4377
|
}
|
|
3917
4378
|
this._clients = [...clients];
|
|
4379
|
+
this._shared = opts.shared ?? false;
|
|
3918
4380
|
}
|
|
3919
4381
|
/**
|
|
3920
4382
|
* Build one client per funded chain from a `{ chain → wallet }` map — the
|
|
@@ -3929,20 +4391,26 @@ var MultiChainPayer = class _MultiChainPayer {
|
|
|
3929
4391
|
* solana: { key: process.env.SOLANA_SECRET! },
|
|
3930
4392
|
* xrpl: { key: process.env.XRPL_SEED! },
|
|
3931
4393
|
* },
|
|
3932
|
-
*
|
|
4394
|
+
* // ONE budget across all three chains: $20 total + at most 100 payments.
|
|
4395
|
+
* policy: { maxAmount: '1.00', maxTotalPerDenom: { USD: '20.00' }, maxPayments: 100 },
|
|
3933
4396
|
* })
|
|
3934
4397
|
* const res = await payer.get('https://api.example.com/paid') // pays on the first funded chain that can settle
|
|
3935
4398
|
* ```
|
|
4399
|
+
*
|
|
4400
|
+
* Every client shares ONE {@link SpendLedger} (optionally backed by `spendStore`), so
|
|
4401
|
+
* the cross-token grand total + the count caps span all chains as a single budget.
|
|
3936
4402
|
*/
|
|
3937
4403
|
static fromWallets(opts) {
|
|
3938
4404
|
const entries = Object.entries(opts.wallets);
|
|
3939
4405
|
if (entries.length === 0) {
|
|
3940
4406
|
throw new TypeError("MultiChainPayer.fromWallets needs at least one wallet.");
|
|
3941
4407
|
}
|
|
4408
|
+
const ledger = new SpendLedger(opts.spendStore);
|
|
3942
4409
|
const clients = entries.map(
|
|
3943
4410
|
([chain, wallet]) => new PipRailClient({
|
|
3944
4411
|
chain,
|
|
3945
4412
|
wallet,
|
|
4413
|
+
ledger,
|
|
3946
4414
|
...opts.policy ? { policy: opts.policy } : {},
|
|
3947
4415
|
...opts.schemes ? { schemes: opts.schemes } : {},
|
|
3948
4416
|
...opts.rpcUrls?.[chain] ? { rpcUrl: opts.rpcUrls[chain] } : {},
|
|
@@ -3952,7 +4420,7 @@ var MultiChainPayer = class _MultiChainPayer {
|
|
|
3952
4420
|
...opts.retryTimeoutMs != null ? { retryTimeoutMs: opts.retryTimeoutMs } : {}
|
|
3953
4421
|
})
|
|
3954
4422
|
);
|
|
3955
|
-
return new _MultiChainPayer(clients);
|
|
4423
|
+
return new _MultiChainPayer(clients, { shared: true });
|
|
3956
4424
|
}
|
|
3957
4425
|
/** The underlying single-chain clients, in preference order. Reach for one of
|
|
3958
4426
|
* these for chain-specific reads (`estimateCost`, `discoverySigner`, per-chain
|
|
@@ -4046,28 +4514,50 @@ var MultiChainPayer = class _MultiChainPayer {
|
|
|
4046
4514
|
register(url, opts = {}) {
|
|
4047
4515
|
return this._clients[0].register(url, opts);
|
|
4048
4516
|
}
|
|
4049
|
-
/**
|
|
4050
|
-
*
|
|
4517
|
+
/**
|
|
4518
|
+
* Aggregate spend across every chain. With a SHARED ledger (`fromWallets`), this reads
|
|
4519
|
+
* that single ledger ONCE (its records already span every chain) — so the count and the
|
|
4520
|
+
* cross-token `byDenom` grand total are correct and never double-counted. With
|
|
4521
|
+
* independent clients (the explicit constructor), it concatenates: counts summed,
|
|
4522
|
+
* per-(network,asset) `byAsset` + `byDenom` + records concatenated.
|
|
4523
|
+
*/
|
|
4051
4524
|
spent() {
|
|
4525
|
+
if (this._shared) return this._clients[0].spent();
|
|
4052
4526
|
const summaries = this._clients.map((c) => c.spent());
|
|
4053
4527
|
return {
|
|
4054
4528
|
count: summaries.reduce((n, s) => n + s.count, 0),
|
|
4055
4529
|
byAsset: summaries.flatMap((s) => s.byAsset),
|
|
4530
|
+
byDenom: summaries.flatMap((s) => s.byDenom),
|
|
4056
4531
|
records: summaries.flatMap((s) => s.records)
|
|
4057
4532
|
};
|
|
4058
4533
|
}
|
|
4059
|
-
/**
|
|
4060
|
-
*
|
|
4061
|
-
*
|
|
4062
|
-
*
|
|
4534
|
+
/**
|
|
4535
|
+
* A merged budget view. With a SHARED ledger (`fromWallets`) it's the single source of
|
|
4536
|
+
* truth — `byAsset`/`byDenom`/`counts` already span every chain, so the grand-total and
|
|
4537
|
+
* count leashes read as ONE budget. With independent clients it merges: per-(network,
|
|
4538
|
+
* asset) rows + per-denom rows concatenated, counts summed, and the MOST-RESTRICTIVE
|
|
4539
|
+
* session time envelope (the soonest deadline wins). Per-chain detail is on each
|
|
4540
|
+
* `clients[i].budget()`.
|
|
4541
|
+
*/
|
|
4063
4542
|
budget() {
|
|
4543
|
+
if (this._shared) return this._clients[0].budget();
|
|
4064
4544
|
const budgets = this._clients.map((c) => c.budget());
|
|
4065
4545
|
const session = budgets.map((b) => b.session).reduce((soonest, s) => {
|
|
4066
4546
|
if (soonest.secondsRemaining == null) return s;
|
|
4067
4547
|
if (s.secondsRemaining == null) return soonest;
|
|
4068
4548
|
return s.secondsRemaining < soonest.secondsRemaining ? s : soonest;
|
|
4069
4549
|
});
|
|
4070
|
-
return {
|
|
4550
|
+
return {
|
|
4551
|
+
session,
|
|
4552
|
+
byAsset: budgets.flatMap((b) => b.byAsset),
|
|
4553
|
+
byDenom: budgets.flatMap((b) => b.byDenom),
|
|
4554
|
+
counts: { settled: budgets.reduce((n, b) => n + b.counts.settled, 0) }
|
|
4555
|
+
};
|
|
4556
|
+
}
|
|
4557
|
+
/** The CONFIGURED spend policy (the shared policy `fromWallets` applies to every chain;
|
|
4558
|
+
* the first client's for the explicit constructor). `undefined` when none is set. */
|
|
4559
|
+
policy() {
|
|
4560
|
+
return this._clients[0].policy();
|
|
4071
4561
|
}
|
|
4072
4562
|
};
|
|
4073
4563
|
function isBodyInit(value) {
|
|
@@ -4172,9 +4662,12 @@ function explainDecline(err) {
|
|
|
4172
4662
|
}
|
|
4173
4663
|
function formatSpendReport(summary) {
|
|
4174
4664
|
if (summary.count === 0) return "No payments yet.";
|
|
4175
|
-
|
|
4665
|
+
const perAsset = summary.byAsset.map(
|
|
4176
4666
|
(a) => `${a.totalFormatted} ${a.symbol ?? a.asset} on ${a.network} (${a.count} payment${a.count === 1 ? "" : "s"})`
|
|
4177
4667
|
).join("; ");
|
|
4668
|
+
if (summary.byDenom.length === 0) return perAsset;
|
|
4669
|
+
const grand = summary.byDenom.map((d) => `${d.totalFormatted} ${d.denom} total`).join("; ");
|
|
4670
|
+
return `${perAsset} \u2014 grand total: ${grand}`;
|
|
4178
4671
|
}
|
|
4179
4672
|
function describeChallenge(challenge) {
|
|
4180
4673
|
const first = challenge.accepts[0];
|
|
@@ -4253,9 +4746,11 @@ Branch on \`code\` (always reliable). Key cases:
|
|
|
4253
4746
|
once (enable the exact scheme); report it, don't retry the same call blindly.
|
|
4254
4747
|
|
|
4255
4748
|
## Knowing your leash \u2014 call piprail_budget
|
|
4256
|
-
piprail_budget tells you how much budget and time you have left
|
|
4257
|
-
|
|
4258
|
-
|
|
4749
|
+
piprail_budget tells you how much budget and time you have left: per (network,
|
|
4750
|
+
asset) remaining, the cross-token GRAND TOTAL per denomination (e.g. how much
|
|
4751
|
+
USD you can still spend across every stablecoin and chain), the payment-count
|
|
4752
|
+
leash, the session time envelope, your spend so far, and the configured policy
|
|
4753
|
+
read back. Read-only; moves no funds. Use it in Mode A to self-check before paying.
|
|
4259
4754
|
|
|
4260
4755
|
## Two modes
|
|
4261
4756
|
- Mode A (headless, default): you run FREE inside a pre-set budget + time
|
|
@@ -4266,10 +4761,15 @@ Mode A to self-check before paying.
|
|
|
4266
4761
|
do NOT retry it as if it were a transient error.
|
|
4267
4762
|
|
|
4268
4763
|
## Hard facts
|
|
4269
|
-
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4764
|
+
- Per-payment + per-(network, asset) caps always apply. A cross-token GRAND TOTAL per
|
|
4765
|
+
denomination (maxTotalPerDenom, e.g. "$20 across every USD stablecoin + chain") is
|
|
4766
|
+
OPTIONAL \u2014 it sums tokens declared as one unit, each 1:1; it is NOT a price oracle and
|
|
4767
|
+
never prices a volatile native coin. Payment-COUNT caps (maxPayments / per-window) also
|
|
4768
|
+
span every chain + token.
|
|
4769
|
+
- The time envelope lives IN-MEMORY for THIS process (resets on restart). The money + count
|
|
4770
|
+
totals also reset on restart UNLESS a durable spend store is configured \u2014 then they resume.
|
|
4771
|
+
- A refusal arrives as declined:true with a reasonCode; 'BUDGET' covers the lifetime, denom,
|
|
4772
|
+
and count caps; 'OUTSIDE_WINDOW' covers both the rolling money and rolling count windows.
|
|
4273
4773
|
`;
|
|
4274
4774
|
function agentGuide() {
|
|
4275
4775
|
return PIPRAIL_AGENT_GUIDE;
|
|
@@ -4584,11 +5084,11 @@ function paymentTools(client) {
|
|
|
4584
5084
|
},
|
|
4585
5085
|
{
|
|
4586
5086
|
name: "piprail_budget",
|
|
4587
|
-
description: "Read how much of your spend budget and time leash is left \u2014 per (network, asset) remaining, the session time envelope, and your spend so far. Use it in Mode A (headless) to self-check BEFORE paying, so you never discover the leash by hitting a decline. Read-only; moves no funds. NOTE:
|
|
5087
|
+
description: "Read how much of your spend budget and time leash is left \u2014 per (network, asset) remaining, the cross-token GRAND TOTAL per denomination (e.g. how much USD you can still spend across every stablecoin and chain), the payment-count leash, the session time envelope, and your spend so far. Use it in Mode A (headless) to self-check BEFORE paying, so you never discover the leash by hitting a decline. Read-only; moves no funds. NOTE: the time envelope is in-memory for THIS process; the money/count totals persist only if a spend store is configured.",
|
|
4588
5088
|
annotations: {
|
|
4589
5089
|
title: "Check remaining budget",
|
|
4590
5090
|
readOnlyHint: true,
|
|
4591
|
-
// reads the
|
|
5091
|
+
// reads the ledger + policy; never pays
|
|
4592
5092
|
idempotentHint: true
|
|
4593
5093
|
// a pure read
|
|
4594
5094
|
},
|
|
@@ -4601,7 +5101,13 @@ function paymentTools(client) {
|
|
|
4601
5101
|
return {
|
|
4602
5102
|
spent,
|
|
4603
5103
|
remaining: budget.byAsset,
|
|
5104
|
+
grandTotal: budget.byDenom,
|
|
5105
|
+
// cross-token spend cap per denomination (USD/EUR/…)
|
|
5106
|
+
counts: budget.counts,
|
|
5107
|
+
// payment-count leash (settled + lifetime/window caps)
|
|
4604
5108
|
session: budget.session,
|
|
5109
|
+
policy: client.policy() ?? null,
|
|
5110
|
+
// the configured leash, read back
|
|
4605
5111
|
report: formatSpendReport(spent)
|
|
4606
5112
|
};
|
|
4607
5113
|
} catch (err) {
|
|
@@ -5906,8 +6412,10 @@ async function deliverReceipt(receipt, options) {
|
|
|
5906
6412
|
}
|
|
5907
6413
|
export {
|
|
5908
6414
|
BRAND,
|
|
6415
|
+
BUILTIN_DENOMS,
|
|
5909
6416
|
CHAINS,
|
|
5910
6417
|
ConfirmationTimeoutError,
|
|
6418
|
+
DENOM_PRECISION,
|
|
5911
6419
|
DIRECTORY_INFO,
|
|
5912
6420
|
EIP3009_TYPES,
|
|
5913
6421
|
EXACT_NETWORK_SLUGS,
|
|
@@ -5937,6 +6445,7 @@ export {
|
|
|
5937
6445
|
REGISTER_ATTRIBUTION,
|
|
5938
6446
|
RecipientNotReadyError,
|
|
5939
6447
|
SettlementError,
|
|
6448
|
+
SpendLedger,
|
|
5940
6449
|
UnknownTokenError,
|
|
5941
6450
|
UnsupportedNetworkError,
|
|
5942
6451
|
UnsupportedSchemeError,
|
|
@@ -5964,6 +6473,7 @@ export {
|
|
|
5964
6473
|
createPaymentGate,
|
|
5965
6474
|
decorateOutcome,
|
|
5966
6475
|
deliverReceipt,
|
|
6476
|
+
denomOf,
|
|
5967
6477
|
describeChallenge,
|
|
5968
6478
|
discoveryHeaders,
|
|
5969
6479
|
eip3009Abi,
|
|
@@ -5977,6 +6487,7 @@ export {
|
|
|
5977
6487
|
getDirectoryInfo,
|
|
5978
6488
|
isPermit2ProxyChain,
|
|
5979
6489
|
knownFacilitatorsFor,
|
|
6490
|
+
memorySpendStore,
|
|
5980
6491
|
normalizeNetwork,
|
|
5981
6492
|
parseChallenge,
|
|
5982
6493
|
parseExactPaymentHeader,
|