@selat-ai/router-client 0.1.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 +126 -0
- package/dist/index.cjs +589 -0
- package/dist/index.d.cts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +555 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
// src/errors/index.ts
|
|
2
|
+
var RouterSdkError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "RouterSdkError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var QuoteParseError = class extends RouterSdkError {
|
|
9
|
+
constructor(message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "QuoteParseError";
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var RouterClientConfigError = class extends RouterSdkError {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "RouterClientConfigError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/protocols/circleNanopaymentPayloadBuilder.ts
|
|
22
|
+
import { BatchEvmScheme, CHAIN_CONFIGS } from "@circle-fin/x402-batching/client";
|
|
23
|
+
function selectGatewayWalletBatchedOption(quote, chain) {
|
|
24
|
+
const expectedNetwork = `eip155:${CHAIN_CONFIGS[chain].chain.id}`;
|
|
25
|
+
const selected = quote.accepts.find((accept) => {
|
|
26
|
+
const extraName = typeof accept.extra?.name === "string" ? accept.extra.name : void 0;
|
|
27
|
+
return accept.scheme === "exact" && accept.network === expectedNetwork && extraName === "GatewayWalletBatched";
|
|
28
|
+
});
|
|
29
|
+
if (!selected) {
|
|
30
|
+
throw new QuoteParseError(
|
|
31
|
+
`no GatewayWalletBatched accept for ${expectedNetwork}; available networks: ${quote.accepts.map((item) => item.network).join(",")}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return selected;
|
|
35
|
+
}
|
|
36
|
+
var CircleNanopaymentPayloadBuilder = class {
|
|
37
|
+
constructor(options) {
|
|
38
|
+
this.options = options;
|
|
39
|
+
}
|
|
40
|
+
options;
|
|
41
|
+
async build(challenge) {
|
|
42
|
+
const selected = selectGatewayWalletBatchedOption(challenge, this.options.chain);
|
|
43
|
+
if (typeof this.options.signer.circleAgentWalletProcessor === "function") {
|
|
44
|
+
const verifyingContract = typeof selected.extra?.verifyingContract === "string" ? selected.extra.verifyingContract : void 0;
|
|
45
|
+
const version = typeof selected.extra?.version === "string" ? selected.extra.version : void 0;
|
|
46
|
+
await this.options.signer.circleAgentWalletProcessor({
|
|
47
|
+
chainId: CHAIN_CONFIGS[this.options.chain].chain.id,
|
|
48
|
+
...verifyingContract ? { verifyingContract } : {},
|
|
49
|
+
...version ? { version } : {}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const batchScheme = new BatchEvmScheme({
|
|
53
|
+
address: this.options.signer.address,
|
|
54
|
+
signTypedData: async (params) => this.options.signer.signTypedData(params)
|
|
55
|
+
});
|
|
56
|
+
const paymentPayload = await batchScheme.createPaymentPayload(challenge.x402Version, {
|
|
57
|
+
scheme: selected.scheme,
|
|
58
|
+
network: selected.network,
|
|
59
|
+
asset: selected.asset,
|
|
60
|
+
amount: selected.amount,
|
|
61
|
+
payTo: selected.payTo,
|
|
62
|
+
maxTimeoutSeconds: selected.maxTimeoutSeconds,
|
|
63
|
+
...selected.extra ? { extra: selected.extra } : {}
|
|
64
|
+
});
|
|
65
|
+
const paymentSignature = Buffer.from(
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
...paymentPayload,
|
|
68
|
+
...challenge.resource ? { resource: challenge.resource } : {},
|
|
69
|
+
accepted: selected
|
|
70
|
+
})
|
|
71
|
+
).toString("base64");
|
|
72
|
+
return { paymentSignature };
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/quote/QuoteParser.ts
|
|
77
|
+
var ROUTER_QUOTE_TTL_SECONDS = 60;
|
|
78
|
+
var QUOTE_TTL_SAFETY_SKEW_SECONDS = 5;
|
|
79
|
+
var LOCAL_QUOTE_TTL_MS = (ROUTER_QUOTE_TTL_SECONDS - QUOTE_TTL_SAFETY_SKEW_SECONDS) * 1e3;
|
|
80
|
+
function parsePaymentRequiredHeader(headerValue) {
|
|
81
|
+
let decoded;
|
|
82
|
+
try {
|
|
83
|
+
decoded = Buffer.from(headerValue, "base64").toString("utf8");
|
|
84
|
+
} catch {
|
|
85
|
+
throw new QuoteParseError("failed to base64 decode payment-required header");
|
|
86
|
+
}
|
|
87
|
+
let payload;
|
|
88
|
+
try {
|
|
89
|
+
payload = JSON.parse(decoded);
|
|
90
|
+
} catch {
|
|
91
|
+
throw new QuoteParseError("failed to parse payment-required header as JSON");
|
|
92
|
+
}
|
|
93
|
+
const rawVersion = payload.x402Version;
|
|
94
|
+
const x402Version = typeof rawVersion === "number" && Number.isFinite(rawVersion) ? rawVersion : 2;
|
|
95
|
+
const resource = typeof payload.resource === "object" && payload.resource !== null ? payload.resource : void 0;
|
|
96
|
+
const rawAccepts = Array.isArray(payload.accepts) ? payload.accepts : [];
|
|
97
|
+
const accepts = rawAccepts.map((entry) => {
|
|
98
|
+
if (typeof entry !== "object" || entry === null) {
|
|
99
|
+
return void 0;
|
|
100
|
+
}
|
|
101
|
+
const record = entry;
|
|
102
|
+
const scheme = typeof record.scheme === "string" ? record.scheme : void 0;
|
|
103
|
+
const network = typeof record.network === "string" ? record.network : void 0;
|
|
104
|
+
const asset = typeof record.asset === "string" ? record.asset : void 0;
|
|
105
|
+
const amount = typeof record.amount === "string" ? record.amount : void 0;
|
|
106
|
+
const payTo = typeof record.payTo === "string" ? record.payTo : void 0;
|
|
107
|
+
const maxTimeoutSeconds = typeof record.maxTimeoutSeconds === "number" ? record.maxTimeoutSeconds : void 0;
|
|
108
|
+
const extra = typeof record.extra === "object" && record.extra !== null ? record.extra : void 0;
|
|
109
|
+
if (!scheme || !network || !asset || !amount || !payTo || maxTimeoutSeconds === void 0) {
|
|
110
|
+
return void 0;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
scheme,
|
|
114
|
+
network,
|
|
115
|
+
asset,
|
|
116
|
+
amount,
|
|
117
|
+
payTo,
|
|
118
|
+
maxTimeoutSeconds,
|
|
119
|
+
...extra ? { extra } : {}
|
|
120
|
+
};
|
|
121
|
+
}).filter((item) => item !== void 0);
|
|
122
|
+
if (accepts.length === 0) {
|
|
123
|
+
throw new QuoteParseError("payment-required header has no valid accepts");
|
|
124
|
+
}
|
|
125
|
+
return resource ? {
|
|
126
|
+
x402Version,
|
|
127
|
+
resource,
|
|
128
|
+
accepts
|
|
129
|
+
} : {
|
|
130
|
+
x402Version,
|
|
131
|
+
accepts
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
var QuoteParser = class {
|
|
135
|
+
async parse(response) {
|
|
136
|
+
if (response.status !== 402) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const quoteId = response.headers.get("x-selat-quote-id");
|
|
140
|
+
if (!quoteId) {
|
|
141
|
+
throw new QuoteParseError("missing x-selat-quote-id in 402 response");
|
|
142
|
+
}
|
|
143
|
+
const paymentRequiredHeader = response.headers.get("payment-required");
|
|
144
|
+
if (!paymentRequiredHeader) {
|
|
145
|
+
throw new QuoteParseError("missing payment-required header in 402 response");
|
|
146
|
+
}
|
|
147
|
+
const parsed = parsePaymentRequiredHeader(paymentRequiredHeader);
|
|
148
|
+
const expiresAt = Date.now() + LOCAL_QUOTE_TTL_MS;
|
|
149
|
+
return {
|
|
150
|
+
quoteId,
|
|
151
|
+
expiresAt,
|
|
152
|
+
x402Version: parsed.x402Version,
|
|
153
|
+
...parsed.resource ? { resource: parsed.resource } : {},
|
|
154
|
+
accepts: parsed.accepts
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/signers/createViemSigner.ts
|
|
160
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
161
|
+
function createViemSigner(privateKey) {
|
|
162
|
+
const account = privateKeyToAccount(privateKey);
|
|
163
|
+
return {
|
|
164
|
+
address: account.address,
|
|
165
|
+
signTypedData: async (params) => account.signTypedData(params)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/client/RouterClient.ts
|
|
170
|
+
var DEFAULT_ROUTER_URL = "https://router.selat.ai";
|
|
171
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
172
|
+
function toHeaders(base, override) {
|
|
173
|
+
const headers = new Headers(base);
|
|
174
|
+
if (override) {
|
|
175
|
+
new Headers(override).forEach((value, key) => headers.set(key, value));
|
|
176
|
+
}
|
|
177
|
+
return headers;
|
|
178
|
+
}
|
|
179
|
+
function toRouterProxyUrl(routerUrl, targetUrl) {
|
|
180
|
+
const proxyUrl = new URL("/proxy", routerUrl);
|
|
181
|
+
proxyUrl.searchParams.set("target", targetUrl);
|
|
182
|
+
return proxyUrl.toString();
|
|
183
|
+
}
|
|
184
|
+
function withSignal(init, signal) {
|
|
185
|
+
if (!signal) {
|
|
186
|
+
return init;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
...init,
|
|
190
|
+
signal
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function createRequestSignal(callerSignal, requestTimeoutMs) {
|
|
194
|
+
const timeoutController = new AbortController();
|
|
195
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), requestTimeoutMs);
|
|
196
|
+
const timeoutSignal = timeoutController.signal;
|
|
197
|
+
if (!callerSignal) {
|
|
198
|
+
return {
|
|
199
|
+
signal: timeoutSignal,
|
|
200
|
+
cleanup: () => clearTimeout(timeoutId)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
const abort = () => controller.abort();
|
|
205
|
+
const cleanup = () => {
|
|
206
|
+
clearTimeout(timeoutId);
|
|
207
|
+
callerSignal.removeEventListener("abort", abort);
|
|
208
|
+
timeoutSignal.removeEventListener("abort", abort);
|
|
209
|
+
};
|
|
210
|
+
if (callerSignal.aborted || timeoutSignal.aborted) {
|
|
211
|
+
cleanup();
|
|
212
|
+
controller.abort();
|
|
213
|
+
return {
|
|
214
|
+
signal: controller.signal,
|
|
215
|
+
cleanup
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
callerSignal.addEventListener("abort", abort);
|
|
219
|
+
timeoutSignal.addEventListener("abort", abort);
|
|
220
|
+
return {
|
|
221
|
+
signal: controller.signal,
|
|
222
|
+
cleanup
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
var RouterClient = class {
|
|
226
|
+
constructor(options) {
|
|
227
|
+
this.options = options;
|
|
228
|
+
this.routerUrl = options.routerUrl ?? DEFAULT_ROUTER_URL;
|
|
229
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
230
|
+
if (!options.chain) {
|
|
231
|
+
throw new RouterClientConfigError(
|
|
232
|
+
"chain is required"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const signer = options.signer ?? (() => {
|
|
236
|
+
if (!options.privateKey) {
|
|
237
|
+
return void 0;
|
|
238
|
+
}
|
|
239
|
+
return createViemSigner(options.privateKey);
|
|
240
|
+
})();
|
|
241
|
+
if (!signer) {
|
|
242
|
+
throw new RouterClientConfigError(
|
|
243
|
+
"Provide either signer, or privateKey (with chain) for Circle nanopayments"
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
this.paymentPayloadBuilder = new CircleNanopaymentPayloadBuilder({
|
|
247
|
+
chain: options.chain,
|
|
248
|
+
signer
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
options;
|
|
252
|
+
quoteParser = new QuoteParser();
|
|
253
|
+
routerUrl;
|
|
254
|
+
requestTimeoutMs;
|
|
255
|
+
paymentPayloadBuilder;
|
|
256
|
+
createFetch(config) {
|
|
257
|
+
return (path, init) => this.fetch(new URL(path, config.baseUrl).toString(), init);
|
|
258
|
+
}
|
|
259
|
+
async fetch(input, init) {
|
|
260
|
+
const { preferProtocol, ...requestInit } = init ?? {};
|
|
261
|
+
const proxyUrl = toRouterProxyUrl(this.routerUrl, input);
|
|
262
|
+
const firstHeaders = toHeaders(this.options.defaultHeaders ?? {}, requestInit.headers);
|
|
263
|
+
firstHeaders.set("x-selat-prefer-protocol", preferProtocol ?? "mpp");
|
|
264
|
+
const firstRequest = createRequestSignal(requestInit.signal ?? void 0, this.requestTimeoutMs);
|
|
265
|
+
let firstResponse;
|
|
266
|
+
try {
|
|
267
|
+
firstResponse = await fetch(proxyUrl, withSignal({
|
|
268
|
+
...requestInit,
|
|
269
|
+
headers: firstHeaders
|
|
270
|
+
}, firstRequest.signal));
|
|
271
|
+
} finally {
|
|
272
|
+
firstRequest.cleanup();
|
|
273
|
+
}
|
|
274
|
+
const challenge = await this.quoteParser.parse(firstResponse.clone());
|
|
275
|
+
if (!challenge) {
|
|
276
|
+
return firstResponse;
|
|
277
|
+
}
|
|
278
|
+
const payment = await this.paymentPayloadBuilder.build(challenge);
|
|
279
|
+
const paidHeaders = firstHeaders;
|
|
280
|
+
paidHeaders.set("PAYMENT-SIGNATURE", payment.paymentSignature);
|
|
281
|
+
paidHeaders.set("x-selat-quote-id", challenge.quoteId);
|
|
282
|
+
const paidRequest = createRequestSignal(requestInit.signal ?? void 0, this.requestTimeoutMs);
|
|
283
|
+
try {
|
|
284
|
+
return await fetch(proxyUrl, withSignal({
|
|
285
|
+
...requestInit,
|
|
286
|
+
headers: paidHeaders
|
|
287
|
+
}, paidRequest.signal));
|
|
288
|
+
} finally {
|
|
289
|
+
paidRequest.cleanup();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
// src/signers/createRemoteSigner.ts
|
|
295
|
+
function createRemoteSigner(address, requester) {
|
|
296
|
+
return {
|
|
297
|
+
address,
|
|
298
|
+
signTypedData: async (params) => requester({ address, typedData: params })
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/signers/createCircleAgentWalletSigner.ts
|
|
303
|
+
import { spawn } from "child_process";
|
|
304
|
+
import { recoverTypedDataAddress } from "viem";
|
|
305
|
+
var DEFAULT_CIRCLE_CLI_COMMAND = "circle";
|
|
306
|
+
var DEFAULT_CIRCLE_CLI_TIMEOUT_MS = 3e4;
|
|
307
|
+
function toCliChain(chain) {
|
|
308
|
+
switch (chain) {
|
|
309
|
+
case "base":
|
|
310
|
+
return "BASE";
|
|
311
|
+
case "ethereum":
|
|
312
|
+
return "ETHEREUM";
|
|
313
|
+
case "avalanche":
|
|
314
|
+
return "AVALANCHE";
|
|
315
|
+
case "optimism":
|
|
316
|
+
return "OPTIMISM";
|
|
317
|
+
case "polygon":
|
|
318
|
+
return "POLYGON";
|
|
319
|
+
default:
|
|
320
|
+
return chain.toUpperCase();
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function stringifySafeJson(value) {
|
|
324
|
+
return JSON.stringify(value, (_key, item) => {
|
|
325
|
+
if (typeof item !== "bigint") {
|
|
326
|
+
return item;
|
|
327
|
+
}
|
|
328
|
+
if (item <= BigInt(Number.MAX_SAFE_INTEGER) && item >= BigInt(Number.MIN_SAFE_INTEGER)) {
|
|
329
|
+
return Number(item);
|
|
330
|
+
}
|
|
331
|
+
return item.toString();
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function ensureEip712DomainTypeForCircleCli(typedData) {
|
|
335
|
+
if (!typedData || typeof typedData !== "object") {
|
|
336
|
+
return typedData;
|
|
337
|
+
}
|
|
338
|
+
const root = typedData;
|
|
339
|
+
const domain = root.domain;
|
|
340
|
+
const types = root.types;
|
|
341
|
+
if (!domain || typeof domain !== "object" || !types || typeof types !== "object") {
|
|
342
|
+
return typedData;
|
|
343
|
+
}
|
|
344
|
+
const typesRecord = types;
|
|
345
|
+
if (Array.isArray(typesRecord.EIP712Domain)) {
|
|
346
|
+
return typedData;
|
|
347
|
+
}
|
|
348
|
+
const domainRecord = domain;
|
|
349
|
+
const eip712Domain = [];
|
|
350
|
+
if (typeof domainRecord.name === "string") {
|
|
351
|
+
eip712Domain.push({ name: "name", type: "string" });
|
|
352
|
+
}
|
|
353
|
+
if (typeof domainRecord.version === "string") {
|
|
354
|
+
eip712Domain.push({ name: "version", type: "string" });
|
|
355
|
+
}
|
|
356
|
+
if (typeof domainRecord.chainId === "number" || typeof domainRecord.chainId === "bigint" || typeof domainRecord.chainId === "string") {
|
|
357
|
+
eip712Domain.push({ name: "chainId", type: "uint256" });
|
|
358
|
+
}
|
|
359
|
+
if (typeof domainRecord.verifyingContract === "string") {
|
|
360
|
+
eip712Domain.push({ name: "verifyingContract", type: "address" });
|
|
361
|
+
}
|
|
362
|
+
if (typeof domainRecord.salt === "string") {
|
|
363
|
+
eip712Domain.push({ name: "salt", type: "bytes32" });
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
...root,
|
|
367
|
+
types: {
|
|
368
|
+
EIP712Domain: eip712Domain,
|
|
369
|
+
...typesRecord
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function normalizeAddress(address) {
|
|
374
|
+
return address.toLowerCase();
|
|
375
|
+
}
|
|
376
|
+
function extractHexSignature(text) {
|
|
377
|
+
const matches = text.match(/0x[0-9a-fA-F]{130}/g);
|
|
378
|
+
if (!matches || matches.length === 0) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
return matches[matches.length - 1];
|
|
382
|
+
}
|
|
383
|
+
async function ensureSignatureMatchesExpectedAddress(typedData, signature, fallbackExpectedAddress) {
|
|
384
|
+
try {
|
|
385
|
+
const typedDataRecord = typedData;
|
|
386
|
+
const message = typedDataRecord.message;
|
|
387
|
+
const expectedAddress = typeof message?.from === "string" ? normalizeAddress(message.from) : fallbackExpectedAddress;
|
|
388
|
+
const recoveredAddress = await recoverTypedDataAddress({
|
|
389
|
+
...typedDataRecord,
|
|
390
|
+
signature
|
|
391
|
+
});
|
|
392
|
+
if (normalizeAddress(recoveredAddress) !== normalizeAddress(expectedAddress)) {
|
|
393
|
+
throw new Error(
|
|
394
|
+
`Circle CLI produced a signature for ${recoveredAddress}, but expected ${expectedAddress}. This commonly happens when an outdated Circle CLI is used or when the selected wallet signs as a different key model. Upgrade @circle-fin/cli and verify the wallet address with \`circle wallet list --chain <CHAIN>\` before running the SDK.`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error instanceof Error && error.message.startsWith("Circle CLI produced a signature")) {
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function signTypedDataWithCircleCli(command, timeoutMs, address, chain, typedData, options) {
|
|
404
|
+
const normalizedTypedData = ensureEip712DomainTypeForCircleCli(typedData);
|
|
405
|
+
const typedDataJson = stringifySafeJson(normalizedTypedData);
|
|
406
|
+
const args = [
|
|
407
|
+
"wallet",
|
|
408
|
+
"sign",
|
|
409
|
+
"typed-data",
|
|
410
|
+
typedDataJson,
|
|
411
|
+
"--address",
|
|
412
|
+
address,
|
|
413
|
+
"--chain",
|
|
414
|
+
toCliChain(chain)
|
|
415
|
+
];
|
|
416
|
+
return new Promise((resolve, reject) => {
|
|
417
|
+
const child = spawn(command, args, {
|
|
418
|
+
shell: false,
|
|
419
|
+
windowsHide: true
|
|
420
|
+
});
|
|
421
|
+
let stdout = "";
|
|
422
|
+
let stderr = "";
|
|
423
|
+
const timeout = setTimeout(() => {
|
|
424
|
+
child.kill();
|
|
425
|
+
reject(new Error(`Circle CLI signing timed out after ${timeoutMs}ms`));
|
|
426
|
+
}, timeoutMs);
|
|
427
|
+
child.stdout.on("data", (chunk) => {
|
|
428
|
+
stdout += chunk.toString();
|
|
429
|
+
});
|
|
430
|
+
child.stderr.on("data", (chunk) => {
|
|
431
|
+
stderr += chunk.toString();
|
|
432
|
+
});
|
|
433
|
+
child.on("error", (error) => {
|
|
434
|
+
clearTimeout(timeout);
|
|
435
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
436
|
+
reject(
|
|
437
|
+
new Error(
|
|
438
|
+
`Failed to execute Circle CLI ("${command}"): ${message}. Ensure @circle-fin/cli is installed and authenticated.`
|
|
439
|
+
)
|
|
440
|
+
);
|
|
441
|
+
});
|
|
442
|
+
child.on("close", (code) => {
|
|
443
|
+
clearTimeout(timeout);
|
|
444
|
+
if (code !== 0) {
|
|
445
|
+
const details = stderr.trim() || stdout.trim() || `exit code ${code}`;
|
|
446
|
+
reject(new Error(`Circle CLI signing failed: ${details}`));
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const signature = extractHexSignature(stdout);
|
|
450
|
+
if (!signature) {
|
|
451
|
+
reject(new Error(`Circle CLI output did not contain a valid signature: ${stdout.trim()}`));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (options?.skipSignatureAddressCheck) {
|
|
455
|
+
resolve(signature);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
ensureSignatureMatchesExpectedAddress(normalizedTypedData, signature, address).then(() => resolve(signature)).catch((error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
var GATEWAY_AUTH_TYPES = {
|
|
463
|
+
TransferWithAuthorization: [
|
|
464
|
+
{ name: "from", type: "address" },
|
|
465
|
+
{ name: "to", type: "address" },
|
|
466
|
+
{ name: "value", type: "uint256" },
|
|
467
|
+
{ name: "validAfter", type: "uint256" },
|
|
468
|
+
{ name: "validBefore", type: "uint256" },
|
|
469
|
+
{ name: "nonce", type: "bytes32" }
|
|
470
|
+
]
|
|
471
|
+
};
|
|
472
|
+
async function resolveGatewaySignerAddress(command, timeoutMs, walletAddress, chain, input) {
|
|
473
|
+
if (!input.verifyingContract) {
|
|
474
|
+
return walletAddress;
|
|
475
|
+
}
|
|
476
|
+
const typedData = {
|
|
477
|
+
domain: {
|
|
478
|
+
name: "GatewayWalletBatched",
|
|
479
|
+
version: input.version ?? "1",
|
|
480
|
+
chainId: input.chainId,
|
|
481
|
+
verifyingContract: input.verifyingContract
|
|
482
|
+
},
|
|
483
|
+
primaryType: "TransferWithAuthorization",
|
|
484
|
+
types: GATEWAY_AUTH_TYPES,
|
|
485
|
+
message: {
|
|
486
|
+
from: walletAddress,
|
|
487
|
+
to: "0x0000000000000000000000000000000000000000",
|
|
488
|
+
value: 0,
|
|
489
|
+
validAfter: 0,
|
|
490
|
+
validBefore: 0,
|
|
491
|
+
nonce: "0x" + "0".repeat(64)
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
const probeSignature = await signTypedDataWithCircleCli(
|
|
495
|
+
command,
|
|
496
|
+
timeoutMs,
|
|
497
|
+
walletAddress,
|
|
498
|
+
chain,
|
|
499
|
+
typedData,
|
|
500
|
+
{ skipSignatureAddressCheck: true }
|
|
501
|
+
);
|
|
502
|
+
const owner = await recoverTypedDataAddress({
|
|
503
|
+
...typedData,
|
|
504
|
+
signature: probeSignature
|
|
505
|
+
});
|
|
506
|
+
return normalizeAddress(owner);
|
|
507
|
+
}
|
|
508
|
+
function createCircleAgentWalletSigner(options) {
|
|
509
|
+
const command = options.cliCommand ?? DEFAULT_CIRCLE_CLI_COMMAND;
|
|
510
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CIRCLE_CLI_TIMEOUT_MS;
|
|
511
|
+
const walletAddress = normalizeAddress(options.address);
|
|
512
|
+
let effectiveAddress = walletAddress;
|
|
513
|
+
let resolvedOwnerAddress = null;
|
|
514
|
+
let resolveOwnerAddressInFlight = null;
|
|
515
|
+
const resolveOwnerAddress = async (input) => {
|
|
516
|
+
if (!input.verifyingContract) {
|
|
517
|
+
return walletAddress;
|
|
518
|
+
}
|
|
519
|
+
if (resolvedOwnerAddress) {
|
|
520
|
+
return resolvedOwnerAddress;
|
|
521
|
+
}
|
|
522
|
+
if (!resolveOwnerAddressInFlight) {
|
|
523
|
+
resolveOwnerAddressInFlight = resolveGatewaySignerAddress(command, timeoutMs, walletAddress, options.chain, input).then((owner2) => {
|
|
524
|
+
resolvedOwnerAddress = owner2;
|
|
525
|
+
effectiveAddress = owner2;
|
|
526
|
+
return owner2;
|
|
527
|
+
}).finally(() => {
|
|
528
|
+
resolveOwnerAddressInFlight = null;
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
const owner = await resolveOwnerAddressInFlight;
|
|
532
|
+
resolvedOwnerAddress = owner;
|
|
533
|
+
effectiveAddress = owner;
|
|
534
|
+
return owner;
|
|
535
|
+
};
|
|
536
|
+
return {
|
|
537
|
+
get address() {
|
|
538
|
+
return effectiveAddress;
|
|
539
|
+
},
|
|
540
|
+
circleAgentWalletProcessor: async (input) => {
|
|
541
|
+
await resolveOwnerAddress(input);
|
|
542
|
+
},
|
|
543
|
+
signTypedData: async (typedData) => signTypedDataWithCircleCli(command, timeoutMs, walletAddress, options.chain, typedData)
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
export {
|
|
547
|
+
CircleNanopaymentPayloadBuilder,
|
|
548
|
+
QuoteParseError,
|
|
549
|
+
RouterClient,
|
|
550
|
+
RouterClientConfigError,
|
|
551
|
+
RouterSdkError,
|
|
552
|
+
createCircleAgentWalletSigner,
|
|
553
|
+
createRemoteSigner,
|
|
554
|
+
createViemSigner
|
|
555
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@selat-ai/router-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "TypeScript client for interacting with SELATA Router.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public",
|
|
22
|
+
"provenance": true
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"clean": "rimraf dist",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"lint": "tsc --noEmit",
|
|
33
|
+
"example": "tsx examples/pay-with-selat.ts",
|
|
34
|
+
"prepublishOnly": "pnpm run clean && pnpm run build"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"selat",
|
|
38
|
+
"router",
|
|
39
|
+
"x402",
|
|
40
|
+
"mpp",
|
|
41
|
+
"payments",
|
|
42
|
+
"circle",
|
|
43
|
+
"nanopayments"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@circle-fin/x402-batching": "^3.1.2",
|
|
47
|
+
"viem": "^2.48.8"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.13.5",
|
|
51
|
+
"dotenv": "^17.2.1",
|
|
52
|
+
"rimraf": "^6.0.1",
|
|
53
|
+
"tsx": "^4.21.0",
|
|
54
|
+
"tsup": "^8.3.6",
|
|
55
|
+
"typescript": "^5.7.3",
|
|
56
|
+
"vitest": "^3.0.5"
|
|
57
|
+
}
|
|
58
|
+
}
|