From aeb32bb6a54c1bc2e527cf587b8e0a44e3c397a5 Mon Sep 17 00:00:00 2001 From: Michael Absolon Date: Thu, 3 Oct 2024 01:24:26 +0200 Subject: [PATCH] =?UTF-8?q?test:=20Improve=20overall=20unit=20test=20cover?= =?UTF-8?q?age=20to=20above=2090%=20=F0=9F=A7=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/TransferInfo.tsx | 3 +- apps/playground/src/main.tsx | 6 - apps/playground/src/utils/replaceBigInt.ts | 2 + apps/visualizator-be/package.json | 3 + .../src/channels/channel.entity.test.ts | 49 ++++ .../src/channels/channel.entity.ts | 19 +- apps/visualizator-be/src/main.test.ts | 30 +++ apps/visualizator-be/src/main.ts | 5 +- .../src/messages/message.entity.test.ts | 47 ++++ .../src/messages/message.entity.ts | 28 ++- .../src/messages/messages.module.test.ts | 44 ++++ .../src/messages/messages.service.test.ts | 25 +- .../src/messages/messages.service.ts | 2 +- .../models/account-msg-count.model.ts | 5 +- .../src/messages/models/asset-count.model.ts | 7 +- .../models/message-count-by-day.model.ts | 5 +- .../models/message-count-by-status.model.ts | 9 +- .../messages/models/message-count.model.ts | 7 +- .../src/messages/models/models.test.ts | 162 ++++++++++++ .../src/utils/graphql.utils.test.ts | 9 + .../src/utils/graphql.utils.ts | 3 + ...vice.spec.ts => analytics.service.test.ts} | 36 ++- .../src/analytics/analytics.service.ts | 2 +- apps/xcm-api/src/app.controller.spec.ts | 21 -- apps/xcm-api/src/app.controller.test.ts | 36 +++ .../asset-claim.controller.test.ts | 91 +++++++ .../asset-claim/asset-claim.service.test.ts | 48 +++- ...ller.spec.ts => assets.controller.test.ts} | 104 ++++---- ...service.spec.ts => assets.service.test.ts} | 18 +- apps/xcm-api/src/auth/auth.guard.ts | 2 +- apps/xcm-api/src/auth/auth.service.ts | 2 +- .../generateConfirmationEmailHtml.test.ts | 36 +++ .../generateNewHigherLimitRequestHtml.test.ts | 43 ++++ apps/xcm-api/src/auth/utils/utils.test.ts | 113 +++++++++ apps/xcm-api/src/config/sentry.config.test.ts | 48 ++++ .../src/config/throttler.config.test.ts | 100 ++++++++ apps/xcm-api/src/config/throttler.config.ts | 2 +- .../xcm-api/src/config/typeorm.config.test.ts | 51 ++++ apps/xcm-api/src/main.test.ts | 49 ++++ apps/xcm-api/src/main.ts | 17 +- ...ler.spec.ts => pallets.controller.test.ts} | 14 +- .../xcm-api/src/pallets/pallets.controller.ts | 4 +- ...ervice.spec.ts => pallets.service.test.ts} | 0 apps/xcm-api/src/router/dto/RouterDto.ts | 13 +- .../src/router/router.controller.spec.ts | 61 ----- .../src/router/router.controller.test.ts | 115 +++++++++ ...service.spec.ts => router.service.test.ts} | 78 +++--- .../transfer-info/dto/transfer-info.dto.ts | 13 +- .../transfer-info.controller.test.ts | 7 +- .../transfer-info.service.test.ts | 16 ++ apps/xcm-api/src/types/types.ts | 4 +- apps/xcm-api/src/users/users.service.test.ts | 64 +++++ .../src/{utils.spec.ts => utils.test.ts} | 3 - apps/xcm-api/src/utils.ts | 28 +-- apps/xcm-api/src/utils/replaceBigInt.test.ts | 26 ++ apps/xcm-api/src/utils/replaceBigInt.ts | 2 + apps/xcm-api/src/utils/validateAmount.test.ts | 15 ++ apps/xcm-api/src/utils/validateAmount.ts | 4 + .../src/utils/validateRecaptcha.test.ts | 61 +++++ apps/xcm-api/src/utils/validateRecaptcha.ts | 24 ++ .../x-transfer-eth/dto/x-transfer-eth.dto.ts | 13 +- .../x-transfer-eth.controller.test.ts | 7 +- .../x-transfer-eth.service.test.ts | 115 +++++++++ .../src/x-transfer/dto/XTransferDto.ts | 13 +- ....spec.ts => x-transfer.controller.test.ts} | 22 +- .../src/x-transfer/x-transfer.controller.ts | 5 +- ...ice.spec.ts => x-transfer.service.test.ts} | 80 +++++- .../src/x-transfer/x-transfer.service.ts | 1 + .../xcm-analyser.controller.test.ts | 75 ++++++ .../xcm-analyser/xcm-analyser.service.test.ts | 4 +- .../src/xcm-analyser/xcm-analyser.service.ts | 4 +- apps/xcm-api/test/app.e2e-spec.ts | 10 +- apps/xcm-api/vitest.config.ts | 4 +- eslint.config.js | 1 + .../sdk/src/errors/MissingApiPromiseError.ts | 6 - packages/sdk/src/index.test.ts | 30 +++ packages/sdk/src/nodes/ParachainNode.test.ts | 4 +- packages/sdk/src/nodes/ParachainNode.ts | 9 +- .../sdk/src/nodes/supported/Acala.test.ts | 63 +++++ .../sdk/src/nodes/supported/Altair.test.ts | 51 ++++ .../sdk/src/nodes/supported/Amplitude.test.ts | 38 +++ .../nodes/supported/AssetHubPolkadot.test.ts | 195 +++++++++------ .../src/nodes/supported/AssetHubPolkadot.ts | 5 +- .../sdk/src/nodes/supported/Astar.test.ts | 71 ++++++ .../sdk/src/nodes/supported/Bajun.test.ts | 66 +++++ .../sdk/src/nodes/supported/Basilisk.test.ts | 49 +++- .../src/nodes/supported/BifrostKusama.test.ts | 48 ++++ .../nodes/supported/BifrostPolkadot.test.ts | 48 ++++ .../nodes/supported/BridgeHubKusama.test.ts | 76 ++++++ .../nodes/supported/BridgeHubPolkadot.test.ts | 76 ++++++ .../sdk/src/nodes/supported/Calamari.test.ts | 38 +++ .../src/nodes/supported/Centrifuge.test.ts | 49 ++++ .../src/nodes/supported/Collectives.test.ts | 71 ++++++ .../nodes/supported/ComposableFinance.test.ts | 38 +++ .../nodes/supported/CoretimeKusama.test.ts | 79 ++++++ .../nodes/supported/CoretimePolkadot.test.ts | 20 +- .../sdk/src/nodes/supported/Crust.test.ts | 59 +++++ .../src/nodes/supported/CrustShadow.test.ts | 59 +++++ .../sdk/src/nodes/supported/Curio.test.ts | 50 ++++ .../sdk/src/nodes/supported/Darwinia.test.ts | 99 ++++++++ .../sdk/src/nodes/supported/Encointer.test.ts | 70 ++++++ .../sdk/src/nodes/supported/Hydration.test.ts | 38 +++ .../sdk/src/nodes/supported/Imbue.test.ts | 38 +++ .../src/nodes/supported/Integritee.test.ts | 51 ++++ .../sdk/src/nodes/supported/Interlay.test.ts | 50 ++++ .../src/nodes/supported/InvArchTinker.test.ts | 38 +++ .../sdk/src/nodes/supported/Khala.test.ts | 47 ++++ .../src/nodes/supported/KiltSpiritnet.test.ts | 52 ++++ .../sdk/src/nodes/supported/Kintsugi.test.ts | 50 ++++ .../sdk/src/nodes/supported/Litentry.test.ts | 38 +++ .../sdk/src/nodes/supported/Manta.test.ts | 38 +++ .../sdk/src/nodes/supported/Moonbeam.test.ts | 87 +++++++ .../sdk/src/nodes/supported/Moonriver.test.ts | 72 ++++++ .../sdk/src/nodes/supported/Mythos.test.ts | 77 ++++++ .../sdk/src/nodes/supported/NeuroWeb.test.ts | 38 +++ .../sdk/src/nodes/supported/Nodle.test.ts | 63 +++++ .../sdk/src/nodes/supported/Parallel.test.ts | 38 +++ packages/sdk/src/nodes/supported/Peaq.test.ts | 55 +++++ .../sdk/src/nodes/supported/Pendulum.test.ts | 64 +++++ .../sdk/src/nodes/supported/Phala.test.ts | 48 ++++ .../sdk/src/nodes/supported/Picasso.test.ts | 38 +++ .../sdk/src/nodes/supported/Pioneer.test.ts | 39 +++ .../sdk/src/nodes/supported/Polkadex.test.ts | 38 +++ .../sdk/src/nodes/supported/Quartz.test.ts | 40 +++ .../src/nodes/supported/Robonomics.test.ts | 10 +- .../sdk/src/nodes/supported/Subsocial.test.ts | 10 +- .../sdk/src/nodes/supported/Turing.test.ts | 38 +++ .../sdk/src/nodes/supported/Unique.test.ts | 38 +++ .../sdk/src/nodes/supported/Zeitgeist.test.ts | 51 ++++ .../balance/getBalanceForeignXTokens.test.ts | 143 +++++++++++ .../assets/getExistentialDeposit.test.ts | 55 +++++ packages/sdk/src/pallets/xcmPallet/utils.ts | 3 +- packages/sdk/src/utils.ts | 230 ------------------ .../utils/callPolkadotJsTxFunction.test.ts | 41 ++++ .../sdk/src/utils/callPolkadotJsTxFunction.ts | 7 + packages/sdk/src/utils/createApiInstance.ts | 6 + .../utils/createApiInstanceForNode.test.ts | 50 ++++ .../sdk/src/utils/createApiInstanceForNode.ts | 13 + .../sdk/src/utils/createX1Payload.test.ts | 17 ++ packages/sdk/src/utils/createX1Payload.ts | 4 + .../utils/determineRelayChainSymbol.test.ts | 34 +++ .../src/utils/determineRelayChainSymbol.ts | 12 + .../generateAddressMultiLocationV4.test.ts | 87 +++++++ .../utils/generateAddressMultiLocationV4.ts | 28 +++ .../src/utils/generateAddressPayload.test.ts | 214 ++++++++++++++++ .../sdk/src/utils/generateAddressPayload.ts | 99 ++++++++ .../sdk/src/utils/getAllNodeProviders.test.ts | 47 ++++ packages/sdk/src/utils/getAllNodeProviders.ts | 10 + packages/sdk/src/utils/getFees.test.ts | 21 ++ packages/sdk/src/utils/getFees.ts | 12 + packages/sdk/src/utils/getNode.test.ts | 14 ++ packages/sdk/src/utils/getNode.ts | 4 + .../getNodeEndpointOption.test.ts} | 19 +- .../sdk/src/utils/getNodeEndpointOption.ts | 14 ++ .../sdk/src/utils/getNodeProvider.test.ts | 37 +++ packages/sdk/src/utils/getNodeProvider.ts | 12 + packages/sdk/src/utils/index.ts | 32 +++ ...ultilocation.ts => verifyMultiLocation.ts} | 0 .../sdk/src/utils/verifyMultilocation.test.ts | 2 +- packages/sdk/vitest.config.ts | 2 +- packages/xcm-analyser/vitest.config.ts | 4 + .../src/dexNodes/Hydration/utils.test.ts | 17 +- 162 files changed, 5500 insertions(+), 736 deletions(-) create mode 100644 apps/playground/src/utils/replaceBigInt.ts create mode 100644 apps/visualizator-be/src/channels/channel.entity.test.ts create mode 100644 apps/visualizator-be/src/main.test.ts create mode 100644 apps/visualizator-be/src/messages/message.entity.test.ts create mode 100644 apps/visualizator-be/src/messages/messages.module.test.ts create mode 100644 apps/visualizator-be/src/messages/models/models.test.ts create mode 100644 apps/visualizator-be/src/utils/graphql.utils.test.ts create mode 100644 apps/visualizator-be/src/utils/graphql.utils.ts rename apps/xcm-api/src/analytics/{analytics.service.spec.ts => analytics.service.test.ts} (69%) delete mode 100644 apps/xcm-api/src/app.controller.spec.ts create mode 100644 apps/xcm-api/src/app.controller.test.ts create mode 100644 apps/xcm-api/src/asset-claim/asset-claim.controller.test.ts rename apps/xcm-api/src/assets/{assets.controller.spec.ts => assets.controller.test.ts} (64%) rename apps/xcm-api/src/assets/{assets.service.spec.ts => assets.service.test.ts} (97%) create mode 100644 apps/xcm-api/src/auth/utils/generateConfirmationEmailHtml.test.ts create mode 100644 apps/xcm-api/src/auth/utils/generateNewHigherLimitRequestHtml.test.ts create mode 100644 apps/xcm-api/src/auth/utils/utils.test.ts create mode 100644 apps/xcm-api/src/config/sentry.config.test.ts create mode 100644 apps/xcm-api/src/config/throttler.config.test.ts create mode 100644 apps/xcm-api/src/config/typeorm.config.test.ts create mode 100644 apps/xcm-api/src/main.test.ts rename apps/xcm-api/src/pallets/{pallets.controller.spec.ts => pallets.controller.test.ts} (84%) rename apps/xcm-api/src/pallets/{pallets.service.spec.ts => pallets.service.test.ts} (100%) delete mode 100644 apps/xcm-api/src/router/router.controller.spec.ts create mode 100644 apps/xcm-api/src/router/router.controller.test.ts rename apps/xcm-api/src/router/{router.service.spec.ts => router.service.test.ts} (75%) create mode 100644 apps/xcm-api/src/users/users.service.test.ts rename apps/xcm-api/src/{utils.spec.ts => utils.test.ts} (87%) create mode 100644 apps/xcm-api/src/utils/replaceBigInt.test.ts create mode 100644 apps/xcm-api/src/utils/replaceBigInt.ts create mode 100644 apps/xcm-api/src/utils/validateAmount.test.ts create mode 100644 apps/xcm-api/src/utils/validateAmount.ts create mode 100644 apps/xcm-api/src/utils/validateRecaptcha.test.ts create mode 100644 apps/xcm-api/src/utils/validateRecaptcha.ts create mode 100644 apps/xcm-api/src/x-transfer-eth/x-transfer-eth.service.test.ts rename apps/xcm-api/src/x-transfer/{x-transfer.controller.spec.ts => x-transfer.controller.test.ts} (83%) rename apps/xcm-api/src/x-transfer/{x-transfer.service.spec.ts => x-transfer.service.test.ts} (74%) create mode 100644 apps/xcm-api/src/xcm-analyser/xcm-analyser.controller.test.ts delete mode 100644 packages/sdk/src/errors/MissingApiPromiseError.ts create mode 100644 packages/sdk/src/index.test.ts create mode 100644 packages/sdk/src/nodes/supported/Acala.test.ts create mode 100644 packages/sdk/src/nodes/supported/Altair.test.ts create mode 100644 packages/sdk/src/nodes/supported/Amplitude.test.ts create mode 100644 packages/sdk/src/nodes/supported/Astar.test.ts create mode 100644 packages/sdk/src/nodes/supported/Bajun.test.ts create mode 100644 packages/sdk/src/nodes/supported/BifrostKusama.test.ts create mode 100644 packages/sdk/src/nodes/supported/BifrostPolkadot.test.ts create mode 100644 packages/sdk/src/nodes/supported/BridgeHubKusama.test.ts create mode 100644 packages/sdk/src/nodes/supported/BridgeHubPolkadot.test.ts create mode 100644 packages/sdk/src/nodes/supported/Calamari.test.ts create mode 100644 packages/sdk/src/nodes/supported/Centrifuge.test.ts create mode 100644 packages/sdk/src/nodes/supported/Collectives.test.ts create mode 100644 packages/sdk/src/nodes/supported/ComposableFinance.test.ts create mode 100644 packages/sdk/src/nodes/supported/CoretimeKusama.test.ts create mode 100644 packages/sdk/src/nodes/supported/Crust.test.ts create mode 100644 packages/sdk/src/nodes/supported/CrustShadow.test.ts create mode 100644 packages/sdk/src/nodes/supported/Curio.test.ts create mode 100644 packages/sdk/src/nodes/supported/Darwinia.test.ts create mode 100644 packages/sdk/src/nodes/supported/Encointer.test.ts create mode 100644 packages/sdk/src/nodes/supported/Hydration.test.ts create mode 100644 packages/sdk/src/nodes/supported/Imbue.test.ts create mode 100644 packages/sdk/src/nodes/supported/Integritee.test.ts create mode 100644 packages/sdk/src/nodes/supported/Interlay.test.ts create mode 100644 packages/sdk/src/nodes/supported/InvArchTinker.test.ts create mode 100644 packages/sdk/src/nodes/supported/Khala.test.ts create mode 100644 packages/sdk/src/nodes/supported/KiltSpiritnet.test.ts create mode 100644 packages/sdk/src/nodes/supported/Kintsugi.test.ts create mode 100644 packages/sdk/src/nodes/supported/Litentry.test.ts create mode 100644 packages/sdk/src/nodes/supported/Manta.test.ts create mode 100644 packages/sdk/src/nodes/supported/Moonbeam.test.ts create mode 100644 packages/sdk/src/nodes/supported/Moonriver.test.ts create mode 100644 packages/sdk/src/nodes/supported/Mythos.test.ts create mode 100644 packages/sdk/src/nodes/supported/NeuroWeb.test.ts create mode 100644 packages/sdk/src/nodes/supported/Nodle.test.ts create mode 100644 packages/sdk/src/nodes/supported/Parallel.test.ts create mode 100644 packages/sdk/src/nodes/supported/Peaq.test.ts create mode 100644 packages/sdk/src/nodes/supported/Pendulum.test.ts create mode 100644 packages/sdk/src/nodes/supported/Phala.test.ts create mode 100644 packages/sdk/src/nodes/supported/Picasso.test.ts create mode 100644 packages/sdk/src/nodes/supported/Pioneer.test.ts create mode 100644 packages/sdk/src/nodes/supported/Polkadex.test.ts create mode 100644 packages/sdk/src/nodes/supported/Quartz.test.ts create mode 100644 packages/sdk/src/nodes/supported/Turing.test.ts create mode 100644 packages/sdk/src/nodes/supported/Unique.test.ts create mode 100644 packages/sdk/src/nodes/supported/Zeitgeist.test.ts create mode 100644 packages/sdk/src/pallets/assets/getExistentialDeposit.test.ts delete mode 100644 packages/sdk/src/utils.ts create mode 100644 packages/sdk/src/utils/callPolkadotJsTxFunction.test.ts create mode 100644 packages/sdk/src/utils/callPolkadotJsTxFunction.ts create mode 100644 packages/sdk/src/utils/createApiInstance.ts create mode 100644 packages/sdk/src/utils/createApiInstanceForNode.test.ts create mode 100644 packages/sdk/src/utils/createApiInstanceForNode.ts create mode 100644 packages/sdk/src/utils/createX1Payload.test.ts create mode 100644 packages/sdk/src/utils/createX1Payload.ts create mode 100644 packages/sdk/src/utils/determineRelayChainSymbol.test.ts create mode 100644 packages/sdk/src/utils/determineRelayChainSymbol.ts create mode 100644 packages/sdk/src/utils/generateAddressMultiLocationV4.test.ts create mode 100644 packages/sdk/src/utils/generateAddressMultiLocationV4.ts create mode 100644 packages/sdk/src/utils/generateAddressPayload.test.ts create mode 100644 packages/sdk/src/utils/generateAddressPayload.ts create mode 100644 packages/sdk/src/utils/getAllNodeProviders.test.ts create mode 100644 packages/sdk/src/utils/getAllNodeProviders.ts create mode 100644 packages/sdk/src/utils/getFees.test.ts create mode 100644 packages/sdk/src/utils/getFees.ts create mode 100644 packages/sdk/src/utils/getNode.test.ts create mode 100644 packages/sdk/src/utils/getNode.ts rename packages/sdk/src/{utils.test.ts => utils/getNodeEndpointOption.test.ts} (57%) create mode 100644 packages/sdk/src/utils/getNodeEndpointOption.ts create mode 100644 packages/sdk/src/utils/getNodeProvider.test.ts create mode 100644 packages/sdk/src/utils/getNodeProvider.ts create mode 100644 packages/sdk/src/utils/index.ts rename packages/sdk/src/utils/{verifyMultilocation.ts => verifyMultiLocation.ts} (100%) diff --git a/apps/playground/src/components/TransferInfo.tsx b/apps/playground/src/components/TransferInfo.tsx index 2385af32..a74aaec7 100644 --- a/apps/playground/src/components/TransferInfo.tsx +++ b/apps/playground/src/components/TransferInfo.tsx @@ -7,6 +7,7 @@ import TransferInfoForm, { FormValues } from "./TransferInfoForm"; import OutputAlert from "./OutputAlert"; import { getTransferInfo } from "@paraspell/sdk"; import { fetchFromApi } from "../utils/submitUsingApi"; +import { replaceBigInt } from "../utils/replaceBigInt"; const TransferInfo = () => { const { selectedAccount } = useWallet(); @@ -79,7 +80,7 @@ const TransferInfo = () => { try { const output = await getQueryResult(formValues); - setOutput(JSON.stringify(output, null, 2)); + setOutput(JSON.stringify(output, replaceBigInt, 2)); openOutputAlert(); closeAlert(); } catch (e) { diff --git a/apps/playground/src/main.tsx b/apps/playground/src/main.tsx index a9728d65..328c62f5 100644 --- a/apps/playground/src/main.tsx +++ b/apps/playground/src/main.tsx @@ -3,12 +3,6 @@ import ReactDOM from "react-dom/client"; import App from "./App"; import WalletProvider from "./providers/WalletProvider"; -(BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function ( - this: bigint -) { - return this.toString(); -}; - ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/apps/playground/src/utils/replaceBigInt.ts b/apps/playground/src/utils/replaceBigInt.ts new file mode 100644 index 00000000..1de40dc3 --- /dev/null +++ b/apps/playground/src/utils/replaceBigInt.ts @@ -0,0 +1,2 @@ +export const replaceBigInt = (_key: string, value: unknown) => + typeof value === 'bigint' ? value.toString() : value; diff --git a/apps/visualizator-be/package.json b/apps/visualizator-be/package.json index c2304965..9b0c468d 100644 --- a/apps/visualizator-be/package.json +++ b/apps/visualizator-be/package.json @@ -72,6 +72,9 @@ "**/*.(t|j)s", "!**/*.resolver.ts" ], + "coveragePathIgnorePatterns": [ + ".module.ts" + ], "coverageDirectory": "../coverage", "testEnvironment": "node" } diff --git a/apps/visualizator-be/src/channels/channel.entity.test.ts b/apps/visualizator-be/src/channels/channel.entity.test.ts new file mode 100644 index 00000000..e7a2a1bd --- /dev/null +++ b/apps/visualizator-be/src/channels/channel.entity.test.ts @@ -0,0 +1,49 @@ +import { Channel } from './channel.entity'; + +describe('Channel Entity', () => { + it('should create a Channel entity with the correct fields', () => { + const channel = new Channel(); + channel.id = 1; + channel.sender = 101; + channel.recipient = 202; + channel.status = 'active'; + channel.transfer_count = 5; + channel.message_count = 15; + channel.active_at = Date.now(); + channel.proposed_max_capacity = 1000; + channel.proposed_max_message_size = 256; + + expect(channel.id).toBe(1); + expect(channel.sender).toBe(101); + expect(channel.recipient).toBe(202); + expect(channel.status).toBe('active'); + expect(channel.transfer_count).toBe(5); + expect(channel.message_count).toBe(15); + expect(channel.active_at).toBeGreaterThan(0); + expect(channel.proposed_max_capacity).toBe(1000); + expect(channel.proposed_max_message_size).toBe(256); + }); + + it('should handle nullable or optional fields correctly', () => { + const channel = new Channel(); + channel.id = 2; + channel.sender = 103; + channel.recipient = 204; + channel.status = 'pending'; + channel.transfer_count = 0; + channel.message_count = 0; + channel.active_at = 0; + channel.proposed_max_capacity = 500; + channel.proposed_max_message_size = 128; + + expect(channel.id).toBe(2); + expect(channel.sender).toBe(103); + expect(channel.recipient).toBe(204); + expect(channel.status).toBe('pending'); + expect(channel.transfer_count).toBe(0); + expect(channel.message_count).toBe(0); + expect(channel.active_at).toBe(0); + expect(channel.proposed_max_capacity).toBe(500); + expect(channel.proposed_max_message_size).toBe(128); + }); +}); diff --git a/apps/visualizator-be/src/channels/channel.entity.ts b/apps/visualizator-be/src/channels/channel.entity.ts index 50135939..c9ee007c 100644 --- a/apps/visualizator-be/src/channels/channel.entity.ts +++ b/apps/visualizator-be/src/channels/channel.entity.ts @@ -1,18 +1,19 @@ -import { ObjectType, Field, Int } from '@nestjs/graphql'; +import { ObjectType, Field } from '@nestjs/graphql'; +import { returnInt } from '../utils/graphql.utils'; import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @ObjectType() @Entity('channels') export class Channel { - @Field(() => Int) + @Field(returnInt) @PrimaryGeneratedColumn() id: number; - @Field(() => Int) + @Field(returnInt) @Column() sender: number; - @Field(() => Int) + @Field(returnInt) @Column() recipient: number; @@ -20,23 +21,23 @@ export class Channel { @Column() status: string; - @Field(() => Int) + @Field(returnInt) @Column() transfer_count: number; - @Field(() => Int) + @Field(returnInt) @Column() message_count: number; - @Field(() => Int) + @Field(returnInt) @Column('bigint') active_at: number; - @Field(() => Int) + @Field(returnInt) @Column() proposed_max_capacity: number; - @Field(() => Int) + @Field(returnInt) @Column() proposed_max_message_size: number; } diff --git a/apps/visualizator-be/src/main.test.ts b/apps/visualizator-be/src/main.test.ts new file mode 100644 index 00000000..3cfcea33 --- /dev/null +++ b/apps/visualizator-be/src/main.test.ts @@ -0,0 +1,30 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app/app.module'; + +jest.mock('@nestjs/core', () => ({ + NestFactory: { + create: jest.fn(), + }, +})); + +describe('Application Bootstrap', () => { + let mockApp: { listen: jest.Mock }; + + beforeAll(() => { + mockApp = { + listen: jest.fn(), + }; + + (NestFactory.create as jest.Mock).mockResolvedValue(mockApp); + }); + + it('should bootstrap the application and listen on the correct port', async () => { + const { bootstrap } = await import('./main'); + + await bootstrap(); + + expect(() => NestFactory.create(AppModule, { cors: true })).not.toThrow(); + + expect(mockApp.listen).toHaveBeenCalledWith(4201); + }); +}); diff --git a/apps/visualizator-be/src/main.ts b/apps/visualizator-be/src/main.ts index dbdb457d..14db8af6 100644 --- a/apps/visualizator-be/src/main.ts +++ b/apps/visualizator-be/src/main.ts @@ -1,9 +1,10 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app/app.module'; -async function bootstrap() { +export const bootstrap = async () => { const options = { cors: true }; const app = await NestFactory.create(AppModule, options); await app.listen(4201); -} +}; + void bootstrap(); diff --git a/apps/visualizator-be/src/messages/message.entity.test.ts b/apps/visualizator-be/src/messages/message.entity.test.ts new file mode 100644 index 00000000..79860213 --- /dev/null +++ b/apps/visualizator-be/src/messages/message.entity.test.ts @@ -0,0 +1,47 @@ +import { ID } from '@nestjs/graphql'; +import { Message, returnID, returnAssetArray, Asset } from './message.entity'; + +describe('Message Entity', () => { + it('should create a Message entity with the correct fields', () => { + const assets: Asset[] = [ + { + enum_key: 'key', + asset_module: 'module', + amount: '1000', + decimals: 6, + symbol: 'TOKEN', + }, + ]; + + const message = new Message(); + message.message_hash = 'hash'; + message.origin_event_index = 'event_1'; + message.from_account_id = 'account_1'; + message.origin_para_id = 1000; + message.origin_block_timestamp = Date.now(); + message.relayed_block_timestamp = Date.now(); + message.block_num = 1; + message.status = 'pending'; + message.relayed_event_index = 'event_2'; + message.dest_event_index = 'event_3'; + message.dest_para_id = 2000; + message.to_account_id = 'account_2'; + message.confirm_block_timestamp = Date.now(); + message.extrinsic_index = 'extrinsic_1'; + message.relayed_extrinsic_index = 'extrinsic_2'; + message.dest_extrinsic_index = 'extrinsic_3'; + message.child_para_id = 3000; + message.child_dest = 'child_dest'; + message.protocol = 'protocol_1'; + message.message_type = 'type_1'; + message.unique_id = 'unique_1'; + message.xcm_version = 2; + message.assets = assets; + + expect(message.message_hash).toBe('hash'); + expect(message.assets[0].symbol).toBe('TOKEN'); + expect(message.xcm_version).toBe(2); + expect(returnID()).toBe(ID); + expect(returnAssetArray()).toStrictEqual([Asset]); + }); +}); diff --git a/apps/visualizator-be/src/messages/message.entity.ts b/apps/visualizator-be/src/messages/message.entity.ts index ae8ee697..b48511cf 100644 --- a/apps/visualizator-be/src/messages/message.entity.ts +++ b/apps/visualizator-be/src/messages/message.entity.ts @@ -1,10 +1,14 @@ -import { ObjectType, Field, ID, Int } from '@nestjs/graphql'; +import { ObjectType, Field, ID } from '@nestjs/graphql'; +import { returnInt } from '../utils/graphql.utils'; import { Entity, Column, PrimaryColumn } from 'typeorm'; +export const returnID = () => ID; +export const returnAssetArray = () => [Asset]; + @ObjectType() @Entity('messages') export class Message { - @Field(() => ID) + @Field(returnID) @PrimaryColumn() message_hash: string; @@ -16,19 +20,19 @@ export class Message { @Column() from_account_id: string; - @Field(() => Int) + @Field(returnInt) @Column() origin_para_id: number; - @Field(() => Int) + @Field(returnInt) @Column('bigint') origin_block_timestamp: number; - @Field(() => Int) + @Field(returnInt) @Column('bigint') relayed_block_timestamp: number; - @Field(() => Int) + @Field(returnInt) @Column('bigint') block_num: number; @@ -44,7 +48,7 @@ export class Message { @Column() dest_event_index: string; - @Field(() => Int) + @Field(returnInt) @Column() dest_para_id: number; @@ -52,7 +56,7 @@ export class Message { @Column() to_account_id: string; - @Field(() => Int) + @Field(returnInt) @Column('bigint') confirm_block_timestamp: number; @@ -68,7 +72,7 @@ export class Message { @Column() dest_extrinsic_index: string; - @Field(() => Int) + @Field(returnInt) @Column() child_para_id: number; @@ -88,11 +92,11 @@ export class Message { @Column() unique_id: string; - @Field(() => Int) + @Field(returnInt) @Column() xcm_version: number; - @Field(() => [Asset]) + @Field(returnAssetArray) @Column('jsonb') assets: Asset[]; } @@ -108,7 +112,7 @@ export class Asset { @Field() amount: string; - @Field(() => Int) + @Field(returnInt) decimals: number; @Field() diff --git a/apps/visualizator-be/src/messages/messages.module.test.ts b/apps/visualizator-be/src/messages/messages.module.test.ts new file mode 100644 index 00000000..13186906 --- /dev/null +++ b/apps/visualizator-be/src/messages/messages.module.test.ts @@ -0,0 +1,44 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { MessageService } from './messages.service'; +import { MessageResolver } from './messages.resolver'; +import { Message } from './message.entity'; + +describe('MessageModule', () => { + let module: TestingModule; + let messageService: MessageService; + let messageResolver: MessageResolver; + + beforeAll(async () => { + const mockRepository = { + find: jest.fn(), + save: jest.fn(), + }; + + module = await Test.createTestingModule({ + providers: [ + MessageService, + MessageResolver, + { + provide: getRepositoryToken(Message), + useValue: mockRepository, + }, + ], + }).compile(); + + messageService = module.get(MessageService); + messageResolver = module.get(MessageResolver); + }); + + it('should compile the module', () => { + expect(module).toBeDefined(); + }); + + it('should provide MessageService', () => { + expect(messageService).toBeDefined(); + }); + + it('should provide MessageResolver', () => { + expect(messageResolver).toBeDefined(); + }); +}); diff --git a/apps/visualizator-be/src/messages/messages.service.test.ts b/apps/visualizator-be/src/messages/messages.service.test.ts index 738b140b..7dfb36a2 100644 --- a/apps/visualizator-be/src/messages/messages.service.test.ts +++ b/apps/visualizator-be/src/messages/messages.service.test.ts @@ -75,7 +75,7 @@ describe('MessageService', () => { ); const results = await service.countMessagesByStatus( - [], + undefined, startTime, endTime, ); @@ -367,12 +367,12 @@ describe('MessageService', () => { { id: 'account1', count: 6 }, { id: 'account2', count: 7 }, ]); - expect(mockRepository.query).toHaveBeenCalledWith(expect.any(String), [ - startTime, - endTime, - ...paraIds, - threshold, - ]); + + // Ensure that the WHERE clause includes paraIds + expect(mockRepository.query).toHaveBeenCalledWith( + expect.stringContaining('WHERE origin_block_timestamp BETWEEN'), + [startTime, endTime, ...paraIds, threshold], + ); }); it('should return account message counts when no paraIds are provided', async () => { @@ -387,11 +387,12 @@ describe('MessageService', () => { ); expect(results).toEqual([{ id: 'account3', count: 8 }]); - expect(mockRepository.query).toHaveBeenCalledWith(expect.any(String), [ - startTime, - endTime, - threshold, - ]); + + // Ensure that the WHERE clause does not include paraIds + expect(mockRepository.query).toHaveBeenCalledWith( + expect.not.stringContaining('origin_para_id IN'), + [startTime, endTime, threshold], + ); }); it('should handle exceptions', async () => { diff --git a/apps/visualizator-be/src/messages/messages.service.ts b/apps/visualizator-be/src/messages/messages.service.ts index 63fcf6b2..14a7f871 100644 --- a/apps/visualizator-be/src/messages/messages.service.ts +++ b/apps/visualizator-be/src/messages/messages.service.ts @@ -291,7 +291,7 @@ export class MessageService { const query = ` SELECT from_account_id, COUNT(*) as message_count FROM messages - ${whereConditions.length > 0 ? 'WHERE ' + whereConditions.join(' AND ') : ''} + WHERE ${whereConditions.join(' AND ')} GROUP BY from_account_id HAVING COUNT(*) > $${parameters.length + 1} ORDER BY message_count DESC; diff --git a/apps/visualizator-be/src/messages/models/account-msg-count.model.ts b/apps/visualizator-be/src/messages/models/account-msg-count.model.ts index 3ae51853..17fcdd23 100644 --- a/apps/visualizator-be/src/messages/models/account-msg-count.model.ts +++ b/apps/visualizator-be/src/messages/models/account-msg-count.model.ts @@ -1,10 +1,11 @@ -import { ObjectType, Field, Int } from '@nestjs/graphql'; +import { ObjectType, Field } from '@nestjs/graphql'; +import { returnInt } from '../../utils/graphql.utils'; @ObjectType() export class AccountXcmCountType { @Field() id: string; - @Field(() => Int) + @Field(returnInt) count: number; } diff --git a/apps/visualizator-be/src/messages/models/asset-count.model.ts b/apps/visualizator-be/src/messages/models/asset-count.model.ts index 96baf4e0..4292338f 100644 --- a/apps/visualizator-be/src/messages/models/asset-count.model.ts +++ b/apps/visualizator-be/src/messages/models/asset-count.model.ts @@ -1,13 +1,14 @@ -import { ObjectType, Field, Int } from '@nestjs/graphql'; +import { ObjectType, Field } from '@nestjs/graphql'; +import { returnInt } from '../../utils/graphql.utils'; @ObjectType() export class AssetCount { - @Field(() => Int, { nullable: true }) + @Field(returnInt, { nullable: true }) paraId?: number; @Field() symbol: string; - @Field(() => Int) + @Field(returnInt) count: number; } diff --git a/apps/visualizator-be/src/messages/models/message-count-by-day.model.ts b/apps/visualizator-be/src/messages/models/message-count-by-day.model.ts index b9ec404c..4170cbc1 100644 --- a/apps/visualizator-be/src/messages/models/message-count-by-day.model.ts +++ b/apps/visualizator-be/src/messages/models/message-count-by-day.model.ts @@ -1,8 +1,9 @@ -import { Field, Int, ObjectType } from '@nestjs/graphql'; +import { Field, ObjectType } from '@nestjs/graphql'; +import { returnInt } from '../../utils/graphql.utils'; @ObjectType() export class MessageCountByDay { - @Field(() => Int, { nullable: true }) + @Field(returnInt, { nullable: true }) paraId?: number; @Field() diff --git a/apps/visualizator-be/src/messages/models/message-count-by-status.model.ts b/apps/visualizator-be/src/messages/models/message-count-by-status.model.ts index 49d52828..b04f1d1b 100644 --- a/apps/visualizator-be/src/messages/models/message-count-by-status.model.ts +++ b/apps/visualizator-be/src/messages/models/message-count-by-status.model.ts @@ -1,13 +1,14 @@ -import { ObjectType, Field, Int } from '@nestjs/graphql'; +import { ObjectType, Field } from '@nestjs/graphql'; +import { returnInt } from '../../utils/graphql.utils'; @ObjectType() export class MessageCountByStatus { - @Field(() => Int, { nullable: true }) + @Field(returnInt, { nullable: true }) paraId?: number; - @Field(() => Int) + @Field(returnInt) success: number; - @Field(() => Int) + @Field(returnInt) failed: number; } diff --git a/apps/visualizator-be/src/messages/models/message-count.model.ts b/apps/visualizator-be/src/messages/models/message-count.model.ts index 18c1825a..7ba38c19 100644 --- a/apps/visualizator-be/src/messages/models/message-count.model.ts +++ b/apps/visualizator-be/src/messages/models/message-count.model.ts @@ -1,5 +1,6 @@ -import { ObjectType, Field, Int, registerEnumType } from '@nestjs/graphql'; +import { ObjectType, Field, registerEnumType } from '@nestjs/graphql'; import { CountOption } from '../count-option'; +import { returnInt } from '../../utils/graphql.utils'; registerEnumType(CountOption, { name: 'CountOption', @@ -8,9 +9,9 @@ registerEnumType(CountOption, { @ObjectType() export class MessageCount { - @Field(() => Int) + @Field(returnInt) paraId: number; - @Field(() => Int) + @Field(returnInt) totalCount: number; } diff --git a/apps/visualizator-be/src/messages/models/models.test.ts b/apps/visualizator-be/src/messages/models/models.test.ts new file mode 100644 index 00000000..3851f87e --- /dev/null +++ b/apps/visualizator-be/src/messages/models/models.test.ts @@ -0,0 +1,162 @@ +import { AccountXcmCountType } from './account-msg-count.model'; +import { AssetCount } from './asset-count.model'; +import { MessageCountByDay } from './message-count-by-day.model'; +import { MessageCountByStatus } from './message-count-by-status.model'; +import { MessageCount } from './message-count.model'; + +describe('AccountXcmCountType', () => { + it('should create an AccountXcmCountType object with the correct fields', () => { + const accountXcmCount = new AccountXcmCountType(); + accountXcmCount.id = 'account-1'; + accountXcmCount.count = 42; + + expect(accountXcmCount.id).toBe('account-1'); + expect(accountXcmCount.count).toBe(42); + }); + + it('should handle edge cases for fields', () => { + const accountXcmCount = new AccountXcmCountType(); + accountXcmCount.id = ''; + accountXcmCount.count = 0; + + expect(accountXcmCount.id).toBe(''); + expect(accountXcmCount.count).toBe(0); + }); +}); + +describe('AssetCount', () => { + it('should create an AssetCount object with the correct fields', () => { + const assetCount = new AssetCount(); + assetCount.paraId = 1000; + assetCount.symbol = 'DOT'; + assetCount.count = 42; + + expect(assetCount.paraId).toBe(1000); + expect(assetCount.symbol).toBe('DOT'); + expect(assetCount.count).toBe(42); + }); + + it('should handle nullable paraId correctly', () => { + const assetCount = new AssetCount(); + assetCount.paraId = undefined; + assetCount.symbol = 'KSM'; + assetCount.count = 10; + + expect(assetCount.paraId).toBeUndefined(); + expect(assetCount.symbol).toBe('KSM'); + expect(assetCount.count).toBe(10); + }); + + it('should handle edge cases for count and symbol', () => { + const assetCount = new AssetCount(); + assetCount.paraId = 0; + assetCount.symbol = ''; + assetCount.count = 0; + + expect(assetCount.paraId).toBe(0); + expect(assetCount.symbol).toBe(''); + expect(assetCount.count).toBe(0); + }); +}); + +describe('MessageCountByDay', () => { + it('should create a MessageCountByDay object with the correct fields', () => { + const messageCountByDay = new MessageCountByDay(); + messageCountByDay.paraId = 1000; + messageCountByDay.date = '2024-10-02'; + messageCountByDay.messageCount = 50; + messageCountByDay.messageCountSuccess = 45; + messageCountByDay.messageCountFailed = 5; + + expect(messageCountByDay.paraId).toBe(1000); + expect(messageCountByDay.date).toBe('2024-10-02'); + expect(messageCountByDay.messageCount).toBe(50); + expect(messageCountByDay.messageCountSuccess).toBe(45); + expect(messageCountByDay.messageCountFailed).toBe(5); + }); + + it('should handle nullable paraId field', () => { + const messageCountByDay = new MessageCountByDay(); + messageCountByDay.paraId = undefined; + messageCountByDay.date = '2024-10-02'; + messageCountByDay.messageCount = 20; + messageCountByDay.messageCountSuccess = 18; + messageCountByDay.messageCountFailed = 2; + + expect(messageCountByDay.paraId).toBeUndefined(); + expect(messageCountByDay.date).toBe('2024-10-02'); + expect(messageCountByDay.messageCount).toBe(20); + expect(messageCountByDay.messageCountSuccess).toBe(18); + expect(messageCountByDay.messageCountFailed).toBe(2); + }); + + it('should handle edge cases for counts', () => { + const messageCountByDay = new MessageCountByDay(); + messageCountByDay.paraId = 0; + messageCountByDay.date = '2024-10-02'; + messageCountByDay.messageCount = 0; + messageCountByDay.messageCountSuccess = 0; + messageCountByDay.messageCountFailed = 0; + + expect(messageCountByDay.paraId).toBe(0); + expect(messageCountByDay.date).toBe('2024-10-02'); + expect(messageCountByDay.messageCount).toBe(0); + expect(messageCountByDay.messageCountSuccess).toBe(0); + expect(messageCountByDay.messageCountFailed).toBe(0); + }); +}); + +describe('MessageCountByStatus', () => { + it('should create a MessageCountByStatus object with the correct fields', () => { + const messageCountByStatus = new MessageCountByStatus(); + messageCountByStatus.paraId = 1000; + messageCountByStatus.success = 30; + messageCountByStatus.failed = 5; + + expect(messageCountByStatus.paraId).toBe(1000); + expect(messageCountByStatus.success).toBe(30); + expect(messageCountByStatus.failed).toBe(5); + }); + + it('should handle nullable paraId field', () => { + const messageCountByStatus = new MessageCountByStatus(); + messageCountByStatus.paraId = undefined; + messageCountByStatus.success = 20; + messageCountByStatus.failed = 10; + + expect(messageCountByStatus.paraId).toBeUndefined(); + expect(messageCountByStatus.success).toBe(20); + expect(messageCountByStatus.failed).toBe(10); + }); + + it('should handle edge cases for success and failed fields', () => { + const messageCountByStatus = new MessageCountByStatus(); + messageCountByStatus.paraId = 0; + messageCountByStatus.success = 0; + messageCountByStatus.failed = 0; + + expect(messageCountByStatus.paraId).toBe(0); + expect(messageCountByStatus.success).toBe(0); + expect(messageCountByStatus.failed).toBe(0); + }); +}); + +describe('MessageCount', () => { + it('should create a MessageCount object with the correct fields', () => { + const messageCount = new MessageCount(); + messageCount.paraId = 1000; + messageCount.totalCount = 500; + + expect(messageCount.paraId).toBe(1000); + expect(messageCount.totalCount).toBe(500); + }); + + it('should handle edge cases for fields', () => { + const messageCount = new MessageCount(); + messageCount.paraId = 0; + messageCount.totalCount = 0; + + expect(messageCount.paraId).toBe(0); + expect(messageCount.totalCount).toBe(0); + }); +}); diff --git a/apps/visualizator-be/src/utils/graphql.utils.test.ts b/apps/visualizator-be/src/utils/graphql.utils.test.ts new file mode 100644 index 00000000..9d3bdf66 --- /dev/null +++ b/apps/visualizator-be/src/utils/graphql.utils.test.ts @@ -0,0 +1,9 @@ +import { Int } from '@nestjs/graphql'; +import { returnInt } from './graphql.utils'; + +describe('returnInt', () => { + it('should return Int type from GraphQL', () => { + const result = returnInt(); + expect(result).toBe(Int); + }); +}); diff --git a/apps/visualizator-be/src/utils/graphql.utils.ts b/apps/visualizator-be/src/utils/graphql.utils.ts new file mode 100644 index 00000000..a6da6b7a --- /dev/null +++ b/apps/visualizator-be/src/utils/graphql.utils.ts @@ -0,0 +1,3 @@ +import { Int } from '@nestjs/graphql'; + +export const returnInt = () => Int; diff --git a/apps/xcm-api/src/analytics/analytics.service.spec.ts b/apps/xcm-api/src/analytics/analytics.service.test.ts similarity index 69% rename from apps/xcm-api/src/analytics/analytics.service.spec.ts rename to apps/xcm-api/src/analytics/analytics.service.test.ts index b3dcca2e..5fc758c1 100644 --- a/apps/xcm-api/src/analytics/analytics.service.spec.ts +++ b/apps/xcm-api/src/analytics/analytics.service.test.ts @@ -3,7 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AnalyticsService } from './analytics.service.js'; import { ConfigService } from '@nestjs/config'; import { EventName } from './EventName.js'; -import { RequestWithUser } from 'src/types/types.js'; +import { RequestWithUser } from '../types/types.js'; import * as Mixpanel from 'mixpanel'; vi.mock('mixpanel', () => ({ @@ -62,9 +62,11 @@ describe('AnalyticsService', () => { const eventName = EventName.CLAIM_ASSETS; const properties = { additional: 'info' }; + const spy = vi.spyOn(mockMixpanel, 'track'); + service.track(eventName, req, properties); - expect(mockMixpanel.track).toHaveBeenCalledWith( + expect(spy).toHaveBeenCalledWith( eventName, expect.objectContaining({ ...properties, @@ -77,12 +79,40 @@ describe('AnalyticsService', () => { ); }); + it('tracks an event correctly without user', () => { + const req = { + headers: { + 'user-agent': 'Mozilla/5.0', + 'x-forwarded-for': '192.168.1.1', + }, + } as unknown as RequestWithUser; + const eventName = EventName.CLAIM_ASSETS; + const properties = { additional: 'info' }; + + const spy = vi.spyOn(mockMixpanel, 'track'); + + service.track(eventName, req, properties); + + expect(spy).toHaveBeenCalledWith( + eventName, + expect.objectContaining({ + ...properties, + ip: '192.168.1.1', + $browser: { name: 'Chrome', version: '93' }, + $device: { vendor: 'Apple', model: 'iPhone', type: 'mobile' }, + $os: { name: 'iOS', version: '14' }, + }), + ); + }); + it('should call Mixpanel people.set with correct parameters when client is initialized', () => { const userId = 'user123'; const properties = { email: 'user@example.com', age: 30 }; + const spy = vi.spyOn(mockMixpanel.people, 'set'); + service.identify(userId, properties); - expect(mockMixpanel.people.set).toHaveBeenCalledWith(userId, properties); + expect(spy).toHaveBeenCalledWith(userId, properties); }); }); diff --git a/apps/xcm-api/src/analytics/analytics.service.ts b/apps/xcm-api/src/analytics/analytics.service.ts index 6f36aa78..5d875a02 100644 --- a/apps/xcm-api/src/analytics/analytics.service.ts +++ b/apps/xcm-api/src/analytics/analytics.service.ts @@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config'; import * as Mixpanel from 'mixpanel'; import { EventName } from './EventName.js'; import UAParser from 'ua-parser-js'; -import { RequestWithUser } from 'src/types/types.js'; +import { RequestWithUser } from '../types/types.js'; @Injectable() export class AnalyticsService { diff --git a/apps/xcm-api/src/app.controller.spec.ts b/apps/xcm-api/src/app.controller.spec.ts deleted file mode 100644 index 17f86d5d..00000000 --- a/apps/xcm-api/src/app.controller.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, beforeEach, it, expect } from 'vitest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller.js'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return undefined because it redirects to XCM API Github repo"', () => { - expect(appController.root()).toBeUndefined(); - }); - }); -}); diff --git a/apps/xcm-api/src/app.controller.test.ts b/apps/xcm-api/src/app.controller.test.ts new file mode 100644 index 00000000..afc1fefc --- /dev/null +++ b/apps/xcm-api/src/app.controller.test.ts @@ -0,0 +1,36 @@ +import { describe, beforeEach, it, expect } from 'vitest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { AppController } from './app.controller.js'; + +describe('AppController', () => { + let appController: AppController; + + beforeEach(async () => { + const app: TestingModule = await Test.createTestingModule({ + controllers: [AppController], + }).compile(); + + appController = app.get(AppController); + }); + + describe('root', () => { + it('should return undefined because it redirects to LightSpell homepage', () => { + expect(appController.root()).toBeUndefined(); + }); + }); + + describe('sentryTest', () => { + it('should return a message when NODE_ENV is production', () => { + process.env.NODE_ENV = 'production'; + + const result = appController.sentryTest(); + expect(result).toBe('Sentry test is only available in development mode.'); + }); + + it('should throw an error when NODE_ENV is not production', () => { + process.env.NODE_ENV = 'development'; + + expect(() => appController.sentryTest()).toThrowError('Sentry test'); + }); + }); +}); diff --git a/apps/xcm-api/src/asset-claim/asset-claim.controller.test.ts b/apps/xcm-api/src/asset-claim/asset-claim.controller.test.ts new file mode 100644 index 00000000..ba6029ca --- /dev/null +++ b/apps/xcm-api/src/asset-claim/asset-claim.controller.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { AssetClaimController } from './asset-claim.controller.js'; +import { AssetClaimService } from './asset-claim.service.js'; +import { AnalyticsService } from '../analytics/analytics.service.js'; +import { EventName } from '../analytics/EventName.js'; +import { AssetClaimDto } from './dto/asset-claim.dto.js'; +import { RequestWithUser } from '../types/types.js'; +import { TTransferReturn } from '@paraspell/sdk'; + +describe('AssetClaimController', () => { + let controller: AssetClaimController; + let assetClaimService: AssetClaimService; + let analyticsService: AnalyticsService; + + beforeEach(() => { + assetClaimService = { + claimAssets: vi.fn(), + } as unknown as AssetClaimService; + + analyticsService = { + track: vi.fn(), + } as unknown as AnalyticsService; + + controller = new AssetClaimController(assetClaimService, analyticsService); + }); + + describe('claimAssets', () => { + it('should call trackAnalytics and claimAssets with correct parameters', async () => { + const bodyParams = { + from: 'address1', + fungible: [{ id: 'asset1', fun: 100 }], + } as AssetClaimDto; + + const req = { + headers: { + 'user-agent': 'Mozilla/5.0', + 'x-forwarded-for': '127.0.0.1', + }, + user: { id: '123', requestLimit: 100 }, + } as unknown as RequestWithUser; + + const spyClaimAssets = vi + .spyOn(assetClaimService, 'claimAssets') + .mockResolvedValue('success' as unknown as TTransferReturn); + const spyTrack = vi.spyOn(analyticsService, 'track'); + + const result = await controller.claimAssets(bodyParams, req); + + expect(spyClaimAssets).toHaveBeenCalledWith(bodyParams); + + expect(spyTrack).toHaveBeenCalledWith(EventName.CLAIM_ASSETS, req, { + from: 'address1', + assetLength: 1, + }); + + expect(result).toEqual('success'); + }); + }); + + describe('claimAssetsHash', () => { + it('should call trackAnalytics and claimAssets with hashEnabled set to true', async () => { + const bodyParams: AssetClaimDto = { + from: 'address1', + fungible: [{ id: 'asset1', fun: 100 }], + } as AssetClaimDto; + const req = { + headers: { + 'user-agent': 'Mozilla/5.0', + 'x-forwarded-for': '127.0.0.1', + }, + user: { id: '123', requestLimit: 100 }, + } as unknown as RequestWithUser; + + const spyClaimAssets = vi + .spyOn(assetClaimService, 'claimAssets') + .mockResolvedValue('success' as unknown as TTransferReturn); + const spyTrack = vi.spyOn(analyticsService, 'track'); + + const result = await controller.claimAssetsHash(bodyParams, req); + + expect(spyClaimAssets).toHaveBeenCalledWith(bodyParams, true); + + expect(spyTrack).toHaveBeenCalledWith(EventName.CLAIM_ASSETS_HASH, req, { + from: 'address1', + assetLength: 1, + }); + + expect(result).toEqual('success'); + }); + }); +}); diff --git a/apps/xcm-api/src/asset-claim/asset-claim.service.test.ts b/apps/xcm-api/src/asset-claim/asset-claim.service.test.ts index 916310fc..8c4bf4de 100644 --- a/apps/xcm-api/src/asset-claim/asset-claim.service.test.ts +++ b/apps/xcm-api/src/asset-claim/asset-claim.service.test.ts @@ -3,7 +3,10 @@ import { AssetClaimService } from './asset-claim.service.js'; import { Test, TestingModule } from '@nestjs/testing'; import * as sdk from '@paraspell/sdk'; import * as utils from '../utils.js'; -import { BadRequestException } from '@nestjs/common'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; import { ApiPromise } from '@polkadot/api'; import { AssetClaimDto } from './dto/asset-claim.dto.js'; @@ -47,8 +50,17 @@ describe('AssetClaimService', () => { await expect(service.claimAssets(dto)).rejects.toThrow(BadRequestException); }); + it('throws BadRequestException when fromNode is undefined', async () => { + const dto = { from: undefined, fungible: [], address: 'validAddress' }; + + await expect(service.claimAssets(dto)).rejects.toThrow( + new BadRequestException("You need to provide a 'from' parameter"), + ); + }); + it('throws BadRequestException if the wallet address is not valid', async () => { const dto = { from: 'Acala', fungible: [], address: 'invalidAddress' }; + sdk.NODES_WITH_RELAY_CHAINS.includes = vi.fn().mockReturnValue(true); vi.mocked(utils.isValidWalletAddress).mockReturnValue(false); await expect(service.claimAssets(dto)).rejects.toThrow(BadRequestException); @@ -109,4 +121,38 @@ describe('AssetClaimService', () => { expect(result).toEqual('hash'); expect(sdk.createApiInstanceForNode).toHaveBeenCalledWith('Acala'); }); + + it('throws BadRequestException when InvalidCurrencyError is thrown', async () => { + vi.mocked(sdk.Builder).mockImplementation(() => { + throw new sdk.InvalidCurrencyError('Invalid currency error'); + }); + + const dto = { from: 'Acala', fungible: [], address: 'validAddress' }; + sdk.NODES_WITH_RELAY_CHAINS.includes = vi.fn().mockReturnValue(true); + vi.mocked(utils.isValidWalletAddress).mockReturnValue(true); + vi.mocked(sdk.createApiInstanceForNode).mockResolvedValue({ + disconnect: vi.fn(), + } as unknown as ApiPromise); + + await expect(service.claimAssets(dto)).rejects.toThrow( + new BadRequestException('Invalid currency error'), + ); + }); + + it('throws InternalServerErrorException when a generic error is thrown', async () => { + vi.mocked(sdk.Builder).mockImplementation(() => { + throw new Error('Some internal error'); + }); + + const dto = { from: 'Acala', fungible: [], address: 'validAddress' }; + sdk.NODES_WITH_RELAY_CHAINS.includes = vi.fn().mockReturnValue(true); + vi.mocked(utils.isValidWalletAddress).mockReturnValue(true); + vi.mocked(sdk.createApiInstanceForNode).mockResolvedValue({ + disconnect: vi.fn(), + } as unknown as ApiPromise); + + await expect(service.claimAssets(dto)).rejects.toThrow( + new InternalServerErrorException('Some internal error'), + ); + }); }); diff --git a/apps/xcm-api/src/assets/assets.controller.spec.ts b/apps/xcm-api/src/assets/assets.controller.test.ts similarity index 64% rename from apps/xcm-api/src/assets/assets.controller.spec.ts rename to apps/xcm-api/src/assets/assets.controller.test.ts index e59d468c..ee4464fa 100644 --- a/apps/xcm-api/src/assets/assets.controller.spec.ts +++ b/apps/xcm-api/src/assets/assets.controller.test.ts @@ -2,7 +2,7 @@ import { vi, describe, beforeEach, it, expect } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import { AssetsController } from './assets.controller.js'; import { AssetsService } from './assets.service.js'; -import { TNode } from '@paraspell/sdk'; +import { NODE_NAMES, TNode, TNodeAssets } from '@paraspell/sdk'; import { AnalyticsService } from '../analytics/analytics.service.js'; import { mockRequestObject } from '../testUtils.js'; @@ -36,29 +36,15 @@ describe('AssetsController', () => { describe('getNodeNames', () => { it('should return the list of node names', () => { - const mockResult = ['Acala', 'Basilisk']; - vi.spyOn(assetsService, 'getNodeNames' as any).mockReturnValue( - mockResult, - ); + const mockResult = NODE_NAMES; + const spy = vi + .spyOn(assetsService, 'getNodeNames') + .mockReturnValue(mockResult); const result = controller.getNodeNames(mockRequestObject); expect(result).toBe(mockResult); - expect(assetsService.getNodeNames).toHaveBeenCalled(); - }); - }); - - describe('getNodeNames', () => { - it('should return the list of node names', () => { - const mockResult = [node, 'Basilisk']; - vi.spyOn(assetsService, 'getNodeNames' as any).mockReturnValue( - mockResult, - ); - - const result = controller.getNodeNames(mockRequestObject); - - expect(result).toBe(mockResult); - expect(assetsService.getNodeNames).toHaveBeenCalled(); + expect(spy).toHaveBeenCalled(); }); }); @@ -68,30 +54,29 @@ describe('AssetsController', () => { relayChainAssetSymbol: 'KSM', nativeAssets: [{ symbol, decimals }], otherAssets: [{ assetId: '234123123', symbol: 'FKK', decimals }], - }; + nativeAssetSymbol: symbol, + } as TNodeAssets; it('should return assets object for a valid node', () => { - vi.spyOn(assetsService, 'getAssetsObject' as any).mockReturnValue( - mockResult, - ); + const spy = vi + .spyOn(assetsService, 'getAssetsObject') + .mockReturnValue(mockResult); const result = controller.getAssetsObject(node, mockRequestObject); expect(result).toBe(mockResult); - expect(assetsService.getAssetsObject).toHaveBeenCalledWith(node); + expect(spy).toHaveBeenCalledWith(node); }); it('should return assets object for a valid parachain id', () => { const paraId = '2009'; - vi.spyOn(assetsService, 'getNodeByParaId' as any).mockReturnValue( - mockResult, - ); + const spy = vi + .spyOn(assetsService, 'getNodeByParaId') + .mockReturnValue(paraId); const result = controller.getAssetsObject(paraId, mockRequestObject); - expect(result).toBe(mockResult); - expect(assetsService.getNodeByParaId).toHaveBeenCalledWith( - Number(paraId), - ); + expect(result).toBe(paraId); + expect(spy).toHaveBeenCalledWith(Number(paraId)); }); }); @@ -99,71 +84,79 @@ describe('AssetsController', () => { it('should return asset ID for a valid node and symbol', () => { const symbol = 'DOT'; const mockResult = '1'; - vi.spyOn(assetsService, 'getAssetId').mockReturnValue(mockResult); + const spy = vi + .spyOn(assetsService, 'getAssetId') + .mockReturnValue(mockResult); const result = controller.getAssetId(node, { symbol }, mockRequestObject); expect(result).toBe(mockResult); - expect(assetsService.getAssetId).toHaveBeenCalledWith(node, symbol); + expect(spy).toHaveBeenCalledWith(node, symbol); }); }); describe('getRelayChainSymbol', () => { it('should return relay chain symbol for a valid node', () => { const mockResult = 'KSM'; - vi.spyOn(assetsService, 'getRelayChainSymbol').mockReturnValue( - mockResult, - ); + const spy = vi + .spyOn(assetsService, 'getRelayChainSymbol') + .mockReturnValue(mockResult); const result = controller.getRelayChainSymbol(node, mockRequestObject); expect(result).toBe(mockResult); - expect(assetsService.getRelayChainSymbol).toHaveBeenCalledWith(node); + expect(spy).toHaveBeenCalledWith(node); }); }); describe('getNativeAssets', () => { it('should return native assets for a valid node', () => { const mockResult = [{ symbol, decimals }]; - vi.spyOn(assetsService, 'getNativeAssets').mockReturnValue(mockResult); + const spy = vi + .spyOn(assetsService, 'getNativeAssets') + .mockReturnValue(mockResult); const result = controller.getNativeAssets(node, mockRequestObject); expect(result).toBe(mockResult); - expect(assetsService.getNativeAssets).toHaveBeenCalledWith(node); + expect(spy).toHaveBeenCalledWith(node); }); }); describe('getOtherAssets', () => { it('should return other assets for a valid node', () => { const mockResult = [{ assetId: '234123123', symbol: 'FKK', decimals }]; - vi.spyOn(assetsService, 'getOtherAssets').mockReturnValue(mockResult); + const spy = vi + .spyOn(assetsService, 'getOtherAssets') + .mockReturnValue(mockResult); const result = controller.getOtherAssets(node, mockRequestObject); expect(result).toBe(mockResult); - expect(assetsService.getOtherAssets).toHaveBeenCalledWith(node); + expect(spy).toHaveBeenCalledWith(node); }); }); describe('getAllAssetsSymbol', () => { it('should return all assets symbols for a valid node', () => { const mockResult = [symbol, 'DOT']; - vi.spyOn(assetsService, 'getAllAssetsSymbols').mockReturnValue( - mockResult, - ); + const spy = vi + .spyOn(assetsService, 'getAllAssetsSymbols') + .mockReturnValue(mockResult); const result = controller.getAllAssetsSymbol(node, mockRequestObject); expect(result).toBe(mockResult); - expect(assetsService.getAllAssetsSymbols).toHaveBeenCalledWith(node); + expect(spy).toHaveBeenCalledWith(node); }); }); describe('getDecimals', () => { it('should return decimals for a valid node and symbol', () => { const mockResult = 18; - vi.spyOn(assetsService, 'getDecimals').mockReturnValue(mockResult); + const spy = vi + .spyOn(assetsService, 'getDecimals') + .mockReturnValue(mockResult); const result = controller.getDecimals( node, @@ -172,14 +165,16 @@ describe('AssetsController', () => { ); expect(result).toBe(mockResult); - expect(assetsService.getDecimals).toHaveBeenCalledWith(node, symbol); + expect(spy).toHaveBeenCalledWith(node, symbol); }); }); describe('hasSupportForAsset', () => { it('should return true if asset is supported for a valid node and symbol', () => { const mockResult = true; - vi.spyOn(assetsService, 'hasSupportForAsset').mockReturnValue(mockResult); + const spy = vi + .spyOn(assetsService, 'hasSupportForAsset') + .mockReturnValue(mockResult); const result = controller.hasSupportForAsset( node, @@ -188,22 +183,21 @@ describe('AssetsController', () => { ); expect(result).toBe(mockResult); - expect(assetsService.hasSupportForAsset).toHaveBeenCalledWith( - node, - symbol, - ); + expect(spy).toHaveBeenCalledWith(node, symbol); }); }); describe('getParaId', () => { it('should return parachain id for a valid node', () => { const mockResult = 2009; - vi.spyOn(assetsService, 'getParaId').mockReturnValue(mockResult); + const spy = vi + .spyOn(assetsService, 'getParaId') + .mockReturnValue(mockResult); const result = controller.getParaId(node, mockRequestObject); expect(result).toBe(mockResult); - expect(assetsService.getParaId).toHaveBeenCalledWith(node); + expect(spy).toHaveBeenCalledWith(node); }); }); }); diff --git a/apps/xcm-api/src/assets/assets.service.spec.ts b/apps/xcm-api/src/assets/assets.service.test.ts similarity index 97% rename from apps/xcm-api/src/assets/assets.service.spec.ts rename to apps/xcm-api/src/assets/assets.service.test.ts index 61223ebe..f546f2bc 100644 --- a/apps/xcm-api/src/assets/assets.service.spec.ts +++ b/apps/xcm-api/src/assets/assets.service.test.ts @@ -104,7 +104,7 @@ describe('AssetsService', () => { describe('getAssetId', () => { let getAssetIdSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { getAssetIdSpy = vi.spyOn(paraspellSdk, 'getAssetId'); }); @@ -149,7 +149,7 @@ describe('AssetsService', () => { describe('getRelayChainSymbol', () => { let getRelayChainSymbolSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { getRelayChainSymbolSpy = vi.spyOn(paraspellSdk, 'getRelayChainSymbol'); }); @@ -186,7 +186,7 @@ describe('AssetsService', () => { describe('getNativeAssets', () => { let getNativeAssetsSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { getNativeAssetsSpy = vi.spyOn(paraspellSdk, 'getNativeAssets'); }); @@ -223,7 +223,7 @@ describe('AssetsService', () => { describe('getOtherAssets', () => { let getOtherAssetsSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { getOtherAssetsSpy = vi.spyOn(paraspellSdk, 'getOtherAssets'); }); @@ -260,7 +260,7 @@ describe('AssetsService', () => { describe('getAllAssetsSymbols', () => { let getAllAssetsSymbolsSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { getAllAssetsSymbolsSpy = vi.spyOn(paraspellSdk, 'getAllAssetsSymbols'); }); @@ -297,7 +297,7 @@ describe('AssetsService', () => { describe('getDecimals', () => { let getAssetDecimalsSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { getAssetDecimalsSpy = vi.spyOn(paraspellSdk, 'getAssetDecimals'); }); @@ -345,7 +345,7 @@ describe('AssetsService', () => { describe('hasSupportForAsset', () => { let hasSupportForAssetSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { hasSupportForAssetSpy = vi.spyOn(paraspellSdk, 'hasSupportForAsset'); }); @@ -390,7 +390,7 @@ describe('AssetsService', () => { describe('AssetsService', () => { let getParaIdSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { getParaIdSpy = vi.spyOn(paraspellSdk, 'getParaId'); }); @@ -425,7 +425,7 @@ describe('AssetsService', () => { describe('getNodeByParaId', () => { let getTNodeSpy: MockInstance; - beforeEach(async () => { + beforeEach(() => { getTNodeSpy = vi.spyOn(paraspellSdk, 'getTNode'); }); diff --git a/apps/xcm-api/src/auth/auth.guard.ts b/apps/xcm-api/src/auth/auth.guard.ts index b12c6d4e..44220844 100644 --- a/apps/xcm-api/src/auth/auth.guard.ts +++ b/apps/xcm-api/src/auth/auth.guard.ts @@ -7,7 +7,7 @@ import { import { JwtService } from '@nestjs/jwt'; import { UsersService } from '../users/users.service.js'; import { AnalyticsService } from '../analytics/analytics.service.js'; -import { RequestWithUser } from 'src/types/types.js'; +import { RequestWithUser } from '../types/types.js'; @Injectable() export class AuthGuard implements CanActivate { diff --git a/apps/xcm-api/src/auth/auth.service.ts b/apps/xcm-api/src/auth/auth.service.ts index 8e4c4a5b..4e7e1487 100644 --- a/apps/xcm-api/src/auth/auth.service.ts +++ b/apps/xcm-api/src/auth/auth.service.ts @@ -1,12 +1,12 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import { HigherRequestLimitDto } from './dto/HigherRequestLimitDto.js'; -import { validateRecaptcha } from '../utils.js'; import { sendEmail } from './utils/utils.js'; import { ConfigService } from '@nestjs/config'; import { UsersService } from '../users/users.service.js'; import { generateConfirmationEmailHtml } from './utils/generateConfirmationEmailHtml.js'; import { generateNewHigherLimitRequestHtml } from './utils/generateNewHigherLimitRequestHtml.js'; +import { validateRecaptcha } from '../utils/validateRecaptcha.js'; const sendEmails = async ( { email, reason, requestedLimit }: HigherRequestLimitDto, diff --git a/apps/xcm-api/src/auth/utils/generateConfirmationEmailHtml.test.ts b/apps/xcm-api/src/auth/utils/generateConfirmationEmailHtml.test.ts new file mode 100644 index 00000000..ec61be5a --- /dev/null +++ b/apps/xcm-api/src/auth/utils/generateConfirmationEmailHtml.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest'; +import { generateConfirmationEmailHtml } from './generateConfirmationEmailHtml.js'; + +describe('generateConfirmationEmailHtml', () => { + it('should generate the correct HTML content', () => { + const title = 'Confirmation Email'; + const reason = 'Increased API requests'; + const requestedLimit = '100 requests per minute'; + + const result = generateConfirmationEmailHtml(title, reason, requestedLimit); + + expect(result).toContain(title); + expect(result).toContain(reason); + expect(result).toContain(requestedLimit); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('Your request has been submitted successfully.'); + expect(result).toContain('Team LightSpell✨'); + }); + + it('should handle empty strings gracefully', () => { + const result = generateConfirmationEmailHtml('', '', ''); + + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('Your request has been submitted successfully.'); + }); +}); diff --git a/apps/xcm-api/src/auth/utils/generateNewHigherLimitRequestHtml.test.ts b/apps/xcm-api/src/auth/utils/generateNewHigherLimitRequestHtml.test.ts new file mode 100644 index 00000000..73edb9c6 --- /dev/null +++ b/apps/xcm-api/src/auth/utils/generateNewHigherLimitRequestHtml.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from 'vitest'; +import { generateNewHigherLimitRequestHtml } from './generateNewHigherLimitRequestHtml.js'; + +describe('generateNewHigherLimitRequestHtml', () => { + it('should generate the correct HTML content', () => { + const userEmail = 'testuser@example.com'; + const userId = '12345'; + const reason = 'Increase API usage'; + const requestedLimit = '500 requests per minute'; + + const result = generateNewHigherLimitRequestHtml( + userEmail, + userId, + reason, + requestedLimit, + ); + + expect(result).toContain(userEmail); + expect(result).toContain(userId); + expect(result).toContain(reason); + expect(result).toContain(requestedLimit); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('New higher limit request for submitted:'); + expect(result).toContain('Team LightSpell✨'); + }); + + it('should handle empty strings gracefully', () => { + const result = generateNewHigherLimitRequestHtml('', '', '', ''); + + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('New higher limit request for submitted:'); + }); +}); diff --git a/apps/xcm-api/src/auth/utils/utils.test.ts b/apps/xcm-api/src/auth/utils/utils.test.ts new file mode 100644 index 00000000..c9356790 --- /dev/null +++ b/apps/xcm-api/src/auth/utils/utils.test.ts @@ -0,0 +1,113 @@ +import { describe, it, expect, vi } from 'vitest'; +import { ConfigService } from '@nestjs/config'; +import * as nodemailer from 'nodemailer'; +import { google } from 'googleapis'; +import { sendEmail } from './utils.js'; + +vi.mock('nodemailer'); +vi.mock('googleapis', () => ({ + google: { + auth: { + OAuth2: vi.fn().mockImplementation(() => ({ + setCredentials: vi.fn(), + getAccessToken: vi.fn().mockResolvedValue({ token: 'mockAccessToken' }), + })), + }, + }, +})); + +describe('sendEmail', () => { + const mockConfigService = { + get: vi.fn(), + } as unknown as ConfigService; + + const mockTransporter = { + sendMail: vi.fn((_mailOptions, callback: (err: Error | null) => void) => { + callback(null); + }), + }; + + vi.mocked(nodemailer.createTransport).mockReturnValue( + mockTransporter as unknown as ReturnType, + ); + + it('should send an email successfully', async () => { + mockConfigService.get = vi.fn().mockImplementation((key: string) => { + switch (key) { + case 'EMAIL_ADDRESS_SENDER': + return 'sender@example.com'; + case 'EMAIL_CLIENT_ID': + return 'mockClientId'; + case 'EMAIL_CLIENT_SECRET': + return 'mockClientSecret'; + case 'EMAIL_REDIRECT_URI': + return 'mockRedirectUri'; + case 'EMAIL_REFRESH_TOKEN': + return 'mockRefreshToken'; + default: + return null; + } + }); + + await sendEmail( + 'Test Subject', + '

This is a test email

', + 'recipient@example.com', + mockConfigService, + ); + + expect(google.auth.OAuth2).toHaveBeenCalledWith( + 'mockClientId', + 'mockClientSecret', + 'mockRedirectUri', + ); + + expect(nodemailer.createTransport).toHaveBeenCalledWith({ + service: 'Gmail', + auth: { + type: 'OAuth2', + user: 'sender@example.com', + clientId: 'mockClientId', + clientSecret: 'mockClientSecret', + refreshToken: 'mockRefreshToken', + accessToken: 'mockAccessToken', + }, + }); + + expect(mockTransporter.sendMail).toHaveBeenCalledWith( + { + from: 'sender@example.com', + to: 'recipient@example.com', + subject: 'Test Subject', + html: '

This is a test email

', + }, + expect.any(Function), + ); + }); + + it('should log an error if sendMail fails', async () => { + mockTransporter.sendMail.mockImplementationOnce( + (_mailOptions, callback) => { + callback(new Error('SendMail Error')); + }, + ); + + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await sendEmail( + 'Test Subject', + '

This is a test email

', + 'recipient@example.com', + mockConfigService, + ); + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Error sending email:', + new Error('SendMail Error'), + ); + + consoleErrorSpy.mockRestore(); + }); +}); diff --git a/apps/xcm-api/src/config/sentry.config.test.ts b/apps/xcm-api/src/config/sentry.config.test.ts new file mode 100644 index 00000000..b83e5b27 --- /dev/null +++ b/apps/xcm-api/src/config/sentry.config.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, vi } from 'vitest'; +import { ConfigService } from '@nestjs/config'; +import { SentryModuleOptions } from '@ntegral/nestjs-sentry'; +import { sentryConfig } from './sentry.config.js'; + +describe('sentryConfig', () => { + const mockConfigService = { + get: vi.fn(), + } as unknown as ConfigService; + + it('should return the correct SentryModuleOptions with provided DSN', () => { + mockConfigService.get = vi.fn().mockReturnValue('https://exampleSentryDsn'); + + const spy = vi.spyOn(mockConfigService, 'get'); + + const result: SentryModuleOptions = sentryConfig(mockConfigService); + + expect(result).toEqual({ + dsn: 'https://exampleSentryDsn', + environment: 'test', + }); + expect(spy).toHaveBeenCalledWith('SENTRY_DSN'); + }); + + it('should return the correct environment when NODE_ENV is set', () => { + process.env.NODE_ENV = 'production'; + mockConfigService.get = vi.fn().mockReturnValue('https://exampleSentryDsn'); + + const result: SentryModuleOptions = sentryConfig(mockConfigService); + + expect(result).toEqual({ + dsn: 'https://exampleSentryDsn', + environment: 'production', + }); + }); + + it('should default to development when NODE_ENV is not set', () => { + delete process.env.NODE_ENV; + mockConfigService.get = vi.fn().mockReturnValue('https://exampleSentryDsn'); + + const result: SentryModuleOptions = sentryConfig(mockConfigService); + + expect(result).toEqual({ + dsn: 'https://exampleSentryDsn', + environment: 'development', // environment defaults to development + }); + }); +}); diff --git a/apps/xcm-api/src/config/throttler.config.test.ts b/apps/xcm-api/src/config/throttler.config.test.ts new file mode 100644 index 00000000..00716e24 --- /dev/null +++ b/apps/xcm-api/src/config/throttler.config.test.ts @@ -0,0 +1,100 @@ +import { describe, it, expect, vi } from 'vitest'; +import { ThrottlerModuleOptions, ThrottlerOptions } from '@nestjs/throttler'; +import { ConfigService } from '@nestjs/config'; +import { ExecutionContext } from '@nestjs/common'; +import { RequestWithUser } from '../types/types.js'; +import { throttlerConfig } from './throttler.config.js'; + +describe('throttlerConfig', () => { + const mockConfigService = { + get: vi.fn(), + } as unknown as ConfigService; + + const mockHttpContext = { + switchToHttp: vi.fn().mockReturnThis(), + getRequest: vi.fn(), + }; + + const mockExecutionContext = { + switchToHttp: vi.fn(() => mockHttpContext), + } as unknown as ExecutionContext; + + const getThrottlers = ( + result: ThrottlerModuleOptions, + ): ThrottlerOptions[] => { + if (Array.isArray(result)) { + return result; + } + return result.throttlers; + }; + + it('should return 0 ttl when NODE_ENV is "test"', () => { + process.env.NODE_ENV = 'test'; + const result: ThrottlerModuleOptions = throttlerConfig(mockConfigService); + + const throttlers = getThrottlers(result); + expect(throttlers[0].ttl).toBe(0); + }); + + it('should return ttl from config when NODE_ENV is not "test"', () => { + process.env.NODE_ENV = 'production'; + + const spy = vi.spyOn(mockConfigService, 'get').mockReturnValue(60); + + const result: ThrottlerModuleOptions = throttlerConfig(mockConfigService); + + const throttlers = getThrottlers(result); + + expect(throttlers[0].ttl).toBe(60); + expect(spy).toHaveBeenCalledWith('RATE_LIMIT_TTL_SEC'); + }); + + it('should return request limit from user-specific requestLimit when present', () => { + mockConfigService.get = vi.fn(); + mockHttpContext.getRequest = vi.fn().mockReturnValue({ + user: { requestLimit: 100 }, + } as RequestWithUser); + + const result: ThrottlerModuleOptions = throttlerConfig(mockConfigService); + const throttlers = getThrottlers(result); + const limit = ( + throttlers[0].limit as (context: ExecutionContext) => number + )(mockExecutionContext); + + expect(limit).toBe(100); + }); + + it('should return authenticated request limit from config when user is authenticated and requestLimit is not set', () => { + const spy = vi.spyOn(mockConfigService, 'get').mockReturnValue(50); + + mockHttpContext.getRequest = vi.fn().mockReturnValue({ + user: {}, // User without a specific requestLimit + } as RequestWithUser); + + const result: ThrottlerModuleOptions = throttlerConfig(mockConfigService); + const throttlers = getThrottlers(result); + const limit = ( + throttlers[0].limit as (context: ExecutionContext) => number + )(mockExecutionContext); + + expect(limit).toBe(50); + expect(spy).toHaveBeenCalledWith('RATE_LIMIT_REQ_COUNT_AUTH'); + }); + + it('should return public request limit from config when user is not authenticated', () => { + const spy = vi.spyOn(mockConfigService, 'get').mockReturnValue(20); + + mockHttpContext.getRequest = vi.fn().mockReturnValue({ + user: undefined, // No user, public request + } as RequestWithUser); + + const result: ThrottlerModuleOptions = throttlerConfig(mockConfigService); + const throttlers = getThrottlers(result); + const limit = ( + throttlers[0].limit as (context: ExecutionContext) => number + )(mockExecutionContext); + + expect(limit).toBe(20); + expect(spy).toHaveBeenCalledWith('RATE_LIMIT_REQ_COUNT_PUBLIC'); + }); +}); diff --git a/apps/xcm-api/src/config/throttler.config.ts b/apps/xcm-api/src/config/throttler.config.ts index 8f7eda48..98aa7565 100644 --- a/apps/xcm-api/src/config/throttler.config.ts +++ b/apps/xcm-api/src/config/throttler.config.ts @@ -1,6 +1,6 @@ import { ThrottlerModuleOptions } from '@nestjs/throttler'; import { ConfigService } from '@nestjs/config'; -import { RequestWithUser } from 'src/types/types.js'; +import { RequestWithUser } from '../types/types.js'; export const throttlerConfig = ( config: ConfigService, diff --git a/apps/xcm-api/src/config/typeorm.config.test.ts b/apps/xcm-api/src/config/typeorm.config.test.ts new file mode 100644 index 00000000..6e95cbe4 --- /dev/null +++ b/apps/xcm-api/src/config/typeorm.config.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, vi } from 'vitest'; +import { ConfigService } from '@nestjs/config'; +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { typeOrmConfig } from './typeorm.config.js'; +import { User } from '../users/user.entity.js'; + +describe('typeOrmConfig', () => { + const mockConfigService = { + get: vi.fn(), + } as unknown as ConfigService; + + it('should return correct TypeOrmModuleOptions with the provided configuration', () => { + const spy = vi + .spyOn(mockConfigService, 'get') + .mockImplementation((key: string) => { + switch (key) { + case 'DB_HOST': + return 'localhost'; + case 'DB_PORT': + return 5432; + case 'DB_USER': + return 'testuser'; + case 'DB_PASS': + return 'testpassword'; + case 'DB_NAME': + return 'testdb'; + default: + return null; + } + }); + + const result: TypeOrmModuleOptions = typeOrmConfig(mockConfigService); + + expect(result).toEqual({ + type: 'postgres', + host: 'localhost', + port: 5432, + username: 'testuser', + password: 'testpassword', + database: 'testdb', + entities: [User], + synchronize: true, + }); + + expect(spy).toHaveBeenCalledWith('DB_HOST'); + expect(spy).toHaveBeenCalledWith('DB_PORT'); + expect(spy).toHaveBeenCalledWith('DB_USER'); + expect(spy).toHaveBeenCalledWith('DB_PASS'); + expect(spy).toHaveBeenCalledWith('DB_NAME'); + }); +}); diff --git a/apps/xcm-api/src/main.test.ts b/apps/xcm-api/src/main.test.ts new file mode 100644 index 00000000..64128ac5 --- /dev/null +++ b/apps/xcm-api/src/main.test.ts @@ -0,0 +1,49 @@ +import { describe, it, vi, expect, beforeAll, Mock } from 'vitest'; +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module.js'; +import { ValidationPipe } from '@nestjs/common'; + +vi.mock('@nestjs/core', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + NestFactory: { + create: vi.fn(), + }, + }; +}); + +describe('Application Bootstrap', () => { + let mockApp: { + enableCors: Mock; + useGlobalPipes: Mock; + listen: Mock; + getHttpAdapter: () => { getInstance: () => { set: Mock } }; + }; + + beforeAll(() => { + mockApp = { + enableCors: vi.fn(), + useGlobalPipes: vi.fn(), + listen: vi.fn(), + getHttpAdapter: () => ({ getInstance: () => ({ set: vi.fn() }) }), + }; + + (NestFactory.create as Mock).mockResolvedValue(mockApp); + }); + + it('should bootstrap the application and listen on the correct port', async () => { + const { bootstrap } = await import('./main.js'); + + await bootstrap(); + + expect(() => NestFactory.create(AppModule, { cors: true })).not.toThrow(); + + expect(mockApp.enableCors).toHaveBeenCalled(); + expect(mockApp.useGlobalPipes).toHaveBeenCalledWith( + expect.any(ValidationPipe), + ); + + expect(mockApp.listen).toHaveBeenCalledWith(process.env.PORT || 3001); + }); +}); diff --git a/apps/xcm-api/src/main.ts b/apps/xcm-api/src/main.ts index 36e4401f..5f062591 100644 --- a/apps/xcm-api/src/main.ts +++ b/apps/xcm-api/src/main.ts @@ -1,18 +1,19 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module.js'; import { ValidationPipe } from '@nestjs/common'; +import { ExpressAdapter } from '@nestjs/platform-express'; +import { replaceBigInt } from './utils/replaceBigInt.js'; -(BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function ( - this: bigint, -) { - return this.toString(); -}; - -async function bootstrap() { +export const bootstrap = async () => { const app = await NestFactory.create(AppModule); + (app.getHttpAdapter().getInstance() as ExpressAdapter).set( + 'json replacer', + replaceBigInt, + ); app.enableCors(); app.useGlobalPipes(new ValidationPipe()); const DEFAULT_PORT = 3001; await app.listen(process.env.PORT || DEFAULT_PORT); -} +}; + void bootstrap(); diff --git a/apps/xcm-api/src/pallets/pallets.controller.spec.ts b/apps/xcm-api/src/pallets/pallets.controller.test.ts similarity index 84% rename from apps/xcm-api/src/pallets/pallets.controller.spec.ts rename to apps/xcm-api/src/pallets/pallets.controller.test.ts index 5c05345d..863ec29f 100644 --- a/apps/xcm-api/src/pallets/pallets.controller.spec.ts +++ b/apps/xcm-api/src/pallets/pallets.controller.test.ts @@ -35,26 +35,28 @@ describe('PalletsController', () => { describe('getDefaultPallet', () => { it('should return the default pallet for the given node', async () => { const defaultPallet: TPallet = 'OrmlXTokens'; - vi.spyOn(palletsService, 'getDefaultPallet' as any).mockResolvedValue( - defaultPallet, - ); + const spy = vi + .spyOn(palletsService, 'getDefaultPallet') + .mockResolvedValue(defaultPallet); const result = await controller.getDefaultPallet(node, mockRequestObject); expect(result).toBe(defaultPallet); - expect(palletsService.getDefaultPallet).toHaveBeenCalledWith(node); + expect(spy).toHaveBeenCalledWith(node); }); }); describe('getPallets', () => { it('should return the list of pallets for the given node', async () => { const pallets: TPallet[] = ['OrmlXTokens', 'PolkadotXcm']; - vi.spyOn(palletsService, 'getPallets' as any).mockResolvedValue(pallets); + const spy = vi + .spyOn(palletsService, 'getPallets') + .mockResolvedValue(pallets); const result = await controller.getPallets(node, mockRequestObject); expect(result).toBe(pallets); - expect(palletsService.getPallets).toHaveBeenCalledWith(node); + expect(spy).toHaveBeenCalledWith(node); }); }); }); diff --git a/apps/xcm-api/src/pallets/pallets.controller.ts b/apps/xcm-api/src/pallets/pallets.controller.ts index 20f68031..35afacef 100644 --- a/apps/xcm-api/src/pallets/pallets.controller.ts +++ b/apps/xcm-api/src/pallets/pallets.controller.ts @@ -13,12 +13,12 @@ export class PalletsController { @Get(':node/default') getDefaultPallet(@Param('node') node: string, @Req() req: Request) { this.analyticsService.track(EventName.GET_DEFAULT_PALLET, req, { node }); - return this.palletsService.getDefaultPallet(node); + return Promise.resolve(this.palletsService.getDefaultPallet(node)); } @Get(':node') getPallets(@Param('node') node: string, @Req() req: Request) { this.analyticsService.track(EventName.GET_SUPPORTED_PALLETS, req, { node }); - return this.palletsService.getPallets(node); + return Promise.resolve(this.palletsService.getPallets(node)); } } diff --git a/apps/xcm-api/src/pallets/pallets.service.spec.ts b/apps/xcm-api/src/pallets/pallets.service.test.ts similarity index 100% rename from apps/xcm-api/src/pallets/pallets.service.spec.ts rename to apps/xcm-api/src/pallets/pallets.service.test.ts diff --git a/apps/xcm-api/src/router/dto/RouterDto.ts b/apps/xcm-api/src/router/dto/RouterDto.ts index 1cafb047..7822da73 100644 --- a/apps/xcm-api/src/router/dto/RouterDto.ts +++ b/apps/xcm-api/src/router/dto/RouterDto.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import { CurrencyCoreSchema } from '../../x-transfer/dto/XTransferDto.js'; import { TCurrencyCore } from '@paraspell/sdk'; import { TransactionType } from '@paraspell/xcm-router'; +import { validateAmount } from '../../utils/validateAmount.js'; export const RouterDtoSchema = z.object({ from: z.string(), @@ -24,15 +25,9 @@ export const RouterDtoSchema = z.object({ .min(1, 'Asset hub address is required') .optional(), amount: z.union([ - z.string().refine( - (val) => { - const num = parseFloat(val); - return !isNaN(num) && num > 0; - }, - { - message: 'Amount must be a positive number', - }, - ), + z.string().refine(validateAmount, { + message: 'Amount must be a positive number', + }), z.number().positive({ message: 'Amount must be a positive number' }), ]), slippagePct: z.string().optional(), diff --git a/apps/xcm-api/src/router/router.controller.spec.ts b/apps/xcm-api/src/router/router.controller.spec.ts deleted file mode 100644 index 4059380e..00000000 --- a/apps/xcm-api/src/router/router.controller.spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { vi, describe, beforeEach, it, expect } from 'vitest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { RouterController } from './router.controller.js'; -import { RouterService } from './router.service.js'; -import { RouterDto } from './dto/RouterDto.js'; -import { AnalyticsService } from '../analytics/analytics.service.js'; - -// Integration tests to ensure controller and service are working together -describe('RouterController', () => { - let controller: RouterController; - let service: RouterService; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [RouterController], - providers: [ - RouterService, - { - provide: AnalyticsService, - useValue: { get: () => '', track: vi.fn() }, - }, - ], - }).compile(); - - controller = module.get(RouterController); - service = module.get(RouterService); - }); - - it('should be defined', () => { - expect(controller).toBeDefined(); - }); - - describe('generateXcmCall', () => { - it('should call generateExtrinsics service method with correct parameters and return result', async () => { - const queryParams: RouterDto = { - from: 'Astar', - exchange: 'AcalaDex', - to: 'Moonbeam', - currencyFrom: 'ASTR', - currencyTo: 'GLMR', - amount: '1000000000000000000', - injectorAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', - recipientAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', - }; - - const mockResult = 'serialized-extrinsics'; - - vi.spyOn(service, 'generateExtrinsics').mockResolvedValue( - mockResult as any, - ); - - const result = await controller.generateExtrinsics( - queryParams, - {} as any, - ); - - expect(result).toBe(mockResult); - expect(service.generateExtrinsics).toHaveBeenCalledWith(queryParams); - }); - }); -}); diff --git a/apps/xcm-api/src/router/router.controller.test.ts b/apps/xcm-api/src/router/router.controller.test.ts new file mode 100644 index 00000000..9fb64ecb --- /dev/null +++ b/apps/xcm-api/src/router/router.controller.test.ts @@ -0,0 +1,115 @@ +import { vi, describe, beforeEach, it, expect } from 'vitest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { RouterController } from './router.controller.js'; +import { RouterService } from './router.service.js'; +import { PatchedRouterDto } from './dto/RouterDto.js'; +import { AnalyticsService } from '../analytics/analytics.service.js'; + +// Integration tests to ensure controller and service are working together +describe('RouterController', () => { + let controller: RouterController; + let service: RouterService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [RouterController], + providers: [ + RouterService, + { + provide: AnalyticsService, + useValue: { get: () => '', track: vi.fn() }, + }, + ], + }).compile(); + + controller = module.get(RouterController); + service = module.get(RouterService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('generateXcmCall', () => { + it('should call generateExtrinsics service method with correct parameters and return result', async () => { + const queryParams: PatchedRouterDto = { + from: 'Astar', + exchange: 'AcalaDex', + to: 'Moonbeam', + currencyFrom: { symbol: 'ASTR' }, + currencyTo: { symbol: 'GLMR' }, + amount: '1000000000000000000', + injectorAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + recipientAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + }; + + const mockResult: Awaited> = + []; + const spy = vi + .spyOn(service, 'generateExtrinsics') + .mockResolvedValue(mockResult); + + const result = await controller.generateExtrinsics( + queryParams, + {} as unknown as Request, + ); + + expect(result).toBe(mockResult); + expect(spy).toHaveBeenCalledWith(queryParams); + }); + + it('should call generateExtrinsics service method with correct parameters and return result - hash', async () => { + const queryParams: PatchedRouterDto = { + from: 'Astar', + exchange: 'AcalaDex', + to: 'Moonbeam', + currencyFrom: { symbol: 'ASTR' }, + currencyTo: { symbol: 'GLMR' }, + amount: '1000000000000000000', + injectorAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + recipientAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + }; + + const mockResult: Awaited> = + []; + const spy = vi + .spyOn(service, 'generateExtrinsics') + .mockResolvedValue(mockResult); + + const result = await controller.generateExtrinsicsV2Hash( + queryParams, + {} as unknown as Request, + ); + + expect(result).toBe(mockResult); + expect(spy).toHaveBeenCalledWith(queryParams, true); + }); + + it('should call generateExtrinsics service method with correct parameters and return result - V2 POST', async () => { + const queryParams: PatchedRouterDto = { + from: 'Astar', + exchange: 'AcalaDex', + to: 'Moonbeam', + currencyFrom: { symbol: 'ASTR' }, + currencyTo: { symbol: 'GLMR' }, + amount: '1000000000000000000', + injectorAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + recipientAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', + }; + + const mockResult: Awaited> = + []; + const spy = vi + .spyOn(service, 'generateExtrinsics') + .mockResolvedValue(mockResult); + + const result = await controller.generateExtrinsicsV2( + queryParams, + {} as unknown as Request, + ); + + expect(result).toBe(mockResult); + expect(spy).toHaveBeenCalledWith(queryParams); + }); + }); +}); diff --git a/apps/xcm-api/src/router/router.service.spec.ts b/apps/xcm-api/src/router/router.service.test.ts similarity index 75% rename from apps/xcm-api/src/router/router.service.spec.ts rename to apps/xcm-api/src/router/router.service.test.ts index 67c0461c..666d372c 100644 --- a/apps/xcm-api/src/router/router.service.spec.ts +++ b/apps/xcm-api/src/router/router.service.test.ts @@ -2,13 +2,18 @@ import { Test, TestingModule } from '@nestjs/testing'; import { RouterService } from './router.service.js'; import * as utils from '../utils.js'; import * as spellRouter from '@paraspell/xcm-router'; -import { RouterDto } from './dto/RouterDto.js'; +import { PatchedRouterDto } from './dto/RouterDto.js'; import { BadRequestException, InternalServerErrorException, } from '@nestjs/common'; -import { vi, describe, beforeEach, it, expect } from 'vitest'; -import { InvalidCurrencyError } from '@paraspell/sdk'; +import { vi, describe, beforeEach, it, expect, MockInstance } from 'vitest'; +import { + Extrinsic, + InvalidCurrencyError, + TNode, + TSerializedApiCall, +} from '@paraspell/sdk'; vi.mock('@paraspell/xcm-router', async () => { const actual = await vi.importActual('@paraspell/xcm-router'); @@ -16,38 +21,39 @@ vi.mock('@paraspell/xcm-router', async () => { ...actual, buildTransferExtrinsics: vi.fn().mockReturnValue([ { - node: 'Astar', + node: 'Ethereum', tx: 'serialized-api-call', - type: 'EXTRINSICS', + type: 'ETH_TRANSFER', statusType: 'TO_EXCHANGE', }, { - node: 'Hydration', + node: 'AssetHubPolkadot', tx: 'serialized-api-call', - type: 'EXTRINSICS', + type: 'EXTRINSIC', statusType: 'SWAP', }, { node: 'Astar', tx: 'serialized-api-call', - type: 'EXTRINSICS', + type: 'EXTRINSIC', statusType: 'TO_DESTINATION', }, ]), }; }); -// Unit tests to ensure service methods are working as expected describe('RouterService', () => { let service: RouterService; - let serializeExtrinsicSpy: any; + let serializeExtrinsicSpy: MockInstance< + (tx: Extrinsic) => TSerializedApiCall + >; - const options: RouterDto = { + const options: PatchedRouterDto = { from: 'Astar', exchange: 'AcalaDex', to: 'Moonbeam', - currencyFrom: 'ASTR', - currencyTo: 'GLMR', + currencyFrom: { symbol: 'ASTR' }, + currencyTo: { symbol: 'GLMR' }, amount: '1000000000000000000', injectorAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', recipientAddress: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', @@ -59,21 +65,21 @@ describe('RouterService', () => { const serializedTx = 'serialized-api-call'; const serializedExtrinsics = [ { - node: 'Astar', + node: 'Ethereum', tx: serializedTx, - type: 'EXTRINSICS', + type: 'ETH_TRANSFER', statusType: 'TO_EXCHANGE', }, { - node: 'Hydration', + node: 'AssetHubPolkadot', tx: serializedTx, - type: 'EXTRINSICS', + type: 'EXTRINSIC', statusType: 'SWAP', }, { node: 'Astar', tx: serializedTx, - type: 'EXTRINSICS', + type: 'EXTRINSIC', statusType: 'TO_DESTINATION', }, ]; @@ -87,7 +93,7 @@ describe('RouterService', () => { serializeExtrinsicSpy = vi .spyOn(utils, 'serializeExtrinsic') - .mockReturnValue(serializedTx as any); + .mockReturnValue(serializedTx as unknown as TSerializedApiCall); }); it('should be defined', () => { @@ -98,7 +104,23 @@ describe('RouterService', () => { it('should generate 3 extrinsics with manual exchange selection', async () => { const spy = vi.spyOn(spellRouter, 'buildTransferExtrinsics'); - const result = await service.generateExtrinsics(options); + const result = await service.generateExtrinsics({ + ...options, + type: undefined, + }); + + expect(result).toStrictEqual(serializedExtrinsics); + expect(spy).toHaveBeenCalledWith({ + ...options, + slippagePct: '1', + type: undefined, + }); + }); + + it('should generate 3 extrinsics with manual exchange selection - hash', async () => { + const spy = vi.spyOn(spellRouter, 'buildTransferExtrinsics'); + + const result = await service.generateExtrinsics(options, true); expect(result).toStrictEqual(serializedExtrinsics); expect(spy).toHaveBeenCalledWith({ @@ -108,9 +130,9 @@ describe('RouterService', () => { }); it('should throw BadRequestException for invalid from node', async () => { - const modifiedOptions: RouterDto = { + const modifiedOptions: PatchedRouterDto = { ...options, - from: invalidNode as any, + from: invalidNode as TNode, }; const spy = vi.spyOn(spellRouter, 'buildTransferExtrinsics'); @@ -123,9 +145,9 @@ describe('RouterService', () => { }); it('should throw BadRequestException for invalid to node', async () => { - const modifiedOptions: RouterDto = { + const modifiedOptions: PatchedRouterDto = { ...options, - to: invalidNode as any, + to: invalidNode as TNode, }; const spy = vi.spyOn(spellRouter, 'buildTransferExtrinsics'); @@ -138,9 +160,9 @@ describe('RouterService', () => { }); it('should throw BadRequestException for invalid exchange node', async () => { - const modifiedOptions: RouterDto = { + const modifiedOptions: PatchedRouterDto = { ...options, - exchange: invalidNode as any, + exchange: invalidNode as TNode, }; const spy = vi.spyOn(spellRouter, 'buildTransferExtrinsics'); @@ -153,7 +175,7 @@ describe('RouterService', () => { }); it('should throw BadRequestException for invalid injector address', async () => { - const modifiedOptions: RouterDto = { + const modifiedOptions: PatchedRouterDto = { ...options, injectorAddress: invalidNode, }; @@ -168,7 +190,7 @@ describe('RouterService', () => { }); it('should throw BadRequestException for invalid recipient address', async () => { - const modifiedOptions: RouterDto = { + const modifiedOptions: PatchedRouterDto = { ...options, recipientAddress: invalidNode, }; 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 4faf8085..e9d342e3 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,6 +1,7 @@ import { TCurrencyCore } from '@paraspell/sdk'; import { CurrencyCoreSchema } from '../../x-transfer/dto/XTransferDto.js'; import { z } from 'zod'; +import { validateAmount } from '../../utils/validateAmount.js'; export const TransferInfoSchema = z.object({ origin: z.string(), @@ -11,15 +12,9 @@ export const TransferInfoSchema = z.object({ .min(1, { message: 'Destination address is required' }), currency: CurrencyCoreSchema, amount: z.union([ - z.string().refine( - (val) => { - const num = parseFloat(val); - return !isNaN(num) && num > 0; - }, - { - message: 'Amount must be a positive number', - }, - ), + z.string().refine(validateAmount, { + message: 'Amount must be a positive number', + }), z.number().positive({ message: 'Amount must be a positive number' }), ]), }); 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 index ffb65251..576a2915 100644 --- a/apps/xcm-api/src/transfer-info/transfer-info.controller.test.ts +++ b/apps/xcm-api/src/transfer-info/transfer-info.controller.test.ts @@ -1,4 +1,3 @@ -/* 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'; @@ -43,7 +42,9 @@ describe('TransferInfoController', () => { amount: 100, }; const mockResult = {} as TTransferInfo; - vi.spyOn(service, 'getTransferInfo').mockResolvedValue(mockResult); + const spy = vi + .spyOn(service, 'getTransferInfo') + .mockResolvedValue(mockResult); const result = await controller.getTransferInfo( queryParams, @@ -51,7 +52,7 @@ describe('TransferInfoController', () => { ); expect(result).toBe(mockResult); - expect(service.getTransferInfo).toHaveBeenCalledWith(queryParams); + expect(spy).toHaveBeenCalledWith(queryParams); }); }); }); 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 abc35af5..dc72cbaa 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 @@ -139,4 +139,20 @@ describe('TransferInfoService', () => { }), ).rejects.toThrow(InternalServerErrorException); }); + + it('should validate destination wallet address', async () => { + vi.mocked(isValidWalletAddress).mockImplementation((address) => { + return address === '0x123'; + }); + await expect( + service.getTransferInfo({ + origin: 'Polkadot', + destination: 'Kusama', + accountOrigin: '0x123', + accountDestination: '0x456', + currency: { symbol: 'DOT' }, + amount: '1000', + }), + ).rejects.toThrow('Invalid destination wallet address.'); + }); }); diff --git a/apps/xcm-api/src/types/types.ts b/apps/xcm-api/src/types/types.ts index d0cef86c..4c8856f0 100644 --- a/apps/xcm-api/src/types/types.ts +++ b/apps/xcm-api/src/types/types.ts @@ -1,6 +1,6 @@ -export interface RequestWithUser extends Request { +export type RequestWithUser = Request & { user?: { id: string; requestLimit: number; }; -} +}; diff --git a/apps/xcm-api/src/users/users.service.test.ts b/apps/xcm-api/src/users/users.service.test.ts new file mode 100644 index 00000000..c919976d --- /dev/null +++ b/apps/xcm-api/src/users/users.service.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { UsersService } from './users.service.js'; +import { Repository } from 'typeorm'; +import { User } from './user.entity.js'; + +describe('UsersService', () => { + let service: UsersService; + let usersRepository: Repository; + + beforeEach(() => { + usersRepository = { + save: vi.fn(), + findOneBy: vi.fn(), + } as unknown as Repository; + + service = new UsersService(usersRepository); + }); + + describe('create', () => { + it('should call usersRepository.save with an empty object and return the result', async () => { + const mockUser = { id: '1', name: 'Test User', requestLimit: 1 } as User; + + const spy = vi.spyOn(usersRepository, 'save').mockResolvedValue(mockUser); + + const result = await service.create(); + + expect(spy).toHaveBeenCalledWith({}); + expect(result).toBe(mockUser); + }); + }); + + describe('findOne', () => { + it('should call usersRepository.findOneBy with the correct userId and return the result', async () => { + const userId = '1'; + const mockUser = { + id: userId, + name: 'Test User', + requestLimit: 1, + } as User; + + const spy = vi + .spyOn(usersRepository, 'findOneBy') + .mockResolvedValue(mockUser); + + const result = await service.findOne(userId); + + expect(spy).toHaveBeenCalledWith({ id: userId }); + expect(result).toBe(mockUser); + }); + + it('should return null if no user is found', async () => { + const userId = '1'; + + const spy = vi + .spyOn(usersRepository, 'findOneBy') + .mockResolvedValue(null); + + const result = await service.findOne(userId); + + expect(spy).toHaveBeenCalledWith({ id: userId }); + expect(result).toBeNull(); + }); + }); +}); diff --git a/apps/xcm-api/src/utils.spec.ts b/apps/xcm-api/src/utils.test.ts similarity index 87% rename from apps/xcm-api/src/utils.spec.ts rename to apps/xcm-api/src/utils.test.ts index 2db12eb5..8397b968 100644 --- a/apps/xcm-api/src/utils.spec.ts +++ b/apps/xcm-api/src/utils.test.ts @@ -4,7 +4,6 @@ import { isNumeric, validateNode } from './utils.js'; describe('isNumeric', () => { it('should return true for numeric values', () => { - expect(isNumeric(123)).toBe(true); expect(isNumeric('123')).toBe(true); expect(isNumeric('0')).toBe(true); }); @@ -13,8 +12,6 @@ describe('isNumeric', () => { expect(isNumeric('abc')).toBe(false); expect(isNumeric('123abc')).toBe(false); expect(isNumeric(undefined)).toBe(false); - expect(isNumeric(NaN)).toBe(false); - expect(isNumeric({})).toBe(false); }); }); diff --git a/apps/xcm-api/src/utils.ts b/apps/xcm-api/src/utils.ts index 64d1c903..90331010 100644 --- a/apps/xcm-api/src/utils.ts +++ b/apps/xcm-api/src/utils.ts @@ -1,7 +1,4 @@ -import { - BadRequestException, - InternalServerErrorException, -} from '@nestjs/common'; +import { BadRequestException } from '@nestjs/common'; import { Extrinsic, NODE_NAMES, @@ -10,7 +7,6 @@ import { } from '@paraspell/sdk'; import { decodeAddress, encodeAddress } from '@polkadot/keyring'; import { hexToU8a, isHex } from '@polkadot/util'; -import axios from 'axios'; import { isAddress } from 'web3-validator'; export const isNumeric = (num: string) => !isNaN(Number(num)); @@ -23,28 +19,6 @@ export const validateNode = (node: string) => { } }; -export const validateRecaptcha = async ( - recaptcha: string, - recaptchaSecretKey: string, -): Promise => { - const data = { - secret: recaptchaSecretKey, - response: recaptcha, - }; - - const response = await axios - .post('https://www.google.com/recaptcha/api/siteverify', null, { - params: data, - }) - .catch((error) => { - throw new InternalServerErrorException( - 'Error verifying reCAPTCHA: ' + error, - ); - }); - - return (response.data as { success: boolean }).success; -}; - export const isValidPolkadotAddress = (address: string) => { try { encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address)); diff --git a/apps/xcm-api/src/utils/replaceBigInt.test.ts b/apps/xcm-api/src/utils/replaceBigInt.test.ts new file mode 100644 index 00000000..a592fe8e --- /dev/null +++ b/apps/xcm-api/src/utils/replaceBigInt.test.ts @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import { replaceBigInt } from './replaceBigInt.js'; + +describe('replaceBigInt', () => { + it('should convert bigint to string', () => { + const result = replaceBigInt('', BigInt(123456789)); + expect(result).toBe('123456789'); + }); + + it('should return non-bigint values as-is', () => { + const result = replaceBigInt('', 123); + expect(result).toBe(123); + + const stringValue = replaceBigInt('', 'test'); + expect(stringValue).toBe('test'); + + const boolValue = replaceBigInt('', true); + expect(boolValue).toBe(true); + + const nullValue = replaceBigInt('', null); + expect(nullValue).toBe(null); + + const undefinedValue = replaceBigInt('', undefined); + expect(undefinedValue).toBe(undefined); + }); +}); diff --git a/apps/xcm-api/src/utils/replaceBigInt.ts b/apps/xcm-api/src/utils/replaceBigInt.ts new file mode 100644 index 00000000..1de40dc3 --- /dev/null +++ b/apps/xcm-api/src/utils/replaceBigInt.ts @@ -0,0 +1,2 @@ +export const replaceBigInt = (_key: string, value: unknown) => + typeof value === 'bigint' ? value.toString() : value; diff --git a/apps/xcm-api/src/utils/validateAmount.test.ts b/apps/xcm-api/src/utils/validateAmount.test.ts new file mode 100644 index 00000000..a5264b7c --- /dev/null +++ b/apps/xcm-api/src/utils/validateAmount.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; +import { validateAmount } from './validateAmount.js'; + +describe('validateAmount', () => { + it('should return true for valid positive numbers as strings', () => { + expect(validateAmount('100')).toBe(true); + expect(validateAmount('0.5')).toBe(true); + expect(validateAmount('1234.567')).toBe(true); + }); + + it('should return false for negative numbers', () => { + expect(validateAmount('-100')).toBe(false); + expect(validateAmount('-0.5')).toBe(false); + }); +}); diff --git a/apps/xcm-api/src/utils/validateAmount.ts b/apps/xcm-api/src/utils/validateAmount.ts new file mode 100644 index 00000000..4ca5bc10 --- /dev/null +++ b/apps/xcm-api/src/utils/validateAmount.ts @@ -0,0 +1,4 @@ +export const validateAmount = (val: string) => { + const num = parseFloat(val); + return !isNaN(num) && num > 0; +}; diff --git a/apps/xcm-api/src/utils/validateRecaptcha.test.ts b/apps/xcm-api/src/utils/validateRecaptcha.test.ts new file mode 100644 index 00000000..06ceaf6d --- /dev/null +++ b/apps/xcm-api/src/utils/validateRecaptcha.test.ts @@ -0,0 +1,61 @@ +import { describe, it, expect, vi } from 'vitest'; +import axios from 'axios'; +import { InternalServerErrorException } from '@nestjs/common'; +import { validateRecaptcha } from './validateRecaptcha.js'; + +vi.mock('axios'); + +describe('validateRecaptcha', () => { + const mockRecaptcha = 'mock-recaptcha-token'; + const mockSecretKey = 'mock-secret-key'; + + it('should return true when recaptcha validation is successful', async () => { + // Mock axios.post to resolve with a success response + + const spy = vi.spyOn(axios, 'post').mockResolvedValue({ + data: { success: true }, + }); + + const result = await validateRecaptcha(mockRecaptcha, mockSecretKey); + + expect(result).toBe(true); + expect(spy).toHaveBeenCalledWith( + 'https://www.google.com/recaptcha/api/siteverify', + null, + { + params: { + secret: mockSecretKey, + response: mockRecaptcha, + }, + }, + ); + }); + + it('should return false when recaptcha validation fails', async () => { + const spy = vi.spyOn(axios, 'post').mockResolvedValue({ + data: { success: false }, + }); + + const result = await validateRecaptcha(mockRecaptcha, mockSecretKey); + + expect(result).toBe(false); + expect(spy).toHaveBeenCalledWith( + 'https://www.google.com/recaptcha/api/siteverify', + null, + { + params: { + secret: mockSecretKey, + response: mockRecaptcha, + }, + }, + ); + }); + + it('should throw an InternalServerErrorException when axios throws an error', async () => { + vi.spyOn(axios, 'post').mockRejectedValue(new Error('Network Error')); + + await expect( + validateRecaptcha(mockRecaptcha, mockSecretKey), + ).rejects.toThrow(InternalServerErrorException); + }); +}); diff --git a/apps/xcm-api/src/utils/validateRecaptcha.ts b/apps/xcm-api/src/utils/validateRecaptcha.ts new file mode 100644 index 00000000..bf08f124 --- /dev/null +++ b/apps/xcm-api/src/utils/validateRecaptcha.ts @@ -0,0 +1,24 @@ +import { InternalServerErrorException } from '@nestjs/common'; +import axios from 'axios'; + +export const validateRecaptcha = async ( + recaptcha: string, + recaptchaSecretKey: string, +): Promise => { + const data = { + secret: recaptchaSecretKey, + response: recaptcha, + }; + + const response = await axios + .post('https://www.google.com/recaptcha/api/siteverify', null, { + params: data, + }) + .catch((error) => { + throw new InternalServerErrorException( + 'Error verifying reCAPTCHA: ' + error, + ); + }); + + return (response.data as { success: boolean }).success; +}; 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 index b04472d9..3ad9caf4 100644 --- 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 @@ -1,19 +1,14 @@ import { TCurrencyCore } from '@paraspell/sdk'; import { CurrencyCoreSchema } from '../../x-transfer/dto/XTransferDto.js'; import { z } from 'zod'; +import { validateAmount } from '../../utils/validateAmount.js'; 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.string().refine(validateAmount, { + 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' }), diff --git a/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.controller.test.ts b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.controller.test.ts index 64a894bc..a6e8f7bb 100644 --- a/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.controller.test.ts +++ b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.controller.test.ts @@ -1,4 +1,3 @@ -/* 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'; @@ -41,7 +40,9 @@ describe('XTransferEthController', () => { currency: { symbol: 'WETH' }, }; const mockResult = {} as TSerializedEthTransfer; - vi.spyOn(service, 'generateEthCall').mockResolvedValue(mockResult); + const spy = vi + .spyOn(service, 'generateEthCall') + .mockResolvedValue(mockResult); const result = await controller.generateXcmCall( queryParams, @@ -49,7 +50,7 @@ describe('XTransferEthController', () => { ); expect(result).toBe(mockResult); - expect(service.generateEthCall).toHaveBeenCalledWith(queryParams); + expect(spy).toHaveBeenCalledWith(queryParams); }); }); }); diff --git a/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.service.test.ts b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.service.test.ts new file mode 100644 index 00000000..4086d778 --- /dev/null +++ b/apps/xcm-api/src/x-transfer-eth/x-transfer-eth.service.test.ts @@ -0,0 +1,115 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { + buildEthTransferOptions, + TSerializedEthTransfer, +} from '@paraspell/sdk'; +import { isValidPolkadotAddress } from '../utils.js'; +import { PatchedXTransferEthDto } from './dto/x-transfer-eth.dto.js'; +import { XTransferEthService } from './x-transfer-eth.service.js'; + +vi.mock('@paraspell/sdk', () => ({ + NODE_NAMES_DOT_KSM: ['Polkadot', 'Kusama'], + buildEthTransferOptions: vi.fn(), +})); + +vi.mock('../utils.js', () => ({ + isValidPolkadotAddress: vi.fn(), +})); + +describe('XTransferEthService', () => { + let service: XTransferEthService; + + beforeEach(() => { + service = new XTransferEthService(); + }); + + it('should throw BadRequestException if the node is invalid', async () => { + const dto: PatchedXTransferEthDto = { + to: 'InvalidNode', + amount: 100, + address: '0xAddress', + destAddress: '1DestinationAddress', + currency: { symbol: 'DOT' }, + }; + + await expect(service.generateEthCall(dto)).rejects.toThrow( + BadRequestException, + ); + await expect(service.generateEthCall(dto)).rejects.toThrow( + 'Node InvalidNode is not valid. Check docs for valid nodes.', + ); + }); + + it('should throw BadRequestException if the destination address is invalid', async () => { + const dto: PatchedXTransferEthDto = { + to: 'Polkadot', + amount: 100, + address: '0xAddress', + destAddress: 'InvalidAddress', + currency: { symbol: 'DOT' }, + }; + + vi.mocked(isValidPolkadotAddress).mockReturnValue(false); + + await expect(service.generateEthCall(dto)).rejects.toThrow( + BadRequestException, + ); + await expect(service.generateEthCall(dto)).rejects.toThrow( + 'Invalid wallet address.', + ); + }); + + it('should return the result from buildEthTransferOptions for valid input', async () => { + const dto: PatchedXTransferEthDto = { + to: 'Polkadot', + amount: 100, + address: '0xAddress', + destAddress: '1DestinationAddress', + currency: { symbol: 'DOT' }, + }; + + const mockResult = { success: true }; + + vi.mocked(isValidPolkadotAddress).mockReturnValue(true); + vi.mocked(buildEthTransferOptions).mockResolvedValue( + mockResult as unknown as TSerializedEthTransfer, + ); + + const result = await service.generateEthCall(dto); + + expect(result).toBe(mockResult); + expect(buildEthTransferOptions).toHaveBeenCalledWith({ + to: 'Polkadot', + amount: '100', + address: '0xAddress', + destAddress: '1DestinationAddress', + currency: { symbol: 'DOT' }, + }); + }); + + it('should throw InternalServerErrorException when buildEthTransferOptions throws an error', async () => { + const dto: PatchedXTransferEthDto = { + to: 'Polkadot', + amount: 100, + address: '0xAddress', + destAddress: '1DestinationAddress', + currency: { symbol: 'DOT' }, + }; + + vi.mocked(isValidPolkadotAddress).mockReturnValue(true); + vi.mocked(buildEthTransferOptions).mockRejectedValue( + new Error('Something went wrong'), + ); + + await expect(service.generateEthCall(dto)).rejects.toThrow( + InternalServerErrorException, + ); + await expect(service.generateEthCall(dto)).rejects.toThrow( + 'Something went wrong', + ); + }); +}); diff --git a/apps/xcm-api/src/x-transfer/dto/XTransferDto.ts b/apps/xcm-api/src/x-transfer/dto/XTransferDto.ts index c1826b58..22bb4583 100644 --- a/apps/xcm-api/src/x-transfer/dto/XTransferDto.ts +++ b/apps/xcm-api/src/x-transfer/dto/XTransferDto.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { TAddress, TCurrencyInput, Version } from '@paraspell/sdk'; import { MultiLocationSchema } from '@paraspell/xcm-analyser'; +import { validateAmount } from '../../utils/validateAmount.js'; const StringOrNumber = z .string() @@ -66,15 +67,9 @@ export const XTransferDtoSchema = z.object({ from: z.string().optional(), to: z.union([z.string().optional(), MultiLocationSchema]), amount: z.union([ - z.string().refine( - (val) => { - const num = parseFloat(val); - return !isNaN(num) && num > 0; - }, - { - message: 'Amount must be a positive number', - }, - ), + z.string().refine(validateAmount, { + message: 'Amount must be a positive number', + }), z.number().positive({ message: 'Amount must be a positive number' }), ]), address: z.union([ diff --git a/apps/xcm-api/src/x-transfer/x-transfer.controller.spec.ts b/apps/xcm-api/src/x-transfer/x-transfer.controller.test.ts similarity index 83% rename from apps/xcm-api/src/x-transfer/x-transfer.controller.spec.ts rename to apps/xcm-api/src/x-transfer/x-transfer.controller.test.ts index 6c280471..87dd24a6 100644 --- a/apps/xcm-api/src/x-transfer/x-transfer.controller.spec.ts +++ b/apps/xcm-api/src/x-transfer/x-transfer.controller.test.ts @@ -4,7 +4,7 @@ import { XTransferController } from './x-transfer.controller.js'; import { XTransferService } from './x-transfer.service.js'; import { mockRequestObject } from '../testUtils.js'; import { AnalyticsService } from '../analytics/analytics.service.js'; -import { PatchedXTransferDto, XTransferDto } from './dto/XTransferDto.js'; +import { PatchedXTransferDto } from './dto/XTransferDto.js'; import { Extrinsic } from '@paraspell/sdk'; // Integration tests to ensure controller and service are working together @@ -42,7 +42,9 @@ describe('XTransferController', () => { currency: { symbol: 'DOT' }, }; const mockResult = {} as Extrinsic; - vi.spyOn(service, 'generateXcmCall').mockResolvedValue(mockResult); + const spy = vi + .spyOn(service, 'generateXcmCall') + .mockResolvedValue(mockResult); const result = await controller.generateXcmCall( queryParams, @@ -50,7 +52,7 @@ describe('XTransferController', () => { ); expect(result).toBe(mockResult); - expect(service.generateXcmCall).toHaveBeenCalledWith(queryParams); + expect(spy).toHaveBeenCalledWith(queryParams); }); }); @@ -59,12 +61,14 @@ describe('XTransferController', () => { const bodyParams: PatchedXTransferDto = { from: 'Acala', to: 'Basilisk', - amount: 100, + amount: '100', address: '5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96', currency: { symbol: 'DOT' }, }; const mockResult = {} as Extrinsic; - vi.spyOn(service, 'generateXcmCall').mockResolvedValue(mockResult); + const spy = vi + .spyOn(service, 'generateXcmCall') + .mockResolvedValue(mockResult); const result = await controller.generateXcmCallV2( bodyParams, @@ -72,7 +76,7 @@ describe('XTransferController', () => { ); expect(result).toBe(mockResult); - expect(service.generateXcmCall).toHaveBeenCalledWith(bodyParams); + expect(spy).toHaveBeenCalledWith(bodyParams); }); }); @@ -86,7 +90,9 @@ describe('XTransferController', () => { currency: { symbol: 'DOT' }, }; const mockResult = {} as Extrinsic; - vi.spyOn(service, 'generateXcmCall').mockResolvedValue(mockResult); + const spy = vi + .spyOn(service, 'generateXcmCall') + .mockResolvedValue(mockResult); const result = await controller.generateXcmCallV2Hash( bodyParams, @@ -94,7 +100,7 @@ describe('XTransferController', () => { ); expect(result).toBe(mockResult); - expect(service.generateXcmCall).toHaveBeenCalledWith(bodyParams, true); + expect(spy).toHaveBeenCalledWith(bodyParams, true); }); }); }); diff --git a/apps/xcm-api/src/x-transfer/x-transfer.controller.ts b/apps/xcm-api/src/x-transfer/x-transfer.controller.ts index 9e56aaed..81a5298c 100644 --- a/apps/xcm-api/src/x-transfer/x-transfer.controller.ts +++ b/apps/xcm-api/src/x-transfer/x-transfer.controller.ts @@ -31,9 +31,8 @@ export class XTransferController { params: XTransferDto, ) { const { from, to, currency } = params; - const resolvedCurrency = - typeof currency === 'string' ? currency : 'MultiLocation'; - const resolvedTo = typeof to === 'string' ? to : 'MultiLocation'; + const resolvedCurrency = JSON.stringify(currency); + const resolvedTo = JSON.stringify(to); this.analyticsService.track(eventName, req, { from, resolvedTo, diff --git a/apps/xcm-api/src/x-transfer/x-transfer.service.spec.ts b/apps/xcm-api/src/x-transfer/x-transfer.service.test.ts similarity index 74% rename from apps/xcm-api/src/x-transfer/x-transfer.service.spec.ts rename to apps/xcm-api/src/x-transfer/x-transfer.service.test.ts index e0d2c499..f0cf5aa9 100644 --- a/apps/xcm-api/src/x-transfer/x-transfer.service.spec.ts +++ b/apps/xcm-api/src/x-transfer/x-transfer.service.test.ts @@ -1,7 +1,7 @@ import { vi, describe, expect, it, beforeEach, afterEach } from 'vitest'; import { Test, TestingModule } from '@nestjs/testing'; import { XTransferService } from './x-transfer.service.js'; -import { XTransferDto } from './dto/XTransferDto.js'; +import { PatchedXTransferDto } from './dto/XTransferDto.js'; import { BadRequestException, InternalServerErrorException, @@ -14,6 +14,7 @@ import { } from '@paraspell/sdk'; const builderMock = { + xcmVersion: vi.fn().mockReturnThis(), from: vi.fn().mockReturnThis(), to: vi.fn().mockReturnThis(), currency: vi.fn().mockReturnThis(), @@ -26,7 +27,9 @@ vi.mock('@paraspell/sdk', async () => { const actual = await vi.importActual('@paraspell/sdk'); return { ...actual, - createApiInstanceForNode: vi.fn().mockResolvedValue(undefined), + createApiInstanceForNode: vi.fn().mockResolvedValue({ + disconnect: vi.fn(), + }), Builder: vi.fn().mockImplementation(() => builderMock), }; }); @@ -60,7 +63,7 @@ describe('XTransferService', () => { it('should generate XCM call for parachain to parachain transfer', async () => { const from: TNode = 'Acala'; const to: TNode = 'Basilisk'; - const xTransferDto: XTransferDto = { + const xTransferDto: PatchedXTransferDto = { from, to, amount, @@ -83,7 +86,7 @@ describe('XTransferService', () => { it('should generate XCM call for parachain to relaychain transfer', async () => { const from: TNode = 'Acala'; - const xTransferDto: XTransferDto = { + const xTransferDto: PatchedXTransferDto = { from, amount, address, @@ -102,10 +105,11 @@ describe('XTransferService', () => { it('should generate XCM call for relaychain to parachain transfer', async () => { const to: TNode = 'Acala'; - const xTransferDto: XTransferDto = { + const xTransferDto: PatchedXTransferDto = { to, amount, address, + currency, }; const result = await service.generateXcmCall(xTransferDto); @@ -119,7 +123,7 @@ describe('XTransferService', () => { }); it('should throw BadRequestException for invalid from node', async () => { - const xTransferDto: XTransferDto = { + const xTransferDto: PatchedXTransferDto = { from: invalidNode, amount, address, @@ -133,7 +137,7 @@ describe('XTransferService', () => { }); it('should throw BadRequestException for invalid to node', async () => { - const xTransferDto: XTransferDto = { + const xTransferDto: PatchedXTransferDto = { to: invalidNode, amount, address, @@ -147,7 +151,7 @@ describe('XTransferService', () => { }); it('should throw BadRequestException when from and to node are missing', async () => { - const xTransferDto: XTransferDto = { + const xTransferDto: PatchedXTransferDto = { amount, address, currency, @@ -160,11 +164,12 @@ describe('XTransferService', () => { }); it('should throw BadRequestException for missing currency in parachain to parachain transfer', async () => { - const xTransferDto: XTransferDto = { + const xTransferDto: PatchedXTransferDto = { from: 'Acala', to: 'Basilisk', amount, address, + currency: undefined, }; await expect(service.generateXcmCall(xTransferDto)).rejects.toThrow( @@ -174,7 +179,7 @@ describe('XTransferService', () => { }); it('should throw BadRequestException for invalid currency', async () => { - const xTransferDto: XTransferDto = { + const xTransferDto: PatchedXTransferDto = { from: 'Acala', to: 'Basilisk', amount, @@ -192,7 +197,9 @@ describe('XTransferService', () => { throw new InvalidCurrencyError(''); }), }; - vi.spyOn(paraspellSdk, 'Builder').mockReturnValue(builderMock as any); + vi.spyOn(paraspellSdk, 'Builder').mockReturnValue( + builderMock as unknown as paraspellSdk.GeneralBuilder, + ); await expect(service.generateXcmCall(xTransferDto)).rejects.toThrow( BadRequestException, @@ -201,7 +208,7 @@ describe('XTransferService', () => { }); it('should throw InternalServerError when uknown error occures in the SDK', async () => { - const xTransferDto: XTransferDto = { + const xTransferDto: PatchedXTransferDto = { from: 'Acala', to: 'Basilisk', amount, @@ -219,12 +226,59 @@ describe('XTransferService', () => { throw new Error('Unknown error'); }), }; - vi.spyOn(paraspellSdk, 'Builder').mockReturnValue(builderMock as any); + vi.spyOn(paraspellSdk, 'Builder').mockReturnValue( + builderMock as unknown as paraspellSdk.GeneralBuilder, + ); await expect(service.generateXcmCall(xTransferDto)).rejects.toThrow( InternalServerErrorException, ); expect(createApiInstanceForNode).toHaveBeenCalled(); }); + + it('should throw on invalid wallet address', async () => { + const xTransferDto: PatchedXTransferDto = { + from: 'Acala', + to: 'Basilisk', + amount, + address: 'invalid-address', + currency, + }; + + await expect(service.generateXcmCall(xTransferDto)).rejects.toThrow( + BadRequestException, + ); + expect(createApiInstanceForNode).not.toHaveBeenCalled(); + }); + + it('should specify xcm version if provided', async () => { + const xTransferDto: PatchedXTransferDto = { + from: 'Acala', + to: 'AssetHubPolkadot', + amount, + address, + currency, + xcmVersion: paraspellSdk.Version.V2, + }; + + const builderMock = { + from: vi.fn().mockReturnThis(), + to: vi.fn().mockReturnThis(), + currency: vi.fn().mockReturnThis(), + amount: vi.fn().mockReturnThis(), + address: vi.fn().mockReturnThis(), + xcmVersion: vi.fn().mockReturnThis(), + build: vi.fn().mockReturnValue('serialized-api-call'), + }; + vi.spyOn(paraspellSdk, 'Builder').mockReturnValue( + builderMock as unknown as paraspellSdk.GeneralBuilder, + ); + + await service.generateXcmCall(xTransferDto, true); + + expect(builderMock.xcmVersion).toHaveBeenCalledWith( + paraspellSdk.Version.V2, + ); + }); }); }); 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 593e55fb..e4b4e458 100644 --- a/apps/xcm-api/src/x-transfer/x-transfer.service.ts +++ b/apps/xcm-api/src/x-transfer/x-transfer.service.ts @@ -87,6 +87,7 @@ export class XTransferService { e instanceof InvalidCurrencyError || e instanceof IncompatibleNodesError ) { + console.log(e); throw new BadRequestException(e.message); } const error = e as Error; diff --git a/apps/xcm-api/src/xcm-analyser/xcm-analyser.controller.test.ts b/apps/xcm-api/src/xcm-analyser/xcm-analyser.controller.test.ts new file mode 100644 index 00000000..2a5eb720 --- /dev/null +++ b/apps/xcm-api/src/xcm-analyser/xcm-analyser.controller.test.ts @@ -0,0 +1,75 @@ +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 { XcmAnalyserController } from './xcm-analyser.controller.js'; +import { XcmAnalyserService } from './xcm-analyser.service.js'; +import { XcmAnalyserDto } from './dto/xcm-analyser.dto.js'; + +describe('XcmAnalyserController', () => { + let controller: XcmAnalyserController; + let service: XcmAnalyserService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [XcmAnalyserController], + providers: [ + XcmAnalyserService, + { + provide: AnalyticsService, + useValue: { get: () => '', track: vi.fn() }, + }, + ], + }).compile(); + + controller = module.get(XcmAnalyserController); + service = module.get(XcmAnalyserService); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('getMultiLocationPaths', () => { + it('should call analyticsService and xcmAnalyserService', () => { + const bodyParams: XcmAnalyserDto = { + multilocation: { + parents: 1, + interior: { + X1: { + Parachain: 200, + }, + }, + }, + }; + const req = mockRequestObject; + + const xcmAnalyserServiceSpy = vi.spyOn(service, 'getMultiLocationPaths'); + + controller.getMultiLocationPaths(bodyParams, req); + + expect(xcmAnalyserServiceSpy).toHaveBeenCalledWith(bodyParams); + }); + + it('should return the value from xcmAnalyserService', () => { + const bodyParams: XcmAnalyserDto = { + multilocation: { + parents: 1, + interior: { + X1: { + Parachain: 200, + }, + }, + }, + }; + const req = mockRequestObject; + const mockResponse = ['path1', 'path2']; + + vi.spyOn(service, 'getMultiLocationPaths').mockReturnValue(mockResponse); + + const result = controller.getMultiLocationPaths(bodyParams, req); + + expect(result).toBe(mockResponse); + }); + }); +}); diff --git a/apps/xcm-api/src/xcm-analyser/xcm-analyser.service.test.ts b/apps/xcm-api/src/xcm-analyser/xcm-analyser.service.test.ts index 2c49efbf..d3ed0cf2 100644 --- a/apps/xcm-api/src/xcm-analyser/xcm-analyser.service.test.ts +++ b/apps/xcm-api/src/xcm-analyser/xcm-analyser.service.test.ts @@ -75,7 +75,7 @@ describe('XcmAnalyserService', () => { }); expect(convertMultilocationToUrl).toHaveBeenCalledWith(multilocation); - expect(result).toEqual(JSON.stringify(expectedUrl)); + expect(result).toEqual(`"${expectedUrl}"`); }); it('returns the correct URLs for xcm', () => { @@ -98,7 +98,7 @@ describe('XcmAnalyserService', () => { }); expect(convertXCMToUrls).toHaveBeenCalledWith(xcm); - expect(result).toEqual(JSON.stringify(expectedUrls)); + expect(result).toEqual(expectedUrls); }); it('throws InternalServerErrorException when conversion fails', () => { diff --git a/apps/xcm-api/src/xcm-analyser/xcm-analyser.service.ts b/apps/xcm-api/src/xcm-analyser/xcm-analyser.service.ts index 6f5eec93..de04d312 100644 --- a/apps/xcm-api/src/xcm-analyser/xcm-analyser.service.ts +++ b/apps/xcm-api/src/xcm-analyser/xcm-analyser.service.ts @@ -26,9 +26,9 @@ export class XcmAnalyserService { try { if (multilocation) { - return JSON.stringify(convertMultilocationToUrl(multilocation)); + return `"${convertMultilocationToUrl(multilocation)}"`; } else { - return JSON.stringify(convertXCMToUrls(xcm)); + return convertXCMToUrls(xcm); } } catch (e) { if (e instanceof Error) { diff --git a/apps/xcm-api/test/app.e2e-spec.ts b/apps/xcm-api/test/app.e2e-spec.ts index 8b3e02b8..3b2458b3 100644 --- a/apps/xcm-api/test/app.e2e-spec.ts +++ b/apps/xcm-api/test/app.e2e-spec.ts @@ -24,6 +24,8 @@ import { import { RouterDto } from '../src/router/dto/RouterDto'; import { describe, beforeAll, it, expect } from 'vitest'; import { TransferInfoDto } from '../src/transfer-info/dto/transfer-info.dto'; +import { ExpressAdapter } from '@nestjs/platform-express'; +import { replaceBigInt } from '../src/utils/replaceBigInt'; describe('XCM API (e2e)', () => { let app: INestApplication; @@ -32,15 +34,15 @@ 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(); app = moduleFixture.createNestApplication(); + (app.getHttpAdapter().getInstance() as ExpressAdapter).set( + 'json replacer', + replaceBigInt, + ); await app.init(); }); diff --git a/apps/xcm-api/vitest.config.ts b/apps/xcm-api/vitest.config.ts index fdc4e964..e6f84a9d 100644 --- a/apps/xcm-api/vitest.config.ts +++ b/apps/xcm-api/vitest.config.ts @@ -6,8 +6,8 @@ export default defineConfig({ globals: true, root: './', coverage: { - include: ['src/**/*'], - exclude: ['src/**/*.module.ts', 'src/main.ts'], + include: ['src/**/*.ts'], + exclude: ['src/**/*.module.ts', 'src/**/*.test.ts', 'src/types/types.ts'], }, }, plugins: [ diff --git a/eslint.config.js b/eslint.config.js index a6f81868..46c6bb4f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -28,6 +28,7 @@ export default tseslint.config( "**/codegen.ts", "**/playwright.config.ts", "**/e2e/", + "**/coverage/", ], }, eslint.configs.recommended, diff --git a/packages/sdk/src/errors/MissingApiPromiseError.ts b/packages/sdk/src/errors/MissingApiPromiseError.ts deleted file mode 100644 index a2dcc274..00000000 --- a/packages/sdk/src/errors/MissingApiPromiseError.ts +++ /dev/null @@ -1,6 +0,0 @@ -export class MissingApiPromiseError extends Error { - constructor() { - super('Please provide ApiPromise instance.') - this.name = 'MissingApiPromise' - } -} diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts new file mode 100644 index 00000000..52feedf9 --- /dev/null +++ b/packages/sdk/src/index.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest' +import * as sdk from './index' + +describe('Module Exports', () => { + it('should export xcmPallet', () => { + expect(sdk.xcmPallet).toBeDefined() + }) + + it('should export assets', () => { + expect(sdk.assets).toBeDefined() + }) + + it('should export NODE_NAMES_DOT_KSM and other constants', () => { + expect(sdk.NODE_NAMES_DOT_KSM).toBeDefined() + expect(sdk.NODE_NAMES).toBeDefined() + expect(sdk.NODES_WITH_RELAY_CHAINS).toBeDefined() + expect(sdk.NODES_WITH_RELAY_CHAINS_DOT_KSM).toBeDefined() + expect(sdk.SUPPORTED_PALLETS).toBeDefined() + }) + + it('should export utility functions', () => { + expect(sdk.getNodeEndpointOption).toBeDefined() + expect(sdk.getNode).toBeDefined() + expect(sdk.getNodeProvider).toBeDefined() + expect(sdk.getAllNodeProviders).toBeDefined() + expect(sdk.createApiInstanceForNode).toBeDefined() + expect(sdk.isRelayChain).toBeDefined() + expect(sdk.determineRelayChain).toBeDefined() + }) +}) diff --git a/packages/sdk/src/nodes/ParachainNode.test.ts b/packages/sdk/src/nodes/ParachainNode.test.ts index fab4e52a..f4637ae5 100644 --- a/packages/sdk/src/nodes/ParachainNode.test.ts +++ b/packages/sdk/src/nodes/ParachainNode.test.ts @@ -1,11 +1,11 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' import { InvalidCurrencyError } from '../errors' import { isTMultiLocation } from '../pallets/xcmPallet/utils' -import { verifyMultiLocation } from '../utils/verifyMultilocation' import { TMultiLocation, TNode, TSendInternalOptions } from '../types' import { getNode } from '../utils' import AssetHubPolkadot from './supported/AssetHubPolkadot' import { ApiPromise } from '@polkadot/api' +import { verifyMultiLocation } from '../utils/verifyMultiLocation' vi.mock('../pallets/xcmPallet/utils', async () => { const actual = await import('../pallets/xcmPallet/utils') @@ -15,7 +15,7 @@ vi.mock('../pallets/xcmPallet/utils', async () => { } }) -vi.mock('../utils/verifyMultilocation', () => ({ +vi.mock('../utils/verifyMultiLocation', () => ({ verifyMultiLocation: vi.fn() })) diff --git a/packages/sdk/src/nodes/ParachainNode.ts b/packages/sdk/src/nodes/ParachainNode.ts index 348a0d1f..0c5cd3ee 100644 --- a/packages/sdk/src/nodes/ParachainNode.ts +++ b/packages/sdk/src/nodes/ParachainNode.ts @@ -20,7 +20,13 @@ import { type TNodePolkadotKusama, type TTransferReturn } from '../types' -import { generateAddressPayload, getFees, getAllNodeProviders, createApiInstance } from '../utils' +import { + getAllNodeProviders, + createApiInstance, + generateAddressPayload, + getFees, + verifyMultiLocation +} from '../utils' import { constructRelayToParaParameters, createCurrencySpec, @@ -30,7 +36,6 @@ import { import { TMultiLocationHeader, type TMultiLocation } from '../types/TMultiLocation' import { type TMultiAsset } from '../types/TMultiAsset' import { InvalidCurrencyError } from '../errors' -import { verifyMultiLocation } from '../utils/verifyMultilocation' const supportsXTokens = (obj: unknown): obj is IXTokensTransfer => { return typeof obj === 'object' && obj !== null && 'transferXTokens' in obj diff --git a/packages/sdk/src/nodes/supported/Acala.test.ts b/packages/sdk/src/nodes/supported/Acala.test.ts new file mode 100644 index 00000000..6d63b83e --- /dev/null +++ b/packages/sdk/src/nodes/supported/Acala.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import { getNode } from '../../utils/getNode' +import { getAllNodeProviders } from '../../utils/getAllNodeProviders' +import Acala from './Acala' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +vi.mock('../../utils/getAllNodeProviders', () => ({ + getAllNodeProviders: vi.fn() +})) + +describe('Acala', () => { + let acala: Acala + const mockInput = { + currency: 'ACA', + amount: '100' + } as XTokensTransferInput + const spyTransferXTokens = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + beforeEach(() => { + acala = getNode('Acala') + spyTransferXTokens.mockClear() + }) + + it('should initialize with correct values', () => { + expect(acala.node).toBe('Acala') + expect(acala.name).toBe('acala') + expect(acala.type).toBe('polkadot') + expect(acala.version).toBe(Version.V3) + }) + + it('should call transferXTokens with Token when currencyID is undefined', () => { + acala.transferXTokens(mockInput) + + expect(spyTransferXTokens).toHaveBeenCalledWith(mockInput, { Token: 'ACA' }) + }) + + it('should call transferXTokens with ForeignAsset when currencyID is defined', () => { + const inputWithCurrencyID = { ...mockInput, currencyID: '1' } + + acala.transferXTokens(inputWithCurrencyID) + + expect(spyTransferXTokens).toHaveBeenCalledWith(inputWithCurrencyID, { + ForeignAsset: '1' + }) + }) + + it('should return the second WebSocket URL from getProvider', () => { + const mockProviders = ['ws://unreliable-url', 'ws://reliable-url'] + vi.mocked(getAllNodeProviders).mockReturnValue(mockProviders) + + const provider = acala.getProvider() + + expect(getAllNodeProviders).toHaveBeenCalledWith(acala.node) + expect(provider).toBe('ws://reliable-url') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Altair.test.ts b/packages/sdk/src/nodes/supported/Altair.test.ts new file mode 100644 index 00000000..1572632c --- /dev/null +++ b/packages/sdk/src/nodes/supported/Altair.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import { getNode } from '../../utils/getNode' +import Altair from './Altair' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Altair', () => { + let altair: Altair + const mockInput = { + currency: 'AIR', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + altair = getNode('Altair') + }) + + it('should initialize with correct values', () => { + expect(altair.node).toBe('Altair') + expect(altair.name).toBe('altair') + expect(altair.type).toBe('kusama') + expect(altair.version).toBe(Version.V3) + }) + + it('should call transferXTokens with Native when currency matches the native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(altair, 'getNativeAssetSymbol').mockReturnValue('AIR') + + altair.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'Native') + }) + + it('should call transferXTokens with ForeignAsset when currency does not match the native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + const inputWithCurrencyID = { ...mockInput, currencyID: '1' } + vi.spyOn(altair, 'getNativeAssetSymbol').mockReturnValue('NOT_AIR') + + altair.transferXTokens(inputWithCurrencyID) + + expect(spy).toHaveBeenCalledWith(inputWithCurrencyID, { + ForeignAsset: '1' + }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Amplitude.test.ts b/packages/sdk/src/nodes/supported/Amplitude.test.ts new file mode 100644 index 00000000..d8a11943 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Amplitude.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import { getNode } from '../../utils/getNode' +import Amplitude from './Amplitude' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Amplitude', () => { + let amplitude: Amplitude + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + amplitude = getNode('Amplitude') + }) + + it('should initialize with correct values', () => { + expect(amplitude.node).toBe('Amplitude') + expect(amplitude.name).toBe('amplitude') + expect(amplitude.type).toBe('kusama') + expect(amplitude.version).toBe(Version.V3) + }) + + it('should call transferXTokens with XCM asset selection', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + amplitude.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { XCM: '123' }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/AssetHubPolkadot.test.ts b/packages/sdk/src/nodes/supported/AssetHubPolkadot.test.ts index aa00f8c4..63441a8a 100644 --- a/packages/sdk/src/nodes/supported/AssetHubPolkadot.test.ts +++ b/packages/sdk/src/nodes/supported/AssetHubPolkadot.test.ts @@ -1,9 +1,11 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import { describe, it, expect, vi } from 'vitest' -import PolkadotXCMTransferImpl from '../polkadotXcm' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ethers } from 'ethers' import { InvalidCurrencyError, ScenarioNotSupportedError } from '../../errors' -import { Extrinsic, PolkadotXCMTransferInput } from '../../types' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import AssetHubPolkadot from './AssetHubPolkadot' import { ApiPromise } from '@polkadot/api' +import { Extrinsic, PolkadotXCMTransferInput } from '../../types' +import { getOtherAssets } from '../../pallets/assets' import { getNode } from '../../utils' vi.mock('ethers', () => ({ @@ -12,84 +14,137 @@ vi.mock('ethers', () => ({ } })) -vi.mock('../polkadotXcm', async () => { - const actual = await vi.importActual('../polkadotXcm') - return { - default: { - ...actual.default, - transferPolkadotXCM: vi.fn() - } +vi.mock('../polkadotXcm', () => ({ + 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) +vi.mock('../../utils/generateAddressMultiLocationV4', () => ({ + generateAddressMultiLocationV4: vi.fn() +})) + +vi.mock('../../utils/generateAddressPayload', () => ({ + generateAddressPayload: vi.fn() +})) + +describe('AssetHubPolkadot', () => { + let assetHub: AssetHubPolkadot + const mockInput = { + api: {} as unknown as ApiPromise, + currencySymbol: 'DOT', + currencySelection: {}, + currencyId: '0', + scenario: 'ParaToRelay', + header: {}, + addressSelection: {}, + paraIdTo: 1001, + amount: '1000', + address: 'address' + } as PolkadotXCMTransferInput + + beforeEach(() => { + vi.resetAllMocks() + assetHub = getNode('AssetHubPolkadot') }) - it('throws an error for unsupported currency', () => { - const assetHub = getNode('AssetHubPolkadot') - const input = { - ...mockInput, - currencySymbol: 'UNKNOWN' - } - expect(() => assetHub.handleBridgeTransfer(input, 'Kusama')).toThrowError(InvalidCurrencyError) + describe('handleBridgeTransfer', () => { + it('should process a valid DOT transfer to Polkadot', () => { + const mockResult = {} as Extrinsic + + const spy = vi + .spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + .mockReturnValue(mockResult) + + const result = assetHub.handleBridgeTransfer(mockInput, 'Polkadot') + expect(result).toStrictEqual(mockResult) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('throws InvalidCurrencyError for unsupported currency', () => { + const input = { + ...mockInput, + currencySymbol: 'UNKNOWN' + } + expect(() => assetHub.handleBridgeTransfer(input, 'Kusama')).toThrow(InvalidCurrencyError) + }) + + it('should process a valid KSM transfer to Kusama', () => { + const mockResult = {} as Extrinsic + const spy = vi + .spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + .mockReturnValue(mockResult) + const input = { + ...mockInput, + currencySymbol: 'KSM' + } + + const result = assetHub.handleBridgeTransfer(input, 'Kusama') + expect(result).toStrictEqual(mockResult) + expect(spy).toHaveBeenCalledTimes(1) + }) }) -}) -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) + describe('handleEthBridgeTransfer', () => { + it('should throw an error if the address is not a valid Ethereum address', () => { + vi.mocked(ethers.isAddress).mockReturnValue(false) + + expect(() => assetHub.handleEthBridgeTransfer(mockInput)).toThrowError( + 'Only Ethereum addresses are supported for Ethereum transfers' + ) + }) + + it('should throw InvalidCurrencyError if currency is not supported for Ethereum transfers', () => { + vi.mocked(ethers.isAddress).mockReturnValue(true) + vi.mocked(getOtherAssets).mockReturnValue([]) + + expect(() => assetHub.handleEthBridgeTransfer(mockInput)).toThrowError(InvalidCurrencyError) + }) }) - 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) + describe('transferPolkadotXCM', () => { + it('throws ScenarioNotSupportedError for native DOT transfers in para to para scenarios', () => { + 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 input = { + ...mockInput, + currencySymbol: 'KSM', + currencyId: undefined, + scenario: 'ParaToPara', + destination: 'Acala' + } as PolkadotXCMTransferInput + + expect(() => assetHub.transferPolkadotXCM(input)).toThrow(ScenarioNotSupportedError) + }) + + it('should process a valid transfer for non-ParaToPara scenario', () => { + const mockResult = {} as Extrinsic + const spy = vi + .spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + .mockReturnValue(mockResult) + const input = { + ...mockInput, + scenario: 'RelayToPara' + } as PolkadotXCMTransferInput + + const result = assetHub.transferPolkadotXCM(input) + expect(result).toStrictEqual(mockResult) + expect(spy).toHaveBeenCalledTimes(1) + }) }) }) diff --git a/packages/sdk/src/nodes/supported/AssetHubPolkadot.ts b/packages/sdk/src/nodes/supported/AssetHubPolkadot.ts index 9ce700ca..491c1ac6 100644 --- a/packages/sdk/src/nodes/supported/AssetHubPolkadot.ts +++ b/packages/sdk/src/nodes/supported/AssetHubPolkadot.ts @@ -1,4 +1,4 @@ -// Contains detailed structure of XCM call construction for Statemint Parachain +// Contains detailed structure of XCM call construction for AssetHubPolkadot Parachain import { ethers } from 'ethers' import { InvalidCurrencyError, ScenarioNotSupportedError } from '../../errors' @@ -22,10 +22,11 @@ import { type TJunction, Junctions } from '../../types' -import { generateAddressMultiLocationV4, generateAddressPayload } from '../../utils' import ParachainNode from '../ParachainNode' import PolkadotXCMTransferImpl from '../polkadotXcm' import { getOtherAssets, getParaId } from '../../pallets/assets' +import { generateAddressMultiLocationV4 } from '../../utils/generateAddressMultiLocationV4' +import { generateAddressPayload } from '../../utils/generateAddressPayload' class AssetHubPolkadot extends ParachainNode implements IPolkadotXCMTransfer { constructor() { diff --git a/packages/sdk/src/nodes/supported/Astar.test.ts b/packages/sdk/src/nodes/supported/Astar.test.ts new file mode 100644 index 00000000..acb07b40 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Astar.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput, PolkadotXCMTransferInput } from '../../types' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import XTokensTransferImpl from '../xTokens' +import Astar from './Astar' +import { getNode } from '../../utils' + +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Astar', () => { + let astar: Astar + const mockPolkadotXCMInput = { + scenario: 'ParaToPara', + currencySymbol: 'DOT', + amount: '100' + } as PolkadotXCMTransferInput + + const mockXTokensInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + astar = getNode('Astar') + }) + + it('should initialize with correct values', () => { + expect(astar.node).toBe('Astar') + expect(astar.name).toBe('astar') + expect(astar.type).toBe('polkadot') + expect(astar.version).toBe(Version.V3) + }) + + it('should call transferPolkadotXCM with reserveTransferAssets for ParaToPara scenario', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + + astar.transferPolkadotXCM(mockPolkadotXCMInput) + + expect(spy).toHaveBeenCalledWith(mockPolkadotXCMInput, 'reserveTransferAssets') + }) + + it('should call transferPolkadotXCM with reserveWithdrawAssets for non-ParaToPara scenario', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + const inputWithDifferentScenario = { + ...mockPolkadotXCMInput, + scenario: 'RelayToPara' + } as PolkadotXCMTransferInput + + astar.transferPolkadotXCM(inputWithDifferentScenario) + + expect(spy).toHaveBeenCalledWith(inputWithDifferentScenario, 'reserveWithdrawAssets') + }) + + it('should call transferXTokens with currencyID', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + astar.transferXTokens(mockXTokensInput) + + expect(spy).toHaveBeenCalledWith(mockXTokensInput, '123') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Bajun.test.ts b/packages/sdk/src/nodes/supported/Bajun.test.ts new file mode 100644 index 00000000..bf2331c9 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Bajun.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { + ScenarioNotSupportedError, + InvalidCurrencyError, + NodeNotSupportedError +} from '../../errors' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Bajun from './Bajun' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Bajun', () => { + let bajun: Bajun + const mockInput = { + scenario: 'ParaToPara', + currency: 'BAJ', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + bajun = getNode('Bajun') + }) + + it('should initialize with correct values', () => { + expect(bajun.node).toBe('Bajun') + expect(bajun.name).toBe('bajun') + expect(bajun.type).toBe('kusama') + expect(bajun.version).toBe(Version.V3) + }) + + it('should throw ScenarioNotSupportedError for unsupported scenario', () => { + const invalidInput = { ...mockInput, scenario: 'RelayToPara' } as XTokensTransferInput + + expect(() => bajun.transferXTokens(invalidInput)).toThrowError( + new ScenarioNotSupportedError(bajun.node, 'RelayToPara') + ) + }) + + it('should throw InvalidCurrencyError when currency does not match native asset', () => { + const invalidInput = { ...mockInput, currency: 'INVALID' } + vi.spyOn(bajun, 'getNativeAssetSymbol').mockReturnValue('BAJ') + + expect(() => bajun.transferXTokens(invalidInput)).toThrowError( + new InvalidCurrencyError('Node Bajun does not support currency INVALID') + ) + }) + + it('should call transferXTokens with the correct currency', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(bajun, 'getNativeAssetSymbol').mockReturnValue('BAJ') + + bajun.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'BAJ') + }) + + it('should throw NodeNotSupportedError when calling transferRelayToPara', () => { + expect(() => bajun.transferRelayToPara()).toThrowError(NodeNotSupportedError) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Basilisk.test.ts b/packages/sdk/src/nodes/supported/Basilisk.test.ts index c5f6b21d..93f56c7a 100644 --- a/packages/sdk/src/nodes/supported/Basilisk.test.ts +++ b/packages/sdk/src/nodes/supported/Basilisk.test.ts @@ -1,22 +1,53 @@ -import { describe, it, expect, beforeEach } from 'vitest' -import { getNode } from '../../utils' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput, TNodePolkadotKusama } from '../../types' +import XTokensTransferImpl from '../xTokens' +import { getAllNodeProviders } from '../../utils' +import { getNode } from '../../utils/getNode' import Basilisk from './Basilisk' +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +vi.mock('../../utils', () => ({ + getAllNodeProviders: vi.fn() +})) + describe('Basilisk', () => { let basilisk: Basilisk + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + const mockProviders = ['wss://non-preferred-rpc', 'wss://preferred-dwellir-rpc'] beforeEach(() => { basilisk = getNode('Basilisk') + vi.mocked(getAllNodeProviders).mockReturnValue(mockProviders) + }) + + it('should initialize with correct values', () => { + expect(basilisk.node).toBe('Basilisk') + expect(basilisk.name).toBe('basilisk') + expect(basilisk.type).toBe('kusama') + expect(basilisk.version).toBe(Version.V3) }) - it('should be instantiated correctly', () => { - expect(basilisk).toBeInstanceOf(Basilisk) + it('should call transferXTokens with currencyID', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + basilisk.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, '123') }) - describe('getProvider', () => { - it('should return the correct provider URL', () => { - const provider = basilisk.getProvider() - expect(provider).toContain('dwellir') - }) + it('should return the second provider URL from getProvider', () => { + const provider = basilisk.getProvider() + + expect(getAllNodeProviders).toHaveBeenCalledWith(basilisk.node as TNodePolkadotKusama) + expect(provider).toBe('wss://preferred-dwellir-rpc') }) }) diff --git a/packages/sdk/src/nodes/supported/BifrostKusama.test.ts b/packages/sdk/src/nodes/supported/BifrostKusama.test.ts new file mode 100644 index 00000000..beca5859 --- /dev/null +++ b/packages/sdk/src/nodes/supported/BifrostKusama.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import BifrostKusama from './BifrostKusama' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('BifrostKusama', () => { + let bifrostKusama: BifrostKusama + const mockInput = { + currency: 'BNC', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + bifrostKusama = getNode('BifrostKusama') + }) + + it('should initialize with correct values', () => { + expect(bifrostKusama.node).toBe('BifrostKusama') + expect(bifrostKusama.name).toBe('bifrost') + expect(bifrostKusama.type).toBe('kusama') + expect(bifrostKusama.version).toBe(Version.V3) + }) + + it('should call transferXTokens with Native when currency matches native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(bifrostKusama, 'getNativeAssetSymbol').mockReturnValue('BNC') + + bifrostKusama.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { Native: 'BNC' }) + }) + + it('should call transferXTokens with Token when currency does not match native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(bifrostKusama, 'getNativeAssetSymbol').mockReturnValue('NOT_BNC') + + bifrostKusama.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { Token: 'BNC' }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/BifrostPolkadot.test.ts b/packages/sdk/src/nodes/supported/BifrostPolkadot.test.ts new file mode 100644 index 00000000..8eb1f768 --- /dev/null +++ b/packages/sdk/src/nodes/supported/BifrostPolkadot.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import { BifrostPolkadot } from './BifrostPolkadot' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('BifrostPolkadot', () => { + let bifrostPolkadot: BifrostPolkadot + const mockInput = { + currency: 'BNC', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + bifrostPolkadot = getNode('BifrostPolkadot') + }) + + it('should initialize with correct values', () => { + expect(bifrostPolkadot.node).toBe('BifrostPolkadot') + expect(bifrostPolkadot.name).toBe('bifrost') + expect(bifrostPolkadot.type).toBe('polkadot') + expect(bifrostPolkadot.version).toBe(Version.V3) + }) + + it('should call transferXTokens with Native when currency matches native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(bifrostPolkadot, 'getNativeAssetSymbol').mockReturnValue('BNC') + + bifrostPolkadot.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { Native: 'BNC' }) + }) + + it('should call transferXTokens with Token when currency does not match native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(bifrostPolkadot, 'getNativeAssetSymbol').mockReturnValue('NOT_BNC') + + bifrostPolkadot.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { Token: 'BNC' }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/BridgeHubKusama.test.ts b/packages/sdk/src/nodes/supported/BridgeHubKusama.test.ts new file mode 100644 index 00000000..fe2ff05a --- /dev/null +++ b/packages/sdk/src/nodes/supported/BridgeHubKusama.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ScenarioNotSupportedError } from '../../errors' +import { constructRelayToParaParameters } from '../../pallets/xcmPallet/utils' +import { PolkadotXCMTransferInput, TRelayToParaInternalOptions, Version } from '../../types' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import BridgeHubKusama from './BridgeHubKusama' +import { getNode } from '../../utils' + +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + +vi.mock('../../pallets/xcmPallet/utils', () => ({ + constructRelayToParaParameters: vi.fn() +})) + +describe('BridgeHubKusama', () => { + let bridgeHubKusama: BridgeHubKusama + const mockInput = { + scenario: 'RelayToPara', + currencySymbol: 'KSM', + amount: '100' + } as PolkadotXCMTransferInput + + const mockOptions = { + destination: 'BridgeHubKusama' + } as TRelayToParaInternalOptions + + beforeEach(() => { + bridgeHubKusama = getNode('BridgeHubKusama') + }) + + it('should initialize with correct values', () => { + expect(bridgeHubKusama.node).toBe('BridgeHubKusama') + expect(bridgeHubKusama.name).toBe('kusamaBridgeHub') + expect(bridgeHubKusama.type).toBe('kusama') + expect(bridgeHubKusama.version).toBe(Version.V3) + expect(bridgeHubKusama._assetCheckEnabled).toBe(false) + }) + + it('should throw ScenarioNotSupportedError for ParaToPara scenario', () => { + const invalidInput = { ...mockInput, scenario: 'ParaToPara' } as PolkadotXCMTransferInput + + expect(() => bridgeHubKusama.transferPolkadotXCM(invalidInput)).toThrowError( + new ScenarioNotSupportedError( + bridgeHubKusama.node, + 'ParaToPara', + 'Unable to use bridge hub for transfers to other Parachains. Please move your currency to AssetHub to transfer to other Parachains.' + ) + ) + }) + + it('should call transferPolkadotXCM with limitedTeleportAssets for non-ParaToPara scenario', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + + bridgeHubKusama.transferPolkadotXCM(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'limitedTeleportAssets', 'Unlimited') + }) + + it('should call transferRelayToPara with the correct parameters', () => { + const expectedParameters = [{ param: 'value' }] as unknown[] + vi.mocked(constructRelayToParaParameters).mockReturnValue(expectedParameters) + + const result = bridgeHubKusama.transferRelayToPara(mockOptions) + + expect(constructRelayToParaParameters).toHaveBeenCalledWith(mockOptions, Version.V3, true) + expect(result).toEqual({ + module: 'xcmPallet', + section: 'limitedTeleportAssets', + parameters: expectedParameters + }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/BridgeHubPolkadot.test.ts b/packages/sdk/src/nodes/supported/BridgeHubPolkadot.test.ts new file mode 100644 index 00000000..e13c6a70 --- /dev/null +++ b/packages/sdk/src/nodes/supported/BridgeHubPolkadot.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ScenarioNotSupportedError } from '../../errors' +import { constructRelayToParaParameters } from '../../pallets/xcmPallet/utils' +import { PolkadotXCMTransferInput, TRelayToParaInternalOptions, Version } from '../../types' +import BridgeHubPolkadot from './BridgeHubPolkadot' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import { getNode } from '../../utils' + +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + +vi.mock('../../pallets/xcmPallet/utils', () => ({ + constructRelayToParaParameters: vi.fn() +})) + +describe('BridgeHubPolkadot', () => { + let bridgeHubPolkadot: BridgeHubPolkadot + const mockInput = { + scenario: 'RelayToPara', + currencySymbol: 'DOT', + amount: '100' + } as PolkadotXCMTransferInput + + const mockOptions = { + destination: 'BridgeHubPolkadot' + } as TRelayToParaInternalOptions + + beforeEach(() => { + bridgeHubPolkadot = getNode('BridgeHubPolkadot') + }) + + it('should initialize with correct values', () => { + expect(bridgeHubPolkadot.node).toBe('BridgeHubPolkadot') + expect(bridgeHubPolkadot.name).toBe('polkadotBridgeHub') + expect(bridgeHubPolkadot.type).toBe('polkadot') + expect(bridgeHubPolkadot.version).toBe(Version.V3) + expect(bridgeHubPolkadot._assetCheckEnabled).toBe(false) + }) + + it('should throw ScenarioNotSupportedError for ParaToPara scenario', () => { + const invalidInput = { ...mockInput, scenario: 'ParaToPara' } as PolkadotXCMTransferInput + + expect(() => bridgeHubPolkadot.transferPolkadotXCM(invalidInput)).toThrowError( + new ScenarioNotSupportedError( + bridgeHubPolkadot.node, + 'ParaToPara', + 'Unable to use bridge hub for transfers to other Parachains. Please move your currency to AssetHub to transfer to other Parachains.' + ) + ) + }) + + it('should call transferPolkadotXCM with limitedTeleportAssets for non-ParaToPara scenario', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + + bridgeHubPolkadot.transferPolkadotXCM(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'limitedTeleportAssets', 'Unlimited') + }) + + it('should call transferRelayToPara with the correct parameters', () => { + const expectedParameters = [{ param: 'value' }] as unknown[] + vi.mocked(constructRelayToParaParameters).mockReturnValue(expectedParameters) + + const result = bridgeHubPolkadot.transferRelayToPara(mockOptions) + + expect(constructRelayToParaParameters).toHaveBeenCalledWith(mockOptions, Version.V3, true) + expect(result).toEqual({ + module: 'xcmPallet', + section: 'limitedTeleportAssets', + parameters: expectedParameters + }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Calamari.test.ts b/packages/sdk/src/nodes/supported/Calamari.test.ts new file mode 100644 index 00000000..f2eae78b --- /dev/null +++ b/packages/sdk/src/nodes/supported/Calamari.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput, TMantaAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Calamari from './Calamari' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Calamari', () => { + let calamari: Calamari + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + calamari = getNode('Calamari') + }) + + it('should initialize with correct values', () => { + expect(calamari.node).toBe('Calamari') + expect(calamari.name).toBe('calamari') + expect(calamari.type).toBe('kusama') + expect(calamari.version).toBe(Version.V3) + }) + + it('should call transferXTokens with MantaCurrency', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + calamari.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { MantaCurrency: '123' } as TMantaAsset) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Centrifuge.test.ts b/packages/sdk/src/nodes/supported/Centrifuge.test.ts new file mode 100644 index 00000000..0c361545 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Centrifuge.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import { Centrifuge } from './Centrifuge' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Centrifuge', () => { + let centrifuge: Centrifuge + const mockInput = { + currency: 'CFG', + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + centrifuge = getNode('Centrifuge') + }) + + it('should initialize with correct values', () => { + expect(centrifuge.node).toBe('Centrifuge') + expect(centrifuge.name).toBe('centrifuge') + expect(centrifuge.type).toBe('polkadot') + expect(centrifuge.version).toBe(Version.V3) + }) + + it('should call transferXTokens with Native when currency matches native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(centrifuge, 'getNativeAssetSymbol').mockReturnValue('CFG') + + centrifuge.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'Native') + }) + + it('should call transferXTokens with ForeignAsset when currency does not match native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(centrifuge, 'getNativeAssetSymbol').mockReturnValue('NOT_CFG') + + centrifuge.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { ForeignAsset: '123' }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Collectives.test.ts b/packages/sdk/src/nodes/supported/Collectives.test.ts new file mode 100644 index 00000000..d6b69da2 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Collectives.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ScenarioNotSupportedError } from '../../errors' +import { constructRelayToParaParameters } from '../../pallets/xcmPallet/utils' +import { PolkadotXCMTransferInput, TRelayToParaInternalOptions, Version } from '../../types' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import Collectives from './Collectives' +import { getNode } from '../../utils/getNode' + +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + +vi.mock('../../pallets/xcmPallet/utils', () => ({ + constructRelayToParaParameters: vi.fn() +})) + +describe('Collectives', () => { + let collectives: Collectives + const mockInput = { + scenario: 'RelayToPara', + currencySymbol: 'DOT', + amount: '100' + } as PolkadotXCMTransferInput + + const mockOptions = { + destination: 'Collectives' + } as TRelayToParaInternalOptions + + beforeEach(() => { + collectives = getNode('Collectives') + }) + + it('should initialize with correct values', () => { + expect(collectives.node).toBe('Collectives') + expect(collectives.name).toBe('polkadotCollectives') + expect(collectives.type).toBe('polkadot') + expect(collectives.version).toBe(Version.V3) + }) + + it('should throw ScenarioNotSupportedError for ParaToPara scenario', () => { + const invalidInput = { ...mockInput, scenario: 'ParaToPara' } as PolkadotXCMTransferInput + + expect(() => collectives.transferPolkadotXCM(invalidInput)).toThrowError( + ScenarioNotSupportedError + ) + }) + + it('should call transferPolkadotXCM with limitedTeleportAssets for non-ParaToPara scenario', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + + collectives.transferPolkadotXCM(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'limitedTeleportAssets', 'Unlimited') + }) + + it('should call transferRelayToPara with the correct parameters', () => { + const expectedParameters = [{ param: 'value' }] as unknown[] + vi.mocked(constructRelayToParaParameters).mockReturnValue(expectedParameters) + + const result = collectives.transferRelayToPara(mockOptions) + + expect(constructRelayToParaParameters).toHaveBeenCalledWith(mockOptions, Version.V3, true) + expect(result).toEqual({ + module: 'xcmPallet', + section: 'limitedTeleportAssets', + parameters: expectedParameters + }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/ComposableFinance.test.ts b/packages/sdk/src/nodes/supported/ComposableFinance.test.ts new file mode 100644 index 00000000..dfda3786 --- /dev/null +++ b/packages/sdk/src/nodes/supported/ComposableFinance.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import ComposableFinance from './ComposableFinance' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('ComposableFinance', () => { + let composableFinance: ComposableFinance + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + composableFinance = getNode('ComposableFinance') + }) + + it('should initialize with correct values', () => { + expect(composableFinance.node).toBe('ComposableFinance') + expect(composableFinance.name).toBe('composable') + expect(composableFinance.type).toBe('polkadot') + expect(composableFinance.version).toBe(Version.V3) + }) + + it('should call transferXTokens with currencyID', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + composableFinance.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, '123') + }) +}) diff --git a/packages/sdk/src/nodes/supported/CoretimeKusama.test.ts b/packages/sdk/src/nodes/supported/CoretimeKusama.test.ts new file mode 100644 index 00000000..d6e96a59 --- /dev/null +++ b/packages/sdk/src/nodes/supported/CoretimeKusama.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { constructRelayToParaParameters } from '../../pallets/xcmPallet/utils' +import { PolkadotXCMTransferInput, TRelayToParaInternalOptions, Version } from '../../types' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import CoretimeKusama from './CoretimeKusama' +import { getNode } from '../../utils' + +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + +vi.mock('../../pallets/xcmPallet/utils', () => ({ + constructRelayToParaParameters: vi.fn() +})) + +describe('CoretimeKusama', () => { + let coretimeKusama: CoretimeKusama + const mockInput = { + scenario: 'ParaToPara', + currencySymbol: 'KSM', + amount: '100' + } as PolkadotXCMTransferInput + + const mockOptions = { + destination: 'CoretimeKusama' + } as TRelayToParaInternalOptions + + beforeEach(() => { + coretimeKusama = getNode('CoretimeKusama') + }) + + it('should initialize with correct values including assetCheckDisabled', () => { + expect(coretimeKusama.node).toBe('CoretimeKusama') + expect(coretimeKusama.name).toBe('kusamaCoretime') + expect(coretimeKusama.type).toBe('kusama') + expect(coretimeKusama.version).toBe(Version.V3) + expect(coretimeKusama._assetCheckEnabled).toBe(false) + }) + + it('should call transferPolkadotXCM with limitedReserveTransferAssets for ParaToPara scenario', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + + coretimeKusama.transferPolkadotXCM(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'limitedReserveTransferAssets', 'Unlimited') + }) + + it('should call transferPolkadotXCM with limitedTeleportAssets for non-ParaToPara scenario', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + const inputWithDifferentScenario = { + ...mockInput, + scenario: 'RelayToPara' + } as PolkadotXCMTransferInput + + coretimeKusama.transferPolkadotXCM(inputWithDifferentScenario) + + expect(spy).toHaveBeenCalledWith( + inputWithDifferentScenario, + 'limitedTeleportAssets', + 'Unlimited' + ) + }) + + it('should call transferRelayToPara with the correct parameters', () => { + const expectedParameters = [{ param: 'value' }] as unknown[] + vi.mocked(constructRelayToParaParameters).mockReturnValue(expectedParameters) + + const result = coretimeKusama.transferRelayToPara(mockOptions) + + expect(constructRelayToParaParameters).toHaveBeenCalledWith(mockOptions, Version.V3, true) + expect(result).toEqual({ + module: 'xcmPallet', + section: 'limitedTeleportAssets', + parameters: expectedParameters + }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/CoretimePolkadot.test.ts b/packages/sdk/src/nodes/supported/CoretimePolkadot.test.ts index 32bc98dd..e71db2a5 100644 --- a/packages/sdk/src/nodes/supported/CoretimePolkadot.test.ts +++ b/packages/sdk/src/nodes/supported/CoretimePolkadot.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/unbound-method */ import { describe, it, expect, beforeEach, vi } from 'vitest' import { Extrinsic, @@ -48,25 +47,22 @@ describe('CoretimePolkadot', () => { const mockResult = {} as Extrinsic - vi.mocked(PolkadotXCMTransferImpl.transferPolkadotXCM).mockResolvedValue(mockResult) + const spy = vi + .spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + .mockResolvedValue(mockResult) coretimePolkadot.transferPolkadotXCM(input) - expect(PolkadotXCMTransferImpl.transferPolkadotXCM).toHaveBeenCalledWith( - input, - 'limitedReserveTransferAssets', - 'Unlimited' - ) + expect(spy).toHaveBeenCalledWith(input, 'limitedReserveTransferAssets', 'Unlimited') }) it('should use limitedTeleportAssets when scenario is not ParaToPara', () => { const input = { scenario: 'ParaToRelay' } as PolkadotXCMTransferInput + + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + coretimePolkadot.transferPolkadotXCM(input) - expect(PolkadotXCMTransferImpl.transferPolkadotXCM).toHaveBeenCalledWith( - input, - 'limitedTeleportAssets', - 'Unlimited' - ) + expect(spy).toHaveBeenCalledWith(input, 'limitedTeleportAssets', 'Unlimited') }) }) diff --git a/packages/sdk/src/nodes/supported/Crust.test.ts b/packages/sdk/src/nodes/supported/Crust.test.ts new file mode 100644 index 00000000..1debf789 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Crust.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { InvalidCurrencyError } from '../../errors/InvalidCurrencyError' +import { Version, XTokensTransferInput, TReserveAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Crust from './Crust' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Crust', () => { + let crust: Crust + const mockInput = { + currency: 'CRU', + currencyID: '456', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + crust = getNode('Crust') + }) + + it('should initialize with correct values', () => { + expect(crust.node).toBe('Crust') + expect(crust.name).toBe('crustParachain') + expect(crust.type).toBe('polkadot') + expect(crust.version).toBe(Version.V3) + }) + + it('should call transferXTokens with SelfReserve when currency matches native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(crust, 'getNativeAssetSymbol').mockReturnValue('CRU') + + crust.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'SelfReserve' as TReserveAsset) + }) + + it('should call transferXTokens with OtherReserve when currencyID is defined and currency does not match native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(crust, 'getNativeAssetSymbol').mockReturnValue('NOT_CRU') + + crust.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { OtherReserve: '456' } as TReserveAsset) + }) + + it('should throw InvalidCurrencyError when currencyID is undefined and currency does not match native asset', () => { + const invalidInput = { ...mockInput, currencyID: undefined } + vi.spyOn(crust, 'getNativeAssetSymbol').mockReturnValue('NOT_CRU') + + expect(() => crust.transferXTokens(invalidInput)).toThrowError( + new InvalidCurrencyError('Asset CRU is not supported by node Crust.') + ) + }) +}) diff --git a/packages/sdk/src/nodes/supported/CrustShadow.test.ts b/packages/sdk/src/nodes/supported/CrustShadow.test.ts new file mode 100644 index 00000000..9deebdb6 --- /dev/null +++ b/packages/sdk/src/nodes/supported/CrustShadow.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { InvalidCurrencyError } from '../../errors/InvalidCurrencyError' +import { Version, XTokensTransferInput, TReserveAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import CrustShadow from './CrustShadow' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('CrustShadow', () => { + let crustShadow: CrustShadow + const mockInput = { + currency: 'CRU', + currencyID: '456', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + crustShadow = getNode('CrustShadow') + }) + + it('should initialize with correct values', () => { + expect(crustShadow.node).toBe('CrustShadow') + expect(crustShadow.name).toBe('shadow') + expect(crustShadow.type).toBe('kusama') + expect(crustShadow.version).toBe(Version.V3) + }) + + it('should call transferXTokens with SelfReserve when currency matches native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(crustShadow, 'getNativeAssetSymbol').mockReturnValue('CRU') + + crustShadow.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'SelfReserve' as TReserveAsset) + }) + + it('should call transferXTokens with OtherReserve when currencyID is defined and currency does not match native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(crustShadow, 'getNativeAssetSymbol').mockReturnValue('NOT_CRU') + + crustShadow.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { OtherReserve: '456' } as TReserveAsset) + }) + + it('should throw InvalidCurrencyError when currencyID is undefined and currency does not match native asset', () => { + const invalidInput = { ...mockInput, currencyID: undefined } + vi.spyOn(crustShadow, 'getNativeAssetSymbol').mockReturnValue('NOT_CRU') + + expect(() => crustShadow.transferXTokens(invalidInput)).toThrowError( + new InvalidCurrencyError('Asset CRU is not supported by node CrustShadow.') + ) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Curio.test.ts b/packages/sdk/src/nodes/supported/Curio.test.ts new file mode 100644 index 00000000..effcb303 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Curio.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput, TForeignOrTokenAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Curio from './Curio' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Curio', () => { + let curio: Curio + const mockInput = { + currency: 'CUR', + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + curio = getNode('Curio') + }) + + it('should initialize with correct values', () => { + expect(curio.node).toBe('Curio') + expect(curio.name).toBe('curio') + expect(curio.type).toBe('kusama') + expect(curio.version).toBe(Version.V3) + }) + + it('should call transferXTokens with ForeignAsset when currencyID is defined', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + curio.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { ForeignAsset: '123' } as TForeignOrTokenAsset) + }) + + it('should call transferXTokens with Token when currencyID is undefined', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + const inputWithoutCurrencyID = { ...mockInput, currencyID: undefined } + + curio.transferXTokens(inputWithoutCurrencyID) + + expect(spy).toHaveBeenCalledWith(inputWithoutCurrencyID, { + Token: 'CUR' + } as TForeignOrTokenAsset) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Darwinia.test.ts b/packages/sdk/src/nodes/supported/Darwinia.test.ts new file mode 100644 index 00000000..a8902cbc --- /dev/null +++ b/packages/sdk/src/nodes/supported/Darwinia.test.ts @@ -0,0 +1,99 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { + Version, + XTokensTransferInput, + TScenario, + TSelfReserveAsset, + TForeignAsset, + TCurrencySelectionHeaderArr, + Parents +} from '../../types' +import { NodeNotSupportedError } from '../../errors' +import XTokensTransferImpl from '../xTokens' +import Darwinia from './Darwinia' +import { createCurrencySpec } from '../../pallets/xcmPallet/utils' +import { getNode } from '../../utils' +import ParachainNode from '../ParachainNode' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +vi.mock('../../pallets/xcmPallet/utils', () => ({ + createCurrencySpec: vi.fn() +})) + +describe('Darwinia', () => { + let darwinia: Darwinia + const mockInput = { + currency: 'RING', + currencyID: '456', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + darwinia = getNode('Darwinia') + }) + + it('should initialize with correct values', () => { + expect(darwinia.node).toBe('Darwinia') + expect(darwinia.name).toBe('darwinia') + expect(darwinia.type).toBe('polkadot') + expect(darwinia.version).toBe(Version.V3) + }) + + it('should call transferXTokens with SelfReserve when currency matches native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(darwinia, 'getNativeAssetSymbol').mockReturnValue('RING') + + darwinia.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'SelfReserve' as TSelfReserveAsset) + }) + + it('should call transferXTokens with ForeignAsset when currency does not match native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(darwinia, 'getNativeAssetSymbol').mockReturnValue('NOT_RING') + + darwinia.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { ForeignAsset: '456' } as TForeignAsset) + }) + + it('should throw NodeNotSupportedError for transferRelayToPara', () => { + expect(() => darwinia.transferRelayToPara()).toThrowError(NodeNotSupportedError) + }) + + it('should call createCurrencySpec with PalletInstance 5 for ParaToPara scenario', () => { + const expectedSpec = { param: 'value' } as TCurrencySelectionHeaderArr + const mockScenario: TScenario = 'ParaToPara' + const mockAmount = '100' + const mockVersion = Version.V3 + vi.mocked(createCurrencySpec).mockReturnValue(expectedSpec) + + const result = darwinia.createCurrencySpec(mockAmount, mockScenario, mockVersion) + + expect(createCurrencySpec).toHaveBeenCalledWith( + mockAmount, + mockVersion, + Parents.ZERO, + undefined, + { X1: { PalletInstance: 5 } } + ) + expect(result).toEqual(expectedSpec) + }) + + it('should call the superclass createCurrencySpec for non-ParaToPara scenarios', () => { + const spy = vi.spyOn(ParachainNode.prototype, 'createCurrencySpec') + + const mockScenario: TScenario = 'RelayToPara' + const mockAmount = '100' + const mockVersion = Version.V3 + + darwinia.createCurrencySpec(mockAmount, mockScenario, mockVersion) + + expect(spy).toHaveBeenCalledWith(mockAmount, mockScenario, mockVersion, undefined) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Encointer.test.ts b/packages/sdk/src/nodes/supported/Encointer.test.ts new file mode 100644 index 00000000..013280a9 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Encointer.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ScenarioNotSupportedError } from '../../errors/ScenarioNotSupportedError' +import { Version, PolkadotXCMTransferInput, TRelayToParaInternalOptions } from '../../types' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import Encointer from './Encointer' +import { constructRelayToParaParameters } from '../../pallets/xcmPallet/utils' +import { getNode } from '../../utils' + +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + +vi.mock('../../pallets/xcmPallet/utils', () => ({ + constructRelayToParaParameters: vi.fn() +})) + +describe('Encointer', () => { + let encointer: Encointer + const mockInput = { + scenario: 'ParaToRelay', + currencySymbol: 'KSM', + amount: '100' + } as PolkadotXCMTransferInput + + const mockOptions = { + destination: 'Encointer' + } as TRelayToParaInternalOptions + + beforeEach(() => { + encointer = getNode('Encointer') + }) + + it('should initialize with correct values', () => { + expect(encointer.node).toBe('Encointer') + expect(encointer.name).toBe('encointer') + expect(encointer.type).toBe('kusama') + expect(encointer.version).toBe(Version.V3) + }) + + it('should call transferPolkadotXCM with limitedTeleportAssets for ParaToRelay scenario', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + + encointer.transferPolkadotXCM(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'limitedTeleportAssets', 'Unlimited') + }) + + it('should throw ScenarioNotSupportedError for unsupported scenario', () => { + const invalidInput = { ...mockInput, scenario: 'ParaToPara' } as PolkadotXCMTransferInput + expect(() => encointer.transferPolkadotXCM(invalidInput)).toThrowError( + new ScenarioNotSupportedError(encointer.node, 'ParaToPara') + ) + }) + + it('should call transferRelayToPara with the correct parameters', () => { + const expectedParameters = [{ param: 'value' }] as unknown[] + vi.mocked(constructRelayToParaParameters).mockReturnValue(expectedParameters) + + const result = encointer.transferRelayToPara(mockOptions) + + expect(constructRelayToParaParameters).toHaveBeenCalledWith(mockOptions, Version.V1, true) + expect(result).toEqual({ + module: 'xcmPallet', + section: 'limitedTeleportAssets', + parameters: expectedParameters + }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Hydration.test.ts b/packages/sdk/src/nodes/supported/Hydration.test.ts new file mode 100644 index 00000000..e65cd80a --- /dev/null +++ b/packages/sdk/src/nodes/supported/Hydration.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Hydration from './Hydration' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Hydration', () => { + let hydration: Hydration + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + hydration = getNode('Hydration') + }) + + it('should initialize with correct values', () => { + expect(hydration.node).toBe('Hydration') + expect(hydration.name).toBe('hydradx') + expect(hydration.type).toBe('polkadot') + expect(hydration.version).toBe(Version.V3) + }) + + it('should call transferXTokens with currencyID', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + hydration.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, '123') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Imbue.test.ts b/packages/sdk/src/nodes/supported/Imbue.test.ts new file mode 100644 index 00000000..ad5287fe --- /dev/null +++ b/packages/sdk/src/nodes/supported/Imbue.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Imbue from './Imbue' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Imbue', () => { + let imbue: Imbue + const mockInput = { + currency: 'IMBU', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + imbue = getNode('Imbue') + }) + + it('should initialize with correct values', () => { + expect(imbue.node).toBe('Imbue') + expect(imbue.name).toBe('imbue') + expect(imbue.type).toBe('kusama') + expect(imbue.version).toBe(Version.V3) + }) + + it('should call transferXTokens with currency', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + imbue.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'IMBU') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Integritee.test.ts b/packages/sdk/src/nodes/supported/Integritee.test.ts new file mode 100644 index 00000000..fe7c3a4b --- /dev/null +++ b/packages/sdk/src/nodes/supported/Integritee.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { InvalidCurrencyError, NodeNotSupportedError } from '../../errors' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Integritee from './Integritee' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Integritee', () => { + let integritee: Integritee + const mockInput = { + currency: 'TEER', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + integritee = getNode('Integritee') + }) + + it('should initialize with correct values', () => { + expect(integritee.node).toBe('Integritee') + expect(integritee.name).toBe('integritee') + expect(integritee.type).toBe('kusama') + expect(integritee.version).toBe(Version.V3) + }) + + it('should call transferXTokens with valid currency', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + integritee.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'TEER') + }) + + it('should throw InvalidCurrencyError for unsupported currency KSM', () => { + const invalidInput = { ...mockInput, currency: 'KSM' } + + expect(() => integritee.transferXTokens(invalidInput)).toThrowError( + new InvalidCurrencyError('Node Integritee does not support currency KSM') + ) + }) + + it('should throw NodeNotSupportedError for transferRelayToPara', () => { + expect(() => integritee.transferRelayToPara()).toThrowError(NodeNotSupportedError) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Interlay.test.ts b/packages/sdk/src/nodes/supported/Interlay.test.ts new file mode 100644 index 00000000..a84e3240 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Interlay.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput, TForeignOrTokenAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Interlay from './Interlay' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Interlay', () => { + let interlay: Interlay + const mockInput = { + currency: 'INTR', + currencyID: '456', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + interlay = getNode('Interlay') + }) + + it('should initialize with correct values', () => { + expect(interlay.node).toBe('Interlay') + expect(interlay.name).toBe('interlay') + expect(interlay.type).toBe('polkadot') + expect(interlay.version).toBe(Version.V3) + }) + + it('should call transferXTokens with ForeignAsset when currencyID is defined', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + interlay.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { ForeignAsset: '456' } as TForeignOrTokenAsset) + }) + + it('should call transferXTokens with Token when currencyID is undefined', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + const inputWithoutCurrencyID = { ...mockInput, currencyID: undefined } + + interlay.transferXTokens(inputWithoutCurrencyID) + + expect(spy).toHaveBeenCalledWith(inputWithoutCurrencyID, { + Token: 'INTR' + } as TForeignOrTokenAsset) + }) +}) diff --git a/packages/sdk/src/nodes/supported/InvArchTinker.test.ts b/packages/sdk/src/nodes/supported/InvArchTinker.test.ts new file mode 100644 index 00000000..77a6e1a4 --- /dev/null +++ b/packages/sdk/src/nodes/supported/InvArchTinker.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import InvArchTinker from './InvArchTinker' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('InvArchTinker', () => { + let invArchTinker: InvArchTinker + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + invArchTinker = getNode('InvArchTinker') + }) + + it('should initialize with correct values', () => { + expect(invArchTinker.node).toBe('InvArchTinker') + expect(invArchTinker.name).toBe('tinker') + expect(invArchTinker.type).toBe('kusama') + expect(invArchTinker.version).toBe(Version.V3) + }) + + it('should call transferXTokens with currencyID', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + invArchTinker.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, '123') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Khala.test.ts b/packages/sdk/src/nodes/supported/Khala.test.ts new file mode 100644 index 00000000..8df3e0e6 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Khala.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { InvalidCurrencyError } from '../../errors' +import { Version, XTransferTransferInput } from '../../types' +import XTransferTransferImpl from '../xTransfer' +import Khala from './Khala' +import { getNode } from '../../utils' + +vi.mock('../xTransfer', () => ({ + default: { + transferXTransfer: vi.fn() + } +})) + +describe('Khala', () => { + let khala: Khala + const mockInput = { + currency: 'PHA', + amount: '100' + } as XTransferTransferInput + + beforeEach(() => { + khala = getNode('Khala') + }) + + it('should initialize with correct values', () => { + expect(khala.node).toBe('Khala') + expect(khala.name).toBe('khala') + expect(khala.type).toBe('kusama') + expect(khala.version).toBe(Version.V3) + }) + + it('should call transferXTransfer with valid currency PHA', () => { + const spy = vi.spyOn(XTransferTransferImpl, 'transferXTransfer') + + khala.transferXTransfer(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput) + }) + + it('should throw InvalidCurrencyError for unsupported currency', () => { + const invalidInput = { ...mockInput, currency: 'INVALID' } + + expect(() => khala.transferXTransfer(invalidInput)).toThrowError( + new InvalidCurrencyError(`Node Khala does not support currency INVALID`) + ) + }) +}) diff --git a/packages/sdk/src/nodes/supported/KiltSpiritnet.test.ts b/packages/sdk/src/nodes/supported/KiltSpiritnet.test.ts new file mode 100644 index 00000000..fc48fd4d --- /dev/null +++ b/packages/sdk/src/nodes/supported/KiltSpiritnet.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ScenarioNotSupportedError, NodeNotSupportedError } from '../../errors' +import { Version, PolkadotXCMTransferInput } from '../../types' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import KiltSpiritnet from './KiltSpiritnet' +import { getNode } from '../../utils' + +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + +describe('KiltSpiritnet', () => { + let kiltSpiritnet: KiltSpiritnet + const mockInput = { + scenario: 'ParaToPara', + currencySymbol: 'KILT', + amount: '100' + } as PolkadotXCMTransferInput + + beforeEach(() => { + kiltSpiritnet = getNode('KiltSpiritnet') + }) + + it('should initialize with correct values', () => { + expect(kiltSpiritnet.node).toBe('KiltSpiritnet') + expect(kiltSpiritnet.name).toBe('kilt') + expect(kiltSpiritnet.type).toBe('polkadot') + expect(kiltSpiritnet.version).toBe(Version.V2) + }) + + it('should call transferPolkadotXCM with reserveTransferAssets', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + + kiltSpiritnet.transferPolkadotXCM(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'reserveTransferAssets') + }) + + it('should throw ScenarioNotSupportedError for unsupported scenario', () => { + const invalidInput = { ...mockInput, scenario: 'ParaToRelay' } as PolkadotXCMTransferInput + + expect(() => kiltSpiritnet.transferPolkadotXCM(invalidInput)).toThrowError( + ScenarioNotSupportedError + ) + }) + + it('should throw NodeNotSupportedError for transferRelayToPara', () => { + expect(() => kiltSpiritnet.transferRelayToPara()).toThrowError(NodeNotSupportedError) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Kintsugi.test.ts b/packages/sdk/src/nodes/supported/Kintsugi.test.ts new file mode 100644 index 00000000..cf954916 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Kintsugi.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput, TForeignOrTokenAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Kintsugi from './Kintsugi' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Kintsugi', () => { + let kintsugi: Kintsugi + const mockInput = { + currency: 'KINT', + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + kintsugi = getNode('Kintsugi') + }) + + it('should initialize with correct values', () => { + expect(kintsugi.node).toBe('Kintsugi') + expect(kintsugi.name).toBe('kintsugi') + expect(kintsugi.type).toBe('kusama') + expect(kintsugi.version).toBe(Version.V3) + }) + + it('should call transferXTokens with ForeignAsset when currencyID is defined', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + kintsugi.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { ForeignAsset: '123' } as TForeignOrTokenAsset) + }) + + it('should call transferXTokens with Token when currencyID is undefined', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + const inputWithoutCurrencyID = { ...mockInput, currencyID: undefined } + + kintsugi.transferXTokens(inputWithoutCurrencyID) + + expect(spy).toHaveBeenCalledWith(inputWithoutCurrencyID, { + Token: 'KINT' + } as TForeignOrTokenAsset) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Litentry.test.ts b/packages/sdk/src/nodes/supported/Litentry.test.ts new file mode 100644 index 00000000..9ffe4574 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Litentry.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput, TSelfReserveAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Litentry from './Litentry' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Litentry', () => { + let litentry: Litentry + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + litentry = getNode('Litentry') + }) + + it('should initialize with correct values', () => { + expect(litentry.node).toBe('Litentry') + expect(litentry.name).toBe('litentry') + expect(litentry.type).toBe('polkadot') + expect(litentry.version).toBe(Version.V3) + }) + + it('should call transferXTokens with SelfReserve', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + litentry.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'SelfReserve' as TSelfReserveAsset) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Manta.test.ts b/packages/sdk/src/nodes/supported/Manta.test.ts new file mode 100644 index 00000000..32fb3925 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Manta.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput, TMantaAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Manta from './Manta' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Manta', () => { + let manta: Manta + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + manta = getNode('Manta') + }) + + it('should initialize with correct values', () => { + expect(manta.node).toBe('Manta') + expect(manta.name).toBe('manta') + expect(manta.type).toBe('polkadot') + expect(manta.version).toBe(Version.V3) + }) + + it('should call transferXTokens with MantaCurrency selection', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + manta.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { MantaCurrency: '123' } as TMantaAsset) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Moonbeam.test.ts b/packages/sdk/src/nodes/supported/Moonbeam.test.ts new file mode 100644 index 00000000..5e35fd98 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Moonbeam.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { constructRelayToParaParameters } from '../../pallets/xcmPallet/utils' +import { getNode } from '../../utils' +import { getAllNodeProviders } from '../../utils/getAllNodeProviders' +import { Version, XTokensTransferInput, TRelayToParaInternalOptions } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Moonbeam from './Moonbeam' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +vi.mock('../../pallets/xcmPallet/utils', () => ({ + constructRelayToParaParameters: vi.fn() +})) + +vi.mock('../../utils/getAllNodeProviders', () => ({ + getAllNodeProviders: vi.fn() +})) + +describe('Moonbeam', () => { + let moonbeam: Moonbeam + const mockInput = { + currency: 'GLMR', + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + const mockOptions = { + destination: 'Moonbeam' + } as TRelayToParaInternalOptions + + beforeEach(() => { + moonbeam = getNode('Moonbeam') + }) + + it('should initialize with correct values', () => { + expect(moonbeam.node).toBe('Moonbeam') + expect(moonbeam.name).toBe('moonbeam') + expect(moonbeam.type).toBe('polkadot') + expect(moonbeam.version).toBe(Version.V3) + }) + + it('should call transferXTokens with SelfReserve when currency matches native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(moonbeam, 'getNativeAssetSymbol').mockReturnValue('GLMR') + + moonbeam.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'SelfReserve') + }) + + it('should call transferXTokens with ForeignAsset when currency does not match native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(moonbeam, 'getNativeAssetSymbol').mockReturnValue('NOT_GLMR') + + moonbeam.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { ForeignAsset: '123' }) + }) + + it('should call transferRelayToPara with the correct parameters', () => { + const expectedParameters = [{ param: 'value' }] as unknown[] + vi.mocked(constructRelayToParaParameters).mockReturnValue(expectedParameters) + + const result = moonbeam.transferRelayToPara(mockOptions) + + expect(constructRelayToParaParameters).toHaveBeenCalledWith(mockOptions, Version.V3, true) + expect(result).toEqual({ + module: 'xcmPallet', + section: 'limitedReserveTransferAssets', + parameters: expectedParameters + }) + }) + + it('should return the third provider URL from getProvider', () => { + const mockProviders = ['ws://provider1', 'ws://provider2', 'ws://provider3'] + vi.mocked(getAllNodeProviders).mockReturnValue(mockProviders) + + const provider = moonbeam.getProvider() + + expect(getAllNodeProviders).toHaveBeenCalledWith('Moonbeam') + expect(provider).toBe('ws://provider3') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Moonriver.test.ts b/packages/sdk/src/nodes/supported/Moonriver.test.ts new file mode 100644 index 00000000..9c2fe735 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Moonriver.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { constructRelayToParaParameters } from '../../pallets/xcmPallet/utils' +import { Version, XTokensTransferInput, TRelayToParaInternalOptions } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Moonriver from './Moonriver' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +vi.mock('../../pallets/xcmPallet/utils', () => ({ + constructRelayToParaParameters: vi.fn() +})) + +describe('Moonriver', () => { + let moonriver: Moonriver + const mockInput = { + currency: 'MOVR', + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + const mockOptions = { + destination: 'Moonriver' + } as TRelayToParaInternalOptions + + beforeEach(() => { + moonriver = getNode('Moonriver') + }) + + it('should initialize with correct values', () => { + expect(moonriver.node).toBe('Moonriver') + expect(moonriver.name).toBe('moonriver') + expect(moonriver.type).toBe('kusama') + expect(moonriver.version).toBe(Version.V3) + }) + + it('should call transferXTokens with SelfReserve when currency matches native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(moonriver, 'getNativeAssetSymbol').mockReturnValue('MOVR') + + moonriver.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'SelfReserve') + }) + + it('should call transferXTokens with ForeignAsset when currency does not match native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(moonriver, 'getNativeAssetSymbol').mockReturnValue('NOT_MOVR') + + moonriver.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { ForeignAsset: '123' }) + }) + + it('should call transferRelayToPara with the correct parameters', () => { + const expectedParameters = [{ param: 'value' }] as unknown[] + vi.mocked(constructRelayToParaParameters).mockReturnValue(expectedParameters) + + const result = moonriver.transferRelayToPara(mockOptions) + + expect(constructRelayToParaParameters).toHaveBeenCalledWith(mockOptions, Version.V3, true) + expect(result).toEqual({ + module: 'xcmPallet', + section: 'limitedReserveTransferAssets', + parameters: expectedParameters + }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Mythos.test.ts b/packages/sdk/src/nodes/supported/Mythos.test.ts new file mode 100644 index 00000000..85df5663 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Mythos.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { + InvalidCurrencyError, + NodeNotSupportedError, + ScenarioNotSupportedError +} from '../../errors' +import { Version, PolkadotXCMTransferInput } from '../../types' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import Mythos from './Mythos' +import { getNode } from '../../utils' + +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + +describe('Mythos', () => { + let mythos: Mythos + const mockInput = { + currencySymbol: 'MYTH', + scenario: 'ParaToPara', + destination: 'Acala', + amount: '100' + } as PolkadotXCMTransferInput + + beforeEach(() => { + mythos = getNode('Mythos') + }) + + it('should initialize with correct values', () => { + expect(mythos.node).toBe('Mythos') + expect(mythos.name).toBe('mythos') + expect(mythos.type).toBe('polkadot') + expect(mythos.version).toBe(Version.V3) + }) + + it('should call transferPolkadotXCM with limitedReserveTransferAssets for non-AssetHubPolkadot destination', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + vi.spyOn(mythos, 'getNativeAssetSymbol').mockReturnValue('MYTH') + + mythos.transferPolkadotXCM(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'limitedReserveTransferAssets', 'Unlimited') + }) + + it('should call transferPolkadotXCM with limitedTeleportAssets for AssetHubPolkadot destination', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + vi.spyOn(mythos, 'getNativeAssetSymbol').mockReturnValue('MYTH') + + mythos.transferPolkadotXCM({ ...mockInput, destination: 'AssetHubPolkadot' }) + + expect(spy).toHaveBeenCalledWith( + { ...mockInput, destination: 'AssetHubPolkadot' }, + 'limitedTeleportAssets', + 'Unlimited' + ) + }) + + it('should throw ScenarioNotSupportedError for unsupported scenario', () => { + const invalidInput = { ...mockInput, scenario: 'ParaToRelay' } as PolkadotXCMTransferInput + + expect(() => mythos.transferPolkadotXCM(invalidInput)).toThrowError(ScenarioNotSupportedError) + }) + + it('should throw InvalidCurrencyError for unsupported currency', () => { + vi.spyOn(mythos, 'getNativeAssetSymbol').mockReturnValue('NOT_MYTH') + + expect(() => mythos.transferPolkadotXCM(mockInput)).toThrowError( + new InvalidCurrencyError(`Node Mythos does not support currency MYTH`) + ) + }) + + it('should throw NodeNotSupportedError for transferRelayToPara', () => { + expect(() => mythos.transferRelayToPara()).toThrowError(NodeNotSupportedError) + }) +}) diff --git a/packages/sdk/src/nodes/supported/NeuroWeb.test.ts b/packages/sdk/src/nodes/supported/NeuroWeb.test.ts new file mode 100644 index 00000000..2cbb845d --- /dev/null +++ b/packages/sdk/src/nodes/supported/NeuroWeb.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, PolkadotXCMTransferInput } from '../../types' +import PolkadotXCMTransferImpl from '../polkadotXcm' +import NeuroWeb from './NeuroWeb' +import { getNode } from '../../utils' + +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + +describe('NeuroWeb', () => { + let neuroweb: NeuroWeb + const mockInput = { + currencySymbol: 'DOT', + amount: '100' + } as PolkadotXCMTransferInput + + beforeEach(() => { + neuroweb = getNode('NeuroWeb') + }) + + it('should initialize with correct values', () => { + expect(neuroweb.node).toBe('NeuroWeb') + expect(neuroweb.name).toBe('neuroweb') + expect(neuroweb.type).toBe('polkadot') + expect(neuroweb.version).toBe(Version.V3) + }) + + it('should call transferPolkadotXCM with the correct arguments', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + + neuroweb.transferPolkadotXCM(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'limitedReserveTransferAssets', 'Unlimited') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Nodle.test.ts b/packages/sdk/src/nodes/supported/Nodle.test.ts new file mode 100644 index 00000000..7cc8af2a --- /dev/null +++ b/packages/sdk/src/nodes/supported/Nodle.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { + InvalidCurrencyError, + NodeNotSupportedError, + ScenarioNotSupportedError +} from '../../errors' +import { Version, XTokensTransferInput, TNodleAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Nodle from './Nodle' +import { getNode } from '../../utils/getNode' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Nodle', () => { + let nodle: Nodle + const mockInput = { + currency: 'NODL', + scenario: 'ParaToPara', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + nodle = getNode('Nodle') + }) + + it('should initialize with correct values', () => { + expect(nodle.node).toBe('Nodle') + expect(nodle.name).toBe('nodle') + expect(nodle.type).toBe('polkadot') + expect(nodle.version).toBe(Version.V3) + }) + + it('should call transferXTokens with valid scenario and currency', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(nodle, 'getNativeAssetSymbol').mockReturnValue('NODL') + + nodle.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'NodleNative' as TNodleAsset) + }) + + it('should throw ScenarioNotSupportedError for unsupported scenario', () => { + const invalidInput = { ...mockInput, scenario: 'ParaToRelay' } as XTokensTransferInput + + expect(() => nodle.transferXTokens(invalidInput)).toThrowError(ScenarioNotSupportedError) + }) + + it('should throw InvalidCurrencyError for unsupported currency', () => { + vi.spyOn(nodle, 'getNativeAssetSymbol').mockReturnValue('NOT_NODL') + + expect(() => nodle.transferXTokens(mockInput)).toThrowError( + new InvalidCurrencyError(`Asset NODL is not supported by node Nodle.`) + ) + }) + + it('should throw NodeNotSupportedError for transferRelayToPara', () => { + expect(() => nodle.transferRelayToPara()).toThrowError(NodeNotSupportedError) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Parallel.test.ts b/packages/sdk/src/nodes/supported/Parallel.test.ts new file mode 100644 index 00000000..ea5d7fb1 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Parallel.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Parallel from './Parallel' +import { getNode } from '../../utils/getNode' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Parallel', () => { + let parallel: Parallel + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + parallel = getNode('Parallel') + }) + + it('should initialize with correct values', () => { + expect(parallel.node).toBe('Parallel') + expect(parallel.name).toBe('parallel') + expect(parallel.type).toBe('polkadot') + expect(parallel.version).toBe(Version.V3) + }) + + it('should call transferXTokens with currencyID', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + parallel.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, '123') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Peaq.test.ts b/packages/sdk/src/nodes/supported/Peaq.test.ts new file mode 100644 index 00000000..1fbddf3d --- /dev/null +++ b/packages/sdk/src/nodes/supported/Peaq.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ScenarioNotSupportedError, NodeNotSupportedError } from '../../errors' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Peaq from './Peaq' +import { getNode } from '../../utils/getNode' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Peaq', () => { + let peaq: Peaq + const mockInput = { + currencyID: '123', + scenario: 'ParaToPara', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + peaq = getNode('Peaq') + }) + + it('should initialize with correct values', () => { + expect(peaq.node).toBe('Peaq') + expect(peaq.name).toBe('peaq') + expect(peaq.type).toBe('polkadot') + expect(peaq.version).toBe(Version.V2) + }) + + it('should call transferXTokens with valid scenario', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + peaq.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, '123') + }) + + it('should throw ScenarioNotSupportedError for unsupported scenario', () => { + const invalidInput = { ...mockInput, scenario: 'ParaToRelay' } as XTokensTransferInput + + expect(() => peaq.transferXTokens(invalidInput)).toThrowError(ScenarioNotSupportedError) + }) + + it('should throw NodeNotSupportedError for transferRelayToPara', () => { + expect(() => peaq.transferRelayToPara()).toThrowError(NodeNotSupportedError) + }) + + it('should return the correct provider URL', () => { + const provider = peaq.getProvider() + expect(provider).toBe('wss://peaq.api.onfinality.io/public-ws') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Pendulum.test.ts b/packages/sdk/src/nodes/supported/Pendulum.test.ts new file mode 100644 index 00000000..4ed2040a --- /dev/null +++ b/packages/sdk/src/nodes/supported/Pendulum.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { + InvalidCurrencyError, + NodeNotSupportedError, + ScenarioNotSupportedError +} from '../../errors' +import { Version, XTokensTransferInput, TXcmAsset } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Pendulum from './Pendulum' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Pendulum', () => { + let pendulum: Pendulum + const mockInput = { + currency: 'PEN', + currencyID: '123', + scenario: 'ParaToPara', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + pendulum = getNode('Pendulum') + }) + + it('should initialize with correct values', () => { + expect(pendulum.node).toBe('Pendulum') + expect(pendulum.name).toBe('pendulum') + expect(pendulum.type).toBe('polkadot') + expect(pendulum.version).toBe(Version.V3) + }) + + it('should call transferXTokens with valid scenario and currency', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(pendulum, 'getNativeAssetSymbol').mockReturnValue('PEN') + + pendulum.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { XCM: '123' } as TXcmAsset) + }) + + it('should throw ScenarioNotSupportedError for unsupported scenario', () => { + const invalidInput = { ...mockInput, scenario: 'ParaToRelay' } as XTokensTransferInput + + expect(() => pendulum.transferXTokens(invalidInput)).toThrowError(ScenarioNotSupportedError) + }) + + it('should throw InvalidCurrencyError for unsupported currency', () => { + vi.spyOn(pendulum, 'getNativeAssetSymbol').mockReturnValue('NOT_PEN') + + expect(() => pendulum.transferXTokens(mockInput)).toThrowError( + new InvalidCurrencyError(`Asset PEN is not supported by node Pendulum.`) + ) + }) + + it('should throw NodeNotSupportedError for transferRelayToPara', () => { + expect(() => pendulum.transferRelayToPara()).toThrowError(NodeNotSupportedError) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Phala.test.ts b/packages/sdk/src/nodes/supported/Phala.test.ts new file mode 100644 index 00000000..788fbc67 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Phala.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { InvalidCurrencyError } from '../../errors' +import { Version, XTransferTransferInput } from '../../types' +import XTransferTransferImpl from '../xTransfer' +import Phala from './Phala' +import { getNode } from '../../utils' + +vi.mock('../xTransfer', () => ({ + default: { + transferXTransfer: vi.fn() + } +})) + +describe('Phala', () => { + let phala: Phala + const mockInput = { + currency: 'PHA', + amount: '100' + } as XTransferTransferInput + + beforeEach(() => { + phala = getNode('Phala') + }) + + it('should initialize with correct values', () => { + expect(phala.node).toBe('Phala') + expect(phala.name).toBe('phala') + expect(phala.type).toBe('polkadot') + expect(phala.version).toBe(Version.V3) + }) + + it('should call transferXTransfer with valid currency', () => { + const spy = vi.spyOn(XTransferTransferImpl, 'transferXTransfer') + vi.spyOn(phala, 'getNativeAssetSymbol').mockReturnValue('PHA') + + phala.transferXTransfer(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput) + }) + + it('should throw InvalidCurrencyError for unsupported currency', () => { + vi.spyOn(phala, 'getNativeAssetSymbol').mockReturnValue('NOT_PHA') + + expect(() => phala.transferXTransfer(mockInput)).toThrowError( + new InvalidCurrencyError(`Node Phala does not support currency PHA`) + ) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Picasso.test.ts b/packages/sdk/src/nodes/supported/Picasso.test.ts new file mode 100644 index 00000000..e52a1ca0 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Picasso.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Picasso from './Picasso' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Picasso', () => { + let picasso: Picasso + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + picasso = getNode('Picasso') + }) + + it('should initialize with correct values', () => { + expect(picasso.node).toBe('Picasso') + expect(picasso.name).toBe('picasso') + expect(picasso.type).toBe('kusama') + expect(picasso.version).toBe(Version.V3) + }) + + it('should call transferXTokens with currencyID', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + picasso.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, '123') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Pioneer.test.ts b/packages/sdk/src/nodes/supported/Pioneer.test.ts new file mode 100644 index 00000000..9435f9a1 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Pioneer.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Pioneer from './Pioneer' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Pioneer', () => { + let pioneer: Pioneer + const mockInput = { + currencyID: '123', + amount: '100', + fees: 0.01 + } as XTokensTransferInput + + beforeEach(() => { + pioneer = getNode('Pioneer') + }) + + it('should initialize with correct values', () => { + expect(pioneer.node).toBe('Pioneer') + expect(pioneer.name).toBe('pioneer') + expect(pioneer.type).toBe('kusama') + expect(pioneer.version).toBe(Version.V1) + }) + + it('should call transferXTokens with NativeToken and fees', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + pioneer.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'NativeToken', 0.01) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Polkadex.test.ts b/packages/sdk/src/nodes/supported/Polkadex.test.ts new file mode 100644 index 00000000..6be65d07 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Polkadex.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import Polkadex from './Polkadex' +import { getNode } from '../../utils' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Polkadex', () => { + let polkadex: Polkadex + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + polkadex = getNode('Polkadex') + }) + + it('should initialize with correct values', () => { + expect(polkadex.node).toBe('Polkadex') + expect(polkadex.name).toBe('polkadex') + expect(polkadex.type).toBe('polkadot') + expect(polkadex.version).toBe(Version.V3) + }) + + it('should call transferXTokens with currencyID', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + polkadex.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, '123') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Quartz.test.ts b/packages/sdk/src/nodes/supported/Quartz.test.ts new file mode 100644 index 00000000..501de83f --- /dev/null +++ b/packages/sdk/src/nodes/supported/Quartz.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import { getNode } from '../../utils' +import Quartz from './Quartz' +import XTokensTransferImpl from '../xTokens' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Quartz', () => { + let quartz: Quartz + const mockInput = { + currency: 'QTZ', + amount: '100', + currencyID: '123' + } as XTokensTransferInput + + beforeEach(() => { + quartz = getNode('Quartz') + }) + + it('should initialize with correct values', () => { + expect(quartz.node).toBe('Quartz') + expect(quartz.name).toBe('quartz') + expect(quartz.type).toBe('kusama') + expect(quartz.version).toBe(Version.V3) + expect(quartz._assetCheckEnabled).toBe(false) + }) + + it('should call transferXTokens with ForeignAssetId', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + quartz.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { ForeignAssetId: '123' }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Robonomics.test.ts b/packages/sdk/src/nodes/supported/Robonomics.test.ts index 5ca339c2..003e4501 100644 --- a/packages/sdk/src/nodes/supported/Robonomics.test.ts +++ b/packages/sdk/src/nodes/supported/Robonomics.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/unbound-method */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { PolkadotXCMTransferInput } from '../../types' import { getNode } from '../../utils' @@ -28,12 +27,11 @@ describe('Robonomics', () => { }) it('should use limitedTeleportAssets when scenario is not ParaToPara', () => { const input = { scenario: 'ParaToRelay' } as PolkadotXCMTransferInput + + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + robonomics.transferPolkadotXCM(input) - expect(PolkadotXCMTransferImpl.transferPolkadotXCM).toHaveBeenCalledWith( - input, - 'limitedReserveTransferAssets', - 'Unlimited' - ) + expect(spy).toHaveBeenCalledWith(input, 'limitedReserveTransferAssets', 'Unlimited') }) }) }) diff --git a/packages/sdk/src/nodes/supported/Subsocial.test.ts b/packages/sdk/src/nodes/supported/Subsocial.test.ts index 08f24f4d..d802829d 100644 --- a/packages/sdk/src/nodes/supported/Subsocial.test.ts +++ b/packages/sdk/src/nodes/supported/Subsocial.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/unbound-method */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { PolkadotXCMTransferInput } from '../../types' import { getNode } from '../../utils' @@ -40,12 +39,11 @@ describe('Robonomics', () => { it('should use limitedReserveTransferAssets when scenario is ParaToPara', () => { const input = { scenario: 'ParaToPara', currencySymbol: 'SUB' } as PolkadotXCMTransferInput + + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + subsocial.transferPolkadotXCM(input) - expect(PolkadotXCMTransferImpl.transferPolkadotXCM).toHaveBeenCalledWith( - input, - 'limitedReserveTransferAssets', - 'Unlimited' - ) + expect(spy).toHaveBeenCalledWith(input, 'limitedReserveTransferAssets', 'Unlimited') }) }) }) diff --git a/packages/sdk/src/nodes/supported/Turing.test.ts b/packages/sdk/src/nodes/supported/Turing.test.ts new file mode 100644 index 00000000..4019511b --- /dev/null +++ b/packages/sdk/src/nodes/supported/Turing.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import { getNode } from '../../utils/getNode' +import Turing from './Turing' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Turing', () => { + let turing: Turing + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + turing = getNode('Turing') + }) + + it('should initialize with correct values', () => { + expect(turing.node).toBe('Turing') + expect(turing.name).toBe('turing') + expect(turing.type).toBe('kusama') + expect(turing.version).toBe(Version.V3) + }) + + it('should call transferXTokens with currencyID', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + turing.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, '123') + }) +}) diff --git a/packages/sdk/src/nodes/supported/Unique.test.ts b/packages/sdk/src/nodes/supported/Unique.test.ts new file mode 100644 index 00000000..b2f7caf0 --- /dev/null +++ b/packages/sdk/src/nodes/supported/Unique.test.ts @@ -0,0 +1,38 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import { getNode } from '../../utils/getNode' +import Unique from './Unique' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Unique', () => { + let unique: Unique + const mockInput = { + currencyID: '123', + amount: '100' + } as XTokensTransferInput + + beforeEach(() => { + unique = getNode('Unique') + }) + + it('should initialize with correct values', () => { + expect(unique.node).toBe('Unique') + expect(unique.name).toBe('unique') + expect(unique.type).toBe('polkadot') + expect(unique.version).toBe(Version.V3) + }) + + it('should call transferXTokens with ForeignAssetId', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + + unique.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { ForeignAssetId: '123' }) + }) +}) diff --git a/packages/sdk/src/nodes/supported/Zeitgeist.test.ts b/packages/sdk/src/nodes/supported/Zeitgeist.test.ts new file mode 100644 index 00000000..e12003af --- /dev/null +++ b/packages/sdk/src/nodes/supported/Zeitgeist.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { Version, XTokensTransferInput } from '../../types' +import XTokensTransferImpl from '../xTokens' +import { getNode } from '../../utils/getNode' +import Zeitgeist from './Zeitgeist' + +vi.mock('../xTokens', () => ({ + default: { + transferXTokens: vi.fn() + } +})) + +describe('Zeitgeist', () => { + let zeitgeist: Zeitgeist + const mockInput = { + currency: 'ZTG', + amount: '100', + currencyID: '123' + } as XTokensTransferInput + + beforeEach(() => { + zeitgeist = getNode('Zeitgeist') + }) + + it('should initialize with correct values', () => { + expect(zeitgeist.node).toBe('Zeitgeist') + expect(zeitgeist.name).toBe('zeitgeist') + expect(zeitgeist.type).toBe('polkadot') + expect(zeitgeist.version).toBe(Version.V3) + }) + + it('should call transferXTokens with native asset "Ztg" when currency matches native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(zeitgeist, 'getNativeAssetSymbol').mockReturnValue('ZTG') + + zeitgeist.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, 'Ztg') + }) + + it('should call transferXTokens with ForeignAsset when currency does not match the native asset', () => { + const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') + vi.spyOn(zeitgeist, 'getNativeAssetSymbol').mockReturnValue('NOT_ZTG') + + zeitgeist.transferXTokens(mockInput) + + expect(spy).toHaveBeenCalledWith(mockInput, { + ForeignAsset: '123' + }) + }) +}) diff --git a/packages/sdk/src/pallets/assets/balance/getBalanceForeignXTokens.test.ts b/packages/sdk/src/pallets/assets/balance/getBalanceForeignXTokens.test.ts index 6e05f1e3..e9e4ef7d 100644 --- a/packages/sdk/src/pallets/assets/balance/getBalanceForeignXTokens.test.ts +++ b/packages/sdk/src/pallets/assets/balance/getBalanceForeignXTokens.test.ts @@ -1,6 +1,8 @@ import { describe, it, expect, beforeEach, vi } from 'vitest' import { ApiPromise } from '@polkadot/api' import { getBalanceForeignXTokens } from './getBalanceForeignXTokens' +import { Codec } from '@polkadot/types/types' +import { StorageKey } from '@polkadot/types' describe('getBalanceForeignXTokens', () => { let apiMock: ApiPromise @@ -63,4 +65,145 @@ describe('getBalanceForeignXTokens', () => { getBalanceForeignXTokens('0x123', { symbol: 'BTC' }, undefined, undefined, apiMock) ).rejects.toThrow('API Error') }) + + it('should return the correct balance when asset matches by ID', async () => { + vi.mocked(apiMock.query.tokens.accounts.entries).mockResolvedValue([ + [ + { + args: [ + undefined as unknown as Codec, + { toString: () => '1234', toHuman: () => ({}) } as Codec + ] + } as unknown as StorageKey, + { free: { toString: () => '500' } } + ] + ]) + + const result = await getBalanceForeignXTokens( + '0x123', + { id: '1234' }, + undefined, + '1234', + apiMock + ) + expect(result).toEqual(BigInt(500)) + }) + + it('should return the correct balance when asset matches by human-readable symbol', async () => { + vi.mocked(apiMock.query.tokens.accounts.entries).mockResolvedValue([ + [ + { + args: [ + undefined as unknown as Codec, + { toString: () => '0x123', toHuman: () => ({ symbol: 'BTC' }) } as unknown as Codec + ] + } as unknown as StorageKey, + { free: { toString: () => '2000' } } + ] + ]) + + const result = await getBalanceForeignXTokens( + '0x123', + { symbol: 'BTC' }, + undefined, + undefined, + apiMock + ) + expect(result).toEqual(BigInt(2000)) + }) + + it('should return the correct balance when asset matches by human-readable ID', async () => { + vi.mocked(apiMock.query.tokens.accounts.entries).mockResolvedValue([ + [ + { + args: [ + undefined as unknown as Codec, + { toString: () => '0x123', toHuman: () => ({ id: '1234' }) } as unknown as Codec + ] + } as unknown as StorageKey, + { free: { toString: () => '3000' } } + ] + ]) + + const result = await getBalanceForeignXTokens( + '0x123', + { id: '1234' }, + undefined, + undefined, + apiMock + ) + expect(result).toEqual(BigInt(3000)) + }) + + it('should return the correct balance when asset matches by provided symbol', async () => { + vi.mocked(apiMock.query.tokens.accounts.entries).mockResolvedValue([ + [ + { + args: [ + undefined as unknown as Codec, + { toString: () => 'BTC', toHuman: () => ({}) } as Codec + ] + } as unknown as StorageKey, + { free: { toString: () => '1500' } } + ] + ]) + + const result = await getBalanceForeignXTokens( + '0x123', + { symbol: 'BTC' }, + 'BTC', + undefined, + apiMock + ) + expect(result).toEqual(BigInt(1500)) + }) + + it('should return the correct balance when asset matches by provided ID', async () => { + vi.mocked(apiMock.query.tokens.accounts.entries).mockResolvedValue([ + [ + { + args: [ + undefined as unknown as Codec, + { toString: () => '1234', toHuman: () => ({}) } as Codec + ] + } as unknown as StorageKey, + { free: { toString: () => '2500' } } + ] + ]) + + const result = await getBalanceForeignXTokens( + '0x123', + { id: '1234' }, + undefined, + '1234', + apiMock + ) + expect(result).toEqual(BigInt(2500)) + }) + + it('should return null when human-readable asset does not match id or symbol', async () => { + vi.mocked(apiMock.query.tokens.accounts.entries).mockResolvedValue([ + [ + { + args: [ + undefined as unknown as Codec, + { + toString: () => '0x123', + toHuman: () => ({ symbol: 'ETH' }) + } as unknown as Codec + ] + } as unknown as StorageKey, + { free: { toString: () => '1000' } } + ] + ]) + + const result = await getBalanceForeignXTokens( + '0x123', + { symbol: 'BTC' }, + 'BTC', + undefined, + apiMock + ) + expect(result).toBeNull() + }) }) diff --git a/packages/sdk/src/pallets/assets/getExistentialDeposit.test.ts b/packages/sdk/src/pallets/assets/getExistentialDeposit.test.ts new file mode 100644 index 00000000..967f3054 --- /dev/null +++ b/packages/sdk/src/pallets/assets/getExistentialDeposit.test.ts @@ -0,0 +1,55 @@ +import { describe, it, expect, vi } from 'vitest' +import { + getExistentialDeposit, + getMinNativeTransferableAmount, + getMaxNativeTransferableAmount +} from './getExistentialDeposit' +import * as edsMapJson from '../../maps/existential-deposits.json' +import { getBalanceNative } from './balance/getBalanceNative' +import { TNodeDotKsmWithRelayChains } from '../../types' + +vi.mock('./balance/getBalanceNative', () => ({ + getBalanceNative: vi.fn() +})) + +describe('Existential Deposit and Transferable Amounts', () => { + const mockPalletsMap = edsMapJson as { [key: string]: string } + const mockNode: TNodeDotKsmWithRelayChains = 'Polkadot' + const mockAddress = '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa' + + it('should return the correct existential deposit', () => { + const ed = getExistentialDeposit(mockNode) + expect(ed).toBe(BigInt(mockPalletsMap[mockNode])) + }) + + it('should return the correct minimum native transferable amount', () => { + const ed = getExistentialDeposit(mockNode) + const expectedMinTransferableAmount = ed + ed / BigInt(10) + const result = getMinNativeTransferableAmount(mockNode) + + expect(result).toBe(expectedMinTransferableAmount) + }) + + it('should return the correct maximum native transferable amount', async () => { + const mockBalance = BigInt(1000000000000) + vi.mocked(getBalanceNative).mockResolvedValue(mockBalance) + + const ed = getExistentialDeposit(mockNode) + const expectedMaxTransferableAmount = mockBalance - ed - ed / BigInt(10) + + const result = await getMaxNativeTransferableAmount(mockAddress, mockNode) + + expect(result).toBe( + expectedMaxTransferableAmount > BigInt(0) ? expectedMaxTransferableAmount : BigInt(0) + ) + }) + + it('should return 0 for maximum native transferable amount if balance is too low', async () => { + const mockBalance = BigInt(5000) + vi.mocked(getBalanceNative).mockResolvedValue(mockBalance) + + const result = await getMaxNativeTransferableAmount(mockAddress, mockNode) + + expect(result).toBe(BigInt(0)) + }) +}) diff --git a/packages/sdk/src/pallets/xcmPallet/utils.ts b/packages/sdk/src/pallets/xcmPallet/utils.ts index 27bcf935..aef46dd3 100644 --- a/packages/sdk/src/pallets/xcmPallet/utils.ts +++ b/packages/sdk/src/pallets/xcmPallet/utils.ts @@ -8,11 +8,12 @@ import { type TNode, type TCurrencySelectionHeaderArr } from '../../types' -import { createX1Payload, generateAddressPayload } from '../../utils' import { getParaId, getTNode } from '../assets' import { Junctions, TJunction, type TMultiLocation } from '../../types/TMultiLocation' import { type TMultiAsset } from '../../types/TMultiAsset' import { findParachainJunction } from './findParachainJunction' +import { generateAddressPayload } from '../../utils/generateAddressPayload' +import { createX1Payload } from '../../utils/createX1Payload' export const constructRelayToParaParameters = ( { api, destination, address, amount, paraIdTo }: TRelayToParaInternalOptions, diff --git a/packages/sdk/src/utils.ts b/packages/sdk/src/utils.ts deleted file mode 100644 index 68591e95..00000000 --- a/packages/sdk/src/utils.ts +++ /dev/null @@ -1,230 +0,0 @@ -// Contains important call creation utils (Selection of fees,formating of header and more.. ) - -import { ApiPromise, WsProvider } from '@polkadot/api' -import { ethers } from 'ethers' -import { prodRelayPolkadot, prodRelayKusama } from '@polkadot/apps-config/endpoints' -import { - type TNode, - type TPallet, - type TScenario, - type TSerializedApiCall, - Version, - type Extrinsic, - type TNodeWithRelayChains, - type TAddress, - type TMultiLocationHeader, - Parents, - type TJunction, - type Junctions, - TNodePolkadotKusama, - TNodeDotKsmWithRelayChains -} from './types' -import { nodes } from './maps/consts' -import { type HexString } from '@polkadot/util/types' -import { getRelayChainSymbol } from './pallets/assets' - -export const createAccID = (api: ApiPromise, account: string): HexString => { - console.log('Generating AccountId32 address') - return api.createType('AccountId32', account).toHex() -} - -export const getFees = (scenario: TScenario): number => { - if (scenario === 'ParaToRelay') { - console.log('Asigning fees for transfer to Relay chain') - return 4600000000 - } else if (scenario === 'ParaToPara') { - console.log('Asigning fees for transfer to another Parachain chain') - return 399600000000 - } - throw new Error(`Fees for scenario ${scenario} are not defined.`) -} - -export const generateAddressMultiLocationV4 = ( - api: ApiPromise, - address: TAddress -): TMultiLocationHeader => { - const isMultiLocation = typeof address === 'object' - if (isMultiLocation) { - return { [Version.V4]: address } - } - - const isEthAddress = ethers.isAddress(address) - return { - [Version.V4]: { - parents: Parents.ZERO, - interior: { - X1: [ - isEthAddress - ? { AccountKey20: { key: address } } - : { AccountId32: { id: createAccID(api, address), network: null } } - ] - } - } - } -} - -export const createX1Payload = (version: Version, junction: TJunction): Junctions => { - if (version === Version.V4) { - return { X1: [junction] } - } - return { X1: junction } -} - -export const generateAddressPayload = ( - api: ApiPromise, - scenario: TScenario, - pallet: TPallet | null, - recipientAddress: TAddress, - version: Version, - nodeId: number | undefined -): TMultiLocationHeader => { - const isMultiLocation = typeof recipientAddress === 'object' - if (isMultiLocation) { - return { [version]: recipientAddress } - } - - const isEthAddress = ethers.isAddress(recipientAddress) - - if (scenario === 'ParaToRelay') { - return { - [version]: { - parents: pallet === 'XTokens' ? Parents.ONE : Parents.ZERO, - interior: createX1Payload(version, { - AccountId32: { - ...(version === Version.V1 && { network: 'any' }), - id: createAccID(api, recipientAddress) - } - }) - } - } - } - - if (scenario === 'ParaToPara' && pallet === 'XTokens') { - return { - [version]: { - parents: Parents.ONE, - interior: { - X2: [ - { - Parachain: nodeId - }, - isEthAddress - ? { - AccountKey20: { - ...(version === Version.V1 && { network: 'any' }), - key: recipientAddress - } - } - : { - AccountId32: { - ...(version === Version.V1 && { network: 'any' }), - id: createAccID(api, recipientAddress) - } - } - ] - } - } - } - } - - if (scenario === 'ParaToPara' && pallet === 'PolkadotXcm') { - return { - [version]: { - parents: Parents.ZERO, - interior: createX1Payload( - version, - isEthAddress - ? { - AccountKey20: { - ...(version === Version.V1 && { network: 'any' }), - key: recipientAddress - } - } - : { - AccountId32: { - ...(version === Version.V1 && { network: 'any' }), - id: createAccID(api, recipientAddress) - } - } - ) - } - } - } - - return { - [version]: { - parents: Parents.ZERO, - interior: createX1Payload( - version, - isEthAddress - ? { AccountKey20: { key: recipientAddress } } - : { AccountId32: { id: createAccID(api, recipientAddress) } } - ) - } - } -} - -export const getNode = (node: T): (typeof nodes)[T] => nodes[node] - -export const getNodeEndpointOption = (node: TNodePolkadotKusama) => { - const { type, name } = getNode(node) - const { linked } = type === 'polkadot' ? prodRelayPolkadot : prodRelayKusama - - if (linked === undefined) return undefined - - const preferredOption = linked.find(o => o.info === name && Object.values(o.providers).length > 0) - - return preferredOption ?? linked.find(o => o.info === name) -} - -export const getAllNodeProviders = (node: TNodePolkadotKusama): string[] => { - const { providers } = getNodeEndpointOption(node) ?? {} - if (providers && Object.values(providers).length < 1) { - throw new Error(`Node ${node} does not have any providers.`) - } - return Object.values(providers ?? []) -} - -export const getNodeProvider = (node: TNodeWithRelayChains): string => { - if (node === 'Polkadot') { - return prodRelayPolkadot.providers[0] - } else if (node === 'Kusama') { - return prodRelayKusama.providers[0] - } - return getNode(node).getProvider() -} - -export const createApiInstance = async (wsUrl: string): Promise => { - const wsProvider = new WsProvider(wsUrl) - return await ApiPromise.create({ provider: wsProvider }) -} - -export const createApiInstanceForNode = async (node: TNodeWithRelayChains): Promise => { - if (node === 'Polkadot' || node === 'Kusama') { - const endpointOption = node === 'Polkadot' ? prodRelayPolkadot : prodRelayKusama - const wsUrl = Object.values(endpointOption.providers)[0] - return await createApiInstance(wsUrl) - } - return await getNode(node).createApiInstance() -} - -export const callPolkadotJsTxFunction = ( - api: ApiPromise, - { module, section, parameters }: TSerializedApiCall -): Extrinsic => api.tx[module][section](...parameters) - -export const determineRelayChain = (node: TNodeWithRelayChains): TNodeDotKsmWithRelayChains => - getRelayChainSymbol(node) === 'KSM' ? 'Kusama' : 'Polkadot' - -export const determineRelayChainSymbol = (node: TNodeWithRelayChains): string => { - if (node === 'Polkadot') { - return 'DOT' - } else if (node === 'Kusama') { - return 'KSM' - } else { - return getRelayChainSymbol(node) - } -} - -export const isRelayChain = (node: TNodeWithRelayChains): boolean => - node === 'Polkadot' || node === 'Kusama' diff --git a/packages/sdk/src/utils/callPolkadotJsTxFunction.test.ts b/packages/sdk/src/utils/callPolkadotJsTxFunction.test.ts new file mode 100644 index 00000000..209f6c7f --- /dev/null +++ b/packages/sdk/src/utils/callPolkadotJsTxFunction.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, vi } from 'vitest' +import { ApiPromise } from '@polkadot/api' +import { TSerializedApiCall, Extrinsic } from '../types' +import { callPolkadotJsTxFunction } from './callPolkadotJsTxFunction' + +const apiMock = { + tx: { + balances: { + transfer: vi.fn() + } + } +} as unknown as ApiPromise + +describe('callPolkadotJsTxFunction', () => { + it('should call the correct function on the PolkadotJS API with the provided parameters', () => { + const serializedCall: TSerializedApiCall = { + module: 'balances', + section: 'transfer', + parameters: ['recipientAddress', 1000] + } + + const extrinsicMock: Extrinsic = 'mockExtrinsic' as unknown as Extrinsic + + vi.mocked(apiMock.tx.balances.transfer).mockReturnValue(extrinsicMock) + + const result = callPolkadotJsTxFunction(apiMock, serializedCall) + + expect(apiMock.tx.balances.transfer).toHaveBeenCalledWith('recipientAddress', 1000) + expect(result).toBe(extrinsicMock) + }) + + it('should throw an error when an invalid module or section is provided', () => { + const invalidSerializedCall: TSerializedApiCall = { + module: 'invalidModule', + section: 'invalidSection', + parameters: [] + } + + expect(() => callPolkadotJsTxFunction(apiMock, invalidSerializedCall)).toThrowError() + }) +}) diff --git a/packages/sdk/src/utils/callPolkadotJsTxFunction.ts b/packages/sdk/src/utils/callPolkadotJsTxFunction.ts new file mode 100644 index 00000000..4108b8fd --- /dev/null +++ b/packages/sdk/src/utils/callPolkadotJsTxFunction.ts @@ -0,0 +1,7 @@ +import { ApiPromise } from '@polkadot/api' +import { Extrinsic, TSerializedApiCall } from '../types' + +export const callPolkadotJsTxFunction = ( + api: ApiPromise, + { module, section, parameters }: TSerializedApiCall +): Extrinsic => api.tx[module][section](...parameters) diff --git a/packages/sdk/src/utils/createApiInstance.ts b/packages/sdk/src/utils/createApiInstance.ts new file mode 100644 index 00000000..fff16c14 --- /dev/null +++ b/packages/sdk/src/utils/createApiInstance.ts @@ -0,0 +1,6 @@ +import { ApiPromise, WsProvider } from '@polkadot/api' + +export const createApiInstance = async (wsUrl: string): Promise => { + const wsProvider = new WsProvider(wsUrl) + return await ApiPromise.create({ provider: wsProvider }) +} diff --git a/packages/sdk/src/utils/createApiInstanceForNode.test.ts b/packages/sdk/src/utils/createApiInstanceForNode.test.ts new file mode 100644 index 00000000..4179ce99 --- /dev/null +++ b/packages/sdk/src/utils/createApiInstanceForNode.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect, vi } from 'vitest' +import { ApiPromise } from '@polkadot/api' +import { TNodeWithRelayChains } from '../types' +import { createApiInstanceForNode } from './createApiInstanceForNode' +import { createApiInstance } from './createApiInstance' +import { getNode } from './getNode' + +vi.mock('@polkadot/apps-config', () => ({ + prodRelayPolkadot: { providers: { Provider1: 'wss://polkadotProvider.com' } }, + prodRelayKusama: { providers: { Provider1: 'wss://kusamaProvider.com' } } +})) + +vi.mock('./createApiInstance', () => ({ + createApiInstance: vi.fn().mockResolvedValue({} as ApiPromise) +})) + +vi.mock('./getNode', () => ({ + getNode: vi + .fn() + .mockReturnValue({ createApiInstance: vi.fn().mockResolvedValue({} as ApiPromise) }) +})) + +describe('createApiInstanceForNode', () => { + it('should create an ApiPromise instance for Polkadot', async () => { + const result = await createApiInstanceForNode('Polkadot' as TNodeWithRelayChains) + + expect(createApiInstance).toHaveBeenCalledWith('wss://polkadotProvider.com') + expect(result).toStrictEqual({}) + }) + + it('should create an ApiPromise instance for Kusama', async () => { + const mockApiInstance = {} as ApiPromise + vi.mocked(createApiInstance).mockResolvedValue(mockApiInstance) + + const result = await createApiInstanceForNode('Kusama' as TNodeWithRelayChains) + + expect(createApiInstance).toHaveBeenCalledWith('wss://kusamaProvider.com') + expect(result).toBe(mockApiInstance) + }) + + it('should call getNode and create an ApiPromise instance for other nodes', async () => { + const node = 'SomeOtherNode' as TNodeWithRelayChains + const mockApiInstance = {} as ApiPromise + + const result = await createApiInstanceForNode(node) + + expect(getNode).toHaveBeenCalledWith(node) + expect(result).toStrictEqual(mockApiInstance) + }) +}) diff --git a/packages/sdk/src/utils/createApiInstanceForNode.ts b/packages/sdk/src/utils/createApiInstanceForNode.ts new file mode 100644 index 00000000..4662ec9a --- /dev/null +++ b/packages/sdk/src/utils/createApiInstanceForNode.ts @@ -0,0 +1,13 @@ +import { ApiPromise } from '@polkadot/api' +import { TNodeWithRelayChains } from '../types' +import { prodRelayKusama, prodRelayPolkadot } from '@polkadot/apps-config' +import { createApiInstance, getNode } from '.' + +export const createApiInstanceForNode = async (node: TNodeWithRelayChains): Promise => { + if (node === 'Polkadot' || node === 'Kusama') { + const endpointOption = node === 'Polkadot' ? prodRelayPolkadot : prodRelayKusama + const wsUrl = Object.values(endpointOption.providers)[0] + return await createApiInstance(wsUrl) + } + return await getNode(node).createApiInstance() +} diff --git a/packages/sdk/src/utils/createX1Payload.test.ts b/packages/sdk/src/utils/createX1Payload.test.ts new file mode 100644 index 00000000..837a1fbd --- /dev/null +++ b/packages/sdk/src/utils/createX1Payload.test.ts @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest' +import { Version, TJunction } from '../types' +import { createX1Payload } from './createX1Payload' + +describe('createX1Payload', () => { + it('should return an X1 payload with an array of junction when version is V4', () => { + const junction: TJunction = { Parachain: 2000 } + const result = createX1Payload(Version.V4, junction) + expect(result).toEqual({ X1: [junction] }) + }) + + it('should return an X1 payload with a single junction when version is not V4', () => { + const junction: TJunction = { AccountId32: { id: '0x123', network: null } } + const result = createX1Payload('V3' as Version, junction) + expect(result).toEqual({ X1: junction }) + }) +}) diff --git a/packages/sdk/src/utils/createX1Payload.ts b/packages/sdk/src/utils/createX1Payload.ts new file mode 100644 index 00000000..62b77fe2 --- /dev/null +++ b/packages/sdk/src/utils/createX1Payload.ts @@ -0,0 +1,4 @@ +import { Junctions, TJunction, Version } from '../types' + +export const createX1Payload = (version: Version, junction: TJunction): Junctions => + version === Version.V4 ? { X1: [junction] } : { X1: junction } diff --git a/packages/sdk/src/utils/determineRelayChainSymbol.test.ts b/packages/sdk/src/utils/determineRelayChainSymbol.test.ts new file mode 100644 index 00000000..a88da715 --- /dev/null +++ b/packages/sdk/src/utils/determineRelayChainSymbol.test.ts @@ -0,0 +1,34 @@ +import { describe, it, expect, vi } from 'vitest' +import { TNodeWithRelayChains } from '../types' +import { determineRelayChainSymbol } from './determineRelayChainSymbol' +import { getRelayChainSymbol } from '../pallets/assets' + +vi.mock('../pallets/assets', () => ({ + getRelayChainSymbol: vi.fn() +})) + +describe('determineRelayChainSymbol', () => { + it('should return "DOT" for Polkadot', () => { + const node = 'Polkadot' as TNodeWithRelayChains + const result = determineRelayChainSymbol(node) + expect(result).toBe('DOT') + }) + + it('should return "KSM" for Kusama', () => { + const node = 'Kusama' as TNodeWithRelayChains + const result = determineRelayChainSymbol(node) + expect(result).toBe('KSM') + }) + + it('should return the result of getRelayChainSymbol for other nodes', () => { + const node = 'SomeOtherNode' as TNodeWithRelayChains + const relayChainSymbolMock = 'DOT' + + vi.mocked(getRelayChainSymbol).mockReturnValue(relayChainSymbolMock) + + const result = determineRelayChainSymbol(node) + + expect(result).toBe(relayChainSymbolMock) + expect(getRelayChainSymbol).toHaveBeenCalledWith(node) + }) +}) diff --git a/packages/sdk/src/utils/determineRelayChainSymbol.ts b/packages/sdk/src/utils/determineRelayChainSymbol.ts new file mode 100644 index 00000000..60684fc3 --- /dev/null +++ b/packages/sdk/src/utils/determineRelayChainSymbol.ts @@ -0,0 +1,12 @@ +import { getRelayChainSymbol } from '../pallets/assets' +import { TNodeWithRelayChains } from '../types' + +export const determineRelayChainSymbol = (node: TNodeWithRelayChains): string => { + if (node === 'Polkadot') { + return 'DOT' + } else if (node === 'Kusama') { + return 'KSM' + } else { + return getRelayChainSymbol(node) + } +} diff --git a/packages/sdk/src/utils/generateAddressMultiLocationV4.test.ts b/packages/sdk/src/utils/generateAddressMultiLocationV4.test.ts new file mode 100644 index 00000000..517a796a --- /dev/null +++ b/packages/sdk/src/utils/generateAddressMultiLocationV4.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ApiPromise } from '@polkadot/api' +import { Parents, Version, TAddress } from '../types' +import { createAccID } from '../utils' +import { generateAddressMultiLocationV4 } from './generateAddressMultiLocationV4' +import * as ethers from 'ethers' + +vi.mock('../utils', () => ({ + createAccID: vi.fn() +})) + +vi.mock('ethers', () => ({ + ethers: { + isAddress: vi.fn() + } +})) + +describe('generateAddressMultiLocationV4', () => { + let apiMock: ApiPromise + + beforeEach(() => { + apiMock = {} as ApiPromise + }) + + it('should return a multi-location object when the address is a multi-location object', () => { + const multiLocationAddress: TAddress = { parents: Parents.ONE, interior: {} } + const result = generateAddressMultiLocationV4(apiMock, multiLocationAddress) + + expect(result).toEqual({ [Version.V4]: multiLocationAddress }) + }) + + it('should return a multi-location object with AccountKey20 when the address is a valid Ethereum address', () => { + const ethAddress = '0x1234567890123456789012345678901234567890' + vi.mocked(ethers.ethers.isAddress).mockReturnValue(true) + + const result = generateAddressMultiLocationV4(apiMock, ethAddress) + + expect(result).toEqual({ + [Version.V4]: { + parents: Parents.ZERO, + interior: { + X1: [{ AccountKey20: { key: ethAddress } }] + } + } + }) + }) + + it('should return a multi-location object with AccountId32 when the address is not an Ethereum address', () => { + const standardAddress = '5F3sa2TJAWMqDhXG6jhV4N8ko9iFyzPXj7v5jcmn5ySxkPPg' + vi.mocked(ethers.ethers.isAddress).mockReturnValue(false) + const accIDMock = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + vi.mocked(createAccID).mockReturnValue(accIDMock) + + const result = generateAddressMultiLocationV4(apiMock, standardAddress) + + expect(result).toEqual({ + [Version.V4]: { + parents: Parents.ZERO, + interior: { + X1: [{ AccountId32: { id: accIDMock, network: null } }] + } + } + }) + + expect(createAccID).toHaveBeenCalledWith(apiMock, standardAddress) + }) + + it('should handle invalid Ethereum address appropriately', () => { + const invalidEthAddress = 'invalidEthAddress' + vi.mocked(ethers.ethers.isAddress).mockReturnValue(false) + const accIDMock = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + vi.mocked(createAccID).mockReturnValue(accIDMock) + + const result = generateAddressMultiLocationV4(apiMock, invalidEthAddress) + + expect(result).toEqual({ + [Version.V4]: { + parents: Parents.ZERO, + interior: { + X1: [{ AccountId32: { id: accIDMock, network: null } }] + } + } + }) + + expect(createAccID).toHaveBeenCalledWith(apiMock, invalidEthAddress) + }) +}) diff --git a/packages/sdk/src/utils/generateAddressMultiLocationV4.ts b/packages/sdk/src/utils/generateAddressMultiLocationV4.ts new file mode 100644 index 00000000..f2f471c7 --- /dev/null +++ b/packages/sdk/src/utils/generateAddressMultiLocationV4.ts @@ -0,0 +1,28 @@ +import { ApiPromise } from '@polkadot/api' +import { Parents, TAddress, TMultiLocationHeader, Version } from '../types' +import { ethers } from 'ethers' +import { createAccID } from '../utils' + +export const generateAddressMultiLocationV4 = ( + api: ApiPromise, + address: TAddress +): TMultiLocationHeader => { + const isMultiLocation = typeof address === 'object' + if (isMultiLocation) { + return { [Version.V4]: address } + } + + const isEthAddress = ethers.isAddress(address) + return { + [Version.V4]: { + parents: Parents.ZERO, + interior: { + X1: [ + isEthAddress + ? { AccountKey20: { key: address } } + : { AccountId32: { id: createAccID(api, address), network: null } } + ] + } + } + } +} diff --git a/packages/sdk/src/utils/generateAddressPayload.test.ts b/packages/sdk/src/utils/generateAddressPayload.test.ts new file mode 100644 index 00000000..bbb5282b --- /dev/null +++ b/packages/sdk/src/utils/generateAddressPayload.test.ts @@ -0,0 +1,214 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ApiPromise } from '@polkadot/api' +import * as ethers from 'ethers' +import { Parents, Version, TPallet, TScenario } from '../types' +import { createX1Payload } from './createX1Payload' +import { createAccID } from '../utils' +import { generateAddressPayload } from './generateAddressPayload' + +vi.mock('../utils', () => ({ + createAccID: vi.fn() +})) + +vi.mock('./createX1Payload', () => ({ + createX1Payload: vi.fn() +})) + +vi.mock('ethers', () => ({ + ethers: { + isAddress: vi.fn() + } +})) + +describe('generateAddressPayload', () => { + let apiMock: ApiPromise + + beforeEach(() => { + apiMock = {} as ApiPromise + }) + + it('should return a multilocation object for a multilocation recipient address', () => { + const recipientAddress = { parents: Parents.ONE, interior: {} } + const version = Version.V4 + + const result = generateAddressPayload( + apiMock, + 'ParaToRelay' as TScenario, + null, + recipientAddress, + version, + undefined + ) + + expect(result).toEqual({ [version]: recipientAddress }) + }) + + it('should return a correct payload for ParaToRelay scenario with XTokens pallet', () => { + const recipientAddress = '5F3sa2TJAWMqDhXG6jhV4N8ko9iFyzPXj7v5jcmn5ySxkPPg' + const accIDMock = '0x1234567890abcdef' + vi.mocked(ethers.ethers.isAddress).mockReturnValue(false) + vi.mocked(createAccID).mockReturnValue(accIDMock) + vi.mocked(createX1Payload).mockReturnValue({ + X1: [{ AccountId32: { id: accIDMock, network: 'any' } }] + }) + + const result = generateAddressPayload( + apiMock, + 'ParaToRelay' as TScenario, + 'XTokens' as TPallet, + recipientAddress, + Version.V1, + undefined + ) + + expect(result).toEqual({ + [Version.V1]: { + parents: Parents.ONE, + interior: { X1: [{ AccountId32: { id: accIDMock, network: 'any' } }] } + } + }) + + expect(createAccID).toHaveBeenCalledWith(apiMock, recipientAddress) + }) + + it('should return a correct payload for ParaToPara scenario with XTokens pallet and Ethereum address', () => { + const ethAddress = '0x1234567890123456789012345678901234567890' + vi.mocked(ethers.ethers.isAddress).mockReturnValue(true) + + const result = generateAddressPayload( + apiMock, + 'ParaToPara' as TScenario, + 'XTokens' as TPallet, + ethAddress, + Version.V4, + 1000 + ) + + expect(result).toEqual({ + [Version.V4]: { + parents: Parents.ONE, + interior: { + X2: [{ Parachain: 1000 }, { AccountKey20: { key: ethAddress } }] + } + } + }) + + expect(ethers.ethers.isAddress).toHaveBeenCalledWith(ethAddress) + }) + + it('should return a correct payload for ParaToPara scenario with PolkadotXcm pallet and standard address', () => { + const recipientAddress = '5F3sa2TJAWMqDhXG6jhV4N8ko9iFyzPXj7v5jcmn5ySxkPPg' + const accIDMock = '0x1234567890abcdef' + vi.mocked(ethers.ethers.isAddress).mockReturnValue(false) + vi.mocked(createAccID).mockReturnValue(accIDMock) + vi.mocked(createX1Payload).mockReturnValue({ + X1: [{ AccountId32: { id: accIDMock, network: null } }] + }) + + const result = generateAddressPayload( + apiMock, + 'ParaToPara' as TScenario, + 'PolkadotXcm' as TPallet, + recipientAddress, + Version.V4, + undefined + ) + + expect(result).toEqual({ + [Version.V4]: { + parents: Parents.ZERO, + interior: { X1: [{ AccountId32: { id: accIDMock, network: null } }] } + } + }) + + expect(createAccID).toHaveBeenCalledWith(apiMock, recipientAddress) + }) + + it('should return a fallback payload for an unknown scenario', () => { + const recipientAddress = '5F3sa2TJAWMqDhXG6jhV4N8ko9iFyzPXj7v5jcmn5ySxkPPg' + const accIDMock = '0x1234567890abcdef' + vi.mocked(ethers.ethers.isAddress).mockReturnValue(false) + vi.mocked(createAccID).mockReturnValue(accIDMock) + vi.mocked(createX1Payload).mockReturnValue({ + X1: [{ AccountId32: { id: accIDMock, network: null } }] + }) + + const result = generateAddressPayload( + apiMock, + 'UnknownScenario' as TScenario, + null, + recipientAddress, + Version.V4, + undefined + ) + + expect(result).toEqual({ + [Version.V4]: { + parents: Parents.ZERO, + interior: { X1: [{ AccountId32: { id: accIDMock, network: null } }] } + } + }) + + expect(createAccID).toHaveBeenCalledWith(apiMock, recipientAddress) + }) + + it('should return a correct payload for ParaToPara scenario with XTokens pallet and non-Ethereum address', () => { + const recipientAddress = '5F3sa2TJAWMqDhXG6jhV4N8ko9iFyzPXj7v5jcmn5ySxkPPg' + const accIDMock = '0x1234567890abcdef' + vi.mocked(ethers.ethers.isAddress).mockReturnValue(false) + vi.mocked(createAccID).mockReturnValue(accIDMock) + + const result = generateAddressPayload( + apiMock, + 'ParaToPara' as TScenario, + 'XTokens' as TPallet, + recipientAddress, + Version.V4, + 1000 + ) + + expect(result).toEqual({ + [Version.V4]: { + parents: Parents.ONE, + interior: { + X2: [{ Parachain: 1000 }, { AccountId32: { id: accIDMock } }] + } + } + }) + + expect(ethers.ethers.isAddress).toHaveBeenCalledWith(recipientAddress) + expect(createAccID).toHaveBeenCalledWith(apiMock, recipientAddress) + }) + + it('should return a correct payload for ParaToPara scenario with PolkadotXcm pallet and non-Ethereum address', () => { + const recipientAddress = '5F3sa2TJAWMqDhXG6jhV4N8ko9iFyzPXj7v5jcmn5ySxkPPg' + const accIDMock = '0x1234567890abcdef' + vi.mocked(ethers.ethers.isAddress).mockReturnValue(false) + vi.mocked(createAccID).mockReturnValue(accIDMock) + vi.mocked(createX1Payload).mockReturnValue({ + X1: [{ AccountId32: { id: accIDMock, network: null } }] + }) + + const result = generateAddressPayload( + apiMock, + 'ParaToPara' as TScenario, + 'PolkadotXcm' as TPallet, + recipientAddress, + Version.V4, + undefined + ) + + expect(result).toEqual({ + [Version.V4]: { + parents: Parents.ZERO, + interior: { X1: [{ AccountId32: { id: accIDMock, network: null } }] } + } + }) + + expect(ethers.ethers.isAddress).toHaveBeenCalledWith(recipientAddress) + expect(createAccID).toHaveBeenCalledWith(apiMock, recipientAddress) + expect(createX1Payload).toHaveBeenCalledWith(Version.V4, { + AccountId32: { id: accIDMock } + }) + }) +}) diff --git a/packages/sdk/src/utils/generateAddressPayload.ts b/packages/sdk/src/utils/generateAddressPayload.ts new file mode 100644 index 00000000..badf633c --- /dev/null +++ b/packages/sdk/src/utils/generateAddressPayload.ts @@ -0,0 +1,99 @@ +import { ApiPromise } from '@polkadot/api' +import { Parents, TAddress, TMultiLocationHeader, TPallet, TScenario, Version } from '../types' +import { ethers } from 'ethers' +import { createX1Payload } from './createX1Payload' +import { createAccID } from '../utils' + +export const generateAddressPayload = ( + api: ApiPromise, + scenario: TScenario, + pallet: TPallet | null, + recipientAddress: TAddress, + version: Version, + nodeId: number | undefined +): TMultiLocationHeader => { + const isMultiLocation = typeof recipientAddress === 'object' + if (isMultiLocation) { + return { [version]: recipientAddress } + } + + const isEthAddress = ethers.isAddress(recipientAddress) + + if (scenario === 'ParaToRelay') { + return { + [version]: { + parents: pallet === 'XTokens' ? Parents.ONE : Parents.ZERO, + interior: createX1Payload(version, { + AccountId32: { + ...(version === Version.V1 && { network: 'any' }), + id: createAccID(api, recipientAddress) + } + }) + } + } + } + + if (scenario === 'ParaToPara' && pallet === 'XTokens') { + return { + [version]: { + parents: Parents.ONE, + interior: { + X2: [ + { + Parachain: nodeId + }, + isEthAddress + ? { + AccountKey20: { + ...(version === Version.V1 && { network: 'any' }), + key: recipientAddress + } + } + : { + AccountId32: { + ...(version === Version.V1 && { network: 'any' }), + id: createAccID(api, recipientAddress) + } + } + ] + } + } + } + } + + if (scenario === 'ParaToPara' && pallet === 'PolkadotXcm') { + return { + [version]: { + parents: Parents.ZERO, + interior: createX1Payload( + version, + isEthAddress + ? { + AccountKey20: { + ...(version === Version.V1 && { network: 'any' }), + key: recipientAddress + } + } + : { + AccountId32: { + ...(version === Version.V1 && { network: 'any' }), + id: createAccID(api, recipientAddress) + } + } + ) + } + } + } + + return { + [version]: { + parents: Parents.ZERO, + interior: createX1Payload( + version, + isEthAddress + ? { AccountKey20: { key: recipientAddress } } + : { AccountId32: { id: createAccID(api, recipientAddress) } } + ) + } + } +} diff --git a/packages/sdk/src/utils/getAllNodeProviders.test.ts b/packages/sdk/src/utils/getAllNodeProviders.test.ts new file mode 100644 index 00000000..808b9f22 --- /dev/null +++ b/packages/sdk/src/utils/getAllNodeProviders.test.ts @@ -0,0 +1,47 @@ +import { describe, it, expect, vi } from 'vitest' +import { TNodePolkadotKusama } from '../types' +import { getNodeEndpointOption } from '../utils' +import { EndpointOption } from '@polkadot/apps-config/endpoints/types' +import { getAllNodeProviders } from './getAllNodeProviders' + +vi.mock('../utils', () => ({ + getNodeEndpointOption: vi.fn() +})) + +describe('getAllNodeProviders', () => { + it('should return an array of providers when providers are available', () => { + const node = 'polkadot' as TNodePolkadotKusama + const providersMock = { Provider1: 'https://provider1.com', Provider2: 'https://provider2.com' } + + vi.mocked(getNodeEndpointOption).mockReturnValue({ + providers: providersMock + } as unknown as EndpointOption) + + const result = getAllNodeProviders(node) + + expect(result).toEqual(['https://provider1.com', 'https://provider2.com']) + expect(getNodeEndpointOption).toHaveBeenCalledWith(node) + }) + + it('should throw an error if the node does not have any providers', () => { + const node = 'kusama' as TNodePolkadotKusama + + vi.mocked(getNodeEndpointOption).mockReturnValue({ providers: {} } as unknown as EndpointOption) + + expect(() => getAllNodeProviders(node)).toThrowError( + `Node ${node} does not have any providers.` + ) + expect(getNodeEndpointOption).toHaveBeenCalledWith(node) + }) + + it('should return an empty array if providers are not available', () => { + const node = 'polkadot' as TNodePolkadotKusama + + vi.mocked(getNodeEndpointOption).mockReturnValue(undefined) + + const result = getAllNodeProviders(node) + + expect(result).toEqual([]) + expect(getNodeEndpointOption).toHaveBeenCalledWith(node) + }) +}) diff --git a/packages/sdk/src/utils/getAllNodeProviders.ts b/packages/sdk/src/utils/getAllNodeProviders.ts new file mode 100644 index 00000000..b5448269 --- /dev/null +++ b/packages/sdk/src/utils/getAllNodeProviders.ts @@ -0,0 +1,10 @@ +import { TNodePolkadotKusama } from '../types' +import { getNodeEndpointOption } from '../utils' + +export const getAllNodeProviders = (node: TNodePolkadotKusama): string[] => { + const { providers } = getNodeEndpointOption(node) ?? {} + if (providers && Object.values(providers).length < 1) { + throw new Error(`Node ${node} does not have any providers.`) + } + return Object.values(providers ?? []) +} diff --git a/packages/sdk/src/utils/getFees.test.ts b/packages/sdk/src/utils/getFees.test.ts new file mode 100644 index 00000000..7ccbc4ef --- /dev/null +++ b/packages/sdk/src/utils/getFees.test.ts @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest' +import { TScenario } from '../types' +import { getFees } from './getFees' + +describe('getFees', () => { + it('should return the correct fee for ParaToRelay scenario', () => { + const result = getFees('ParaToRelay' as TScenario) + expect(result).toBe(4600000000) + }) + + it('should return the correct fee for ParaToPara scenario', () => { + const result = getFees('ParaToPara' as TScenario) + expect(result).toBe(399600000000) + }) + + it('should throw an error for an undefined scenario', () => { + expect(() => getFees('UnknownScenario' as TScenario)).toThrowError( + 'Fees for scenario UnknownScenario are not defined.' + ) + }) +}) diff --git a/packages/sdk/src/utils/getFees.ts b/packages/sdk/src/utils/getFees.ts new file mode 100644 index 00000000..343bcddf --- /dev/null +++ b/packages/sdk/src/utils/getFees.ts @@ -0,0 +1,12 @@ +import { TScenario } from '../types' + +export const getFees = (scenario: TScenario): number => { + if (scenario === 'ParaToRelay') { + console.log('Asigning fees for transfer to Relay chain') + return 4600000000 + } else if (scenario === 'ParaToPara') { + console.log('Asigning fees for transfer to another Parachain chain') + return 399600000000 + } + throw new Error(`Fees for scenario ${scenario} are not defined.`) +} diff --git a/packages/sdk/src/utils/getNode.test.ts b/packages/sdk/src/utils/getNode.test.ts new file mode 100644 index 00000000..47e32a49 --- /dev/null +++ b/packages/sdk/src/utils/getNode.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest' +import { getNode } from '.' +import { NODE_NAMES } from '../maps/consts' + +describe('getNode', () => { + it('should return node detail for all nodes', () => { + NODE_NAMES.forEach(node => { + const details = getNode(node) + expect(details).toBeDefined() + expect(details.name).toBeTypeOf('string') + expect(['polkadot', 'kusama']).toContain(details.type) + }) + }) +}) diff --git a/packages/sdk/src/utils/getNode.ts b/packages/sdk/src/utils/getNode.ts new file mode 100644 index 00000000..160d0e14 --- /dev/null +++ b/packages/sdk/src/utils/getNode.ts @@ -0,0 +1,4 @@ +import { nodes } from '../maps/consts' +import { TNode } from '../types' + +export const getNode = (node: T): (typeof nodes)[T] => nodes[node] diff --git a/packages/sdk/src/utils.test.ts b/packages/sdk/src/utils/getNodeEndpointOption.test.ts similarity index 57% rename from packages/sdk/src/utils.test.ts rename to packages/sdk/src/utils/getNodeEndpointOption.test.ts index 1dca879c..e8942871 100644 --- a/packages/sdk/src/utils.test.ts +++ b/packages/sdk/src/utils/getNodeEndpointOption.test.ts @@ -1,19 +1,6 @@ -// Contains tests for important utils features that are used during call creation - -import { describe, expect, it } from 'vitest' -import { NODE_NAMES, NODE_NAMES_DOT_KSM } from './maps/consts' -import { getNode, getNodeEndpointOption } from './utils' - -describe('getNodeDetails', () => { - it('should return node detail for all nodes', () => { - NODE_NAMES.forEach(node => { - const details = getNode(node) - expect(details).toBeDefined() - expect(details.name).toBeTypeOf('string') - expect(['polkadot', 'kusama']).toContain(details.type) - }) - }) -}) +import { describe, it, expect } from 'vitest' +import { NODE_NAMES_DOT_KSM } from '../maps/consts' +import { getNodeEndpointOption } from './getNodeEndpointOption' describe('getNodeEndpointOption', () => { it('should return endpoint option for all nodes', () => { diff --git a/packages/sdk/src/utils/getNodeEndpointOption.ts b/packages/sdk/src/utils/getNodeEndpointOption.ts new file mode 100644 index 00000000..a3c848b4 --- /dev/null +++ b/packages/sdk/src/utils/getNodeEndpointOption.ts @@ -0,0 +1,14 @@ +import { prodRelayKusama, prodRelayPolkadot } from '@polkadot/apps-config' +import { TNodePolkadotKusama } from '../types' +import { getNode } from './getNode' + +export const getNodeEndpointOption = (node: TNodePolkadotKusama) => { + const { type, name } = getNode(node) + const { linked } = type === 'polkadot' ? prodRelayPolkadot : prodRelayKusama + + if (linked === undefined) return undefined + + const preferredOption = linked.find(o => o.info === name && Object.values(o.providers).length > 0) + + return preferredOption ?? linked.find(o => o.info === name) +} diff --git a/packages/sdk/src/utils/getNodeProvider.test.ts b/packages/sdk/src/utils/getNodeProvider.test.ts new file mode 100644 index 00000000..6dad83e7 --- /dev/null +++ b/packages/sdk/src/utils/getNodeProvider.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect, vi } from 'vitest' +import { TNodeWithRelayChains } from '../types' +import { getNode, getNodeProvider } from '.' + +vi.mock('@polkadot/apps-config', () => ({ + prodRelayPolkadot: { providers: ['https://polkadotProvider.com'] }, + prodRelayKusama: { providers: ['https://kusamaProvider.com'] } +})) + +vi.mock('./getNode', () => ({ + getNode: vi.fn().mockReturnValue({ + getProvider: () => 'https://otherNodeProvider.com' + }) +})) + +describe('getNodeProvider', () => { + it('should return the first provider for Polkadot', () => { + const node = 'Polkadot' as TNodeWithRelayChains + const result = getNodeProvider(node) + expect(result).toBe('https://polkadotProvider.com') + }) + + it('should return the first provider for Kusama', () => { + const node = 'Kusama' as TNodeWithRelayChains + const result = getNodeProvider(node) + expect(result).toBe('https://kusamaProvider.com') + }) + + it('should call getNode and return the provider for other nodes', () => { + const node = 'SomeOtherNode' as TNodeWithRelayChains + const mockNodeProvider = 'https://otherNodeProvider.com' + + const result = getNodeProvider(node) + expect(result).toBe(mockNodeProvider) + expect(getNode).toHaveBeenCalledWith(node) + }) +}) diff --git a/packages/sdk/src/utils/getNodeProvider.ts b/packages/sdk/src/utils/getNodeProvider.ts new file mode 100644 index 00000000..3134a882 --- /dev/null +++ b/packages/sdk/src/utils/getNodeProvider.ts @@ -0,0 +1,12 @@ +import { prodRelayKusama, prodRelayPolkadot } from '@polkadot/apps-config' +import { TNodeWithRelayChains } from '../types' +import { getNode } from '.' + +export const getNodeProvider = (node: TNodeWithRelayChains): string => { + if (node === 'Polkadot') { + return prodRelayPolkadot.providers[0] + } else if (node === 'Kusama') { + return prodRelayKusama.providers[0] + } + return getNode(node).getProvider() +} diff --git a/packages/sdk/src/utils/index.ts b/packages/sdk/src/utils/index.ts new file mode 100644 index 00000000..464f2d97 --- /dev/null +++ b/packages/sdk/src/utils/index.ts @@ -0,0 +1,32 @@ +// Contains important call creation utils (Selection of fees,formating of header and more.. ) + +import { ApiPromise } from '@polkadot/api' +import { HexString } from '@polkadot/util/types' +import { TNodeDotKsmWithRelayChains, TNodeWithRelayChains } from '../types' +import { getRelayChainSymbol } from '../pallets/assets' + +export const createAccID = (api: ApiPromise, account: string): HexString => { + console.log('Generating AccountId32 address') + return api.createType('AccountId32', account).toHex() +} + +export const determineRelayChain = (node: TNodeWithRelayChains): TNodeDotKsmWithRelayChains => + getRelayChainSymbol(node) === 'KSM' ? 'Kusama' : 'Polkadot' + +export const isRelayChain = (node: TNodeWithRelayChains): boolean => + node === 'Polkadot' || node === 'Kusama' + +export { createX1Payload } from './createX1Payload' +export { deepEqual } from './deepEqual' +export { generateAddressMultiLocationV4 } from './generateAddressMultiLocationV4' +export { generateAddressPayload } from './generateAddressPayload' +export { getNodeProvider } from './getNodeProvider' +export { getAllNodeProviders } from './getAllNodeProviders' +export { getFees } from './getFees' +export { verifyMultiLocation } from './verifyMultiLocation' +export { callPolkadotJsTxFunction } from './callPolkadotJsTxFunction' +export { getNode } from './getNode' +export { createApiInstanceForNode } from './createApiInstanceForNode' +export { getNodeEndpointOption } from './getNodeEndpointOption' +export { createApiInstance } from './createApiInstance' +export { determineRelayChainSymbol } from './determineRelayChainSymbol' diff --git a/packages/sdk/src/utils/verifyMultilocation.ts b/packages/sdk/src/utils/verifyMultiLocation.ts similarity index 100% rename from packages/sdk/src/utils/verifyMultilocation.ts rename to packages/sdk/src/utils/verifyMultiLocation.ts diff --git a/packages/sdk/src/utils/verifyMultilocation.test.ts b/packages/sdk/src/utils/verifyMultilocation.test.ts index ebe799b5..ae968e63 100644 --- a/packages/sdk/src/utils/verifyMultilocation.test.ts +++ b/packages/sdk/src/utils/verifyMultilocation.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi } from 'vitest' import * as deepEqual from './deepEqual' import { getAssetsObject } from '../pallets/assets' import { TNode, TMultiLocation, TNodeAssets } from '../types' -import { verifyMultiLocation } from './verifyMultilocation' +import { verifyMultiLocation } from './verifyMultiLocation' vi.mock('../pallets/assets', () => ({ getAssetsObject: vi.fn() diff --git a/packages/sdk/vitest.config.ts b/packages/sdk/vitest.config.ts index bcb643ff..50b37853 100644 --- a/packages/sdk/vitest.config.ts +++ b/packages/sdk/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ include: ['src/**/*.test.ts'], coverage: { include: ['src/**/*.ts'], - exclude: ['scripts/**/*', 'src/**/*.test.ts'] + exclude: ['scripts/**/*', 'src/**/*.test.ts', 'src/types/*'] } } }) diff --git a/packages/xcm-analyser/vitest.config.ts b/packages/xcm-analyser/vitest.config.ts index 6ec74eee..95ff7044 100644 --- a/packages/xcm-analyser/vitest.config.ts +++ b/packages/xcm-analyser/vitest.config.ts @@ -3,5 +3,9 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { include: ['src/**/*.test.ts'], + coverage: { + include: ['src/**/*.ts'], + exclude: ['scripts/**'], + }, }, }); diff --git a/packages/xcm-router/src/dexNodes/Hydration/utils.test.ts b/packages/xcm-router/src/dexNodes/Hydration/utils.test.ts index d6bd98c7..4cea550c 100644 --- a/packages/xcm-router/src/dexNodes/Hydration/utils.test.ts +++ b/packages/xcm-router/src/dexNodes/Hydration/utils.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/unbound-method */ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Asset, TradeRouter } from '@galacticcouncil/sdk'; import { TCurrencyCore } from '@paraspell/sdk'; @@ -20,33 +19,33 @@ describe('getAssetInfo', () => { }); it('should return asset by symbol if found', async () => { - vi.mocked(mockTradeRouter.getAllAssets).mockResolvedValue(mockAssets); + const spy = vi.spyOn(mockTradeRouter, 'getAllAssets').mockResolvedValue(mockAssets); const currency: TCurrencyCore = { symbol: 'BTC' }; const asset = await getAssetInfo(mockTradeRouter, currency); expect(asset).toEqual(mockAssets[0]); - expect(mockTradeRouter.getAllAssets).toHaveBeenCalledOnce(); + expect(spy).toHaveBeenCalledOnce(); }); it('should return asset by id if found', async () => { - vi.mocked(mockTradeRouter.getAllAssets).mockResolvedValue(mockAssets); + const spy = vi.spyOn(mockTradeRouter, 'getAllAssets').mockResolvedValue(mockAssets); const currency: TCurrencyCore = { id: '2' }; const asset = await getAssetInfo(mockTradeRouter, currency); expect(asset).toEqual(mockAssets[1]); - expect(mockTradeRouter.getAllAssets).toHaveBeenCalledOnce(); + expect(spy).toHaveBeenCalledOnce(); }); it('should return undefined if asset is not found', async () => { - vi.mocked(mockTradeRouter.getAllAssets).mockResolvedValue(mockAssets); + const spy = vi.spyOn(mockTradeRouter, 'getAllAssets').mockResolvedValue(mockAssets); const currency: TCurrencyCore = { symbol: 'XRP' }; // Non-existent symbol const asset = await getAssetInfo(mockTradeRouter, currency); expect(asset).toBeUndefined(); - expect(mockTradeRouter.getAllAssets).toHaveBeenCalledOnce(); + expect(spy).toHaveBeenCalledOnce(); }); it('should throw an error if duplicate assets are found by symbol', async () => { @@ -54,7 +53,7 @@ describe('getAssetInfo', () => { { id: '1', symbol: 'BTC' } as unknown as Asset, { id: '4', symbol: 'BTC' } as unknown as Asset, ]; - vi.mocked(mockTradeRouter.getAllAssets).mockResolvedValue(duplicateAssets); + vi.spyOn(mockTradeRouter, 'getAllAssets').mockResolvedValue(duplicateAssets); const currency: TCurrencyCore = { symbol: 'BTC' }; @@ -68,7 +67,7 @@ describe('getAssetInfo', () => { { id: '1', symbol: 'BTC' } as unknown as Asset, { id: '1', symbol: 'ETH' } as unknown as Asset, ]; - vi.mocked(mockTradeRouter.getAllAssets).mockResolvedValue(duplicateAssets); + vi.spyOn(mockTradeRouter, 'getAllAssets').mockResolvedValue(duplicateAssets); const currency: TCurrencyCore = { id: '1' };