diff --git a/packages/blocknumber/package.json b/packages/blocknumber/package.json index 53e6369..5923d0a 100644 --- a/packages/blocknumber/package.json +++ b/packages/blocknumber/package.json @@ -17,7 +17,11 @@ "author": "", "license": "ISC", "dependencies": { - "viem": "2.17.10", - "@ebo-agent/shared": "workspace:*" + "@ebo-agent/shared": "workspace:*", + "axios": "1.7.7", + "viem": "2.17.10" + }, + "devDependencies": { + "axios-mock-adapter": "2.0.0" } } diff --git a/packages/blocknumber/src/exceptions/undefinedBlockNumber.ts b/packages/blocknumber/src/exceptions/undefinedBlockNumber.ts new file mode 100644 index 0000000..de4356e --- /dev/null +++ b/packages/blocknumber/src/exceptions/undefinedBlockNumber.ts @@ -0,0 +1,7 @@ +export class UndefinedBlockNumber extends Error { + constructor(isoTimestamp: string) { + super(`Undefined block number at ${isoTimestamp}.`); + + this.name = "UndefinedBlockNumber"; + } +} diff --git a/packages/blocknumber/src/providers/blockNumberProviderFactory.ts b/packages/blocknumber/src/providers/blockNumberProviderFactory.ts index 99360c0..bbc0a80 100644 --- a/packages/blocknumber/src/providers/blockNumberProviderFactory.ts +++ b/packages/blocknumber/src/providers/blockNumberProviderFactory.ts @@ -4,6 +4,7 @@ import { FallbackTransport, HttpTransport, PublicClient } from "viem"; import { UnsupportedChain } from "../exceptions/unsupportedChain.js"; import { Caip2ChainId } from "../types.js"; import { Caip2Utils } from "../utils/index.js"; +import { BlockmetaJsonBlockNumberProvider } from "./blockmetaJsonBlockNumberProvider.js"; import { EvmBlockNumberProvider } from "./evmBlockNumberProvider.js"; const DEFAULT_PROVIDER_CONFIG = { @@ -16,20 +17,24 @@ export class BlockNumberProviderFactory { * Build a `BlockNumberProvider` to handle communication with the specified chain. * * @param chainId CAIP-2 chain id - * @param client a viem public client + * @param evmClient a viem public client * @param logger a ILogger instance * @returns */ public static buildProvider( chainId: Caip2ChainId, - client: PublicClient>, + evmClient: PublicClient>, + blockmetaConfig: { baseUrl: URL; servicePath: string; bearerToken: string }, logger: ILogger, ) { const chainNamespace = Caip2Utils.getNamespace(chainId); switch (chainNamespace) { case EBO_SUPPORTED_CHAINS_CONFIG.evm.namespace: - return new EvmBlockNumberProvider(client, DEFAULT_PROVIDER_CONFIG, logger); + return new EvmBlockNumberProvider(evmClient, DEFAULT_PROVIDER_CONFIG, logger); + + case EBO_SUPPORTED_CHAINS_CONFIG.solana.namespace: + return new BlockmetaJsonBlockNumberProvider(blockmetaConfig, logger); default: throw new UnsupportedChain(chainId); diff --git a/packages/blocknumber/src/providers/blockmetaJsonBlockNumberProvider.ts b/packages/blocknumber/src/providers/blockmetaJsonBlockNumberProvider.ts new file mode 100644 index 0000000..88af2b5 --- /dev/null +++ b/packages/blocknumber/src/providers/blockmetaJsonBlockNumberProvider.ts @@ -0,0 +1,126 @@ +import { ILogger, Timestamp } from "@ebo-agent/shared"; +import axios, { AxiosInstance, AxiosResponse, isAxiosError } from "axios"; + +import { UndefinedBlockNumber } from "../exceptions/undefinedBlockNumber.js"; +import { BlockNumberProvider } from "./blockNumberProvider.js"; + +type BlockByTimeResponse = { + num: string; + id: string; + time: string; +}; + +export type BlockmetaClientConfig = { + baseUrl: URL; + servicePath: string; + bearerToken: string; +}; + +/** + * Consumes the blockmeta.BlockByTime substreams' service via HTTP POST JSON requests to provide + * block numbers based on timestamps + * + * Refer to these web pages for more information: + * * https://thegraph.market/ + * * https://substreams.streamingfast.io/documentation/consume/authentication + */ +export class BlockmetaJsonBlockNumberProvider implements BlockNumberProvider { + private readonly axios: AxiosInstance; + + constructor( + private readonly options: BlockmetaClientConfig, + private readonly logger: ILogger, + ) { + const { baseUrl, bearerToken } = options; + + this.axios = axios.create({ + baseURL: baseUrl.toString(), + headers: { + common: { + "Content-Type": "application/json", + Authorization: `Bearer ${bearerToken}`, + }, + }, + }); + } + + /** @inheritdoc */ + async getEpochBlockNumber(timestamp: Timestamp): Promise { + if (timestamp > Number.MAX_SAFE_INTEGER || timestamp < Number.MIN_SAFE_INTEGER) + throw new RangeError(`Timestamp ${timestamp.toString()} cannot be casted to a Number.`); + + const timestampNumber = Number(timestamp); + const isoTimestamp = new Date(timestampNumber).toISOString(); + + try { + // Try to get the block number at a specific timestamp + const blockNumberAt = await this.getBlockNumberAt(isoTimestamp); + + return blockNumberAt; + } catch (err) { + const isAxios404 = isAxiosError(err) && err.status === 404; + const isUndefinedBlockNumber = err instanceof UndefinedBlockNumber; + + if (!isAxios404 && !isUndefinedBlockNumber) throw err; + + // If no block has its timestamp exactly equal to the specified timestamp, + // try to get the most recent block before the specified timestamp. + const blockNumberBefore = await this.getBlockNumberBefore(isoTimestamp); + + return blockNumberBefore; + } + } + + /** + * Gets the block number at a specific timestamp. + * + * @param isoTimestamp ISO UTC timestamp + * @throws { UndefinedBlockNumber } if request was successful but block number is invalid/not present + * @throws { AxiosError } if request fails + * @returns a promise with the block number at the timestamp + */ + private async getBlockNumberAt(isoTimestamp: string): Promise { + const { servicePath } = this.options; + + const response = await this.axios.post(`${servicePath}/At`, { time: isoTimestamp }); + + return this.parseBlockByTimeResponse(response, isoTimestamp); + } + + /** + * Gets the most recent block number before the specified timestamp. + * + * @param isoTimestamp ISO UTC timestamp + * @throws { UndefinedBlockNumber } if request was successful but block number is invalid/not present + * @throws { AxiosError } if request fails + * @returns a promise with the most recent block number before the specified timestamp + */ + private async getBlockNumberBefore(isoTimestamp: string): Promise { + const { servicePath } = this.options; + + const response = await this.axios.post(`${servicePath}/Before`, { time: isoTimestamp }); + + return this.parseBlockByTimeResponse(response, isoTimestamp); + } + + /** + * Parse the BlockByTime response and extracts the block number. + * + * @param response an AxiosResponse of a request to BlockByTime endpoint + * @param isoTimestamp the timestamp that was sent in the request + * @returns + */ + private parseBlockByTimeResponse(response: AxiosResponse, isoTimestamp: string) { + const { data } = response; + // TODO: validate with zod instead + const blockNumber = (data as BlockByTimeResponse)["num"]; + + if (blockNumber === undefined) { + this.logger.error(`Couldn't find a block number for timestamp ${isoTimestamp}`); + + throw new UndefinedBlockNumber(isoTimestamp); + } + + return BigInt(blockNumber); + } +} diff --git a/packages/blocknumber/src/providers/index.ts b/packages/blocknumber/src/providers/index.ts new file mode 100644 index 0000000..e903779 --- /dev/null +++ b/packages/blocknumber/src/providers/index.ts @@ -0,0 +1,4 @@ +export * from "./blockNumberProvider.js"; +export * from "./blockNumberProviderFactory.js"; +export * from "./blockmetaJsonBlockNumberProvider.js"; +export * from "./evmBlockNumberProvider.js"; diff --git a/packages/blocknumber/src/services/blockNumberService.ts b/packages/blocknumber/src/services/blockNumberService.ts index 1dbd1e2..943e634 100644 --- a/packages/blocknumber/src/services/blockNumberService.ts +++ b/packages/blocknumber/src/services/blockNumberService.ts @@ -2,6 +2,7 @@ import { EBO_SUPPORTED_CHAIN_IDS, ILogger, Timestamp } from "@ebo-agent/shared"; import { createPublicClient, fallback, http } from "viem"; import { ChainWithoutProvider, EmptyRpcUrls, UnsupportedChain } from "../exceptions/index.js"; +import { BlockmetaClientConfig } from "../providers/blockmetaJsonBlockNumberProvider.js"; import { BlockNumberProvider } from "../providers/blockNumberProvider.js"; import { BlockNumberProviderFactory } from "../providers/blockNumberProviderFactory.js"; import { Caip2ChainId } from "../types.js"; @@ -20,6 +21,7 @@ export class BlockNumberService { */ constructor( chainRpcUrls: Map, + private readonly blockmetaConfig: BlockmetaClientConfig, private readonly logger: ILogger, ) { this.blockNumberProviders = this.buildBlockNumberProviders(chainRpcUrls); @@ -81,7 +83,12 @@ export class BlockNumberService { transport: fallback(urls.map((url) => http(url))), }); - const provider = BlockNumberProviderFactory.buildProvider(chainId, client, this.logger); + const provider = BlockNumberProviderFactory.buildProvider( + chainId, + client, + this.blockmetaConfig, + this.logger, + ); if (!provider) throw new ChainWithoutProvider(chainId); diff --git a/packages/blocknumber/test/providers/blockNumberProviderFactory.spec.ts b/packages/blocknumber/test/providers/blockNumberProviderFactory.spec.ts index 9c192ec..33092f0 100644 --- a/packages/blocknumber/test/providers/blockNumberProviderFactory.spec.ts +++ b/packages/blocknumber/test/providers/blockNumberProviderFactory.spec.ts @@ -1,29 +1,69 @@ import { Logger } from "@ebo-agent/shared"; -import { createPublicClient, fallback, http } from "viem"; +import { + createPublicClient, + fallback, + FallbackTransport, + http, + HttpTransport, + PublicClient, +} from "viem"; import { describe, expect, it } from "vitest"; -import { UnsupportedChain } from "../../src/exceptions"; -import { BlockNumberProviderFactory } from "../../src/providers/blockNumberProviderFactory"; -import { EvmBlockNumberProvider } from "../../src/providers/evmBlockNumberProvider"; -import { Caip2ChainId } from "../../src/types"; +import { UnsupportedChain } from "../../src/exceptions/index.js"; +import { + BlockmetaClientConfig, + BlockmetaJsonBlockNumberProvider, + BlockNumberProviderFactory, + EvmBlockNumberProvider, +} from "../../src/providers/index.js"; +import { Caip2ChainId } from "../../src/types.js"; describe("BlockNumberProviderFactory", () => { const logger = Logger.getInstance(); - describe("buildProvider", () => { - const client = createPublicClient({ transport: fallback([http("http://localhost:8545")]) }); + const client: PublicClient> = createPublicClient({ + transport: fallback([http("http://localhost:8545")]), + }); - it("builds a provider", () => { - const provider = BlockNumberProviderFactory.buildProvider("eip155:1", client, logger); + const blockmetaConfig: BlockmetaClientConfig = { + baseUrl: new URL("localhost:443"), + servicePath: "/sf.blockmeta.v2.BlockByTime", + bearerToken: "bearer-token", + }; + + describe("buildProvider", () => { + it("builds an EVM provider", () => { + const provider = BlockNumberProviderFactory.buildProvider( + "eip155:1", + client, + blockmetaConfig, + logger, + ); expect(provider).toBeInstanceOf(EvmBlockNumberProvider); }); + it("builds a Solana Blockmeta provider", () => { + const provider = BlockNumberProviderFactory.buildProvider( + "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + client, + blockmetaConfig, + logger, + ); + + expect(provider).toBeInstanceOf(BlockmetaJsonBlockNumberProvider); + }); + it("fails if chain is not supported", () => { - const unsupportedChainId = "solana:80085" as Caip2ChainId; + const unsupportedChainId = "antelope:f16b1833c747c43682f4386fca9cbb32" as Caip2ChainId; expect(() => { - BlockNumberProviderFactory.buildProvider(unsupportedChainId, client, logger); + BlockNumberProviderFactory.buildProvider( + unsupportedChainId, + client, + blockmetaConfig, + logger, + ); }).toThrow(UnsupportedChain); }); }); diff --git a/packages/blocknumber/test/providers/blockmetaBlockNumberProvider.spec.ts b/packages/blocknumber/test/providers/blockmetaBlockNumberProvider.spec.ts new file mode 100644 index 0000000..07f18bb --- /dev/null +++ b/packages/blocknumber/test/providers/blockmetaBlockNumberProvider.spec.ts @@ -0,0 +1,149 @@ +import { Logger } from "@ebo-agent/shared"; +import MockAxiosAdapter from "axios-mock-adapter"; +import { describe, expect, it, vi } from "vitest"; + +import { UndefinedBlockNumber } from "../../src/exceptions/undefinedBlockNumber.js"; +import { BlockmetaJsonBlockNumberProvider } from "../../src/providers/index.js"; + +const logger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), +} as unknown as Logger; + +describe("BlockmetaBlockNumberService", () => { + describe("getEpochBlockNumber", () => { + const providerOptions = { + baseUrl: new URL("localhost:443"), + bearerToken: "bearer-token", + servicePath: "/sf.blockmeta.v2.BlockByTime", + }; + + it("returns the blocknumber from the blockmeta service", async () => { + const provider = new BlockmetaJsonBlockNumberProvider(providerOptions, logger); + const mockProviderAxios = new MockAxiosAdapter(provider["axios"]); + const timestamp = BigInt(Date.UTC(2024, 0, 1, 0, 0, 0, 0)); + const blockNumber = 100n; + + mockProviderAxios + .onPost( + `${providerOptions.servicePath}/At`, + { + time: "2024-01-01T00:00:00.000Z", + }, + { + headers: expect.objectContaining({ + Authorization: `Bearer ${providerOptions.bearerToken}`, + "Content-Type": "application/json", + }), + }, + ) + .reply(200, { + id: "123abc", + num: blockNumber.toString(), + time: "2024-01-01T00:00:00.000Z", + }); + + const result = await provider.getEpochBlockNumber(timestamp); + + expect(result).toEqual(blockNumber); + }); + + it("fetches block number before timestamp if At call fails", async () => { + const provider = new BlockmetaJsonBlockNumberProvider(providerOptions, logger); + const mockProviderAxios = new MockAxiosAdapter(provider["axios"]); + const timestamp = BigInt(Date.UTC(2024, 0, 1, 0, 0, 0, 0)); + const blockNumber = 100n; + + mockProviderAxios + .onPost( + `${providerOptions.servicePath}/At`, + { + time: "2024-01-01T00:00:00.000Z", + }, + { + headers: expect.objectContaining({ + Authorization: `Bearer ${providerOptions.bearerToken}`, + "Content-Type": "application/json", + }), + }, + ) + .reply(404) + .onPost( + `${providerOptions.servicePath}/Before`, + { + time: "2024-01-01T00:00:00.000Z", + }, + { + headers: expect.objectContaining({ + Authorization: `Bearer ${providerOptions.bearerToken}`, + "Content-Type": "application/json", + }), + }, + ) + .reply(200, { + id: "123abc", + num: blockNumber.toString(), + time: "2024-01-01T00:00:00.000Z", + }); + + const result = await provider.getEpochBlockNumber(timestamp); + + expect(result).toEqual(blockNumber); + }); + + it("throws if response has no block number", async () => { + const provider = new BlockmetaJsonBlockNumberProvider(providerOptions, logger); + const mockProviderAxios = new MockAxiosAdapter(provider["axios"]); + const timestamp = BigInt(Date.UTC(2024, 0, 1, 0, 0, 0, 0)); + + mockProviderAxios + .onPost( + `${providerOptions.servicePath}/At`, + { + time: "2024-01-01T00:00:00.000Z", + }, + { + headers: expect.objectContaining({ + Authorization: `Bearer ${providerOptions.bearerToken}`, + "Content-Type": "application/json", + }), + }, + ) + .reply(404) + .onPost( + `${providerOptions.servicePath}/Before`, + { + time: "2024-01-01T00:00:00.000Z", + }, + { + headers: expect.objectContaining({ + Authorization: `Bearer ${providerOptions.bearerToken}`, + "Content-Type": "application/json", + }), + }, + ) + .reply(200, { + id: "123abc", + time: "2024-01-01T00:00:00.000Z", + }); + + expect(provider.getEpochBlockNumber(timestamp)).rejects.toThrow(UndefinedBlockNumber); + }); + + it("throws when timestamp is too big", () => { + const provider = new BlockmetaJsonBlockNumberProvider(providerOptions, logger); + const bigTimestamp = BigInt(Number.MAX_SAFE_INTEGER) + 1n; + + expect(provider.getEpochBlockNumber(bigTimestamp)).rejects.toThrow(RangeError); + }); + + it("throws when timestamp is too small", () => { + const provider = new BlockmetaJsonBlockNumberProvider(providerOptions, logger); + const bigTimestamp = BigInt(Number.MIN_SAFE_INTEGER) - 1n; + + expect(provider.getEpochBlockNumber(bigTimestamp)).rejects.toThrow(RangeError); + }); + }); +}); diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 55d9b77..33c3230 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -7,6 +7,12 @@ export const EBO_SUPPORTED_CHAINS_CONFIG = { arbitrum: "42161", }, }, + solana: { + namespace: "solana", + references: { + mainnet: "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + }, + }, } as const; export const EBO_SUPPORTED_CHAIN_IDS = Object.values(EBO_SUPPORTED_CHAINS_CONFIG).reduce( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dada3bd..f11e728 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,9 +86,16 @@ importers: "@ebo-agent/shared": specifier: workspace:* version: link:../shared + axios: + specifier: 1.7.7 + version: 1.7.7 viem: specifier: 2.17.10 version: 2.17.10(typescript@5.5.3) + devDependencies: + axios-mock-adapter: + specifier: 2.0.0 + version: 2.0.0(axios@1.7.7) packages/shared: dependencies: @@ -1224,6 +1231,26 @@ packages: integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==, } + asynckit@0.4.0: + resolution: + { + integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, + } + + axios-mock-adapter@2.0.0: + resolution: + { + integrity: sha512-D/K0J5Zm6KvaMTnsWrBQZWLzKN9GxUFZEa0mx2qeEHXDeTugCoplWehy8y36dj5vuSjhe1u/Dol8cZ8lzzmDew==, + } + peerDependencies: + axios: ">= 0.17.0" + + axios@1.7.7: + resolution: + { + integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==, + } + balanced-match@1.0.2: resolution: { @@ -1382,6 +1409,13 @@ packages: integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==, } + combined-stream@1.0.8: + resolution: + { + integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, + } + engines: { node: ">= 0.8" } + commander@12.1.0: resolution: { @@ -1497,6 +1531,13 @@ packages: integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, } + delayed-stream@1.0.0: + resolution: + { + integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, + } + engines: { node: ">=0.4.0" } + diff@4.0.2: resolution: { @@ -1808,6 +1849,18 @@ packages: integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==, } + follow-redirects@1.15.9: + resolution: + { + integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==, + } + engines: { node: ">=4.0" } + peerDependencies: + debug: "*" + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.0: resolution: { @@ -1815,6 +1868,13 @@ packages: } engines: { node: ">=14" } + form-data@4.0.0: + resolution: + { + integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==, + } + engines: { node: ">= 6" } + fs.realpath@1.0.0: resolution: { @@ -2034,6 +2094,13 @@ packages: integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==, } + is-buffer@2.0.5: + resolution: + { + integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==, + } + engines: { node: ">=4" } + is-extglob@2.1.1: resolution: { @@ -2431,6 +2498,20 @@ packages: } engines: { node: ">=8.6" } + mime-db@1.52.0: + resolution: + { + integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, + } + engines: { node: ">= 0.6" } + + mime-types@2.1.35: + resolution: + { + integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, + } + engines: { node: ">= 0.6" } + mimic-fn@4.0.0: resolution: { @@ -2703,6 +2784,12 @@ packages: engines: { node: ">=14" } hasBin: true + proxy-from-env@1.1.0: + resolution: + { + integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==, + } + punycode@2.3.1: resolution: { @@ -4149,6 +4236,22 @@ snapshots: async@3.2.6: {} + asynckit@0.4.0: {} + + axios-mock-adapter@2.0.0(axios@1.7.7): + dependencies: + axios: 1.7.7 + fast-deep-equal: 3.1.3 + is-buffer: 2.0.5 + + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + balanced-match@1.0.2: {} brace-expansion@1.1.11: @@ -4244,6 +4347,10 @@ snapshots: color: 3.2.1 text-hex: 1.0.0 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@12.1.0: {} compare-func@2.0.0: @@ -4304,6 +4411,8 @@ snapshots: deep-is@0.1.4: {} + delayed-stream@1.0.0: {} + diff@4.0.2: {} dir-glob@3.0.1: @@ -4522,11 +4631,19 @@ snapshots: fn.name@1.1.0: {} + follow-redirects@1.15.9: {} + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 + form-data@4.0.0: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -4631,6 +4748,8 @@ snapshots: is-arrayish@0.3.2: {} + is-buffer@2.0.5: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4835,6 +4954,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mimic-fn@4.0.0: {} mimic-function@5.0.1: {} @@ -4960,6 +5085,8 @@ snapshots: prettier@3.3.3: {} + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {}