@elizaos/plugin-wallet 2.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +64 -0
- package/auto-enable.ts +76 -0
- package/dist/LpManagementService-BWrQ5-cO.mjs +353 -0
- package/dist/MockLpService-D_Apn4Fd.mjs +99 -0
- package/dist/aerodrome-CfnESC32.mjs +890 -0
- package/dist/chunk-hT5z_Zn9.mjs +35 -0
- package/dist/index.d.mts +34727 -0
- package/dist/index.mjs +21590 -0
- package/dist/lib/server-wallet-trade.d.mts +34 -0
- package/dist/lib/server-wallet-trade.mjs +306 -0
- package/dist/meteora-BPX39hZo.mjs +22640 -0
- package/dist/orca-Bybp1HXO.mjs +249 -0
- package/dist/pancakeswp-CkEXlXti.mjs +604 -0
- package/dist/plugin-ZO_MTyd0.mjs +529 -0
- package/dist/raydium-rfaM9yEf.mjs +539 -0
- package/dist/sdk/index.d.mts +32492 -0
- package/dist/sdk/index.mjs +6415 -0
- package/dist/types-D5252NZk.mjs +487 -0
- package/dist/uniswap-CReXgXVN.mjs +573 -0
- package/dist/wallet-action.d.mts +6 -0
- package/dist/wallet-action.mjs +820 -0
- package/package.json +152 -0
- package/src/actions/failure-codes.ts +79 -0
- package/src/actions/index.ts +1 -0
- package/src/analytics/birdeye/actions/wallet-search-address.ts +9 -0
- package/src/analytics/birdeye/birdeye-task.ts +175 -0
- package/src/analytics/birdeye/birdeye.ts +813 -0
- package/src/analytics/birdeye/constants.ts +74 -0
- package/src/analytics/birdeye/providers/agent-portfolio-provider.ts +18 -0
- package/src/analytics/birdeye/providers/market.ts +227 -0
- package/src/analytics/birdeye/providers/portfolio-factory.test.ts +138 -0
- package/src/analytics/birdeye/providers/portfolio-factory.ts +252 -0
- package/src/analytics/birdeye/providers/trending.ts +365 -0
- package/src/analytics/birdeye/providers/wallet.ts +14 -0
- package/src/analytics/birdeye/search-category.test.ts +207 -0
- package/src/analytics/birdeye/search-category.ts +506 -0
- package/src/analytics/birdeye/service.ts +992 -0
- package/src/analytics/birdeye/tasks/birdeye.ts +232 -0
- package/src/analytics/birdeye/types/api/common.ts +305 -0
- package/src/analytics/birdeye/types/api/defi.ts +220 -0
- package/src/analytics/birdeye/types/api/pair.ts +200 -0
- package/src/analytics/birdeye/types/api/search.ts +86 -0
- package/src/analytics/birdeye/types/api/token.ts +635 -0
- package/src/analytics/birdeye/types/api/trader.ts +76 -0
- package/src/analytics/birdeye/types/api/wallet.ts +181 -0
- package/src/analytics/birdeye/types/shared.ts +106 -0
- package/src/analytics/birdeye/utils.ts +700 -0
- package/src/analytics/dexscreener/errors.ts +28 -0
- package/src/analytics/dexscreener/index.ts +3 -0
- package/src/analytics/dexscreener/search-category.test.ts +49 -0
- package/src/analytics/dexscreener/search-category.ts +42 -0
- package/src/analytics/dexscreener/service.ts +595 -0
- package/src/analytics/dexscreener/types.ts +128 -0
- package/src/analytics/lpinfo/index.d.ts +7 -0
- package/src/analytics/lpinfo/index.ts +52 -0
- package/src/analytics/lpinfo/kamino/README.md +102 -0
- package/src/analytics/lpinfo/kamino/index.ts +24 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoLiquidityProvider.ts +422 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoPoolProvider.ts +365 -0
- package/src/analytics/lpinfo/kamino/providers/kaminoProvider.ts +496 -0
- package/src/analytics/lpinfo/kamino/services/kaminoLiquidityService.ts +1123 -0
- package/src/analytics/lpinfo/kamino/services/kaminoService.ts +758 -0
- package/src/analytics/lpinfo/steer/README.md +169 -0
- package/src/analytics/lpinfo/steer/index.ts +23 -0
- package/src/analytics/lpinfo/steer/providers/steerLiquidityProvider.ts +544 -0
- package/src/analytics/lpinfo/steer/services/steerLiquidityService.ts +1690 -0
- package/src/analytics/lpinfo/steer/steer-display-types.ts +99 -0
- package/src/analytics/news/index.ts +52 -0
- package/src/analytics/news/interfaces/types.ts +222 -0
- package/src/analytics/news/providers/defiNewsProvider.ts +734 -0
- package/src/analytics/news/services/newsDataService.ts +332 -0
- package/src/analytics/news/utils/formatters.ts +151 -0
- package/src/analytics/token-info/action.ts +240 -0
- package/src/analytics/token-info/index.ts +3 -0
- package/src/analytics/token-info/params.ts +215 -0
- package/src/analytics/token-info/providers.ts +681 -0
- package/src/analytics/token-info/service.ts +168 -0
- package/src/analytics/token-info/types.ts +74 -0
- package/src/audit/audit-log.ts +45 -0
- package/src/browser-shim/build-shim.ts +123 -0
- package/src/browser-shim/index.ts +5 -0
- package/src/browser-shim/shim.template.js +563 -0
- package/src/chains/evm/.github/workflows/npm-deploy.yml +112 -0
- package/src/chains/evm/LICENSE +21 -0
- package/src/chains/evm/README.md +106 -0
- package/src/chains/evm/actions/helpers.ts +147 -0
- package/src/chains/evm/actions/swap.ts +839 -0
- package/src/chains/evm/actions/transfer.ts +254 -0
- package/src/chains/evm/biome.json +61 -0
- package/src/chains/evm/bridge-router.ts +660 -0
- package/src/chains/evm/build.ts +89 -0
- package/src/chains/evm/chain-handler.ts +416 -0
- package/src/chains/evm/constants.ts +23 -0
- package/src/chains/evm/contracts/artifacts/OZGovernor.json +1707 -0
- package/src/chains/evm/contracts/artifacts/TimelockController.json +1007 -0
- package/src/chains/evm/contracts/artifacts/VoteToken.json +895 -0
- package/src/chains/evm/dex/aerodrome/index.ts +34 -0
- package/src/chains/evm/dex/aerodrome/services/AerodromeLpService.ts +558 -0
- package/src/chains/evm/dex/aerodrome/types.ts +318 -0
- package/src/chains/evm/dex/pancakeswp/index.ts +35 -0
- package/src/chains/evm/dex/pancakeswp/services/PancakeSwapV3LpService.ts +743 -0
- package/src/chains/evm/dex/pancakeswp/types.ts +65 -0
- package/src/chains/evm/dex/uniswap/index.ts +35 -0
- package/src/chains/evm/dex/uniswap/services/UniswapV3LpService.ts +759 -0
- package/src/chains/evm/dex/uniswap/types.ts +390 -0
- package/src/chains/evm/generated/specs/spec-helpers.ts +73 -0
- package/src/chains/evm/generated/specs/specs.ts +151 -0
- package/src/chains/evm/gov-router.ts +250 -0
- package/src/chains/evm/index.browser.ts +16 -0
- package/src/chains/evm/index.ts +31 -0
- package/src/chains/evm/prompts.ts +193 -0
- package/src/chains/evm/providers/get-balance.ts +123 -0
- package/src/chains/evm/providers/wallet.ts +715 -0
- package/src/chains/evm/routes/sign.ts +333 -0
- package/src/chains/evm/rpc-providers.ts +410 -0
- package/src/chains/evm/service.ts +140 -0
- package/src/chains/evm/templates/index.ts +10 -0
- package/src/chains/evm/types/index.ts +432 -0
- package/src/chains/evm/vitest.config.ts +18 -0
- package/src/chains/registry.ts +668 -0
- package/src/chains/solana/README.md +367 -0
- package/src/chains/wallet-action.ts +533 -0
- package/src/chains/wallet-router.test.ts +296 -0
- package/src/contracts.ts +65 -0
- package/src/core-augmentation.ts +10 -0
- package/src/index.ts +71 -0
- package/src/lib/server-wallet-trade.ts +192 -0
- package/src/lib/wallet-export-guard.ts +330 -0
- package/src/lp/actions/liquidity.ts +827 -0
- package/src/lp/e2e/real-token-tests.ts +428 -0
- package/src/lp/e2e/scenarios.ts +470 -0
- package/src/lp/e2e/test-utils.ts +145 -0
- package/src/lp/lp-manager-entry.ts +303 -0
- package/src/lp/services/ConcentratedLiquidityService.ts +120 -0
- package/src/lp/services/DexInteractionService.ts +226 -0
- package/src/lp/services/LpManagementService.test.ts +148 -0
- package/src/lp/services/LpManagementService.ts +632 -0
- package/src/lp/services/UserLpProfileService.ts +163 -0
- package/src/lp/services/VaultService.ts +153 -0
- package/src/lp/services/YieldOptimizationService.ts +344 -0
- package/src/lp/services/__tests__/MockLpService.ts +146 -0
- package/src/lp/tasks/LpAutoRebalanceTask.ts +117 -0
- package/src/lp/tasks/__tests__/LpAutoRebalanceTask.test.ts +370 -0
- package/src/lp/types.ts +582 -0
- package/src/lp/utils/solanaClient.ts +143 -0
- package/src/plugin.ts +125 -0
- package/src/policy/policy.ts +19 -0
- package/src/providers/canonical-provider.ts +27 -0
- package/src/providers/unified-wallet-provider.ts +79 -0
- package/src/register-routes.ts +11 -0
- package/src/routes/plugin.ts +47 -0
- package/src/routes/wallet-market-overview-route.ts +869 -0
- package/src/sdk/abi.ts +258 -0
- package/src/sdk/bridge/abis.ts +126 -0
- package/src/sdk/bridge/client.ts +518 -0
- package/src/sdk/bridge/index.ts +56 -0
- package/src/sdk/bridge/solana.ts +604 -0
- package/src/sdk/bridge/types.ts +202 -0
- package/src/sdk/convenience.ts +347 -0
- package/src/sdk/escrow/MutualStakeEscrow.ts +480 -0
- package/src/sdk/escrow/types.ts +64 -0
- package/src/sdk/escrow/verifiers.ts +73 -0
- package/src/sdk/identity/erc8004.ts +692 -0
- package/src/sdk/identity/reputation.ts +449 -0
- package/src/sdk/identity/uaid.ts +497 -0
- package/src/sdk/identity/validation.ts +372 -0
- package/src/sdk/index.ts +763 -0
- package/src/sdk/policy/SpendingPolicy.ts +260 -0
- package/src/sdk/policy/UptoBillingPolicy.ts +320 -0
- package/src/sdk/router/PaymentRouter.ts +215 -0
- package/src/sdk/router/index.ts +8 -0
- package/src/sdk/swap/SwapModule.ts +310 -0
- package/src/sdk/swap/abi.ts +117 -0
- package/src/sdk/swap/index.ts +34 -0
- package/src/sdk/swap/types.ts +135 -0
- package/src/sdk/tokens/decimals.ts +140 -0
- package/src/sdk/tokens/registry.ts +911 -0
- package/src/sdk/tokens/solana.ts +419 -0
- package/src/sdk/tokens/transfers.ts +327 -0
- package/src/sdk/types.ts +158 -0
- package/src/sdk/wallet-core.ts +115 -0
- package/src/sdk/x402/budget.ts +168 -0
- package/src/sdk/x402/chains/abstract/index.ts +280 -0
- package/src/sdk/x402/client.ts +320 -0
- package/src/sdk/x402/index.ts +46 -0
- package/src/sdk/x402/middleware.ts +92 -0
- package/src/sdk/x402/multi-asset.ts +144 -0
- package/src/sdk/x402/types.ts +156 -0
- package/src/services/wallet-backend-service.ts +328 -0
- package/src/types/wallet-router.ts +227 -0
- package/src/utils/intent-trajectory.ts +106 -0
- package/src/wallet/backend.ts +62 -0
- package/src/wallet/errors.ts +49 -0
- package/src/wallet/index.ts +27 -0
- package/src/wallet/local-eoa-backend.ts +201 -0
- package/src/wallet/pending.ts +60 -0
- package/src/wallet/select-backend.ts +47 -0
- package/src/wallet/steward-backend.ts +161 -0
- package/src/wallet-action.ts +1 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpendingPolicy — Programmable spending guardrails for AI agents.
|
|
3
|
+
*
|
|
4
|
+
* Beats PolicyLayer.com to market with:
|
|
5
|
+
* - MerchantAllowlist — allowlist-only merchant enforcement
|
|
6
|
+
* - RollingSpendCap — time-windowed spend limits
|
|
7
|
+
* - DraftThenApprove — human-in-the-loop for large transactions
|
|
8
|
+
* - AuditTrail — immutable local log of every payment attempt
|
|
9
|
+
* - FailClosed — policy errors always reject, never approve
|
|
10
|
+
*
|
|
11
|
+
* @module policy/SpendingPolicy
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** A payment the agent wants to make. */
|
|
17
|
+
export interface PaymentIntent {
|
|
18
|
+
/** Merchant contract address or domain (e.g. "0xABC..." or "api.example.com") */
|
|
19
|
+
merchant: string;
|
|
20
|
+
/** Amount in the smallest token unit (e.g. USDC micro-cents = 6 decimals). Use number for JS safety in tests; real on-chain code passes BigInt via adapter. */
|
|
21
|
+
amount: number;
|
|
22
|
+
/** ISO-8601 timestamp string. Defaults to now if omitted. */
|
|
23
|
+
timestamp?: string;
|
|
24
|
+
/** Optional human-readable label */
|
|
25
|
+
description?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type PolicyStatus = "approved" | "rejected" | "draft";
|
|
29
|
+
|
|
30
|
+
/** Result returned by SpendingPolicy.check(). */
|
|
31
|
+
export interface PolicyResult {
|
|
32
|
+
status: PolicyStatus;
|
|
33
|
+
reason?: string;
|
|
34
|
+
draftId?: string; // populated when status === 'draft'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A queued draft transaction awaiting approval. */
|
|
38
|
+
export interface DraftEntry {
|
|
39
|
+
draftId: string;
|
|
40
|
+
payment: PaymentIntent;
|
|
41
|
+
queuedAt: string; // ISO-8601
|
|
42
|
+
approved: boolean;
|
|
43
|
+
rejected: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Immutable record written to the audit log. */
|
|
47
|
+
export interface AuditEntry {
|
|
48
|
+
id: string;
|
|
49
|
+
timestamp: string;
|
|
50
|
+
merchant: string;
|
|
51
|
+
amount: number;
|
|
52
|
+
status: PolicyStatus;
|
|
53
|
+
reason?: string;
|
|
54
|
+
draftId?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Configuration passed to SpendingPolicy constructor. */
|
|
58
|
+
export interface SpendingPolicyConfig {
|
|
59
|
+
/**
|
|
60
|
+
* MerchantAllowlist: list of allowed contract addresses or domains.
|
|
61
|
+
* If provided and non-empty, only these merchants are allowed.
|
|
62
|
+
* Pass an empty array [] to disable allowlist enforcement (allow all).
|
|
63
|
+
*/
|
|
64
|
+
merchantAllowlist?: string[];
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* RollingSpendCap: maximum cumulative spend in a rolling time window.
|
|
68
|
+
* Set to undefined to disable.
|
|
69
|
+
*/
|
|
70
|
+
rollingCap?: {
|
|
71
|
+
/** Max amount (same units as PaymentIntent.amount) */
|
|
72
|
+
maxAmount: number;
|
|
73
|
+
/** Window size in milliseconds (e.g. 86_400_000 for 24 h) */
|
|
74
|
+
windowMs: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* DraftThenApprove: payments above this threshold are placed in draft status
|
|
79
|
+
* for human (or another agent) approval rather than executed immediately.
|
|
80
|
+
* Set to undefined to disable.
|
|
81
|
+
*/
|
|
82
|
+
draftThreshold?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
let _idCounter = 0;
|
|
88
|
+
function nextId(prefix: string): string {
|
|
89
|
+
return `${prefix}-${Date.now()}-${++_idCounter}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── SpendingPolicy ───────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export class SpendingPolicy {
|
|
95
|
+
private config: SpendingPolicyConfig;
|
|
96
|
+
private allowlist: Set<string>;
|
|
97
|
+
private spendWindow: Array<{ amount: number; ts: number }>; // rolling window entries
|
|
98
|
+
private auditLog: AuditEntry[];
|
|
99
|
+
private drafts: Map<string, DraftEntry>;
|
|
100
|
+
|
|
101
|
+
constructor(config: SpendingPolicyConfig) {
|
|
102
|
+
this.config = config;
|
|
103
|
+
this.allowlist = new Set(
|
|
104
|
+
(config.merchantAllowlist ?? []).map((m) => m.toLowerCase()),
|
|
105
|
+
);
|
|
106
|
+
this.spendWindow = [];
|
|
107
|
+
this.auditLog = [];
|
|
108
|
+
this.drafts = new Map();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Public Interface ───────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* FailClosed: wraps the actual check logic so that ANY unhandled error
|
|
115
|
+
* produces a rejection rather than inadvertently approving a payment.
|
|
116
|
+
*/
|
|
117
|
+
async check(payment: PaymentIntent): Promise<PolicyResult> {
|
|
118
|
+
try {
|
|
119
|
+
return await this._check(payment);
|
|
120
|
+
} catch (err: unknown) {
|
|
121
|
+
const reason = `Policy engine error (fail-closed): ${err instanceof Error ? err.message : String(err)}`;
|
|
122
|
+
const result: PolicyResult = { status: "rejected", reason };
|
|
123
|
+
await this.log(payment, result).catch(() => {
|
|
124
|
+
/* ignore log errors in fail-closed path */
|
|
125
|
+
});
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Write a payment attempt to the immutable audit log.
|
|
132
|
+
*/
|
|
133
|
+
async log(payment: PaymentIntent, result: PolicyResult): Promise<void> {
|
|
134
|
+
const entry: AuditEntry = {
|
|
135
|
+
id: nextId("audit"),
|
|
136
|
+
timestamp: payment.timestamp ?? new Date().toISOString(),
|
|
137
|
+
merchant: payment.merchant,
|
|
138
|
+
amount: payment.amount,
|
|
139
|
+
status: result.status,
|
|
140
|
+
reason: result.reason,
|
|
141
|
+
draftId: result.draftId,
|
|
142
|
+
};
|
|
143
|
+
this.auditLog.push(entry);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Return a copy of the current merchant allowlist. */
|
|
147
|
+
getMerchantAllowlist(): string[] {
|
|
148
|
+
return Array.from(this.allowlist);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Add a merchant address/domain to the allowlist at runtime. */
|
|
152
|
+
addMerchant(address: string): void {
|
|
153
|
+
this.allowlist.add(address.toLowerCase());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Return the full, immutable audit log (copy). */
|
|
157
|
+
getAuditLog(): AuditEntry[] {
|
|
158
|
+
return [...this.auditLog];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Draft queue management ─────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/** Approve a queued draft by its draftId. Returns false if not found. */
|
|
164
|
+
approveDraft(draftId: string): boolean {
|
|
165
|
+
const draft = this.drafts.get(draftId);
|
|
166
|
+
if (!draft) return false;
|
|
167
|
+
draft.approved = true;
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Reject a queued draft by its draftId. Returns false if not found. */
|
|
172
|
+
rejectDraft(draftId: string): boolean {
|
|
173
|
+
const draft = this.drafts.get(draftId);
|
|
174
|
+
if (!draft) return false;
|
|
175
|
+
draft.rejected = true;
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Return all pending (not yet approved or rejected) drafts. */
|
|
180
|
+
getPendingDrafts(): DraftEntry[] {
|
|
181
|
+
return Array.from(this.drafts.values()).filter(
|
|
182
|
+
(d) => !d.approved && !d.rejected,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Return all drafts. */
|
|
187
|
+
getAllDrafts(): DraftEntry[] {
|
|
188
|
+
return Array.from(this.drafts.values());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ─── Private Logic ──────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
private async _check(payment: PaymentIntent): Promise<PolicyResult> {
|
|
194
|
+
// 1. MerchantAllowlist check (only enforced when allowlist is non-empty)
|
|
195
|
+
if (this.allowlist.size > 0) {
|
|
196
|
+
const merchant = payment.merchant.toLowerCase();
|
|
197
|
+
if (!this.allowlist.has(merchant)) {
|
|
198
|
+
const result: PolicyResult = {
|
|
199
|
+
status: "rejected",
|
|
200
|
+
reason: `Merchant "${payment.merchant}" is not on the allowlist.`,
|
|
201
|
+
};
|
|
202
|
+
await this.log(payment, result);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 2. RollingSpendCap check
|
|
208
|
+
if (this.config.rollingCap) {
|
|
209
|
+
const { maxAmount, windowMs } = this.config.rollingCap;
|
|
210
|
+
const now = Date.now();
|
|
211
|
+
const windowStart = now - windowMs;
|
|
212
|
+
|
|
213
|
+
// Purge entries outside the window
|
|
214
|
+
this.spendWindow = this.spendWindow.filter((e) => e.ts >= windowStart);
|
|
215
|
+
|
|
216
|
+
const spent = this.spendWindow.reduce((sum, e) => sum + e.amount, 0);
|
|
217
|
+
if (spent + payment.amount > maxAmount) {
|
|
218
|
+
const result: PolicyResult = {
|
|
219
|
+
status: "rejected",
|
|
220
|
+
reason: `Rolling spend cap exceeded: spent ${spent}, cap ${maxAmount}, attempted ${payment.amount}.`,
|
|
221
|
+
};
|
|
222
|
+
await this.log(payment, result);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 3. DraftThenApprove check
|
|
228
|
+
if (
|
|
229
|
+
this.config.draftThreshold !== undefined &&
|
|
230
|
+
payment.amount >= this.config.draftThreshold
|
|
231
|
+
) {
|
|
232
|
+
const draftId = nextId("draft");
|
|
233
|
+
const draft: DraftEntry = {
|
|
234
|
+
draftId,
|
|
235
|
+
payment,
|
|
236
|
+
queuedAt: payment.timestamp ?? new Date().toISOString(),
|
|
237
|
+
approved: false,
|
|
238
|
+
rejected: false,
|
|
239
|
+
};
|
|
240
|
+
this.drafts.set(draftId, draft);
|
|
241
|
+
|
|
242
|
+
const result: PolicyResult = {
|
|
243
|
+
status: "draft",
|
|
244
|
+
reason: `Amount ${payment.amount} meets or exceeds draft threshold ${this.config.draftThreshold}. Awaiting approval.`,
|
|
245
|
+
draftId,
|
|
246
|
+
};
|
|
247
|
+
await this.log(payment, result);
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// 4. Approved — record spend in rolling window
|
|
252
|
+
if (this.config.rollingCap) {
|
|
253
|
+
this.spendWindow.push({ amount: payment.amount, ts: Date.now() });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const result: PolicyResult = { status: "approved" };
|
|
257
|
+
await this.log(payment, result);
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UptoBillingPolicy
|
|
3
|
+
*
|
|
4
|
+
* Local accounting for x402 "upto" flows where a buyer authorizes a maximum
|
|
5
|
+
* amount up front and the seller later settles only actual usage.
|
|
6
|
+
*
|
|
7
|
+
* The policy tracks three things:
|
|
8
|
+
* - the max amount authorized
|
|
9
|
+
* - the amount actually settled
|
|
10
|
+
* - ledger deltas for reservation, settlement, and release of unused capacity
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type UptoAuthorizationStatus =
|
|
14
|
+
| "authorized"
|
|
15
|
+
| "partially_settled"
|
|
16
|
+
| "settled"
|
|
17
|
+
| "released";
|
|
18
|
+
|
|
19
|
+
export interface UptoAuthorizationRequest {
|
|
20
|
+
authorizationId?: string;
|
|
21
|
+
service: string;
|
|
22
|
+
resource?: string;
|
|
23
|
+
network: string;
|
|
24
|
+
asset: string;
|
|
25
|
+
payTo: string;
|
|
26
|
+
maxAmount: bigint;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
28
|
+
authorizedAt?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface UptoSettlementOptions {
|
|
32
|
+
settledAt?: string;
|
|
33
|
+
finalize?: boolean;
|
|
34
|
+
txHash?: string;
|
|
35
|
+
reference?: string;
|
|
36
|
+
metadata?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UptoSettlementRecord {
|
|
40
|
+
settlementId: string;
|
|
41
|
+
authorizationId: string;
|
|
42
|
+
amount: bigint;
|
|
43
|
+
settledAt: string;
|
|
44
|
+
txHash?: string;
|
|
45
|
+
reference?: string;
|
|
46
|
+
metadata?: Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UptoAuthorizationRecord {
|
|
50
|
+
authorizationId: string;
|
|
51
|
+
service: string;
|
|
52
|
+
resource?: string;
|
|
53
|
+
network: string;
|
|
54
|
+
asset: string;
|
|
55
|
+
payTo: string;
|
|
56
|
+
maxAmount: bigint;
|
|
57
|
+
settledAmount: bigint;
|
|
58
|
+
releasedAmount: bigint;
|
|
59
|
+
remainingAmount: bigint;
|
|
60
|
+
authorizedAt: string;
|
|
61
|
+
finalizedAt?: string;
|
|
62
|
+
status: UptoAuthorizationStatus;
|
|
63
|
+
metadata?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface WalletLedgerDelta {
|
|
67
|
+
deltaId: string;
|
|
68
|
+
authorizationId: string;
|
|
69
|
+
type: "authorization" | "settlement" | "release";
|
|
70
|
+
timestamp: string;
|
|
71
|
+
reservedDelta: bigint;
|
|
72
|
+
settledDelta: bigint;
|
|
73
|
+
netWalletDelta: bigint;
|
|
74
|
+
txHash?: string;
|
|
75
|
+
reference?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface UptoBillingSnapshot {
|
|
79
|
+
authorization: UptoAuthorizationRecord;
|
|
80
|
+
settlements: UptoSettlementRecord[];
|
|
81
|
+
ledgerDeltas: WalletLedgerDelta[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let counter = 0;
|
|
85
|
+
function nextId(prefix: string): string {
|
|
86
|
+
counter += 1;
|
|
87
|
+
return `${prefix}-${Date.now()}-${counter}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function nowIso(value?: string): string {
|
|
91
|
+
return value ?? new Date().toISOString();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function cloneAuthorization(
|
|
95
|
+
record: UptoAuthorizationRecord,
|
|
96
|
+
): UptoAuthorizationRecord {
|
|
97
|
+
return {
|
|
98
|
+
...record,
|
|
99
|
+
metadata: record.metadata ? { ...record.metadata } : undefined,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function cloneSettlement(record: UptoSettlementRecord): UptoSettlementRecord {
|
|
104
|
+
return {
|
|
105
|
+
...record,
|
|
106
|
+
metadata: record.metadata ? { ...record.metadata } : undefined,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cloneDelta(record: WalletLedgerDelta): WalletLedgerDelta {
|
|
111
|
+
return { ...record };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export class UptoBillingPolicy {
|
|
115
|
+
private authorizations = new Map<string, UptoAuthorizationRecord>();
|
|
116
|
+
private settlements = new Map<string, UptoSettlementRecord[]>();
|
|
117
|
+
private ledger = new Map<string, WalletLedgerDelta[]>();
|
|
118
|
+
|
|
119
|
+
authorize(request: UptoAuthorizationRequest): UptoAuthorizationRecord {
|
|
120
|
+
if (request.maxAmount <= 0n) {
|
|
121
|
+
throw new Error("maxAmount must be greater than zero");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const authorizationId = request.authorizationId ?? nextId("upto-auth");
|
|
125
|
+
if (this.authorizations.has(authorizationId)) {
|
|
126
|
+
throw new Error(`Authorization already exists: ${authorizationId}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const record: UptoAuthorizationRecord = {
|
|
130
|
+
authorizationId,
|
|
131
|
+
service: request.service,
|
|
132
|
+
resource: request.resource,
|
|
133
|
+
network: request.network,
|
|
134
|
+
asset: request.asset,
|
|
135
|
+
payTo: request.payTo,
|
|
136
|
+
maxAmount: request.maxAmount,
|
|
137
|
+
settledAmount: 0n,
|
|
138
|
+
releasedAmount: 0n,
|
|
139
|
+
remainingAmount: request.maxAmount,
|
|
140
|
+
authorizedAt: nowIso(request.authorizedAt),
|
|
141
|
+
status: "authorized",
|
|
142
|
+
metadata: request.metadata ? { ...request.metadata } : undefined,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
this.authorizations.set(authorizationId, record);
|
|
146
|
+
this.settlements.set(authorizationId, []);
|
|
147
|
+
this.ledger.set(authorizationId, [
|
|
148
|
+
{
|
|
149
|
+
deltaId: nextId("upto-ledger"),
|
|
150
|
+
authorizationId,
|
|
151
|
+
type: "authorization",
|
|
152
|
+
timestamp: record.authorizedAt,
|
|
153
|
+
reservedDelta: record.maxAmount,
|
|
154
|
+
settledDelta: 0n,
|
|
155
|
+
netWalletDelta: 0n,
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
return cloneAuthorization(record);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
recordSettlement(
|
|
163
|
+
authorizationId: string,
|
|
164
|
+
amount: bigint,
|
|
165
|
+
options: UptoSettlementOptions = {},
|
|
166
|
+
): UptoBillingSnapshot {
|
|
167
|
+
const auth = this.requireAuthorization(authorizationId);
|
|
168
|
+
|
|
169
|
+
if (auth.status === "settled" || auth.status === "released") {
|
|
170
|
+
throw new Error(`Authorization is already finalized: ${authorizationId}`);
|
|
171
|
+
}
|
|
172
|
+
if (amount <= 0n) {
|
|
173
|
+
throw new Error("Settlement amount must be greater than zero");
|
|
174
|
+
}
|
|
175
|
+
if (amount > auth.remainingAmount) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Settlement amount ${amount} exceeds remaining authorized amount ${auth.remainingAmount}`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const settledAt = nowIso(options.settledAt);
|
|
182
|
+
const settlement: UptoSettlementRecord = {
|
|
183
|
+
settlementId: nextId("upto-settlement"),
|
|
184
|
+
authorizationId,
|
|
185
|
+
amount,
|
|
186
|
+
settledAt,
|
|
187
|
+
txHash: options.txHash,
|
|
188
|
+
reference: options.reference,
|
|
189
|
+
metadata: options.metadata ? { ...options.metadata } : undefined,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
auth.settledAmount += amount;
|
|
193
|
+
auth.remainingAmount -= amount;
|
|
194
|
+
auth.status = auth.remainingAmount === 0n ? "settled" : "partially_settled";
|
|
195
|
+
|
|
196
|
+
const settlementList = this.settlements.get(authorizationId);
|
|
197
|
+
if (!settlementList) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`[UptoBillingPolicy] Missing settlements for authorization ${authorizationId}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
settlementList.push(settlement);
|
|
203
|
+
|
|
204
|
+
const ledger = this.ledger.get(authorizationId);
|
|
205
|
+
if (!ledger) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`[UptoBillingPolicy] Missing ledger for authorization ${authorizationId}`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
ledger.push({
|
|
211
|
+
deltaId: nextId("upto-ledger"),
|
|
212
|
+
authorizationId,
|
|
213
|
+
type: "settlement",
|
|
214
|
+
timestamp: settledAt,
|
|
215
|
+
reservedDelta: -amount,
|
|
216
|
+
settledDelta: amount,
|
|
217
|
+
netWalletDelta: -amount,
|
|
218
|
+
txHash: options.txHash,
|
|
219
|
+
reference: options.reference,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (options.finalize) {
|
|
223
|
+
this.finalizeAuthorization(authorizationId, settledAt, options.reference);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return this.getSnapshot(authorizationId);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
finalizeAuthorization(
|
|
230
|
+
authorizationId: string,
|
|
231
|
+
finalizedAt?: string,
|
|
232
|
+
reference?: string,
|
|
233
|
+
): UptoAuthorizationRecord {
|
|
234
|
+
const auth = this.requireAuthorization(authorizationId);
|
|
235
|
+
const ledger = this.ledger.get(authorizationId);
|
|
236
|
+
if (!ledger) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`[UptoBillingPolicy] Missing ledger for authorization ${authorizationId}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const timestamp = nowIso(finalizedAt);
|
|
242
|
+
|
|
243
|
+
if (auth.status === "settled" || auth.status === "released") {
|
|
244
|
+
auth.finalizedAt = auth.finalizedAt ?? timestamp;
|
|
245
|
+
return cloneAuthorization(auth);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const released = auth.remainingAmount;
|
|
249
|
+
auth.releasedAmount += released;
|
|
250
|
+
auth.remainingAmount = 0n;
|
|
251
|
+
auth.finalizedAt = timestamp;
|
|
252
|
+
auth.status = auth.settledAmount > 0n ? "settled" : "released";
|
|
253
|
+
|
|
254
|
+
if (released > 0n) {
|
|
255
|
+
ledger.push({
|
|
256
|
+
deltaId: nextId("upto-ledger"),
|
|
257
|
+
authorizationId,
|
|
258
|
+
type: "release",
|
|
259
|
+
timestamp,
|
|
260
|
+
reservedDelta: -released,
|
|
261
|
+
settledDelta: 0n,
|
|
262
|
+
netWalletDelta: 0n,
|
|
263
|
+
reference,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return cloneAuthorization(auth);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
getAuthorization(authorizationId: string): UptoAuthorizationRecord | null {
|
|
271
|
+
const auth = this.authorizations.get(authorizationId);
|
|
272
|
+
return auth ? cloneAuthorization(auth) : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
listAuthorizations(): UptoAuthorizationRecord[] {
|
|
276
|
+
return Array.from(this.authorizations.values()).map(cloneAuthorization);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
getSettlements(authorizationId: string): UptoSettlementRecord[] {
|
|
280
|
+
return (this.settlements.get(authorizationId) ?? []).map(cloneSettlement);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
getWalletLedgerDeltas(authorizationId: string): WalletLedgerDelta[] {
|
|
284
|
+
return (this.ledger.get(authorizationId) ?? []).map(cloneDelta);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
getSnapshot(authorizationId: string): UptoBillingSnapshot {
|
|
288
|
+
return {
|
|
289
|
+
authorization: cloneAuthorization(
|
|
290
|
+
this.requireAuthorization(authorizationId),
|
|
291
|
+
),
|
|
292
|
+
settlements: this.getSettlements(authorizationId),
|
|
293
|
+
ledgerDeltas: this.getWalletLedgerDeltas(authorizationId),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
getNetWalletDelta(authorizationId: string): bigint {
|
|
298
|
+
return this.getWalletLedgerDeltas(authorizationId).reduce(
|
|
299
|
+
(sum, delta) => sum + delta.netWalletDelta,
|
|
300
|
+
0n,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
getReservedAmount(authorizationId: string): bigint {
|
|
305
|
+
return this.getWalletLedgerDeltas(authorizationId).reduce(
|
|
306
|
+
(sum, delta) => sum + delta.reservedDelta,
|
|
307
|
+
0n,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private requireAuthorization(
|
|
312
|
+
authorizationId: string,
|
|
313
|
+
): UptoAuthorizationRecord {
|
|
314
|
+
const auth = this.authorizations.get(authorizationId);
|
|
315
|
+
if (!auth) {
|
|
316
|
+
throw new Error(`Unknown authorization: ${authorizationId}`);
|
|
317
|
+
}
|
|
318
|
+
return auth;
|
|
319
|
+
}
|
|
320
|
+
}
|