From 0cab90e48319e8c6383a706c0745e08483c0c321 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Wed, 21 Aug 2024 16:10:10 +0100 Subject: [PATCH 1/4] feat: update gateway for v3 Signed-off-by: Gregory Hill --- sdk/package.json | 3 +- sdk/src/gateway.ts | 88 ----------------- sdk/src/gateway/client.ts | 194 ++++++++++++++++++++++++++++++++++++++ sdk/src/gateway/index.ts | 2 + sdk/src/gateway/tokens.ts | 38 ++++++++ sdk/src/gateway/types.ts | 32 +++++++ 6 files changed, 268 insertions(+), 89 deletions(-) delete mode 100644 sdk/src/gateway.ts create mode 100644 sdk/src/gateway/client.ts create mode 100644 sdk/src/gateway/index.ts create mode 100644 sdk/src/gateway/tokens.ts create mode 100644 sdk/src/gateway/types.ts diff --git a/sdk/package.json b/sdk/package.json index b0d7270c..501a3783 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -32,6 +32,7 @@ "@scure/base": "^1.1.7", "@scure/btc-signer": "^1.3.2", "bitcoin-address-validation": "^2.2.3", - "bitcoinjs-lib": "^6.1.6" + "bitcoinjs-lib": "^6.1.6", + "ethers": "^6.13.2" } } \ No newline at end of file diff --git a/sdk/src/gateway.ts b/sdk/src/gateway.ts deleted file mode 100644 index d07fdcd6..00000000 --- a/sdk/src/gateway.ts +++ /dev/null @@ -1,88 +0,0 @@ -export type EvmAddress = string; - -type GatewayQuote = { - onramp_address: EvmAddress; - dust_threshold: number; - satoshis: number; - fee: number; - gratuity: string; - bitcoin_address: string; - tx_proof_difficulty_factor: number; -}; - -type GatewayOrderResponse = { - onramp_address: EvmAddress; - token_address: EvmAddress; - txid: string; - status: boolean; - timestamp: number; - tokens: string; - satoshis: number; - fee: number; - tx_proof_difficulty_factor: number; -}; - -export class GatewayApiClient { - private baseUrl: string; - - constructor(baseUrl: string) { - this.baseUrl = baseUrl; - } - - async getQuote(address: string, atomicAmount?: number | string): Promise { - const response = await fetch(`${this.baseUrl}/quote/${address}/${atomicAmount || ''}`, { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' - } - }); - - return await response.json(); - } - - // TODO: add error handling - async createOrder(contractAddress: string, userAddress: EvmAddress, atomicAmount: number | string): Promise { - const response = await fetch(`${this.baseUrl}/order`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' - }, - body: JSON.stringify({ onramp_address: contractAddress, user_address: userAddress, satoshis: atomicAmount }) - }); - - if (!response.ok) { - throw new Error('Failed to create order'); - } - - return await response.json(); - } - - async updateOrder(id: string, tx: string) { - const response = await fetch(`${this.baseUrl}/order/${id}`, { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' - }, - body: JSON.stringify({ bitcoin_tx: tx }) - }); - - if (!response.ok) { - throw new Error('Failed to update order'); - } - } - - async getOrders(userAddress: EvmAddress): Promise { - const response = await fetch(`${this.baseUrl}/orders/${userAddress}`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json' - } - }); - - return response.json(); - } -} - diff --git a/sdk/src/gateway/client.ts b/sdk/src/gateway/client.ts new file mode 100644 index 00000000..7633c7c1 --- /dev/null +++ b/sdk/src/gateway/client.ts @@ -0,0 +1,194 @@ +import { ethers, AbiCoder } from "ethers"; +import { GatewayQuoteParams } from "./types"; +import { TOKENS } from "./tokens"; + +type EvmAddress = string; + +type GatewayQuote = { + gatewayAddress: EvmAddress; + dustThreshold: number; + satoshis: number; + fee: number; + gratuity: string; + bitcoinAddress: string; + txProofDifficultyFactor: number; + strategyAddress: EvmAddress | null, +}; + +type GatewayCreateOrderRequest = { + gatewayAddress: EvmAddress, + strategyAddress: EvmAddress | null, + satsToConvertToEth: number, + userAddress: EvmAddress, + gatewayExtraData: string | null, + strategyExtraData: string | null, + satoshis: number, +}; + +type GatewayOrderResponse = { + gatewayAddress: EvmAddress; + tokenAddress: EvmAddress; + txid: string; + status: boolean; + timestamp: number; + tokens: string; + satoshis: number; + fee: number; + txProofDifficultyFactor: number; +}; + +type GatewayCreateOrderResponse = { + uuid: string, + opReturnHash: string, +}; + +type GatewayStartOrderResult = GatewayCreateOrderResponse & { + bitcoinAddress: string, + satoshis: number; +}; + +/** + * Base url for the mainnet Gateway API. + * @default "https://gateway-api-mainnet.gobob.xyz" + */ +export const MAINNET_GATEWAY_BASE_URL = "https://gateway-api-mainnet.gobob.xyz"; + +/** + * Base url for the testnet Gateway API. + * @default "https://gateway-api-testnet.gobob.xyz" + */ +export const TESTNET_GATEWAY_BASE_URL = "https://gateway-api-testnet.gobob.xyz"; + +export class GatewayApiClient { + private baseUrl: string; + + constructor(networkOrUrl: string = "mainnet") { + switch (networkOrUrl) { + case "mainnet" || "bob": + this.baseUrl = MAINNET_GATEWAY_BASE_URL; + break; + case "testnet" || "bob-sepolia": + this.baseUrl = TESTNET_GATEWAY_BASE_URL; + break; + default: + this.baseUrl = networkOrUrl; + } + } + + async getQuote(params: GatewayQuoteParams): Promise { + const isMainnet = params.toChain == "bob" || params.toChain == 60808; + + let outputToken = ""; + if (params.toToken.startsWith("0x")) { + outputToken = params.toToken; + } else if (params.toToken in TOKENS) { + outputToken = isMainnet ? TOKENS[params.toToken].bob : TOKENS[params.toToken].bob_sepolia; + } else { + throw new Error('Unknown output token'); + } + + const atomicAmount = params.amount; + const response = await fetch(`${this.baseUrl}/quote/${outputToken}/${atomicAmount || ''}`, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + } + }); + + return await response.json(); + } + + // TODO: add error handling + async startOrder(gatewayQuote: GatewayQuote, params: GatewayQuoteParams): Promise { + const request: GatewayCreateOrderRequest = { + gatewayAddress: gatewayQuote.gatewayAddress, + strategyAddress: gatewayQuote.strategyAddress, + satsToConvertToEth: params.gasRefill, + userAddress: params.toUserAddress, + // TODO: figure out how to get extra data + gatewayExtraData: null, + strategyExtraData: null, + satoshis: gatewayQuote.satoshis, + }; + + const response = await fetch(`${this.baseUrl}/order`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify(request) + }); + + if (!response.ok) { + throw new Error('Failed to create order'); + } + + const data: GatewayCreateOrderResponse = await response.json(); + // NOTE: could remove this check but good for sanity + if (data.opReturnHash != calculateOpReturnHash(request)) { + throw new Error('Invalid OP_RETURN hash'); + } + + return { + uuid: data.uuid, + opReturnHash: data.opReturnHash, + bitcoinAddress: gatewayQuote.bitcoinAddress, + satoshis: gatewayQuote.satoshis, + } + } + + async finalizeOrder(orderUuid: string, bitcoinTx: string) { + const response = await fetch(`${this.baseUrl}/order/${orderUuid}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + }, + body: JSON.stringify({ bitcoinTx: bitcoinTx }) + }); + + if (!response.ok) { + throw new Error('Failed to update order'); + } + } + + async getOrders(userAddress: EvmAddress): Promise { + const response = await fetch(`${this.baseUrl}/orders/${userAddress}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + } + }); + + return response.json(); + } + + async getTokens(): Promise { + const response = await fetch(`${this.baseUrl}/tokens`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + } + }); + + return response.json(); + } +} + +function calculateOpReturnHash(req: GatewayCreateOrderRequest) { + const abiCoder = new AbiCoder(); + return ethers.keccak256(abiCoder.encode( + ["address", "address", "uint256", "address", "bytes", "bytes"], + [ + req.gatewayAddress, + req.strategyAddress || ethers.ZeroAddress, + req.satsToConvertToEth, + req.userAddress, + req.gatewayExtraData, + req.strategyExtraData + ] + )) +} \ No newline at end of file diff --git a/sdk/src/gateway/index.ts b/sdk/src/gateway/index.ts new file mode 100644 index 00000000..692500ac --- /dev/null +++ b/sdk/src/gateway/index.ts @@ -0,0 +1,2 @@ +export { GatewayApiClient as GatewaySDK } from "./client"; +export { GatewayQuoteParams } from "./types"; \ No newline at end of file diff --git a/sdk/src/gateway/tokens.ts b/sdk/src/gateway/tokens.ts new file mode 100644 index 00000000..e69588d1 --- /dev/null +++ b/sdk/src/gateway/tokens.ts @@ -0,0 +1,38 @@ +type Token = { + name: string, + bob: string, + bob_sepolia: string +} + +export const TOKENS: { [key: string]: Token } = { + "tBTC": { + name: "tBTC v2", + bob: "0xBBa2eF945D523C4e2608C9E1214C2Cc64D4fc2e2", + bob_sepolia: "0x6744bAbDf02DCF578EA173A9F0637771A9e1c4d0", + }, + "WBTC": { + name: "Wrapped BTC", + bob: "0x03C7054BCB39f7b2e5B2c7AcB37583e32D70Cfa3", + bob_sepolia: "0xe51e40e15e6e1496a0981f90Ca1D632545bdB519", + }, + "sbtBTC": { + name: "sb tBTC v2", + bob: "0x2925dF9Eb2092B53B06A06353A7249aF3a8B139e", + bob_sepolia: "", + }, + "sbWBTC": { + name: "sb Wrapped BTC", + bob: "0x5c46D274ed8AbCAe2964B63c0360ad3Ccc384dAa", + bob_sepolia: "", + }, + "seTBTC": { + name: "Segment TBTC", + bob: "0xD30288EA9873f376016A0250433b7eA375676077", + bob_sepolia: "", + }, + "seWBTC": { + name: "Segment WBTC", + bob: "0x6265C05158f672016B771D6Fb7422823ed2CbcDd", + bob_sepolia: "", + } +} \ No newline at end of file diff --git a/sdk/src/gateway/types.ts b/sdk/src/gateway/types.ts new file mode 100644 index 00000000..671a2795 --- /dev/null +++ b/sdk/src/gateway/types.ts @@ -0,0 +1,32 @@ +type ChainSlug = string | number; +type TokenSymbol = string; + +export interface GatewayQuoteParams { + /** @description Source chain slug or ID */ + fromChain: ChainSlug; + /** @description Destination chain slug or ID */ + toChain: ChainSlug; + /** @description Token symbol or address on source chain */ + fromToken: TokenSymbol; + /** @description Token symbol or address on destination chain */ + toToken: TokenSymbol; + /** @description Wallet address on source chain */ + fromUserAddress: string; + /** @description Wallet address on destination chain */ + toUserAddress: string; + /** @description Amount of tokens to send from the source chain */ + amount: number | string; + + /** @description Maximum slippage percentage between 0.01 and 0.03 (Default: 0.03) */ + maxSlippage?: number; + + /** @description Unique affiliate ID for tracking */ + affiliateId?: string; + /** @description Optionally filter the type of routes returned */ + type?: 'swap' | 'deposit' | 'withdraw' | 'claim'; + /** @description The percentage of fee charged by partners in Basis Points (BPS) units. This will override the default fee rate configured via platform. 1 BPS = 0.01%. The maximum value is 1000 (which equals 10%). The minimum value is 1 (which equals 0.01%). */ + fee?: number; + + /** @description Amount of satoshis to swap for ETH */ + gasRefill?: number, +} From 1b2d9e4bca387a9f51733f4e778d49941471f719 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Thu, 22 Aug 2024 12:45:14 +0100 Subject: [PATCH 2/4] refactor: lowercase tokens and add address lookup Signed-off-by: Gregory Hill --- sdk/src/gateway/client.ts | 14 ++++++++--- sdk/src/gateway/tokens.ts | 52 ++++++++++++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/sdk/src/gateway/client.ts b/sdk/src/gateway/client.ts index 7633c7c1..452e524a 100644 --- a/sdk/src/gateway/client.ts +++ b/sdk/src/gateway/client.ts @@ -1,6 +1,6 @@ import { ethers, AbiCoder } from "ethers"; import { GatewayQuoteParams } from "./types"; -import { TOKENS } from "./tokens"; +import { TOKENS_INFO, ADDRESS_LOOKUP, Token as TokenInfo } from "./tokens"; type EvmAddress = string; @@ -81,8 +81,8 @@ export class GatewayApiClient { let outputToken = ""; if (params.toToken.startsWith("0x")) { outputToken = params.toToken; - } else if (params.toToken in TOKENS) { - outputToken = isMainnet ? TOKENS[params.toToken].bob : TOKENS[params.toToken].bob_sepolia; + } else if (params.toToken in TOKENS_INFO) { + outputToken = isMainnet ? TOKENS_INFO[params.toToken].bob : TOKENS_INFO[params.toToken].bobSepolia; } else { throw new Error('Unknown output token'); } @@ -176,6 +176,14 @@ export class GatewayApiClient { return response.json(); } + + async getTokensInfo(): Promise { + const tokens = await this.getTokens(); + return tokens + .map(token => ADDRESS_LOOKUP[token]) + .filter(token => token !== undefined);; + } + } function calculateOpReturnHash(req: GatewayCreateOrderRequest) { diff --git a/sdk/src/gateway/tokens.ts b/sdk/src/gateway/tokens.ts index e69588d1..552d7b00 100644 --- a/sdk/src/gateway/tokens.ts +++ b/sdk/src/gateway/tokens.ts @@ -1,38 +1,72 @@ -type Token = { +export type Token = { name: string, + symbol: string, bob: string, - bob_sepolia: string + bobSepolia: string } -export const TOKENS: { [key: string]: Token } = { +const TOKENS: { [key: string]: Token } = { "tBTC": { name: "tBTC v2", + symbol: "tBTC", bob: "0xBBa2eF945D523C4e2608C9E1214C2Cc64D4fc2e2", - bob_sepolia: "0x6744bAbDf02DCF578EA173A9F0637771A9e1c4d0", + bobSepolia: "0x6744bAbDf02DCF578EA173A9F0637771A9e1c4d0", }, "WBTC": { name: "Wrapped BTC", + symbol: "WBTC", bob: "0x03C7054BCB39f7b2e5B2c7AcB37583e32D70Cfa3", - bob_sepolia: "0xe51e40e15e6e1496a0981f90Ca1D632545bdB519", + bobSepolia: "0xe51e40e15e6e1496a0981f90Ca1D632545bdB519", }, "sbtBTC": { name: "sb tBTC v2", + symbol: "sbtBTC", bob: "0x2925dF9Eb2092B53B06A06353A7249aF3a8B139e", - bob_sepolia: "", + bobSepolia: "", }, "sbWBTC": { name: "sb Wrapped BTC", + symbol: "sbWBTC", bob: "0x5c46D274ed8AbCAe2964B63c0360ad3Ccc384dAa", - bob_sepolia: "", + bobSepolia: "", }, "seTBTC": { name: "Segment TBTC", + symbol: "seTBTC", bob: "0xD30288EA9873f376016A0250433b7eA375676077", - bob_sepolia: "", + bobSepolia: "", }, "seWBTC": { name: "Segment WBTC", + symbol: "seWBTC", bob: "0x6265C05158f672016B771D6Fb7422823ed2CbcDd", - bob_sepolia: "", + bobSepolia: "", + }, + "stmtBTC": { + name: "Staked mtBTC", + symbol: "stmtBTC", + bob: "", + bobSepolia: "0xc4229678b65e2D9384FDf96F2E5D512d6eeC0C77", } +}; + +export const TOKENS_INFO: { [key: string]: Token } = {}; +export const ADDRESS_LOOKUP: { [address: string]: Token } = {}; + +for (const key in TOKENS_INFO) { + const token = TOKENS[key]; + + const lowerBob = token.bob.toLowerCase(); + const lowerBobSepolia = token.bobSepolia.toLowerCase(); + + const lowercasedToken: Token = { + name: token.name, + symbol: token.symbol, + bob: lowerBob, + bobSepolia: lowerBobSepolia, + }; + + TOKENS_INFO[key] = lowercasedToken; + ADDRESS_LOOKUP[lowerBob] = lowercasedToken; + ADDRESS_LOOKUP[lowerBobSepolia] = lowercasedToken; } \ No newline at end of file From 6dbcee598902ad1b6aa30a9b4550e7710a6e8bbd Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Thu, 22 Aug 2024 12:46:04 +0100 Subject: [PATCH 3/4] chore: bump package to v2 Signed-off-by: Gregory Hill --- sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/package.json b/sdk/package.json index 501a3783..0c62686f 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@gobob/bob-sdk", - "version": "1.3.0", + "version": "2.0.0", "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { From a46138ad71e555a8af7e58cd06df5a35b4031094 Mon Sep 17 00:00:00 2001 From: Gregory Hill Date: Thu, 22 Aug 2024 12:48:59 +0100 Subject: [PATCH 4/4] chore: update package-lock.json Signed-off-by: Gregory Hill --- sdk/package-lock.json | 96 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/sdk/package-lock.json b/sdk/package-lock.json index 6714ee01..016d8399 100644 --- a/sdk/package-lock.json +++ b/sdk/package-lock.json @@ -1,17 +1,18 @@ { "name": "@gobob/bob-sdk", - "version": "1.3.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@gobob/bob-sdk", - "version": "1.3.0", + "version": "2.0.0", "dependencies": { "@scure/base": "^1.1.7", "@scure/btc-signer": "^1.3.2", "bitcoin-address-validation": "^2.2.3", - "bitcoinjs-lib": "^6.1.6" + "bitcoinjs-lib": "^6.1.6", + "ethers": "^6.13.2" }, "devDependencies": { "@types/node": "^22.0.0", @@ -25,6 +26,11 @@ "yargs": "^17.5.1" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -905,6 +911,11 @@ "node": ">=0.4.0" } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -1434,6 +1445,60 @@ "@types/estree": "^1.0.0" } }, + "node_modules/ethers": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.2.tgz", + "integrity": "sha512-9VkriTTed+/27BGuY1s0hf441kqwHJ1wtN2edksEtiRvXx+soxRX3iSXTfFqq2+YwrOqbDoTHjIhQnjJRlzKmg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -2552,6 +2617,11 @@ "node": ">=0.3.1" } }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/typeforce": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", @@ -2856,6 +2926,26 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",