From 308bba7f813e5988cbb1a3ee0153028a43cdb2dd Mon Sep 17 00:00:00 2001 From: Aleksandr Makhnev Date: Tue, 17 Dec 2024 14:33:02 +0500 Subject: [PATCH] fix: support xcm v6 config & delivery fee (#2829) --- .../entities/transaction/ui/Fee/Fee.tsx | 83 ++++++++--------- .../shared/api/xcm/__tests__/mock/xcmData.ts | 1 + .../api/xcm/__tests__/xcm-utils.test.ts | 6 +- src/renderer/shared/api/xcm/lib/constants.ts | 7 +- src/renderer/shared/api/xcm/lib/types.ts | 16 ++++ src/renderer/shared/api/xcm/lib/xcm-utils.ts | 26 +++--- .../shared/api/xcm/service/xcmService.ts | 58 +++++++++++- src/renderer/shared/lib/utils/substrate.ts | 3 +- .../widgets/Transfer/model/form-model.ts | 89 +++++++++++++++++-- .../Transfer/model/xcm-transfer-model.ts | 22 ++++- .../widgets/Transfer/ui/TransferForm.tsx | 2 + 11 files changed, 244 insertions(+), 69 deletions(-) diff --git a/src/renderer/entities/transaction/ui/Fee/Fee.tsx b/src/renderer/entities/transaction/ui/Fee/Fee.tsx index 7ff0041da9..56ad4f2268 100644 --- a/src/renderer/entities/transaction/ui/Fee/Fee.tsx +++ b/src/renderer/entities/transaction/ui/Fee/Fee.tsx @@ -1,5 +1,5 @@ import { type ApiPromise } from '@polkadot/api'; -import { BN } from '@polkadot/util'; +import { BN, BN_ZERO } from '@polkadot/util'; import { useUnit } from 'effector-react'; import { memo, useEffect, useState } from 'react'; @@ -15,55 +15,58 @@ type Props = { asset: Asset; transaction?: Transaction | null; className?: string; + extraFee?: BN; onFeeChange?: (fee: string) => void; onFeeLoading?: (loading: boolean) => void; }; -export const Fee = memo(({ api, multiply = 1, asset, transaction, className, onFeeChange, onFeeLoading }: Props) => { - const fiatFlag = useUnit(priceProviderModel.$fiatFlag); +export const Fee = memo( + ({ api, multiply = 1, asset, transaction, className, extraFee = BN_ZERO, onFeeChange, onFeeLoading }: Props) => { + const fiatFlag = useUnit(priceProviderModel.$fiatFlag); - const [fee, setFee] = useState(''); - const [isLoading, setIsLoading] = useState(true); + const [fee, setFee] = useState(''); + const [isLoading, setIsLoading] = useState(true); - const updateFee = (fee: string) => { - const totalFee = new BN(fee).muln(multiply).toString(); + const updateFee = (fee: string) => { + const totalFee = new BN(fee).muln(multiply).add(extraFee).toString(); - setFee(totalFee); - onFeeChange?.(totalFee); - }; + setFee(totalFee); + onFeeChange?.(totalFee); + }; - useEffect(() => { - onFeeLoading?.(isLoading); - }, [isLoading]); + useEffect(() => { + onFeeLoading?.(isLoading); + }, [isLoading]); - useEffect(() => { - setIsLoading(true); + useEffect(() => { + setIsLoading(true); - if (!api) return; + if (!api) return; - if (!transaction?.address) { - updateFee('0'); - setIsLoading(false); - } else { - transactionService - .getTransactionFee(transaction, api) - .then(updateFee) - .catch((error) => { - updateFee('0'); - console.info('Error getting fee - ', error); - }) - .finally(() => setIsLoading(false)); - } - }, [transaction, api]); + if (!transaction?.address) { + updateFee('0'); + setIsLoading(false); + } else { + transactionService + .getTransactionFee(transaction, api) + .then(updateFee) + .catch((error) => { + updateFee('0'); + console.info('Error getting fee - ', error); + }) + .finally(() => setIsLoading(false)); + } + }, [transaction, api]); - if (isLoading) { - return ; - } + if (isLoading) { + return ; + } - return ( -
- - -
- ); -}); + return ( +
+ + +
+ ); + }, +); diff --git a/src/renderer/shared/api/xcm/__tests__/mock/xcmData.ts b/src/renderer/shared/api/xcm/__tests__/mock/xcmData.ts index 16e9909bc6..2a1e50e843 100644 --- a/src/renderer/shared/api/xcm/__tests__/mock/xcmData.ts +++ b/src/renderer/shared/api/xcm/__tests__/mock/xcmData.ts @@ -53,6 +53,7 @@ export const CONFIG: XcmConfig = { Action.DEPOSIT_ASSET, ], }, + networkDeliveryFee: {}, networkBaseWeight: { b0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe: '1000000000', baf5aabe40646d11f0ee8abbdc64f4a4b7674925cba08e4a05ff9ebed6e2126b: '200000000', diff --git a/src/renderer/shared/api/xcm/__tests__/xcm-utils.test.ts b/src/renderer/shared/api/xcm/__tests__/xcm-utils.test.ts index e39ae45599..3cace57ce3 100644 --- a/src/renderer/shared/api/xcm/__tests__/xcm-utils.test.ts +++ b/src/renderer/shared/api/xcm/__tests__/xcm-utils.test.ts @@ -40,7 +40,7 @@ describe('shared/api/xcm/lib/xcm-utils', () => { const location = xcmUtils.getDestinationLocation({ parentId: '0x00' }, 2000) as any; expect(location.parents).toEqual(1); - expect(location.interior.X1.Parachain).toEqual(2000); + expect(location.interior.X1[0].Parachain).toEqual(2000); }); test('should calculate correct location for parent parachain', () => { @@ -54,13 +54,13 @@ describe('shared/api/xcm/lib/xcm-utils', () => { const location = xcmUtils.getDestinationLocation({ parentId: '0x00' }, undefined, '0x00') as any; expect(location.parents).toEqual(1); - expect(location.interior.X1.AccountId32.id).toEqual('0x00'); + expect(location.interior.X1[0].AccountId32.id).toEqual('0x00'); }); test('should calculate correct location for child parachain', () => { const location = xcmUtils.getDestinationLocation({ parentId: undefined }, 2000) as any; expect(location.parents).toEqual(0); - expect(location.interior.X1.Parachain).toEqual(2000); + expect(location.interior.X1[0].Parachain).toEqual(2000); }); }); diff --git a/src/renderer/shared/api/xcm/lib/constants.ts b/src/renderer/shared/api/xcm/lib/constants.ts index 32436d5ba4..d702decf80 100644 --- a/src/renderer/shared/api/xcm/lib/constants.ts +++ b/src/renderer/shared/api/xcm/lib/constants.ts @@ -1,10 +1,15 @@ +import { BN } from '@polkadot/util'; + import { TEST_ACCOUNTS } from '@/shared/lib/utils'; import { Action } from './types'; -export const XCM_URL = 'https://raw.githubusercontent.com/novasamatech/nova-utils/master/xcm/v4/transfers.json'; +export const XCM_URL = 'https://raw.githubusercontent.com/novasamatech/nova-utils/master/xcm/v6/transfers.json'; export const XCM_KEY = 'xcm-config'; +export const SET_TOPIC_SIZE = new BN(33); +export const FACTOR_MULTIPLIER = new BN(18); + export const INSTRUCTION_OBJECT: Record object> = { [Action.WITHDRAW_ASSET]: (assetLocation: object) => { return { diff --git a/src/renderer/shared/api/xcm/lib/types.ts b/src/renderer/shared/api/xcm/lib/types.ts index b2c5e38735..3b21c94f55 100644 --- a/src/renderer/shared/api/xcm/lib/types.ts +++ b/src/renderer/shared/api/xcm/lib/types.ts @@ -5,6 +5,7 @@ export type AssetName = string; export type XcmConfig = { assetsLocation: AssetsLocation; instructions: Instructions; + networkDeliveryFee: NetworkDeliveryFee; networkBaseWeight: NetworkBaseWeight; chains: ChainXCM[]; }; @@ -43,6 +44,21 @@ export type NetworkBaseWeight = { [chainId: string]: string; }; +export type DeliveryFeeConfig = { + type: 'exponential'; + factorPallet: 'ParachainSystem' | 'XcmpQueue' | 'Dmp'; + sizeBase: string; + sizeFactor: string; + alwaysHoldingPays: boolean; +}; + +export type NetworkDeliveryFee = { + [chainId: string]: { + toParent?: DeliveryFeeConfig; + toParachain?: DeliveryFeeConfig; + }; +}; + export type AssetXCM = { assetId: number; assetLocation: string; diff --git a/src/renderer/shared/api/xcm/lib/xcm-utils.ts b/src/renderer/shared/api/xcm/lib/xcm-utils.ts index 8f1cd37bbe..a5c45cafde 100644 --- a/src/renderer/shared/api/xcm/lib/xcm-utils.ts +++ b/src/renderer/shared/api/xcm/lib/xcm-utils.ts @@ -107,9 +107,11 @@ function createJunctionFromObject(data: Record) { if (entries.length === 1) { return { - X1: { - [JunctionType[entries[0][0] as JunctionTypeKey]]: entries[0][1], - }, + X1: [ + { + [JunctionType[entries[0][0] as JunctionTypeKey]]: entries[0][1], + }, + ], }; } @@ -179,12 +181,14 @@ function getAccountLocation(accountId?: AccountId) { return { parents: 0, interior: { - X1: { - [isEthereum ? 'accountKey20' : 'accountId32']: { - network: 'Any', - [isEthereum ? 'key' : 'id']: accountId, + X1: [ + { + [isEthereum ? 'accountKey20' : 'accountId32']: { + network: null, + [isEthereum ? 'key' : 'id']: accountId, + }, }, - }, + ], }, }; } @@ -195,7 +199,7 @@ function getChildLocation(parachainId: number, accountId?: AccountId) { if (accountId) { location[isEthereum ? 'accountKey' : 'accountId'] = { - network: 'Any', + network: null, [isEthereum ? 'key' : 'id']: accountId, }; } @@ -212,7 +216,7 @@ function getParentLocation(accountId?: AccountId) { if (accountId) { location[isEthereum ? 'accountKey' : 'accountId'] = { - network: 'Any', + network: null, [isEthereum ? 'key' : 'id']: accountId, }; } @@ -229,7 +233,7 @@ function getSiblingLocation(parachainId: number, accountId?: AccountId) { if (accountId) { location[isEthereum ? 'accountKey' : 'accountId'] = { - network: 'Any', + network: null, [isEthereum ? 'key' : 'id']: accountId, }; } diff --git a/src/renderer/shared/api/xcm/service/xcmService.ts b/src/renderer/shared/api/xcm/service/xcmService.ts index bd1c85d1de..bae174994e 100644 --- a/src/renderer/shared/api/xcm/service/xcmService.ts +++ b/src/renderer/shared/api/xcm/service/xcmService.ts @@ -1,13 +1,13 @@ import { type ApiPromise } from '@polkadot/api'; -import { BN } from '@polkadot/util'; -import get from 'lodash/get'; +import { BN, BN_TEN } from '@polkadot/util'; +import { camelCase, get } from 'lodash'; import { type AccountId, type Chain, type ChainId, type HexString } from '@/shared/core'; import { getAssetId, getTypeName, getTypeVersion, toLocalChainId } from '@/shared/lib/utils'; import { type XTokenPalletTransferArgs, type XcmPalletTransferArgs } from '@/entities/transaction'; import { localStorageService } from '../../local-storage'; import { chainsService } from '../../network'; -import { XCM_KEY, XCM_URL } from '../lib/constants'; +import { FACTOR_MULTIPLIER, SET_TOPIC_SIZE, XCM_KEY, XCM_URL } from '../lib/constants'; import { type AssetLocation, type AssetName, @@ -28,6 +28,7 @@ export const xcmService = { getAvailableTransfers, getEstimatedFee, getEstimatedRequiredDestWeight, + getDeliveryFeeFromConfig, getAssetLocation, getVersionedDestinationLocation, @@ -36,6 +37,8 @@ export const xcmService = { parseXcmPalletExtrinsic, parseXTokensExtrinsic, decodeXcm, + + getParentChain, }; async function fetchXcmConfig(): Promise { @@ -151,9 +154,9 @@ function getVersionedDestinationLocation( destinationParaId?: number, accountId?: AccountId, ) { - const location = xcmUtils.getDestinationLocation(originChain, destinationParaId, accountId); const type = getTypeName(api, transferType, 'dest'); const version = getTypeVersion(api, type || ''); + const location = xcmUtils.getDestinationLocation(originChain, destinationParaId, accountId); if (!version) return location; @@ -326,3 +329,50 @@ function decodeXcm(chainId: ChainId, data: XcmPalletPayload | XTokensPayload): D dest: data.destAccountId, }; } + +function getParentChain(chain: Chain, chains: Record) { + if (!chain.parentId) return chain; + + return chains[chain.parentId]; +} + +async function getDeliveryFeeFromConfig({ + config, + originChain, + originApi, + parentApi, + destinationChainId, + txBytesLength, +}: { + config: XcmConfig; + originChain: string; + originApi: ApiPromise; + parentApi: ApiPromise; + destinationChainId: number; + txBytesLength: number; +}): Promise { + const RELAYCHAINS = [1000, 2000]; + const direction = RELAYCHAINS.includes(destinationChainId) ? 'toParent' : 'toParachain'; + + const deliveryFeeConfig = config.networkDeliveryFee[originChain]?.[direction]; + + if (!deliveryFeeConfig) return new BN(0); + + let deliveryFactor: string; + + if (direction === 'toParent') { + deliveryFactor = ( + await parentApi.query[camelCase(deliveryFeeConfig.factorPallet)].upwardDeliveryFeeFactor() + ).toString(); + } else { + deliveryFactor = ( + await originApi.query[camelCase(deliveryFeeConfig.factorPallet)].deliveryFeeFactor(destinationChainId) + ).toString(); + } + + const weight = new BN(txBytesLength).add(SET_TOPIC_SIZE); + const feeSize = new BN(deliveryFeeConfig.sizeBase).add(weight.mul(new BN(deliveryFeeConfig.sizeFactor))); + const deliveryFee = new BN(deliveryFactor).div(new BN(BN_TEN).pow(FACTOR_MULTIPLIER)).mul(feeSize); + + return deliveryFee; +} diff --git a/src/renderer/shared/lib/utils/substrate.ts b/src/renderer/shared/lib/utils/substrate.ts index 460cfab4ba..b53878fe2c 100644 --- a/src/renderer/shared/lib/utils/substrate.ts +++ b/src/renderer/shared/lib/utils/substrate.ts @@ -27,8 +27,7 @@ import { DEFAULT_TIME, ONE_DAY, THRESHOLD } from './constants'; export type TxMetadata = { registry: TypeRegistry; options: OptionsWithMeta; info: BaseTxInfo }; -// TODO: Add V3, V4 support -const SUPPORTED_VERSIONS = ['V2']; +const SUPPORTED_VERSIONS = ['V3', 'V4']; const UNUSED_LABEL = 'unused'; /** diff --git a/src/renderer/widgets/Transfer/model/form-model.ts b/src/renderer/widgets/Transfer/model/form-model.ts index 17499e5f1c..c9b4f43be8 100644 --- a/src/renderer/widgets/Transfer/model/form-model.ts +++ b/src/renderer/widgets/Transfer/model/form-model.ts @@ -1,8 +1,12 @@ -import { combine, createEvent, createStore, restore, sample } from 'effector'; +import { type ApiPromise } from '@polkadot/api'; +import { type SubmittableExtrinsic } from '@polkadot/api/types'; +import { BN, BN_ZERO } from '@polkadot/util'; +import { combine, createEffect, createEvent, createStore, restore, sample } from 'effector'; import { createForm } from 'effector-forms'; import isEmpty from 'lodash/isEmpty'; import { spread } from 'patronum'; +import { type XcmConfig, xcmService } from '@/shared/api/xcm'; import { type Account, type AccountId, @@ -23,13 +27,14 @@ import { nonNullable, toAccountId, toAddress, + toLocalChainId, transferableAmount, validateAddress, } from '@/shared/lib/utils'; import { createTxStore } from '@/shared/transactions'; import { balanceModel, balanceUtils } from '@/entities/balance'; import { networkModel, networkUtils } from '@/entities/network'; -import { TransferType, transactionBuilder, transactionService } from '@/entities/transaction'; +import { TransferType, getExtrinsic, transactionBuilder, transactionService } from '@/entities/transaction'; import { accountUtils, walletModel, walletUtils } from '@/entities/wallet'; import { TransferRules } from '@/features/operations/OperationsValidation'; import { type NetworkStore } from '../lib/types'; @@ -87,9 +92,18 @@ const $fee = restore(feeChanged, ZERO_BALANCE); const $multisigDeposit = restore(multisigDepositChanged, ZERO_BALANCE); const $isFeeLoading = restore(isFeeLoadingChanged, true); const $isXcm = createStore(false); +const $deliveryFee = createStore(BN_ZERO); const $selectedSignatories = createStore([]); +const $totalFee = combine( + { + fee: $fee, + deliveryFee: $deliveryFee, + }, + ({ fee, deliveryFee }) => new BN(fee).add(deliveryFee).toString(), +); + const $transferForm = createForm({ fields: { account: { @@ -97,7 +111,7 @@ const $transferForm = createForm({ rules: [ TransferRules.account.noProxyFee( combine({ - fee: $fee, + fee: $totalFee, isProxy: $isProxy, proxyBalance: $proxyBalance, }), @@ -110,7 +124,7 @@ const $transferForm = createForm({ TransferRules.signatory.noSignatorySelected($isMultisig), TransferRules.signatory.notEnoughTokens( combine({ - fee: $fee, + fee: $totalFee, isMultisig: $isMultisig, multisigDeposit: $multisigDeposit, balance: $signatoryBalance, @@ -138,7 +152,7 @@ const $transferForm = createForm({ ), TransferRules.amount.insufficientBalanceForXcmFee( combine({ - fee: $fee, + fee: $totalFee, xcmFee: xcmTransferModel.$xcmFee, network: $networkStore, balance: $accountBalance, @@ -396,6 +410,47 @@ const $canSubmit = combine( }, ); +const $extrinsic = combine( + { + api: $api, + coreTx: $coreTx, + }, + ({ api, coreTx }) => { + if (!api || !coreTx) return null; + + return getExtrinsic[coreTx.type](coreTx.args, api); + }, +); + +const getDeliveryFeeFx = createEffect( + async ({ + config, + parachainId, + api, + parentApi, + extrinsic, + }: { + config: XcmConfig | null; + parachainId: number | null; + api: ApiPromise | null; + parentApi: ApiPromise | null; + extrinsic?: SubmittableExtrinsic<'promise'> | null; + }) => { + if (config && api && parentApi && parachainId && extrinsic) { + return xcmService.getDeliveryFeeFromConfig({ + config, + originChain: toLocalChainId(api.genesisHash.toHex()) || '', + originApi: api, + parentApi, + destinationChainId: parachainId, + txBytesLength: extrinsic.encodedLength, + }); + } else { + return BN_ZERO; + } + }, +); + // Fields connections sample({ @@ -609,6 +664,29 @@ sample({ target: formSubmitted, }); +sample({ + clock: $extrinsic, + source: { + api: $api, + parentApi: xcmTransferModel.$parentChainApi, + parachainId: xcmTransferModel.$xcmParaId, + config: xcmTransferModel.$config, + extrinsic: $extrinsic, + }, + target: getDeliveryFeeFx, +}); + +sample({ + clock: getDeliveryFeeFx.doneData, + target: $deliveryFee, +}); + +sample({ + clock: getDeliveryFeeFx.fail, + fn: () => BN_ZERO, + target: $deliveryFee, +}); + export const formModel = { $transferForm, $proxyWallet, @@ -626,6 +704,7 @@ export const formModel = { $fee, $multisigDeposit, + $deliveryFee, $coreTx, $fakeTx, diff --git a/src/renderer/widgets/Transfer/model/xcm-transfer-model.ts b/src/renderer/widgets/Transfer/model/xcm-transfer-model.ts index 092587fa8f..8cb05e2ce3 100644 --- a/src/renderer/widgets/Transfer/model/xcm-transfer-model.ts +++ b/src/renderer/widgets/Transfer/model/xcm-transfer-model.ts @@ -166,13 +166,12 @@ const $txBeneficiary = combine( destination: $destination, transferDirection: $transferDirection, }, - (params) => { - const { api, destination, transferDirection } = params; - + ({ api, destination, transferDirection }) => { if (!api || !destination || !transferDirection) return undefined; return xcmService.getVersionedAccountLocation(api, transferDirection.type, destination); }, + // TODO: Remove skipVoid { skipVoid: false }, ); @@ -229,6 +228,21 @@ const $xcmData = combine( { skipVoid: false }, ); +const $parentChainApi = combine( + { + network: $networkStore, + chains: networkModel.$chains, + apis: networkModel.$apis, + }, + ({ network, chains, apis }) => { + if (!chains || !apis || !network) return null; + + const parentChain = xcmService.getParentChain(network.chain, chains); + + return apis[parentChain.chainId] ?? null; + }, +); + sample({ clock: xcmStarted, target: xcmConfigLoaded, @@ -267,10 +281,12 @@ sample({ export const xcmTransferModel = { $config, $apiDestination, + $parentChainApi, $xcmData, $xcmFee, $isXcmFeeLoading, $transferDirections, + $xcmParaId, events: { xcmStarted, diff --git a/src/renderer/widgets/Transfer/ui/TransferForm.tsx b/src/renderer/widgets/Transfer/ui/TransferForm.tsx index 2ca217dee4..402ea34f4b 100644 --- a/src/renderer/widgets/Transfer/ui/TransferForm.tsx +++ b/src/renderer/widgets/Transfer/ui/TransferForm.tsx @@ -283,6 +283,7 @@ const FeeSection = () => { const isXcm = useUnit(formModel.$isXcm); const xcmConfig = useUnit(formModel.$xcmConfig); const xcmApi = useUnit(formModel.$xcmApi); + const deliveryFee = useUnit(formModel.$deliveryFee); if (!network) { return null; @@ -303,6 +304,7 @@ const FeeSection = () => { api={api} asset={network.chain.assets[0]} transaction={transaction?.wrappedTx || fakeTx} + extraFee={deliveryFee} onFeeChange={formModel.events.feeChanged} onFeeLoading={formModel.events.isFeeLoadingChanged} />