From 5d8655a50e26b9c2ca110acfb2caa187e889d581 Mon Sep 17 00:00:00 2001 From: Michael Absolon Date: Mon, 9 Sep 2024 16:26:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(xcm-api):=20Add=20support=20for=20new=20cu?= =?UTF-8?q?rrency=20input=20types=20=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/TransferInfo.tsx | 14 ++- .../src/components/TransferInfoForm.tsx | 1 + .../playground/src/components/XcmTransfer.tsx | 15 ++- .../src/components/assets/AssetsForm.tsx | 50 ++++++++-- .../src/components/assets/AssetsQueries.tsx | 15 ++- apps/playground/src/consts.ts | 2 +- apps/xcm-api/src/testUtils.ts | 11 ++- .../transfer-info/dto/transfer-info.dto.ts | 8 +- .../transfer-info.controller.test.ts | 57 +++++++++++ .../transfer-info/transfer-info.controller.ts | 19 ++-- .../transfer-info.service.test.ts | 14 +-- .../transfer-info/transfer-info.service.ts | 19 ++-- .../src/x-transfer/dto/XTransferDto.ts | 38 +++++++- .../x-transfer/x-transfer.controller.spec.ts | 7 +- .../src/x-transfer/x-transfer.service.spec.ts | 4 +- .../src/x-transfer/x-transfer.service.ts | 5 +- apps/xcm-api/test/app.e2e-spec.ts | 94 ++++++++++-------- packages/sdk/src/maps/assets.json | 8 ++ .../nodes/supported/AssetHubKusama.test.ts | 30 ++++++ .../sdk/src/nodes/supported/AssetHubKusama.ts | 21 +++- .../nodes/supported/AssetHubPolkadot.test.ts | 97 +++++++++++++++++++ .../src/nodes/supported/AssetHubPolkadot.ts | 20 +++- packages/sdk/src/types/index.ts | 1 + 23 files changed, 440 insertions(+), 110 deletions(-) create mode 100644 apps/xcm-api/src/transfer-info/transfer-info.controller.test.ts create mode 100644 packages/sdk/src/nodes/supported/AssetHubKusama.test.ts create mode 100644 packages/sdk/src/nodes/supported/AssetHubPolkadot.test.ts diff --git a/apps/playground/src/components/TransferInfo.tsx b/apps/playground/src/components/TransferInfo.tsx index 109269f3..fc2fa44a 100644 --- a/apps/playground/src/components/TransferInfo.tsx +++ b/apps/playground/src/components/TransferInfo.tsx @@ -37,6 +37,10 @@ const TransferInfo = () => { const getQueryResult = async (formValues: FormValues) => { const { useApi } = formValues; const originAddress = selectedAccount?.address ?? ""; + const currency = + formValues.customCurrencyType === "id" + ? { id: formValues.currency } + : { symbol: formValues.currency }; if (useApi) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return return await fetchFromApi( @@ -45,10 +49,12 @@ const TransferInfo = () => { destination: formValues.to, accountOrigin: originAddress, accountDestination: formValues.destinationAddress, - currency: formValues.currency, + currency, amount: formValues.amount, }, - `/transfer-info` + `/transfer-info`, + "POST", + true ); } else { return await getTransferInfo( @@ -56,9 +62,7 @@ const TransferInfo = () => { formValues.to, originAddress, formValues.destinationAddress, - formValues.customCurrencyType === "id" - ? { id: formValues.currency } - : { symbol: formValues.currency }, + currency, formValues.amount ); } diff --git a/apps/playground/src/components/TransferInfoForm.tsx b/apps/playground/src/components/TransferInfoForm.tsx index 2ac89b4e..1c3eea2c 100644 --- a/apps/playground/src/components/TransferInfoForm.tsx +++ b/apps/playground/src/components/TransferInfoForm.tsx @@ -40,6 +40,7 @@ const TransferInfoForm: FC = ({ onSubmit, loading }) => { amount: "10000000000000000000", address: "5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96", destinationAddress: "5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96", + customCurrencyType: "symbol", useApi: false, }, diff --git a/apps/playground/src/components/XcmTransfer.tsx b/apps/playground/src/components/XcmTransfer.tsx index 054994cd..4ee046a8 100644 --- a/apps/playground/src/components/XcmTransfer.tsx +++ b/apps/playground/src/components/XcmTransfer.tsx @@ -113,13 +113,22 @@ const XcmTransfer = () => { await submitTxUsingApi( { ...formValues, - currency: - formValues.currency?.symbol ?? formValues.currency?.assetId, + from: + formValues.from === "Polkadot" || formValues.from === "Kusama" + ? undefined + : formValues.from, + to: + formValues.to === "Polkadot" || formValues.to === "Kusama" + ? undefined + : formValues.to, + currency: determineCurrency(formValues), }, formValues.from, "/x-transfer", selectedAccount.address, - injector.signer + injector.signer, + "POST", + true ); } else { await submitUsingSdk( diff --git a/apps/playground/src/components/assets/AssetsForm.tsx b/apps/playground/src/components/assets/AssetsForm.tsx index f69ba175..74530def 100644 --- a/apps/playground/src/components/assets/AssetsForm.tsx +++ b/apps/playground/src/components/assets/AssetsForm.tsx @@ -1,6 +1,14 @@ import { useForm } from "@mantine/form"; import { FC } from "react"; -import { Button, Checkbox, Select, Stack, TextInput } from "@mantine/core"; +import { + Button, + Checkbox, + Group, + SegmentedControl, + Select, + Stack, + TextInput, +} from "@mantine/core"; import { NODE_NAMES, TNodePolkadotKusama } from "@paraspell/sdk"; import { TAssetsQuery } from "../../types"; import { ASSET_QUERIES } from "../../consts"; @@ -8,9 +16,10 @@ import { ASSET_QUERIES } from "../../consts"; export type FormValues = { func: TAssetsQuery; node: TNodePolkadotKusama; - symbol: string; + currency: string; address: string; useApi: boolean; + currencyType?: "id" | "symbol"; }; type Props = { @@ -23,9 +32,10 @@ const AssetsForm: FC = ({ onSubmit, loading }) => { initialValues: { func: "ASSETS_OBJECT", node: "Acala", - symbol: "GLMR", + currency: "GLMR", address: "", useApi: false, + currencyType: "symbol", }, }); @@ -37,6 +47,8 @@ const AssetsForm: FC = ({ onSubmit, loading }) => { funcVal == "HAS_SUPPORT" || funcVal === "BALANCE_FOREIGN"; + const supportsCurrencyType = funcVal === "BALANCE_FOREIGN"; + const showAddressInput = funcVal === "BALANCE_FOREIGN" || funcVal === "BALANCE_NATIVE"; @@ -62,12 +74,32 @@ const AssetsForm: FC = ({ onSubmit, loading }) => { /> {showSymbolInput && ( - + + + {supportsCurrencyType && ( + + )} + )} {showAddressInput && ( diff --git a/apps/playground/src/components/assets/AssetsQueries.tsx b/apps/playground/src/components/assets/AssetsQueries.tsx index f1bc951b..87d726c0 100644 --- a/apps/playground/src/components/assets/AssetsQueries.tsx +++ b/apps/playground/src/components/assets/AssetsQueries.tsx @@ -45,14 +45,15 @@ const AssetsQueries = () => { const submitUsingSdk = async ({ func, node, - symbol, + currency, + currencyType, address, }: FormValues) => { switch (func) { case "ASSETS_OBJECT": return getAssetsObject(node); case "ASSET_ID": - return getAssetId(node, symbol); + return getAssetId(node, currency); case "RELAYCHAIN_SYMBOL": return getRelayChainSymbol(node); case "NATIVE_ASSETS": @@ -62,15 +63,19 @@ const AssetsQueries = () => { case "ALL_SYMBOLS": return getAllAssetsSymbols(node); case "DECIMALS": - return getAssetDecimals(node, symbol); + return getAssetDecimals(node, currency); case "HAS_SUPPORT": - return hasSupportForAsset(node, symbol); + return hasSupportForAsset(node, currency); case "PARA_ID": return getParaId(node); case "BALANCE_NATIVE": return getBalanceNative(address, node); case "BALANCE_FOREIGN": - return getBalanceForeign(address, node, { symbol: symbol }); + return getBalanceForeign( + address, + node, + currencyType === "id" ? { id: currency } : { symbol: currency } + ); } }; diff --git a/apps/playground/src/consts.ts b/apps/playground/src/consts.ts index 886af47d..f5d0f243 100644 --- a/apps/playground/src/consts.ts +++ b/apps/playground/src/consts.ts @@ -1,4 +1,4 @@ -export const API_URL = "https://api.lightspell.xyz"; +export const API_URL = "http://localhost:3001"; export const ASSET_QUERIES = [ "ASSETS_OBJECT", diff --git a/apps/xcm-api/src/testUtils.ts b/apps/xcm-api/src/testUtils.ts index 023abc03..aa82ae9e 100644 --- a/apps/xcm-api/src/testUtils.ts +++ b/apps/xcm-api/src/testUtils.ts @@ -1,5 +1,6 @@ -import { Request } from '@nestjs/common'; - -export const mockRequestObject = () => { - return { headers: {}, body: {}, query: {}, params: {} } as unknown as Request; -}; +export const mockRequestObject = { + headers: {}, + body: {}, + query: {}, + params: {}, +} as unknown as Request; diff --git a/apps/xcm-api/src/transfer-info/dto/transfer-info.dto.ts b/apps/xcm-api/src/transfer-info/dto/transfer-info.dto.ts index 6bd4e133..4faf8085 100644 --- a/apps/xcm-api/src/transfer-info/dto/transfer-info.dto.ts +++ b/apps/xcm-api/src/transfer-info/dto/transfer-info.dto.ts @@ -1,3 +1,5 @@ +import { TCurrencyCore } from '@paraspell/sdk'; +import { CurrencyCoreSchema } from '../../x-transfer/dto/XTransferDto.js'; import { z } from 'zod'; export const TransferInfoSchema = z.object({ @@ -7,7 +9,7 @@ export const TransferInfoSchema = z.object({ accountDestination: z .string() .min(1, { message: 'Destination address is required' }), - currency: z.string(), + currency: CurrencyCoreSchema, amount: z.union([ z.string().refine( (val) => { @@ -23,3 +25,7 @@ export const TransferInfoSchema = z.object({ }); export type TransferInfoDto = z.infer; + +export type PatchedTransferInfoDto = TransferInfoDto & { + currency: TCurrencyCore; +}; diff --git a/apps/xcm-api/src/transfer-info/transfer-info.controller.test.ts b/apps/xcm-api/src/transfer-info/transfer-info.controller.test.ts new file mode 100644 index 00000000..ffb65251 --- /dev/null +++ b/apps/xcm-api/src/transfer-info/transfer-info.controller.test.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import { vi, describe, beforeEach, it, expect } from 'vitest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { mockRequestObject } from '../testUtils.js'; +import { AnalyticsService } from '../analytics/analytics.service.js'; +import { TransferInfoController } from './transfer-info.controller.js'; +import { TransferInfoService } from './transfer-info.service.js'; +import { PatchedTransferInfoDto } from './dto/transfer-info.dto.js'; +import { TTransferInfo } from '@paraspell/sdk'; + +describe('TransferInfoController', () => { + let controller: TransferInfoController; + let service: TransferInfoService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [TransferInfoController], + providers: [ + TransferInfoService, + { + provide: AnalyticsService, + useValue: { get: () => '', track: vi.fn() }, + }, + ], + }).compile(); + + controller = module.get(TransferInfoController); + service = module.get(TransferInfoService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('generateXcmCall', () => { + it('should call generateXcmCall service method with correct parameters and return result', async () => { + const queryParams: PatchedTransferInfoDto = { + origin: 'Acala', + destination: 'Basilisk', + accountOrigin: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + accountDestination: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + currency: { symbol: 'DOT' }, + amount: 100, + }; + const mockResult = {} as TTransferInfo; + vi.spyOn(service, 'getTransferInfo').mockResolvedValue(mockResult); + + const result = await controller.getTransferInfo( + queryParams, + mockRequestObject, + ); + + expect(result).toBe(mockResult); + expect(service.getTransferInfo).toHaveBeenCalledWith(queryParams); + }); + }); +}); diff --git a/apps/xcm-api/src/transfer-info/transfer-info.controller.ts b/apps/xcm-api/src/transfer-info/transfer-info.controller.ts index 89a8e02b..64a7a66c 100644 --- a/apps/xcm-api/src/transfer-info/transfer-info.controller.ts +++ b/apps/xcm-api/src/transfer-info/transfer-info.controller.ts @@ -1,10 +1,10 @@ -import { Controller, Get, Request, Query, Req, UsePipes } from '@nestjs/common'; +import { Controller, Request, Req, UsePipes, Post, Body } from '@nestjs/common'; import { AnalyticsService } from '../analytics/analytics.service.js'; import { EventName } from '../analytics/EventName.js'; import { ZodValidationPipe } from '../zod-validation-pipe.js'; import { TransferInfoService } from './transfer-info.service.js'; import { - TransferInfoDto, + PatchedTransferInfoDto, TransferInfoSchema, } from './dto/transfer-info.dto.js'; @@ -18,21 +18,24 @@ export class TransferInfoController { private trackAnalytics( eventName: EventName, req: Request, - params: TransferInfoDto, + params: PatchedTransferInfoDto, ) { const { origin, destination, currency, amount } = params; this.analyticsService.track(eventName, req, { origin, destination, - currency, + currency: JSON.stringify(currency), amount, }); } - @Get() + @Post() @UsePipes(new ZodValidationPipe(TransferInfoSchema)) - getTransferInfo(@Query() queryParams: TransferInfoDto, @Req() req: Request) { - this.trackAnalytics(EventName.GET_TRANSFER_INFO, req, queryParams); - return this.transferInfoService.getTransferInfo(queryParams); + async getTransferInfo( + @Body() params: PatchedTransferInfoDto, + @Req() req: Request, + ) { + this.trackAnalytics(EventName.GET_TRANSFER_INFO, req, params); + return await this.transferInfoService.getTransferInfo(params); } } diff --git a/apps/xcm-api/src/transfer-info/transfer-info.service.test.ts b/apps/xcm-api/src/transfer-info/transfer-info.service.test.ts index b349a081..7ce41bc8 100644 --- a/apps/xcm-api/src/transfer-info/transfer-info.service.test.ts +++ b/apps/xcm-api/src/transfer-info/transfer-info.service.test.ts @@ -34,7 +34,7 @@ describe('TransferInfoService', () => { destination: 'Kusama', accountOrigin: '0x123', accountDestination: '0x456', - currency: 'DOT', + currency: { symbol: 'DOT' }, amount: '1000', }), ).rejects.toThrow(BadRequestException); @@ -47,7 +47,7 @@ describe('TransferInfoService', () => { destination: 'InvalidNode', accountOrigin: '0x123', accountDestination: '0x456', - currency: 'DOT', + currency: { symbol: 'DOT' }, amount: '1000', }), ).rejects.toThrow(BadRequestException); @@ -61,7 +61,7 @@ describe('TransferInfoService', () => { destination: 'Kusama', accountOrigin: '0x123', accountDestination: '0x456', - currency: 'DOT', + currency: { symbol: 'DOT' }, amount: '1000', }), ).rejects.toThrow(BadRequestException); @@ -74,10 +74,10 @@ describe('TransferInfoService', () => { destination: 'Kusama', accountOrigin: '0x123', accountDestination: '0x456', - currency: 'DOT', + currency: { symbol: 'DOT' }, amount: '1000', }); - expect(result).toEqual(JSON.stringify({ some: 'data' })); + expect(result).toEqual({ some: 'data' }); }); it('handles InvalidCurrencyError by throwing BadRequestException', async () => { @@ -90,7 +90,7 @@ describe('TransferInfoService', () => { destination: 'Kusama', accountOrigin: '0x123', accountDestination: '0x456', - currency: 'DOT', + currency: { symbol: 'DOT' }, amount: '1000', }), ).rejects.toThrow(BadRequestException); @@ -104,7 +104,7 @@ describe('TransferInfoService', () => { destination: 'Kusama', accountOrigin: '0x123', accountDestination: '0x456', - currency: 'DOT', + currency: { symbol: 'DOT' }, amount: '1000', }), ).rejects.toThrow(InternalServerErrorException); diff --git a/apps/xcm-api/src/transfer-info/transfer-info.service.ts b/apps/xcm-api/src/transfer-info/transfer-info.service.ts index 54225a57..7dd82fa1 100644 --- a/apps/xcm-api/src/transfer-info/transfer-info.service.ts +++ b/apps/xcm-api/src/transfer-info/transfer-info.service.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-explicit-any */ + import { BadRequestException, Injectable, @@ -9,16 +9,11 @@ import { InvalidCurrencyError, NODES_WITH_RELAY_CHAINS_DOT_KSM, TNodeDotKsmWithRelayChains, + TTransferInfo, getTransferInfo, } from '@paraspell/sdk'; import { isValidWalletAddress } from '../utils.js'; -import { TransferInfoDto } from './dto/transfer-info.dto.js'; - -const serializeJson = (param: any): any => { - return JSON.stringify(param, (_key, value) => - typeof value === 'bigint' ? value.toString() : value, - ); -}; +import { PatchedTransferInfoDto } from './dto/transfer-info.dto.js'; @Injectable() export class TransferInfoService { @@ -29,7 +24,7 @@ export class TransferInfoService { accountDestination, currency, amount, - }: TransferInfoDto) { + }: PatchedTransferInfoDto) { const originNode = origin as TNodeDotKsmWithRelayChains | undefined; const destNode = destination as TNodeDotKsmWithRelayChains | undefined; @@ -53,14 +48,14 @@ export class TransferInfoService { throw new BadRequestException('Invalid destination wallet address.'); } - let response; + let response: TTransferInfo; try { response = await getTransferInfo( originNode, destNode, accountOrigin, accountDestination, - { symbol: currency }, + currency, amount.toString(), ); } catch (e) { @@ -70,6 +65,6 @@ export class TransferInfoService { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access throw new InternalServerErrorException(e.message); } - return serializeJson(response); + return response; } } diff --git a/apps/xcm-api/src/x-transfer/dto/XTransferDto.ts b/apps/xcm-api/src/x-transfer/dto/XTransferDto.ts index 2b917ee4..50e3635a 100644 --- a/apps/xcm-api/src/x-transfer/dto/XTransferDto.ts +++ b/apps/xcm-api/src/x-transfer/dto/XTransferDto.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { Version } from '@paraspell/sdk'; +import { TCurrencyInput, Version } from '@paraspell/sdk'; import { MultiLocationSchema } from '@paraspell/xcm-analyser'; const StringOrNumber = z @@ -32,9 +32,33 @@ export const MultiAssetSchema = z.union([ export type TMultiAsset = z.infer; -const CurrencySchema = z - .union([z.string(), MultiLocationSchema, z.array(MultiAssetSchema)]) - .optional(); +export const CurrencyCoreSchema = z.union([ + z + .object({ + symbol: z.string(), + }) + .required(), + z + .object({ + id: z.union([z.string(), z.number(), z.bigint()]), + }) + .required(), +]); + +export const CurrencySchema = z.union([ + z.object({ + symbol: z.string(), + }), + z.object({ + id: z.union([z.string(), z.number(), z.bigint()]), + }), + z.object({ + multilocation: MultiLocationSchema, + }), + z.object({ + multiasset: z.array(MultiAssetSchema), + }), +]); const versionValues = Object.values(Version) as [Version, ...Version[]]; @@ -57,8 +81,12 @@ export const XTransferDtoSchema = z.object({ z.string().min(1, { message: 'Address is required' }), MultiLocationSchema, ]), - currency: CurrencySchema, + currency: CurrencySchema.optional(), xcmVersion: z.enum(versionValues).optional(), }); export type XTransferDto = z.infer; + +export type XPatchedTransferDto = XTransferDto & { + currency: TCurrencyInput; +}; diff --git a/apps/xcm-api/src/x-transfer/x-transfer.controller.spec.ts b/apps/xcm-api/src/x-transfer/x-transfer.controller.spec.ts index 02816c62..28edb165 100644 --- a/apps/xcm-api/src/x-transfer/x-transfer.controller.spec.ts +++ b/apps/xcm-api/src/x-transfer/x-transfer.controller.spec.ts @@ -5,6 +5,7 @@ import { XTransferService } from './x-transfer.service.js'; import { mockRequestObject } from '../testUtils.js'; import { AnalyticsService } from '../analytics/analytics.service.js'; import { XTransferDto } from './dto/XTransferDto.js'; +import { Extrinsic } from '@paraspell/sdk'; // Integration tests to ensure controller and service are working together describe('XTransferController', () => { @@ -38,10 +39,10 @@ describe('XTransferController', () => { to: 'Basilisk', amount: 100, address: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', - currency: 'DOT', + currency: { symbol: 'DOT' }, }; - const mockResult = 'serialized-api-call'; - vi.spyOn(service, 'generateXcmCall' as any).mockResolvedValue(mockResult); + const mockResult = {} as Extrinsic; + vi.spyOn(service, 'generateXcmCall').mockResolvedValue(mockResult); const result = await controller.generateXcmCall( queryParams, diff --git a/apps/xcm-api/src/x-transfer/x-transfer.service.spec.ts b/apps/xcm-api/src/x-transfer/x-transfer.service.spec.ts index 6432f64a..e0d2c499 100644 --- a/apps/xcm-api/src/x-transfer/x-transfer.service.spec.ts +++ b/apps/xcm-api/src/x-transfer/x-transfer.service.spec.ts @@ -36,7 +36,7 @@ describe('XTransferService', () => { const amount = 100; const address = '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96'; - const currency = 'DOT'; + const currency = { symbol: 'DOT' }; const serializedApiCall = 'serialized-api-call'; const invalidNode = 'InvalidNode'; @@ -179,7 +179,7 @@ describe('XTransferService', () => { to: 'Basilisk', amount, address, - currency: 'UNKNOWN', + currency: { symbol: 'UNKNOWN' }, }; const builderMock = { diff --git a/apps/xcm-api/src/x-transfer/x-transfer.service.ts b/apps/xcm-api/src/x-transfer/x-transfer.service.ts index faadfa3f..bb348e10 100644 --- a/apps/xcm-api/src/x-transfer/x-transfer.service.ts +++ b/apps/xcm-api/src/x-transfer/x-transfer.service.ts @@ -1,7 +1,8 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-explicit-any */ + import { BadRequestException, Injectable, @@ -36,7 +37,7 @@ export class XTransferService { if (fromNode && !NODE_NAMES.includes(fromNode)) { throw new BadRequestException( - `Node ${from} is not valid. Check docs for valid nodes.`, + `Node ${fromNode} is not valid. Check docs for valid nodes.`, ); } diff --git a/apps/xcm-api/test/app.e2e-spec.ts b/apps/xcm-api/test/app.e2e-spec.ts index 7d145da0..fcf698a0 100644 --- a/apps/xcm-api/test/app.e2e-spec.ts +++ b/apps/xcm-api/test/app.e2e-spec.ts @@ -19,10 +19,8 @@ import { getParaId, getRelayChainSymbol, getSupportedPallets, - getTransferInfo, hasSupportForAsset, } from '@paraspell/sdk'; -import { ApiPromise } from '@polkadot/api'; import { RouterDto } from '../src/router/dto/RouterDto'; import { describe, beforeAll, it, expect } from 'vitest'; import { TransferInfoDto } from '../src/transfer-info/dto/transfer-info.dto'; @@ -34,6 +32,10 @@ describe('XCM API (e2e)', () => { const unknownNode = 'UnknownNode'; beforeAll(async () => { + BigInt.prototype['toJSON'] = function () { + return this.toString(); + }; + const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); @@ -308,10 +310,10 @@ describe('XCM API (e2e)', () => { .expect(400); }); - it(`Generate XCM call - Parachain to parachain all valid - ${xTransferUrl} (GET)`, async () => { - const from: TNode = 'AssetHubKusama'; - const to: TNode = 'Basilisk'; - const currency = 'KSM'; + it(`Generate XCM call - Parachain to parachain all valid - ${xTransferUrl} (POST)`, async () => { + const from: TNode = 'Acala'; + const to: TNode = 'Hydration'; + const currency = { symbol: 'HDX' }; const api = await createApiInstanceForNode(from); const serializedApiCall = await Builder(api) .from(from) @@ -322,22 +324,38 @@ describe('XCM API (e2e)', () => { .buildSerializedApiCall(); await api.disconnect(); return request(app.getHttpServer()) - .get(xTransferUrl) - .query({ + .post(xTransferUrl) + .send({ from, to, amount, address, currency, }) - .expect(200) + .expect(201) .expect(serializedApiCall); }); - it(`Generate XCM call - Parachain to parachain all valid - ${xTransferHashUrl} (GET)`, async () => { + it(`Generate XCM call - Parachain to parachain invalid scenario - ${xTransferHashUrl} (POST)`, async () => { + const from: TNode = 'AssetHubKusama'; + const to: TNode = 'Basilisk'; + const currency = { symbol: 'KSM' }; + return request(app.getHttpServer()) + .post(xTransferHashUrl) + .send({ + from, + to, + amount, + address, + currency, + }) + .expect(500); + }); + + it(`Generate XCM call - Parachain to parachain all valid - ${xTransferHashUrl} (POST)`, async () => { const from: TNode = 'AssetHubKusama'; const to: TNode = 'Basilisk'; - const currency = 'KSM'; + const currency = { symbol: 'USDT' }; const api = await createApiInstanceForNode(from); const tx = await Builder(api) .from(from) @@ -375,7 +393,7 @@ describe('XCM API (e2e)', () => { const serializedApiCall = await Builder(api) .from(from) .to(to) - .currency(currency) + .currency({ multilocation: currency }) .amount(amount) .address(address) .buildSerializedApiCall(); @@ -387,13 +405,13 @@ describe('XCM API (e2e)', () => { to, amount, address, - currency, + currency: { multilocation: currency }, }) .expect(201) .expect(serializedApiCall); }); - it(`Generate XCM call - Parachain to parachain override currency as multi asset - ${xTransferUrl} (GET)`, async () => { + it(`Generate XCM call - Parachain to parachain override currency as multi asset - ${xTransferUrl} (POST)`, async () => { const from: TNode = 'AssetHubPolkadot'; const to: TNode = 'Acala'; const currency: TMultiAsset = { @@ -415,7 +433,7 @@ describe('XCM API (e2e)', () => { const serializedApiCall = await Builder(api) .from(from) .to(to) - .currency([currency]) + .currency({ multiasset: [currency] }) .amount(amount) .address(address) .buildSerializedApiCall(); @@ -427,7 +445,7 @@ describe('XCM API (e2e)', () => { to, amount, address, - currency: [currency], + currency: { multiasset: [currency] }, }) .expect(201) .expect(serializedApiCall); @@ -691,59 +709,59 @@ describe('XCM API (e2e)', () => { describe('Transfer info controller', () => { const transferInfo: TransferInfoDto = { - origin: 'AssetHubPolkadot', - destination: 'Polkadot', - accountOrigin: '5EtHZF4E8QagNCz6naobCkCAUT52SbcEqaXiDUu2PjUHxZid', - accountDestination: '5EtHZF4E8QagNCz6naobCkCAUT52SbcEqaXiDUu2PjUHxZid', - currency: 'DOT', - amount: '10000', + origin: 'Acala', + destination: 'Astar', + accountOrigin: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + accountDestination: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + currency: { symbol: 'DOT' }, + amount: '100000000', }; - it('Generate transfer info call - invalid origin provided - /transfer-info (GET)', () => { + it('Generate transfer info call - invalid origin provided - /transfer-info (POST)', () => { return request(app.getHttpServer()) - .get('/transfer-info') - .query({ + .post('/transfer-info') + .send({ ...transferInfo, origin: unknownNode, }) .expect(400); }); - it('Generate transfer info call - invalid destination provided - /transfer-info (GET)', () => { + it('Generate transfer info call - invalid destination provided - /transfer-info (POST)', () => { return request(app.getHttpServer()) - .get('/transfer-info') - .query({ + .post('/transfer-info') + .send({ ...transferInfo, destination: unknownNode, }) .expect(400); }); - it('Generate transfer info call - invalid wallet address origin - /transfer-info (GET)', () => { + it('Generate transfer info call - invalid wallet address origin - /transfer-info (POST)', () => { return request(app.getHttpServer()) - .get('/transfer-info') - .query({ + .post('/transfer-info') + .send({ ...transferInfo, accountOrigin: 'InvalidWalletAddress', }) .expect(400); }); - it('Generate transfer info call - invalid wallet address destination - /transfer-info (GET)', () => { + it('Generate transfer info call - invalid wallet address destination - /transfer-info (POST)', () => { return request(app.getHttpServer()) - .get('/transfer-info') - .query({ + .post('/transfer-info') + .send({ ...transferInfo, accountDestination: 'InvalidWalletAddress', }) .expect(400); }); - it('Generate transfer info call - all valid - /transfer-info (GET)', async () => { + it('Generate transfer info call - all valid - /transfer-info (POST)', async () => { return request(app.getHttpServer()) - .get('/transfer-info') - .query(transferInfo) - .expect(200); + .post('/transfer-info') + .send(transferInfo) + .expect(201); }); }); diff --git a/packages/sdk/src/maps/assets.json b/packages/sdk/src/maps/assets.json index 9a44befc..3d4bc15d 100644 --- a/packages/sdk/src/maps/assets.json +++ b/packages/sdk/src/maps/assets.json @@ -1633,6 +1633,10 @@ { "symbol": "DOT", "decimals": 10 + }, + { + "symbol": "KSM", + "decimals": 12 } ], "otherAssets": [ @@ -4886,6 +4890,10 @@ { "symbol": "KSM", "decimals": 12 + }, + { + "symbol": "DOT", + "decimals": 10 } ], "otherAssets": [ diff --git a/packages/sdk/src/nodes/supported/AssetHubKusama.test.ts b/packages/sdk/src/nodes/supported/AssetHubKusama.test.ts new file mode 100644 index 00000000..d4763086 --- /dev/null +++ b/packages/sdk/src/nodes/supported/AssetHubKusama.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { ScenarioNotSupportedError } from '../../errors' +import { PolkadotXCMTransferInput } from '../../types' +import { getNode } from '../../utils' + +describe('transferPolkadotXCM', () => { + it('throws ScenarioNotSupportedError for native KSM transfers in para to para scenarios', () => { + const assetHub = getNode('AssetHubKusama') + const input = { + currencySymbol: 'KSM', + currencyId: undefined, + scenario: 'ParaToPara', + destination: 'Karura' + } as PolkadotXCMTransferInput + + expect(() => assetHub.transferPolkadotXCM(input)).toThrow(ScenarioNotSupportedError) + }) + + it('throws ScenarioNotSupportedError for native DOT transfers in para to para scenarios', () => { + const assetHub = getNode('AssetHubKusama') + const input = { + currencySymbol: 'DOT', + currencyId: undefined, + scenario: 'ParaToPara', + destination: 'Karura' + } as PolkadotXCMTransferInput + + expect(() => assetHub.transferPolkadotXCM(input)).toThrow(ScenarioNotSupportedError) + }) +}) diff --git a/packages/sdk/src/nodes/supported/AssetHubKusama.ts b/packages/sdk/src/nodes/supported/AssetHubKusama.ts index ffe400ee..21d65e32 100644 --- a/packages/sdk/src/nodes/supported/AssetHubKusama.ts +++ b/packages/sdk/src/nodes/supported/AssetHubKusama.ts @@ -1,5 +1,6 @@ // Contains detailed structure of XCM call construction for AssetHubKusama Parachain +import { ScenarioNotSupportedError } from '../../errors' import { constructRelayToParaParameters } from '../../pallets/xcmPallet/utils' import { type IPolkadotXCMTransfer, @@ -21,14 +22,30 @@ class AssetHubKusama extends ParachainNode implements IPolkadotXCMTransfer { } transferPolkadotXCM(input: PolkadotXCMTransferInput) { + const { destination, currencySymbol, currencyId, scenario } = input // TESTED https://kusama.subscan.io/xcm_message/kusama-ddc2a48f0d8e0337832d7aae26f6c3053e1f4ffd // TESTED https://kusama.subscan.io/xcm_message/kusama-8e423130a4d8b61679af95dbea18a55124f99672 - if (input.destination === 'AssetHubPolkadot') { + if (destination === 'AssetHubPolkadot') { return getNode('AssetHubPolkadot').handleBridgeTransfer(input, 'Polkadot') } - const { scenario } = input + if (scenario === 'ParaToPara' && currencySymbol === 'KSM' && currencyId === undefined) { + throw new ScenarioNotSupportedError( + this.node, + scenario, + 'Para to Para scenarios for KSM transfer from AssetHub are not supported, you have to transfer KSM to Relay chain and transfer to destination chain from Relay chain.' + ) + } + + if (scenario === 'ParaToPara' && currencySymbol === 'DOT' && currencyId === undefined) { + throw new ScenarioNotSupportedError( + this.node, + scenario, + 'Bridged DOT cannot currently be transfered from AssetHubKusama, if you are sending different DOT asset, please specify {id: }.' + ) + } + const section = scenario === 'ParaToPara' ? 'limitedReserveTransferAssets' : 'limitedTeleportAssets' return PolkadotXCMTransferImpl.transferPolkadotXCM(input, section, 'Unlimited') diff --git a/packages/sdk/src/nodes/supported/AssetHubPolkadot.test.ts b/packages/sdk/src/nodes/supported/AssetHubPolkadot.test.ts new file mode 100644 index 00000000..4fcd83f8 --- /dev/null +++ b/packages/sdk/src/nodes/supported/AssetHubPolkadot.test.ts @@ -0,0 +1,97 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/unbound-method */ +import { describe, it, expect, vi } from 'vitest' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import { InvalidCurrencyError, ScenarioNotSupportedError } from '../../errors' +import { Extrinsic, PolkadotXCMTransferInput } from '../../types' +import { ApiPromise } from '@polkadot/api' +import { getNode } from '../../utils' + +vi.mock('ethers', () => ({ + ethers: { + isAddress: vi.fn() + } +})) + +vi.mock('../polkadotXcm', async importOriginal => { + const actual: any = await importOriginal() + return { + default: { + ...actual.default, + transferPolkadotXCM: vi.fn() + } + } +}) + +vi.mock('../../pallets/assets', () => ({ + getOtherAssets: vi.fn(), + getParaId: vi.fn() +})) + +const mockInput = { + api: { + createType: vi.fn().mockReturnValue({ + toHex: vi.fn().mockReturnValue('0x123') + }) + } as unknown as ApiPromise, + currencySymbol: 'DOT', + currencySelection: {}, + currencyId: '0', + scenario: 'ParaToRelay', + header: {}, + addressSelection: {}, + paraIdTo: 1001, + amount: '1000', + address: 'address' +} as PolkadotXCMTransferInput + +describe('handleBridgeTransfer', () => { + it('should process a valid DOT transfer to Polkadot', async () => { + const assetHub = getNode('AssetHubPolkadot') + + const mockResult = {} as Extrinsic + + vi.mocked(PolkadotXCMTransferImpl.transferPolkadotXCM).mockResolvedValue(mockResult) + + await expect(assetHub.handleBridgeTransfer(mockInput, 'Polkadot')).resolves.toStrictEqual({}) + expect(PolkadotXCMTransferImpl.transferPolkadotXCM).toHaveBeenCalledTimes(1) + }) + + it('throws an error for unsupported currency', () => { + const assetHub = getNode('AssetHubPolkadot') + const input = { + ...mockInput, + currencySymbol: 'UNKNOWN' + } + expect(() => assetHub.handleBridgeTransfer(input, 'Kusama')).toThrowError(InvalidCurrencyError) + }) +}) + +describe('transferPolkadotXCM', () => { + it('throws ScenarioNotSupportedError for native DOT transfers in para to para scenarios', () => { + const assetHub = getNode('AssetHubPolkadot') + const input = { + ...mockInput, + currencySymbol: 'DOT', + currencyId: undefined, + scenario: 'ParaToPara', + destination: 'Acala' + } as PolkadotXCMTransferInput + + expect(() => assetHub.transferPolkadotXCM(input)).toThrow(ScenarioNotSupportedError) + }) + + it('throws ScenarioNotSupportedError for native KSM transfers in para to para scenarios', () => { + const assetHub = getNode('AssetHubPolkadot') + const input = { + ...mockInput, + currencySymbol: 'KSM', + currencyId: undefined, + scenario: 'ParaToPara', + destination: 'Acala' + } as PolkadotXCMTransferInput + + expect(() => assetHub.transferPolkadotXCM(input)).toThrow(ScenarioNotSupportedError) + }) +}) diff --git a/packages/sdk/src/nodes/supported/AssetHubPolkadot.ts b/packages/sdk/src/nodes/supported/AssetHubPolkadot.ts index 9361e144..7a531376 100644 --- a/packages/sdk/src/nodes/supported/AssetHubPolkadot.ts +++ b/packages/sdk/src/nodes/supported/AssetHubPolkadot.ts @@ -1,7 +1,7 @@ // Contains detailed structure of XCM call construction for Statemint Parachain import { ethers } from 'ethers' -import { InvalidCurrencyError } from '../../errors' +import { InvalidCurrencyError, ScenarioNotSupportedError } from '../../errors' import { constructRelayToParaParameters, createBridgeCurrencySpec, @@ -179,7 +179,7 @@ class AssetHubPolkadot extends ParachainNode implements IPolkadotXCMTransfer { } transferPolkadotXCM(input: PolkadotXCMTransferInput) { - const { scenario } = input + const { scenario, currencySymbol, currencyId } = input if (input.destination === 'AssetHubKusama') { return this.handleBridgeTransfer(input, 'Kusama') @@ -193,6 +193,22 @@ class AssetHubPolkadot extends ParachainNode implements IPolkadotXCMTransfer { return this.handleMythosTransfer(input) } + if (scenario === 'ParaToPara' && currencySymbol === 'DOT' && currencyId === undefined) { + throw new ScenarioNotSupportedError( + this.node, + scenario, + 'Para to Para scenarios for DOT transfer from AssetHub are not supported, you have to transfer DOT to Relay chain and transfer to destination chain from Relay chain.' + ) + } + + if (scenario === 'ParaToPara' && currencySymbol === 'KSM' && currencyId === undefined) { + throw new ScenarioNotSupportedError( + this.node, + scenario, + 'Bridged KSM cannot currently be transfered from AssetHubPolkadot, if you are sending different KSM asset, please specify {id: }.' + ) + } + const section = scenario === 'ParaToPara' ? 'limitedReserveTransferAssets' : 'limitedTeleportAssets' return PolkadotXCMTransferImpl.transferPolkadotXCM(input, section, 'Unlimited') diff --git a/packages/sdk/src/types/index.ts b/packages/sdk/src/types/index.ts index a760c4f5..b2e09194 100644 --- a/packages/sdk/src/types/index.ts +++ b/packages/sdk/src/types/index.ts @@ -8,3 +8,4 @@ export * from './TExistentialDeposit' export * from './TMultiAsset' export * from './TCurrency' export * from './TBuilder' +export * from './TTransferInfo'