@seedhape/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/acp.test.d.ts +2 -0
- package/dist/__tests__/acp.test.d.ts.map +1 -0
- package/dist/__tests__/acp.test.js +16 -0
- package/dist/__tests__/acp.test.js.map +1 -0
- package/dist/__tests__/ap2-official.test.d.ts +2 -0
- package/dist/__tests__/ap2-official.test.d.ts.map +1 -0
- package/dist/__tests__/ap2-official.test.js +49 -0
- package/dist/__tests__/ap2-official.test.js.map +1 -0
- package/dist/__tests__/mpp.test.d.ts +2 -0
- package/dist/__tests__/mpp.test.d.ts.map +1 -0
- package/dist/__tests__/mpp.test.js +42 -0
- package/dist/__tests__/mpp.test.js.map +1 -0
- package/dist/__tests__/outbound.test.d.ts +2 -0
- package/dist/__tests__/outbound.test.d.ts.map +1 -0
- package/dist/__tests__/outbound.test.js +23 -0
- package/dist/__tests__/outbound.test.js.map +1 -0
- package/dist/__tests__/protocols.test.d.ts +2 -0
- package/dist/__tests__/protocols.test.d.ts.map +1 -0
- package/dist/__tests__/protocols.test.js +16 -0
- package/dist/__tests__/protocols.test.js.map +1 -0
- package/dist/__tests__/providers.test.d.ts +2 -0
- package/dist/__tests__/providers.test.d.ts.map +1 -0
- package/dist/__tests__/providers.test.js +41 -0
- package/dist/__tests__/providers.test.js.map +1 -0
- package/dist/__tests__/reconciliation.test.d.ts +2 -0
- package/dist/__tests__/reconciliation.test.d.ts.map +1 -0
- package/dist/__tests__/reconciliation.test.js +35 -0
- package/dist/__tests__/reconciliation.test.js.map +1 -0
- package/dist/__tests__/spending-controls.test.d.ts +2 -0
- package/dist/__tests__/spending-controls.test.d.ts.map +1 -0
- package/dist/__tests__/spending-controls.test.js +47 -0
- package/dist/__tests__/spending-controls.test.js.map +1 -0
- package/dist/__tests__/tax-and-policy.test.d.ts +7 -0
- package/dist/__tests__/tax-and-policy.test.d.ts.map +1 -0
- package/dist/__tests__/tax-and-policy.test.js +389 -0
- package/dist/__tests__/tax-and-policy.test.js.map +1 -0
- package/dist/__tests__/web-bot-auth.test.d.ts +2 -0
- package/dist/__tests__/web-bot-auth.test.d.ts.map +1 -0
- package/dist/__tests__/web-bot-auth.test.js +33 -0
- package/dist/__tests__/web-bot-auth.test.js.map +1 -0
- package/dist/__tests__/x402-solana.test.d.ts +2 -0
- package/dist/__tests__/x402-solana.test.d.ts.map +1 -0
- package/dist/__tests__/x402-solana.test.js +155 -0
- package/dist/__tests__/x402-solana.test.js.map +1 -0
- package/dist/acp.d.ts +28 -0
- package/dist/acp.d.ts.map +1 -0
- package/dist/acp.js +49 -0
- package/dist/acp.js.map +1 -0
- package/dist/ap2/mandates.d.ts +319 -0
- package/dist/ap2/mandates.d.ts.map +1 -0
- package/dist/ap2/mandates.js +214 -0
- package/dist/ap2/mandates.js.map +1 -0
- package/dist/ap2/official.d.ts +156 -0
- package/dist/ap2/official.d.ts.map +1 -0
- package/dist/ap2/official.js +222 -0
- package/dist/ap2/official.js.map +1 -0
- package/dist/ap2/policy.d.ts +93 -0
- package/dist/ap2/policy.d.ts.map +1 -0
- package/dist/ap2/policy.js +216 -0
- package/dist/ap2/policy.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/india/fx.d.ts +59 -0
- package/dist/india/fx.d.ts.map +1 -0
- package/dist/india/fx.js +93 -0
- package/dist/india/fx.js.map +1 -0
- package/dist/india/vda-tax.d.ts +151 -0
- package/dist/india/vda-tax.d.ts.map +1 -0
- package/dist/india/vda-tax.js +226 -0
- package/dist/india/vda-tax.js.map +1 -0
- package/dist/mandate-builder.d.ts +102 -0
- package/dist/mandate-builder.d.ts.map +1 -0
- package/dist/mandate-builder.js +99 -0
- package/dist/mandate-builder.js.map +1 -0
- package/dist/mpp.d.ts +53 -0
- package/dist/mpp.d.ts.map +1 -0
- package/dist/mpp.js +164 -0
- package/dist/mpp.js.map +1 -0
- package/dist/orchestrator.d.ts +115 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +351 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/postgres-store.d.ts +60 -0
- package/dist/postgres-store.d.ts.map +1 -0
- package/dist/postgres-store.js +441 -0
- package/dist/postgres-store.js.map +1 -0
- package/dist/protocols.d.ts +18 -0
- package/dist/protocols.d.ts.map +1 -0
- package/dist/protocols.js +24 -0
- package/dist/protocols.js.map +1 -0
- package/dist/reconciliation.d.ts +27 -0
- package/dist/reconciliation.d.ts.map +1 -0
- package/dist/reconciliation.js +38 -0
- package/dist/reconciliation.js.map +1 -0
- package/dist/store.d.ts +110 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +369 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +195 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -0
- package/dist/util/canonical.d.ts +9 -0
- package/dist/util/canonical.d.ts.map +1 -0
- package/dist/util/canonical.js +35 -0
- package/dist/util/canonical.js.map +1 -0
- package/dist/wallet/provider.d.ts +118 -0
- package/dist/wallet/provider.d.ts.map +1 -0
- package/dist/wallet/provider.js +160 -0
- package/dist/wallet/provider.js.map +1 -0
- package/dist/wallet/providers.d.ts +107 -0
- package/dist/wallet/providers.d.ts.map +1 -0
- package/dist/wallet/providers.js +188 -0
- package/dist/wallet/providers.js.map +1 -0
- package/dist/web-bot-auth.d.ts +54 -0
- package/dist/web-bot-auth.d.ts.map +1 -0
- package/dist/web-bot-auth.js +112 -0
- package/dist/web-bot-auth.js.map +1 -0
- package/dist/x402/client.d.ts +101 -0
- package/dist/x402/client.d.ts.map +1 -0
- package/dist/x402/client.js +291 -0
- package/dist/x402/client.js.map +1 -0
- package/dist/x402/outbound.d.ts +15 -0
- package/dist/x402/outbound.d.ts.map +1 -0
- package/dist/x402/outbound.js +59 -0
- package/dist/x402/outbound.js.map +1 -0
- package/dist/x402/types.d.ts +80 -0
- package/dist/x402/types.d.ts.map +1 -0
- package/dist/x402/types.js +2 -0
- package/dist/x402/types.js.map +1 -0
- package/package.json +29 -0
package/dist/mpp.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MPP (Machine Payments Protocol) HTTP Authentication Scheme primitives.
|
|
3
|
+
*
|
|
4
|
+
* This module deliberately implements only the provider-neutral wire format.
|
|
5
|
+
* A parsed credential is untrusted input; a payment-method adapter must verify
|
|
6
|
+
* it, enforce the shared spending policy, and settle before fulfillment.
|
|
7
|
+
*/
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
function encode(value) {
|
|
11
|
+
return Buffer.from(encoder.encode(JSON.stringify(value))).toString("base64url");
|
|
12
|
+
}
|
|
13
|
+
function decode(value, label) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(decoder.decode(Buffer.from(value, "base64url")));
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw new Error(`Invalid MPP ${label} encoding.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function quote(value) {
|
|
23
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
24
|
+
}
|
|
25
|
+
function unquote(value) {
|
|
26
|
+
if (!value.startsWith('"') || !value.endsWith('"'))
|
|
27
|
+
throw new Error("Invalid MPP quoted value.");
|
|
28
|
+
return value.slice(1, -1).replaceAll('\\"', '"').replaceAll("\\\\", "\\");
|
|
29
|
+
}
|
|
30
|
+
function splitAuthParams(value) {
|
|
31
|
+
const parts = [];
|
|
32
|
+
let start = 0;
|
|
33
|
+
let quoted = false;
|
|
34
|
+
let escaped = false;
|
|
35
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
36
|
+
const character = value[index];
|
|
37
|
+
if (escaped) {
|
|
38
|
+
escaped = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (quoted && character === "\\") {
|
|
42
|
+
escaped = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (character === '"') {
|
|
46
|
+
quoted = !quoted;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (!quoted && character === ",") {
|
|
50
|
+
parts.push(value.slice(start, index).trim());
|
|
51
|
+
start = index + 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (quoted || escaped)
|
|
55
|
+
throw new Error("Invalid MPP authentication parameters.");
|
|
56
|
+
parts.push(value.slice(start).trim());
|
|
57
|
+
return parts.filter(Boolean);
|
|
58
|
+
}
|
|
59
|
+
/** Serialize a `WWW-Authenticate: Payment ...` challenge. */
|
|
60
|
+
export function formatMppChallenge(challenge) {
|
|
61
|
+
if (!challenge.id || !challenge.realm || !challenge.method || !challenge.intent) {
|
|
62
|
+
throw new Error("MPP challenge requires id, realm, method, and intent.");
|
|
63
|
+
}
|
|
64
|
+
const { id, realm, method, intent, request, expires, ...extensions } = challenge;
|
|
65
|
+
const params = [`id=${quote(id)}`, `realm=${quote(realm)}`, `method=${quote(method)}`, `intent=${quote(intent)}`, `request=${quote(encode(request))}`];
|
|
66
|
+
if (expires !== undefined)
|
|
67
|
+
params.push(`expires=${String(expires)}`);
|
|
68
|
+
for (const [key, value] of Object.entries(extensions)) {
|
|
69
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(key))
|
|
70
|
+
throw new Error(`Invalid MPP extension name: ${key}`);
|
|
71
|
+
if (typeof value !== "string")
|
|
72
|
+
throw new Error(`MPP extension ${key} must be a string.`);
|
|
73
|
+
params.push(`${key}=${quote(value)}`);
|
|
74
|
+
}
|
|
75
|
+
return `Payment ${params.join(", ")}`;
|
|
76
|
+
}
|
|
77
|
+
/** Parse a `WWW-Authenticate: Payment ...` challenge and reject duplicates. */
|
|
78
|
+
export function parseMppChallenge(header) {
|
|
79
|
+
const match = /^Payment\s+(.+)$/i.exec(header.trim());
|
|
80
|
+
if (!match)
|
|
81
|
+
throw new Error("Expected an MPP Payment challenge.");
|
|
82
|
+
const values = new Map();
|
|
83
|
+
for (const part of splitAuthParams(match[1])) {
|
|
84
|
+
const separator = part.indexOf("=");
|
|
85
|
+
if (separator <= 0)
|
|
86
|
+
throw new Error("Invalid MPP authentication parameter.");
|
|
87
|
+
const key = part.slice(0, separator).trim();
|
|
88
|
+
const raw = part.slice(separator + 1).trim();
|
|
89
|
+
if (values.has(key))
|
|
90
|
+
throw new Error(`Duplicate MPP authentication parameter: ${key}`);
|
|
91
|
+
values.set(key, raw.startsWith('"') ? unquote(raw) : raw);
|
|
92
|
+
}
|
|
93
|
+
const required = ["id", "realm", "method", "intent", "request"];
|
|
94
|
+
for (const key of required)
|
|
95
|
+
if (!values.has(key))
|
|
96
|
+
throw new Error(`MPP challenge is missing ${key}.`);
|
|
97
|
+
const expiresRaw = values.get("expires");
|
|
98
|
+
const expires = expiresRaw === undefined ? undefined : Number(expiresRaw);
|
|
99
|
+
if (expiresRaw !== undefined && !Number.isSafeInteger(expires))
|
|
100
|
+
throw new Error("MPP challenge expires must be an integer.");
|
|
101
|
+
const request = decode(values.get("request"), "challenge request");
|
|
102
|
+
if (!request || typeof request !== "object" || Array.isArray(request))
|
|
103
|
+
throw new Error("MPP challenge request must be an object.");
|
|
104
|
+
const challenge = { id: values.get("id"), realm: values.get("realm"), method: values.get("method"), intent: values.get("intent"), request };
|
|
105
|
+
if (expires !== undefined)
|
|
106
|
+
challenge.expires = expires;
|
|
107
|
+
for (const [key, value] of values)
|
|
108
|
+
if (!required.includes(key) && key !== "expires")
|
|
109
|
+
challenge[key] = value;
|
|
110
|
+
return challenge;
|
|
111
|
+
}
|
|
112
|
+
/** Serialize an MPP `Authorization: Payment <base64url>` credential. */
|
|
113
|
+
export function formatMppCredential(credential) {
|
|
114
|
+
if (!credential.challenge || !credential.payload)
|
|
115
|
+
throw new Error("MPP credential requires challenge and payload.");
|
|
116
|
+
return `Payment ${encode(credential)}`;
|
|
117
|
+
}
|
|
118
|
+
/** Parse an MPP `Authorization: Payment <base64url>` credential. */
|
|
119
|
+
export function parseMppCredential(header) {
|
|
120
|
+
const match = /^Payment\s+([^\s]+)$/i.exec(header.trim());
|
|
121
|
+
if (!match)
|
|
122
|
+
throw new Error("Expected an MPP Payment credential.");
|
|
123
|
+
const credential = decode(match[1], "credential");
|
|
124
|
+
if (!credential || typeof credential !== "object" || !credential.challenge || !credential.payload)
|
|
125
|
+
throw new Error("MPP credential must contain challenge and payload.");
|
|
126
|
+
return credential;
|
|
127
|
+
}
|
|
128
|
+
/** Serialize an MPP `Payment-Receipt` response header. */
|
|
129
|
+
export function formatMppReceipt(receipt) {
|
|
130
|
+
if (!receipt.status || !receipt.method || !receipt.reference || !Number.isSafeInteger(receipt.timestamp))
|
|
131
|
+
throw new Error("MPP receipt requires status, method, timestamp, and reference.");
|
|
132
|
+
return encode(receipt);
|
|
133
|
+
}
|
|
134
|
+
/** Parse an MPP `Payment-Receipt` response header. */
|
|
135
|
+
export function parseMppReceipt(header) {
|
|
136
|
+
const receipt = decode(header.trim(), "receipt");
|
|
137
|
+
if (!receipt || typeof receipt !== "object" || !receipt.status || !receipt.method || !receipt.reference || !Number.isSafeInteger(receipt.timestamp))
|
|
138
|
+
throw new Error("Invalid MPP receipt.");
|
|
139
|
+
return receipt;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Execute one MPP-protected request. Credential creation is injected so the
|
|
143
|
+
* caller can enforce its own user mandate, budget, and provider policy before
|
|
144
|
+
* producing a payment credential. The transport retries at most once after a
|
|
145
|
+
* valid 402 challenge and never retries an ambiguous response.
|
|
146
|
+
*/
|
|
147
|
+
export async function fetchWithMpp(input, init, createCredential, fetchImpl = fetch) {
|
|
148
|
+
const initial = await fetchImpl(input, init);
|
|
149
|
+
if (initial.status !== 402)
|
|
150
|
+
return { response: initial };
|
|
151
|
+
const challengeHeader = initial.headers.get("www-authenticate");
|
|
152
|
+
if (!challengeHeader)
|
|
153
|
+
throw new Error("MPP payment challenge is missing WWW-Authenticate.");
|
|
154
|
+
const challenge = parseMppChallenge(challengeHeader);
|
|
155
|
+
if (challenge.expires !== undefined && challenge.expires <= Math.floor(Date.now() / 1000))
|
|
156
|
+
throw new Error("MPP payment challenge has expired.");
|
|
157
|
+
const credential = await createCredential(challenge);
|
|
158
|
+
const headers = new Headers(init?.headers);
|
|
159
|
+
headers.set("authorization", formatMppCredential(credential));
|
|
160
|
+
const response = await fetchImpl(input, { ...init, headers });
|
|
161
|
+
const receiptHeader = response.headers.get("payment-receipt");
|
|
162
|
+
return { response, challenge, ...(receiptHeader ? { receipt: parseMppReceipt(receiptHeader) } : {}) };
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=mpp.js.map
|
package/dist/mpp.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mpp.js","sourceRoot":"","sources":["../src/mpp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The payment orchestrator. This is where the four subsystems meet:
|
|
3
|
+
* AP2 mandates, the policy engine, the x402 wire protocol, and the India
|
|
4
|
+
* tax ledger.
|
|
5
|
+
*
|
|
6
|
+
* Ordering matters and is deliberate:
|
|
7
|
+
*
|
|
8
|
+
* quote → build cart mandate → policy check → build payment mandate →
|
|
9
|
+
* sign → send → record → settle
|
|
10
|
+
*
|
|
11
|
+
* The policy check happens after the cart mandate exists but before any
|
|
12
|
+
* signature. That means a declined payment leaves a signed record of what was
|
|
13
|
+
* refused and why, with no signature that could be replayed. The audit trail
|
|
14
|
+
* covers refusals as well as spends, which is what makes it useful in a dispute.
|
|
15
|
+
*/
|
|
16
|
+
import { PolicyEngine } from "./ap2/policy.js";
|
|
17
|
+
import type { FxProvider } from "./india/fx.js";
|
|
18
|
+
import { type Ap2SignerConfig } from "./ap2/official.js";
|
|
19
|
+
import type { Store } from "./store.js";
|
|
20
|
+
import type { WalletProvider } from "./wallet/provider.js";
|
|
21
|
+
import type { ComposablePaymentProvider } from "./wallet/providers.js";
|
|
22
|
+
import type { OutboundRequestPolicy } from "./x402/outbound.js";
|
|
23
|
+
import type { Address, AssetRef, Hex, MandateHash, PolicyDecision, SpendRecord } from "./types.js";
|
|
24
|
+
export interface OrchestratorConfig {
|
|
25
|
+
userId: string;
|
|
26
|
+
store: Store;
|
|
27
|
+
wallet: WalletProvider;
|
|
28
|
+
policy: PolicyEngine;
|
|
29
|
+
fx: FxProvider;
|
|
30
|
+
asset: AssetRef;
|
|
31
|
+
absoluteMaxPerPayment: bigint;
|
|
32
|
+
outboundPolicy?: OutboundRequestPolicy;
|
|
33
|
+
paymentProviders?: readonly ComposablePaymentProvider[];
|
|
34
|
+
ap2?: {
|
|
35
|
+
loadSigner: () => Promise<Ap2SignerConfig>;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Called when a payment is legal under the mandate but flagged for review.
|
|
39
|
+
* Returning false aborts. A local MCP server can surface this to the human;
|
|
40
|
+
* a headless deployment should default to false, never true.
|
|
41
|
+
*/
|
|
42
|
+
confirmUnusual?: (decision: PolicyDecision, detail: {
|
|
43
|
+
host: string;
|
|
44
|
+
amount: bigint;
|
|
45
|
+
resource: string;
|
|
46
|
+
}) => Promise<boolean>;
|
|
47
|
+
}
|
|
48
|
+
export interface PayResult {
|
|
49
|
+
ok: boolean;
|
|
50
|
+
spendId: string;
|
|
51
|
+
status: SpendRecord["status"];
|
|
52
|
+
/** Body returned by the merchant once paid. */
|
|
53
|
+
body?: string;
|
|
54
|
+
contentType?: string;
|
|
55
|
+
amount?: bigint;
|
|
56
|
+
inrPaise?: bigint;
|
|
57
|
+
txHash?: Hex;
|
|
58
|
+
decision?: PolicyDecision;
|
|
59
|
+
error?: string;
|
|
60
|
+
}
|
|
61
|
+
export declare class Orchestrator {
|
|
62
|
+
private readonly cfg;
|
|
63
|
+
private readonly client;
|
|
64
|
+
constructor(cfg: OrchestratorConfig);
|
|
65
|
+
/** Price a resource without paying and without touching the ledger. */
|
|
66
|
+
quote(url: string): Promise<{
|
|
67
|
+
accepts: {
|
|
68
|
+
inrPaise: string;
|
|
69
|
+
scheme: "exact" | string;
|
|
70
|
+
network: string;
|
|
71
|
+
maxAmountRequired: string;
|
|
72
|
+
amount?: string;
|
|
73
|
+
resource: string;
|
|
74
|
+
description: string;
|
|
75
|
+
mimeType?: string;
|
|
76
|
+
payTo: string;
|
|
77
|
+
maxTimeoutSeconds: number;
|
|
78
|
+
asset: string;
|
|
79
|
+
extra?: {
|
|
80
|
+
name?: string;
|
|
81
|
+
version?: string;
|
|
82
|
+
decimals?: number;
|
|
83
|
+
assetTransferMethod?: "eip3009" | "erc7710" | "permit2" | "coinbase-spend-permission" | string;
|
|
84
|
+
facilitatorAddresses?: Address[];
|
|
85
|
+
feePayer?: string;
|
|
86
|
+
recentBlockhash?: string;
|
|
87
|
+
lastValidBlockHeight?: string;
|
|
88
|
+
memo?: string;
|
|
89
|
+
[key: string]: unknown;
|
|
90
|
+
};
|
|
91
|
+
outputSchema?: unknown;
|
|
92
|
+
}[];
|
|
93
|
+
x402Version: number;
|
|
94
|
+
resource?: {
|
|
95
|
+
url: string;
|
|
96
|
+
description?: string;
|
|
97
|
+
mimeType?: string;
|
|
98
|
+
[key: string]: unknown;
|
|
99
|
+
};
|
|
100
|
+
extensions?: Record<string, unknown>;
|
|
101
|
+
error?: string;
|
|
102
|
+
ap2CheckoutJwt?: string;
|
|
103
|
+
} | null>;
|
|
104
|
+
/**
|
|
105
|
+
* Fetch a resource under a specific intent mandate, paying if required.
|
|
106
|
+
*/
|
|
107
|
+
pay(url: string, intentHash: MandateHash, init?: RequestInit): Promise<PayResult>;
|
|
108
|
+
/**
|
|
109
|
+
* Consume cost-basis lots FIFO. Runs after settlement so a failed payment
|
|
110
|
+
* never disturbs the tax ledger.
|
|
111
|
+
*/
|
|
112
|
+
private recordDisposal;
|
|
113
|
+
private selectRequirement;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=orchestrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../src/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH,OAAO,EAAE,YAAY,EAAsB,MAAM,iBAAiB,CAAC;AAGnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAmE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC1H,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,KAAK,EACV,OAAO,EACP,QAAQ,EAER,GAAG,EACH,WAAW,EAEX,cAAc,EACd,WAAW,EACZ,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,YAAY,CAAC;IACrB,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,QAAQ,CAAC;IAChB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,CAAC,EAAE,qBAAqB,CAAC;IACvC,gBAAgB,CAAC,EAAE,SAAS,yBAAyB,EAAE,CAAC;IACxD,GAAG,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAA;KAAE,CAAC;IACrD;;;;OAIG;IACH,cAAc,CAAC,EAAE,CACf,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KACvD,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC9B,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,YAAY;IAGX,OAAO,CAAC,QAAQ,CAAC,GAAG;IAFhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAa;gBAEP,GAAG,EAAE,kBAAkB;IAYpD,uEAAuE;IACjE,KAAK,CAAC,GAAG,EAAE,MAAM;;;;;;;;;;;;;;oBA1Ec,CAAC;uBAAqB,CAAC;wBAC9C,CAAC;mCAAiC,CAAC;oCAEvC,CAAA;wBAAyB,CAAC;+BACpB,CAAC;oCACV,CAAC;oBAAkB,CAAC;;;;;;;;uBAGnB,CAAC;oBAAsB,CAAC;;;;;;;IAmFhC;;OAEG;IACG,GAAG,CACP,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,WAAW,EACvB,IAAI,GAAE,WAAgB,GACrB,OAAO,CAAC,SAAS,CAAC;IA8SrB;;;OAGG;YACW,cAAc;IAoB5B,OAAO,CAAC,iBAAiB;CAY1B"}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The payment orchestrator. This is where the four subsystems meet:
|
|
3
|
+
* AP2 mandates, the policy engine, the x402 wire protocol, and the India
|
|
4
|
+
* tax ledger.
|
|
5
|
+
*
|
|
6
|
+
* Ordering matters and is deliberate:
|
|
7
|
+
*
|
|
8
|
+
* quote → build cart mandate → policy check → build payment mandate →
|
|
9
|
+
* sign → send → record → settle
|
|
10
|
+
*
|
|
11
|
+
* The policy check happens after the cart mandate exists but before any
|
|
12
|
+
* signature. That means a declined payment leaves a signed record of what was
|
|
13
|
+
* refused and why, with no signature that could be replayed. The audit trail
|
|
14
|
+
* covers refusals as well as spends, which is what makes it useful in a dispute.
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from "node:crypto";
|
|
17
|
+
import { toHex } from "viem";
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
19
|
+
import { hashCartMandate, hashPaymentMandate, payloadHash, } from "./ap2/mandates.js";
|
|
20
|
+
import { X402Client, chainIdFor, PaymentDeclinedError } from "./x402/client.js";
|
|
21
|
+
import { toInrPaise } from "./india/fx.js";
|
|
22
|
+
import { consumeFifo, InsufficientLotsError } from "./india/vda-tax.js";
|
|
23
|
+
import { importAp2PublicKey, issueClosedAp2MandateBundle, verifyAp2SdJwt } from "./ap2/official.js";
|
|
24
|
+
export class Orchestrator {
|
|
25
|
+
cfg;
|
|
26
|
+
client;
|
|
27
|
+
constructor(cfg) {
|
|
28
|
+
this.cfg = cfg;
|
|
29
|
+
this.client = new X402Client({
|
|
30
|
+
signer: cfg.wallet,
|
|
31
|
+
allowedNetworks: [cfg.asset.network],
|
|
32
|
+
allowedAssets: [cfg.asset.address],
|
|
33
|
+
absoluteMaxPerPayment: cfg.absoluteMaxPerPayment,
|
|
34
|
+
outboundPolicy: cfg.outboundPolicy,
|
|
35
|
+
authorise: async () => ({ approved: true }), // gate lives in `pay`, below
|
|
36
|
+
paymentProviders: cfg.paymentProviders,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** Price a resource without paying and without touching the ledger. */
|
|
40
|
+
async quote(url) {
|
|
41
|
+
const challenge = await this.client.quote(url);
|
|
42
|
+
if (!challenge)
|
|
43
|
+
return null;
|
|
44
|
+
const fx = await this.cfg.fx.usdcInr();
|
|
45
|
+
return {
|
|
46
|
+
...challenge,
|
|
47
|
+
accepts: challenge.accepts.map((a) => ({
|
|
48
|
+
...a,
|
|
49
|
+
inrPaise: toInrPaise(BigInt(a.maxAmountRequired), this.cfg.asset.decimals, fx.rateScaled).toString(),
|
|
50
|
+
})),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Fetch a resource under a specific intent mandate, paying if required.
|
|
55
|
+
*/
|
|
56
|
+
async pay(url, intentHash, init = {}) {
|
|
57
|
+
if (await this.cfg.store.isSpendingPaused(this.cfg.userId)) {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
spendId: "",
|
|
61
|
+
status: "blocked",
|
|
62
|
+
error: "Spending is paused for this account. Resume spending before starting a new payment.",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const intent = await this.cfg.store.getIntent(intentHash);
|
|
66
|
+
if (!intent) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
spendId: "",
|
|
70
|
+
status: "blocked",
|
|
71
|
+
error: `No mandate found with hash ${intentHash}. List your mandates first.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const walletOwner = /^wallet:(0x[0-9a-fA-F]{40})$/.exec(this.cfg.userId)?.[1];
|
|
75
|
+
if (walletOwner && intent.mandate.subject.toLowerCase() !== walletOwner.toLowerCase()) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
spendId: "",
|
|
79
|
+
status: "blocked",
|
|
80
|
+
error: "This mandate does not belong to the authenticated connected wallet.",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const spendId = randomUUID();
|
|
84
|
+
const host = new URL(url).host;
|
|
85
|
+
const now = Math.floor(Date.now() / 1000);
|
|
86
|
+
// --- 1. Probe for the price ---
|
|
87
|
+
const challenge = await this.client.quote(url);
|
|
88
|
+
if (!challenge) {
|
|
89
|
+
// Not a paid resource. Pass through without touching the ledger.
|
|
90
|
+
const res = await fetch(url, init);
|
|
91
|
+
return {
|
|
92
|
+
ok: res.ok,
|
|
93
|
+
spendId,
|
|
94
|
+
status: "settled",
|
|
95
|
+
body: await res.text(),
|
|
96
|
+
contentType: res.headers.get("content-type") ?? undefined,
|
|
97
|
+
amount: 0n,
|
|
98
|
+
inrPaise: 0n,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const requirement = this.selectRequirement(challenge.accepts);
|
|
102
|
+
if (!requirement) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
spendId,
|
|
106
|
+
status: "blocked",
|
|
107
|
+
error: `${host} offers no payment option this wallet can use. It accepts: ` +
|
|
108
|
+
challenge.accepts.map((a) => `${a.scheme} on ${a.network}`).join("; ") +
|
|
109
|
+
`. This wallet pays ${this.cfg.asset.symbol} on ${this.cfg.asset.network}.`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// The standalone X402Client supports Solana signing. The AP2 orchestrator's
|
|
113
|
+
// EIP-712 cart/payment mandates remain EVM-specific until their schemas are
|
|
114
|
+
// migrated to chain-neutral account identifiers.
|
|
115
|
+
if (requirement.network.startsWith("solana:")) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
spendId,
|
|
119
|
+
status: "blocked",
|
|
120
|
+
error: "Solana x402 is supported by X402Client, but AP2-orchestrated Solana mandates are not enabled yet.",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const amount = BigInt(requirement.maxAmountRequired);
|
|
124
|
+
const fx = await this.cfg.fx.usdcInr();
|
|
125
|
+
const inrPaise = toInrPaise(amount, this.cfg.asset.decimals, fx.rateScaled);
|
|
126
|
+
// --- 2. Cart mandate: bind the merchant's quote to the intent ---
|
|
127
|
+
const chainId = chainIdFor(requirement.network);
|
|
128
|
+
const cart = {
|
|
129
|
+
intentHash,
|
|
130
|
+
payTo: requirement.payTo,
|
|
131
|
+
resource: requirement.resource || url,
|
|
132
|
+
description: requirement.description ?? "",
|
|
133
|
+
amount,
|
|
134
|
+
asset: this.cfg.asset,
|
|
135
|
+
quotedAt: now,
|
|
136
|
+
expiry: now + (requirement.maxTimeoutSeconds || 300),
|
|
137
|
+
nonce: toHex(randomBytes(32)),
|
|
138
|
+
chainId,
|
|
139
|
+
};
|
|
140
|
+
const cartHash = hashCartMandate(cart);
|
|
141
|
+
// --- 3. Policy. No signature exists yet at this point. ---
|
|
142
|
+
const ctx = {
|
|
143
|
+
now,
|
|
144
|
+
history: await this.cfg.store.spendsForIntent(intentHash, now - intent.mandate.periodSeconds),
|
|
145
|
+
medianPriceForResource: await this.cfg.store.medianPriceForResource(this.cfg.userId, cart.resource),
|
|
146
|
+
medianPriceOverall: await this.cfg.store.medianPriceOverall(this.cfg.userId),
|
|
147
|
+
knownHosts: await this.cfg.store.knownHosts(this.cfg.userId),
|
|
148
|
+
};
|
|
149
|
+
const decision = this.cfg.policy.evaluate(intent, {
|
|
150
|
+
resource: cart.resource,
|
|
151
|
+
host,
|
|
152
|
+
payTo: requirement.payTo,
|
|
153
|
+
amount,
|
|
154
|
+
assetAddress: requirement.asset,
|
|
155
|
+
network: requirement.network,
|
|
156
|
+
}, ctx);
|
|
157
|
+
const baseRecord = {
|
|
158
|
+
id: spendId,
|
|
159
|
+
userId: this.cfg.userId,
|
|
160
|
+
intentHash,
|
|
161
|
+
cartHash,
|
|
162
|
+
resource: cart.resource,
|
|
163
|
+
host,
|
|
164
|
+
payTo: requirement.payTo,
|
|
165
|
+
amount,
|
|
166
|
+
asset: this.cfg.asset,
|
|
167
|
+
inrPaise,
|
|
168
|
+
fxRateScaled: fx.rateScaled,
|
|
169
|
+
status: "blocked",
|
|
170
|
+
createdAt: now,
|
|
171
|
+
};
|
|
172
|
+
if (!decision.allowed) {
|
|
173
|
+
await this.cfg.store.putSpend({
|
|
174
|
+
...baseRecord,
|
|
175
|
+
blockReason: decision.reasons.join(","),
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
spendId,
|
|
180
|
+
status: "blocked",
|
|
181
|
+
decision,
|
|
182
|
+
error: decision.explanation,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
if (decision.requiresConfirmation) {
|
|
186
|
+
const confirmed = await this.cfg.confirmUnusual?.(decision, {
|
|
187
|
+
host,
|
|
188
|
+
amount,
|
|
189
|
+
resource: cart.resource,
|
|
190
|
+
});
|
|
191
|
+
if (!confirmed) {
|
|
192
|
+
await this.cfg.store.putSpend({
|
|
193
|
+
...baseRecord,
|
|
194
|
+
blockReason: "AWAITING_CONFIRMATION," + decision.reasons.join(","),
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
spendId,
|
|
199
|
+
status: "blocked",
|
|
200
|
+
decision,
|
|
201
|
+
error: decision.explanation +
|
|
202
|
+
" Confirm explicitly to proceed; this payment was not sent.",
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// --- 4. Check the on-chain permission actually covers this ---
|
|
207
|
+
const remaining = await this.cfg.wallet.remainingAllowance();
|
|
208
|
+
if (remaining !== null && remaining < amount) {
|
|
209
|
+
await this.cfg.store.putSpend({
|
|
210
|
+
...baseRecord,
|
|
211
|
+
blockReason: "ONCHAIN_ALLOWANCE_EXHAUSTED",
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
spendId,
|
|
216
|
+
status: "blocked",
|
|
217
|
+
error: `The on-chain spend permission has ${remaining} base units left this period, ` +
|
|
218
|
+
`which is less than the ${amount} required. Top up the permission or wait for the period to roll.`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const accountBudget = await this.cfg.store.getAccountBudget(this.cfg.userId);
|
|
222
|
+
if (accountBudget && accountBudget.currency.toUpperCase() !== this.cfg.asset.symbol.toUpperCase()) {
|
|
223
|
+
await this.cfg.store.putSpend({ ...baseRecord, blockReason: "ACCOUNT_BUDGET_CURRENCY_UNSUPPORTED" });
|
|
224
|
+
return { ok: false, spendId, status: "blocked", decision, error: `The account budget is denominated in ${accountBudget.currency}, but this payment is ${this.cfg.asset.symbol}. Add a compatible budget before paying.` };
|
|
225
|
+
}
|
|
226
|
+
const reservationStart = accountBudget?.periodStart ?? now - intent.mandate.periodSeconds;
|
|
227
|
+
const reservationLimit = accountBudget ? (accountBudget.amount < intent.mandate.maxPerPeriod ? accountBudget.amount : intent.mandate.maxPerPeriod) : intent.mandate.maxPerPeriod;
|
|
228
|
+
const reserved = await this.cfg.store.reserveSpend(this.cfg.userId, spendId, amount, reservationStart, reservationLimit, now);
|
|
229
|
+
if (!reserved) {
|
|
230
|
+
await this.cfg.store.putSpend({ ...baseRecord, blockReason: "PERIOD_VALUE_CAP_EXCEEDED" });
|
|
231
|
+
return { ok: false, spendId, status: "blocked", decision, error: "This payment would exceed the shared spending limit for the current period." };
|
|
232
|
+
}
|
|
233
|
+
let ap2Evidence;
|
|
234
|
+
if (intent.ap2) {
|
|
235
|
+
if (!challenge.ap2CheckoutJwt || !this.cfg.ap2) {
|
|
236
|
+
await this.cfg.store.putSpend({ ...baseRecord, blockReason: "AP2_CHECKOUT_MANDATE_UNAVAILABLE" });
|
|
237
|
+
return { ok: false, spendId, status: "blocked", error: "Merchant did not provide a signed AP2 Checkout JWT, or the AP2 issuer is not configured." };
|
|
238
|
+
}
|
|
239
|
+
const signer = await this.cfg.ap2.loadSigner();
|
|
240
|
+
const ap2PublicKey = await importAp2PublicKey(signer.publicJwk);
|
|
241
|
+
const openCheckoutPayload = await verifyAp2SdJwt(intent.ap2.checkout, ap2PublicKey, "mandate.checkout.open.1");
|
|
242
|
+
const openPaymentPayload = await verifyAp2SdJwt(intent.ap2.payment, ap2PublicKey, "mandate.payment.open.1");
|
|
243
|
+
const closed = await issueClosedAp2MandateBundle({
|
|
244
|
+
open: { checkout: { token: intent.ap2.checkout, issuerJwt: "", disclosures: [], payload: openCheckoutPayload }, payment: { token: intent.ap2.payment, issuerJwt: "", disclosures: [], payload: openPaymentPayload }, issuer: intent.ap2.issuer, issuedAt: intent.ap2.issuedAt, expiresAt: intent.ap2.expiresAt },
|
|
245
|
+
checkoutJwt: challenge.ap2CheckoutJwt, amount, decimals: this.cfg.asset.decimals, currency: this.cfg.asset.symbol,
|
|
246
|
+
payee: { id: requirement.payTo, name: requirement.extra?.name ?? host, website: `https://${host}`, payTo: requirement.payTo },
|
|
247
|
+
paymentInstrument: { id: "erc7710", type: "x402-erc7710", description: "Bound MetaMask delegated payment permission" },
|
|
248
|
+
issuer: signer.issuer, privateKey: signer.privateKey, publicJwk: signer.publicJwk, keyId: signer.keyId,
|
|
249
|
+
expiresAt: Math.min(intent.ap2.expiresAt, cart.expiry),
|
|
250
|
+
});
|
|
251
|
+
ap2Evidence = { checkoutMandate: closed.checkout.token, paymentMandate: closed.payment.token, checkoutJwt: challenge.ap2CheckoutJwt };
|
|
252
|
+
}
|
|
253
|
+
// --- 5. Sign and send ---
|
|
254
|
+
// Pause is checked again at the last local boundary. A pause committed
|
|
255
|
+
// while quote/policy work was in flight must prevent a new submission.
|
|
256
|
+
if (await this.cfg.store.isSpendingPaused(this.cfg.userId)) {
|
|
257
|
+
await this.cfg.store.putSpend({ ...baseRecord, blockReason: "SPENDING_PAUSED" });
|
|
258
|
+
return { ok: false, spendId, status: "blocked", decision, error: "Spending was paused before this payment could be sent." };
|
|
259
|
+
}
|
|
260
|
+
await this.cfg.store.putSpend({ ...baseRecord, status: "pending", ...(ap2Evidence ? { ap2: ap2Evidence } : {}) });
|
|
261
|
+
try {
|
|
262
|
+
const result = await this.client.fetch(url, init, ap2Evidence ? { ap2: ap2Evidence } : {});
|
|
263
|
+
const paymentMandate = {
|
|
264
|
+
intentHash,
|
|
265
|
+
cartHash,
|
|
266
|
+
payTo: requirement.payTo,
|
|
267
|
+
amount,
|
|
268
|
+
asset: this.cfg.asset,
|
|
269
|
+
payloadHash: payloadHash({ url, amount: amount.toString(), payTo: requirement.payTo }),
|
|
270
|
+
createdAt: now,
|
|
271
|
+
nonce: toHex(randomBytes(32)),
|
|
272
|
+
chainId,
|
|
273
|
+
};
|
|
274
|
+
const paymentHash = hashPaymentMandate(paymentMandate);
|
|
275
|
+
const settled = result.settlement?.success !== false && result.response.ok;
|
|
276
|
+
await this.cfg.store.updateSpend(spendId, {
|
|
277
|
+
paymentHash,
|
|
278
|
+
status: settled ? "settled" : "failed",
|
|
279
|
+
txHash: result.settlement?.transaction,
|
|
280
|
+
settledAt: Math.floor(Date.now() / 1000),
|
|
281
|
+
blockReason: settled ? undefined : result.settlement?.errorReason,
|
|
282
|
+
});
|
|
283
|
+
if (settled) {
|
|
284
|
+
await this.cfg.store.commitSpendReservation(spendId);
|
|
285
|
+
await this.recordDisposal(spendId, amount, inrPaise);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
await this.cfg.store.releaseSpendReservation(spendId);
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
ok: settled,
|
|
292
|
+
spendId,
|
|
293
|
+
status: settled ? "settled" : "failed",
|
|
294
|
+
body: await result.response.text(),
|
|
295
|
+
contentType: result.response.headers.get("content-type") ?? undefined,
|
|
296
|
+
amount,
|
|
297
|
+
inrPaise,
|
|
298
|
+
txHash: result.settlement?.transaction,
|
|
299
|
+
decision,
|
|
300
|
+
error: settled
|
|
301
|
+
? undefined
|
|
302
|
+
: (result.settlement?.errorReason ??
|
|
303
|
+
`Merchant returned ${result.response.status} after payment.`),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
const msg = e instanceof PaymentDeclinedError ? e.message : String(e);
|
|
308
|
+
if (e instanceof PaymentDeclinedError) {
|
|
309
|
+
await this.cfg.store.releaseSpendReservation(spendId);
|
|
310
|
+
}
|
|
311
|
+
await this.cfg.store.updateSpend(spendId, {
|
|
312
|
+
status: e instanceof PaymentDeclinedError ? "failed" : "pending",
|
|
313
|
+
blockReason: msg.slice(0, 500),
|
|
314
|
+
});
|
|
315
|
+
return { ok: false, spendId, status: e instanceof PaymentDeclinedError ? "failed" : "pending", error: e instanceof PaymentDeclinedError ? msg : `Payment outcome is uncertain and is being reconciled: ${msg}` };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Consume cost-basis lots FIFO. Runs after settlement so a failed payment
|
|
320
|
+
* never disturbs the tax ledger.
|
|
321
|
+
*/
|
|
322
|
+
async recordDisposal(spendId, amount, inrPaise) {
|
|
323
|
+
const lots = await this.cfg.store.listLots(this.cfg.userId);
|
|
324
|
+
try {
|
|
325
|
+
const disposal = consumeFifo(lots, amount, inrPaise);
|
|
326
|
+
for (const c of disposal.consumptions) {
|
|
327
|
+
const lot = lots.find((l) => l.id === c.lotId);
|
|
328
|
+
await this.cfg.store.updateLotRemaining(lot.id, lot.remaining);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (e) {
|
|
332
|
+
if (e instanceof InsufficientLotsError) {
|
|
333
|
+
// Do not fail the payment — it already settled on-chain. Surface the
|
|
334
|
+
// gap at report time instead, where the user can add a manual lot.
|
|
335
|
+
console.warn(`[tax] spend ${spendId}: cost basis short by ${e.shortfall} base units. ` +
|
|
336
|
+
`Year-end report will flag this.`);
|
|
337
|
+
}
|
|
338
|
+
else
|
|
339
|
+
throw e;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
selectRequirement(accepts) {
|
|
343
|
+
const viable = accepts.filter((a) => a.scheme === "exact" &&
|
|
344
|
+
a.network === this.cfg.asset.network &&
|
|
345
|
+
a.asset?.toLowerCase() === this.cfg.asset.address.toLowerCase());
|
|
346
|
+
if (viable.length === 0)
|
|
347
|
+
return null;
|
|
348
|
+
return viable.reduce((best, cur) => BigInt(cur.maxAmountRequired) < BigInt(best.maxAmountRequired) ? cur : best);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=orchestrator.js.map
|