From 683065cfc64c9b69c04e77a8fbeab204711040e0 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 22 Feb 2024 13:05:33 +0200 Subject: [PATCH] Fix cross transfers from hydradx network --- src/services/fees/custom/HydraAdapter.ts | 535 +++++++++++++++++++++++ src/services/fees/custom/utils/index.ts | 64 +++ src/services/fees/utils.ts | 5 +- 3 files changed, 602 insertions(+), 2 deletions(-) create mode 100644 src/services/fees/custom/HydraAdapter.ts diff --git a/src/services/fees/custom/HydraAdapter.ts b/src/services/fees/custom/HydraAdapter.ts new file mode 100644 index 0000000..30c798a --- /dev/null +++ b/src/services/fees/custom/HydraAdapter.ts @@ -0,0 +1,535 @@ +import { Storage } from '@acala-network/sdk/utils/storage' +import { AnyApi, FixedPointNumber as FN } from '@acala-network/sdk-core' +import { combineLatest, map, Observable } from 'rxjs' + +import { SubmittableExtrinsic } from '@polkadot/api/types' +import { DeriveBalancesAll } from '@polkadot/api-derive/balances/types' +import { ISubmittableResult } from '@polkadot/types/types' +import { + createRouteConfigs, + createXTokensAssetsParam, + createXTokensDestParam, + isChainEqual, + validateAddress, +} from '@polkawallet/bridge/utils' +import { BaseCrossChainAdapter } from '@polkawallet/bridge/base-chain-adapter' +import { + BalanceData, + ChainId, + chains, + ExtendedToken, + TransferParams, +} from '@polkawallet/bridge' +import { + ApiNotFound, + InvalidAddress, + TokenNotFound, +} from '@polkawallet/bridge/errors' +import { statemineTokensConfig, statemintTokensConfig } from './utils/index' +import { + BalanceAdapter, + BalanceAdapterConfigs, +} from '@polkawallet/bridge/balance-adapter' +import { getDestAccountInfo } from './utils/destination-utils' + +export const basiliskRouteConfigs = createRouteConfigs('basilisk', [ + { + to: 'kusama', + token: 'KSM', + xcm: { + fee: { token: 'KSM', amount: '104571640' }, + }, + }, + { + to: 'karura', + token: 'BSX', + xcm: { + fee: { token: 'BSX', amount: '93240000000' }, + }, + }, + { + to: 'karura', + token: 'aUSD', + xcm: { + fee: { token: 'aUSD', amount: '5060238106' }, + }, + }, + { + to: 'karura', + token: 'KSM', + xcm: { + fee: { token: 'KSM', amount: '90741527' }, + }, + }, + { + to: 'karura', + token: 'DAI', + xcm: { + fee: { token: 'DAI', amount: '808240000000000' }, + }, + }, + { + to: 'karura', + token: 'USDCet', + xcm: { + fee: { token: 'USDCet', amount: '808' }, + }, + }, + { + to: 'karura', + token: 'WETH', + xcm: { + fee: { token: 'WETH', amount: '449022222222' }, + }, + }, + { + to: 'karura', + token: 'WBTC', + xcm: { + fee: { token: 'WBTC', amount: '2' }, + }, + }, + { + to: 'statemine', + token: 'USDT', + xcm: { + fee: { token: 'USDT', amount: '1183' }, + }, + }, +]) + +export const basiliskTokensConfig: Record = { + BSX: { + name: 'BSX', + symbol: 'BSX', + decimals: 12, + ed: '1000000000000', + toRaw: () => 0, + }, + aUSD: { + name: 'aUSD', + symbol: 'aUSD', + decimals: 12, + ed: '10000000000', + toRaw: () => 2, + }, + KSM: { + name: 'KSM', + symbol: 'KSM', + decimals: 12, + ed: '100000000', + toRaw: () => 1, + }, + USDT: { + name: 'USDT', + symbol: 'USDT', + decimals: 6, + ed: '10000', + toRaw: () => 14, + }, + TNKR: { + name: 'TNKR', + symbol: 'TNKR', + decimals: 12, + ed: '1000000000', + toRaw: () => 6, + }, + XRT: { + name: 'XRT', + symbol: 'XRT', + decimals: 9, + ed: '1683502', + toRaw: () => 16, + }, + DAI: { + name: 'DAI', + symbol: 'DAI', + decimals: 18, + ed: '10000000000', + toRaw: () => 13, + }, + USDCet: { + name: 'USDCet', + symbol: 'USDCet', + decimals: 6, + ed: '10000', + toRaw: () => 9, + }, + WETH: { + name: 'WETH', + symbol: 'WETH', + decimals: 18, + ed: '6230529595016', + toRaw: () => 10, + }, + WBTC: { + name: 'WBTC', + symbol: 'WBTC', + decimals: 8, + ed: '33', + toRaw: () => 11, + }, +} + +export const hydradxRoutersConfig = createRouteConfigs('hydradx', [ + { + to: 'polkadot', + token: 'DOT', + xcm: { + fee: { token: 'DOT', amount: '469417452' }, + }, + }, + { + to: 'subsocial' as ChainId, + token: 'SUB', + xcm: { + fee: { + token: 'SUB', + amount: '63199000', + }, + }, + }, + { + to: 'acala', + token: 'DAI', + xcm: { + fee: { token: 'DAI', amount: '926960000000000' }, + }, + }, + { + to: 'acala', + token: 'DOT', + xcm: { + fee: { token: 'DOT', amount: '471820453' }, + }, + }, + { + to: 'acala', + token: 'WETH', + xcm: { + fee: { token: 'WETH', amount: '687004000000' }, + }, + }, + { + to: 'acala', + token: 'WBTC', + xcm: { + fee: { token: 'WBTC', amount: '4' }, + }, + }, + { + to: 'interlay', + token: 'IBTC', + xcm: { fee: { token: 'IBTC', amount: '62' } }, + }, + { + to: 'statemint', + token: 'USDT', + xcm: { + fee: { token: 'USDT', amount: '700000' }, + }, + }, + { + to: 'zeitgeist', + token: 'ZTG', + xcm: { + fee: { token: 'ZTG', amount: '93000000' }, + }, + }, + { + to: 'astar', + token: 'ASTR', + xcm: { + fee: { token: 'ASTR', amount: '4041465440000000' }, + }, + }, +]) + +export const hydradxTokensConfig: Record = { + HDX: { + name: 'HDX', + symbol: 'HDX', + decimals: 12, + ed: '1000000000000', + toRaw: () => 0, + }, + WETH: { + name: 'WETH', + symbol: 'WETH', + decimals: 18, + ed: '7000000000000', + toRaw: () => 4, + }, + WBTC: { name: 'WBTC', symbol: 'WBTC', decimals: 8, ed: '44', toRaw: () => 3 }, + IBTC: { + name: 'IBTC', + symbol: 'IBTC', + decimals: 8, + ed: '36', + toRaw: () => 11, + }, + SUB: { + name: 'SUB', + symbol: 'SUB', + decimals: 10, + ed: '100000000', + toRaw: () => 24, + }, + DOT: { + name: 'DOT', + symbol: 'DOT', + decimals: 10, + ed: '17540000', + toRaw: () => 5, + }, + DAI: { + name: 'DAI', + symbol: 'DAI', + decimals: 18, + ed: '10000000000000000', + toRaw: () => 2, + }, + USDT: { + name: 'USDT', + symbol: 'USDT', + decimals: 6, + ed: '10000', + toRaw: () => 10, + }, + ZTG: { + name: 'ZTG', + symbol: 'ZTG', + decimals: 10, + ed: '1204151916', + toRaw: () => 12, + }, + ASTR: { + name: 'ASTR', + symbol: 'ASTR', + decimals: 18, + ed: '147058823529412000', + toRaw: () => 9, + }, + CFG: { + name: 'CFG', + symbol: 'CFG', + decimals: 18, + ed: '32467532467532500', + toRaw: () => 13, + }, +} + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +const createBalanceStorages = (api: AnyApi) => { + return { + balances: (address: string) => + Storage.create({ + api, + path: 'derive.balances.all', + params: [address], + }), + assets: (tokenId: number, address: string) => + Storage.create({ + api, + path: 'query.tokens.accounts', + params: [address, tokenId], + }), + } +} + +class HydradxBalanceAdapter extends BalanceAdapter { + private storages: ReturnType + + constructor({ api, chain, tokens }: BalanceAdapterConfigs) { + super({ api, chain, tokens }) + this.storages = createBalanceStorages(api) + } + + public subscribeBalance( + tokenName: string, + address: string + ): Observable { + const storage = this.storages.balances(address) + + if (tokenName === this.nativeToken) { + return storage.observable.pipe( + map((data) => ({ + free: FN.fromInner(data.freeBalance.toString(), this.decimals), + locked: FN.fromInner(data.lockedBalance.toString(), this.decimals), + reserved: FN.fromInner( + data.reservedBalance.toString(), + this.decimals + ), + available: FN.fromInner( + data.availableBalance.toString(), + this.decimals + ), + })) + ) + } + + const token = this.getToken(tokenName) + + return this.storages.assets(token.toRaw(), address).observable.pipe( + map((balance) => { + const amount = FN.fromInner( + balance.free?.toString() || '0', + token.decimals + ) + + return { + free: amount, + locked: new FN(0), + reserved: new FN(0), + available: amount, + } + }) + ) + } +} + +class BaseHydradxAdapter extends BaseCrossChainAdapter { + private balanceAdapter?: HydradxBalanceAdapter + + public async init(api: AnyApi) { + this.api = api + + await api.isReady + + this.balanceAdapter = new HydradxBalanceAdapter({ + chain: this.chain.id as ChainId, + api, + tokens: this.tokens, + }) + } + + public subscribeTokenBalance( + token: string, + address: string + ): Observable { + if (!this.balanceAdapter) { + throw new ApiNotFound(this.chain.id) + } + + return this.balanceAdapter.subscribeBalance(token, address) + } + + public subscribeMaxInput( + token: string, + address: string, + to: ChainId + ): Observable { + if (!this.balanceAdapter) { + throw new ApiNotFound(this.chain.id) + } + + return combineLatest({ + txFee: + token === this.balanceAdapter?.nativeToken + ? this.estimateTxFee({ + amount: FN.ZERO, + to, + token, + address, + signer: address, + }) + : '0', + balance: this.balanceAdapter + .subscribeBalance(token, address) + .pipe(map((i) => i.available)), + }).pipe( + map(({ balance, txFee }) => { + const tokenMeta = this.balanceAdapter?.getToken(token) + const feeFactor = 1.2 + const fee = FN.fromInner(txFee, tokenMeta?.decimals).mul( + new FN(feeFactor) + ) + + // always minus ed + return balance + .minus(fee) + .minus(FN.fromInner(tokenMeta?.ed || '0', tokenMeta?.decimals)) + }) + ) + } + + public createTx( + params: TransferParams + ): + | SubmittableExtrinsic<'promise', ISubmittableResult> + | SubmittableExtrinsic<'rxjs', ISubmittableResult> { + if (!this.api) throw new ApiNotFound(this.chain.id) + + const { amount, to, token, address } = params + const toChain = chains[to] + + // For statemine & statemint + if ( + isChainEqual(toChain, 'statemine') || + isChainEqual(toChain, 'statemint') + ) { + const tokenData: ExtendedToken = isChainEqual(toChain, 'statemine') + ? statemineTokensConfig[token] + : statemintTokensConfig[token] + + const accountId = this.api?.createType('AccountId32', address).toHex() + + if (!token) throw new TokenNotFound(token) + return this.api.tx.xTokens.transferMultiasset( + createXTokensAssetsParam( + this.api, + toChain.paraChainId, + tokenData.toRaw(), + amount.toChainData() + ), + createXTokensDestParam(this.api, toChain.paraChainId, accountId) as any, + 'Unlimited' + ) + } + + const tokenData: ExtendedToken = this.getToken(token) + + const { accountId, accountType, addrType } = getDestAccountInfo( + address, + token, + this.api, + to + ) + + if (!validateAddress(address, addrType)) throw new InvalidAddress(address) + + return this.api.tx.xTokens.transfer( + tokenData.toRaw(), + amount.toChainData(), + { + V2: { + parents: 1, + interior: { + X2: [ + { Parachain: toChain.paraChainId }, + { + [accountType]: { + [accountType === 'AccountId32' ? 'id' : 'key']: accountId, + network: 'Any', + }, + }, + ], + }, + }, + } as any, + 'Unlimited' + ) + } +} + +export class BasiliskAdapter extends BaseHydradxAdapter { + constructor() { + super(chains.basilisk, basiliskRouteConfigs, basiliskTokensConfig) + } +} + +export class HydraDxAdapter extends BaseHydradxAdapter { + constructor() { + super(chains.hydradx, hydradxRoutersConfig, hydradxTokensConfig) + } +} diff --git a/src/services/fees/custom/utils/index.ts b/src/services/fees/custom/utils/index.ts index ef100a7..714b158 100644 --- a/src/services/fees/custom/utils/index.ts +++ b/src/services/fees/custom/utils/index.ts @@ -1,6 +1,8 @@ import { isEthereumAddress } from '@polkadot/util-crypto' import { AnyApi } from '@acala-network/sdk-core' import { checkMessageVersionIsV3 } from '@polkawallet/bridge/utils/check-message-version' +import { ExtendedToken } from '@polkawallet/bridge' +import { BN } from '@polkadot/util'; export type AddressType = 'substract' | 'ethereum' export const getValidDestAddrType = ( @@ -34,3 +36,65 @@ export function createPolkadotXCMAccount ( }, } } + +export const statemineTokensConfig: Record = { + KSM: { + name: 'KSM', + symbol: 'KSM', + decimals: 12, + ed: '79999999', + toRaw: () => 'NATIVE', + }, + RMRK: { + name: 'RMRK', + symbol: 'RMRK', + decimals: 10, + ed: '100000000', + toRaw: () => new BN(8), + }, + ARIS: { + name: 'ARIS', + symbol: 'ARIS', + decimals: 8, + ed: '10000000', + toRaw: () => new BN(16), + }, + USDT: { + name: 'USDT', + symbol: 'USDT', + decimals: 6, + ed: '1000', + toRaw: () => new BN(1984), + }, +} + +export const statemintTokensConfig: Record = { + DOT: { + name: 'DOT', + symbol: 'DOT', + decimals: 10, + ed: '10000000000', + toRaw: () => 'NATIVE', + }, + USDT: { + name: 'USDT', + symbol: 'USDT', + decimals: 6, + ed: '700000', + toRaw: () => new BN(1984), + }, + USDC: { + name: 'USDC', + symbol: 'USDC', + decimals: 6, + ed: '700000', + toRaw: () => new BN(1337), + }, + PINK: { + name: 'PINK', + symbol: 'PINK', + decimals: 10, + ed: '1', + toRaw: () => new BN(23), + }, +} diff --git a/src/services/fees/utils.ts b/src/services/fees/utils.ts index 0c5e431..56e187b 100644 --- a/src/services/fees/utils.ts +++ b/src/services/fees/utils.ts @@ -5,7 +5,7 @@ import { BifrostAdapter } from '@polkawallet/bridge/adapters/bifrost' import { AltairAdapter } from '@polkawallet/bridge/adapters/centrifuge' import { ShadowAdapter } from '@polkawallet/bridge/adapters/crust' import { CrabAdapter } from '@polkawallet/bridge/adapters/darwinia' -import { BasiliskAdapter, HydraAdapter } from '@polkawallet/bridge/adapters/hydradx' +import { BasiliskAdapter } from '@polkawallet/bridge/adapters/hydradx' import { IntegriteeAdapter } from '@polkawallet/bridge/adapters/integritee' import { InterlayAdapter, KintsugiAdapter } from '@polkawallet/bridge/adapters/interlay' import { KicoAdapter } from '@polkawallet/bridge/adapters/kico' @@ -22,6 +22,7 @@ import { BaseCrossChainAdapter } from '@polkawallet/bridge/base-chain-adapter' import { firstValueFrom } from 'rxjs' import { SubsocialAdapter } from './custom/SubsocialAdapter' import { MoonbeamAdapter } from './custom/MoonbeamAdapter' +import { HydraDxAdapter } from './custom/HydraAdapter' const transferAdapters: Record = { polkadot: { @@ -103,7 +104,7 @@ const transferAdapters: Record