From a986bf886f977499057a5ed49cb078953b770330 Mon Sep 17 00:00:00 2001 From: Michael Absolon Date: Thu, 31 Oct 2024 23:41:34 +0100 Subject: [PATCH] =?UTF-8?q?fix(sdk):=20Fix=20Moonbeam=20'xc'=20prefix=20ha?= =?UTF-8?q?ndling=20=F0=9F=94=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playground/src/components/XcmTransfer.tsx | 1 - packages/sdk/scripts/assets/fetchAssets.ts | 17 +-- packages/sdk/src/maps/assets.json | 136 +++++++++--------- .../nodes/supported/BifrostPolkadot.test.ts | 135 ++++++++++++++--- .../src/nodes/supported/BifrostPolkadot.ts | 56 +++++++- packages/sdk/src/pallets/assets/assets.ts | 19 ++- .../sdk/src/pallets/assets/assetsUtils.ts | 66 ++++++++- .../assets/getAssetBySymbolOrId.test.ts | 99 ++++++++++++- .../assets/getOriginFeeDetails.test.ts | 30 ++-- .../src/pallets/assets/getOriginFeeDetails.ts | 19 +-- .../src/pallets/assets/getSupportedAssets.ts | 20 ++- .../assets/transfer-info/getTransferInfo.ts | 18 +-- .../sdk/src/pallets/xcmPallet/transfer.ts | 7 +- packages/sdk/src/papi/PapiApi.ts | 1 - packages/sdk/src/papi/assets.ts | 5 + packages/sdk/src/pjs/assets.ts | 6 + packages/sdk/src/types/TBalance.ts | 33 +++++ 17 files changed, 508 insertions(+), 160 deletions(-) diff --git a/apps/playground/src/components/XcmTransfer.tsx b/apps/playground/src/components/XcmTransfer.tsx index 227766bd..5d7cc2ae 100644 --- a/apps/playground/src/components/XcmTransfer.tsx +++ b/apps/playground/src/components/XcmTransfer.tsx @@ -138,7 +138,6 @@ const XcmTransfer = () => { .from(from) .to(to) .currency(determineCurrency(formValues)) - .feeAsset("0") .amount(amount) .address(address) .build(); diff --git a/packages/sdk/scripts/assets/fetchAssets.ts b/packages/sdk/scripts/assets/fetchAssets.ts index e1f3e5c3..cca30e0d 100644 --- a/packages/sdk/scripts/assets/fetchAssets.ts +++ b/packages/sdk/scripts/assets/fetchAssets.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -265,19 +263,6 @@ const fetchAssetsType2 = async (api: ApiPromise, query: string): Promise { - if (inputString.startsWith(prefix)) { - return inputString.substring(prefix.length) - } - return inputString -} - -const transformOtherAssets = (otherAssets: any, node: TNodePolkadotKusama) => { - return node === 'Moonbeam' || node === 'Moonriver' - ? otherAssets.map((asset: any) => ({ ...asset, symbol: removePrefix(asset.symbol, 'xc') })) - : otherAssets -} - const fetchNativeAsset = async (api: ApiPromise): Promise => { const propertiesRes = await api.rpc.system.properties() const json = propertiesRes.toHuman() @@ -420,7 +405,7 @@ const fetchNodeAssets = async ( return { nativeAssets, - otherAssets: transformOtherAssets(otherAssets, node), + otherAssets, nativeAssetSymbol } } diff --git a/packages/sdk/src/maps/assets.json b/packages/sdk/src/maps/assets.json index f8f5b9e0..473497a3 100644 --- a/packages/sdk/src/maps/assets.json +++ b/packages/sdk/src/maps/assets.json @@ -1317,22 +1317,22 @@ "otherAssets": [ { "assetId": "78407957940239408223554844611219482002", - "symbol": "ibcMOVR", + "symbol": "xcibcMOVR", "decimals": 18 }, { "assetId": "150874409661081770150564009349448205842", - "symbol": "ZTG", + "symbol": "xcZTG", "decimals": 10 }, { "assetId": "283870493414747423842723289889816153538", - "symbol": "UNQ", + "symbol": "xcUNQ", "decimals": 18 }, { "assetId": "90225766094594282577230355136633846906", - "symbol": "PDEX", + "symbol": "xcPDEX", "decimals": 12 }, { @@ -1342,27 +1342,27 @@ }, { "assetId": "314077021455772878282433861213184736939", - "symbol": "PEAQ", + "symbol": "xcPEAQ", "decimals": 18 }, { "assetId": "144012926827374458669278577633504620722", - "symbol": "FIL", + "symbol": "xcFIL", "decimals": 18 }, { "assetId": "125699734534028342599692732320197985871", - "symbol": "RING", + "symbol": "xcRING", "decimals": 18 }, { "assetId": "91372035960551235635465443179559840483", - "symbol": "CFG", + "symbol": "xcCFG", "decimals": 18 }, { "assetId": "187224307232923873519830480073807488153", - "symbol": "EQD", + "symbol": "xcEQD", "decimals": 9 }, { @@ -1372,37 +1372,37 @@ }, { "assetId": "69606720909260275826784788104880799692", - "symbol": "HDX", + "symbol": "xcHDX", "decimals": 12 }, { "assetId": "133307414193833606001516599592873928539", - "symbol": "ibcTIA", + "symbol": "xcibcTIA", "decimals": 6 }, { "assetId": "190590555344745888270686124937537713878", - "symbol": "EQ", + "symbol": "xcEQ", "decimals": 9 }, { "assetId": "61295607754960722617854661686514597014", - "symbol": "WIFD", + "symbol": "xcWIFD", "decimals": 10 }, { "assetId": "141196559012917796508928734717797136690", - "symbol": "ibcIST", + "symbol": "xcibcIST", "decimals": 6 }, { "assetId": "29085784439601774464560083082574142143", - "symbol": "vDOT", + "symbol": "xcvDOT", "decimals": 10 }, { "assetId": "225719522181998468294117309041779353812", - "symbol": "LDOT", + "symbol": "xcLDOT", "decimals": 10 }, { @@ -1412,142 +1412,142 @@ }, { "assetId": "124463719055550872076363892993240202694", - "symbol": "DED", + "symbol": "xcDED", "decimals": 10 }, { "assetId": "224077081838586484055667086558292981199", - "symbol": "ASTR", + "symbol": "xcASTR", "decimals": 18 }, { "assetId": "199907282886248358976504623107230837230", - "symbol": "ibcBLD", + "symbol": "xcibcBLD", "decimals": 6 }, { "assetId": "166377000701797186346254371275954761085", - "symbol": "USDC", + "symbol": "xcUSDC", "decimals": 6 }, { "assetId": "142155548796783636521833385094843759961", - "symbol": "BNCS", + "symbol": "xcBNCS", "decimals": 12 }, { "assetId": "89994634370519791027168048838578580624", - "symbol": "SUB", + "symbol": "xcSUB", "decimals": 10 }, { "assetId": "138280378441551394289980644963240827219", - "symbol": "ibcATOM", + "symbol": "xcibcATOM", "decimals": 6 }, { "assetId": "228510780171552721666262089780561563481", - "symbol": "ibcPICA", + "symbol": "xcibcPICA", "decimals": 12 }, { "assetId": "204507659831918931608354793288110796652", - "symbol": "vGLMR", + "symbol": "xcvGLMR", "decimals": 18 }, { "assetId": "184218609779515850660274730699350567246", - "symbol": "NCTR", + "symbol": "xcNCTR", "decimals": 18 }, { "assetId": "289989900872525819559124583375550296953", - "symbol": "vMANTA", + "symbol": "xcvMANTA", "decimals": 18 }, { "assetId": "110021739665376159354538090254163045594", - "symbol": "aUSD", + "symbol": "xcaUSD", "decimals": 12 }, { "assetId": "311091173110107856861649819128533077277", - "symbol": "USDT", + "symbol": "xcUSDT", "decimals": 6 }, { "assetId": "64174511183114006009298114091987195453", - "symbol": "PINK", + "symbol": "xcPINK", "decimals": 10 }, { "assetId": "309163521958167876851250718453738106865", - "symbol": "NODL", + "symbol": "xcNODL", "decimals": 11 }, { "assetId": "120637696315203257380661607956669368914", - "symbol": "IBTC", + "symbol": "xcIBTC", "decimals": 8 }, { "assetId": "166446646689194205559791995948102903873", - "symbol": "MANTA", + "symbol": "xcMANTA", "decimals": 18 }, { "assetId": "101170542313601871197860408087030232491", - "symbol": "INTR", + "symbol": "xcINTR", "decimals": 10 }, { "assetId": "165823357460190568952172802245839421906", - "symbol": "BNC", + "symbol": "xcBNC", "decimals": 12 }, { "assetId": "32615670524745285411807346420584982855", - "symbol": "PARA", + "symbol": "xcPARA", "decimals": 12 }, { "assetId": "42259045809535163221576417993425387648", - "symbol": "DOT", + "symbol": "xcDOT", "decimals": 10 }, { "assetId": "45647473099451451833602657905356404688", - "symbol": "PEN", + "symbol": "xcPEN", "decimals": 12 }, { "assetId": "224821240862170613278369189818311486111", - "symbol": "ACA", + "symbol": "xcACA", "decimals": 12 }, { "assetId": "272547899416482196831721420898811311297", - "symbol": "vFIL", + "symbol": "xcvFIL", "decimals": 18 }, { "assetId": "114018676402354620972806895487280206446", - "symbol": "vASTR", + "symbol": "xcvASTR", "decimals": 18 }, { "assetId": "238111524681612888331172110363070489924", - "symbol": "NEURO", + "symbol": "xcNEURO", "decimals": 12 }, { "assetId": "112679793397406599376365943185137098326", - "symbol": "STINK", + "symbol": "xcSTINK", "decimals": 10 }, { "assetId": "132685552157663328694213725410064821485", - "symbol": "PHA", + "symbol": "xcPHA", "decimals": 12 } ] @@ -4715,122 +4715,122 @@ "otherAssets": [ { "assetId": "108457044225666871745333730479173774551", - "symbol": "CSM", + "symbol": "xcCSM", "decimals": 12 }, { "assetId": "16797826370226091782818345603793389938", - "symbol": "SDN", + "symbol": "xcSDN", "decimals": 18 }, { "assetId": "76100021443485661246318545281171740067", - "symbol": "HKO", + "symbol": "xcHKO", "decimals": 12 }, { "assetId": "138512078356357941985706694377215053953", - "symbol": "TNKR", + "symbol": "xcTNKR", "decimals": 12 }, { "assetId": "328179947973504579459046439826496046832", - "symbol": "KBTC", + "symbol": "xcKBTC", "decimals": 8 }, { "assetId": "108036400430056508975016746969135344601", - "symbol": "XRT", + "symbol": "xcXRT", "decimals": 9 }, { "assetId": "133300872918374599700079037156071917454", - "symbol": "TUR", + "symbol": "xcTUR", "decimals": 10 }, { "assetId": "213357169630950964874127107356898319277", - "symbol": "KMA", + "symbol": "xcKMA", "decimals": 12 }, { "assetId": "65216491554813189869575508812319036608", - "symbol": "LIT", + "symbol": "xcLIT", "decimals": 12 }, { "assetId": "173481220575862801646329923366065693029", - "symbol": "CRAB", + "symbol": "xcCRAB", "decimals": 18 }, { "assetId": "189307976387032586987344677431204943363", - "symbol": "PHA", + "symbol": "xcPHA", "decimals": 12 }, { "assetId": "264344629840762281112027368930249420542", - "symbol": "vKSM", + "symbol": "xcvKSM", "decimals": 12 }, { "assetId": "72145018963825376852137222787619937732", - "symbol": "vBNC", + "symbol": "xcvBNC", "decimals": 12 }, { "assetId": "214920334981412447805621250067209749032", - "symbol": "aSeed", + "symbol": "xcaSeed", "decimals": 12 }, { "assetId": "175400718394635817552109270754364440562", - "symbol": "KINT", + "symbol": "xcKINT", "decimals": 12 }, { "assetId": "167283995827706324502761431814209211090", - "symbol": "PICA", + "symbol": "xcPICA", "decimals": 12 }, { "assetId": "105075627293246237499203909093923548958", - "symbol": "TEER", + "symbol": "xcTEER", "decimals": 12 }, { "assetId": "118095707745084482624853002839493125353", - "symbol": "MGX", + "symbol": "xcMGX", "decimals": 18 }, { "assetId": "203223821023327994093278529517083736593", - "symbol": "vMOVR", + "symbol": "xcvMOVR", "decimals": 18 }, { "assetId": "311091173110107856861649819128533077277", - "symbol": "USDT", + "symbol": "xcUSDT", "decimals": 6 }, { "assetId": "182365888117048807484804376330534607370", - "symbol": "RMRK", + "symbol": "xcRMRK", "decimals": 10 }, { "assetId": "42259045809535163221576417993425387648", - "symbol": "KSM", + "symbol": "xcKSM", "decimals": 12 }, { "assetId": "10810581592933651521121702237638664357", - "symbol": "KAR", + "symbol": "xcKAR", "decimals": 12 }, { "assetId": "319623561105283008236062145480775032445", - "symbol": "BNC", + "symbol": "xcBNC", "decimals": 12 } ] diff --git a/packages/sdk/src/nodes/supported/BifrostPolkadot.test.ts b/packages/sdk/src/nodes/supported/BifrostPolkadot.test.ts index b5d4f79c..f6c2cf34 100644 --- a/packages/sdk/src/nodes/supported/BifrostPolkadot.test.ts +++ b/packages/sdk/src/nodes/supported/BifrostPolkadot.test.ts @@ -1,11 +1,18 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' -import type { XTokensTransferInput } from '../../types' -import { Version } from '../../types' +import type { + XTokensTransferInput, + PolkadotXCMTransferInput, + TSendInternalOptions +} from '../../types' +import { Parents, Version } from '../../types' import XTokensTransferImpl from '../xTokens' +import PolkadotXCMTransferImpl from '../polkadotXcm' import type { BifrostPolkadot } from './BifrostPolkadot' import { getNode } from '../../utils' import type { ApiPromise } from '@polkadot/api' import type { Extrinsic } from '../../pjs/types' +import { createCurrencySpec } from '../../pallets/xcmPallet/utils' +import { getAssetId } from '../../pallets/assets' vi.mock('../xTokens', () => ({ default: { @@ -13,13 +20,24 @@ vi.mock('../xTokens', () => ({ } })) +vi.mock('../polkadotXcm', () => ({ + default: { + transferPolkadotXCM: vi.fn() + } +})) + describe('BifrostPolkadot', () => { let bifrostPolkadot: BifrostPolkadot - const mockInput = { + const mockXTokensInput = { currency: 'BNC', amount: '100' } as XTokensTransferInput + const mockPolkadotXCMInput = { + amount: '200', + currencySymbol: 'WETH' + } as PolkadotXCMTransferInput + beforeEach(() => { bifrostPolkadot = getNode('BifrostPolkadot') }) @@ -35,38 +53,109 @@ describe('BifrostPolkadot', () => { const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') vi.spyOn(bifrostPolkadot, 'getNativeAssetSymbol').mockReturnValue('BNC') - bifrostPolkadot.transferXTokens(mockInput) + bifrostPolkadot.transferXTokens(mockXTokensInput) - expect(spy).toHaveBeenCalledWith(mockInput, { Native: 'BNC' }) + expect(spy).toHaveBeenCalledWith(mockXTokensInput, { 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') + it('should call transferPolkadotXCM with correct parameters for WETH transfer', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') + const ETH_CHAIN_ID = BigInt(1) + const ethJunction = { + GlobalConsensus: { Ethereum: { chain_id: ETH_CHAIN_ID } } + } - bifrostPolkadot.transferXTokens(mockInput) + bifrostPolkadot.transferPolkadotXCM(mockPolkadotXCMInput) - expect(spy).toHaveBeenCalledWith(mockInput, { Token: 'BNC' }) + expect(spy).toHaveBeenCalledWith( + { + ...mockPolkadotXCMInput, + currencySelection: createCurrencySpec( + mockPolkadotXCMInput.amount, + Version.V3, + 2, // Parents.TWO + mockPolkadotXCMInput.overridedCurrency, + { + X2: [ + ethJunction, + { + AccountKey20: { key: getAssetId('Ethereum', 'WETH') ?? '' } + } + ] + } + ) + }, + 'transfer_assets', + 'Unlimited' + ) }) - it('should call transferXTokens with VSToken', () => { - const spy = vi.spyOn(XTokensTransferImpl, 'transferXTokens') - vi.spyOn(bifrostPolkadot, 'getNativeAssetSymbol').mockReturnValue('NOT_BNC') - - const input = { - ...mockInput, - currency: 'vsDOT', - currencyID: '0' - } + it('should call transferPolkadotXCM with correct parameters for DOT transfer', () => { + const spy = vi.spyOn(PolkadotXCMTransferImpl, 'transferPolkadotXCM') - bifrostPolkadot.transferXTokens(input) + bifrostPolkadot.transferPolkadotXCM({ ...mockPolkadotXCMInput, currencySymbol: 'DOT' }) - expect(spy).toHaveBeenCalledWith(input, { VSToken2: 0 }) + expect(spy).toHaveBeenCalledWith( + { + ...mockPolkadotXCMInput, + currencySymbol: 'DOT', + currencySelection: createCurrencySpec( + mockPolkadotXCMInput.amount, + Version.V3, + Parents.ONE, + mockPolkadotXCMInput.overridedCurrency + ) + }, + 'transfer_assets', + 'Unlimited' + ) }) - it('should throw error when currency symbol is undefined', () => { + it('should throw error when currency symbol is undefined in transferXTokens', () => { expect(() => - bifrostPolkadot.transferXTokens({ ...mockInput, currency: undefined }) + bifrostPolkadot.transferXTokens({ ...mockXTokensInput, currency: undefined }) ).toThrowError('Currency symbol is undefined') }) + + describe('canUseXTokens', () => { + it('should return false when currencySymbol is WETH and destination is AssetHubPolkadot', () => { + const options = { + currencySymbol: 'WETH', + destination: 'AssetHubPolkadot' + } as TSendInternalOptions + expect(bifrostPolkadot['canUseXTokens'](options)).toBe(false) + }) + + it('should return false when currencySymbol is DOT and destination is AssetHubPolkadot', () => { + const options: TSendInternalOptions = { + currencySymbol: 'DOT', + destination: 'AssetHubPolkadot' + } as TSendInternalOptions + expect(bifrostPolkadot['canUseXTokens'](options)).toBe(false) + }) + + it('should return true when currencySymbol is not WETH or DOT and destination is AssetHubPolkadot', () => { + const options: TSendInternalOptions = { + currencySymbol: 'BNC', + destination: 'AssetHubPolkadot' + } as TSendInternalOptions + expect(bifrostPolkadot['canUseXTokens'](options)).toBe(true) + }) + + it('should return true when currencySymbol is WETH but destination is not AssetHubPolkadot', () => { + const options: TSendInternalOptions = { + currencySymbol: 'WETH', + destination: 'Acala' + } as TSendInternalOptions + expect(bifrostPolkadot['canUseXTokens'](options)).toBe(true) + }) + + it('should return true when currencySymbol is DOT but destination is not AssetHubPolkadot', () => { + const options: TSendInternalOptions = { + currencySymbol: 'DOT', + destination: 'Acala' + } as TSendInternalOptions + expect(bifrostPolkadot['canUseXTokens'](options)).toBe(true) + }) + }) }) diff --git a/packages/sdk/src/nodes/supported/BifrostPolkadot.ts b/packages/sdk/src/nodes/supported/BifrostPolkadot.ts index 5e9889fd..250e677c 100644 --- a/packages/sdk/src/nodes/supported/BifrostPolkadot.ts +++ b/packages/sdk/src/nodes/supported/BifrostPolkadot.ts @@ -1,12 +1,21 @@ // Contains detailed structure of XCM call construction for Bifrost Parachain on Polkadot -import { type IXTokensTransfer, Version, type XTokensTransferInput } from '../../types' +import { createCurrencySpec } from '../../pallets/xcmPallet/utils' +import { getAssetId } from '../../pallets/assets' +import type { + IPolkadotXCMTransfer, + PolkadotXCMTransferInput, + TJunction, + TSendInternalOptions +} from '../../types' +import { type IXTokensTransfer, Parents, Version, type XTokensTransferInput } from '../../types' import ParachainNode from '../ParachainNode' +import PolkadotXCMTransferImpl from '../polkadotXcm' import XTokensTransferImpl from '../xTokens' export class BifrostPolkadot extends ParachainNode - implements IXTokensTransfer + implements IXTokensTransfer, IPolkadotXCMTransfer { constructor() { super('BifrostPolkadot', 'bifrost', 'polkadot', Version.V3) @@ -44,4 +53,47 @@ export class BifrostPolkadot const currencySelection = this.getCurrencySelection(currency, currencyID) return XTokensTransferImpl.transferXTokens(input, currencySelection) } + + // Handles DOT, WETH transfers to AssetHubPolkadot + transferPolkadotXCM(input: PolkadotXCMTransferInput) { + const { amount, overridedCurrency, currencySymbol } = input + + const ETH_CHAIN_ID = BigInt(1) + const ethJunction: TJunction = { + GlobalConsensus: { Ethereum: { chain_id: ETH_CHAIN_ID } } + } + + return PolkadotXCMTransferImpl.transferPolkadotXCM( + { + ...input, + currencySelection: createCurrencySpec( + amount, + this.version, + currencySymbol === 'DOT' ? Parents.ONE : Parents.TWO, + overridedCurrency, + currencySymbol === 'WETH' + ? { + X2: [ + ethJunction, + { + AccountKey20: { key: getAssetId('Ethereum', 'WETH') ?? '' } + } + ] + } + : undefined + ) + }, + 'transfer_assets', + 'Unlimited' + ) + } + + protected canUseXTokens({ + currencySymbol, + destination + }: TSendInternalOptions): boolean { + return ( + (currencySymbol !== 'WETH' && currencySymbol !== 'DOT') || destination !== 'AssetHubPolkadot' + ) + } } diff --git a/packages/sdk/src/pallets/assets/assets.ts b/packages/sdk/src/pallets/assets/assets.ts index d7accce0..6337ebc6 100644 --- a/packages/sdk/src/pallets/assets/assets.ts +++ b/packages/sdk/src/pallets/assets/assets.ts @@ -118,11 +118,22 @@ export const getNativeAssetSymbol = (node: TNodeWithRelayChains): string => { * @param symbol - The symbol of the asset to check. * @returns True if the asset is supported; otherwise, false. */ -export const hasSupportForAsset = (node: TNode, symbol: string): boolean => - getAllAssetsSymbols(node) - .map(s => s.toLowerCase()) - .includes(symbol.toLowerCase()) +export const hasSupportForAsset = (node: TNode, symbol: string): boolean => { + const lowerSymbol = symbol.toLowerCase() + const symbolsToCheck = new Set() + symbolsToCheck.add(lowerSymbol) + + if (lowerSymbol.startsWith('xc')) { + symbolsToCheck.add(lowerSymbol.substring(2)) + } else { + symbolsToCheck.add(`xc${lowerSymbol}`) + } + + const nodeSymbols = getAllAssetsSymbols(node).map(s => s.toLowerCase()) + + return nodeSymbols.some(nodeSymbol => symbolsToCheck.has(nodeSymbol)) +} /** * Retrieves the number of decimals for an asset with the given symbol on a specified node. * diff --git a/packages/sdk/src/pallets/assets/assetsUtils.ts b/packages/sdk/src/pallets/assets/assetsUtils.ts index b1f65f8f..7781a6f4 100644 --- a/packages/sdk/src/pallets/assets/assetsUtils.ts +++ b/packages/sdk/src/pallets/assets/assetsUtils.ts @@ -1,6 +1,6 @@ // Contains function for getting Asset ID or Symbol used in XCM call creation -import { DuplicateAssetError, DuplicateAssetIdError } from '../../errors' +import { DuplicateAssetError, DuplicateAssetIdError, InvalidCurrencyError } from '../../errors' import type { TAsset, TAssetDetails, @@ -21,18 +21,20 @@ export const findAssetBySymbol = ( symbol: string, isRelayDestination: boolean ) => { + const lowerSymbol = symbol.toLowerCase() + if (destination === 'Ethereum') { return combinedAssets.find( - ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === symbol.toLowerCase() + ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === lowerSymbol ) } - const otherAssetsMatches = otherAssets.filter( - ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === symbol.toLowerCase() + let otherAssetsMatches = otherAssets.filter( + ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === lowerSymbol ) - const nativeAssetsMatches = nativeAssets.filter( - ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === symbol.toLowerCase() + let nativeAssetsMatches = nativeAssets.filter( + ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === lowerSymbol ) const isPolkadotXcm = @@ -40,6 +42,58 @@ export const findAssetBySymbol = ( node !== 'Ethereum' && getDefaultPallet(node as TNodePolkadotKusama) === 'PolkadotXcm' + if (otherAssetsMatches.length === 0 && nativeAssetsMatches.length === 0) { + if (lowerSymbol.startsWith('xc')) { + // No exact matches found, and symbol starts with 'xc', try stripping 'xc' + const strippedSymbol = symbol.substring(2) + const strippedLowerSymbol = strippedSymbol.toLowerCase() + + otherAssetsMatches = otherAssets.filter( + ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === strippedLowerSymbol + ) + + nativeAssetsMatches = nativeAssets.filter( + ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === strippedLowerSymbol + ) + + if (node === 'Astar' || node === 'Shiden' || isPolkadotXcm) { + return nativeAssetsMatches[0] || otherAssetsMatches[0] || null + } + + const totalMatches = otherAssetsMatches.length + nativeAssetsMatches.length + + if (totalMatches > 1) { + throw new InvalidCurrencyError( + `Multiple assets found for symbol ${symbol} after stripping 'xc' prefix. Please specify by ID.` + ) + } + } else { + // No matches found, and symbol does not start with 'xc', try adding 'xc' prefix + const prefixedSymbol = `xc${symbol}` + const prefixedLowerSymbol = prefixedSymbol.toLowerCase() + + otherAssetsMatches = otherAssets.filter( + ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === prefixedLowerSymbol + ) + + nativeAssetsMatches = nativeAssets.filter( + ({ symbol: assetSymbol }) => assetSymbol?.toLowerCase() === prefixedLowerSymbol + ) + + if (node === 'Astar' || node === 'Shiden' || isPolkadotXcm) { + return nativeAssetsMatches[0] || otherAssetsMatches[0] || null + } + + const totalMatches = otherAssetsMatches.length + nativeAssetsMatches.length + + if (totalMatches > 1) { + throw new InvalidCurrencyError( + `Multiple assets found for symbol ${symbol} after adding 'xc' prefix. Please specify by ID.` + ) + } + } + } + if (node === 'Astar' || node === 'Shiden' || isPolkadotXcm) { return nativeAssetsMatches[0] || otherAssetsMatches[0] || null } diff --git a/packages/sdk/src/pallets/assets/getAssetBySymbolOrId.test.ts b/packages/sdk/src/pallets/assets/getAssetBySymbolOrId.test.ts index 2308c149..f601014f 100644 --- a/packages/sdk/src/pallets/assets/getAssetBySymbolOrId.test.ts +++ b/packages/sdk/src/pallets/assets/getAssetBySymbolOrId.test.ts @@ -1,14 +1,20 @@ // Contains tests for different Asset queries used in XCM call creation -import { describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { NODE_NAMES } from '../../maps/consts' -import { getAssetsObject } from './assets' import { getAssetBySymbolOrId } from './getAssetBySymbolOrId' +import * as assetFunctions from './assets' import { getDefaultPallet } from '../pallets' import { isRelayChain } from '../../utils' import type { TNodePolkadotKusama } from '../../types' +const getAssetsObject = assetFunctions.getAssetsObject + describe('getAssetBySymbolOrId', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + it('should return assetId and symbol for every foreign asset', () => { NODE_NAMES.forEach(node => { const { otherAssets } = getAssetsObject(node) @@ -95,4 +101,93 @@ describe('getAssetBySymbolOrId', () => { expect(asset).toHaveProperty('symbol') expect(asset).toHaveProperty('assetId') }) + + it('Should find asset starting with "xc" for Moonbeam', () => { + const asset = getAssetBySymbolOrId('Moonbeam', { symbol: 'xcZTG' }) + expect(asset).toHaveProperty('symbol') + expect(asset).toHaveProperty('assetId') + }) + + it('Should find asset starting with "xc" for Moonbeam', () => { + const asset = getAssetBySymbolOrId('Moonbeam', { symbol: 'xcWETH.e' }) + expect(asset).toHaveProperty('symbol') + expect(asset).toHaveProperty('assetId') + }) + + it('Should throw error when duplicate dot asset on Hydration', () => { + vi.spyOn(assetFunctions, 'getAssetsObject').mockResolvedValueOnce({ + nativeAssetSymbol: 'DOT', + relayChainAssetSymbol: 'DOT', + otherAssets: [ + { + assetId: '1', + symbol: 'DOT' + }, + { + assetId: '2', + symbol: 'DOT' + } + ], + nativeAssets: [] + }) + expect(() => getAssetBySymbolOrId('Hydration', { symbol: 'HDX' })).toThrow() + }) + + it('should throw error when multiple assets found for symbol after stripping "xc" prefix', () => { + vi.spyOn(assetFunctions, 'getAssetsObject').mockReturnValue({ + nativeAssetSymbol: 'DOT', + relayChainAssetSymbol: 'DOT', + otherAssets: [ + { + assetId: '1', + symbol: 'DOT' + }, + { + assetId: '2', + symbol: 'DOT' + } + ], + nativeAssets: [] + }) + expect(() => getAssetBySymbolOrId('Hydration', { symbol: 'xcDOT' })).toThrow() + }) + + it('should throw error when multiple assets found for symbol after adding "xc" prefix', () => { + vi.spyOn(assetFunctions, 'getAssetsObject').mockReturnValue({ + nativeAssetSymbol: 'DOT', + relayChainAssetSymbol: 'DOT', + otherAssets: [ + { + assetId: '1', + symbol: 'xcDOT' + }, + { + assetId: '2', + symbol: 'xcDOT' + } + ], + nativeAssets: [] + }) + expect(() => getAssetBySymbolOrId('Hydration', { symbol: 'DOT' })).toThrow() + }) + + it('Should find ethereum assets', () => { + const asset = getAssetBySymbolOrId('AssetHubPolkadot', { symbol: 'WETH' }, false, 'Ethereum') + expect(asset).toHaveProperty('symbol') + expect(asset).toHaveProperty('assetId') + }) + + it('Should return null when passing a multilocation currency', () => { + const asset = getAssetBySymbolOrId('Astar', { + multilocation: { + parents: 1, + interior: { + X1: { + PalletInstance: 1 + } + } + } + }) + expect(asset).toBeNull() + }) }) diff --git a/packages/sdk/src/pallets/assets/getOriginFeeDetails.test.ts b/packages/sdk/src/pallets/assets/getOriginFeeDetails.test.ts index eea4d3b4..5b41e1d4 100644 --- a/packages/sdk/src/pallets/assets/getOriginFeeDetails.test.ts +++ b/packages/sdk/src/pallets/assets/getOriginFeeDetails.test.ts @@ -54,14 +54,14 @@ describe('getOriginFeeDetails', () => { const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const result = await getOriginFeeDetails( - originNode, - destinationNode, + const result = await getOriginFeeDetails({ + origin: originNode, + destination: destinationNode, currency, amount, account, - apiMock - ) + api: apiMock + }) expect(result).toEqual({ sufficientForXCM: true, @@ -109,14 +109,14 @@ describe('getOriginFeeDetails', () => { const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const result = await getOriginFeeDetails( - originNode, - destinationNode, + const result = await getOriginFeeDetails({ + origin: originNode, + destination: destinationNode, currency, amount, account, - apiMock - ) + api: apiMock + }) expect(result).toEqual({ sufficientForXCM: true, @@ -164,14 +164,14 @@ describe('getOriginFeeDetails', () => { const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) - const result = await getOriginFeeDetails( - originNode, - destinationNode, + const result = await getOriginFeeDetails({ + origin: originNode, + destination: destinationNode, currency, amount, account, - apiMock - ) + api: apiMock + }) expect(result).toEqual({ sufficientForXCM: true, diff --git a/packages/sdk/src/pallets/assets/getOriginFeeDetails.ts b/packages/sdk/src/pallets/assets/getOriginFeeDetails.ts index e230699a..118c0409 100644 --- a/packages/sdk/src/pallets/assets/getOriginFeeDetails.ts +++ b/packages/sdk/src/pallets/assets/getOriginFeeDetails.ts @@ -5,6 +5,7 @@ import { getMinNativeTransferableAmount } from './getExistentialDeposit' import { isRelayChain } from '../../utils' import { Builder } from '../../builder' import type { IPolkadotApi } from '../../api/IPolkadotApi' +import type { TGetOriginFeeDetailsOptions } from '../../types/TBalance' const createTx = async ( originApi: IPolkadotApi, @@ -37,15 +38,15 @@ const createTx = async ( } } -export const getOriginFeeDetails = async ( - origin: TNodeDotKsmWithRelayChains, - destination: TNodeDotKsmWithRelayChains, - currency: TCurrencyCore, - amount: string, - account: string, - api: IPolkadotApi, - feeMarginPercentage: number = 10 -): Promise => { +export const getOriginFeeDetails = async ({ + api, + account, + amount, + currency, + origin: origin, + destination, + feeMarginPercentage = 10 +}: TGetOriginFeeDetailsOptions): Promise => { const nativeBalance = await getBalanceNative({ address: account, node: origin, diff --git a/packages/sdk/src/pallets/assets/getSupportedAssets.ts b/packages/sdk/src/pallets/assets/getSupportedAssets.ts index 86b521a8..5ef8cdee 100644 --- a/packages/sdk/src/pallets/assets/getSupportedAssets.ts +++ b/packages/sdk/src/pallets/assets/getSupportedAssets.ts @@ -4,6 +4,18 @@ import { getDefaultPallet } from '../pallets' import { getAssets, getOtherAssets } from './assets' import { getAssetBySymbolOrId } from './getAssetBySymbolOrId' +/** + * Normalizes an asset symbol by stripping the 'xc' prefix (if present) and converting it to lowercase. + * + * @param symbol - The symbol to normalize. + * @returns The normalized symbol. + */ +const normalizeSymbol = (symbol: string | undefined): string => { + if (!symbol) return '' + const lowerSymbol = symbol.toLowerCase() + return lowerSymbol.startsWith('xc') ? lowerSymbol.substring(2) : lowerSymbol +} + /** * Retrieves the list of assets that are supported for transfers between two specified nodes. * @@ -38,12 +50,14 @@ export const getSupportedAssets = ( ) { return getOtherAssets(destination).filter( asset => - originAssets.some(a => a.symbol?.toLowerCase() === asset.symbol?.toLowerCase()) && + originAssets.some(a => normalizeSymbol(a.symbol) === normalizeSymbol(asset.symbol)) && asset.assetId !== '' ) } - return originAssets.filter(asset => - destinationAssets.some(a => a.symbol?.toLocaleLowerCase() === asset.symbol?.toLowerCase()) + const supportedAssets = originAssets.filter(asset => + destinationAssets.some(a => normalizeSymbol(a.symbol) === normalizeSymbol(asset.symbol)) ) + + return supportedAssets } diff --git a/packages/sdk/src/pallets/assets/transfer-info/getTransferInfo.ts b/packages/sdk/src/pallets/assets/transfer-info/getTransferInfo.ts index d349d8a5..2adb5bf4 100644 --- a/packages/sdk/src/pallets/assets/transfer-info/getTransferInfo.ts +++ b/packages/sdk/src/pallets/assets/transfer-info/getTransferInfo.ts @@ -27,14 +27,14 @@ export const getTransferInfo = async ({ node: origin, api: originApi }) - const { xcmFee: destXcmFee } = await getOriginFeeDetails( + const { xcmFee: destXcmFee } = await getOriginFeeDetails({ origin, destination, currency, amount, - accountOrigin, - originApi - ) + account: accountOrigin, + api: originApi + }) const expectedBalanceAfterXCMDelivery = originBalance - destXcmFee const asset = @@ -69,14 +69,14 @@ export const getTransferInfo = async ({ api: originApi }), expectedBalanceAfterXCMFee: expectedBalanceAfterXCMDelivery, - xcmFee: await getOriginFeeDetails( - origin, + xcmFee: await getOriginFeeDetails({ + origin: origin, destination, currency, amount, - accountOrigin, - originApi - ), + account: accountOrigin, + api: originApi + }), existentialDeposit: BigInt(getExistentialDeposit(origin) ?? 0), asset: getNativeAssetSymbol(origin), minNativeTransferableAmount: getMinNativeTransferableAmount(origin), diff --git a/packages/sdk/src/pallets/xcmPallet/transfer.ts b/packages/sdk/src/pallets/xcmPallet/transfer.ts index 5d32e217..14416dba 100644 --- a/packages/sdk/src/pallets/xcmPallet/transfer.ts +++ b/packages/sdk/src/pallets/xcmPallet/transfer.ts @@ -119,9 +119,14 @@ const sendCommon = async ( if (!isBridge && isDestAssetHub && pallet === 'XTokens') { asset = getAssetBySymbolOrId(destination, currency, false, destination) + let nativeAssets = getNativeAssets(destination) + if (origin === 'BifrostPolkadot' && asset?.symbol === 'DOT') { + nativeAssets = nativeAssets.filter(nativeAsset => nativeAsset.symbol !== 'DOT') + } + if ( 'symbol' in currency && - getNativeAssets(destination).some( + nativeAssets.some( nativeAsset => nativeAsset.symbol.toLowerCase() === currency.symbol.toLowerCase() ) ) { diff --git a/packages/sdk/src/papi/PapiApi.ts b/packages/sdk/src/papi/PapiApi.ts index 16d917e8..a606a470 100644 --- a/packages/sdk/src/papi/PapiApi.ts +++ b/packages/sdk/src/papi/PapiApi.ts @@ -85,7 +85,6 @@ class PapiApi implements IPolkadotApi { async getBalanceForeignPolkadotXcm(address: string, id?: string): Promise { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const res = await this.api.getUnsafeApi().query.Assets.Account.getValue(id, address) - console.log(res) // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access return res && res.balance ? BigInt(res.balance) : BigInt(0) diff --git a/packages/sdk/src/papi/assets.ts b/packages/sdk/src/papi/assets.ts index 1dae1e1b..a34283f7 100644 --- a/packages/sdk/src/papi/assets.ts +++ b/packages/sdk/src/papi/assets.ts @@ -2,6 +2,7 @@ import { getBalanceNative as getBalanceNativeImpl } from '../pallets/assets/bala import { getBalanceForeign as getBalanceForeignImpl } from '../pallets/assets/balance/getBalanceForeign' import { getTransferInfo as getTransferInfoImpl } from '../pallets/assets/transfer-info/getTransferInfo' import { getAssetBalance as getAssetBalanceImpl } from '../pallets/assets/balance/getAssetBalance' +import { getOriginFeeDetails as getOriginFeeDetailsImpl } from '../pallets/assets/getOriginFeeDetails' import { default as claimAssetsImpl } from '../pallets/assets/asset-claim' import { createPapiApiCall } from './utils' import type { PolkadotClient } from 'polkadot-api' @@ -50,6 +51,10 @@ export const getAssetBalance = createPapiApiCall( */ export const claimAssets = createPapiApiCall(claimAssetsImpl) +export const getOriginFeeDetails = createPapiApiCall( + getOriginFeeDetailsImpl +) + export * from '../pallets/assets/assets' export * from '../pallets/assets/eds' export { getSupportedAssets } from '../pallets/assets/getSupportedAssets' diff --git a/packages/sdk/src/pjs/assets.ts b/packages/sdk/src/pjs/assets.ts index 3a65bdd4..5a573d46 100644 --- a/packages/sdk/src/pjs/assets.ts +++ b/packages/sdk/src/pjs/assets.ts @@ -3,6 +3,7 @@ import { getBalanceNative as getBalanceNativeImpl } from '../pallets/assets/bala import { getBalanceForeign as getBalanceForeignImpl } from '../pallets/assets/balance/getBalanceForeign' import { getTransferInfo as getTransferInfoImpl } from '../pallets/assets/transfer-info/getTransferInfo' import { getAssetBalance as getAssetBalanceImpl } from '../pallets/assets/balance/getAssetBalance' +import { getOriginFeeDetails as getOriginFeeDetailsImpl } from '../pallets/assets/getOriginFeeDetails' import { default as claimAssetsImpl } from '../pallets/assets/asset-claim' import type { Extrinsic } from './types' import { createPolkadotJsApiCall } from './utils' @@ -44,6 +45,11 @@ export const getAssetBalance = createPolkadotJsApiCall(getAssetBalanceImpl) +export const getOriginFeeDetails = createPolkadotJsApiCall( + getOriginFeeDetailsImpl +) + export * from '../pallets/assets/assets' export * from '../pallets/assets/eds' +export * from '../pallets/assets/getOriginFeeDetails' export { getSupportedAssets } from '../pallets/assets/getSupportedAssets' diff --git a/packages/sdk/src/types/TBalance.ts b/packages/sdk/src/types/TBalance.ts index 7a26c4c1..8b80f132 100644 --- a/packages/sdk/src/types/TBalance.ts +++ b/packages/sdk/src/types/TBalance.ts @@ -65,3 +65,36 @@ export type TGetAssetBalanceOptionsBase = { } export type TGetAssetBalanceOptions = WithApi + +export type TGetOriginFeeDetailsOptionsBase = { + /** + * The origin node. + */ + origin: TNodeDotKsmWithRelayChains + /** + * The destination node. + */ + destination: TNodeDotKsmWithRelayChains + /** + * The currency to transfer. + */ + currency: TCurrencyCore + /** + * The amount to transfer. + */ + amount: string + /** + * The account to transfer from. + */ + account: string + /** + * The fee margin percentage. + */ + feeMarginPercentage?: number +} + +export type TGetOriginFeeDetailsOptions = WithApi< + TGetOriginFeeDetailsOptionsBase, + TApi, + TRes +>