@pay-skill/sdk 0.1.7 → 0.2.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 +143 -154
- package/dist/auth.d.ts +11 -6
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +19 -7
- package/dist/auth.js.map +1 -1
- package/dist/errors.d.ts +4 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -3
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -6
- package/dist/index.js.map +1 -1
- package/dist/keychain.d.ts +8 -0
- package/dist/keychain.d.ts.map +1 -0
- package/dist/keychain.js +17 -0
- package/dist/keychain.js.map +1 -0
- package/dist/wallet.d.ts +135 -104
- package/dist/wallet.d.ts.map +1 -1
- package/dist/wallet.js +631 -276
- package/dist/wallet.js.map +1 -1
- package/jsr.json +13 -13
- package/knip.json +5 -5
- package/package.json +51 -48
- package/src/auth.ts +210 -200
- package/src/eip3009.ts +79 -79
- package/src/errors.ts +55 -48
- package/src/index.ts +24 -51
- package/src/keychain.ts +18 -0
- package/src/wallet.ts +1111 -445
- package/tests/test_auth_rejection.ts +102 -154
- package/tests/test_crypto.ts +138 -251
- package/tests/test_e2e.ts +99 -158
- package/tests/test_errors.ts +44 -36
- package/tests/test_ows.ts +153 -0
- package/tests/test_wallet.ts +194 -0
- package/dist/client.d.ts +0 -94
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -443
- package/dist/client.js.map +0 -1
- package/dist/models.d.ts +0 -78
- package/dist/models.d.ts.map +0 -1
- package/dist/models.js +0 -2
- package/dist/models.js.map +0 -1
- package/dist/ows-signer.d.ts +0 -75
- package/dist/ows-signer.d.ts.map +0 -1
- package/dist/ows-signer.js +0 -130
- package/dist/ows-signer.js.map +0 -1
- package/dist/signer.d.ts +0 -46
- package/dist/signer.d.ts.map +0 -1
- package/dist/signer.js +0 -111
- package/dist/signer.js.map +0 -1
- package/src/client.ts +0 -644
- package/src/models.ts +0 -77
- package/src/ows-signer.ts +0 -223
- package/src/signer.ts +0 -147
- package/tests/test_ows_integration.ts +0 -92
- package/tests/test_ows_signer.ts +0 -365
- package/tests/test_signer.ts +0 -47
- package/tests/test_validation.ts +0 -66
package/src/client.ts
DELETED
|
@@ -1,644 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PayClient — single entry point for the pay SDK.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type {
|
|
6
|
-
DirectPaymentResult,
|
|
7
|
-
DiscoverOptions,
|
|
8
|
-
DiscoverService,
|
|
9
|
-
StatusResponse,
|
|
10
|
-
Tab,
|
|
11
|
-
WebhookRegistration,
|
|
12
|
-
} from "./models.js";
|
|
13
|
-
import {
|
|
14
|
-
PayNetworkError,
|
|
15
|
-
PayServerError,
|
|
16
|
-
PayValidationError,
|
|
17
|
-
} from "./errors.js";
|
|
18
|
-
import type { Signer } from "./signer.js";
|
|
19
|
-
import { createSigner } from "./signer.js";
|
|
20
|
-
import {
|
|
21
|
-
buildAuthHeaders,
|
|
22
|
-
buildAuthHeadersWithSigner,
|
|
23
|
-
type AuthConfig,
|
|
24
|
-
type AuthHeaders,
|
|
25
|
-
} from "./auth.js";
|
|
26
|
-
import type { Hex, Address } from "viem";
|
|
27
|
-
import { sign as viemSign, serializeSignature, privateKeyToAccount } from "viem/accounts";
|
|
28
|
-
import {
|
|
29
|
-
signTransferAuthorization,
|
|
30
|
-
combinedSignature,
|
|
31
|
-
} from "./eip3009.js";
|
|
32
|
-
|
|
33
|
-
const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
34
|
-
const DIRECT_MIN = 1_000_000; // $1.00 USDC
|
|
35
|
-
const TAB_MIN = 5_000_000; // $5.00 USDC
|
|
36
|
-
|
|
37
|
-
export const DEFAULT_API_URL = "https://pay-skill.com/api/v1";
|
|
38
|
-
|
|
39
|
-
function validateAddress(address: string, field = "address"): void {
|
|
40
|
-
if (!ADDRESS_RE.test(address)) {
|
|
41
|
-
throw new PayValidationError(
|
|
42
|
-
`Invalid Ethereum address: ${address}`,
|
|
43
|
-
field
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function validateAmount(
|
|
49
|
-
amount: number,
|
|
50
|
-
minimum: number,
|
|
51
|
-
field = "amount"
|
|
52
|
-
): void {
|
|
53
|
-
if (amount < minimum) {
|
|
54
|
-
const minUsd = minimum / 1_000_000;
|
|
55
|
-
throw new PayValidationError(
|
|
56
|
-
`Amount ${amount} below minimum ($${minUsd.toFixed(2)})`,
|
|
57
|
-
field
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface PayClientOptions {
|
|
63
|
-
apiUrl?: string;
|
|
64
|
-
signer?: Signer | "cli" | "raw" | "custom";
|
|
65
|
-
signerOptions?: {
|
|
66
|
-
command?: string;
|
|
67
|
-
key?: string;
|
|
68
|
-
address?: string;
|
|
69
|
-
callback?: (hash: Uint8Array) => Uint8Array;
|
|
70
|
-
};
|
|
71
|
-
/** Private key for direct auth signing (alternative to signer). */
|
|
72
|
-
privateKey?: string;
|
|
73
|
-
/** Chain ID for EIP-712 domain (default: 8453 for Base). */
|
|
74
|
-
chainId?: number;
|
|
75
|
-
/** Router contract address for EIP-712 domain. */
|
|
76
|
-
routerAddress?: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export class PayClient {
|
|
80
|
-
private readonly apiUrl: string;
|
|
81
|
-
/** URL path prefix extracted from apiUrl (e.g., "/api/v1"). */
|
|
82
|
-
private readonly _basePath: string;
|
|
83
|
-
private readonly signer: Signer;
|
|
84
|
-
private readonly _privateKey: Hex | null;
|
|
85
|
-
private readonly _authConfig: AuthConfig | null;
|
|
86
|
-
private readonly _chainId: number;
|
|
87
|
-
private readonly _address: string;
|
|
88
|
-
|
|
89
|
-
constructor(options: PayClientOptions = {}) {
|
|
90
|
-
this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
91
|
-
// Extract the URL path to prepend to auth signing paths.
|
|
92
|
-
// e.g., "http://host:3001/api/v1" → "/api/v1"
|
|
93
|
-
try {
|
|
94
|
-
this._basePath = new URL(this.apiUrl).pathname.replace(/\/+$/, "");
|
|
95
|
-
} catch {
|
|
96
|
-
this._basePath = "";
|
|
97
|
-
}
|
|
98
|
-
if (typeof options.signer === "object") {
|
|
99
|
-
this.signer = options.signer;
|
|
100
|
-
} else {
|
|
101
|
-
this.signer = createSigner(options.signer ?? "cli", {
|
|
102
|
-
...options.signerOptions,
|
|
103
|
-
key: options.signerOptions?.key ?? options.privateKey,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Private key for direct signing (preferred over Signer for auth)
|
|
108
|
-
this._privateKey = options.privateKey
|
|
109
|
-
? ((options.privateKey.startsWith("0x")
|
|
110
|
-
? options.privateKey
|
|
111
|
-
: "0x" + options.privateKey) as Hex)
|
|
112
|
-
: null;
|
|
113
|
-
|
|
114
|
-
this._chainId = options.chainId ?? 8453;
|
|
115
|
-
this._address = this._privateKey
|
|
116
|
-
? privateKeyToAccount(this._privateKey).address
|
|
117
|
-
: (options.signerOptions?.address ?? "");
|
|
118
|
-
|
|
119
|
-
// Auth config for EIP-712 domain
|
|
120
|
-
if (options.chainId && options.routerAddress) {
|
|
121
|
-
this._authConfig = {
|
|
122
|
-
chainId: options.chainId,
|
|
123
|
-
routerAddress: options.routerAddress as Address,
|
|
124
|
-
};
|
|
125
|
-
} else {
|
|
126
|
-
this._authConfig = null;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
private async getContracts(): Promise<{ router: string; tab: string; direct: string; usdc: string; chain_id: number }> {
|
|
131
|
-
return this.get("/contracts");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── Direct Payment ──────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
async payDirect(
|
|
137
|
-
to: string,
|
|
138
|
-
amount: number,
|
|
139
|
-
options: { memo?: string } = {}
|
|
140
|
-
): Promise<DirectPaymentResult> {
|
|
141
|
-
validateAddress(to, "to");
|
|
142
|
-
validateAmount(amount, DIRECT_MIN);
|
|
143
|
-
|
|
144
|
-
// Get contract addresses to determine the spender
|
|
145
|
-
const contracts = await this.get<{ direct: string }>("/contracts");
|
|
146
|
-
const permit = await this.prepareAndSignPermit(amount, contracts.direct);
|
|
147
|
-
|
|
148
|
-
const data = await this.post<DirectPaymentResult>("/direct", {
|
|
149
|
-
to,
|
|
150
|
-
amount,
|
|
151
|
-
memo: options.memo ?? "",
|
|
152
|
-
permit,
|
|
153
|
-
});
|
|
154
|
-
return data;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── Tab Management ──────────────────────────────────────────────
|
|
158
|
-
|
|
159
|
-
async openTab(
|
|
160
|
-
provider: string,
|
|
161
|
-
amount: number,
|
|
162
|
-
options: { maxChargePerCall: number }
|
|
163
|
-
): Promise<Tab> {
|
|
164
|
-
validateAddress(provider, "provider");
|
|
165
|
-
validateAmount(amount, TAB_MIN);
|
|
166
|
-
if (options.maxChargePerCall <= 0) {
|
|
167
|
-
throw new PayValidationError(
|
|
168
|
-
"maxChargePerCall must be positive",
|
|
169
|
-
"maxChargePerCall"
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
const contracts = await this.get<{ tab: string }>("/contracts");
|
|
173
|
-
const permit = await this.prepareAndSignPermit(amount, contracts.tab);
|
|
174
|
-
|
|
175
|
-
return this.post<Tab>("/tabs", {
|
|
176
|
-
provider,
|
|
177
|
-
amount,
|
|
178
|
-
max_charge_per_call: options.maxChargePerCall,
|
|
179
|
-
permit,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async closeTab(tabId: string): Promise<Tab> {
|
|
184
|
-
return this.post<Tab>(`/tabs/${tabId}/close`, {});
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
async topUpTab(tabId: string, amount: number): Promise<Tab> {
|
|
188
|
-
validateAmount(amount, 1, "amount");
|
|
189
|
-
const contracts = await this.get<{ tab: string }>("/contracts");
|
|
190
|
-
const permit = await this.prepareAndSignPermit(amount, contracts.tab);
|
|
191
|
-
return this.post<Tab>(`/tabs/${tabId}/topup`, { amount, permit });
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async listTabs(): Promise<Tab[]> {
|
|
195
|
-
return this.get<Tab[]>("/tabs");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async getTab(tabId: string): Promise<Tab> {
|
|
199
|
-
return this.get<Tab>(`/tabs/${tabId}`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ── x402 ────────────────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
private static readonly X402_TAB_MULTIPLIER = 10;
|
|
205
|
-
|
|
206
|
-
async request(
|
|
207
|
-
url: string,
|
|
208
|
-
options: {
|
|
209
|
-
method?: string;
|
|
210
|
-
body?: unknown;
|
|
211
|
-
headers?: Record<string, string>;
|
|
212
|
-
} = {}
|
|
213
|
-
): Promise<Response> {
|
|
214
|
-
const method = options.method ?? "GET";
|
|
215
|
-
const headers = options.headers ?? {};
|
|
216
|
-
const bodyStr = options.body ? JSON.stringify(options.body) : undefined;
|
|
217
|
-
|
|
218
|
-
const resp = await fetch(url, { method, body: bodyStr, headers });
|
|
219
|
-
|
|
220
|
-
if (resp.status !== 402) return resp;
|
|
221
|
-
|
|
222
|
-
return this.handle402(resp, url, method, bodyStr, headers);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Parse x402 V2 payment requirements from a 402 response.
|
|
227
|
-
*
|
|
228
|
-
* Checks PAYMENT-REQUIRED header first (base64-encoded JSON),
|
|
229
|
-
* falls back to response body for requirements.
|
|
230
|
-
*/
|
|
231
|
-
private async parse402Requirements(resp: Response): Promise<{
|
|
232
|
-
settlement: string;
|
|
233
|
-
amount: number;
|
|
234
|
-
to: string;
|
|
235
|
-
accepted?: Record<string, unknown>;
|
|
236
|
-
}> {
|
|
237
|
-
// Try PAYMENT-REQUIRED header (base64-encoded JSON)
|
|
238
|
-
const prHeader = resp.headers.get("payment-required");
|
|
239
|
-
if (prHeader) {
|
|
240
|
-
try {
|
|
241
|
-
const decoded = JSON.parse(atob(prHeader)) as Record<string, unknown>;
|
|
242
|
-
return PayClient.extractRequirements(decoded);
|
|
243
|
-
} catch {
|
|
244
|
-
// Fall through to body parsing
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Fallback: parse from response body
|
|
249
|
-
const body = (await resp.json()) as Record<string, unknown>;
|
|
250
|
-
const requirements = (body.requirements ?? body) as Record<string, unknown>;
|
|
251
|
-
return PayClient.extractRequirements(requirements);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private static extractRequirements(obj: Record<string, unknown>): {
|
|
255
|
-
settlement: string;
|
|
256
|
-
amount: number;
|
|
257
|
-
to: string;
|
|
258
|
-
accepted?: Record<string, unknown>;
|
|
259
|
-
} {
|
|
260
|
-
// x402 v2 format: { accepts: [{ payTo, amount, extra: { settlement } }] }
|
|
261
|
-
const accepts = obj.accepts as Array<Record<string, unknown>> | undefined;
|
|
262
|
-
if (Array.isArray(accepts) && accepts.length > 0) {
|
|
263
|
-
const offer = accepts[0];
|
|
264
|
-
const extra = (offer.extra ?? {}) as Record<string, unknown>;
|
|
265
|
-
return {
|
|
266
|
-
settlement: String(extra.settlement ?? "direct"),
|
|
267
|
-
amount: Number(offer.amount ?? 0),
|
|
268
|
-
to: String(offer.payTo ?? ""),
|
|
269
|
-
accepted: offer,
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Legacy v1 format
|
|
274
|
-
return {
|
|
275
|
-
settlement: String(obj.settlement ?? "direct"),
|
|
276
|
-
amount: Number(obj.amount ?? 0),
|
|
277
|
-
to: String(obj.to ?? ""),
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private async handle402(
|
|
282
|
-
resp: Response,
|
|
283
|
-
url: string,
|
|
284
|
-
method: string,
|
|
285
|
-
body: string | undefined,
|
|
286
|
-
headers: Record<string, string>
|
|
287
|
-
): Promise<Response> {
|
|
288
|
-
const reqs = await this.parse402Requirements(resp);
|
|
289
|
-
|
|
290
|
-
if (reqs.settlement === "tab") {
|
|
291
|
-
return this.settleViaTab(url, method, body, headers, reqs);
|
|
292
|
-
}
|
|
293
|
-
return this.settleViaDirect(url, method, body, headers, reqs);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
private async settleViaDirect(
|
|
297
|
-
url: string,
|
|
298
|
-
method: string,
|
|
299
|
-
body: string | undefined,
|
|
300
|
-
headers: Record<string, string>,
|
|
301
|
-
reqs: { settlement: string; amount: number; to: string; accepted?: Record<string, unknown> },
|
|
302
|
-
): Promise<Response> {
|
|
303
|
-
if (!this._privateKey) {
|
|
304
|
-
throw new PayValidationError("privateKey required for x402 direct settlement", "privateKey");
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const contracts = await this.getContracts();
|
|
308
|
-
const auth = await signTransferAuthorization(
|
|
309
|
-
this._privateKey,
|
|
310
|
-
reqs.to as Address,
|
|
311
|
-
reqs.amount,
|
|
312
|
-
this._chainId,
|
|
313
|
-
contracts.usdc as Address,
|
|
314
|
-
);
|
|
315
|
-
|
|
316
|
-
const paymentPayload = {
|
|
317
|
-
x402Version: 2,
|
|
318
|
-
accepted: reqs.accepted ?? {
|
|
319
|
-
scheme: "exact",
|
|
320
|
-
network: `eip155:${this._chainId}`,
|
|
321
|
-
amount: String(reqs.amount),
|
|
322
|
-
payTo: reqs.to,
|
|
323
|
-
},
|
|
324
|
-
payload: {
|
|
325
|
-
signature: combinedSignature(auth),
|
|
326
|
-
authorization: {
|
|
327
|
-
from: auth.from,
|
|
328
|
-
to: auth.to,
|
|
329
|
-
value: String(reqs.amount),
|
|
330
|
-
validAfter: "0",
|
|
331
|
-
validBefore: "0",
|
|
332
|
-
nonce: auth.nonce,
|
|
333
|
-
},
|
|
334
|
-
},
|
|
335
|
-
extensions: {},
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
return fetch(url, {
|
|
339
|
-
method,
|
|
340
|
-
body,
|
|
341
|
-
headers: {
|
|
342
|
-
...headers,
|
|
343
|
-
"Content-Type": "application/json",
|
|
344
|
-
"PAYMENT-SIGNATURE": btoa(JSON.stringify(paymentPayload)),
|
|
345
|
-
},
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
private async settleViaTab(
|
|
350
|
-
url: string,
|
|
351
|
-
method: string,
|
|
352
|
-
body: string | undefined,
|
|
353
|
-
headers: Record<string, string>,
|
|
354
|
-
reqs: { settlement: string; amount: number; to: string; accepted?: Record<string, unknown> },
|
|
355
|
-
): Promise<Response> {
|
|
356
|
-
const tabs = await this.listTabs();
|
|
357
|
-
let tab = tabs.find((t) => t.provider === reqs.to && t.status === "open");
|
|
358
|
-
|
|
359
|
-
if (!tab) {
|
|
360
|
-
const tabAmount = Math.max(
|
|
361
|
-
reqs.amount * PayClient.X402_TAB_MULTIPLIER,
|
|
362
|
-
TAB_MIN
|
|
363
|
-
);
|
|
364
|
-
tab = await this.openTab(reqs.to, tabAmount, {
|
|
365
|
-
maxChargePerCall: reqs.amount,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const chargeData = await this.post<{ charge_id: string }>(
|
|
370
|
-
`/tabs/${tab.tabId}/charge`,
|
|
371
|
-
{ amount: reqs.amount }
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
const paymentPayload = {
|
|
375
|
-
x402Version: 2,
|
|
376
|
-
accepted: reqs.accepted ?? {
|
|
377
|
-
scheme: "exact",
|
|
378
|
-
network: `eip155:${this._chainId}`,
|
|
379
|
-
amount: String(reqs.amount),
|
|
380
|
-
payTo: reqs.to,
|
|
381
|
-
},
|
|
382
|
-
payload: {
|
|
383
|
-
authorization: { from: this._address },
|
|
384
|
-
},
|
|
385
|
-
extensions: {
|
|
386
|
-
pay: {
|
|
387
|
-
settlement: "tab",
|
|
388
|
-
tabId: tab.tabId,
|
|
389
|
-
chargeId: chargeData.charge_id ?? "",
|
|
390
|
-
},
|
|
391
|
-
},
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
return fetch(url, {
|
|
395
|
-
method,
|
|
396
|
-
body,
|
|
397
|
-
headers: {
|
|
398
|
-
...headers,
|
|
399
|
-
"Content-Type": "application/json",
|
|
400
|
-
"PAYMENT-SIGNATURE": btoa(JSON.stringify(paymentPayload)),
|
|
401
|
-
},
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ── Wallet ──────────────────────────────────────────────────────
|
|
406
|
-
|
|
407
|
-
async getStatus(): Promise<StatusResponse> {
|
|
408
|
-
const raw = await this.get<{
|
|
409
|
-
wallet: string;
|
|
410
|
-
balance_usdc: string | null;
|
|
411
|
-
open_tabs: number;
|
|
412
|
-
total_locked: number;
|
|
413
|
-
}>("/status");
|
|
414
|
-
return {
|
|
415
|
-
address: raw.wallet,
|
|
416
|
-
balance: raw.balance_usdc ? Number(raw.balance_usdc) : 0,
|
|
417
|
-
openTabs: [],
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// ── Webhooks ────────────────────────────────────────────────────
|
|
422
|
-
|
|
423
|
-
async registerWebhook(
|
|
424
|
-
url: string,
|
|
425
|
-
options: { events?: string[]; secret?: string } = {}
|
|
426
|
-
): Promise<WebhookRegistration> {
|
|
427
|
-
const payload: Record<string, unknown> = { url };
|
|
428
|
-
if (options.events) payload.events = options.events;
|
|
429
|
-
if (options.secret) payload.secret = options.secret;
|
|
430
|
-
const raw = await this.post<{ id: string; wallet: string; url: string; events: string[]; active: boolean }>("/webhooks", payload);
|
|
431
|
-
return { webhookId: raw.id, url: raw.url, events: raw.events };
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
async listWebhooks(): Promise<WebhookRegistration[]> {
|
|
435
|
-
const raw = await this.get<{ id: string; wallet: string; url: string; events: string[]; active: boolean }[]>("/webhooks");
|
|
436
|
-
return raw.map(w => ({ webhookId: w.id, url: w.url, events: w.events }));
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async deleteWebhook(webhookId: string): Promise<void> {
|
|
440
|
-
await this.del(`/webhooks/${webhookId}`);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// ── Funding ─────────────────────────────────────────────────────
|
|
444
|
-
|
|
445
|
-
/** Create a one-time fund link via the server. Returns the dashboard URL. */
|
|
446
|
-
async createFundLink(options?: {
|
|
447
|
-
messages?: unknown[];
|
|
448
|
-
agentName?: string;
|
|
449
|
-
}): Promise<string> {
|
|
450
|
-
const data = await this.post<{ url: string }>("/links/fund", {
|
|
451
|
-
messages: options?.messages ?? [],
|
|
452
|
-
agent_name: options?.agentName,
|
|
453
|
-
});
|
|
454
|
-
return data.url;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/** Create a one-time withdraw link via the server. Returns the dashboard URL. */
|
|
458
|
-
async createWithdrawLink(options?: {
|
|
459
|
-
messages?: unknown[];
|
|
460
|
-
agentName?: string;
|
|
461
|
-
}): Promise<string> {
|
|
462
|
-
const data = await this.post<{ url: string }>("/links/withdraw", {
|
|
463
|
-
messages: options?.messages ?? [],
|
|
464
|
-
agent_name: options?.agentName,
|
|
465
|
-
});
|
|
466
|
-
return data.url;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// ── Discovery ──────────────────────────────────────────────────
|
|
470
|
-
|
|
471
|
-
/** Search for discoverable paid API services. Public, no auth required. */
|
|
472
|
-
async discover(options?: DiscoverOptions): Promise<DiscoverService[]> {
|
|
473
|
-
const params = new URLSearchParams();
|
|
474
|
-
if (options?.query) params.set("q", options.query);
|
|
475
|
-
if (options?.sort) params.set("sort", options.sort);
|
|
476
|
-
if (options?.category) params.set("category", options.category);
|
|
477
|
-
if (options?.settlement) params.set("settlement", options.settlement);
|
|
478
|
-
|
|
479
|
-
const qs = params.toString();
|
|
480
|
-
const url = `${this.apiUrl}/discover${qs ? `?${qs}` : ""}`;
|
|
481
|
-
const resp = await fetch(url);
|
|
482
|
-
|
|
483
|
-
if (!resp.ok) {
|
|
484
|
-
const body = await resp.text().catch(() => "");
|
|
485
|
-
throw new PayServerError(`discover failed: ${body}`, resp.status);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const data = (await resp.json()) as { services: DiscoverService[] };
|
|
489
|
-
return data.services;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
// ── Permit signing ────────────────────────────────────────────
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Prepare and sign a USDC EIP-2612 permit.
|
|
496
|
-
*
|
|
497
|
-
* 1. Calls GET /api/v1/permit/prepare to get the EIP-712 hash
|
|
498
|
-
* 2. Signs the hash with the agent's private key
|
|
499
|
-
* 3. Returns {nonce, deadline, v, r, s} for inclusion in payment body
|
|
500
|
-
*/
|
|
501
|
-
private async prepareAndSignPermit(
|
|
502
|
-
amount: number,
|
|
503
|
-
spender: string
|
|
504
|
-
): Promise<{ nonce: string; deadline: number; v: number; r: string; s: string }> {
|
|
505
|
-
if (!this._privateKey) {
|
|
506
|
-
throw new PayValidationError(
|
|
507
|
-
"privateKey required for permit signing",
|
|
508
|
-
"privateKey"
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const prepare = await this.post<{
|
|
513
|
-
hash: string;
|
|
514
|
-
nonce: string;
|
|
515
|
-
deadline: number;
|
|
516
|
-
}>("/permit/prepare", { amount, spender });
|
|
517
|
-
|
|
518
|
-
// Sign the hash
|
|
519
|
-
const hashHex = prepare.hash as Hex;
|
|
520
|
-
const raw = await viemSign({ hash: hashHex, privateKey: this._privateKey });
|
|
521
|
-
const sigHex = serializeSignature(raw);
|
|
522
|
-
|
|
523
|
-
// Parse signature into v, r, s
|
|
524
|
-
const sigBytes = Buffer.from(sigHex.slice(2), "hex");
|
|
525
|
-
const r = "0x" + sigBytes.subarray(0, 32).toString("hex");
|
|
526
|
-
const s = "0x" + sigBytes.subarray(32, 64).toString("hex");
|
|
527
|
-
const v = sigBytes[64];
|
|
528
|
-
|
|
529
|
-
return {
|
|
530
|
-
nonce: prepare.nonce,
|
|
531
|
-
deadline: prepare.deadline,
|
|
532
|
-
v,
|
|
533
|
-
r,
|
|
534
|
-
s,
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// ── Auth headers ──────────────────────────────────────────────
|
|
539
|
-
|
|
540
|
-
private async authHeaders(
|
|
541
|
-
method: string,
|
|
542
|
-
path: string
|
|
543
|
-
): Promise<AuthHeaders | null> {
|
|
544
|
-
if (!this._authConfig) return null;
|
|
545
|
-
|
|
546
|
-
// Sign only the path portion (no query string) — server verifies against uri.path().
|
|
547
|
-
// e.g., basePath="/api/v1" + path="/status" → "/api/v1/status"
|
|
548
|
-
const fullPath = this._basePath + path.split("?")[0];
|
|
549
|
-
|
|
550
|
-
if (this._privateKey) {
|
|
551
|
-
return buildAuthHeaders(
|
|
552
|
-
this._privateKey,
|
|
553
|
-
method,
|
|
554
|
-
fullPath,
|
|
555
|
-
this._authConfig
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
if (this.signer.address) {
|
|
560
|
-
return buildAuthHeadersWithSigner(
|
|
561
|
-
this.signer,
|
|
562
|
-
method,
|
|
563
|
-
fullPath,
|
|
564
|
-
this._authConfig
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return null;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// ── HTTP helpers ────────────────────────────────────────────────
|
|
572
|
-
|
|
573
|
-
private async get<T>(path: string): Promise<T> {
|
|
574
|
-
let resp: Response;
|
|
575
|
-
try {
|
|
576
|
-
const auth = await this.authHeaders("GET", path);
|
|
577
|
-
resp = await fetch(`${this.apiUrl}${path}`, {
|
|
578
|
-
method: "GET",
|
|
579
|
-
headers: {
|
|
580
|
-
"Content-Type": "application/json",
|
|
581
|
-
...auth,
|
|
582
|
-
},
|
|
583
|
-
});
|
|
584
|
-
} catch (e) {
|
|
585
|
-
throw new PayNetworkError(String(e));
|
|
586
|
-
}
|
|
587
|
-
return this.handleResponse<T>(resp);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
private async post<T>(path: string, payload: unknown): Promise<T> {
|
|
591
|
-
let resp: Response;
|
|
592
|
-
try {
|
|
593
|
-
const auth = await this.authHeaders("POST", path);
|
|
594
|
-
resp = await fetch(`${this.apiUrl}${path}`, {
|
|
595
|
-
method: "POST",
|
|
596
|
-
headers: {
|
|
597
|
-
"Content-Type": "application/json",
|
|
598
|
-
...auth,
|
|
599
|
-
},
|
|
600
|
-
body: JSON.stringify(payload),
|
|
601
|
-
});
|
|
602
|
-
} catch (e) {
|
|
603
|
-
throw new PayNetworkError(String(e));
|
|
604
|
-
}
|
|
605
|
-
return this.handleResponse<T>(resp);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
private async del(path: string): Promise<void> {
|
|
609
|
-
let resp: Response;
|
|
610
|
-
try {
|
|
611
|
-
const auth = await this.authHeaders("DELETE", path);
|
|
612
|
-
resp = await fetch(`${this.apiUrl}${path}`, {
|
|
613
|
-
method: "DELETE",
|
|
614
|
-
headers: {
|
|
615
|
-
"Content-Type": "application/json",
|
|
616
|
-
...auth,
|
|
617
|
-
},
|
|
618
|
-
});
|
|
619
|
-
} catch (e) {
|
|
620
|
-
throw new PayNetworkError(String(e));
|
|
621
|
-
}
|
|
622
|
-
if (resp.status >= 400) {
|
|
623
|
-
const text = await resp.text();
|
|
624
|
-
throw new PayServerError(text, resp.status);
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
private async handleResponse<T>(resp: Response): Promise<T> {
|
|
629
|
-
if (resp.status >= 400) {
|
|
630
|
-
let msg: string;
|
|
631
|
-
try {
|
|
632
|
-
const body = (await resp.json()) as { error?: string };
|
|
633
|
-
msg = body.error ?? (await resp.text());
|
|
634
|
-
} catch {
|
|
635
|
-
msg = await resp.text();
|
|
636
|
-
}
|
|
637
|
-
throw new PayServerError(msg, resp.status);
|
|
638
|
-
}
|
|
639
|
-
if (resp.status === 204) {
|
|
640
|
-
return undefined as T;
|
|
641
|
-
}
|
|
642
|
-
return (await resp.json()) as T;
|
|
643
|
-
}
|
|
644
|
-
}
|