@remitmd/sdk 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.
Files changed (132) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +250 -0
  3. package/dist/a2a.d.ts +137 -0
  4. package/dist/a2a.d.ts.map +1 -0
  5. package/dist/a2a.js +121 -0
  6. package/dist/a2a.js.map +1 -0
  7. package/dist/client.d.ts +41 -0
  8. package/dist/client.d.ts.map +1 -0
  9. package/dist/client.js +81 -0
  10. package/dist/client.js.map +1 -0
  11. package/dist/errors.d.ts +108 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +218 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/http.d.ts +23 -0
  16. package/dist/http.d.ts.map +1 -0
  17. package/dist/http.js +150 -0
  18. package/dist/http.js.map +1 -0
  19. package/dist/index.d.ts +18 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +21 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/integrations/vercel-ai.d.ts +44 -0
  24. package/dist/integrations/vercel-ai.d.ts.map +1 -0
  25. package/dist/integrations/vercel-ai.js +175 -0
  26. package/dist/integrations/vercel-ai.js.map +1 -0
  27. package/dist/models/bounty.d.ts +22 -0
  28. package/dist/models/bounty.d.ts.map +1 -0
  29. package/dist/models/bounty.js +2 -0
  30. package/dist/models/bounty.js.map +1 -0
  31. package/dist/models/common.d.ts +78 -0
  32. package/dist/models/common.d.ts.map +1 -0
  33. package/dist/models/common.js +3 -0
  34. package/dist/models/common.js.map +1 -0
  35. package/dist/models/deposit.d.ts +13 -0
  36. package/dist/models/deposit.d.ts.map +1 -0
  37. package/dist/models/deposit.js +2 -0
  38. package/dist/models/deposit.js.map +1 -0
  39. package/dist/models/escrow.d.ts +16 -0
  40. package/dist/models/escrow.d.ts.map +1 -0
  41. package/dist/models/escrow.js +2 -0
  42. package/dist/models/escrow.js.map +1 -0
  43. package/dist/models/index.d.ts +9 -0
  44. package/dist/models/index.d.ts.map +1 -0
  45. package/dist/models/index.js +9 -0
  46. package/dist/models/index.js.map +1 -0
  47. package/dist/models/invoice.d.ts +30 -0
  48. package/dist/models/invoice.d.ts.map +1 -0
  49. package/dist/models/invoice.js +2 -0
  50. package/dist/models/invoice.js.map +1 -0
  51. package/dist/models/reputation.d.ts +7 -0
  52. package/dist/models/reputation.d.ts.map +1 -0
  53. package/dist/models/reputation.js +2 -0
  54. package/dist/models/reputation.js.map +1 -0
  55. package/dist/models/stream.d.ts +15 -0
  56. package/dist/models/stream.d.ts.map +1 -0
  57. package/dist/models/stream.js +2 -0
  58. package/dist/models/stream.js.map +1 -0
  59. package/dist/models/tab.d.ts +21 -0
  60. package/dist/models/tab.d.ts.map +1 -0
  61. package/dist/models/tab.js +2 -0
  62. package/dist/models/tab.js.map +1 -0
  63. package/dist/provider.d.ts +135 -0
  64. package/dist/provider.d.ts.map +1 -0
  65. package/dist/provider.js +218 -0
  66. package/dist/provider.js.map +1 -0
  67. package/dist/signer.d.ts +31 -0
  68. package/dist/signer.d.ts.map +1 -0
  69. package/dist/signer.js +35 -0
  70. package/dist/signer.js.map +1 -0
  71. package/dist/testing/local.d.ts +31 -0
  72. package/dist/testing/local.d.ts.map +1 -0
  73. package/dist/testing/local.js +100 -0
  74. package/dist/testing/local.js.map +1 -0
  75. package/dist/testing/mock.d.ts +95 -0
  76. package/dist/testing/mock.d.ts.map +1 -0
  77. package/dist/testing/mock.js +407 -0
  78. package/dist/testing/mock.js.map +1 -0
  79. package/dist/wallet.d.ts +162 -0
  80. package/dist/wallet.d.ts.map +1 -0
  81. package/dist/wallet.js +365 -0
  82. package/dist/wallet.js.map +1 -0
  83. package/dist/x402.d.ts +78 -0
  84. package/dist/x402.d.ts.map +1 -0
  85. package/dist/x402.js +151 -0
  86. package/dist/x402.js.map +1 -0
  87. package/eslint.config.js +27 -0
  88. package/package.json +39 -0
  89. package/src/a2a.ts +241 -0
  90. package/src/client.ts +104 -0
  91. package/src/errors.ts +261 -0
  92. package/src/http.ts +190 -0
  93. package/src/index.ts +94 -0
  94. package/src/integrations/vercel-ai.ts +213 -0
  95. package/src/models/bounty.ts +23 -0
  96. package/src/models/common.ts +106 -0
  97. package/src/models/deposit.ts +13 -0
  98. package/src/models/escrow.ts +16 -0
  99. package/src/models/index.ts +8 -0
  100. package/src/models/invoice.ts +32 -0
  101. package/src/models/reputation.ts +7 -0
  102. package/src/models/stream.ts +15 -0
  103. package/src/models/tab.ts +22 -0
  104. package/src/provider.ts +281 -0
  105. package/src/signer.ts +70 -0
  106. package/src/testing/local.ts +118 -0
  107. package/src/testing/mock.ts +507 -0
  108. package/src/wallet.ts +546 -0
  109. package/src/x402.ts +202 -0
  110. package/tests/acceptance/bounty.test.ts +82 -0
  111. package/tests/acceptance/deposit.test.ts +70 -0
  112. package/tests/acceptance/direct.test.ts +53 -0
  113. package/tests/acceptance/escrow.test.ts +67 -0
  114. package/tests/acceptance/setup.ts +113 -0
  115. package/tests/acceptance/stream.test.ts +98 -0
  116. package/tests/acceptance/tab.test.ts +108 -0
  117. package/tests/acceptance/x402.test.ts +140 -0
  118. package/tests/compliance/auth.ts +69 -0
  119. package/tests/compliance/escrows.ts +96 -0
  120. package/tests/compliance/helpers.ts +90 -0
  121. package/tests/compliance/payments.ts +69 -0
  122. package/tests/compliance/tabs.ts +52 -0
  123. package/tests/test_a2a.ts +151 -0
  124. package/tests/test_errors.ts +80 -0
  125. package/tests/test_golden_vectors.ts +162 -0
  126. package/tests/test_integrations.ts +115 -0
  127. package/tests/test_mock.ts +217 -0
  128. package/tests/test_permit.ts +216 -0
  129. package/tests/test_provider.ts +304 -0
  130. package/tests/test_wallet.ts +108 -0
  131. package/tests/test_x402.ts +302 -0
  132. package/tsconfig.json +19 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * x402 service provider middleware for gating HTTP endpoints behind payments.
3
+ *
4
+ * Providers use this module to:
5
+ * - Return HTTP 402 responses with properly formatted `PAYMENT-REQUIRED` headers
6
+ * - Verify incoming `PAYMENT-SIGNATURE` headers against the remit.md facilitator
7
+ *
8
+ * Usage (Hono / Cloudflare Workers / any fetch-based framework):
9
+ * ```typescript
10
+ * import { X402Paywall } from "@remitmd/sdk/provider";
11
+ *
12
+ * const paywall = new X402Paywall({
13
+ * walletAddress: "0xYourProviderWallet",
14
+ * amountUsdc: 0.001,
15
+ * network: "eip155:84532",
16
+ * asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
17
+ * facilitatorToken: "your-bearer-jwt",
18
+ * resource: "/v1/data",
19
+ * description: "Realtime market data feed",
20
+ * mimeType: "application/json",
21
+ * });
22
+ *
23
+ * // Returns a 402 Response if payment is absent/invalid, null if payment ok.
24
+ * const block = await paywall.handle(request);
25
+ * if (block) return block;
26
+ * return new Response("here is your data");
27
+ * ```
28
+ *
29
+ * Usage (Hono):
30
+ * ```typescript
31
+ * app.use("/v1/*", paywall.honoMiddleware());
32
+ * ```
33
+ *
34
+ * Usage (Express-style):
35
+ * ```typescript
36
+ * app.get("/v1/data", paywall.expressMiddleware(), (req, res) => {
37
+ * res.json({ data: "..." });
38
+ * });
39
+ * ```
40
+ */
41
+
42
+ /** Configuration for {@link X402Paywall}. */
43
+ export interface PaywallOptions {
44
+ /** Provider's checksummed Ethereum address (the `payTo` field). */
45
+ walletAddress: string;
46
+ /** Price per request in USDC (e.g. `0.001`). */
47
+ amountUsdc: number;
48
+ /** CAIP-2 network string (e.g. `"eip155:84532"` for Base Sepolia). */
49
+ network: string;
50
+ /** USDC contract address on the target network. */
51
+ asset: string;
52
+ /** Base URL of the remit.md facilitator (default: `"https://remit.md"`). */
53
+ facilitatorUrl?: string;
54
+ /** Bearer JWT for authenticating calls to `/api/v0/x402/verify`. */
55
+ facilitatorToken?: string;
56
+ /** How long the payment authorization remains valid in seconds (default: 60). */
57
+ maxTimeoutSeconds?: number;
58
+ /** V2 — URL or path of the resource being protected (e.g. `"/v1/data"`). */
59
+ resource?: string;
60
+ /** V2 — Human-readable description of what the payment is for. */
61
+ description?: string;
62
+ /** V2 — MIME type of the resource (e.g. `"application/json"`). */
63
+ mimeType?: string;
64
+ }
65
+
66
+ /** Result of {@link X402Paywall.check}. */
67
+ export interface CheckResult {
68
+ isValid: boolean;
69
+ /** Populated when `isValid` is false and the signature was present. */
70
+ invalidReason?: string;
71
+ }
72
+
73
+ /** x402 paywall for service providers. */
74
+ export class X402Paywall {
75
+ readonly #walletAddress: string;
76
+ readonly #amountBaseUnits: string;
77
+ readonly #network: string;
78
+ readonly #asset: string;
79
+ readonly #facilitatorUrl: string;
80
+ readonly #facilitatorToken: string;
81
+ readonly #maxTimeoutSeconds: number;
82
+ readonly #resource: string | undefined;
83
+ readonly #description: string | undefined;
84
+ readonly #mimeType: string | undefined;
85
+
86
+ constructor({
87
+ walletAddress,
88
+ amountUsdc,
89
+ network,
90
+ asset,
91
+ facilitatorUrl = "https://remit.md",
92
+ facilitatorToken = "",
93
+ maxTimeoutSeconds = 60,
94
+ resource,
95
+ description,
96
+ mimeType,
97
+ }: PaywallOptions) {
98
+ this.#walletAddress = walletAddress;
99
+ this.#amountBaseUnits = String(Math.round(amountUsdc * 1_000_000));
100
+ this.#network = network;
101
+ this.#asset = asset;
102
+ this.#facilitatorUrl = facilitatorUrl.replace(/\/$/, "");
103
+ this.#facilitatorToken = facilitatorToken;
104
+ this.#maxTimeoutSeconds = maxTimeoutSeconds;
105
+ this.#resource = resource;
106
+ this.#description = description;
107
+ this.#mimeType = mimeType;
108
+ }
109
+
110
+ /** Return the base64-encoded JSON `PAYMENT-REQUIRED` header value. */
111
+ paymentRequiredHeader(): string {
112
+ const payload: Record<string, unknown> = {
113
+ scheme: "exact",
114
+ network: this.#network,
115
+ amount: this.#amountBaseUnits,
116
+ asset: this.#asset,
117
+ payTo: this.#walletAddress,
118
+ maxTimeoutSeconds: this.#maxTimeoutSeconds,
119
+ };
120
+ if (this.#resource !== undefined) payload["resource"] = this.#resource;
121
+ if (this.#description !== undefined) payload["description"] = this.#description;
122
+ if (this.#mimeType !== undefined) payload["mimeType"] = this.#mimeType;
123
+ return Buffer.from(JSON.stringify(payload)).toString("base64");
124
+ }
125
+
126
+ #paymentRequiredObject() {
127
+ return {
128
+ scheme: "exact",
129
+ network: this.#network,
130
+ amount: this.#amountBaseUnits,
131
+ asset: this.#asset,
132
+ payTo: this.#walletAddress,
133
+ maxTimeoutSeconds: this.#maxTimeoutSeconds,
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Check whether a `PAYMENT-SIGNATURE` header represents a valid payment.
139
+ *
140
+ * Calls the remit.md facilitator's `/api/v0/x402/verify` endpoint.
141
+ *
142
+ * @param paymentSig The raw header value (base64 JSON), or null if absent.
143
+ * @returns `{ isValid: true }` or `{ isValid: false, invalidReason }`.
144
+ */
145
+ async check(paymentSig: string | null): Promise<CheckResult> {
146
+ if (!paymentSig) {
147
+ return { isValid: false };
148
+ }
149
+
150
+ let paymentPayload: unknown;
151
+ try {
152
+ paymentPayload = JSON.parse(Buffer.from(paymentSig, "base64").toString("utf8"));
153
+ } catch {
154
+ return { isValid: false, invalidReason: "INVALID_PAYLOAD" };
155
+ }
156
+
157
+ const body = {
158
+ paymentPayload,
159
+ paymentRequired: this.#paymentRequiredObject(),
160
+ };
161
+
162
+ const headers: Record<string, string> = {
163
+ "Content-Type": "application/json",
164
+ };
165
+ if (this.#facilitatorToken) {
166
+ headers["Authorization"] = `Bearer ${this.#facilitatorToken}`;
167
+ }
168
+
169
+ let data: { isValid?: boolean; invalidReason?: string };
170
+ try {
171
+ const resp = await globalThis.fetch(`${this.#facilitatorUrl}/api/v0/x402/verify`, {
172
+ method: "POST",
173
+ headers,
174
+ body: JSON.stringify(body),
175
+ });
176
+ if (!resp.ok) {
177
+ return { isValid: false, invalidReason: "FACILITATOR_ERROR" };
178
+ }
179
+ data = (await resp.json()) as { isValid?: boolean; invalidReason?: string };
180
+ } catch {
181
+ return { isValid: false, invalidReason: "FACILITATOR_ERROR" };
182
+ }
183
+
184
+ return {
185
+ isValid: data.isValid === true,
186
+ invalidReason: data.invalidReason,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Web-standard (fetch API) middleware compatible with Hono, Cloudflare Workers, etc.
192
+ *
193
+ * Returns a 402 `Response` if payment is absent or invalid, or `null` to allow
194
+ * the request to proceed.
195
+ *
196
+ * @param request Incoming `Request` object.
197
+ */
198
+ async handle(request: Request): Promise<Response | null> {
199
+ const paymentSig = request.headers.get("payment-signature");
200
+ const result = await this.check(paymentSig);
201
+ if (!result.isValid) {
202
+ return new Response(JSON.stringify({ error: "Payment required", invalidReason: result.invalidReason }), {
203
+ status: 402,
204
+ headers: {
205
+ "Content-Type": "application/json",
206
+ "PAYMENT-REQUIRED": this.paymentRequiredHeader(),
207
+ },
208
+ });
209
+ }
210
+ return null;
211
+ }
212
+
213
+ /**
214
+ * Express-style middleware factory.
215
+ *
216
+ * @example
217
+ * ```typescript
218
+ * app.get("/v1/data", paywall.expressMiddleware(), (req, res) => {
219
+ * res.json({ data: "..." });
220
+ * });
221
+ * ```
222
+ */
223
+ expressMiddleware(): (
224
+ req: { headers: Record<string, string | string[] | undefined> },
225
+ res: {
226
+ status(code: number): { set(headers: Record<string, string>): { json(body: unknown): void } };
227
+ },
228
+ next: () => void,
229
+ ) => Promise<void> {
230
+ return async (req, res, next) => {
231
+ const raw = req.headers["payment-signature"];
232
+ const paymentSig = Array.isArray(raw) ? raw[0] ?? null : (raw ?? null);
233
+ const result = await this.check(paymentSig);
234
+ if (!result.isValid) {
235
+ res
236
+ .status(402)
237
+ .set({ "PAYMENT-REQUIRED": this.paymentRequiredHeader(), "Content-Type": "application/json" })
238
+ .json({ error: "Payment required", invalidReason: result.invalidReason });
239
+ return;
240
+ }
241
+ next();
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Hono middleware factory.
247
+ *
248
+ * Compatible with Hono v3/v4 and any framework that uses the same
249
+ * `(c, next) => Promise<void>` middleware signature.
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * import { Hono } from "hono";
254
+ * app.use("/v1/*", paywall.honoMiddleware());
255
+ * ```
256
+ */
257
+ honoMiddleware(): (
258
+ c: {
259
+ req: { raw: Request };
260
+ header(name: string, value: string): void;
261
+ body(content: string, status?: number): Response;
262
+ },
263
+ next: () => Promise<void>,
264
+ ) => Promise<Response | void> {
265
+ return async (c, next) => {
266
+ const paymentSig = c.req.raw.headers.get("payment-signature");
267
+ const result = await this.check(paymentSig);
268
+ if (!result.isValid) {
269
+ c.header("PAYMENT-REQUIRED", this.paymentRequiredHeader());
270
+ c.header("Content-Type", "application/json");
271
+ return c.body(JSON.stringify({ error: "Payment required", invalidReason: result.invalidReason }), 402);
272
+ }
273
+ await next();
274
+ };
275
+ }
276
+
277
+ /** Prevent sensitive config leakage. */
278
+ toJSON(): Record<string, string> {
279
+ return { walletAddress: this.#walletAddress, network: this.#network };
280
+ }
281
+ }
package/src/signer.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { privateKeyToAccount } from "viem/accounts";
2
+ import type { PrivateKeyAccount, SignTypedDataParameters } from "viem/accounts";
3
+
4
+ /** EIP-712 typed data domain. */
5
+ export interface TypedDataDomain {
6
+ name: string;
7
+ version: string;
8
+ chainId?: number;
9
+ verifyingContract?: string;
10
+ }
11
+
12
+ /** EIP-712 type definitions. */
13
+ export type TypedDataTypes = Record<string, Array<{ name: string; type: string }>>;
14
+
15
+ /** Pluggable signing interface. Implementations must isolate key material. */
16
+ export interface Signer {
17
+ /** Sign EIP-712 typed data and return hex signature. */
18
+ signTypedData(
19
+ domain: TypedDataDomain,
20
+ types: TypedDataTypes,
21
+ value: Record<string, unknown>,
22
+ ): Promise<string>;
23
+
24
+ /** Return the checksummed public address. */
25
+ getAddress(): string;
26
+ }
27
+
28
+ /** Default signer: raw private key via viem. Key is held in closure, never exposed. */
29
+ export class PrivateKeySigner implements Signer {
30
+ readonly #account: PrivateKeyAccount;
31
+
32
+ constructor(privateKey: string) {
33
+ // Normalise: ensure 0x prefix
34
+ const hex = (privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`) as `0x${string}`;
35
+ this.#account = privateKeyToAccount(hex);
36
+ }
37
+
38
+ /** Create from hex private key string. */
39
+ static fromHex(privateKey: string): PrivateKeySigner {
40
+ return new PrivateKeySigner(privateKey);
41
+ }
42
+
43
+ async signTypedData(
44
+ domain: TypedDataDomain,
45
+ types: TypedDataTypes,
46
+ value: Record<string, unknown>,
47
+ ): Promise<string> {
48
+ const primaryType = Object.keys(types).filter((k) => k !== "EIP712Domain")[0] ?? "Request";
49
+ // Cast through unknown to satisfy viem's highly-parameterized overloads
50
+ return this.#account.signTypedData({
51
+ domain,
52
+ types,
53
+ primaryType,
54
+ message: value,
55
+ } as unknown as SignTypedDataParameters);
56
+ }
57
+
58
+ getAddress(): string {
59
+ return this.#account.address;
60
+ }
61
+
62
+ /** Prevent key leakage in serialization. */
63
+ toJSON(): Record<string, string> {
64
+ return { address: this.#account.address };
65
+ }
66
+
67
+ [Symbol.for("nodejs.util.inspect.custom")](): string {
68
+ return `PrivateKeySigner { address: '${this.#account.address}' }`;
69
+ }
70
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * LocalChain — wraps a local Anvil instance for integration testing.
3
+ * Real EVM, <100ms per operation (requires Anvil in PATH).
4
+ */
5
+
6
+ import { spawn, type ChildProcess } from "node:child_process";
7
+ import { MockWallet, MockRemit } from "./mock.js";
8
+
9
+ /** Anvil default pre-funded accounts (well-known test keys). */
10
+ const ANVIL_KEYS: string[] = [
11
+ "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
12
+ "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
13
+ "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a",
14
+ "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6",
15
+ "0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926b",
16
+ ];
17
+
18
+ export interface LocalChainOptions {
19
+ port?: number;
20
+ blockTime?: number; // seconds; 0 = instant mining
21
+ }
22
+
23
+ export class LocalChain {
24
+ readonly #port: number;
25
+ readonly #mock: MockRemit;
26
+ #process: ChildProcess | null = null;
27
+
28
+ private constructor(port: number) {
29
+ this.#port = port;
30
+ this.#mock = new MockRemit();
31
+ }
32
+
33
+ /**
34
+ * Start Anvil, wait for it to be ready, and return a LocalChain instance.
35
+ * Throws if Anvil is not in PATH.
36
+ */
37
+ static async start(options: LocalChainOptions = {}): Promise<LocalChain> {
38
+ const port = options.port ?? 8545;
39
+ const chain = new LocalChain(port);
40
+ await chain.#spawn(options.blockTime ?? 0);
41
+ return chain;
42
+ }
43
+
44
+ async #spawn(blockTime: number): Promise<void> {
45
+ const args = ["--port", String(this.#port)];
46
+ if (blockTime > 0) args.push("--block-time", String(blockTime));
47
+
48
+ this.#process = spawn("anvil", args, { stdio: "pipe" });
49
+
50
+ await new Promise<void>((resolve, reject) => {
51
+ const timeout = setTimeout(() => reject(new Error("Anvil did not start within 5s")), 5000);
52
+
53
+ this.#process!.stdout?.on("data", (data: Buffer) => {
54
+ if (data.toString().includes("Listening on")) {
55
+ clearTimeout(timeout);
56
+ resolve();
57
+ }
58
+ });
59
+
60
+ this.#process!.on("error", (err) => {
61
+ clearTimeout(timeout);
62
+ reject(new Error(`Failed to start Anvil: ${err.message}. Is foundry installed?`));
63
+ });
64
+ });
65
+ }
66
+
67
+ /** Get a pre-funded wallet by index (0-4). Uses Anvil's default accounts. */
68
+ getWallet(index = 0): MockWallet {
69
+ const key = ANVIL_KEYS[index];
70
+ if (!key) throw new Error(`Wallet index ${index} out of range (0-${ANVIL_KEYS.length - 1})`);
71
+ const wallet = new MockWallet(key, this.#mock);
72
+ this.#mock["_getState"](wallet.address).balance = 10000;
73
+ return wallet;
74
+ }
75
+
76
+ /** Advance chain time by `seconds`. */
77
+ async advanceTime(seconds: number): Promise<void> {
78
+ this.#mock.advanceTime(seconds);
79
+ // Also call evm_increaseTime + evm_mine on Anvil
80
+ await this.#rpc("evm_increaseTime", [seconds]);
81
+ await this.#rpc("evm_mine", []);
82
+ }
83
+
84
+ /** Mine additional blocks. */
85
+ async mine(blocks = 1): Promise<void> {
86
+ for (let i = 0; i < blocks; i++) {
87
+ await this.#rpc("evm_mine", []);
88
+ }
89
+ }
90
+
91
+ /** Save a snapshot. Returns snapshot ID. */
92
+ async snapshot(): Promise<string> {
93
+ const result = await this.#rpc("evm_snapshot", []);
94
+ return result as string;
95
+ }
96
+
97
+ /** Revert to a saved snapshot. */
98
+ async revert(snapshotId: string): Promise<void> {
99
+ await this.#rpc("evm_revert", [snapshotId]);
100
+ }
101
+
102
+ /** Stop Anvil. */
103
+ stop(): void {
104
+ this.#process?.kill();
105
+ this.#process = null;
106
+ }
107
+
108
+ async #rpc(method: string, params: unknown[]): Promise<unknown> {
109
+ const response = await fetch(`http://127.0.0.1:${this.#port}`, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
113
+ });
114
+ const data = (await response.json()) as { result?: unknown; error?: { message: string } };
115
+ if (data.error) throw new Error(`RPC error: ${data.error.message}`);
116
+ return data.result;
117
+ }
118
+ }