@armory-sh/base 0.2.27-alpha.23.76 → 0.2.27-alpha.23.78

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.
@@ -0,0 +1,201 @@
1
+ import type {
2
+ PaymentPayload,
3
+ PaymentRequirements,
4
+ SettlementResponse,
5
+ VerifyResponse,
6
+ } from "./types/x402";
7
+ import { isExactEvmPayload, isPaymentPayload } from "./types/x402";
8
+ import { decodeBase64ToUtf8 } from "./utils/base64";
9
+
10
+ export interface FacilitatorClientConfig {
11
+ url: string;
12
+ createHeaders?: () =>
13
+ | Record<string, string>
14
+ | Promise<Record<string, string>>;
15
+ }
16
+
17
+ const DEFAULT_FACILITATOR_URL = "https://facilitator.payai.network";
18
+
19
+ function toJsonSafe(data: object): object {
20
+ function convert(value: unknown): unknown {
21
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
22
+ return Object.fromEntries(
23
+ Object.entries(value).map(([key, val]) => [key, convert(val)]),
24
+ );
25
+ }
26
+ if (Array.isArray(value)) {
27
+ return value.map(convert);
28
+ }
29
+ if (typeof value === "bigint") {
30
+ return value.toString();
31
+ }
32
+ return value;
33
+ }
34
+ return convert(data) as object;
35
+ }
36
+
37
+ function resolveUrl(config?: FacilitatorClientConfig): string {
38
+ return config?.url ?? DEFAULT_FACILITATOR_URL;
39
+ }
40
+
41
+ async function resolveHeaders(
42
+ config?: FacilitatorClientConfig,
43
+ ): Promise<Record<string, string>> {
44
+ const base: Record<string, string> = { "Content-Type": "application/json" };
45
+ if (!config?.createHeaders) return base;
46
+ const extra = await config.createHeaders();
47
+ return { ...base, ...extra };
48
+ }
49
+
50
+ export async function verifyPayment(
51
+ payload: PaymentPayload,
52
+ requirements: PaymentRequirements,
53
+ config?: FacilitatorClientConfig,
54
+ ): Promise<VerifyResponse> {
55
+ const url = resolveUrl(config);
56
+ const headers = await resolveHeaders(config);
57
+
58
+ const response = await fetch(`${url}/verify`, {
59
+ method: "POST",
60
+ headers,
61
+ body: JSON.stringify({
62
+ paymentPayload: toJsonSafe(payload),
63
+ paymentRequirements: toJsonSafe(requirements),
64
+ }),
65
+ });
66
+
67
+ if (response.status !== 200) {
68
+ const text = await response.text().catch(() => response.statusText);
69
+ throw new Error(`Facilitator verify failed: ${response.status} ${text}`);
70
+ }
71
+
72
+ return (await response.json()) as VerifyResponse;
73
+ }
74
+
75
+ export async function settlePayment(
76
+ payload: PaymentPayload,
77
+ requirements: PaymentRequirements,
78
+ config?: FacilitatorClientConfig,
79
+ ): Promise<SettlementResponse> {
80
+ const url = resolveUrl(config);
81
+ const headers = await resolveHeaders(config);
82
+
83
+ const response = await fetch(`${url}/settle`, {
84
+ method: "POST",
85
+ headers,
86
+ body: JSON.stringify({
87
+ paymentPayload: toJsonSafe(payload),
88
+ paymentRequirements: toJsonSafe(requirements),
89
+ }),
90
+ });
91
+
92
+ if (response.status !== 200) {
93
+ const text = await response.text().catch(() => response.statusText);
94
+ throw new Error(`Facilitator settle failed: ${response.status} ${text}`);
95
+ }
96
+
97
+ return (await response.json()) as SettlementResponse;
98
+ }
99
+
100
+ export interface SupportedKind {
101
+ x402Version: number;
102
+ scheme: string;
103
+ network: string;
104
+ extra?: Record<string, unknown>;
105
+ }
106
+
107
+ export interface SupportedResponse {
108
+ kinds: SupportedKind[];
109
+ }
110
+
111
+ interface DecodePayloadDefaults {
112
+ accepted?: PaymentRequirements;
113
+ }
114
+
115
+ export async function getSupported(
116
+ config?: FacilitatorClientConfig,
117
+ ): Promise<SupportedResponse> {
118
+ const url = resolveUrl(config);
119
+ const headers = await resolveHeaders(config);
120
+
121
+ const response = await fetch(`${url}/supported`, {
122
+ method: "GET",
123
+ headers,
124
+ });
125
+
126
+ if (response.status !== 200) {
127
+ throw new Error(`Facilitator supported failed: ${response.statusText}`);
128
+ }
129
+
130
+ return (await response.json()) as SupportedResponse;
131
+ }
132
+
133
+ function isCompactV2Payload(
134
+ payload: unknown,
135
+ ): payload is { x402Version: number; payload: unknown } {
136
+ if (typeof payload !== "object" || payload === null) return false;
137
+ const record = payload as Record<string, unknown>;
138
+ return (
139
+ typeof record.x402Version === "number" &&
140
+ "payload" in record &&
141
+ !("accepted" in record)
142
+ );
143
+ }
144
+
145
+ export function decodePayloadHeader(
146
+ headerValue: string,
147
+ defaults?: DecodePayloadDefaults,
148
+ ): PaymentPayload {
149
+ if (headerValue.startsWith("{")) {
150
+ const parsed = JSON.parse(headerValue);
151
+ if (isPaymentPayload(parsed)) return parsed;
152
+ if (isCompactV2Payload(parsed)) {
153
+ const compact = parsed as { payload: PaymentPayload["payload"] };
154
+ if (!defaults?.accepted) {
155
+ throw new Error(
156
+ "Invalid payment payload: missing 'accepted' field and no defaults provided",
157
+ );
158
+ }
159
+ return {
160
+ x402Version: 2,
161
+ accepted: defaults.accepted,
162
+ payload: compact.payload,
163
+ };
164
+ }
165
+ throw new Error("Invalid payment payload: unrecognized format");
166
+ }
167
+
168
+ const normalized = headerValue
169
+ .replace(/-/g, "+")
170
+ .replace(/_/g, "/")
171
+ .padEnd(Math.ceil(headerValue.length / 4) * 4, "=");
172
+
173
+ try {
174
+ const decoded = JSON.parse(decodeBase64ToUtf8(normalized));
175
+ if (isPaymentPayload(decoded)) return decoded;
176
+ if (isCompactV2Payload(decoded)) {
177
+ const compact = decoded as { payload: PaymentPayload["payload"] };
178
+ if (!defaults?.accepted) {
179
+ throw new Error(
180
+ "Invalid payment payload: missing 'accepted' field and no defaults provided",
181
+ );
182
+ }
183
+ return {
184
+ x402Version: 2,
185
+ accepted: defaults.accepted,
186
+ payload: compact.payload,
187
+ };
188
+ }
189
+ throw new Error("Invalid payment payload: unrecognized format");
190
+ } catch {
191
+ throw new Error("Invalid payment payload: failed to decode");
192
+ }
193
+ }
194
+
195
+ export function extractPayerAddress(payload: PaymentPayload): string {
196
+ if (isExactEvmPayload(payload.payload)) {
197
+ return payload.payload.authorization.from;
198
+ }
199
+
200
+ throw new Error("Unable to extract payer address from payload");
201
+ }
@@ -0,0 +1,256 @@
1
+ import type {
2
+ NetworkId,
3
+ ResolvedNetwork,
4
+ ResolvedToken,
5
+ TokenId,
6
+ ValidationError,
7
+ } from "./types/api";
8
+ import { getNetworkConfig } from "./types/networks";
9
+ import type { Address, PaymentRequirementsV2 } from "./types/v2";
10
+ import { toAtomicUnits } from "./utils/x402";
11
+ import {
12
+ normalizeNetworkName,
13
+ resolveNetwork,
14
+ resolveToken,
15
+ } from "./validation";
16
+
17
+ export interface PaymentConfig {
18
+ payTo: Address | string;
19
+ chains?: NetworkId[];
20
+ chain?: NetworkId;
21
+ tokens?: TokenId[];
22
+ token?: TokenId;
23
+ amount?: string;
24
+ maxTimeoutSeconds?: number;
25
+
26
+ payToByChain?: Record<string, Address | string>;
27
+ payToByToken?: Record<string, Record<string, Address | string>>;
28
+
29
+ facilitatorUrl?: string;
30
+ facilitatorUrlByChain?: Record<string, string>;
31
+ facilitatorUrlByToken?: Record<string, Record<string, string>>;
32
+ }
33
+
34
+ export interface ResolvedRequirementsConfig {
35
+ requirements: PaymentRequirementsV2[];
36
+ error?: ValidationError;
37
+ }
38
+
39
+ const DEFAULT_NETWORKS: NetworkId[] = [
40
+ "ethereum",
41
+ "base",
42
+ "base-sepolia",
43
+ "skale-base",
44
+ "skale-base-sepolia",
45
+ "ethereum-sepolia",
46
+ ];
47
+
48
+ const DEFAULT_TOKENS: TokenId[] = ["usdc"];
49
+
50
+ const isValidationError = (value: unknown): value is ValidationError => {
51
+ return typeof value === "object" && value !== null && "code" in value;
52
+ };
53
+
54
+ function resolvePayTo(
55
+ config: PaymentConfig,
56
+ network: ResolvedNetwork,
57
+ token: ResolvedToken,
58
+ ): Address | string {
59
+ const chainId = network.config.chainId;
60
+
61
+ if (config.payToByToken) {
62
+ for (const [chainKey, tokenMap] of Object.entries(config.payToByToken)) {
63
+ const resolvedChain = resolveNetwork(chainKey);
64
+ if (
65
+ !isValidationError(resolvedChain) &&
66
+ resolvedChain.config.chainId === chainId
67
+ ) {
68
+ for (const [tokenKey, address] of Object.entries(tokenMap)) {
69
+ const resolvedToken = resolveToken(tokenKey, network);
70
+ if (
71
+ !isValidationError(resolvedToken) &&
72
+ resolvedToken.config.contractAddress.toLowerCase() ===
73
+ token.config.contractAddress.toLowerCase()
74
+ ) {
75
+ return address;
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ if (config.payToByChain) {
83
+ for (const [chainKey, address] of Object.entries(config.payToByChain)) {
84
+ const resolvedChain = resolveNetwork(chainKey);
85
+ if (
86
+ !isValidationError(resolvedChain) &&
87
+ resolvedChain.config.chainId === chainId
88
+ ) {
89
+ return address;
90
+ }
91
+ }
92
+ }
93
+
94
+ return config.payTo;
95
+ }
96
+
97
+ function resolveFacilitatorUrl(
98
+ config: PaymentConfig,
99
+ network: ResolvedNetwork,
100
+ token: ResolvedToken,
101
+ ): string | undefined {
102
+ const chainId = network.config.chainId;
103
+
104
+ if (config.facilitatorUrlByToken) {
105
+ for (const [chainKey, tokenMap] of Object.entries(
106
+ config.facilitatorUrlByToken,
107
+ )) {
108
+ const resolvedChain = resolveNetwork(chainKey);
109
+ if (
110
+ !isValidationError(resolvedChain) &&
111
+ resolvedChain.config.chainId === chainId
112
+ ) {
113
+ for (const [tokenKey, url] of Object.entries(tokenMap)) {
114
+ const resolvedToken = resolveToken(tokenKey, network);
115
+ if (
116
+ !isValidationError(resolvedToken) &&
117
+ resolvedToken.config.contractAddress.toLowerCase() ===
118
+ token.config.contractAddress.toLowerCase()
119
+ ) {
120
+ return url;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ if (config.facilitatorUrlByChain) {
128
+ for (const [chainKey, url] of Object.entries(
129
+ config.facilitatorUrlByChain,
130
+ )) {
131
+ const resolvedChain = resolveNetwork(chainKey);
132
+ if (
133
+ !isValidationError(resolvedChain) &&
134
+ resolvedChain.config.chainId === chainId
135
+ ) {
136
+ return url;
137
+ }
138
+ }
139
+ }
140
+
141
+ return config.facilitatorUrl;
142
+ }
143
+
144
+ function resolveNetworks(chainInputs: NetworkId[] | undefined): {
145
+ networks: ResolvedNetwork[];
146
+ error?: ValidationError;
147
+ } {
148
+ const resolvedNetworks: ResolvedNetwork[] = [];
149
+ const errors: string[] = [];
150
+
151
+ const networks = chainInputs?.length ? chainInputs : DEFAULT_NETWORKS;
152
+
153
+ for (const networkId of networks) {
154
+ const resolved = resolveNetwork(networkId);
155
+ if (isValidationError(resolved)) {
156
+ errors.push(`Network "${networkId}": ${resolved.message}`);
157
+ } else {
158
+ resolvedNetworks.push(resolved);
159
+ }
160
+ }
161
+
162
+ if (errors.length > 0) {
163
+ return {
164
+ networks: resolvedNetworks,
165
+ error: {
166
+ code: "VALIDATION_FAILED",
167
+ message: errors.join("; "),
168
+ },
169
+ };
170
+ }
171
+
172
+ return { networks: resolvedNetworks };
173
+ }
174
+
175
+ export function createPaymentRequirements(
176
+ config: PaymentConfig,
177
+ ): ResolvedRequirementsConfig {
178
+ const {
179
+ payTo,
180
+ chains,
181
+ chain,
182
+ tokens,
183
+ token,
184
+ amount = "1.0",
185
+ maxTimeoutSeconds = 300,
186
+ } = config;
187
+
188
+ const chainInputs = chain ? [chain] : chains;
189
+ const tokenInputs = token ? [token] : tokens;
190
+
191
+ const { networks, error: networksError } = resolveNetworks(chainInputs);
192
+ if (networksError) {
193
+ return { requirements: [], error: networksError };
194
+ }
195
+
196
+ const requirements: PaymentRequirementsV2[] = [];
197
+
198
+ for (const network of networks) {
199
+ const tokensToResolve = tokenInputs?.length ? tokenInputs : DEFAULT_TOKENS;
200
+
201
+ for (const tokenId of tokensToResolve) {
202
+ const resolvedToken = resolveToken(tokenId, network);
203
+ if (isValidationError(resolvedToken)) {
204
+ continue;
205
+ }
206
+
207
+ const atomicAmount = toAtomicUnits(amount);
208
+ const tokenConfig = resolvedToken.config;
209
+
210
+ const resolvedPayTo = resolvePayTo(config, network, resolvedToken);
211
+ const resolvedFacilitatorUrl = resolveFacilitatorUrl(
212
+ config,
213
+ network,
214
+ resolvedToken,
215
+ );
216
+
217
+ const requirement: PaymentRequirementsV2 = {
218
+ scheme: "exact",
219
+ network: network.caip2,
220
+ amount: atomicAmount,
221
+ asset: tokenConfig.contractAddress,
222
+ payTo: resolvedPayTo as `0x${string}`,
223
+ maxTimeoutSeconds,
224
+ extra: {
225
+ name: tokenConfig.name,
226
+ version: tokenConfig.version,
227
+ },
228
+ };
229
+
230
+ requirements.push(requirement);
231
+ }
232
+ }
233
+
234
+ if (requirements.length === 0) {
235
+ return {
236
+ requirements: [],
237
+ error: {
238
+ code: "VALIDATION_FAILED",
239
+ message: "No valid network/token combinations found",
240
+ },
241
+ };
242
+ }
243
+
244
+ return { requirements };
245
+ }
246
+
247
+ export function findRequirementByNetwork(
248
+ requirements: PaymentRequirementsV2[],
249
+ network: string,
250
+ ): PaymentRequirementsV2 | undefined {
251
+ const normalized = normalizeNetworkName(network);
252
+ const netConfig = getNetworkConfig(normalized);
253
+ if (!netConfig) return undefined;
254
+
255
+ return requirements.find((r) => r.network === netConfig.caip2Id);
256
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * X402 Protocol Utilities (V2 Only)
3
+ *
4
+ * Shared protocol utilities for x402 client implementations.
5
+ * These functions are protocol-level and don't depend on wallet libraries.
6
+ */
7
+
8
+ import { V2_HEADERS } from "./types/v2";
9
+ import { decodeBase64ToUtf8, normalizeBase64Url } from "./utils/base64";
10
+
11
+ /**
12
+ * Parse JSON or Base64-encoded JSON
13
+ * Handles both raw JSON and Base64URL-encoded JSON
14
+ */
15
+ export function parseJsonOrBase64(value: string): unknown {
16
+ try {
17
+ return JSON.parse(value);
18
+ } catch {
19
+ const normalized = normalizeBase64Url(value);
20
+ return JSON.parse(decodeBase64ToUtf8(normalized));
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Detect x402 protocol version from response
26
+ * V2-only: Always returns 2
27
+ */
28
+ export function detectX402Version(_response: Response): 2 {
29
+ return 2;
30
+ }
31
+
32
+ /**
33
+ * Get payment header name for protocol version
34
+ * V2-only: Returns PAYMENT-SIGNATURE header name
35
+ */
36
+ export function getPaymentHeaderName(_version: 2): string {
37
+ return V2_HEADERS.PAYMENT_SIGNATURE;
38
+ }
39
+
40
+ /**
41
+ * Generate a timestamp-based nonce as hex string
42
+ * Matches Coinbase SDK format: "0x" + 64 hex characters
43
+ * Uses timestamp for reproducibility (vs random)
44
+ */
45
+ export function generateNonce(): `0x${string}` {
46
+ const now = Math.floor(Date.now() / 1000);
47
+ return `0x${(now * 1000).toString(16).padStart(64, "0")}` as `0x${string}`;
48
+ }
49
+
50
+ /**
51
+ * Calculate expiry timestamp
52
+ * @param expirySeconds - Seconds from now (default: 3600 = 1 hour)
53
+ * @returns Unix timestamp
54
+ */
55
+ export function calculateValidBefore(expirySeconds: number = 3600): number {
56
+ return Math.floor(Date.now() / 1000) + expirySeconds;
57
+ }