@armory-sh/base 0.2.28 → 0.2.29

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/src/eip712.ts ADDED
@@ -0,0 +1,108 @@
1
+ export type TypedDataDomain = {
2
+ name?: string;
3
+ version?: string;
4
+ chainId?: number;
5
+ verifyingContract?: `0x${string}`;
6
+ salt?: `0x${string}`;
7
+ };
8
+
9
+ export interface EIP712Domain {
10
+ name: string;
11
+ version: string;
12
+ chainId: number;
13
+ verifyingContract: `0x${string}`;
14
+ }
15
+
16
+ export interface TransferWithAuthorization {
17
+ from: `0x${string}`;
18
+ to: `0x${string}`;
19
+ value: bigint;
20
+ validAfter: bigint;
21
+ validBefore: bigint;
22
+ nonce: `0x${string}`;
23
+ }
24
+
25
+ export type TransferWithAuthorizationRecord = TransferWithAuthorization &
26
+ Record<string, unknown>;
27
+
28
+ export type TypedDataField = { name: string; type: string };
29
+
30
+ export type EIP712Types = {
31
+ TransferWithAuthorization: TypedDataField[];
32
+ };
33
+
34
+ export const EIP712_TYPES = {
35
+ TransferWithAuthorization: [
36
+ { name: "from", type: "address" },
37
+ { name: "to", type: "address" },
38
+ { name: "value", type: "uint256" },
39
+ { name: "validAfter", type: "uint256" },
40
+ { name: "validBefore", type: "uint256" },
41
+ { name: "nonce", type: "bytes32" },
42
+ ] as const,
43
+ } as const satisfies EIP712Types;
44
+
45
+ export const USDC_DOMAIN = {
46
+ NAME: "USD Coin",
47
+ VERSION: "2",
48
+ } as const;
49
+
50
+ export const createEIP712Domain = (
51
+ chainId: number,
52
+ contractAddress: `0x${string}`,
53
+ ): EIP712Domain => ({
54
+ name: USDC_DOMAIN.NAME,
55
+ version: USDC_DOMAIN.VERSION,
56
+ chainId,
57
+ verifyingContract: contractAddress,
58
+ });
59
+
60
+ export const createTransferWithAuthorization = (
61
+ params: Omit<
62
+ TransferWithAuthorization,
63
+ "value" | "validAfter" | "validBefore" | "nonce"
64
+ > & {
65
+ value: bigint | number;
66
+ validAfter: bigint | number;
67
+ validBefore: bigint | number;
68
+ nonce: `0x${string}`;
69
+ },
70
+ ): TransferWithAuthorizationRecord => ({
71
+ from: params.from,
72
+ to: params.to,
73
+ value: BigInt(params.value),
74
+ validAfter: BigInt(params.validAfter),
75
+ validBefore: BigInt(params.validBefore),
76
+ nonce: params.nonce,
77
+ });
78
+
79
+ const isAddress = (value: string): boolean => /^0x[a-fA-F0-9]{40}$/.test(value);
80
+
81
+ const isBytes32 = (value: string): boolean => /^0x[a-fA-F0-9]{64}$/.test(value);
82
+
83
+ export const validateTransferWithAuthorization = (
84
+ message: TransferWithAuthorization,
85
+ ): boolean => {
86
+ if (!isAddress(message.from))
87
+ throw new Error(`Invalid "from" address: ${message.from}`);
88
+ if (!isAddress(message.to))
89
+ throw new Error(`Invalid "to" address: ${message.to}`);
90
+ if (message.value < 0n)
91
+ throw new Error(`"value" must be non-negative: ${message.value}`);
92
+ if (message.validAfter < 0n)
93
+ throw new Error(`"validAfter" must be non-negative: ${message.validAfter}`);
94
+ if (message.validBefore < 0n)
95
+ throw new Error(
96
+ `"validBefore" must be non-negative: ${message.validBefore}`,
97
+ );
98
+ if (message.validAfter >= message.validBefore) {
99
+ throw new Error(
100
+ `"validAfter" (${message.validAfter}) must be before "validBefore" (${message.validBefore})`,
101
+ );
102
+ }
103
+ if (!isBytes32(message.nonce))
104
+ throw new Error(
105
+ `"nonce" must be a valid bytes32 hex string: ${message.nonce}`,
106
+ );
107
+ return true;
108
+ };
@@ -0,0 +1,205 @@
1
+ /**
2
+ * X402 Protocol Encoding - V2 Only (Coinbase Compatible)
3
+ *
4
+ * Payment payloads are JSON-encoded for transport in HTTP headers.
5
+ * Matches the Coinbase x402 v2 SDK format.
6
+ */
7
+
8
+ import { V2_HEADERS } from "../types/v2";
9
+ import type {
10
+ PaymentPayload,
11
+ PaymentRequirements,
12
+ SettlementResponse,
13
+ X402Response,
14
+ } from "../types/x402";
15
+ import { isPaymentPayload } from "../types/x402";
16
+ import {
17
+ decodeBase64ToUtf8,
18
+ encodeUtf8ToBase64,
19
+ normalizeBase64Url,
20
+ toBase64Url,
21
+ } from "../utils/base64";
22
+
23
+ /**
24
+ * Safe Base64 encode (URL-safe, no padding)
25
+ */
26
+ export function safeBase64Encode(str: string): string {
27
+ return toBase64Url(encodeUtf8ToBase64(str));
28
+ }
29
+
30
+ /**
31
+ * Safe Base64 decode (handles URL-safe format)
32
+ */
33
+ export function safeBase64Decode(str: string): string {
34
+ return decodeBase64ToUtf8(normalizeBase64Url(str));
35
+ }
36
+
37
+ /**
38
+ * Encode payment payload to JSON string
39
+ */
40
+ export function encodePayment(payload: PaymentPayload): string {
41
+ if (!isPaymentPayload(payload)) {
42
+ throw new Error("Invalid payment payload format");
43
+ }
44
+ // Convert any bigint values to strings for JSON serialization
45
+ const safePayload = JSON.parse(
46
+ JSON.stringify(payload, (_, value) => {
47
+ if (typeof value === "bigint") {
48
+ return value.toString();
49
+ }
50
+ return value;
51
+ }),
52
+ );
53
+
54
+ return safeBase64Encode(JSON.stringify(safePayload));
55
+ }
56
+
57
+ /**
58
+ * Decode payment payload from JSON string
59
+ */
60
+ export function decodePayment(encoded: string): PaymentPayload {
61
+ let parsed: unknown;
62
+ try {
63
+ parsed = JSON.parse(encoded);
64
+ } catch {
65
+ const decoded = safeBase64Decode(encoded);
66
+ parsed = JSON.parse(decoded);
67
+ }
68
+
69
+ if (!isPaymentPayload(parsed)) {
70
+ throw new Error("Invalid payment payload format");
71
+ }
72
+
73
+ return parsed;
74
+ }
75
+
76
+ /**
77
+ * Encode settlement response to JSON string
78
+ */
79
+ export function encodeSettlementResponse(response: SettlementResponse): string {
80
+ const safeResponse = JSON.parse(
81
+ JSON.stringify(response, (_, value) => {
82
+ if (typeof value === "bigint") {
83
+ return value.toString();
84
+ }
85
+ return value;
86
+ }),
87
+ );
88
+
89
+ return safeBase64Encode(JSON.stringify(safeResponse));
90
+ }
91
+
92
+ /**
93
+ * Decode settlement response from JSON string
94
+ */
95
+ export function decodeSettlementResponse(encoded: string): SettlementResponse {
96
+ try {
97
+ return JSON.parse(encoded);
98
+ } catch {
99
+ const decoded = safeBase64Decode(encoded);
100
+ return JSON.parse(decoded);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Encode X402 error response
106
+ */
107
+ export function encodeX402Response(response: X402Response): string {
108
+ return safeBase64Encode(JSON.stringify(response));
109
+ }
110
+
111
+ /**
112
+ * Decode X402 error response
113
+ */
114
+ export function decodeX402Response(encoded: string): X402Response {
115
+ try {
116
+ return JSON.parse(encoded);
117
+ } catch {
118
+ const decoded = safeBase64Decode(encoded);
119
+ return JSON.parse(decoded);
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Detect payment version from headers
125
+ * Returns null if no payment header found
126
+ *
127
+ * V2-only: checks for PAYMENT-SIGNATURE header
128
+ */
129
+ export function detectPaymentVersion(headers: Headers): number | null {
130
+ if (headers.has("PAYMENT-SIGNATURE")) {
131
+ return 2;
132
+ }
133
+
134
+ return null;
135
+ }
136
+
137
+ /**
138
+ * Extract payment from V2 headers
139
+ */
140
+ export function extractPaymentFromHeaders(
141
+ headers: Headers,
142
+ ): PaymentPayload | null {
143
+ const encoded = headers.get("PAYMENT-SIGNATURE");
144
+ if (!encoded) return null;
145
+
146
+ try {
147
+ return decodePayment(encoded);
148
+ } catch {
149
+ return null;
150
+ }
151
+ }
152
+
153
+ export interface PaymentRequiredOptions {
154
+ error?: string;
155
+ resource?: {
156
+ url: string;
157
+ description?: string;
158
+ mimeType?: string;
159
+ };
160
+ extensions?: Record<string, unknown>;
161
+ }
162
+
163
+ /**
164
+ * Create payment required headers (V2 format)
165
+ */
166
+ export function createPaymentRequiredHeaders(
167
+ requirements: PaymentRequirements | PaymentRequirements[],
168
+ options?: PaymentRequiredOptions,
169
+ ): Record<string, string> {
170
+ const accepts = Array.isArray(requirements) ? requirements : [requirements];
171
+
172
+ const response = {
173
+ x402Version: 2,
174
+ error: options?.error ?? "Payment required",
175
+ resource: options?.resource ?? { url: "", mimeType: "application/json" },
176
+ accepts,
177
+ extensions: options?.extensions ?? {},
178
+ };
179
+
180
+ return {
181
+ [V2_HEADERS.PAYMENT_REQUIRED]: safeBase64Encode(JSON.stringify(response)),
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Create settlement response headers (V2 format)
187
+ */
188
+ export function createSettlementHeaders(
189
+ settlement: SettlementResponse,
190
+ ): Record<string, string> {
191
+ return {
192
+ [V2_HEADERS.PAYMENT_RESPONSE]: encodeSettlementResponse(settlement),
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Decode settlement from headers (V2 only)
198
+ */
199
+ export function decodeSettlement(headers: Headers): SettlementResponse {
200
+ const encoded = headers.get(V2_HEADERS.PAYMENT_RESPONSE);
201
+ if (!encoded) {
202
+ throw new Error(`No ${V2_HEADERS.PAYMENT_RESPONSE} header found`);
203
+ }
204
+ return decodeSettlementResponse(encoded);
205
+ }
@@ -0,0 +1,98 @@
1
+ import type { PaymentPayloadV2, SettlementResponseV2 } from "./types/v2";
2
+ import { V2_HEADERS } from "./types/v2";
3
+ import {
4
+ decodeBase64ToUtf8,
5
+ encodeUtf8ToBase64,
6
+ normalizeBase64Url,
7
+ toBase64Url,
8
+ } from "./utils/base64";
9
+
10
+ export { V2_HEADERS };
11
+
12
+ export type PaymentPayload = PaymentPayloadV2;
13
+ export type SettlementResponse = SettlementResponseV2;
14
+
15
+ /**
16
+ * Safe Base64 decode (handles URL-safe format)
17
+ */
18
+ function safeBase64Decode(str: string): string {
19
+ return decodeBase64ToUtf8(normalizeBase64Url(str));
20
+ }
21
+
22
+ /**
23
+ * Safe Base64 encode (URL-safe, no padding)
24
+ */
25
+ function safeBase64Encode(str: string): string {
26
+ return toBase64Url(encodeUtf8ToBase64(str));
27
+ }
28
+
29
+ /**
30
+ * JSON encode/decode for V2 (Base64URL-encoded JSON for x402 compatibility)
31
+ */
32
+ const jsonEncode = (data: unknown): string => JSON.stringify(data);
33
+
34
+ const base64JsonEncode = (data: unknown): string =>
35
+ safeBase64Encode(JSON.stringify(data));
36
+
37
+ const base64JsonDecode = <T>(encoded: string): T =>
38
+ JSON.parse(safeBase64Decode(encoded)) as T;
39
+
40
+ export const encodePaymentV2 = (payload: PaymentPayloadV2): string =>
41
+ base64JsonEncode(payload);
42
+ export const decodePaymentV2 = (encoded: string): PaymentPayloadV2 =>
43
+ base64JsonDecode(encoded);
44
+
45
+ export const encodeSettlementV2 = (response: SettlementResponseV2): string =>
46
+ base64JsonEncode(response);
47
+ export const decodeSettlementV2 = (encoded: string): SettlementResponseV2 =>
48
+ base64JsonDecode(encoded);
49
+
50
+ /**
51
+ * Always returns 2 for V2-only mode
52
+ */
53
+ export const detectPaymentVersion = (headers: Headers): 2 | null => {
54
+ if (headers.has(V2_HEADERS.PAYMENT_SIGNATURE)) return 2;
55
+ return null;
56
+ };
57
+
58
+ /**
59
+ * Decode payment from headers (V2 only)
60
+ */
61
+ export const decodePayment = (headers: Headers): PaymentPayload => {
62
+ const encoded = headers.get(V2_HEADERS.PAYMENT_SIGNATURE);
63
+ if (!encoded) {
64
+ throw new Error(`No ${V2_HEADERS.PAYMENT_SIGNATURE} header found`);
65
+ }
66
+ return decodePaymentV2(encoded);
67
+ };
68
+
69
+ /**
70
+ * Decode settlement from headers (V2 only)
71
+ */
72
+ export const decodeSettlement = (headers: Headers): SettlementResponse => {
73
+ const v2Response = headers.get(V2_HEADERS.PAYMENT_RESPONSE);
74
+ if (v2Response) return decodeSettlementV2(v2Response);
75
+
76
+ throw new Error(`No ${V2_HEADERS.PAYMENT_RESPONSE} header found`);
77
+ };
78
+
79
+ /**
80
+ * Type guard for V2 payload
81
+ */
82
+ export const isPaymentV2 = (
83
+ payload: PaymentPayload,
84
+ ): payload is PaymentPayloadV2 =>
85
+ "x402Version" in payload &&
86
+ (payload as PaymentPayloadV2).x402Version === 2 &&
87
+ "accepted" in payload &&
88
+ "payload" in payload;
89
+
90
+ /**
91
+ * Type guard for V2 settlement
92
+ */
93
+ export const isSettlementV2 = (
94
+ response: SettlementResponse,
95
+ ): response is SettlementResponseV2 =>
96
+ "success" in response &&
97
+ typeof (response as SettlementResponseV2).success === "boolean" &&
98
+ "network" in response;
package/src/errors.ts ADDED
@@ -0,0 +1,23 @@
1
+ export class X402ClientError extends Error {
2
+ override readonly cause?: unknown;
3
+
4
+ constructor(message: string, cause?: unknown) {
5
+ super(message);
6
+ this.name = "X402ClientError";
7
+ this.cause = cause;
8
+ }
9
+ }
10
+
11
+ export class SigningError extends X402ClientError {
12
+ constructor(message: string, cause?: unknown) {
13
+ super(`Signing failed: ${message}`, cause);
14
+ this.name = "SigningError";
15
+ }
16
+ }
17
+
18
+ export class PaymentException extends X402ClientError {
19
+ constructor(message: string, cause?: unknown) {
20
+ super(`Payment failed: ${message}`, cause);
21
+ this.name = "PaymentException";
22
+ }
23
+ }