@secondlayer/sdk 7.0.0 → 8.0.1
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 +1 -5
- package/dist/index.d.ts +1 -158
- package/dist/index.js +1 -230
- package/dist/index.js.map +3 -4
- package/package.json +5 -8
- package/dist/x402.d.ts +0 -160
- package/dist/x402.js +0 -506
- package/dist/x402.js.map +0 -12
package/dist/x402.d.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { X402TokenSymbol } from "@secondlayer/shared/x402";
|
|
2
|
-
import { LocalAccount } from "@secondlayer/stacks/accounts";
|
|
3
|
-
import { StacksChain } from "@secondlayer/stacks/chains";
|
|
4
|
-
/**
|
|
5
|
-
* Client for the x402 pay-per-request rail. Turns a 402 challenge into a signed
|
|
6
|
-
* `PAYMENT-SIGNATURE` and retries — gasless (the agent signs origin-only; the
|
|
7
|
-
* facilitator sponsors the STX fee) and accountless (no key/Stripe/session).
|
|
8
|
-
*
|
|
9
|
-
* Three layers:
|
|
10
|
-
* - `withX402(fetch, opts)` — drop-in fetch that auto-pays on 402.
|
|
11
|
-
* - `createX402Client(opts)` — `.get/.post` returning `{ data, payment }`.
|
|
12
|
-
* - `buildSignedX402Payment` / `readX402Challenge` — primitives for custom transports.
|
|
13
|
-
*/
|
|
14
|
-
type X402Accept = {
|
|
15
|
-
scheme: "exact";
|
|
16
|
-
network: string;
|
|
17
|
-
asset: string;
|
|
18
|
-
/** Atomic units. */
|
|
19
|
-
amount: string;
|
|
20
|
-
payTo: string;
|
|
21
|
-
maxTimeoutSeconds: number;
|
|
22
|
-
extra: {
|
|
23
|
-
nonce: string;
|
|
24
|
-
};
|
|
25
|
-
};
|
|
26
|
-
type X402Challenge = {
|
|
27
|
-
x402Version: number;
|
|
28
|
-
accepts: X402Accept[];
|
|
29
|
-
error?: string;
|
|
30
|
-
};
|
|
31
|
-
/** Decoded `PAYMENT-RESPONSE` settlement receipt. */
|
|
32
|
-
type X402Receipt = {
|
|
33
|
-
success: boolean;
|
|
34
|
-
/** Settlement tier: `optimistic` (served on broadcast-accept, reconciled
|
|
35
|
-
* async) or `confirmed` (canonical). Absent on older deployments. */
|
|
36
|
-
state?: "optimistic" | "confirmed";
|
|
37
|
-
txid: string;
|
|
38
|
-
payer: string;
|
|
39
|
-
network: string;
|
|
40
|
-
};
|
|
41
|
-
/** Bitcoin-native first — sBTC is the compelling micropay asset; USDCx the
|
|
42
|
-
* dollar peg; STX the fallback. Override via `preferAssets`. */
|
|
43
|
-
declare const DEFAULT_PREFER_ASSETS: X402TokenSymbol[];
|
|
44
|
-
/** Default node for auto-resolving the payer's account nonce (`/v2/accounts`). */
|
|
45
|
-
declare const DEFAULT_NONCE_NODE_URL = "https://api.hiro.so";
|
|
46
|
-
/** Thrown when no offered asset is within the caller's spend guard / preferences. */
|
|
47
|
-
declare class X402SpendGuardError extends Error {
|
|
48
|
-
constructor(message: string);
|
|
49
|
-
}
|
|
50
|
-
/** Read the x402 challenge from a 402 response — prefer the wire header, fall
|
|
51
|
-
* back to the JSON body. */
|
|
52
|
-
declare function readX402Challenge(res: Response): Promise<X402Challenge | null>;
|
|
53
|
-
/** Read the settlement receipt from a paid 200 response. */
|
|
54
|
-
declare function readX402Receipt(res: Response): X402Receipt | null;
|
|
55
|
-
type SelectOfferOptions = {
|
|
56
|
-
preferAssets?: X402TokenSymbol[];
|
|
57
|
-
maxAmountPerCall?: Partial<Record<X402TokenSymbol, bigint>>;
|
|
58
|
-
};
|
|
59
|
-
/** Choose an `accepts[]` entry by preference order, skipping any that exceed the
|
|
60
|
-
* per-asset spend cap. Throws {@link X402SpendGuardError} if none qualify. */
|
|
61
|
-
declare function selectOffer(challenge: X402Challenge, opts?: SelectOfferOptions): {
|
|
62
|
-
accept: X402Accept;
|
|
63
|
-
symbol: X402TokenSymbol;
|
|
64
|
-
};
|
|
65
|
-
/** Fetch the payer account's next nonce from a Stacks node (`/v2/accounts`). */
|
|
66
|
-
declare function resolveAccountNonce(address: string, nodeUrl?: string): Promise<number>;
|
|
67
|
-
type BuildSignedX402PaymentOptions = {
|
|
68
|
-
challenge: X402Challenge;
|
|
69
|
-
account: LocalAccount;
|
|
70
|
-
/** The payer account's next nonce. */
|
|
71
|
-
accountNonce: bigint | number | string;
|
|
72
|
-
/** Preferred asset string (e.g. an sBTC/USDCx id). Defaults to the first offer. */
|
|
73
|
-
asset?: string;
|
|
74
|
-
chain?: StacksChain;
|
|
75
|
-
};
|
|
76
|
-
/** Build a signed, base64 `PAYMENT-SIGNATURE` header for one `accepts[]` entry. */
|
|
77
|
-
declare function buildSignedX402Payment(opts: BuildSignedX402PaymentOptions): Promise<{
|
|
78
|
-
header: string;
|
|
79
|
-
accept: X402Accept;
|
|
80
|
-
}>;
|
|
81
|
-
type WithX402Options = SelectOfferOptions & {
|
|
82
|
-
account: LocalAccount;
|
|
83
|
-
/** Prepaid-credit token (PAYMENT-BALANCE) from a prior deposit — calls
|
|
84
|
-
* debit the tab server-side instead of settling on-chain per call. */
|
|
85
|
-
balanceToken?: string;
|
|
86
|
-
/** Autonomous treasury policy: when the tab's remaining balance (read from
|
|
87
|
-
* X-BALANCE-REMAINING-USD) drops below `whenBelow`, deposit `usd` more in
|
|
88
|
-
* the background (one on-chain payment) and adopt the fresh token. */
|
|
89
|
-
topUp?: {
|
|
90
|
-
usd: number;
|
|
91
|
-
whenBelow: number;
|
|
92
|
-
};
|
|
93
|
-
/** Override the payer nonce; auto-resolved from `nodeUrl` when omitted. */
|
|
94
|
-
accountNonce?: bigint | number | string;
|
|
95
|
-
/** Node for auto-nonce lookup. Defaults to {@link DEFAULT_NONCE_NODE_URL}. */
|
|
96
|
-
nodeUrl?: string;
|
|
97
|
-
chain?: StacksChain;
|
|
98
|
-
/** Fired just before the paid retry (the call then blocks on confirmed-tier
|
|
99
|
-
* settle, or returns near-instantly on an optimistic surface). */
|
|
100
|
-
onSettling?: (info: {
|
|
101
|
-
asset: string;
|
|
102
|
-
amount: string;
|
|
103
|
-
}) => void;
|
|
104
|
-
/** Abort the paid retry after this long. */
|
|
105
|
-
timeoutMs?: number;
|
|
106
|
-
};
|
|
107
|
-
type X402Fetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
108
|
-
/**
|
|
109
|
-
* Wrap a `fetch` so requests transparently pay on 402: select an offer (by
|
|
110
|
-
* `preferAssets` + `maxAmountPerCall`), resolve the nonce, sign origin-only, and
|
|
111
|
-
* retry once with `PAYMENT-SIGNATURE`. Returns the final `Response` (read the
|
|
112
|
-
* receipt with {@link readX402Receipt}).
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* const x402fetch = withX402(fetch, { account });
|
|
116
|
-
* const res = await x402fetch("http://127.0.0.1:3800/v1/index/events?event_type=ft_transfer");
|
|
117
|
-
*/
|
|
118
|
-
declare function withX402(baseFetch: typeof fetch, opts: WithX402Options): X402Fetch;
|
|
119
|
-
type X402ClientOptions = WithX402Options & {
|
|
120
|
-
/** Instance API. Defaults to {@link resolveBaseUrl}. */
|
|
121
|
-
baseUrl?: string;
|
|
122
|
-
/** Override the underlying fetch (tests). */
|
|
123
|
-
fetch?: typeof fetch;
|
|
124
|
-
};
|
|
125
|
-
type X402Result<T = unknown> = {
|
|
126
|
-
data: T;
|
|
127
|
-
/** Settlement receipt, or null if the response carried none. */
|
|
128
|
-
payment: X402Receipt | null;
|
|
129
|
-
response: Response;
|
|
130
|
-
};
|
|
131
|
-
type X402Client = {
|
|
132
|
-
get<T = unknown>(path: string, o?: {
|
|
133
|
-
query?: Record<string, string>;
|
|
134
|
-
}): Promise<X402Result<T>>;
|
|
135
|
-
post<T = unknown>(path: string, o?: {
|
|
136
|
-
body?: unknown;
|
|
137
|
-
}): Promise<X402Result<T>>;
|
|
138
|
-
};
|
|
139
|
-
/**
|
|
140
|
-
* A small client over {@link withX402}: `.get/.post` against `baseUrl`, returning
|
|
141
|
-
* parsed JSON plus the settlement receipt.
|
|
142
|
-
*
|
|
143
|
-
* @example
|
|
144
|
-
* const sl = createX402Client({ account, baseUrl: "http://127.0.0.1:3800" });
|
|
145
|
-
* const { data, payment } = await sl.get("/v1/index/events", { query: { event_type: "ft_transfer" } });
|
|
146
|
-
*/
|
|
147
|
-
declare function createX402Client(opts: X402ClientOptions): X402Client;
|
|
148
|
-
/** Caller-supplied fetch that re-runs the request with the given extra headers. */
|
|
149
|
-
type X402FetchFn = (extraHeaders: Record<string, string>) => Promise<Response>;
|
|
150
|
-
type PayAndRetryOptions = Omit<WithX402Options, never> & {
|
|
151
|
-
/** Required here (use `withX402` for auto-nonce). */
|
|
152
|
-
accountNonce: bigint | number | string;
|
|
153
|
-
};
|
|
154
|
-
/**
|
|
155
|
-
* Low-level: run a request via a caller-controlled fetch; if it 402s, pay and
|
|
156
|
-
* retry once. Prefer {@link withX402} for the common case (it auto-resolves the
|
|
157
|
-
* nonce + selects the asset). Kept for custom transports.
|
|
158
|
-
*/
|
|
159
|
-
declare function payAndRetry(doFetch: X402FetchFn, opts: PayAndRetryOptions): Promise<Response>;
|
|
160
|
-
export { withX402, selectOffer, resolveAccountNonce, readX402Receipt, readX402Challenge, payAndRetry, createX402Client, buildSignedX402Payment, X402SpendGuardError, X402Result, X402Receipt, X402FetchFn, X402Fetch, X402ClientOptions, X402Client, X402Challenge, X402Accept, WithX402Options, SelectOfferOptions, PayAndRetryOptions, DEFAULT_PREFER_ASSETS, DEFAULT_NONCE_NODE_URL, BuildSignedX402PaymentOptions };
|
package/dist/x402.js
DELETED
|
@@ -1,506 +0,0 @@
|
|
|
1
|
-
// src/x402.ts
|
|
2
|
-
import {
|
|
3
|
-
X402_TOKENS,
|
|
4
|
-
findX402TokenByAsset
|
|
5
|
-
} from "@secondlayer/shared/x402";
|
|
6
|
-
import { http, createPublicClient } from "@secondlayer/stacks";
|
|
7
|
-
import { getNonce } from "@secondlayer/stacks/actions";
|
|
8
|
-
import {
|
|
9
|
-
serializeTransactionHex,
|
|
10
|
-
signTransactionWithAccount
|
|
11
|
-
} from "@secondlayer/stacks/transactions";
|
|
12
|
-
import { buildExactTransfer } from "@secondlayer/stacks/x402";
|
|
13
|
-
|
|
14
|
-
// src/errors.ts
|
|
15
|
-
var DOCS_BASE = "https://www.secondlayer.tools";
|
|
16
|
-
|
|
17
|
-
class SecondLayerError extends Error {
|
|
18
|
-
name = "SecondLayerError";
|
|
19
|
-
shortMessage;
|
|
20
|
-
code;
|
|
21
|
-
docsUrl;
|
|
22
|
-
metaMessages;
|
|
23
|
-
retryable;
|
|
24
|
-
retryAfterSeconds;
|
|
25
|
-
constructor(shortMessage, options = {}) {
|
|
26
|
-
const detailBlocks = [
|
|
27
|
-
...options.metaMessages ?? [],
|
|
28
|
-
...options.details ? [options.details] : [],
|
|
29
|
-
...options.docsUrl ? [`Docs: ${options.docsUrl}`] : []
|
|
30
|
-
];
|
|
31
|
-
const message = [
|
|
32
|
-
shortMessage,
|
|
33
|
-
detailBlocks.length > 0 ? `
|
|
34
|
-
${detailBlocks.join(`
|
|
35
|
-
`)}` : ""
|
|
36
|
-
].join("");
|
|
37
|
-
super(message, options.cause ? { cause: options.cause } : undefined);
|
|
38
|
-
this.shortMessage = shortMessage;
|
|
39
|
-
this.code = options.code;
|
|
40
|
-
this.docsUrl = options.docsUrl;
|
|
41
|
-
this.metaMessages = options.metaMessages;
|
|
42
|
-
this.retryable = options.retryable ?? false;
|
|
43
|
-
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
44
|
-
}
|
|
45
|
-
walk(fn) {
|
|
46
|
-
let err = this;
|
|
47
|
-
let last = this;
|
|
48
|
-
while (err != null) {
|
|
49
|
-
if (fn?.(err))
|
|
50
|
-
return err;
|
|
51
|
-
last = err;
|
|
52
|
-
err = err.cause;
|
|
53
|
-
}
|
|
54
|
-
return fn ? null : last;
|
|
55
|
-
}
|
|
56
|
-
toJSON() {
|
|
57
|
-
return {
|
|
58
|
-
name: this.name,
|
|
59
|
-
message: this.message,
|
|
60
|
-
shortMessage: this.shortMessage,
|
|
61
|
-
cause: this.cause instanceof Error ? this.cause.message : undefined,
|
|
62
|
-
code: this.code,
|
|
63
|
-
docsUrl: this.docsUrl,
|
|
64
|
-
retryable: this.retryable,
|
|
65
|
-
retryAfterSeconds: this.retryAfterSeconds
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function statusRetryable(status) {
|
|
70
|
-
return status === 429 || status >= 500;
|
|
71
|
-
}
|
|
72
|
-
function docsFor(status, code) {
|
|
73
|
-
if (code === "UPGRADE_REQUIRED" || status === 402)
|
|
74
|
-
return `${DOCS_BASE}/docs/authentication#pay-as-you-go-credits`;
|
|
75
|
-
if (status === 429 || status === 401)
|
|
76
|
-
return `${DOCS_BASE}/docs/authentication`;
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
class ApiError extends SecondLayerError {
|
|
81
|
-
status;
|
|
82
|
-
body;
|
|
83
|
-
constructor(status, message, body, code, options = {}) {
|
|
84
|
-
super(message, {
|
|
85
|
-
code,
|
|
86
|
-
retryable: options.retryable ?? statusRetryable(status),
|
|
87
|
-
docsUrl: options.docsUrl ?? docsFor(status, code),
|
|
88
|
-
...options.cause ? { cause: options.cause } : {},
|
|
89
|
-
...options.details ? { details: options.details } : {},
|
|
90
|
-
...options.metaMessages ? { metaMessages: options.metaMessages } : {},
|
|
91
|
-
...options.retryAfterSeconds !== undefined ? { retryAfterSeconds: options.retryAfterSeconds } : {}
|
|
92
|
-
});
|
|
93
|
-
this.status = status;
|
|
94
|
-
this.body = body;
|
|
95
|
-
this.name = "ApiError";
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
class AuthError extends ApiError {
|
|
100
|
-
status = 401;
|
|
101
|
-
constructor(message = "API key invalid or expired.") {
|
|
102
|
-
super(401, message);
|
|
103
|
-
this.name = "AuthError";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
class RateLimitError extends ApiError {
|
|
108
|
-
retryAfter;
|
|
109
|
-
status = 429;
|
|
110
|
-
constructor(message = "Rate limited. Try again later.", retryAfter) {
|
|
111
|
-
super(429, message, undefined, undefined, {
|
|
112
|
-
retryable: true,
|
|
113
|
-
retryAfterSeconds: parseRetryAfter(retryAfter)
|
|
114
|
-
});
|
|
115
|
-
this.retryAfter = retryAfter;
|
|
116
|
-
this.name = "RateLimitError";
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
class ValidationError extends ApiError {
|
|
121
|
-
constructor(message, status, body) {
|
|
122
|
-
super(status, message, body, undefined, { retryable: false });
|
|
123
|
-
this.name = "ValidationError";
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function parseRetryAfter(value) {
|
|
127
|
-
if (!value)
|
|
128
|
-
return;
|
|
129
|
-
const seconds = Number(value);
|
|
130
|
-
if (Number.isFinite(seconds) && seconds >= 0)
|
|
131
|
-
return seconds;
|
|
132
|
-
const date = Date.parse(value);
|
|
133
|
-
if (!Number.isNaN(date))
|
|
134
|
-
return Math.max(0, (date - Date.now()) / 1000);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
class VersionConflictError extends ApiError {
|
|
139
|
-
currentVersion;
|
|
140
|
-
expectedVersion;
|
|
141
|
-
constructor(currentVersion, expectedVersion, message = `Version conflict: expected ${expectedVersion}, current ${currentVersion}`) {
|
|
142
|
-
super(409, message, { currentVersion, expectedVersion });
|
|
143
|
-
this.currentVersion = currentVersion;
|
|
144
|
-
this.expectedVersion = expectedVersion;
|
|
145
|
-
this.name = "VersionConflictError";
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// src/base.ts
|
|
150
|
-
var LOCAL_API_URL = "http://127.0.0.1:3800";
|
|
151
|
-
function resolveBaseUrl(explicit) {
|
|
152
|
-
if (explicit && explicit.length > 0)
|
|
153
|
-
return explicit.replace(/\/+$/, "");
|
|
154
|
-
if (typeof process !== "undefined") {
|
|
155
|
-
const fromEnv = process.env?.SL_API_URL || process.env?.SECONDLAYER_API_URL;
|
|
156
|
-
if (fromEnv)
|
|
157
|
-
return fromEnv.replace(/\/+$/, "");
|
|
158
|
-
}
|
|
159
|
-
return LOCAL_API_URL;
|
|
160
|
-
}
|
|
161
|
-
function resolveApiKey(apiKey) {
|
|
162
|
-
if (apiKey !== undefined)
|
|
163
|
-
return apiKey;
|
|
164
|
-
if (typeof process === "undefined")
|
|
165
|
-
return;
|
|
166
|
-
return process.env?.SL_API_KEY || undefined;
|
|
167
|
-
}
|
|
168
|
-
function buildQuery(params) {
|
|
169
|
-
const search = new URLSearchParams;
|
|
170
|
-
for (const [name, value] of Object.entries(params)) {
|
|
171
|
-
if (value === undefined || value === null)
|
|
172
|
-
continue;
|
|
173
|
-
const serialized = Array.isArray(value) ? value.join(",") : String(value);
|
|
174
|
-
if (serialized.length === 0)
|
|
175
|
-
continue;
|
|
176
|
-
search.set(name, serialized);
|
|
177
|
-
}
|
|
178
|
-
const query = search.toString();
|
|
179
|
-
return query ? `?${query}` : "";
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
class BaseClient {
|
|
183
|
-
baseUrl;
|
|
184
|
-
apiKey;
|
|
185
|
-
origin;
|
|
186
|
-
fetchImpl;
|
|
187
|
-
constructor(options = {}) {
|
|
188
|
-
this.baseUrl = resolveBaseUrl(options.baseUrl);
|
|
189
|
-
this.apiKey = resolveApiKey(options.apiKey);
|
|
190
|
-
this.origin = options.origin ?? "cli";
|
|
191
|
-
this.fetchImpl = options.fetchImpl ?? ((...args) => fetch(...args));
|
|
192
|
-
}
|
|
193
|
-
static authHeaders(apiKey) {
|
|
194
|
-
const headers = {
|
|
195
|
-
"Content-Type": "application/json"
|
|
196
|
-
};
|
|
197
|
-
if (apiKey) {
|
|
198
|
-
headers.Authorization = `Bearer ${apiKey}`;
|
|
199
|
-
}
|
|
200
|
-
return headers;
|
|
201
|
-
}
|
|
202
|
-
async request(method, path, body) {
|
|
203
|
-
const response = await this.fetchResponse(method, path, body);
|
|
204
|
-
if (response.status === 204) {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
return response.json();
|
|
208
|
-
}
|
|
209
|
-
async requestOrNull(method, path, body) {
|
|
210
|
-
try {
|
|
211
|
-
return await this.request(method, path, body);
|
|
212
|
-
} catch (err) {
|
|
213
|
-
if (err instanceof ApiError && err.status === 404)
|
|
214
|
-
return null;
|
|
215
|
-
throw err;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
async requestText(method, path, body) {
|
|
219
|
-
const response = await this.fetchResponse(method, path, body);
|
|
220
|
-
return response.text();
|
|
221
|
-
}
|
|
222
|
-
async fetchResponse(method, path, body) {
|
|
223
|
-
const url = `${this.baseUrl}${path}`;
|
|
224
|
-
const headers = BaseClient.authHeaders(this.apiKey);
|
|
225
|
-
headers["x-sl-origin"] = this.origin;
|
|
226
|
-
let serializedBody;
|
|
227
|
-
if (body !== undefined && body !== null) {
|
|
228
|
-
try {
|
|
229
|
-
serializedBody = JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value);
|
|
230
|
-
} catch (err) {
|
|
231
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
232
|
-
throw new ApiError(0, `Failed to serialize request body: ${detail}`, undefined, undefined, { retryable: false });
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
let response;
|
|
236
|
-
try {
|
|
237
|
-
response = await this.fetchImpl(url, {
|
|
238
|
-
method,
|
|
239
|
-
headers,
|
|
240
|
-
body: serializedBody
|
|
241
|
-
});
|
|
242
|
-
} catch (err) {
|
|
243
|
-
throw new ApiError(0, `Cannot reach API at ${this.baseUrl}. Check your connection or try again.`, undefined, undefined, { retryable: true, cause: err instanceof Error ? err : undefined });
|
|
244
|
-
}
|
|
245
|
-
if (!response.ok) {
|
|
246
|
-
if (response.status === 401) {
|
|
247
|
-
throw new AuthError;
|
|
248
|
-
}
|
|
249
|
-
if (response.status === 429) {
|
|
250
|
-
const retryAfter = response.headers.get("Retry-After");
|
|
251
|
-
const msg = retryAfter ? `Rate limited. Wait ${retryAfter} seconds.` : "Rate limited. Try again later.";
|
|
252
|
-
throw new RateLimitError(msg, retryAfter ?? undefined);
|
|
253
|
-
}
|
|
254
|
-
if (response.status >= 500) {
|
|
255
|
-
throw new ApiError(response.status, `Server error. Try again or check status at ${this.baseUrl}/health`);
|
|
256
|
-
}
|
|
257
|
-
const errorBody = await response.text();
|
|
258
|
-
let message = `HTTP ${response.status}`;
|
|
259
|
-
let parsedBody = errorBody;
|
|
260
|
-
let code;
|
|
261
|
-
try {
|
|
262
|
-
const json = JSON.parse(errorBody);
|
|
263
|
-
parsedBody = json;
|
|
264
|
-
const err = json.error ?? json.message;
|
|
265
|
-
if (typeof err === "string") {
|
|
266
|
-
message = err;
|
|
267
|
-
} else if (err && typeof err === "object") {
|
|
268
|
-
message = JSON.stringify(err);
|
|
269
|
-
}
|
|
270
|
-
if (typeof json.code === "string") {
|
|
271
|
-
code = json.code;
|
|
272
|
-
}
|
|
273
|
-
} catch {
|
|
274
|
-
if (errorBody)
|
|
275
|
-
message = errorBody;
|
|
276
|
-
}
|
|
277
|
-
throw new ApiError(response.status, message, parsedBody, code);
|
|
278
|
-
}
|
|
279
|
-
return response;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// src/x402.ts
|
|
284
|
-
var DEFAULT_PREFER_ASSETS = [
|
|
285
|
-
"sBTC",
|
|
286
|
-
"USDCx",
|
|
287
|
-
"STX"
|
|
288
|
-
];
|
|
289
|
-
var DEFAULT_NONCE_NODE_URL = "https://api.hiro.so";
|
|
290
|
-
|
|
291
|
-
class X402SpendGuardError extends Error {
|
|
292
|
-
constructor(message) {
|
|
293
|
-
super(message);
|
|
294
|
-
this.name = "X402SpendGuardError";
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
function b64encode(value) {
|
|
298
|
-
const utf8 = new TextEncoder().encode(JSON.stringify(value));
|
|
299
|
-
let binary = "";
|
|
300
|
-
for (const byte of utf8)
|
|
301
|
-
binary += String.fromCharCode(byte);
|
|
302
|
-
return btoa(binary);
|
|
303
|
-
}
|
|
304
|
-
function b64decodeJson(value) {
|
|
305
|
-
try {
|
|
306
|
-
const binary = atob(value);
|
|
307
|
-
const utf8 = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
|
|
308
|
-
return JSON.parse(new TextDecoder().decode(utf8));
|
|
309
|
-
} catch {
|
|
310
|
-
return null;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
async function readX402Challenge(res) {
|
|
314
|
-
const header = res.headers.get("PAYMENT-REQUIRED");
|
|
315
|
-
if (header) {
|
|
316
|
-
const decoded = b64decodeJson(header);
|
|
317
|
-
if (decoded)
|
|
318
|
-
return decoded;
|
|
319
|
-
}
|
|
320
|
-
try {
|
|
321
|
-
return await res.clone().json();
|
|
322
|
-
} catch {
|
|
323
|
-
return null;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
function readX402Receipt(res) {
|
|
327
|
-
const header = res.headers.get("PAYMENT-RESPONSE");
|
|
328
|
-
return header ? b64decodeJson(header) : null;
|
|
329
|
-
}
|
|
330
|
-
function selectOffer(challenge, opts = {}) {
|
|
331
|
-
const prefer = opts.preferAssets ?? DEFAULT_PREFER_ASSETS;
|
|
332
|
-
for (const symbol of prefer) {
|
|
333
|
-
const token = X402_TOKENS[symbol];
|
|
334
|
-
const accept = challenge.accepts.find((a) => a.asset === token.asset);
|
|
335
|
-
if (!accept)
|
|
336
|
-
continue;
|
|
337
|
-
const cap = opts.maxAmountPerCall?.[symbol];
|
|
338
|
-
if (cap !== undefined && BigInt(accept.amount) > cap)
|
|
339
|
-
continue;
|
|
340
|
-
return { accept, symbol };
|
|
341
|
-
}
|
|
342
|
-
throw new X402SpendGuardError("no x402 offer matched preferAssets within maxAmountPerCall");
|
|
343
|
-
}
|
|
344
|
-
async function resolveAccountNonce(address, nodeUrl = DEFAULT_NONCE_NODE_URL) {
|
|
345
|
-
const client = createPublicClient({ transport: http(nodeUrl) });
|
|
346
|
-
return Number(await getNonce(client, { address }));
|
|
347
|
-
}
|
|
348
|
-
async function buildSignedX402Payment(opts) {
|
|
349
|
-
const accept = opts.asset ? opts.challenge.accepts.find((a) => a.asset === opts.asset) : opts.challenge.accepts[0];
|
|
350
|
-
if (!accept) {
|
|
351
|
-
throw new Error(`No x402 offer${opts.asset ? ` for asset ${opts.asset}` : ""}`);
|
|
352
|
-
}
|
|
353
|
-
const token = findX402TokenByAsset(accept.asset);
|
|
354
|
-
if (!token)
|
|
355
|
-
throw new Error(`Unknown x402 asset: ${accept.asset}`);
|
|
356
|
-
const asset = token.contractId && token.assetName ? {
|
|
357
|
-
kind: "sip010",
|
|
358
|
-
contractId: token.contractId,
|
|
359
|
-
assetName: token.assetName
|
|
360
|
-
} : { kind: "stx" };
|
|
361
|
-
const tx = buildExactTransfer({
|
|
362
|
-
asset,
|
|
363
|
-
amount: BigInt(accept.amount),
|
|
364
|
-
payTo: accept.payTo,
|
|
365
|
-
payer: opts.account.address,
|
|
366
|
-
payerPublicKey: opts.account.publicKey,
|
|
367
|
-
accountNonce: opts.accountNonce,
|
|
368
|
-
nonce: accept.extra.nonce,
|
|
369
|
-
chain: opts.chain
|
|
370
|
-
});
|
|
371
|
-
const signed = await signTransactionWithAccount(tx, opts.account);
|
|
372
|
-
const header = b64encode({
|
|
373
|
-
x402Version: opts.challenge.x402Version ?? 2,
|
|
374
|
-
scheme: "exact",
|
|
375
|
-
network: accept.network,
|
|
376
|
-
asset: accept.asset,
|
|
377
|
-
payload: { transaction: serializeTransactionHex(signed) },
|
|
378
|
-
extra: { nonce: accept.extra.nonce }
|
|
379
|
-
});
|
|
380
|
-
return { header, accept };
|
|
381
|
-
}
|
|
382
|
-
function withX402(baseFetch, opts) {
|
|
383
|
-
const sessions = new Map;
|
|
384
|
-
let balanceToken = opts.balanceToken ?? null;
|
|
385
|
-
let toppingUp = false;
|
|
386
|
-
const originOf = (input) => {
|
|
387
|
-
try {
|
|
388
|
-
return new URL(String(input)).origin;
|
|
389
|
-
} catch {
|
|
390
|
-
return "";
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
const wrapped = async (input, init) => {
|
|
394
|
-
const origin = originOf(input);
|
|
395
|
-
const run = (extra, signal2) => baseFetch(input, {
|
|
396
|
-
...init,
|
|
397
|
-
headers: { ...init?.headers, ...extra },
|
|
398
|
-
...signal2 ? { signal: signal2 } : {}
|
|
399
|
-
});
|
|
400
|
-
const maybeTopUp = (res) => {
|
|
401
|
-
const policy = opts.topUp;
|
|
402
|
-
if (!policy || toppingUp || !origin)
|
|
403
|
-
return;
|
|
404
|
-
const remaining = res.headers.get("X-BALANCE-REMAINING-USD");
|
|
405
|
-
if (remaining === null || Number(remaining) >= policy.whenBelow)
|
|
406
|
-
return;
|
|
407
|
-
toppingUp = true;
|
|
408
|
-
(async () => {
|
|
409
|
-
try {
|
|
410
|
-
const dep = await wrapped(`${origin}/v1/x402/deposit?usd=${policy.usd}`, { method: "POST" });
|
|
411
|
-
if (dep.ok) {
|
|
412
|
-
const body = await dep.json();
|
|
413
|
-
if (body.balance_token)
|
|
414
|
-
balanceToken = body.balance_token;
|
|
415
|
-
}
|
|
416
|
-
} catch {} finally {
|
|
417
|
-
toppingUp = false;
|
|
418
|
-
}
|
|
419
|
-
})();
|
|
420
|
-
};
|
|
421
|
-
const remember = (res) => {
|
|
422
|
-
const voucher = res.headers.get("PAYMENT-SESSION");
|
|
423
|
-
if (voucher && origin)
|
|
424
|
-
sessions.set(origin, voucher);
|
|
425
|
-
maybeTopUp(res);
|
|
426
|
-
return res;
|
|
427
|
-
};
|
|
428
|
-
const cached = origin ? sessions.get(origin) : undefined;
|
|
429
|
-
const first = await run({
|
|
430
|
-
...cached ? { "PAYMENT-SESSION": cached } : {},
|
|
431
|
-
...balanceToken ? { "PAYMENT-BALANCE": balanceToken } : {}
|
|
432
|
-
});
|
|
433
|
-
if (first.status !== 402)
|
|
434
|
-
return remember(first);
|
|
435
|
-
if (cached && origin)
|
|
436
|
-
sessions.delete(origin);
|
|
437
|
-
const challenge = await readX402Challenge(first);
|
|
438
|
-
if (!challenge)
|
|
439
|
-
return first;
|
|
440
|
-
const { accept } = selectOffer(challenge, opts);
|
|
441
|
-
const accountNonce = opts.accountNonce ?? await resolveAccountNonce(opts.account.address, opts.nodeUrl);
|
|
442
|
-
const { header } = await buildSignedX402Payment({
|
|
443
|
-
challenge,
|
|
444
|
-
account: opts.account,
|
|
445
|
-
accountNonce,
|
|
446
|
-
asset: accept.asset,
|
|
447
|
-
chain: opts.chain
|
|
448
|
-
});
|
|
449
|
-
opts.onSettling?.({ asset: accept.asset, amount: accept.amount });
|
|
450
|
-
const signal = opts.timeoutMs ? AbortSignal.timeout(opts.timeoutMs) : undefined;
|
|
451
|
-
return remember(await run({ "PAYMENT-SIGNATURE": header }, signal));
|
|
452
|
-
};
|
|
453
|
-
return wrapped;
|
|
454
|
-
}
|
|
455
|
-
function createX402Client(opts) {
|
|
456
|
-
const f = withX402(opts.fetch ?? fetch, opts);
|
|
457
|
-
const base = resolveBaseUrl(opts.baseUrl);
|
|
458
|
-
async function request(method, path, o = {}) {
|
|
459
|
-
const qs = o.query ? `?${new URLSearchParams(o.query).toString()}` : "";
|
|
460
|
-
const init = { method };
|
|
461
|
-
if (o.body !== undefined) {
|
|
462
|
-
init.body = JSON.stringify(o.body);
|
|
463
|
-
init.headers = { "content-type": "application/json" };
|
|
464
|
-
}
|
|
465
|
-
const response = await f(`${base}${path}${qs}`, init);
|
|
466
|
-
const data = await response.json().catch(() => null);
|
|
467
|
-
return { data, payment: readX402Receipt(response), response };
|
|
468
|
-
}
|
|
469
|
-
return {
|
|
470
|
-
get: (path, o) => request("GET", path, o),
|
|
471
|
-
post: (path, o) => request("POST", path, o)
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
async function payAndRetry(doFetch, opts) {
|
|
475
|
-
const first = await doFetch({});
|
|
476
|
-
if (first.status !== 402)
|
|
477
|
-
return first;
|
|
478
|
-
const challenge = await readX402Challenge(first);
|
|
479
|
-
if (!challenge)
|
|
480
|
-
return first;
|
|
481
|
-
const { accept } = selectOffer(challenge, opts);
|
|
482
|
-
const { header } = await buildSignedX402Payment({
|
|
483
|
-
challenge,
|
|
484
|
-
account: opts.account,
|
|
485
|
-
accountNonce: opts.accountNonce,
|
|
486
|
-
asset: accept.asset,
|
|
487
|
-
chain: opts.chain
|
|
488
|
-
});
|
|
489
|
-
return doFetch({ "PAYMENT-SIGNATURE": header });
|
|
490
|
-
}
|
|
491
|
-
export {
|
|
492
|
-
withX402,
|
|
493
|
-
selectOffer,
|
|
494
|
-
resolveAccountNonce,
|
|
495
|
-
readX402Receipt,
|
|
496
|
-
readX402Challenge,
|
|
497
|
-
payAndRetry,
|
|
498
|
-
createX402Client,
|
|
499
|
-
buildSignedX402Payment,
|
|
500
|
-
X402SpendGuardError,
|
|
501
|
-
DEFAULT_PREFER_ASSETS,
|
|
502
|
-
DEFAULT_NONCE_NODE_URL
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
//# debugId=BE9E5DFBC321406664756E2164756E21
|
|
506
|
-
//# sourceMappingURL=x402.js.map
|