@piprail/sdk 2.7.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 +96 -0
- package/README.md +5 -3
- package/dist/{algorand-WB6PBJU4.js → algorand-AA3WXKW4.js} +1 -1
- package/dist/{algorand-TMA62DN2.cjs → algorand-FCEECDG6.cjs} +32 -32
- package/dist/{aptos-JQMZNTMD.cjs → aptos-GHJPO6JJ.cjs} +31 -31
- package/dist/{aptos-QAAXIUY3.js → aptos-LY67Q6QF.js} +1 -1
- package/dist/{chunk-JG6KRAW6.cjs → chunk-MWBT7MCE.cjs} +13 -1
- package/dist/chunk-O4UQOZ4Z.cjs +14 -0
- package/dist/{chunk-7XK22JSQ.js → chunk-SC2ZYDHD.js} +12 -0
- package/dist/chunk-SK3CB7UA.js +14 -0
- package/dist/index.cjs +754 -238
- package/dist/index.d.cts +2386 -2577
- package/dist/index.d.ts +2386 -2577
- package/dist/index.js +595 -79
- package/dist/ledger-BtzrfO-3.d.cts +658 -0
- package/dist/ledger-BtzrfO-3.d.ts +658 -0
- package/dist/{near-H5AQ253I.cjs → near-6KAQVVG2.cjs} +26 -26
- package/dist/{near-OTPQD6BI.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-3FMCWSEE.js → solana-ELUWO6N5.js} +33 -2
- package/dist/{solana-M3VOHCMO.cjs → solana-MYF4HBO4.cjs} +64 -33
- package/dist/{stellar-U5NCRIOJ.js → stellar-BEMT7UYF.js} +1 -1
- package/dist/{stellar-FW6C6FBE.cjs → stellar-SUKASK4N.cjs} +20 -20
- package/dist/{sui-Y53M4GUM.js → sui-F5JQ2N6I.js} +1 -1
- package/dist/{sui-47C2KEZI.cjs → sui-VE5LT7BL.cjs} +16 -16
- package/dist/{ton-5ZPT5PSP.js → ton-7GKCTC5H.js} +11 -7
- package/dist/{ton-MMPKWT6N.cjs → ton-AOR3EURW.cjs} +25 -21
- package/dist/{tron-JOT4STIG.cjs → tron-EMFXDFHW.cjs} +24 -24
- package/dist/{tron-WYS4X2I5.js → tron-ZZZS3FNN.js} +1 -1
- package/dist/{xrpl-2MZEOIFY.js → xrpl-DD7TJL5L.js} +1 -1
- package/dist/{xrpl-PECT4IMX.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`,
|
|
@@ -2027,7 +2031,7 @@ var SLUG_TO_CAIP2 = {
|
|
|
2027
2031
|
bsc: "eip155:56",
|
|
2028
2032
|
// non-EVM families — values mirror each driver's bound caip2 exactly
|
|
2029
2033
|
solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
2030
|
-
ton: "
|
|
2034
|
+
ton: "tvm:-239",
|
|
2031
2035
|
tron: "tron:mainnet",
|
|
2032
2036
|
near: "near:mainnet",
|
|
2033
2037
|
sui: "sui:mainnet",
|
|
@@ -2036,7 +2040,12 @@ var SLUG_TO_CAIP2 = {
|
|
|
2036
2040
|
stellar: "stellar:pubnet",
|
|
2037
2041
|
xrpl: "xrpl:0"
|
|
2038
2042
|
};
|
|
2043
|
+
var LEGACY_CAIP2_ALIAS = {
|
|
2044
|
+
"ton:-239": "tvm:-239"
|
|
2045
|
+
};
|
|
2039
2046
|
function normalizeNetwork(network) {
|
|
2047
|
+
const legacy = LEGACY_CAIP2_ALIAS[network];
|
|
2048
|
+
if (legacy) return legacy;
|
|
2040
2049
|
if (network.includes(":")) return network;
|
|
2041
2050
|
return SLUG_TO_CAIP2[network.toLowerCase()] ?? network;
|
|
2042
2051
|
}
|
|
@@ -2577,6 +2586,53 @@ function encodeBase642(str) {
|
|
|
2577
2586
|
}
|
|
2578
2587
|
|
|
2579
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
|
+
}
|
|
2580
2636
|
var ALLOW = { allowed: true };
|
|
2581
2637
|
var deny = (code, reason) => ({
|
|
2582
2638
|
allowed: false,
|
|
@@ -2660,6 +2716,34 @@ function evaluatePolicy(intent, policy, spentForAssetBase, ctx) {
|
|
|
2660
2716
|
);
|
|
2661
2717
|
}
|
|
2662
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
|
+
}
|
|
2663
2747
|
if (ctx && policy.windowTotal !== void 0 && policy.windowSeconds !== void 0) {
|
|
2664
2748
|
const cap = floorUnits(policy.windowTotal, intent.decimals);
|
|
2665
2749
|
if (ctx.spentInWindowBase + intent.amountBase > cap) {
|
|
@@ -2669,6 +2753,14 @@ function evaluatePolicy(intent, policy, spentForAssetBase, ctx) {
|
|
|
2669
2753
|
);
|
|
2670
2754
|
}
|
|
2671
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
|
+
}
|
|
2672
2764
|
return ALLOW;
|
|
2673
2765
|
}
|
|
2674
2766
|
|
|
@@ -2677,37 +2769,135 @@ var keyFor = (network, asset) => `${network}|${asset}`;
|
|
|
2677
2769
|
var SpendLedger = class {
|
|
2678
2770
|
records = [];
|
|
2679
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;
|
|
2680
2781
|
/**
|
|
2681
2782
|
* Session clock origin (epoch-ms) — process/session start = ledger
|
|
2682
2783
|
* construction. In-memory; a new process is a new session. The client reads it
|
|
2683
2784
|
* to compute the `ttlSeconds` deadline and the rolling-window slice.
|
|
2684
2785
|
*/
|
|
2685
2786
|
sessionStart = Date.now();
|
|
2686
|
-
/**
|
|
2687
|
-
*
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
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);
|
|
2691
2825
|
const bucket = this.buckets.get(key);
|
|
2692
2826
|
if (bucket) {
|
|
2693
|
-
bucket.total += BigInt(
|
|
2827
|
+
bucket.total += BigInt(rec.amountBase);
|
|
2694
2828
|
bucket.count += 1;
|
|
2695
|
-
if (!bucket.symbol &&
|
|
2829
|
+
if (!bucket.symbol && rec.symbol) bucket.symbol = rec.symbol;
|
|
2696
2830
|
} else {
|
|
2697
2831
|
this.buckets.set(key, {
|
|
2698
|
-
network:
|
|
2699
|
-
asset:
|
|
2700
|
-
symbol:
|
|
2832
|
+
network: rec.network,
|
|
2833
|
+
asset: rec.asset,
|
|
2834
|
+
symbol: rec.symbol,
|
|
2701
2835
|
decimals,
|
|
2702
|
-
total: BigInt(
|
|
2836
|
+
total: BigInt(rec.amountBase),
|
|
2703
2837
|
count: 1
|
|
2704
2838
|
});
|
|
2705
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
|
+
}
|
|
2706
2861
|
}
|
|
2707
2862
|
/** Running total (base units) already spent on this (network, asset). */
|
|
2708
2863
|
totalFor(network, asset) {
|
|
2709
2864
|
return this.buckets.get(keyFor(network, asset))?.total ?? 0n;
|
|
2710
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
|
+
}
|
|
2711
2901
|
/**
|
|
2712
2902
|
* Sum of base-unit amounts for (network, asset) whose record `at` (ISO
|
|
2713
2903
|
* timestamp) is at or after `sinceMs` (epoch-ms). Backs the rolling window
|
|
@@ -2718,9 +2908,9 @@ var SpendLedger = class {
|
|
|
2718
2908
|
totalSince(network, asset, sinceMs) {
|
|
2719
2909
|
let sum = 0n;
|
|
2720
2910
|
for (const r of this.records) {
|
|
2721
|
-
if (r.network
|
|
2722
|
-
|
|
2723
|
-
|
|
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);
|
|
2724
2914
|
}
|
|
2725
2915
|
return sum;
|
|
2726
2916
|
}
|
|
@@ -2740,6 +2930,14 @@ var SpendLedger = class {
|
|
|
2740
2930
|
totalBase: b.total
|
|
2741
2931
|
}));
|
|
2742
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
|
+
}
|
|
2743
2941
|
/** An immutable snapshot of all spend so far. */
|
|
2744
2942
|
summary() {
|
|
2745
2943
|
return {
|
|
@@ -2753,6 +2951,12 @@ var SpendLedger = class {
|
|
|
2753
2951
|
totalFormatted: formatUnits(b.total, b.decimals),
|
|
2754
2952
|
count: b.count
|
|
2755
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
|
+
})),
|
|
2756
2960
|
records: [...this.records]
|
|
2757
2961
|
};
|
|
2758
2962
|
}
|
|
@@ -2771,9 +2975,14 @@ var PipRailClient = class {
|
|
|
2771
2975
|
maxRetries;
|
|
2772
2976
|
retryTimeoutMs;
|
|
2773
2977
|
onEvent;
|
|
2774
|
-
// Per-asset tally of everything this client has paid
|
|
2775
|
-
//
|
|
2776
|
-
|
|
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.
|
|
2777
2986
|
// Resolved lazily on first request — this is what lets Solana (and future
|
|
2778
2987
|
// families) auto-mount with no setup call.
|
|
2779
2988
|
bound;
|
|
@@ -2782,8 +2991,15 @@ var PipRailClient = class {
|
|
|
2782
2991
|
this.maxRetries = Math.max(1, opts.maxPaymentRetries ?? 3);
|
|
2783
2992
|
this.retryTimeoutMs = opts.retryTimeoutMs ?? 3e4;
|
|
2784
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);
|
|
2785
3000
|
this.assertPolicyAmountCaps(opts.policy);
|
|
2786
3001
|
this.assertPolicyTimeOptions(opts.policy);
|
|
3002
|
+
this.assertPolicySpendControls(opts.policy);
|
|
2787
3003
|
}
|
|
2788
3004
|
/**
|
|
2789
3005
|
* Fail LOUDLY at construction on a malformed amount cap — a security boundary
|
|
@@ -2804,6 +3020,65 @@ var PipRailClient = class {
|
|
|
2804
3020
|
);
|
|
2805
3021
|
}
|
|
2806
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
|
+
}
|
|
2807
3082
|
}
|
|
2808
3083
|
/**
|
|
2809
3084
|
* Fail LOUDLY at construction on a misconfigured time policy — a security
|
|
@@ -2818,11 +3093,17 @@ var PipRailClient = class {
|
|
|
2818
3093
|
if (!policy) return;
|
|
2819
3094
|
const hasWindowTotal = policy.windowTotal !== void 0;
|
|
2820
3095
|
const hasWindowSeconds = policy.windowSeconds !== void 0;
|
|
2821
|
-
|
|
3096
|
+
const hasWindowCount = policy.maxPaymentsPerWindow !== void 0;
|
|
3097
|
+
if (hasWindowTotal && !hasWindowSeconds) {
|
|
2822
3098
|
throw new TypeError(
|
|
2823
3099
|
"policy.windowTotal and policy.windowSeconds must be set together \u2014 a rolling-window cap can't be half-armed (set both, or neither)."
|
|
2824
3100
|
);
|
|
2825
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
|
+
}
|
|
2826
3107
|
if (hasWindowSeconds && !(Number.isSafeInteger(policy.windowSeconds) && policy.windowSeconds > 0)) {
|
|
2827
3108
|
throw new TypeError("policy.windowSeconds must be a positive integer number of seconds.");
|
|
2828
3109
|
}
|
|
@@ -2834,6 +3115,14 @@ var PipRailClient = class {
|
|
|
2834
3115
|
);
|
|
2835
3116
|
}
|
|
2836
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
|
+
}
|
|
2837
3126
|
}
|
|
2838
3127
|
/** Emit an observability event, never letting a throwing handler break the
|
|
2839
3128
|
* payment flow (mirrors the server gate's `onPaid` isolation). */
|
|
@@ -2927,15 +3216,25 @@ var PipRailClient = class {
|
|
|
2927
3216
|
return { quote, cost };
|
|
2928
3217
|
}
|
|
2929
3218
|
/** Aggregated snapshot of every payment this client has settled — total
|
|
2930
|
-
* 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. */
|
|
2931
3221
|
spent() {
|
|
2932
3222
|
return this.ledger.summary();
|
|
2933
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
|
+
}
|
|
2934
3230
|
/**
|
|
2935
3231
|
* Read-only budget + time leash for a Mode-A (headless) agent — the policy IS
|
|
2936
3232
|
* the consent, and this is how the agent SEES what's left of it before paying.
|
|
2937
|
-
* Composes the
|
|
2938
|
-
*
|
|
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}).
|
|
2939
3238
|
*/
|
|
2940
3239
|
budget() {
|
|
2941
3240
|
const view = this.sessionView();
|
|
@@ -2946,9 +3245,54 @@ var PipRailClient = class {
|
|
|
2946
3245
|
expiresAt: view?.expiresAt != null ? new Date(view.expiresAt).toISOString() : null,
|
|
2947
3246
|
secondsRemaining: view?.secondsRemaining ?? null
|
|
2948
3247
|
},
|
|
2949
|
-
byAsset: this.remaining()
|
|
3248
|
+
byAsset: this.remaining(),
|
|
3249
|
+
byDenom: this.denomRemaining(),
|
|
3250
|
+
counts: this.countStatus()
|
|
2950
3251
|
};
|
|
2951
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
|
+
}
|
|
2952
3296
|
/**
|
|
2953
3297
|
* Per-(network, asset) remaining budget — ONE row per pair the ledger already
|
|
2954
3298
|
* holds (decimals are known only after the first spend), so a fresh client with
|
|
@@ -3366,7 +3710,7 @@ var PipRailClient = class {
|
|
|
3366
3710
|
const shortfall = {};
|
|
3367
3711
|
if (!quote.withinPolicy) {
|
|
3368
3712
|
blockers.push(
|
|
3369
|
-
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"
|
|
3370
3714
|
);
|
|
3371
3715
|
}
|
|
3372
3716
|
if (quote.symbolMismatch) warnings.push("SYMBOL_MISMATCH");
|
|
@@ -3445,6 +3789,11 @@ var PipRailClient = class {
|
|
|
3445
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.`
|
|
3446
3790
|
);
|
|
3447
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
|
+
}
|
|
3448
3797
|
const symbol = described?.symbol ?? accept.extra?.symbol;
|
|
3449
3798
|
const amountFormatted = formatUnits(amountBase, decimals);
|
|
3450
3799
|
const intent = {
|
|
@@ -3459,9 +3808,14 @@ var PipRailClient = class {
|
|
|
3459
3808
|
};
|
|
3460
3809
|
const policy = this.opts.policy;
|
|
3461
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;
|
|
3462
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;
|
|
3463
3817
|
const now = Date.now();
|
|
3464
|
-
const ctx =
|
|
3818
|
+
const ctx = needsCtx ? {
|
|
3465
3819
|
now,
|
|
3466
3820
|
sessionStart: this.ledger.sessionStart,
|
|
3467
3821
|
// Window slice ONLY when BOTH fields are set — never a `?? 0` width.
|
|
@@ -3469,7 +3823,12 @@ var PipRailClient = class {
|
|
|
3469
3823
|
accept.network,
|
|
3470
3824
|
accept.asset,
|
|
3471
3825
|
now - policy.windowSeconds * 1e3
|
|
3472
|
-
) : 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
|
|
3473
3832
|
} : void 0;
|
|
3474
3833
|
const decision = evaluatePolicy(
|
|
3475
3834
|
intent,
|
|
@@ -3505,9 +3864,11 @@ var PipRailClient = class {
|
|
|
3505
3864
|
async authorize(quote) {
|
|
3506
3865
|
if (!quote.withinPolicy) {
|
|
3507
3866
|
const reason = `Payment refused by policy: ${quote.policyReason ?? "not allowed"}`;
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3867
|
+
this.refuse(reason, {
|
|
3868
|
+
reasonCode: reasonCodeForPolicy(quote.policyCode),
|
|
3869
|
+
policyCode: quote.policyCode,
|
|
3870
|
+
quote
|
|
3871
|
+
});
|
|
3511
3872
|
}
|
|
3512
3873
|
const hook = this.opts.onBeforePay;
|
|
3513
3874
|
if (!hook) return;
|
|
@@ -3515,32 +3876,125 @@ var PipRailClient = class {
|
|
|
3515
3876
|
try {
|
|
3516
3877
|
approved = await hook(quote);
|
|
3517
3878
|
} catch (err) {
|
|
3518
|
-
|
|
3519
|
-
this.safeEmit({ kind: "payment-failed", reason, code: "APPROVAL" });
|
|
3520
|
-
throw new PaymentDeclinedError(reason, { cause: err, reasonCode: "APPROVAL" });
|
|
3879
|
+
this.refuse("onBeforePay threw \u2014 refusing to pay.", { reasonCode: "APPROVAL", quote, cause: err });
|
|
3521
3880
|
}
|
|
3522
3881
|
if (!approved) {
|
|
3523
3882
|
const reason = `onBeforePay declined ${quote.amountFormatted} ${quote.symbol ?? ""}`.trimEnd() + ` on ${quote.network}.`;
|
|
3524
|
-
this.
|
|
3525
|
-
throw new PaymentDeclinedError(reason, { reasonCode: "APPROVAL" });
|
|
3883
|
+
this.refuse(reason, { reasonCode: "APPROVAL", quote });
|
|
3526
3884
|
}
|
|
3527
3885
|
}
|
|
3528
|
-
/**
|
|
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. */
|
|
3529
3913
|
recordSpend(quote, ref) {
|
|
3530
|
-
this.
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
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
|
+
}
|
|
3544
3998
|
}
|
|
3545
3999
|
async payAndConfirm(net, wallet, accept) {
|
|
3546
4000
|
if (!this.supportsNetwork(net, accept.network)) {
|
|
@@ -3838,8 +4292,11 @@ function reasonCodeForPolicy(code) {
|
|
|
3838
4292
|
case "SESSION_EXPIRED":
|
|
3839
4293
|
return "SESSION_EXPIRED";
|
|
3840
4294
|
case "WINDOW_TOTAL":
|
|
4295
|
+
case "WINDOW_COUNT":
|
|
3841
4296
|
return "OUTSIDE_WINDOW";
|
|
3842
4297
|
case "MAX_TOTAL":
|
|
4298
|
+
case "MAX_TOTAL_DENOM":
|
|
4299
|
+
case "MAX_PAYMENTS":
|
|
3843
4300
|
return "BUDGET";
|
|
3844
4301
|
case void 0:
|
|
3845
4302
|
return void 0;
|
|
@@ -3897,6 +4354,10 @@ async function readInvalidReason(response) {
|
|
|
3897
4354
|
// src/payer.ts
|
|
3898
4355
|
var MultiChainPayer = class _MultiChainPayer {
|
|
3899
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;
|
|
3900
4361
|
/**
|
|
3901
4362
|
* Wrap an explicit, ordered set of single-chain clients — use this when a client
|
|
3902
4363
|
* needs full control (e.g. a custom EVM chain configured by a viem `Chain`). The
|
|
@@ -3904,12 +4365,18 @@ var MultiChainPayer = class _MultiChainPayer {
|
|
|
3904
4365
|
* at MOST one client per chain — two clients on the SAME network would double-count in
|
|
3905
4366
|
* `spent()`/`budget()` and waste a plan round-trip (`fromWallets` can't produce this).
|
|
3906
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.
|
|
3907
4373
|
*/
|
|
3908
|
-
constructor(clients) {
|
|
4374
|
+
constructor(clients, opts = {}) {
|
|
3909
4375
|
if (clients.length === 0) {
|
|
3910
4376
|
throw new TypeError("MultiChainPayer needs at least one PipRailClient.");
|
|
3911
4377
|
}
|
|
3912
4378
|
this._clients = [...clients];
|
|
4379
|
+
this._shared = opts.shared ?? false;
|
|
3913
4380
|
}
|
|
3914
4381
|
/**
|
|
3915
4382
|
* Build one client per funded chain from a `{ chain → wallet }` map — the
|
|
@@ -3924,20 +4391,26 @@ var MultiChainPayer = class _MultiChainPayer {
|
|
|
3924
4391
|
* solana: { key: process.env.SOLANA_SECRET! },
|
|
3925
4392
|
* xrpl: { key: process.env.XRPL_SEED! },
|
|
3926
4393
|
* },
|
|
3927
|
-
*
|
|
4394
|
+
* // ONE budget across all three chains: $20 total + at most 100 payments.
|
|
4395
|
+
* policy: { maxAmount: '1.00', maxTotalPerDenom: { USD: '20.00' }, maxPayments: 100 },
|
|
3928
4396
|
* })
|
|
3929
4397
|
* const res = await payer.get('https://api.example.com/paid') // pays on the first funded chain that can settle
|
|
3930
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.
|
|
3931
4402
|
*/
|
|
3932
4403
|
static fromWallets(opts) {
|
|
3933
4404
|
const entries = Object.entries(opts.wallets);
|
|
3934
4405
|
if (entries.length === 0) {
|
|
3935
4406
|
throw new TypeError("MultiChainPayer.fromWallets needs at least one wallet.");
|
|
3936
4407
|
}
|
|
4408
|
+
const ledger = new SpendLedger(opts.spendStore);
|
|
3937
4409
|
const clients = entries.map(
|
|
3938
4410
|
([chain, wallet]) => new PipRailClient({
|
|
3939
4411
|
chain,
|
|
3940
4412
|
wallet,
|
|
4413
|
+
ledger,
|
|
3941
4414
|
...opts.policy ? { policy: opts.policy } : {},
|
|
3942
4415
|
...opts.schemes ? { schemes: opts.schemes } : {},
|
|
3943
4416
|
...opts.rpcUrls?.[chain] ? { rpcUrl: opts.rpcUrls[chain] } : {},
|
|
@@ -3947,7 +4420,7 @@ var MultiChainPayer = class _MultiChainPayer {
|
|
|
3947
4420
|
...opts.retryTimeoutMs != null ? { retryTimeoutMs: opts.retryTimeoutMs } : {}
|
|
3948
4421
|
})
|
|
3949
4422
|
);
|
|
3950
|
-
return new _MultiChainPayer(clients);
|
|
4423
|
+
return new _MultiChainPayer(clients, { shared: true });
|
|
3951
4424
|
}
|
|
3952
4425
|
/** The underlying single-chain clients, in preference order. Reach for one of
|
|
3953
4426
|
* these for chain-specific reads (`estimateCost`, `discoverySigner`, per-chain
|
|
@@ -4041,28 +4514,50 @@ var MultiChainPayer = class _MultiChainPayer {
|
|
|
4041
4514
|
register(url, opts = {}) {
|
|
4042
4515
|
return this._clients[0].register(url, opts);
|
|
4043
4516
|
}
|
|
4044
|
-
/**
|
|
4045
|
-
*
|
|
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
|
+
*/
|
|
4046
4524
|
spent() {
|
|
4525
|
+
if (this._shared) return this._clients[0].spent();
|
|
4047
4526
|
const summaries = this._clients.map((c) => c.spent());
|
|
4048
4527
|
return {
|
|
4049
4528
|
count: summaries.reduce((n, s) => n + s.count, 0),
|
|
4050
4529
|
byAsset: summaries.flatMap((s) => s.byAsset),
|
|
4530
|
+
byDenom: summaries.flatMap((s) => s.byDenom),
|
|
4051
4531
|
records: summaries.flatMap((s) => s.records)
|
|
4052
4532
|
};
|
|
4053
4533
|
}
|
|
4054
|
-
/**
|
|
4055
|
-
*
|
|
4056
|
-
*
|
|
4057
|
-
*
|
|
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
|
+
*/
|
|
4058
4542
|
budget() {
|
|
4543
|
+
if (this._shared) return this._clients[0].budget();
|
|
4059
4544
|
const budgets = this._clients.map((c) => c.budget());
|
|
4060
4545
|
const session = budgets.map((b) => b.session).reduce((soonest, s) => {
|
|
4061
4546
|
if (soonest.secondsRemaining == null) return s;
|
|
4062
4547
|
if (s.secondsRemaining == null) return soonest;
|
|
4063
4548
|
return s.secondsRemaining < soonest.secondsRemaining ? s : soonest;
|
|
4064
4549
|
});
|
|
4065
|
-
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();
|
|
4066
4561
|
}
|
|
4067
4562
|
};
|
|
4068
4563
|
function isBodyInit(value) {
|
|
@@ -4167,9 +4662,12 @@ function explainDecline(err) {
|
|
|
4167
4662
|
}
|
|
4168
4663
|
function formatSpendReport(summary) {
|
|
4169
4664
|
if (summary.count === 0) return "No payments yet.";
|
|
4170
|
-
|
|
4665
|
+
const perAsset = summary.byAsset.map(
|
|
4171
4666
|
(a) => `${a.totalFormatted} ${a.symbol ?? a.asset} on ${a.network} (${a.count} payment${a.count === 1 ? "" : "s"})`
|
|
4172
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}`;
|
|
4173
4671
|
}
|
|
4174
4672
|
function describeChallenge(challenge) {
|
|
4175
4673
|
const first = challenge.accepts[0];
|
|
@@ -4248,9 +4746,11 @@ Branch on \`code\` (always reliable). Key cases:
|
|
|
4248
4746
|
once (enable the exact scheme); report it, don't retry the same call blindly.
|
|
4249
4747
|
|
|
4250
4748
|
## Knowing your leash \u2014 call piprail_budget
|
|
4251
|
-
piprail_budget tells you how much budget and time you have left
|
|
4252
|
-
|
|
4253
|
-
|
|
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.
|
|
4254
4754
|
|
|
4255
4755
|
## Two modes
|
|
4256
4756
|
- Mode A (headless, default): you run FREE inside a pre-set budget + time
|
|
@@ -4261,10 +4761,15 @@ Mode A to self-check before paying.
|
|
|
4261
4761
|
do NOT retry it as if it were a transient error.
|
|
4262
4762
|
|
|
4263
4763
|
## Hard facts
|
|
4264
|
-
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
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.
|
|
4268
4773
|
`;
|
|
4269
4774
|
function agentGuide() {
|
|
4270
4775
|
return PIPRAIL_AGENT_GUIDE;
|
|
@@ -4579,11 +5084,11 @@ function paymentTools(client) {
|
|
|
4579
5084
|
},
|
|
4580
5085
|
{
|
|
4581
5086
|
name: "piprail_budget",
|
|
4582
|
-
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.",
|
|
4583
5088
|
annotations: {
|
|
4584
5089
|
title: "Check remaining budget",
|
|
4585
5090
|
readOnlyHint: true,
|
|
4586
|
-
// reads the
|
|
5091
|
+
// reads the ledger + policy; never pays
|
|
4587
5092
|
idempotentHint: true
|
|
4588
5093
|
// a pure read
|
|
4589
5094
|
},
|
|
@@ -4596,7 +5101,13 @@ function paymentTools(client) {
|
|
|
4596
5101
|
return {
|
|
4597
5102
|
spent,
|
|
4598
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)
|
|
4599
5108
|
session: budget.session,
|
|
5109
|
+
policy: client.policy() ?? null,
|
|
5110
|
+
// the configured leash, read back
|
|
4600
5111
|
report: formatSpendReport(spent)
|
|
4601
5112
|
};
|
|
4602
5113
|
} catch (err) {
|
|
@@ -5901,8 +6412,10 @@ async function deliverReceipt(receipt, options) {
|
|
|
5901
6412
|
}
|
|
5902
6413
|
export {
|
|
5903
6414
|
BRAND,
|
|
6415
|
+
BUILTIN_DENOMS,
|
|
5904
6416
|
CHAINS,
|
|
5905
6417
|
ConfirmationTimeoutError,
|
|
6418
|
+
DENOM_PRECISION,
|
|
5906
6419
|
DIRECTORY_INFO,
|
|
5907
6420
|
EIP3009_TYPES,
|
|
5908
6421
|
EXACT_NETWORK_SLUGS,
|
|
@@ -5932,6 +6445,7 @@ export {
|
|
|
5932
6445
|
REGISTER_ATTRIBUTION,
|
|
5933
6446
|
RecipientNotReadyError,
|
|
5934
6447
|
SettlementError,
|
|
6448
|
+
SpendLedger,
|
|
5935
6449
|
UnknownTokenError,
|
|
5936
6450
|
UnsupportedNetworkError,
|
|
5937
6451
|
UnsupportedSchemeError,
|
|
@@ -5959,6 +6473,7 @@ export {
|
|
|
5959
6473
|
createPaymentGate,
|
|
5960
6474
|
decorateOutcome,
|
|
5961
6475
|
deliverReceipt,
|
|
6476
|
+
denomOf,
|
|
5962
6477
|
describeChallenge,
|
|
5963
6478
|
discoveryHeaders,
|
|
5964
6479
|
eip3009Abi,
|
|
@@ -5972,6 +6487,7 @@ export {
|
|
|
5972
6487
|
getDirectoryInfo,
|
|
5973
6488
|
isPermit2ProxyChain,
|
|
5974
6489
|
knownFacilitatorsFor,
|
|
6490
|
+
memorySpendStore,
|
|
5975
6491
|
normalizeNetwork,
|
|
5976
6492
|
parseChallenge,
|
|
5977
6493
|
parseExactPaymentHeader,
|