@armory-sh/base 0.2.28 → 0.2.30
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 +304 -28
- package/dist/client-hooks-runtime.d.ts +9 -0
- package/dist/encoding/x402.d.ts +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/facilitator-capabilities.d.ts +9 -0
- package/dist/index.d.ts +30 -22
- package/dist/index.js +1198 -650
- package/dist/payment-client.d.ts +1 -1
- package/dist/payment-requirements.d.ts +35 -0
- package/dist/protocol.d.ts +33 -0
- package/dist/types/api.d.ts +1 -1
- package/dist/types/hooks.d.ts +17 -1
- package/dist/types/protocol.d.ts +1 -1
- package/dist/types/wallet.d.ts +24 -0
- package/dist/utils/base64.d.ts +4 -0
- package/dist/utils/x402.d.ts +1 -1
- package/dist/validation.d.ts +1 -1
- package/package.json +15 -2
- package/src/abi/erc20.ts +84 -0
- package/src/client-hooks-runtime.ts +153 -0
- package/src/data/tokens.ts +199 -0
- package/src/eip712.ts +108 -0
- package/src/encoding/x402.ts +205 -0
- package/src/encoding.ts +98 -0
- package/src/errors.ts +23 -0
- package/src/facilitator-capabilities.ts +125 -0
- package/src/index.ts +330 -0
- package/src/payment-client.ts +201 -0
- package/src/payment-requirements.ts +354 -0
- package/src/protocol.ts +57 -0
- package/src/types/api.ts +304 -0
- package/src/types/hooks.ts +85 -0
- package/src/types/networks.ts +175 -0
- package/src/types/protocol.ts +182 -0
- package/src/types/v2.ts +282 -0
- package/src/types/wallet.ts +30 -0
- package/src/types/x402.ts +151 -0
- package/src/utils/base64.ts +48 -0
- package/src/utils/routes.ts +240 -0
- package/src/utils/utils/index.ts +7 -0
- package/src/utils/utils/mock-facilitator.ts +184 -0
- package/src/utils/x402.ts +147 -0
- package/src/validation.ts +654 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
export type RoutePattern = string;
|
|
2
|
+
export type RouteMatcher = (path: string) => boolean;
|
|
3
|
+
|
|
4
|
+
export interface RouteConfig<T = unknown> {
|
|
5
|
+
pattern: RoutePattern;
|
|
6
|
+
config: T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ParsedPattern {
|
|
10
|
+
segments: string[];
|
|
11
|
+
isWildcard: boolean;
|
|
12
|
+
isParametrized: boolean;
|
|
13
|
+
paramNames: string[];
|
|
14
|
+
priority: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PRIORITY_EXACT = 3;
|
|
18
|
+
const PRIORITY_PARAMETRIZED = 2;
|
|
19
|
+
const PRIORITY_WILDCARD = 1;
|
|
20
|
+
|
|
21
|
+
export function parseRoutePattern(pattern: string): ParsedPattern {
|
|
22
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
23
|
+
const segments = normalizedPattern.split("/").filter(Boolean);
|
|
24
|
+
|
|
25
|
+
let isWildcard = false;
|
|
26
|
+
let isParametrized = false;
|
|
27
|
+
const paramNames: string[] = [];
|
|
28
|
+
const seenParamNames = new Set<string>();
|
|
29
|
+
const recordParamName = (name: string) => {
|
|
30
|
+
if (!name) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (seenParamNames.has(name)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
seenParamNames.add(name);
|
|
37
|
+
paramNames.push(name);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
for (const segment of segments) {
|
|
41
|
+
if (segment === "*") {
|
|
42
|
+
isWildcard = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const hasWildcardToken = segment.includes("*");
|
|
47
|
+
|
|
48
|
+
if (segment.startsWith(":")) {
|
|
49
|
+
isParametrized = true;
|
|
50
|
+
const paramName = segment.replace(/\*+$/, "").slice(1);
|
|
51
|
+
recordParamName(paramName);
|
|
52
|
+
if (hasWildcardToken) {
|
|
53
|
+
isWildcard = true;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (hasWildcardToken) {
|
|
59
|
+
const parts = segment.split("*");
|
|
60
|
+
for (const part of parts) {
|
|
61
|
+
if (part.startsWith(":")) {
|
|
62
|
+
isParametrized = true;
|
|
63
|
+
recordParamName(part.slice(1));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
isWildcard = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let priority = PRIORITY_WILDCARD;
|
|
71
|
+
if (!isWildcard && !isParametrized) {
|
|
72
|
+
priority = PRIORITY_EXACT;
|
|
73
|
+
} else if (isParametrized && !isWildcard) {
|
|
74
|
+
priority = PRIORITY_PARAMETRIZED;
|
|
75
|
+
} else if (isParametrized && isWildcard) {
|
|
76
|
+
priority = PRIORITY_PARAMETRIZED;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { segments, isWildcard, isParametrized, paramNames, priority };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matchSegment(patternSegment: string, pathSegment: string): boolean {
|
|
83
|
+
if (patternSegment === "*") {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (patternSegment.startsWith(":")) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (patternSegment.includes("*")) {
|
|
92
|
+
const regex = new RegExp(
|
|
93
|
+
`^${patternSegment.replace(/\*/g, ".*").replace(/:/g, "")}$`,
|
|
94
|
+
);
|
|
95
|
+
return regex.test(pathSegment);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return patternSegment === pathSegment;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function matchWildcardPattern(
|
|
102
|
+
patternSegments: string[],
|
|
103
|
+
pathSegments: string[],
|
|
104
|
+
): boolean {
|
|
105
|
+
const requiredSegments = patternSegments.filter((s) => s !== "*");
|
|
106
|
+
|
|
107
|
+
if (requiredSegments.length > pathSegments.length) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < requiredSegments.length; i++) {
|
|
112
|
+
const patternIndex = patternSegments.indexOf(requiredSegments[i]);
|
|
113
|
+
if (pathSegments[patternIndex] !== requiredSegments[i].replace(/^:/, "")) {
|
|
114
|
+
if (!requiredSegments[i].startsWith(":") && requiredSegments[i] !== "*") {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function matchRoute(pattern: string, path: string): boolean {
|
|
124
|
+
const normalizedPattern = pattern.startsWith("/") ? pattern : `/${pattern}`;
|
|
125
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
126
|
+
|
|
127
|
+
if (normalizedPattern === normalizedPath) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const parsed = parseRoutePattern(normalizedPattern);
|
|
132
|
+
const patternSegments = parsed.segments;
|
|
133
|
+
const pathSegments = normalizedPath.split("/").filter(Boolean);
|
|
134
|
+
|
|
135
|
+
if (!parsed.isWildcard && patternSegments.length !== pathSegments.length) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (parsed.isWildcard && patternSegments.length > pathSegments.length + 1) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (parsed.isWildcard) {
|
|
144
|
+
return matchWildcardPattern(patternSegments, pathSegments);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (let i = 0; i < patternSegments.length; i++) {
|
|
148
|
+
if (!matchSegment(patternSegments[i], pathSegments[i])) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function findMatchingRoute<T>(
|
|
157
|
+
routes: RouteConfig<T>[],
|
|
158
|
+
path: string,
|
|
159
|
+
): RouteConfig<T> | null {
|
|
160
|
+
const matchingRoutes: Array<{
|
|
161
|
+
route: RouteConfig<T>;
|
|
162
|
+
parsed: ParsedPattern;
|
|
163
|
+
}> = [];
|
|
164
|
+
|
|
165
|
+
for (const route of routes) {
|
|
166
|
+
if (matchRoute(route.pattern, path)) {
|
|
167
|
+
const parsed = parseRoutePattern(route.pattern);
|
|
168
|
+
matchingRoutes.push({ route, parsed });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (matchingRoutes.length === 0) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
matchingRoutes.sort((a, b) => {
|
|
177
|
+
if (b.parsed.priority !== a.parsed.priority) {
|
|
178
|
+
return b.parsed.priority - a.parsed.priority;
|
|
179
|
+
}
|
|
180
|
+
if (b.parsed.segments.length !== a.parsed.segments.length) {
|
|
181
|
+
return b.parsed.segments.length - a.parsed.segments.length;
|
|
182
|
+
}
|
|
183
|
+
return b.route.pattern.length - a.route.pattern.length;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return matchingRoutes[0].route;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface RouteInputConfig {
|
|
190
|
+
route?: string;
|
|
191
|
+
routes?: string[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface RouteValidationError {
|
|
195
|
+
code: string;
|
|
196
|
+
message: string;
|
|
197
|
+
path?: string;
|
|
198
|
+
value?: unknown;
|
|
199
|
+
validOptions?: string[];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function containsWildcard(pattern: string): boolean {
|
|
203
|
+
return pattern.includes("*");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function validateRouteConfig(
|
|
207
|
+
config: RouteInputConfig,
|
|
208
|
+
): RouteValidationError | null {
|
|
209
|
+
const { route, routes } = config;
|
|
210
|
+
|
|
211
|
+
if (!route && !routes) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (route && routes) {
|
|
216
|
+
return {
|
|
217
|
+
code: "INVALID_ROUTE_CONFIG",
|
|
218
|
+
message:
|
|
219
|
+
"Cannot specify both 'route' and 'routes'. Use 'route' for a single exact path or 'routes' for multiple paths.",
|
|
220
|
+
path: "route",
|
|
221
|
+
value: { route, routes },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (route && containsWildcard(route)) {
|
|
226
|
+
return {
|
|
227
|
+
code: "INVALID_ROUTE_PATTERN",
|
|
228
|
+
message:
|
|
229
|
+
"Wildcard routes must use the routes array, not 'route'. Use 'routes: [\"/api/*\"]' instead of 'route: \"/api/*\"'.",
|
|
230
|
+
path: "route",
|
|
231
|
+
value: route,
|
|
232
|
+
validOptions: [
|
|
233
|
+
'routes: ["/api/*"]',
|
|
234
|
+
'routes: ["/api/users", "/api/posts"]',
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Facilitator for Testing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/// <reference types="bun-types" />
|
|
6
|
+
|
|
7
|
+
import type { PaymentPayload } from "@armory-sh/base";
|
|
8
|
+
import { serve } from "bun";
|
|
9
|
+
|
|
10
|
+
export interface FacilitatorVerifyRequest {
|
|
11
|
+
payload: PaymentPayload;
|
|
12
|
+
requirements: Record<string, unknown>;
|
|
13
|
+
options?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FacilitatorVerifyResponse {
|
|
17
|
+
success: boolean;
|
|
18
|
+
payerAddress?: string;
|
|
19
|
+
balance?: string;
|
|
20
|
+
requiredAmount?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FacilitatorSettleRequest {
|
|
25
|
+
payload: PaymentPayload;
|
|
26
|
+
requirements: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FacilitatorSettleResponse {
|
|
30
|
+
success: boolean;
|
|
31
|
+
txHash?: string;
|
|
32
|
+
error?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type AnyFacilitatorRequest =
|
|
36
|
+
| FacilitatorVerifyRequest
|
|
37
|
+
| FacilitatorSettleRequest;
|
|
38
|
+
export type AnyFacilitatorResponse =
|
|
39
|
+
| FacilitatorVerifyResponse
|
|
40
|
+
| FacilitatorSettleResponse;
|
|
41
|
+
|
|
42
|
+
export interface MockFacilitatorOptions {
|
|
43
|
+
port?: number;
|
|
44
|
+
alwaysVerify?: boolean;
|
|
45
|
+
alwaysSettle?: boolean;
|
|
46
|
+
verifyDelay?: number;
|
|
47
|
+
settleDelay?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let nextPort = 9100;
|
|
51
|
+
|
|
52
|
+
export const createMockFacilitator = async (
|
|
53
|
+
options: MockFacilitatorOptions = {},
|
|
54
|
+
): Promise<{ url: string; close: () => Promise<void> }> => {
|
|
55
|
+
const {
|
|
56
|
+
port = nextPort++,
|
|
57
|
+
alwaysVerify = true,
|
|
58
|
+
alwaysSettle = true,
|
|
59
|
+
verifyDelay = 0,
|
|
60
|
+
settleDelay = 0,
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
const server = serve({
|
|
64
|
+
port,
|
|
65
|
+
hostname: "127.0.0.1",
|
|
66
|
+
async fetch(req: Request) {
|
|
67
|
+
const url = new URL(req.url);
|
|
68
|
+
const path = url.pathname;
|
|
69
|
+
|
|
70
|
+
const corsHeaders = {
|
|
71
|
+
"Access-Control-Allow-Origin": "*",
|
|
72
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
73
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (req.method === "OPTIONS") {
|
|
77
|
+
return new Response(null, { status: 204, headers: corsHeaders });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (req.method !== "POST") {
|
|
81
|
+
return Response.json(
|
|
82
|
+
{ error: "Method not allowed" },
|
|
83
|
+
{ status: 405, headers: corsHeaders },
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (path === "/verify") {
|
|
88
|
+
const body = (await req.json()) as FacilitatorVerifyRequest;
|
|
89
|
+
|
|
90
|
+
if (verifyDelay > 0) {
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, verifyDelay));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const result: FacilitatorVerifyResponse = alwaysVerify
|
|
95
|
+
? {
|
|
96
|
+
success: true,
|
|
97
|
+
payerAddress: extractPayerAddress(body.payload),
|
|
98
|
+
balance: "10000000",
|
|
99
|
+
requiredAmount: "1000000",
|
|
100
|
+
}
|
|
101
|
+
: {
|
|
102
|
+
success: false,
|
|
103
|
+
error: "Verification failed (mock error)",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return Response.json(result, { headers: corsHeaders });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (path === "/settle") {
|
|
110
|
+
const body = (await req.json()) as FacilitatorSettleRequest;
|
|
111
|
+
|
|
112
|
+
if (settleDelay > 0) {
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, settleDelay));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const result: FacilitatorSettleResponse = alwaysSettle
|
|
117
|
+
? {
|
|
118
|
+
success: true,
|
|
119
|
+
txHash: `0x${"a".repeat(64)}`,
|
|
120
|
+
}
|
|
121
|
+
: {
|
|
122
|
+
success: false,
|
|
123
|
+
error: "Settlement failed (mock error)",
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return Response.json(result, { headers: corsHeaders });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return Response.json(
|
|
130
|
+
{ error: "Not found" },
|
|
131
|
+
{ status: 404, headers: corsHeaders },
|
|
132
|
+
);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
url: `http://127.0.0.1:${port}`,
|
|
140
|
+
close: async () => {
|
|
141
|
+
server.stop();
|
|
142
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
function extractPayerAddress(payload: PaymentPayload | unknown): string {
|
|
148
|
+
if (typeof payload === "string") {
|
|
149
|
+
try {
|
|
150
|
+
payload = JSON.parse(payload);
|
|
151
|
+
} catch {
|
|
152
|
+
return "0x0000000000000000000000000000000000000000000000";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
typeof payload === "object" &&
|
|
158
|
+
payload !== null &&
|
|
159
|
+
"x402Version" in payload
|
|
160
|
+
) {
|
|
161
|
+
const p = payload as Record<string, unknown>;
|
|
162
|
+
if ("payload" in p && typeof p.payload === "object" && p.payload !== null) {
|
|
163
|
+
const schemePayload = p.payload as Record<string, unknown>;
|
|
164
|
+
if (
|
|
165
|
+
"authorization" in schemePayload &&
|
|
166
|
+
typeof schemePayload.authorization === "object"
|
|
167
|
+
) {
|
|
168
|
+
const auth = schemePayload.authorization as Record<string, unknown>;
|
|
169
|
+
if ("from" in auth && typeof auth.from === "string") {
|
|
170
|
+
return auth.from;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (typeof payload === "object" && payload !== null && "from" in payload) {
|
|
177
|
+
const p = payload as Record<string, unknown>;
|
|
178
|
+
if (typeof p.from === "string") {
|
|
179
|
+
return p.from;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return "0x0000000000000000000000000000000000000000000000";
|
|
184
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* X402 Protocol Utilities - Coinbase Compatible
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for nonce generation, amount conversion, and other utilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { randomBytes } from "node:crypto";
|
|
8
|
+
import type { Address, Hex } from "../types/x402";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a random 32-byte nonce as hex string
|
|
12
|
+
* Matches Coinbase SDK format: "0x" + 64 hex characters
|
|
13
|
+
*/
|
|
14
|
+
export function createNonce(): Hex {
|
|
15
|
+
const bytes = randomBytes(32);
|
|
16
|
+
return `0x${bytes.toString("hex")}` as Hex;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert human-readable amount to atomic units
|
|
21
|
+
* e.g., "1.5" USDC -> "1500000" (6 decimals)
|
|
22
|
+
*/
|
|
23
|
+
export function toAtomicUnits(amount: string, decimals: number = 6): string {
|
|
24
|
+
const parts = amount.split(".");
|
|
25
|
+
const whole = parts[0];
|
|
26
|
+
const fractional = parts[1] || "";
|
|
27
|
+
const paddedFractional = fractional.padEnd(decimals, "0").slice(0, decimals);
|
|
28
|
+
return `${whole}${paddedFractional}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Convert atomic units to human-readable amount
|
|
33
|
+
* e.g., "1500000" -> "1.5" USDC (6 decimals)
|
|
34
|
+
*/
|
|
35
|
+
export function fromAtomicUnits(amount: string, decimals: number = 6): string {
|
|
36
|
+
const value = BigInt(amount);
|
|
37
|
+
const divisor = BigInt(10 ** decimals);
|
|
38
|
+
const whole = (value / divisor).toString();
|
|
39
|
+
const fractional = (value % divisor)
|
|
40
|
+
.toString()
|
|
41
|
+
.padStart(decimals, "0")
|
|
42
|
+
.replace(/0+$/, "");
|
|
43
|
+
|
|
44
|
+
if (fractional.length === 0) {
|
|
45
|
+
return whole;
|
|
46
|
+
}
|
|
47
|
+
return `${whole}.${fractional}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse signature from hex string to components
|
|
52
|
+
* Returns { r, s, v } where v is 27 or 28
|
|
53
|
+
*/
|
|
54
|
+
export function parseSignature(signature: Hex): { r: Hex; s: Hex; v: number } {
|
|
55
|
+
const hex = signature.startsWith("0x") ? signature.slice(2) : signature;
|
|
56
|
+
const r = `0x${hex.slice(0, 64)}` as Hex;
|
|
57
|
+
const s = `0x${hex.slice(64, 128)}` as Hex;
|
|
58
|
+
const v = parseInt(hex.slice(128, 130) || "1c", 16);
|
|
59
|
+
return { r, s, v };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Combine signature components into hex string
|
|
64
|
+
*/
|
|
65
|
+
export function combineSignature(r: Hex, s: Hex, v: number): Hex {
|
|
66
|
+
const rHex = r.startsWith("0x") ? r.slice(2) : r;
|
|
67
|
+
const sHex = s.startsWith("0x") ? s.slice(2) : s;
|
|
68
|
+
const vHex = v.toString(16).padStart(2, "0");
|
|
69
|
+
return `0x${rHex}${sHex}${vHex}` as Hex;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Convert CAIP-2 chain ID to network name
|
|
74
|
+
* e.g., "eip155:8453" -> "base"
|
|
75
|
+
*/
|
|
76
|
+
export function caip2ToNetwork(caip2Id: string): string {
|
|
77
|
+
const match = caip2Id.match(/^eip155:(\d+)$/);
|
|
78
|
+
if (!match) {
|
|
79
|
+
return caip2Id;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const chainId = parseInt(match[1], 10);
|
|
83
|
+
const chainIdToNetwork: Record<number, string> = {
|
|
84
|
+
1: "ethereum",
|
|
85
|
+
8453: "base",
|
|
86
|
+
84532: "base-sepolia",
|
|
87
|
+
137: "polygon",
|
|
88
|
+
42161: "arbitrum",
|
|
89
|
+
421614: "arbitrum-sepolia",
|
|
90
|
+
10: "optimism",
|
|
91
|
+
11155420: "optimism-sepolia",
|
|
92
|
+
11155111: "ethereum-sepolia",
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return chainIdToNetwork[chainId] || caip2Id;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Convert network name to CAIP-2 chain ID
|
|
100
|
+
* e.g., "base" -> "eip155:8453"
|
|
101
|
+
*/
|
|
102
|
+
export function networkToCaip2(network: string): string {
|
|
103
|
+
const networkToChainId: Record<string, number> = {
|
|
104
|
+
ethereum: 1,
|
|
105
|
+
base: 8453,
|
|
106
|
+
"base-sepolia": 84532,
|
|
107
|
+
polygon: 137,
|
|
108
|
+
arbitrum: 42161,
|
|
109
|
+
"arbitrum-sepolia": 421614,
|
|
110
|
+
optimism: 10,
|
|
111
|
+
"optimism-sepolia": 11155420,
|
|
112
|
+
"ethereum-sepolia": 11155111,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const chainId = networkToChainId[network];
|
|
116
|
+
if (chainId) {
|
|
117
|
+
return `eip155:${chainId}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// If already in CAIP-2 format, return as-is
|
|
121
|
+
if (network.startsWith("eip155:")) {
|
|
122
|
+
return network;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return `eip155:0`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get current Unix timestamp in seconds
|
|
130
|
+
*/
|
|
131
|
+
export function getCurrentTimestamp(): number {
|
|
132
|
+
return Math.floor(Date.now() / 1000);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate Ethereum address
|
|
137
|
+
*/
|
|
138
|
+
export function isValidAddress(address: string): address is Address {
|
|
139
|
+
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Normalize address to checksum format (basic lowercase)
|
|
144
|
+
*/
|
|
145
|
+
export function normalizeAddress(address: string): Address {
|
|
146
|
+
return address.toLowerCase() as Address;
|
|
147
|
+
}
|