From 6866142caabb12d54bc3dc06a6d98d39781dbaf0 Mon Sep 17 00:00:00 2001 From: Michael Absolon Date: Sat, 10 Aug 2024 03:33:35 +0200 Subject: [PATCH] =?UTF-8?q?feat(xcm-api):=20Add=20support=20for=20Snowbrid?= =?UTF-8?q?ge=20=E2=9D=84=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/playground/package.json | 1 + .../eth-bridge/EthBridgeTransfer.tsx | 89 ++++++++++++- .../eth-bridge/EthBridgeTransferForm.tsx | 6 +- apps/xcm-api/package.json | 2 + apps/xcm-api/src/analytics/EventName.ts | 1 + apps/xcm-api/src/app.module.ts | 2 + .../x-transfer-eth/dto/x-transfer-eth.dto.ts | 24 ++++ .../x-transfer-eth.controller.ts | 37 ++++++ .../x-transfer-eth/x-transfer-eth.module.ts | 9 ++ .../x-transfer-eth/x-transfer-eth.service.ts | 103 +++++++++++++++ pnpm-lock.yaml | 118 ++++++++++++++++-- 11 files changed, 379 insertions(+), 13 deletions(-) create mode 100644 apps/xcm-api/src/x-transfer-eth/dto/x-transfer-eth.dto.ts create mode 100644 apps/xcm-api/src/x-transfer-eth/x-transfer-eth.controller.ts create mode 100644 apps/xcm-api/src/x-transfer-eth/x-transfer-eth.module.ts create mode 100644 apps/xcm-api/src/x-transfer-eth/x-transfer-eth.service.ts diff --git a/apps/playground/package.json b/apps/playground/package.json index 5ec25497..5b33e19f 100644 --- a/apps/playground/package.json +++ b/apps/playground/package.json @@ -22,6 +22,7 @@ "@polkadot/extension-inject": "^0.49.3", "@polkadot/keyring": "^13.0.2", "@polkadot/util": "^13.0.2", + "@snowbridge/contract-types": "^0.1.18", "@tabler/icons-react": "^3.11.0", "axios": "^1.7.2", "ethers": "^6.13.2", diff --git a/apps/playground/src/components/eth-bridge/EthBridgeTransfer.tsx b/apps/playground/src/components/eth-bridge/EthBridgeTransfer.tsx index e9187ebf..f1c1b051 100644 --- a/apps/playground/src/components/eth-bridge/EthBridgeTransfer.tsx +++ b/apps/playground/src/components/eth-bridge/EthBridgeTransfer.tsx @@ -1,13 +1,24 @@ import { Stack, Title, Box, Button } from "@mantine/core"; import { useDisclosure, useScrollIntoView } from "@mantine/hooks"; import { useState, useEffect } from "react"; -import { BrowserProvider, ethers } from "ethers"; +import { BrowserProvider, ethers, LogDescription } from "ethers"; import ErrorAlert from "../ErrorAlert"; import EthBridgeTransferForm, { FormValues, FormValuesTransformed, } from "./EthBridgeTransferForm"; import { EvmBuilder } from "@paraspell/sdk"; +import { fetchFromApi } from "../../utils/submitUsingApi"; +import { IGateway__factory } from "@snowbridge/contract-types"; +import { MultiAddressStruct } from "@snowbridge/contract-types/dist/IGateway"; + +interface ApiResponse { + token: string; + destinationParaId: number; + destinationFee: string; + amount: string; + fee: string; +} const EthBridgeTransfer = () => { const [selectedAccount, setSelectedAccount] = useState(null); @@ -76,7 +87,7 @@ const EthBridgeTransfer = () => { } }, [error, scrollIntoView]); - const submitEthTransaction = async ({ + const submitEthTransactionSdk = async ({ to, amount, currency, @@ -101,6 +112,74 @@ const EthBridgeTransfer = () => { .build(); }; + const submitEthTransactionApi = async (formValues: FormValuesTransformed) => { + if (!provider) { + throw new Error("Provider not initialized"); + } + + const signer = await provider.getSigner(); + + if (!signer) { + throw new Error("Signer not initialized"); + } + + const apiResonse = (await fetchFromApi( + { + ...formValues, + destAddress: formValues.address, + address: await signer.getAddress(), + currency: formValues.currency?.symbol, + }, + "/x-transfer-eth", + "POST", + true + )) as ApiResponse; + + const GATEWAY_CONTRACT = "0xEDa338E4dC46038493b885327842fD3E301CaB39"; + + const contract = IGateway__factory.connect(GATEWAY_CONTRACT, signer); + + const abi = ethers.AbiCoder.defaultAbiCoder(); + + const address: MultiAddressStruct = { + data: abi.encode(["bytes32"], [formValues.address]), + kind: 1, + }; + + const response = await contract.sendToken( + apiResonse.token, + apiResonse.destinationParaId, + address, + apiResonse.destinationFee, + apiResonse.amount, + { + value: apiResonse.fee, + } + ); + const receipt = await response.wait(1); + + if (receipt === null) { + throw new Error("Error waiting for transaction completion"); + } + + if (receipt?.status !== 1) { + throw new Error("Transaction failed"); + } + + const events: LogDescription[] = []; + receipt.logs.forEach((log) => { + const event = contract.interface.parseLog({ + topics: [...log.topics], + data: log.data, + }); + if (event !== null) { + events.push(event); + } + }); + + return true; + }; + const submit = async (formValues: FormValues) => { if (!selectedAccount) { alert("No account selected, connect wallet first"); @@ -110,7 +189,11 @@ const EthBridgeTransfer = () => { setLoading(true); try { - await submitEthTransaction(formValues); + if (formValues.useApi) { + await submitEthTransactionApi(formValues); + } else { + await submitEthTransactionSdk(formValues); + } alert("Transaction was successful!"); } catch (e) { if (e instanceof Error) { diff --git a/apps/playground/src/components/eth-bridge/EthBridgeTransferForm.tsx b/apps/playground/src/components/eth-bridge/EthBridgeTransferForm.tsx index 2f78570a..57de2e69 100644 --- a/apps/playground/src/components/eth-bridge/EthBridgeTransferForm.tsx +++ b/apps/playground/src/components/eth-bridge/EthBridgeTransferForm.tsx @@ -1,6 +1,6 @@ import { useForm } from "@mantine/form"; import { FC } from "react"; -import { Button, Select, Stack, TextInput } from "@mantine/core"; +import { Button, Checkbox, Select, Stack, TextInput } from "@mantine/core"; import { NODES_WITH_RELAY_CHAINS, TAsset, @@ -14,6 +14,7 @@ export type FormValues = { currencyOptionId: string; address: string; amount: string; + useApi: boolean; }; export type FormValuesTransformed = FormValues & { @@ -32,6 +33,7 @@ const EthBridgeTransferForm: FC = ({ onSubmit, loading }) => { currencyOptionId: "", amount: "1000000000", address: "5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96", + useApi: false, }, validate: { @@ -102,6 +104,8 @@ const EthBridgeTransferForm: FC = ({ onSubmit, loading }) => { {...form.getInputProps("amount")} /> + + diff --git a/apps/xcm-api/package.json b/apps/xcm-api/package.json index 39832345..d133e6c9 100644 --- a/apps/xcm-api/package.json +++ b/apps/xcm-api/package.json @@ -42,9 +42,11 @@ "@polkadot/types": "^12.2.2", "@polkadot/util": "^13.0.2", "@sentry/node": "^7.73.0", + "@snowbridge/api": "^0.1.17", "axios": "^1.7.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "ethers": "^6.13.2", "express": "^4.19.2", "googleapis": "^140.0.1", "mixpanel": "^0.18.0", diff --git a/apps/xcm-api/src/analytics/EventName.ts b/apps/xcm-api/src/analytics/EventName.ts index 8cec89ad..cdb5344e 100644 --- a/apps/xcm-api/src/analytics/EventName.ts +++ b/apps/xcm-api/src/analytics/EventName.ts @@ -15,6 +15,7 @@ export enum EventName { GET_DEFAULT_PALLET = 'Get Default Pallet', GET_SUPPORTED_PALLETS = 'Get Supported Pallets', GENERATE_XCM_CALL = 'Generate XCM Call', + GENERATE_ETH_CALL = 'Generate ETH Call', GENERATE_API_KEY = 'Generate API Key', GENERATE_ROUTER_EXTRINSICS = 'Generate router extrinsics', CLAIM_ASSETS = 'Claim assets', diff --git a/apps/xcm-api/src/app.module.ts b/apps/xcm-api/src/app.module.ts index f4c0076b..3df148b0 100644 --- a/apps/xcm-api/src/app.module.ts +++ b/apps/xcm-api/src/app.module.ts @@ -24,6 +24,7 @@ import path from 'path'; import { AssetClaimModule } from './asset-claim/asset-claim.module.js'; import { TransferInfoModule } from './transfer-info/transfer-info.module.js'; import { XcmAnalyserModule } from './xcm-analyser/xcm-analyser.module.js'; +import { XTransferEthModule } from './x-transfer-eth/x-transfer-eth.module.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -32,6 +33,7 @@ const __dirname = path.dirname(__filename); imports: [ AnalyticsModule, XTransferModule, + XTransferEthModule, AssetClaimModule, TransferInfoModule, XcmAnalyserModule, diff --git a/apps/xcm-api/src/x-transfer-eth/dto/x-transfer-eth.dto.ts b/apps/xcm-api/src/x-transfer-eth/dto/x-transfer-eth.dto.ts new file mode 100644 index 00000000..a62c654e --- /dev/null +++ b/apps/xcm-api/src/x-transfer-eth/dto/x-transfer-eth.dto.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; + +export const XTransferEthDtoSchema = z.object({ + to: z.string(), + amount: z.union([ + z.string().refine( + (val) => { + const num = parseFloat(val); + return !isNaN(num) && num > 0; + }, + { + message: 'Amount must be a positive number', + }, + ), + z.number().positive({ message: 'Amount must be a positive number' }), + ]), + address: z.string().min(1, { message: 'Source address is required' }), + destAddress: z + .string() + .min(1, { message: 'Destination address is required' }), + currency: z.string(), +}); + +export type XTransferEthDto = z.infer; diff --git a/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.controller.ts b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.controller.ts new file mode 100644 index 00000000..0a6de464 --- /dev/null +++ b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.controller.ts @@ -0,0 +1,37 @@ +import { Body, Controller, Post, Req, UsePipes } from '@nestjs/common'; +import { AnalyticsService } from '../analytics/analytics.service.js'; +import { XTransferEthService } from './x-transfer-eth.service.js'; +import { EventName } from '../analytics/EventName.js'; +import { ZodValidationPipe } from '../zod-validation-pipe.js'; +import { + XTransferEthDtoSchema, + XTransferEthDto, +} from './dto/x-transfer-eth.dto.js'; + +@Controller('x-transfer-eth') +export class XTransferEthController { + constructor( + private xTransferEthService: XTransferEthService, + private analyticsService: AnalyticsService, + ) {} + + private trackAnalytics( + eventName: EventName, + req: Request, + params: XTransferEthDto, + ) { + const { to, address, currency } = params; + this.analyticsService.track(eventName, req, { + to, + address, + currency, + }); + } + + @Post() + @UsePipes(new ZodValidationPipe(XTransferEthDtoSchema)) + generateXcmCall(@Body() bodyParams: XTransferEthDto, @Req() req: Request) { + this.trackAnalytics(EventName.GENERATE_ETH_CALL, req, bodyParams); + return this.xTransferEthService.generateEthCall(bodyParams); + } +} diff --git a/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.module.ts b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.module.ts new file mode 100644 index 00000000..058173cf --- /dev/null +++ b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { XTransferEthController } from './x-transfer-eth.controller.js'; +import { XTransferEthService } from './x-transfer-eth.service.js'; + +@Module({ + controllers: [XTransferEthController], + providers: [XTransferEthService], +}) +export class XTransferEthModule {} diff --git a/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.service.ts b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.service.ts new file mode 100644 index 00000000..ded2ba5a --- /dev/null +++ b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.service.ts @@ -0,0 +1,103 @@ +import { + BadRequestException, + Injectable, + InternalServerErrorException, +} from '@nestjs/common'; +import { + getOtherAssets, + getParaId, + InvalidCurrencyError, + NODE_NAMES, + TNode, +} from '@paraspell/sdk'; +import { isValidPolkadotAddress } from '../utils.js'; +import { XTransferEthDto } from './dto/x-transfer-eth.dto.js'; +import { toPolkadot, environment, contextFactory } from '@snowbridge/api'; + +@Injectable() +export class XTransferEthService { + async generateEthCall({ + to, + amount, + address, + destAddress, + currency, + }: XTransferEthDto) { + const toNode = to as TNode; + + if (!NODE_NAMES.includes(toNode)) { + throw new BadRequestException( + `Node ${toNode} is not valid. Check docs for valid nodes.`, + ); + } + + if (!isValidPolkadotAddress(destAddress)) { + throw new BadRequestException('Invalid wallet address.'); + } + + const ethAssets = getOtherAssets('Ethereum'); + const ethAsset = ethAssets.find((asset) => asset.symbol === currency); + if (!ethAsset) { + throw new InvalidCurrencyError( + `Currency ${currency} is not supported for Ethereum transfers`, + ); + } + + const env = environment.SNOWBRIDGE_ENV['polkadot_mainnet']; + const { config } = env; + + const EXECUTION_URL = 'https://eth.llamarpc.com'; + + const context = await contextFactory({ + ethereum: { + execution_url: EXECUTION_URL, + beacon_url: config.BEACON_HTTP_API, + }, + polkadot: { + url: { + bridgeHub: config.BRIDGE_HUB_URL, + assetHub: config.ASSET_HUB_URL, + relaychain: config.RELAY_CHAIN_URL, + parachains: config.PARACHAINS, + }, + }, + appContracts: { + gateway: config.GATEWAY_CONTRACT, + beefy: config.BEEFY_CONTRACT, + }, + }); + const destParaId = getParaId(toNode); + + const signer = { + getAddress: () => Promise.resolve(address), + }; + + try { + const plan = await toPolkadot.validateSend( + context, + signer, + destAddress, + ethAsset.assetId, + destParaId, + BigInt(amount), + BigInt(0), + ); + + if (plan.failure) { + throw new Error( + `Failed to validate send: ${plan.failure.errors.map((e) => e.message).join('\n\n')}`, + ); + } + + return { + token: plan.success.token, + destinationParaId: plan.success.destinationParaId, + destinationFee: plan.success.destinationFee, + amount: plan.success.amount, + }; + } catch (e) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + throw new InternalServerErrorException(e.message); + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f33b39cd..e0377fe0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,7 +93,7 @@ importers: version: 7.0.1(postcss@8.4.41) vite: specifier: ^5.4.0 - version: 5.4.0(@types/node@20.14.12)(sugarss@4.0.1(postcss@8.4.41))(terser@5.31.0) + version: 5.4.0(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0) apps/playground: dependencies: @@ -130,6 +130,9 @@ importers: '@polkadot/util': specifier: ^13.0.2 version: 13.0.2 + '@snowbridge/contract-types': + specifier: ^0.1.18 + version: 0.1.18(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@tabler/icons-react': specifier: ^3.11.0 version: 3.11.0(react@18.3.1) @@ -459,10 +462,10 @@ importers: version: 12.2.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@polkadot/apps-config': specifier: ^0.142.1 - version: 0.142.1(@polkadot/keyring@13.0.2(@polkadot/util-crypto@13.0.2(@polkadot/util@13.0.2))(@polkadot/util@13.0.2))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(utf-8-validate@5.0.10) + version: 0.142.1(@polkadot/keyring@13.0.2(@polkadot/util-crypto@12.6.2(@polkadot/util@13.0.2))(@polkadot/util@13.0.2))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(utf-8-validate@5.0.10) '@polkadot/keyring': specifier: ^13.0.2 - version: 13.0.2(@polkadot/util-crypto@13.0.2(@polkadot/util@13.0.2))(@polkadot/util@13.0.2) + version: 13.0.2(@polkadot/util-crypto@12.6.2(@polkadot/util@13.0.2))(@polkadot/util@13.0.2) '@polkadot/types': specifier: ^12.2.2 version: 12.2.2 @@ -472,6 +475,9 @@ importers: '@sentry/node': specifier: ^7.73.0 version: 7.118.0 + '@snowbridge/api': + specifier: ^0.1.17 + version: 0.1.17(bufferutil@4.0.8)(typechain@8.3.2(typescript@5.5.4))(typescript@5.5.4)(utf-8-validate@5.0.10) axios: specifier: ^1.7.2 version: 1.7.2 @@ -481,6 +487,9 @@ importers: class-validator: specifier: ^0.14.1 version: 0.14.1 + ethers: + specifier: ^6.13.2 + version: 6.13.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) express: specifier: ^4.19.2 version: 4.19.2 @@ -580,7 +589,7 @@ importers: version: 12.2.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@polkadot/apps-config': specifier: ^0.142.1 - version: 0.142.1(@polkadot/keyring@13.0.2(@polkadot/util-crypto@12.6.2(@polkadot/util@13.0.2))(@polkadot/util@13.0.2))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(utf-8-validate@5.0.10) + version: 0.142.1(@polkadot/keyring@13.0.2(@polkadot/util-crypto@13.0.2(@polkadot/util@13.0.2))(@polkadot/util@13.0.2))(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1)(utf-8-validate@5.0.10) '@polkadot/types': specifier: ^12.2.2 version: 12.2.2 @@ -645,7 +654,7 @@ importers: version: 6.1.0(rollup@4.19.0) '@vitest/coverage-v8': specifier: ^2.0.5 - version: 2.0.5(vitest@2.0.5(@types/node@20.14.12)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))(sugarss@4.0.1)(terser@5.31.0)) + version: 2.0.5(vitest@2.0.5(@types/node@20.14.12)(jsdom@24.1.1)(sugarss@4.0.1)(terser@5.31.0)) rollup: specifier: ^4.19.0 version: 4.19.0 @@ -4047,6 +4056,9 @@ packages: '@snowbridge/contract-types@0.1.17': resolution: {integrity: sha512-obMU6z4RfJSh3fmXbmmHhlLl7J55K6ItzJYkE92aQcVG7LE3f+4DyEiWBwkkp2Ijb2+AJLCByld582XfFmnTVA==} + '@snowbridge/contract-types@0.1.18': + resolution: {integrity: sha512-lXg66i0aj3HHMz7UgIiN+NNvEHHlgWQiO8TA41KxVzu4wmlVjZGPWiS+PHPsMtwGT1WURX2XyY7XgSKHUBSApA==} + '@snowfork/snowbridge-types@0.2.7': resolution: {integrity: sha512-Dz3OM8xvYhzL7XU/QOjgyPWZI4IgPKGByaJo6eZe3UMS6F7TLaFaZW1oYhQVTTahGWWAE6ZwwCuMkVh2FC/9bw==} @@ -14694,6 +14706,13 @@ snapshots: - bufferutil - utf-8-validate + '@snowbridge/contract-types@0.1.18(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + dependencies: + ethers: 6.13.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@snowfork/snowbridge-types@0.2.7(@polkadot/util-crypto@13.0.2(@polkadot/util@13.0.2))(@polkadot/util@13.0.2)(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@polkadot/api': 12.2.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -15404,7 +15423,7 @@ snapshots: '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.7) '@types/babel__core': 7.20.5 react-refresh: 0.14.2 - vite: 5.4.0(@types/node@20.14.12)(sugarss@4.0.1(postcss@8.4.41))(terser@5.31.0) + vite: 5.4.0(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0) transitivePeerDependencies: - supports-color @@ -15427,6 +15446,24 @@ snapshots: - supports-color '@vitest/coverage-v8@2.0.5(vitest@2.0.5(@types/node@20.14.12)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))(sugarss@4.0.1)(terser@5.31.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.3.5 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.10 + magicast: 0.3.4 + std-env: 3.7.0 + test-exclude: 7.0.1 + tinyrainbow: 1.2.0 + vitest: 2.0.5(@types/node@20.14.12)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))(sugarss@4.0.1)(terser@5.31.0) + transitivePeerDependencies: + - supports-color + + '@vitest/coverage-v8@2.0.5(vitest@2.0.5(@types/node@20.14.12)(jsdom@24.1.1)(sugarss@4.0.1)(terser@5.31.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -21004,6 +21041,24 @@ snapshots: - supports-color - terser + vite-node@2.0.5(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0): + dependencies: + cac: 6.7.14 + debug: 4.3.5 + pathe: 1.1.2 + tinyrainbow: 1.2.0 + vite: 5.4.0(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite-plugin-node-polyfills@0.22.0(rollup@4.19.0)(vite@5.3.5(@types/node@20.14.12)(sugarss@4.0.1(postcss@8.4.40))(terser@5.31.0)): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.19.0) @@ -21038,6 +21093,17 @@ snapshots: sugarss: 4.0.1(postcss@8.4.40) terser: 5.31.0 + vite@5.3.5(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.41 + rollup: 4.19.0 + optionalDependencies: + '@types/node': 20.14.12 + fsevents: 2.3.3 + sugarss: 4.0.1(postcss@8.4.41) + terser: 5.31.0 + vite@5.4.0(@types/node@20.14.12)(sugarss@4.0.1(postcss@8.4.40))(terser@5.31.0): dependencies: esbuild: 0.21.5 @@ -21049,7 +21115,7 @@ snapshots: sugarss: 4.0.1(postcss@8.4.40) terser: 5.31.0 - vite@5.4.0(@types/node@20.14.12)(sugarss@4.0.1(postcss@8.4.41))(terser@5.31.0): + vite@5.4.0(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0): dependencies: esbuild: 0.21.5 postcss: 8.4.41 @@ -21094,6 +21160,40 @@ snapshots: - supports-color - terser + vitest@2.0.5(@types/node@20.14.12)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))(sugarss@4.0.1)(terser@5.31.0): + dependencies: + '@ampproject/remapping': 2.3.0 + '@vitest/expect': 2.0.5 + '@vitest/pretty-format': 2.0.5 + '@vitest/runner': 2.0.5 + '@vitest/snapshot': 2.0.5 + '@vitest/spy': 2.0.5 + '@vitest/utils': 2.0.5 + chai: 5.1.1 + debug: 4.3.5 + execa: 8.0.1 + magic-string: 0.30.10 + pathe: 1.1.2 + std-env: 3.7.0 + tinybench: 2.8.0 + tinypool: 1.0.0 + tinyrainbow: 1.2.0 + vite: 5.3.5(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0) + vite-node: 2.0.5(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.14.12 + jsdom: 24.1.1(bufferutil@4.0.8)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vitest@2.0.5(@types/node@20.14.12)(jsdom@24.1.1)(sugarss@4.0.1)(terser@5.31.0): dependencies: '@ampproject/remapping': 2.3.0 @@ -21112,8 +21212,8 @@ snapshots: tinybench: 2.8.0 tinypool: 1.0.0 tinyrainbow: 1.2.0 - vite: 5.3.5(@types/node@20.14.12)(sugarss@4.0.1(postcss@8.4.40))(terser@5.31.0) - vite-node: 2.0.5(@types/node@20.14.12)(sugarss@4.0.1(postcss@8.4.40))(terser@5.31.0) + vite: 5.3.5(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0) + vite-node: 2.0.5(@types/node@20.14.12)(sugarss@4.0.1)(terser@5.31.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.14.12