@daski/pay 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -11
- package/dist/bin/daski.js +26 -11
- package/dist/bin/daski.js.map +1 -1
- package/dist/chain/reader.d.ts +92 -0
- package/dist/chain/reader.d.ts.map +1 -0
- package/dist/chain/reader.js +130 -0
- package/dist/chain/reader.js.map +1 -0
- package/dist/commands/approval.js +2 -2
- package/dist/commands/approval.js.map +1 -1
- package/dist/commands/buy.d.ts +0 -1
- package/dist/commands/buy.d.ts.map +1 -1
- package/dist/commands/buy.js +15 -22
- package/dist/commands/buy.js.map +1 -1
- package/dist/commands/confirmation.d.ts +322 -4
- package/dist/commands/confirmation.d.ts.map +1 -1
- package/dist/commands/confirmation.js +771 -60
- package/dist/commands/confirmation.js.map +1 -1
- package/dist/commands/doctor.d.ts +20 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +204 -59
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/order.d.ts +37 -6
- package/dist/commands/order.d.ts.map +1 -1
- package/dist/commands/order.js +181 -103
- package/dist/commands/order.js.map +1 -1
- package/dist/commands/signPayment.d.ts +2 -2
- package/dist/commands/signPayment.d.ts.map +1 -1
- package/dist/commands/signPayment.js +10 -5
- package/dist/commands/signPayment.js.map +1 -1
- package/dist/commands/wallet.d.ts +3 -0
- package/dist/commands/wallet.d.ts.map +1 -1
- package/dist/commands/wallet.js +72 -16
- package/dist/commands/wallet.js.map +1 -1
- package/dist/config.d.ts +10 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -1
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +28 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +72 -3
- package/dist/context.js.map +1 -1
- package/dist/gateway/client.d.ts +20 -17
- package/dist/gateway/client.d.ts.map +1 -1
- package/dist/gateway/client.js +62 -43
- package/dist/gateway/client.js.map +1 -1
- package/dist/gateway/lifecycle.d.ts +4 -1
- package/dist/gateway/lifecycle.d.ts.map +1 -1
- package/dist/gateway/lifecycle.js +10 -5
- package/dist/gateway/lifecycle.js.map +1 -1
- package/dist/gateway/metadata.d.ts +66 -0
- package/dist/gateway/metadata.d.ts.map +1 -0
- package/dist/gateway/metadata.js +152 -0
- package/dist/gateway/metadata.js.map +1 -0
- package/dist/gateway/purchase.d.ts +51 -67
- package/dist/gateway/purchase.d.ts.map +1 -1
- package/dist/gateway/purchase.js +153 -125
- package/dist/gateway/purchase.js.map +1 -1
- package/dist/host.d.ts +41 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +112 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/signers/circleAgent.d.ts +37 -0
- package/dist/signers/circleAgent.d.ts.map +1 -0
- package/dist/signers/circleAgent.js +244 -0
- package/dist/signers/circleAgent.js.map +1 -0
- package/dist/signers/contract.d.ts +34 -0
- package/dist/signers/contract.d.ts.map +1 -0
- package/dist/signers/contract.js +47 -0
- package/dist/signers/contract.js.map +1 -0
- package/dist/signers/index.d.ts +16 -7
- package/dist/signers/index.d.ts.map +1 -1
- package/dist/signers/index.js +7 -3
- package/dist/signers/index.js.map +1 -1
- package/dist/signers/local.d.ts +5 -1
- package/dist/signers/local.d.ts.map +1 -1
- package/dist/signers/local.js +16 -3
- package/dist/signers/local.js.map +1 -1
- package/dist/signers/selfTest.d.ts +32 -12
- package/dist/signers/selfTest.d.ts.map +1 -1
- package/dist/signers/selfTest.js +85 -12
- package/dist/signers/selfTest.js.map +1 -1
- package/dist/signers/signature.d.ts +10 -0
- package/dist/signers/signature.d.ts.map +1 -0
- package/dist/signers/signature.js +11 -0
- package/dist/signers/signature.js.map +1 -0
- package/dist/store/keystore.d.ts +48 -6
- package/dist/store/keystore.d.ts.map +1 -1
- package/dist/store/keystore.js +414 -110
- package/dist/store/keystore.js.map +1 -1
- package/dist/store/lock.d.ts +59 -0
- package/dist/store/lock.d.ts.map +1 -0
- package/dist/store/lock.js +269 -0
- package/dist/store/lock.js.map +1 -0
- package/dist/store/orders.d.ts +49 -9
- package/dist/store/orders.d.ts.map +1 -1
- package/dist/store/orders.js +173 -37
- package/dist/store/orders.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,58 +1,51 @@
|
|
|
1
|
+
/** Purchase authorization, submission, and exact-identifier reconciliation. */
|
|
1
2
|
import { type OrderBinding, type PolicyConfig, type SignerAdapter } from "@daski/x402-scheme";
|
|
2
3
|
import { type Address } from "viem";
|
|
3
4
|
import { CliError } from "../cli/errors.js";
|
|
4
|
-
import { updateOrder, type OrderRecord } from "../store/orders.js";
|
|
5
|
+
import { updateOrder, type OrderRecord, type OrderState } from "../store/orders.js";
|
|
5
6
|
import { GatewayClient, type McpToolResult, type PaymentChallenge, type PaymentRequirement, type PaymentSubmission } from "./client.js";
|
|
6
|
-
/** Prepares pricing and a draft without payment
|
|
7
|
+
/** Prepares pricing and a draft without payment; the only source of a challenge. */
|
|
7
8
|
export declare const CHALLENGE_TOOL = "daski_get_payment_challenge";
|
|
8
9
|
export declare const BUY_TOOL = "daski_buy_outcome";
|
|
9
10
|
export interface ChallengeResult {
|
|
10
11
|
challenge: PaymentChallenge;
|
|
11
12
|
requirement: PaymentRequirement;
|
|
12
13
|
binding: OrderBinding | undefined;
|
|
13
|
-
/** True when the spec-01 challenge tool served this. */
|
|
14
|
-
viaChallengeTool: boolean;
|
|
15
14
|
/** The prepare tool's balance and eligibility preflight, when it served one. */
|
|
16
15
|
preflight?: Record<string, unknown> | undefined;
|
|
17
16
|
}
|
|
18
|
-
/** A fresh reconciliation key. Also the idempotency key the gateway echoes. */
|
|
19
|
-
export declare function newIntentId(): string;
|
|
20
17
|
/** The identifier the gateway pinned in a challenge's `payment-identifier` extension, if any. */
|
|
21
18
|
export declare function issuedPaymentIdentifier(extensions: Record<string, unknown> | undefined): string | undefined;
|
|
22
19
|
/**
|
|
23
|
-
* The payment identifier a submission must carry
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `PAYMENT_IDENTIFIER_CONFLICT` before settlement (2026-09-03).
|
|
20
|
+
* The payment identifier a submission must carry: the one the gateway bound
|
|
21
|
+
* to the challenge, since the gateway looks a paid submission up by it and
|
|
22
|
+
* nothing else exists server-side. A challenge that carries none is refused,
|
|
23
|
+
* never given a minted identifier (0.1.1 minted one and every such
|
|
24
|
+
* submission was refused with `PAYMENT_IDENTIFIER_CONFLICT` before
|
|
25
|
+
* settlement, 2026-09-03); a challenge bound to a different identifier than
|
|
26
|
+
* the one a purchase recorded is refused too.
|
|
31
27
|
*/
|
|
32
|
-
export declare function resolvePaymentIdentifier(extensions: Record<string, unknown> | undefined, proposed: string | undefined): string
|
|
28
|
+
export declare function resolvePaymentIdentifier(extensions: Record<string, unknown> | undefined, proposed: string | undefined): string;
|
|
33
29
|
/**
|
|
34
30
|
* The reconciliation key for a purchase: the identifier the gateway bound to
|
|
35
31
|
* the challenge, which is also what the submission must carry, so the local
|
|
36
|
-
* ledger and the gateway's `daski_list_my_orders` filter agree.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* DASKI_PAYMENT_IDENTIFIER_MISMATCH: the gateway never accepted a proposal, so
|
|
40
|
-
* there is nothing to propose (2026-09-04).
|
|
32
|
+
* ledger and the gateway's `daski_list_my_orders` filter agree. The gateway
|
|
33
|
+
* never accepted a proposal, so there is nothing to propose; a challenge
|
|
34
|
+
* without an identifier is refused.
|
|
41
35
|
*/
|
|
42
36
|
export declare function challengeIntentId(extensions: Record<string, unknown> | undefined): string;
|
|
43
37
|
/**
|
|
44
38
|
* Whether a paid submission's error answer leaves the payment outcome unknown.
|
|
45
|
-
*
|
|
46
|
-
* the
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
39
|
+
* Only the gateway's own `paymentMayHaveSettled: false` says nothing settled;
|
|
40
|
+
* `true`, a refusal without the flag, and a body this CLI cannot read are all
|
|
41
|
+
* ambiguous, so the identifier is reconciled rather than signed for again. On
|
|
42
|
+
* 2026-09-04 a code list missed PAYMENT_IDENTIFIER_CONFLICT and an agent
|
|
43
|
+
* re-signed on the strength of a local ledger message.
|
|
50
44
|
*/
|
|
51
|
-
export declare function isAmbiguousPurchaseAnswer(
|
|
45
|
+
export declare function isAmbiguousPurchaseAnswer(body: Record<string, unknown> | undefined): boolean;
|
|
52
46
|
/**
|
|
53
|
-
* Step 1: obtain a payment challenge
|
|
54
|
-
*
|
|
55
|
-
* by a longer road.
|
|
47
|
+
* Step 1: obtain a payment challenge from `daski_get_payment_challenge`, the
|
|
48
|
+
* only tool that issues one. A gateway without it is unsupported.
|
|
56
49
|
*/
|
|
57
50
|
export declare function requestChallenge(options: {
|
|
58
51
|
client: GatewayClient;
|
|
@@ -88,50 +81,34 @@ export interface SubmitOptions {
|
|
|
88
81
|
outcomeId: string;
|
|
89
82
|
request: Record<string, unknown>;
|
|
90
83
|
submission: PaymentSubmission;
|
|
91
|
-
/** Pass the payload as a tool argument instead of `_meta`. */
|
|
92
|
-
legacyArg?: boolean;
|
|
93
84
|
timeoutMs?: number;
|
|
94
85
|
}
|
|
95
|
-
/**
|
|
96
|
-
* Step 5: the paid retry. The payload rides in `_meta["x402/payment"]` unless
|
|
97
|
-
* `--legacy-arg` asks for the argument form.
|
|
98
|
-
*/
|
|
86
|
+
/** Step 5: the paid retry. The payload rides in `_meta["x402/payment"]`. */
|
|
99
87
|
export declare function submitPayment(options: SubmitOptions): Promise<McpToolResult>;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
signer: SignerAdapter;
|
|
103
|
-
record: OrderRecord;
|
|
104
|
-
payer: Address;
|
|
105
|
-
chainId: number;
|
|
106
|
-
gatewayUrl: string;
|
|
107
|
-
submission: PaymentSubmission;
|
|
108
|
-
request: Record<string, unknown>;
|
|
109
|
-
legacyArg?: boolean;
|
|
110
|
-
}
|
|
111
|
-
export interface ReconcileOutcome {
|
|
112
|
-
/** `settled` when an order exists; `absent` when provably none does. */
|
|
113
|
-
status: "settled" | "absent";
|
|
114
|
-
orderHandle?: string | undefined;
|
|
115
|
-
body?: Record<string, unknown> | undefined;
|
|
116
|
-
/** How the conclusion was reached, for the audit log. */
|
|
117
|
-
evidence: string;
|
|
118
|
-
}
|
|
119
|
-
/** Compatibility wrapper around exact-identifier reconciliation. */
|
|
120
|
-
export declare function reconcileAmbiguousPurchase(options: ReconcileOptions): Promise<ReconcileOutcome>;
|
|
121
|
-
interface PayerOrderRow {
|
|
88
|
+
/** One row of the payer's order history, as `daski_list_my_orders` states it. */
|
|
89
|
+
export interface PayerOrderRow {
|
|
122
90
|
orderHandle: string;
|
|
91
|
+
/** The gateway's payment identifier: the buyer's ledger key. */
|
|
92
|
+
paymentIdentifier: string;
|
|
123
93
|
providerAgentId: string;
|
|
124
94
|
outcomeId: string;
|
|
95
|
+
/** Atomic USDC. */
|
|
125
96
|
grossAmount: string;
|
|
97
|
+
/** One of the gateway's order states. */
|
|
126
98
|
state: string;
|
|
127
|
-
createdAt
|
|
128
|
-
/** Present once the gateway echoes the buyer's idempotency key. */
|
|
129
|
-
paymentIdentifier?: string;
|
|
99
|
+
createdAt?: string | undefined;
|
|
130
100
|
}
|
|
131
101
|
/**
|
|
132
|
-
* The
|
|
133
|
-
*
|
|
134
|
-
*
|
|
102
|
+
* The rows of a `daski_list_my_orders` answer. A row this CLI cannot read is
|
|
103
|
+
* an error, never a guess: a missing identifier used to be matched on other
|
|
104
|
+
* invariants and a missing amount recorded as zero, both of which put
|
|
105
|
+
* something in the ledger the gateway never said.
|
|
106
|
+
*/
|
|
107
|
+
export declare function readPayerOrderRows(body: Record<string, unknown>): PayerOrderRow[];
|
|
108
|
+
/**
|
|
109
|
+
* The payer's order history, filtered by the payment identifier when one is
|
|
110
|
+
* given: the gateway filters server-side, and the rows are narrowed again
|
|
111
|
+
* here so a row for another identifier can never be read as this one's.
|
|
135
112
|
*/
|
|
136
113
|
export declare function listPayerOrders(options: {
|
|
137
114
|
client: GatewayClient;
|
|
@@ -144,10 +121,18 @@ export declare function listPayerOrders(options: {
|
|
|
144
121
|
/** What a gateway order state says about the money. */
|
|
145
122
|
export type SettlementReading = "not_settled" | "in_flight" | "ambiguous" | "settled";
|
|
146
123
|
/**
|
|
147
|
-
* The
|
|
148
|
-
*
|
|
124
|
+
* The money question. An unknown state is ambiguous, never absent: nothing
|
|
125
|
+
* is signed again on the strength of a state this CLI does not understand.
|
|
149
126
|
*/
|
|
150
127
|
export declare function readSettlement(state: string): SettlementReading;
|
|
128
|
+
/** A gateway order state this release does not understand; the ledger records nothing for it. */
|
|
129
|
+
export declare function orderStateUnreadable(state: unknown, orderHandle?: string | undefined): CliError;
|
|
130
|
+
/**
|
|
131
|
+
* The ledger question: the local state a gateway order state records as. An
|
|
132
|
+
* unknown state is refused, never recorded as SUBMITTED; the caller decides
|
|
133
|
+
* what else it has already learned (a handle, a receipt) and keeps that.
|
|
134
|
+
*/
|
|
135
|
+
export declare function localOrderState(state: unknown, orderHandle?: string | undefined): OrderState;
|
|
151
136
|
export interface IdentifierReconciliation {
|
|
152
137
|
status: "settled" | "in_flight" | "ambiguous" | "absent";
|
|
153
138
|
orderHandle?: string | undefined;
|
|
@@ -159,8 +144,7 @@ export interface IdentifierReconciliation {
|
|
|
159
144
|
* but the read authorization: no replay, no balance reading, no local
|
|
160
145
|
* ledger. This is the lookup buy.md names for every ambiguous outcome and
|
|
161
146
|
* for PAYMENT_IDENTIFIER_CONFLICT. An identifier the gateway never issued
|
|
162
|
-
*
|
|
163
|
-
* nothing could have settled under it.
|
|
147
|
+
* matches nothing, which is the truth: nothing could have settled under it.
|
|
164
148
|
*/
|
|
165
149
|
export declare function reconcileIdentifierRows(intentId: string, rows: readonly PayerOrderRow[]): IdentifierReconciliation;
|
|
166
150
|
export declare function reconcileByIdentifier(options: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"purchase.d.ts","sourceRoot":"","sources":["../../src/gateway/purchase.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"purchase.d.ts","sourceRoot":"","sources":["../../src/gateway/purchase.ts"],"names":[],"mappings":"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"}
|
package/dist/gateway/purchase.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/** Purchase authorization, submission, and exact-identifier reconciliation. */
|
|
2
|
-
import { randomBytes } from "node:crypto";
|
|
3
2
|
import { bindingFromExtensions, formatUsdc, paymentEchoExtensions, transferWithAuthorizationTypedData, validatePurchaseAuthorization, withPaymentIdentifier, } from "@daski/x402-scheme";
|
|
4
3
|
import { getAddress } from "viem";
|
|
5
4
|
import { CliError } from "../cli/errors.js";
|
|
6
5
|
import { findByIntent, updateOrder, upsertOrder, } from "../store/orders.js";
|
|
7
|
-
import { describeResult, GatewayClient, unreadableResultError, } from "./client.js";
|
|
6
|
+
import { describeResult, GatewayClient, gatewayRefusalRemediation, gatewayUnsupported, isRetryableGatewayCode, unreadableResultError, } from "./client.js";
|
|
8
7
|
import { callWalletQuery } from "./lifecycle.js";
|
|
9
|
-
/** Prepares pricing and a draft without payment
|
|
8
|
+
/** Prepares pricing and a draft without payment; the only source of a challenge. */
|
|
10
9
|
export const CHALLENGE_TOOL = "daski_get_payment_challenge";
|
|
11
10
|
export const BUY_TOOL = "daski_buy_outcome";
|
|
12
|
-
/** A
|
|
13
|
-
|
|
14
|
-
return
|
|
11
|
+
/** A challenge without the gateway's identifier cannot be paid: the gateway looks a submission up by it. */
|
|
12
|
+
function paymentIdentifierMissing() {
|
|
13
|
+
return new CliError({
|
|
14
|
+
code: "DASKI_PAYMENT_IDENTIFIER_MISSING",
|
|
15
|
+
message: "The challenge carries no payment-identifier extension, so there is no identifier the " +
|
|
16
|
+
"gateway would look the paid submission up by.",
|
|
17
|
+
remediation: "Do not sign: a submission under an identifier the gateway did not issue is refused before " +
|
|
18
|
+
"settlement. Request a fresh challenge from daski_get_payment_challenge; every challenge a " +
|
|
19
|
+
"supported gateway issues carries payment-identifier.info.id.",
|
|
20
|
+
});
|
|
15
21
|
}
|
|
16
22
|
/** The identifier the gateway pinned in a challenge's `payment-identifier` extension, if any. */
|
|
17
23
|
export function issuedPaymentIdentifier(extensions) {
|
|
@@ -20,18 +26,19 @@ export function issuedPaymentIdentifier(extensions) {
|
|
|
20
26
|
return typeof id === "string" && id.length > 0 ? id : undefined;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
|
-
* The payment identifier a submission must carry
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `PAYMENT_IDENTIFIER_CONFLICT` before settlement (2026-09-03).
|
|
29
|
+
* The payment identifier a submission must carry: the one the gateway bound
|
|
30
|
+
* to the challenge, since the gateway looks a paid submission up by it and
|
|
31
|
+
* nothing else exists server-side. A challenge that carries none is refused,
|
|
32
|
+
* never given a minted identifier (0.1.1 minted one and every such
|
|
33
|
+
* submission was refused with `PAYMENT_IDENTIFIER_CONFLICT` before
|
|
34
|
+
* settlement, 2026-09-03); a challenge bound to a different identifier than
|
|
35
|
+
* the one a purchase recorded is refused too.
|
|
31
36
|
*/
|
|
32
37
|
export function resolvePaymentIdentifier(extensions, proposed) {
|
|
33
38
|
const issued = issuedPaymentIdentifier(extensions);
|
|
34
|
-
if (issued
|
|
39
|
+
if (issued === undefined)
|
|
40
|
+
throw paymentIdentifierMissing();
|
|
41
|
+
if (proposed !== undefined && issued !== proposed) {
|
|
35
42
|
throw new CliError({
|
|
36
43
|
code: "DASKI_PAYMENT_IDENTIFIER_MISMATCH",
|
|
37
44
|
message: `The gateway bound this challenge to payment identifier ${issued}, not to ` +
|
|
@@ -40,60 +47,44 @@ export function resolvePaymentIdentifier(extensions, proposed) {
|
|
|
40
47
|
"is refused by the gateway. Request a fresh challenge and retry.",
|
|
41
48
|
});
|
|
42
49
|
}
|
|
43
|
-
return issued
|
|
50
|
+
return issued;
|
|
44
51
|
}
|
|
45
52
|
/**
|
|
46
53
|
* The reconciliation key for a purchase: the identifier the gateway bound to
|
|
47
54
|
* the challenge, which is also what the submission must carry, so the local
|
|
48
|
-
* ledger and the gateway's `daski_list_my_orders` filter agree.
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* DASKI_PAYMENT_IDENTIFIER_MISMATCH: the gateway never accepted a proposal, so
|
|
52
|
-
* there is nothing to propose (2026-09-04).
|
|
55
|
+
* ledger and the gateway's `daski_list_my_orders` filter agree. The gateway
|
|
56
|
+
* never accepted a proposal, so there is nothing to propose; a challenge
|
|
57
|
+
* without an identifier is refused.
|
|
53
58
|
*/
|
|
54
59
|
export function challengeIntentId(extensions) {
|
|
55
|
-
return
|
|
60
|
+
return resolvePaymentIdentifier(extensions, undefined);
|
|
56
61
|
}
|
|
57
|
-
/** Gateway codes that mean "authorized, outcome unknown" on older gateways. */
|
|
58
|
-
const AMBIGUOUS_CODES = new Set([
|
|
59
|
-
"PAYMENT_PENDING_RECONCILIATION",
|
|
60
|
-
"PAYMENT_OUTCOME_PENDING",
|
|
61
|
-
]);
|
|
62
62
|
/**
|
|
63
63
|
* Whether a paid submission's error answer leaves the payment outcome unknown.
|
|
64
|
-
*
|
|
65
|
-
* the
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
64
|
+
* Only the gateway's own `paymentMayHaveSettled: false` says nothing settled;
|
|
65
|
+
* `true`, a refusal without the flag, and a body this CLI cannot read are all
|
|
66
|
+
* ambiguous, so the identifier is reconciled rather than signed for again. On
|
|
67
|
+
* 2026-09-04 a code list missed PAYMENT_IDENTIFIER_CONFLICT and an agent
|
|
68
|
+
* re-signed on the strength of a local ledger message.
|
|
69
69
|
*/
|
|
70
|
-
export function isAmbiguousPurchaseAnswer(
|
|
71
|
-
|
|
72
|
-
return true;
|
|
73
|
-
if (body?.paymentMayHaveSettled === false)
|
|
74
|
-
return false;
|
|
75
|
-
return code !== undefined && AMBIGUOUS_CODES.has(code);
|
|
70
|
+
export function isAmbiguousPurchaseAnswer(body) {
|
|
71
|
+
return body?.paymentMayHaveSettled !== false;
|
|
76
72
|
}
|
|
77
73
|
/**
|
|
78
|
-
* Step 1: obtain a payment challenge
|
|
79
|
-
*
|
|
80
|
-
* by a longer road.
|
|
74
|
+
* Step 1: obtain a payment challenge from `daski_get_payment_challenge`, the
|
|
75
|
+
* only tool that issues one. A gateway without it is unsupported.
|
|
81
76
|
*/
|
|
82
77
|
export async function requestChallenge(options) {
|
|
83
78
|
const { client, providerAgentId, outcomeId, request, payerAddress } = options;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
providerAgentId, outcomeId, request, payerAddress,
|
|
88
|
-
})
|
|
89
|
-
: await client.callTool(BUY_TOOL, { providerAgentId, outcomeId, request });
|
|
79
|
+
if (!(await client.hasTool(CHALLENGE_TOOL)))
|
|
80
|
+
throw gatewayUnsupported(client.gatewayUrl, CHALLENGE_TOOL);
|
|
81
|
+
const result = await client.callTool(CHALLENGE_TOOL, { providerAgentId, outcomeId, request, payerAddress });
|
|
90
82
|
const challenge = GatewayClient.challenge(result);
|
|
91
83
|
if (!challenge) {
|
|
92
84
|
// A success answer with no payload this client can read is a protocol
|
|
93
85
|
// mismatch, not a refusal; say so instead of "the gateway rejected it".
|
|
94
|
-
if (GatewayClient.unreadable(result))
|
|
95
|
-
throw unreadableResultError(
|
|
96
|
-
}
|
|
86
|
+
if (GatewayClient.unreadable(result))
|
|
87
|
+
throw unreadableResultError(CHALLENGE_TOOL, result);
|
|
97
88
|
throw purchaseFailure(result);
|
|
98
89
|
}
|
|
99
90
|
const requirement = challenge.accepts[0];
|
|
@@ -108,7 +99,6 @@ export async function requestChallenge(options) {
|
|
|
108
99
|
challenge,
|
|
109
100
|
requirement,
|
|
110
101
|
binding: bindingFromExtensions(challenge.extensions),
|
|
111
|
-
viaChallengeTool,
|
|
112
102
|
preflight: GatewayClient.preflight(result),
|
|
113
103
|
};
|
|
114
104
|
}
|
|
@@ -184,40 +174,62 @@ export async function authorizePayment(options) {
|
|
|
184
174
|
nonce: validated.recomputedNonce,
|
|
185
175
|
};
|
|
186
176
|
}
|
|
187
|
-
/**
|
|
188
|
-
* Step 5: the paid retry. The payload rides in `_meta["x402/payment"]` unless
|
|
189
|
-
* `--legacy-arg` asks for the argument form.
|
|
190
|
-
*/
|
|
177
|
+
/** Step 5: the paid retry. The payload rides in `_meta["x402/payment"]`. */
|
|
191
178
|
export async function submitPayment(options) {
|
|
192
|
-
|
|
179
|
+
return options.client.callTool(BUY_TOOL, {
|
|
193
180
|
providerAgentId: options.providerAgentId,
|
|
194
181
|
outcomeId: options.outcomeId,
|
|
195
182
|
request: options.request,
|
|
196
|
-
}
|
|
197
|
-
if (options.legacyArg) {
|
|
198
|
-
args.paymentPayload = options.submission;
|
|
199
|
-
return options.client.callTool(BUY_TOOL, args);
|
|
200
|
-
}
|
|
201
|
-
return options.client.callTool(BUY_TOOL, args, {
|
|
183
|
+
}, {
|
|
202
184
|
"x402/payment": options.submission,
|
|
203
185
|
});
|
|
204
186
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
187
|
+
function orderHistoryUnreadable(reason) {
|
|
188
|
+
return new CliError({
|
|
189
|
+
code: "DASKI_ORDER_HISTORY_UNREADABLE",
|
|
190
|
+
message: `The gateway's order history cannot be read: ${reason}.`,
|
|
191
|
+
remediation: "Nothing was signed and nothing was recorded from the unreadable answer. Run daski doctor " +
|
|
192
|
+
"--json to check the gateway pin, then retry; a gateway whose history rows lack " +
|
|
193
|
+
"paymentIdentifier or a decimal grossAmount is not one this release supports.",
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* The rows of a `daski_list_my_orders` answer. A row this CLI cannot read is
|
|
198
|
+
* an error, never a guess: a missing identifier used to be matched on other
|
|
199
|
+
* invariants and a missing amount recorded as zero, both of which put
|
|
200
|
+
* something in the ledger the gateway never said.
|
|
201
|
+
*/
|
|
202
|
+
export function readPayerOrderRows(body) {
|
|
203
|
+
if (!Array.isArray(body.orders))
|
|
204
|
+
throw orderHistoryUnreadable("the answer carries no orders array");
|
|
205
|
+
return body.orders.map((value, index) => {
|
|
206
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
207
|
+
throw orderHistoryUnreadable(`row ${index} is not an object`);
|
|
208
|
+
const row = value;
|
|
209
|
+
const text = (field) => {
|
|
210
|
+
const candidate = row[field];
|
|
211
|
+
if (typeof candidate !== "string" || candidate.length === 0)
|
|
212
|
+
throw orderHistoryUnreadable(`row ${index} has no ${field}`);
|
|
213
|
+
return candidate;
|
|
214
|
+
};
|
|
215
|
+
const grossAmount = row.grossAmount;
|
|
216
|
+
if (typeof grossAmount !== "string" || !/^\d+$/.test(grossAmount))
|
|
217
|
+
throw orderHistoryUnreadable(`row ${index} has no decimal grossAmount`);
|
|
218
|
+
return {
|
|
219
|
+
orderHandle: text("orderHandle"),
|
|
220
|
+
paymentIdentifier: text("paymentIdentifier"),
|
|
221
|
+
providerAgentId: text("providerAgentId"),
|
|
222
|
+
outcomeId: text("outcomeId"),
|
|
223
|
+
grossAmount,
|
|
224
|
+
state: text("state"),
|
|
225
|
+
createdAt: typeof row.createdAt === "string" ? row.createdAt : undefined,
|
|
226
|
+
};
|
|
227
|
+
});
|
|
216
228
|
}
|
|
217
229
|
/**
|
|
218
|
-
* The payer's order history
|
|
219
|
-
*
|
|
220
|
-
*
|
|
230
|
+
* The payer's order history, filtered by the payment identifier when one is
|
|
231
|
+
* given: the gateway filters server-side, and the rows are narrowed again
|
|
232
|
+
* here so a row for another identifier can never be read as this one's.
|
|
221
233
|
*/
|
|
222
234
|
export async function listPayerOrders(options) {
|
|
223
235
|
const body = await callWalletQuery({
|
|
@@ -236,66 +248,78 @@ export async function listPayerOrders(options) {
|
|
|
236
248
|
chainId: options.chainId,
|
|
237
249
|
gatewayUrl: options.gatewayUrl,
|
|
238
250
|
});
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
message: "The gateway returned no readable order history.",
|
|
242
|
-
remediation: "Retry order reconcile with the recorded intent identifier." });
|
|
243
|
-
const rows = body.orders;
|
|
244
|
-
if (!options.intentId)
|
|
245
|
-
return rows;
|
|
246
|
-
const filtered = rows.filter((row) => row.paymentIdentifier === options.intentId);
|
|
247
|
-
// Only narrow when the gateway actually echoes the key; an empty result from
|
|
248
|
-
// a field that does not exist is not evidence of absence.
|
|
249
|
-
return filtered.length > 0 || rows.some((row) => row.paymentIdentifier !== undefined)
|
|
250
|
-
? filtered
|
|
251
|
-
: rows;
|
|
251
|
+
const rows = readPayerOrderRows(body);
|
|
252
|
+
return options.intentId ? rows.filter((row) => row.paymentIdentifier === options.intentId) : rows;
|
|
252
253
|
}
|
|
253
254
|
/**
|
|
254
|
-
* The gateway's order states, read for
|
|
255
|
-
* settle
|
|
255
|
+
* The gateway's closed order states, read for two questions: did the
|
|
256
|
+
* authorization settle, and what does the local ledger record. One table
|
|
257
|
+
* answers both, so a state cannot be settled for the budget and unknown for
|
|
258
|
+
* the record.
|
|
259
|
+
*/
|
|
260
|
+
const GATEWAY_ORDER_STATES = {
|
|
261
|
+
DRAFT: { settlement: "not_settled", local: "NOT_SETTLED" },
|
|
262
|
+
CHALLENGE_ISSUED: { settlement: "not_settled", local: "NOT_SETTLED" },
|
|
263
|
+
VERIFY_REJECTED: { settlement: "not_settled", local: "NOT_SETTLED" },
|
|
264
|
+
SETTLEMENT_FAILED: { settlement: "not_settled", local: "NOT_SETTLED" },
|
|
265
|
+
NOT_SETTLED: { settlement: "not_settled", local: "NOT_SETTLED" },
|
|
266
|
+
ATTEMPT_OPENED: { settlement: "in_flight", local: "PENDING_RECONCILIATION" },
|
|
267
|
+
VERIFIED: { settlement: "in_flight", local: "PENDING_RECONCILIATION" },
|
|
268
|
+
SETTLE_INVOKED: { settlement: "in_flight", local: "PENDING_RECONCILIATION" },
|
|
269
|
+
SETTLEMENT_AMBIGUOUS: { settlement: "ambiguous", local: "PENDING_RECONCILIATION" },
|
|
270
|
+
EXTERNAL_OR_UNPROVEN_DEPOSIT: { settlement: "ambiguous", local: "PENDING_RECONCILIATION" },
|
|
271
|
+
// Dispatch ambiguity is not payment ambiguity: the order exists and was paid.
|
|
272
|
+
DISPATCH_AMBIGUOUS: { settlement: "ambiguous", local: "SUBMITTED" },
|
|
273
|
+
FACILITATOR_CONFIRMED: { settlement: "settled", local: "SUBMITTED" },
|
|
274
|
+
DEPOSIT_FINAL: { settlement: "settled", local: "SUBMITTED" },
|
|
275
|
+
RELEASE_FINAL: { settlement: "settled", local: "SUBMITTED" },
|
|
276
|
+
DISPATCH_STARTED: { settlement: "settled", local: "SUBMITTED" },
|
|
277
|
+
DISPATCHED: { settlement: "settled", local: "SUBMITTED" },
|
|
278
|
+
LEGAL_HOLD: { settlement: "settled", local: "SUBMITTED" },
|
|
279
|
+
FULFILLED: { settlement: "settled", local: "FULFILLED" },
|
|
280
|
+
PROVIDER_FAILED: { settlement: "settled", local: "PROVIDER_FAILED" },
|
|
281
|
+
INPUT_REQUIRED: { settlement: "settled", local: "INPUT_REQUIRED" },
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* The money question. An unknown state is ambiguous, never absent: nothing
|
|
285
|
+
* is signed again on the strength of a state this CLI does not understand.
|
|
256
286
|
*/
|
|
257
287
|
export function readSettlement(state) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
case "LEGAL_HOLD":
|
|
282
|
-
return "settled";
|
|
283
|
-
default:
|
|
284
|
-
return "ambiguous";
|
|
285
|
-
}
|
|
288
|
+
return GATEWAY_ORDER_STATES[state]?.settlement ?? "ambiguous";
|
|
289
|
+
}
|
|
290
|
+
/** A gateway order state this release does not understand; the ledger records nothing for it. */
|
|
291
|
+
export function orderStateUnreadable(state, orderHandle) {
|
|
292
|
+
return new CliError({
|
|
293
|
+
code: "DASKI_ORDER_STATE_UNREADABLE",
|
|
294
|
+
message: `The gateway reports order state ${JSON.stringify(state)}, which this release does not understand.`,
|
|
295
|
+
remediation: (orderHandle ? `The order exists as ${orderHandle} and the local record keeps its previous state. ` : "") +
|
|
296
|
+
"Upgrade to the @daski/pay release the gateway pins (daski doctor --json shows the pin), then " +
|
|
297
|
+
`run daski order status ${orderHandle ?? "<handle>"}.`,
|
|
298
|
+
details: { gatewayState: state, ...(orderHandle ? { orderHandle } : {}) },
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The ledger question: the local state a gateway order state records as. An
|
|
303
|
+
* unknown state is refused, never recorded as SUBMITTED; the caller decides
|
|
304
|
+
* what else it has already learned (a handle, a receipt) and keeps that.
|
|
305
|
+
*/
|
|
306
|
+
export function localOrderState(state, orderHandle) {
|
|
307
|
+
const known = typeof state === "string" ? GATEWAY_ORDER_STATES[state] : undefined;
|
|
308
|
+
if (!known)
|
|
309
|
+
throw orderStateUnreadable(state, orderHandle);
|
|
310
|
+
return known.local;
|
|
286
311
|
}
|
|
287
312
|
/**
|
|
288
313
|
* The gateway's own answer for one payment identifier, with nothing signed
|
|
289
314
|
* but the read authorization: no replay, no balance reading, no local
|
|
290
315
|
* ledger. This is the lookup buy.md names for every ambiguous outcome and
|
|
291
316
|
* for PAYMENT_IDENTIFIER_CONFLICT. An identifier the gateway never issued
|
|
292
|
-
*
|
|
293
|
-
* nothing could have settled under it.
|
|
317
|
+
* matches nothing, which is the truth: nothing could have settled under it.
|
|
294
318
|
*/
|
|
295
319
|
export function reconcileIdentifierRows(intentId, rows) {
|
|
296
320
|
const matches = rows.filter((row) => row.paymentIdentifier === intentId);
|
|
297
|
-
if (
|
|
298
|
-
return { status: "ambiguous", evidence: "The gateway history
|
|
321
|
+
if (matches.length > 1) {
|
|
322
|
+
return { status: "ambiguous", evidence: "The gateway history lists more than one order for the identifier." };
|
|
299
323
|
}
|
|
300
324
|
if (matches.length === 0) {
|
|
301
325
|
return {
|
|
@@ -350,8 +374,12 @@ export function purchaseFailure(result, options = {}) {
|
|
|
350
374
|
message: typeof body?.message === "string"
|
|
351
375
|
? body.message
|
|
352
376
|
: "The gateway rejected the purchase.",
|
|
353
|
-
remediation:
|
|
354
|
-
|
|
377
|
+
remediation: gatewayRefusalRemediation(code, body) ??
|
|
378
|
+
(typeof body?.next_action === "string" ? body.next_action : fallbackRemediation),
|
|
379
|
+
details: {
|
|
380
|
+
gateway: body ?? describeResult(result),
|
|
381
|
+
...(isRetryableGatewayCode(code) ? { retryable: true } : {}),
|
|
382
|
+
},
|
|
355
383
|
});
|
|
356
384
|
}
|
|
357
385
|
/** Human-readable price, for approval prompts and receipts. */
|