@onchaindiligence/sdk 0.2.0 → 0.3.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 +95 -8
- package/conformance/attestation-v1-v2-vectors.json +98 -0
- package/conformance/rfc8785-vectors.json +31 -0
- package/dist/commerce/client.d.ts +134 -0
- package/dist/commerce/client.js +511 -0
- package/dist/commerce/evidenceExport.d.ts +51 -0
- package/dist/commerce/evidenceExport.js +44 -0
- package/dist/commerce/executor.d.ts +64 -0
- package/dist/commerce/executor.js +19 -0
- package/dist/commerce/index.d.ts +29 -0
- package/dist/commerce/index.js +29 -0
- package/dist/commerce/mockExecutor.d.ts +41 -0
- package/dist/commerce/mockExecutor.js +74 -0
- package/dist/commerce/nodeFileRecoveryStore.d.ts +17 -0
- package/dist/commerce/nodeFileRecoveryStore.js +126 -0
- package/dist/commerce/policyTemplates.d.ts +48 -0
- package/dist/commerce/policyTemplates.js +54 -0
- package/dist/commerce/recoveryStore.d.ts +73 -0
- package/dist/commerce/recoveryStore.js +77 -0
- package/dist/commerce/results.d.ts +90 -0
- package/dist/commerce/results.js +3 -0
- package/dist/commerce/types.d.ts +147 -0
- package/dist/commerce/types.js +13 -0
- package/dist/commerce/x402Executor.d.ts +43 -0
- package/dist/commerce/x402Executor.js +222 -0
- package/dist/index.d.ts +36 -32
- package/dist/index.js +155 -86
- package/dist/verification.d.ts +109 -0
- package/dist/verification.js +621 -0
- package/package.json +19 -3
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* results.ts — discriminated unions for every lifecycle outcome (D2.5,
|
|
3
|
+
* Section 2). Ordinary lifecycle states are never thrown as exceptions —
|
|
4
|
+
* only genuinely exceptional conditions (bad input, a network the caller
|
|
5
|
+
* cannot reasonably plan around) do. `receipt-produced` deliberately does
|
|
6
|
+
* NOT mean "payment succeeded": the receipt may describe a confirmed
|
|
7
|
+
* payment, a failed execution, a mismatch, or an uncertain observation —
|
|
8
|
+
* read `receipt.receipt.execution.status` / `.settlement.status` /
|
|
9
|
+
* `.decision.status`, never infer success from a receipt merely existing.
|
|
10
|
+
*/
|
|
11
|
+
import type { ReceiptEnvelope, OperationStatus } from './types.js';
|
|
12
|
+
/** Shared shape for every non-terminal "come back later" outcome. */
|
|
13
|
+
export interface PendingInfo {
|
|
14
|
+
/** Where in the lifecycle this operation currently sits. */
|
|
15
|
+
phase: 'preflight-in-progress' | 'awaiting-execution' | 'execution-ambiguous' | 'awaiting-observation' | 'observation-pending';
|
|
16
|
+
/** What the caller should do next, in plain language a UI can show directly. */
|
|
17
|
+
safeNextAction: string;
|
|
18
|
+
/** Seconds to wait before retrying, when known. */
|
|
19
|
+
retryAfterSeconds?: number;
|
|
20
|
+
/** True whenever OCD's fee (or, for execution, the merchant payment) may already have been taken — the caller must NEVER treat this as "safe to pay again". */
|
|
21
|
+
mayAlreadyHavePaid: boolean;
|
|
22
|
+
operationId: string;
|
|
23
|
+
executionRequestId?: string | null;
|
|
24
|
+
}
|
|
25
|
+
export type PreflightEvaluation = {
|
|
26
|
+
kind: 'ready';
|
|
27
|
+
operationId: string;
|
|
28
|
+
receipt: ReceiptEnvelope;
|
|
29
|
+
capabilityExpiresAt: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'blocked';
|
|
32
|
+
operationId: string;
|
|
33
|
+
receipt: ReceiptEnvelope;
|
|
34
|
+
reasons: string[];
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'approval-required';
|
|
37
|
+
operationId: string;
|
|
38
|
+
receipt: ReceiptEnvelope;
|
|
39
|
+
reasons: string[];
|
|
40
|
+
} | ({
|
|
41
|
+
kind: 'pending';
|
|
42
|
+
} & PendingInfo) | {
|
|
43
|
+
kind: 'terminal-error';
|
|
44
|
+
operationId: string;
|
|
45
|
+
error: string;
|
|
46
|
+
};
|
|
47
|
+
export type ExecutionRecord = {
|
|
48
|
+
kind: 'execution-recorded';
|
|
49
|
+
operationId: string;
|
|
50
|
+
executionRequestId: string;
|
|
51
|
+
transactionHash: string;
|
|
52
|
+
providerReference?: string | null;
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'manual-recovery-required';
|
|
55
|
+
operationId: string;
|
|
56
|
+
executionRequestId: string;
|
|
57
|
+
reason: string;
|
|
58
|
+
} | ({
|
|
59
|
+
kind: 'pending';
|
|
60
|
+
} & PendingInfo) | {
|
|
61
|
+
kind: 'terminal-error';
|
|
62
|
+
operationId: string;
|
|
63
|
+
error: string;
|
|
64
|
+
};
|
|
65
|
+
export type FinalizeResult = {
|
|
66
|
+
kind: 'receipt-produced';
|
|
67
|
+
operationId: string;
|
|
68
|
+
receipt: ReceiptEnvelope;
|
|
69
|
+
evidence: {
|
|
70
|
+
bundle_digest: string;
|
|
71
|
+
binding_strength: 'TRANSFER_MATCH_ONLY' | 'EXECUTOR_CORRELATED' | 'PAYMENT_IDENTITY_LINKED';
|
|
72
|
+
} | null;
|
|
73
|
+
} | ({
|
|
74
|
+
kind: 'pending';
|
|
75
|
+
} & PendingInfo) | {
|
|
76
|
+
kind: 'terminal-error';
|
|
77
|
+
operationId: string;
|
|
78
|
+
error: string;
|
|
79
|
+
};
|
|
80
|
+
export type ResumeResult = {
|
|
81
|
+
kind: 'resumed';
|
|
82
|
+
operationId: string;
|
|
83
|
+
status: OperationStatus;
|
|
84
|
+
} | {
|
|
85
|
+
kind: 'recovery-failed';
|
|
86
|
+
reason: string;
|
|
87
|
+
};
|
|
88
|
+
export declare function pending(operationId: string, info: Omit<PendingInfo, 'operationId'>): {
|
|
89
|
+
kind: 'pending';
|
|
90
|
+
} & PendingInfo;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — the commerce-lifecycle wire contract, ported by hand from
|
|
3
|
+
* onchaindiligence-mcp's src/preflight.ts / src/receipts.ts / src/lifecycleRoute.ts
|
|
4
|
+
* / src/lifecycleFinalizeRoute.ts (D2.1/D2.4).
|
|
5
|
+
*
|
|
6
|
+
* SOURCE OF TRUTH: the deployed mcp.onchaindiligence.com API. This file does
|
|
7
|
+
* not implement or re-verify anything — it exists so the commerce client has
|
|
8
|
+
* exact types for what it sends and receives, mirroring the same
|
|
9
|
+
* "ported minimal copy, kept in sync by hand" discipline
|
|
10
|
+
* onchaindiligence-mcp's own receipts.ts already documents for its
|
|
11
|
+
* relationship to the canonical packages/agent-evidence source.
|
|
12
|
+
*/
|
|
13
|
+
export interface CommerceAction {
|
|
14
|
+
kind: 'PAYMENT';
|
|
15
|
+
/** URL of the resource/service the payment is for, if any. */
|
|
16
|
+
resource: string | null;
|
|
17
|
+
/** CAIP-2 network identifier, e.g. "eip155:8453" for Base mainnet. */
|
|
18
|
+
network: string;
|
|
19
|
+
/** Canonical ERC-20 token contract address — never a bare ticker. */
|
|
20
|
+
asset: string;
|
|
21
|
+
/** Canonical decimal string, e.g. "1.00". Never a float. */
|
|
22
|
+
amount: string;
|
|
23
|
+
sender: string | null;
|
|
24
|
+
recipient: string;
|
|
25
|
+
}
|
|
26
|
+
export interface CommercePolicy {
|
|
27
|
+
max_amount?: string | null;
|
|
28
|
+
allowed_networks?: string[] | null;
|
|
29
|
+
allowed_assets?: string[] | null;
|
|
30
|
+
expected_recipient?: string | null;
|
|
31
|
+
allowed_resource_origins?: string[] | null;
|
|
32
|
+
/** Structural confirmation that every field above being null/omitted is intentional, not an accident. */
|
|
33
|
+
acknowledge_unconstrained?: boolean;
|
|
34
|
+
/** A frozen commitment to the wallet expected to AUTHORIZE the eventual on-chain payment (D2.4) — decision-neutral. */
|
|
35
|
+
expected_payer?: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface CommercePublication {
|
|
38
|
+
preflight?: boolean;
|
|
39
|
+
commerce?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export type DecisionStatus = 'ALLOW' | 'REQUIRE_APPROVAL' | 'BLOCK' | 'UNKNOWN';
|
|
42
|
+
export interface ReceiptDecision {
|
|
43
|
+
status: DecisionStatus;
|
|
44
|
+
authorized: boolean | null;
|
|
45
|
+
reasons: string[];
|
|
46
|
+
}
|
|
47
|
+
export interface ReceiptCheck {
|
|
48
|
+
id: string;
|
|
49
|
+
result: 'PASS' | 'FAIL' | 'UNKNOWN' | 'NOT_CHECKED';
|
|
50
|
+
summary: string;
|
|
51
|
+
evidence_digest: string | null;
|
|
52
|
+
}
|
|
53
|
+
export type ExecutionStatus = 'NOT_SUBMITTED' | 'SUBMITTED' | 'CONFIRMED' | 'FAILED' | 'UNKNOWN';
|
|
54
|
+
export type SettlementStatus = 'CONFIRMED' | 'NOT_CONFIRMED' | 'UNVERIFIED' | 'NOT_APPLICABLE';
|
|
55
|
+
export interface Receipt {
|
|
56
|
+
receipt_id: string;
|
|
57
|
+
receipt_digest: string;
|
|
58
|
+
receipt_type: 'ACTION' | 'PREFLIGHT' | 'COMMERCE';
|
|
59
|
+
issued_at: string;
|
|
60
|
+
action: CommerceAction & {
|
|
61
|
+
resource: string | null;
|
|
62
|
+
};
|
|
63
|
+
decision: ReceiptDecision;
|
|
64
|
+
execution: {
|
|
65
|
+
provider: string | null;
|
|
66
|
+
status: ExecutionStatus;
|
|
67
|
+
transaction_hash: string | null;
|
|
68
|
+
submitted_at: string | null;
|
|
69
|
+
confirmed_at: string | null;
|
|
70
|
+
};
|
|
71
|
+
settlement: {
|
|
72
|
+
status: SettlementStatus;
|
|
73
|
+
detail: string | null;
|
|
74
|
+
};
|
|
75
|
+
checks: ReceiptCheck[];
|
|
76
|
+
links: {
|
|
77
|
+
agent_evidence_bundle_digest: string | null;
|
|
78
|
+
preflight_receipt_id: string | null;
|
|
79
|
+
};
|
|
80
|
+
limitations: string[];
|
|
81
|
+
}
|
|
82
|
+
export interface ReceiptEnvelope {
|
|
83
|
+
schema: string;
|
|
84
|
+
receipt: Receipt;
|
|
85
|
+
proof: {
|
|
86
|
+
signed: boolean;
|
|
87
|
+
schema_version?: string;
|
|
88
|
+
issuer?: string;
|
|
89
|
+
purpose?: string;
|
|
90
|
+
issued_at?: string;
|
|
91
|
+
key_id?: string;
|
|
92
|
+
algorithm?: string;
|
|
93
|
+
canonicalization?: string;
|
|
94
|
+
signature?: string;
|
|
95
|
+
signing_input_hint?: string;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export interface PreflightFinalization {
|
|
99
|
+
capability: string;
|
|
100
|
+
expires_at: string;
|
|
101
|
+
endpoint: string;
|
|
102
|
+
}
|
|
103
|
+
export interface PreflightResponseBody {
|
|
104
|
+
decision: ReceiptDecision;
|
|
105
|
+
checks: ReceiptCheck[];
|
|
106
|
+
receipt: ReceiptEnvelope;
|
|
107
|
+
finalization: PreflightFinalization;
|
|
108
|
+
}
|
|
109
|
+
export interface CreatedOperation {
|
|
110
|
+
operation_id: string;
|
|
111
|
+
recovery_credential: string;
|
|
112
|
+
}
|
|
113
|
+
export type PreflightState = 'not_started' | 'in_progress' | 'completed';
|
|
114
|
+
export type ExecutionState = 'not_submitted' | 'prepared' | 'submission_ambiguous' | 'submitted' | 'outcome_unknown' | 'transaction_known' | 'manual_recovery_required';
|
|
115
|
+
export type ObservationState = 'none' | 'pending' | 'confirmed' | 'contradicted';
|
|
116
|
+
export type ReceiptState = 'none' | 'preflight_only' | 'commerce_issued';
|
|
117
|
+
export interface OperationStatus {
|
|
118
|
+
operation_id: string;
|
|
119
|
+
preflight_state: PreflightState;
|
|
120
|
+
execution_state: ExecutionState;
|
|
121
|
+
observation_state: ObservationState;
|
|
122
|
+
receipt_state: ReceiptState;
|
|
123
|
+
preflight_receipt_id: string | null;
|
|
124
|
+
}
|
|
125
|
+
export interface ExecutionBindingResponse {
|
|
126
|
+
execution_request_id: string;
|
|
127
|
+
submission_state: ExecutionState;
|
|
128
|
+
idempotent_replay: boolean;
|
|
129
|
+
}
|
|
130
|
+
export interface FinalizeExecutionInput {
|
|
131
|
+
transaction_hash: string;
|
|
132
|
+
execution_provider: 'x402' | 'paybox' | 'wallet' | 'other';
|
|
133
|
+
provider_reference?: string | null;
|
|
134
|
+
result_digest?: string | null;
|
|
135
|
+
execution_request_id?: string | null;
|
|
136
|
+
}
|
|
137
|
+
export interface OperationFinalizeResponseBody extends ReceiptEnvelope {
|
|
138
|
+
ocd_lifecycle_evidence: {
|
|
139
|
+
bundle_digest: string;
|
|
140
|
+
binding_strength: 'TRANSFER_MATCH_ONLY' | 'EXECUTOR_CORRELATED' | 'PAYMENT_IDENTITY_LINKED';
|
|
141
|
+
} | null;
|
|
142
|
+
}
|
|
143
|
+
/** The shape of an API error body across every endpoint used here. */
|
|
144
|
+
export interface ApiErrorBody {
|
|
145
|
+
error: string;
|
|
146
|
+
reason?: string;
|
|
147
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — the commerce-lifecycle wire contract, ported by hand from
|
|
3
|
+
* onchaindiligence-mcp's src/preflight.ts / src/receipts.ts / src/lifecycleRoute.ts
|
|
4
|
+
* / src/lifecycleFinalizeRoute.ts (D2.1/D2.4).
|
|
5
|
+
*
|
|
6
|
+
* SOURCE OF TRUTH: the deployed mcp.onchaindiligence.com API. This file does
|
|
7
|
+
* not implement or re-verify anything — it exists so the commerce client has
|
|
8
|
+
* exact types for what it sends and receives, mirroring the same
|
|
9
|
+
* "ported minimal copy, kept in sync by hand" discipline
|
|
10
|
+
* onchaindiligence-mcp's own receipts.ts already documents for its
|
|
11
|
+
* relationship to the canonical packages/agent-evidence source.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { toClientEvmSigner, type ClientEvmSigner } from '@x402/evm';
|
|
2
|
+
import type { CommerceExecutor, PrepareContext, PrepareResult, ExecutionResult, ExecutorRecoveryMode } from './executor.js';
|
|
3
|
+
export type { ClientEvmSigner };
|
|
4
|
+
export { toClientEvmSigner };
|
|
5
|
+
export declare const BASE_NETWORK = "eip155:8453";
|
|
6
|
+
export declare const BASE_USDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
7
|
+
/** The subset of viem's PublicClient resume() actually calls -- narrowed so tests can inject a minimal fake instead of a real RPC connection. */
|
|
8
|
+
export interface MinimalResumeClient {
|
|
9
|
+
getTransactionReceipt: (args: {
|
|
10
|
+
hash: `0x${string}`;
|
|
11
|
+
}) => Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
export interface X402ExecutorOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Signs the EIP-3009 payment authorization. Never logged, never persisted
|
|
16
|
+
* by this class. `@x402/evm`'s own ExactEvmScheme wants exactly this
|
|
17
|
+
* shape (address + signTypedData), NOT a full viem Account/WalletClient --
|
|
18
|
+
* that's deliberate: it's the one interface both a Node private-key
|
|
19
|
+
* signer (`toClientEvmSigner(privateKeyToAccount(pk))`, re-exported from
|
|
20
|
+
* this module) and a browser injected-wallet signer (hand-built around
|
|
21
|
+
* `walletClient.signTypedData`, since an injected wallet's viem account has
|
|
22
|
+
* no signing methods of its own) can equally satisfy.
|
|
23
|
+
*/
|
|
24
|
+
signer: ClientEvmSigner;
|
|
25
|
+
/** Base RPC used ONLY for read-only resume confirmation. Defaults to the public Base RPC. */
|
|
26
|
+
rpcUrl?: string;
|
|
27
|
+
fetch?: typeof globalThis.fetch;
|
|
28
|
+
/** Test seam: inject a fake read-only client instead of connecting to rpcUrl. */
|
|
29
|
+
publicClient?: MinimalResumeClient;
|
|
30
|
+
}
|
|
31
|
+
export declare class X402BaseUsdcExecutor implements CommerceExecutor {
|
|
32
|
+
readonly id = "x402-base-usdc-exact";
|
|
33
|
+
readonly version = "v1";
|
|
34
|
+
readonly recoveryMode: ExecutorRecoveryMode;
|
|
35
|
+
private readonly signer;
|
|
36
|
+
private readonly fetchImpl;
|
|
37
|
+
private readonly rpcUrl;
|
|
38
|
+
private readonly injectedPublicClient?;
|
|
39
|
+
constructor(options: X402ExecutorOptions);
|
|
40
|
+
prepare(context: PrepareContext): Promise<PrepareResult>;
|
|
41
|
+
submit(prepared: PrepareResult): Promise<ExecutionResult>;
|
|
42
|
+
resume(prepared: PrepareResult, priorOutcome?: ExecutionResult): Promise<ExecutionResult>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* x402Executor.ts — the one narrow, production-quality executor adapter for
|
|
3
|
+
* the existing Base USDC x402 v2 "exact" flow (D2.5, Section 4).
|
|
4
|
+
*
|
|
5
|
+
* SCOPE, DELIBERATE: Base mainnet, USDC, x402 v2 exact scheme only — no
|
|
6
|
+
* other chain/asset/scheme. Reuses the EXACT same proven pattern as
|
|
7
|
+
* onchaindiligence-mcp's scripts/first-commerce-lifecycle.ts (D2.2A) and
|
|
8
|
+
* operator/src/main.ts (D2.2B), the only two places this integration has
|
|
9
|
+
* ever moved real money: `wrapFetchWithPayment` + `x402Client` +
|
|
10
|
+
* `ExactEvmScheme(signer)`, called through EXACTLY ONCE per submit().
|
|
11
|
+
*
|
|
12
|
+
* RECOVERY MODE IS HONESTLY 'manual', not 'stable-payment-identity':
|
|
13
|
+
* `wrapFetchWithPayment` is an atomic sign-and-pay primitive — it does not
|
|
14
|
+
* expose the ERC-3009 nonce it generates internally, so if `submit()` never
|
|
15
|
+
* receives a response (timeout, connection drop), this adapter has no
|
|
16
|
+
* independent identity to query "was this specific authorization consumed"
|
|
17
|
+
* against. A deeper integration COULD extract the nonce via
|
|
18
|
+
* `ExactEvmScheme.createPaymentPayload()` and later check it on-chain via
|
|
19
|
+
* USDC's own `authorizationState(authorizer, nonce)` view function — but
|
|
20
|
+
* that path is unproven against the real facilitator (verifying it would
|
|
21
|
+
* require a live payment, which no milestone through D2.5 permits) and is
|
|
22
|
+
* therefore NOT implemented here. Reporting 'manual' honestly, and resolving
|
|
23
|
+
* to manual-recovery-required on an ambiguous submit, is the correct choice
|
|
24
|
+
* over pretending a recovery guarantee this code cannot actually back up —
|
|
25
|
+
* see onchaindiligence-mcp's own D2.4 executionBinding.ts: "It is preferable
|
|
26
|
+
* to pretending exactly-once execution can be guaranteed."
|
|
27
|
+
*
|
|
28
|
+
* The ONE exception: if `submit()` DID receive a response before some LATER
|
|
29
|
+
* step failed (so a transaction hash is already known), `resume()` can and
|
|
30
|
+
* does independently re-confirm it read-only on-chain — no guessing, no new
|
|
31
|
+
* payment, just checking what's already claimed.
|
|
32
|
+
*/
|
|
33
|
+
import { createPublicClient, http } from 'viem';
|
|
34
|
+
import { base } from 'viem/chains';
|
|
35
|
+
import { wrapFetchWithPayment } from '@x402/fetch';
|
|
36
|
+
import { x402Client } from '@x402/core/client';
|
|
37
|
+
import { ExactEvmScheme } from '@x402/evm/exact/client';
|
|
38
|
+
import { toClientEvmSigner } from '@x402/evm';
|
|
39
|
+
export { toClientEvmSigner };
|
|
40
|
+
export const BASE_NETWORK = 'eip155:8453';
|
|
41
|
+
export const BASE_USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
|
|
42
|
+
/**
|
|
43
|
+
* `Buffer` is a Node global, not a browser one -- calling `Buffer.from(...)`
|
|
44
|
+
* here used to throw `ReferenceError: Buffer is not defined` in a real
|
|
45
|
+
* browser (confirmed live, D2.5A: a real OneSource 402 challenge, valid and
|
|
46
|
+
* byte-identical through the local proxy, failed to decode). That
|
|
47
|
+
* ReferenceError was thrown INSIDE decodeChallenge()'s try/catch below and
|
|
48
|
+
* silently relabeled as "Payment-Required header was not base64-encoded
|
|
49
|
+
* JSON" -- a misleading error that looks like a merchant-format problem but
|
|
50
|
+
* isn't one. `atob`/`TextDecoder` are the browser-safe equivalents (both
|
|
51
|
+
* also globally available in Node), mirroring lifecycleCore.ts's own
|
|
52
|
+
* isomorphic decodeChallenge in onchaindiligence-mcp exactly.
|
|
53
|
+
*/
|
|
54
|
+
function base64ToUtf8(base64) {
|
|
55
|
+
const binary = atob(base64);
|
|
56
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
57
|
+
return new TextDecoder().decode(bytes);
|
|
58
|
+
}
|
|
59
|
+
class X402ChallengeError extends Error {
|
|
60
|
+
}
|
|
61
|
+
function decodeChallenge(res) {
|
|
62
|
+
const header = res.headers.get('payment-required');
|
|
63
|
+
if (!header)
|
|
64
|
+
throw new X402ChallengeError(`${res.url}: 402 response carried no Payment-Required header`);
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(base64ToUtf8(header));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
throw new X402ChallengeError(`${res.url}: Payment-Required header was not base64-encoded JSON`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Validates a decoded x402 v2 "exact" challenge against the frozen preflighted action. Never mutates anything; throws before any signing could occur. */
|
|
73
|
+
function validateChallenge(challenge, expected) {
|
|
74
|
+
if (challenge?.x402Version !== 2)
|
|
75
|
+
throw new X402ChallengeError(`unexpected x402 version ${challenge?.x402Version} (expected 2)`);
|
|
76
|
+
const accepts = challenge?.accepts?.[0];
|
|
77
|
+
if (!accepts)
|
|
78
|
+
throw new X402ChallengeError('challenge contained no accepts entry');
|
|
79
|
+
if (accepts.scheme !== 'exact')
|
|
80
|
+
throw new X402ChallengeError(`unexpected scheme "${accepts.scheme}" (expected "exact")`);
|
|
81
|
+
if (accepts.network !== expected.network)
|
|
82
|
+
throw new X402ChallengeError(`network mismatch: quoted "${accepts.network}", expected "${expected.network}"`);
|
|
83
|
+
if (String(accepts.asset).toLowerCase() !== expected.asset.toLowerCase()) {
|
|
84
|
+
throw new X402ChallengeError(`asset mismatch: quoted "${accepts.asset}", expected "${expected.asset}"`);
|
|
85
|
+
}
|
|
86
|
+
if (String(accepts.payTo).toLowerCase() !== expected.recipient.toLowerCase()) {
|
|
87
|
+
throw new X402ChallengeError(`recipient mismatch: quoted "${accepts.payTo}", expected "${expected.recipient}" -- refusing to pay an unexpected address`);
|
|
88
|
+
}
|
|
89
|
+
if (String(accepts.amount) !== expected.amount) {
|
|
90
|
+
throw new X402ChallengeError(`amount mismatch: quoted "${accepts.amount}", expected exactly "${expected.amount}" atomic units`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function decodeSettlementResponse(res) {
|
|
94
|
+
const header = res.headers.get('x-payment-response') ?? res.headers.get('payment-response');
|
|
95
|
+
if (!header)
|
|
96
|
+
return { transactionHash: null };
|
|
97
|
+
try {
|
|
98
|
+
const decoded = JSON.parse(base64ToUtf8(header));
|
|
99
|
+
return { transactionHash: typeof decoded?.transaction === 'string' ? decoded.transaction : null };
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return { transactionHash: null };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Converts a canonical decimal amount (e.g. "1.00") into USDC's 6-decimal
|
|
107
|
+
* atomic unit string, WITHOUT floating point — mirrors
|
|
108
|
+
* onchaindiligence-mcp's src/money.ts exactly (kept independent here since
|
|
109
|
+
* this package does not depend on that server-side module).
|
|
110
|
+
*/
|
|
111
|
+
function decimalToAtomic6(amount) {
|
|
112
|
+
const [intPart, fracPart = ''] = amount.split('.');
|
|
113
|
+
if (fracPart.length > 6)
|
|
114
|
+
throw new X402ChallengeError(`amount "${amount}" has more precision than USDC's 6 decimals support`);
|
|
115
|
+
return BigInt(intPart + fracPart.padEnd(6, '0')).toString();
|
|
116
|
+
}
|
|
117
|
+
export class X402BaseUsdcExecutor {
|
|
118
|
+
id = 'x402-base-usdc-exact';
|
|
119
|
+
version = 'v1';
|
|
120
|
+
recoveryMode = 'manual';
|
|
121
|
+
signer;
|
|
122
|
+
fetchImpl;
|
|
123
|
+
rpcUrl;
|
|
124
|
+
injectedPublicClient;
|
|
125
|
+
constructor(options) {
|
|
126
|
+
this.signer = options.signer;
|
|
127
|
+
// See client.ts's constructor comment: a bare `globalThis.fetch`
|
|
128
|
+
// reference, later invoked as `this.fetchImpl(...)`, throws "Illegal
|
|
129
|
+
// invocation" in real browsers (detached from its required receiver) --
|
|
130
|
+
// invisible under Node, which is why this only surfaces against a real
|
|
131
|
+
// injected-wallet browser flow. Bind it here so this executor is safe to
|
|
132
|
+
// construct with no `fetch` option in either environment.
|
|
133
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
134
|
+
this.rpcUrl = options.rpcUrl ?? 'https://mainnet.base.org';
|
|
135
|
+
this.injectedPublicClient = options.publicClient;
|
|
136
|
+
}
|
|
137
|
+
async prepare(context) {
|
|
138
|
+
if (context.action.network !== BASE_NETWORK)
|
|
139
|
+
throw new X402ChallengeError(`this executor only supports ${BASE_NETWORK}, got "${context.action.network}"`);
|
|
140
|
+
if (context.action.asset.toLowerCase() !== BASE_USDC.toLowerCase())
|
|
141
|
+
throw new X402ChallengeError(`this executor only supports USDC (${BASE_USDC}), got "${context.action.asset}"`);
|
|
142
|
+
if (!context.action.resource)
|
|
143
|
+
throw new X402ChallengeError('action.resource (the x402 resource URL) is required to prepare a submission');
|
|
144
|
+
// Provoke the 402 challenge with a plain, unauthenticated request --
|
|
145
|
+
// read-only, no signing, no payment. This is the ENTIRE point of
|
|
146
|
+
// prepare(): validate what we'd be agreeing to pay BEFORE any
|
|
147
|
+
// authorization exists.
|
|
148
|
+
const probe = await this.fetchImpl(context.action.resource);
|
|
149
|
+
if (probe.status !== 402) {
|
|
150
|
+
throw new X402ChallengeError(`expected HTTP 402 from ${context.action.resource}, got ${probe.status}`);
|
|
151
|
+
}
|
|
152
|
+
const challenge = decodeChallenge(probe);
|
|
153
|
+
const atomicAmount = decimalToAtomic6(context.action.amount);
|
|
154
|
+
validateChallenge(challenge, { network: context.action.network, asset: context.action.asset, amount: atomicAmount, recipient: context.action.recipient });
|
|
155
|
+
const reference = {
|
|
156
|
+
resourceUrl: context.action.resource,
|
|
157
|
+
network: context.action.network,
|
|
158
|
+
asset: context.action.asset,
|
|
159
|
+
atomicAmount,
|
|
160
|
+
recipient: context.action.recipient,
|
|
161
|
+
};
|
|
162
|
+
return { clientSubmissionKey: context.clientSubmissionKey, reference, preparedAt: new Date().toISOString() };
|
|
163
|
+
}
|
|
164
|
+
async submit(prepared) {
|
|
165
|
+
const ref = prepared.reference;
|
|
166
|
+
const client = new x402Client().register(ref.network, new ExactEvmScheme(this.signer));
|
|
167
|
+
const payingFetch = wrapFetchWithPayment(this.fetchImpl, client);
|
|
168
|
+
let res;
|
|
169
|
+
try {
|
|
170
|
+
res = await payingFetch(ref.resourceUrl);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
// No response at all -- the single most dangerous case: we genuinely
|
|
174
|
+
// do not know whether the facilitator ever broadcast the
|
|
175
|
+
// authorization. Never retry automatically; report ambiguous.
|
|
176
|
+
return { clientSubmissionKey: prepared.clientSubmissionKey, status: 'submission-ambiguous', reason: err?.message || 'no response from resource/facilitator' };
|
|
177
|
+
}
|
|
178
|
+
if (res.status === 402) {
|
|
179
|
+
// The payment was rejected outright (e.g. facilitator declined the
|
|
180
|
+
// authorization before ever broadcasting) -- this is a DEFINITIVE
|
|
181
|
+
// non-payment, safe to report as ambiguous-for-manual-review rather
|
|
182
|
+
// than a silent failure, since we still can't be 100% sure nothing
|
|
183
|
+
// was ever submitted on-chain by a misbehaving facilitator.
|
|
184
|
+
return { clientSubmissionKey: prepared.clientSubmissionKey, status: 'submission-ambiguous', reason: `resource still returned 402 after payment attempt (status ${res.status})` };
|
|
185
|
+
}
|
|
186
|
+
if (!res.ok) {
|
|
187
|
+
return { clientSubmissionKey: prepared.clientSubmissionKey, status: 'submission-ambiguous', reason: `resource returned HTTP ${res.status} after a payment attempt was made -- outcome unknown` };
|
|
188
|
+
}
|
|
189
|
+
const { transactionHash } = decodeSettlementResponse(res);
|
|
190
|
+
if (!transactionHash) {
|
|
191
|
+
// Paid successfully per the resource's own 2xx, but we couldn't parse
|
|
192
|
+
// a transaction hash out of the settlement response -- still
|
|
193
|
+
// ambiguous from OCD's perspective (D2.4 finalize needs the hash).
|
|
194
|
+
return { clientSubmissionKey: prepared.clientSubmissionKey, status: 'submission-ambiguous', reason: 'resource responded successfully but no transaction hash could be parsed from the settlement response' };
|
|
195
|
+
}
|
|
196
|
+
return { clientSubmissionKey: prepared.clientSubmissionKey, status: 'transaction-known', transactionHash, providerReference: this.id };
|
|
197
|
+
}
|
|
198
|
+
async resume(prepared, priorOutcome) {
|
|
199
|
+
// The only safe resume this adapter can do: if a transaction hash is
|
|
200
|
+
// ALREADY known (from a prior submit() that got a response before some
|
|
201
|
+
// later step failed), independently re-confirm it exists on-chain --
|
|
202
|
+
// never guess a new one, never resubmit.
|
|
203
|
+
if (priorOutcome?.status === 'transaction-known') {
|
|
204
|
+
const client = this.injectedPublicClient ?? createPublicClient({ chain: base, transport: http(this.rpcUrl) });
|
|
205
|
+
try {
|
|
206
|
+
await client.getTransactionReceipt({ hash: priorOutcome.transactionHash });
|
|
207
|
+
return priorOutcome;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Not found (yet, or ever) -- still don't fabricate a different
|
|
211
|
+
// outcome; report ambiguous so the caller keeps trying, mirroring
|
|
212
|
+
// observeTransaction's own "not-found is not the same as failed".
|
|
213
|
+
return { clientSubmissionKey: prepared.clientSubmissionKey, status: 'submission-ambiguous', reason: 'previously reported transaction hash was not found on Base mainnet (may still be propagating)' };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
clientSubmissionKey: prepared.clientSubmissionKey,
|
|
218
|
+
status: 'manual-recovery-required',
|
|
219
|
+
reason: 'this executor cannot independently identify a submitted-but-unconfirmed x402 payment authorization -- check the merchant/your wallet history manually before retrying',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -22,31 +22,29 @@
|
|
|
22
22
|
* build a payment header, or retry a request by hand.
|
|
23
23
|
*/
|
|
24
24
|
import type { Account } from 'viem';
|
|
25
|
+
import { type AttestationKeyRecord, type AttestationVerificationResult, type Signed, type VerifyAttestationOptions } from './verification.js';
|
|
26
|
+
export * from './verification.js';
|
|
25
27
|
export interface OnchainDiligenceOptions {
|
|
26
28
|
/** A viem account used to sign/settle payments (e.g. privateKeyToAccount). */
|
|
27
29
|
account: Account;
|
|
28
30
|
/** Base URL of the API. Defaults to production. */
|
|
29
31
|
baseUrl?: string;
|
|
32
|
+
/** Expected issuer for versioned attestations. Defaults to the production issuer. */
|
|
33
|
+
expectedAttestationIssuer?: string;
|
|
30
34
|
}
|
|
31
|
-
/**
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export interface VerifyResult {
|
|
45
|
-
valid: boolean;
|
|
46
|
-
/** The key_id the signature was checked against, when available. */
|
|
47
|
-
keyId?: string;
|
|
48
|
-
/** Human-readable reason when `valid` is false. */
|
|
49
|
-
reason?: string;
|
|
35
|
+
/** @deprecated Use AttestationVerificationResult and inspect its state. */
|
|
36
|
+
export type VerifyResult = AttestationVerificationResult;
|
|
37
|
+
export interface OnlineAttestationVerificationOptions extends VerifyAttestationOptions {
|
|
38
|
+
baseUrl?: string;
|
|
39
|
+
fetch?: typeof globalThis.fetch;
|
|
40
|
+
/**
|
|
41
|
+
* Explicit policy decision to trust exact key records delivered by the
|
|
42
|
+
* configured HTTPS issuer registry. Without this, valid bytes remain
|
|
43
|
+
* UNVERIFIABLE for publisher identity.
|
|
44
|
+
*/
|
|
45
|
+
trustRegistry?: boolean | ((record: AttestationKeyRecord, context: {
|
|
46
|
+
baseUrl: string;
|
|
47
|
+
}) => boolean | Promise<boolean>);
|
|
50
48
|
}
|
|
51
49
|
/** One address's outcome within a re-screen batch. */
|
|
52
50
|
export interface RescreenItem {
|
|
@@ -168,10 +166,18 @@ export declare class OnchainDiligenceError extends Error {
|
|
|
168
166
|
status: number;
|
|
169
167
|
constructor(status: number, message: string);
|
|
170
168
|
}
|
|
169
|
+
export declare function resolveAttestationKeyOnline(keyId: string, options?: {
|
|
170
|
+
baseUrl?: string;
|
|
171
|
+
fetch?: typeof globalThis.fetch;
|
|
172
|
+
}): Promise<AttestationKeyRecord>;
|
|
173
|
+
/** Validate and return the complete signed envelope required by `/anchor`. */
|
|
174
|
+
export declare function buildAnchorRequest(envelope: Signed<unknown>): Signed<unknown>;
|
|
175
|
+
/** Explicit online-discovery wrapper around the zero-network verifier core. */
|
|
176
|
+
export declare function verifyAttestationOnline(signed: unknown, options?: OnlineAttestationVerificationOptions): Promise<AttestationVerificationResult>;
|
|
171
177
|
export declare class OnchainDiligence {
|
|
172
178
|
private readonly baseUrl;
|
|
179
|
+
private readonly expectedAttestationIssuer;
|
|
173
180
|
private readonly fetch;
|
|
174
|
-
private attestationKeyPem;
|
|
175
181
|
constructor(opts: OnchainDiligenceOptions);
|
|
176
182
|
private get;
|
|
177
183
|
private post;
|
|
@@ -207,8 +213,8 @@ export declare class OnchainDiligence {
|
|
|
207
213
|
* bad address never sinks the whole batch.
|
|
208
214
|
*/
|
|
209
215
|
rescreen(addresses: string[], opts?: RescreenOptions): Promise<RescreenReport>;
|
|
210
|
-
/** Anchor
|
|
211
|
-
anchor(
|
|
216
|
+
/** Anchor a complete authentic attestation envelope on Tempo (paid). */
|
|
217
|
+
anchor(envelope: Signed<unknown>): Promise<Signed<AnchorResult>>;
|
|
212
218
|
/** Check whether an attestation has been anchored on-chain (free). */
|
|
213
219
|
anchored(signature: string): Promise<AnchorStatus>;
|
|
214
220
|
/** Service health (free). */
|
|
@@ -219,20 +225,18 @@ export declare class OnchainDiligence {
|
|
|
219
225
|
}>;
|
|
220
226
|
/**
|
|
221
227
|
* Verify a signed attestation locally. Fetches the server's published
|
|
222
|
-
* Ed25519
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* this SDK.
|
|
228
|
+
* exact Ed25519 key identified by `key_id` (cached), then verifies either
|
|
229
|
+
* the domain-separated RFC 8785 version 2 input or the legacy version 1
|
|
230
|
+
* JSON.stringify input. Revoked/compromised keys remain cryptographically
|
|
231
|
+
* checkable but return `valid: false` and `trusted: false`.
|
|
227
232
|
*
|
|
228
233
|
* Uses WebCrypto (`globalThis.crypto.subtle`) so it runs dependency-free in
|
|
229
234
|
* Node 18+, edge runtimes, and modern browsers.
|
|
230
235
|
*/
|
|
231
|
-
verifyAttestation(signed: Signed<unknown>): Promise<VerifyResult>;
|
|
232
236
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* exposing the PEM under a common field name.
|
|
237
|
+
* Compatibility online wrapper. Prefer the standalone zero-network
|
|
238
|
+
* verifyAttestationOffline() with caller-supplied trust material.
|
|
236
239
|
*/
|
|
237
|
-
|
|
240
|
+
verifyAttestation(signed: Signed<unknown>): Promise<VerifyResult>;
|
|
241
|
+
verifyAttestationOnline(signed: Signed<unknown>, options?: Omit<OnlineAttestationVerificationOptions, 'baseUrl' | 'fetch'>): Promise<AttestationVerificationResult>;
|
|
238
242
|
}
|