From a7211fe0a84debf90f69b162d1262c8cdb46f4f7 Mon Sep 17 00:00:00 2001 From: Vladyslav Samchuk <44325051+samchuk-vlad@users.noreply.github.com> Date: Sun, 11 Feb 2024 08:42:34 +0200 Subject: [PATCH] feat: add more routes to chain adapters (#112) * add more routes to chain adapters * Add moonriver adapter --- src/adapters/acala/acala-configs.ts | 7 + src/adapters/acala/karura-configs.ts | 21 ++ src/adapters/astar.ts | 35 +++- src/adapters/hydradx.ts | 17 ++ src/adapters/interlay.ts | 12 ++ src/adapters/moonbeam.ts | 270 +++++++++++++++++++++++--- src/adapters/parallel.ts | 23 +++ src/adapters/statemint.ts | 32 +++ src/adapters/subsocial.ts | 197 +++++++++++++++++++ src/configs/chains/polkadot-chains.ts | 8 + 10 files changed, 594 insertions(+), 28 deletions(-) create mode 100644 src/adapters/subsocial.ts diff --git a/src/adapters/acala/acala-configs.ts b/src/adapters/acala/acala-configs.ts index 10601fde..70cc4e36 100644 --- a/src/adapters/acala/acala-configs.ts +++ b/src/adapters/acala/acala-configs.ts @@ -58,6 +58,13 @@ export const acalaRouteConfigs = createRouteConfigs("acala", [ fee: { token: "ACA", amount: "1920000000" }, }, }, + { + to: "parallel", + token: "AUSD", + xcm: { + fee: { token: "AUSD", amount: "700000" }, + }, + }, { to: "parallel", token: "LDOT", diff --git a/src/adapters/acala/karura-configs.ts b/src/adapters/acala/karura-configs.ts index b70cc32a..bd585390 100644 --- a/src/adapters/acala/karura-configs.ts +++ b/src/adapters/acala/karura-configs.ts @@ -51,6 +51,13 @@ export const karuraRouteConfigs = createRouteConfigs("karura", [ fee: { token: "BNC", amount: "5120000000" }, }, }, + { + to: "bifrost", + token: "KSM", + xcm: { + fee: { token: "KSM", amount: "5120000000" }, + }, + }, { to: "bifrost", token: "KAR", @@ -184,6 +191,13 @@ export const karuraRouteConfigs = createRouteConfigs("karura", [ fee: { token: "KUSD", amount: "320000000000" }, }, }, + { + to: "calamari", + token: "KSM", + xcm: { + fee: { token: "KSM", amount: "100000000000" }, + }, + }, { to: "calamari", token: "KMA", @@ -254,6 +268,13 @@ export const karuraRouteConfigs = createRouteConfigs("karura", [ fee: { token: "LKSM", amount: "48000000" }, }, }, + { + to: "heiko", + token: "KUSD", + xcm: { + fee: { token: "KUSD", amount: "48000000" }, + }, + }, { to: "pichiu", token: "PCHU", diff --git a/src/adapters/astar.ts b/src/adapters/astar.ts index 01deca86..ceb754a6 100644 --- a/src/adapters/astar.ts +++ b/src/adapters/astar.ts @@ -15,6 +15,8 @@ import { validateAddress, createRouteConfigs, getDestAccountInfo, + createXTokensAssetsParam, + createXTokensDestParam, } from "../utils"; type TokenData = ExtendedToken & { toQuery: () => string }; @@ -60,6 +62,14 @@ export const astarRouteConfigs = createRouteConfigs("astar", [ weightLimit: "Unlimited", }, }, + { + to: "statemint", + token: "USDT", + xcm: { + fee: { token: "USDT", amount: "44306118000000000" }, + weightLimit: "Unlimited", + }, + }, ]); export const shidenRouteConfigs = createRouteConfigs("shiden", [ @@ -107,13 +117,21 @@ export const astarTokensConfig: Record> = { "0x0001000000000000000000000000000000000000000000000000000000000000", toQuery: () => "18446744073709551617", }, + USDT: { + name: "USDT", + symbol: "USDT", + decimals: 6, + ed: "1", + toRaw: () => + "0x0001000000000000000000000000000000000000000000000000000000000000", // TODO: update + toQuery: () => "4294969280", + }, LDOT: { name: "LDOT", symbol: "LDOT", decimals: 10, ed: "1", - toRaw: () => - "0x0003000000000000000000000000000000000000000000000000000000000000", + toRaw: () => "1984", toQuery: () => "18446744073709551618", }, }, @@ -323,6 +341,19 @@ class BaseAstarAdapter extends BaseCrossChainAdapter { const tokenData: TokenData = this.getToken(params.token); + if (tokenData.symbol === "USDT") { + return this.api.tx.xTokens.transferMultiasset( + createXTokensAssetsParam( + this.api, + toChain.paraChainId, + tokenData.toRaw(), + amount.toChainData() + ), + createXTokensDestParam(this.api, toChain.paraChainId, accountId) as any, + "Unlimited" + ); + } + return this.api.tx.xTokens.transferMultiasset( { V3: { diff --git a/src/adapters/hydradx.ts b/src/adapters/hydradx.ts index fc52cc66..5b4c96bb 100644 --- a/src/adapters/hydradx.ts +++ b/src/adapters/hydradx.ts @@ -181,6 +181,16 @@ export const hydradxRoutersConfig = createRouteConfigs("hydradx", [ fee: { token: "DOT", amount: "469417452" }, }, }, + { + to: "subsocial", + token: "SUB", + xcm: { + fee: { + token: "SUB", + amount: "63199000", + }, + }, + }, { to: "acala", token: "DAI", @@ -267,6 +277,13 @@ export const hydradxTokensConfig: Record = { ed: "36", toRaw: () => 11, }, + SUB: { + name: "SUB", + symbol: "SUB", + decimals: 10, + ed: "100000000", + toRaw: () => 24, + }, DOT: { name: "DOT", symbol: "DOT", diff --git a/src/adapters/interlay.ts b/src/adapters/interlay.ts index 2be05bc8..5de84515 100644 --- a/src/adapters/interlay.ts +++ b/src/adapters/interlay.ts @@ -27,6 +27,11 @@ export const interlayRouteConfigs = createRouteConfigs("interlay", [ token: "IBTC", xcm: { fee: { token: "IBTC", amount: "9" } }, }, + { + to: "statemint", + token: "USDT", + xcm: { fee: { token: "USDT", amount: "9" } }, + }, { to: "hydradx", token: "IBTC", @@ -77,6 +82,13 @@ export const interlayTokensConfig: Record< ed: "0", toRaw: () => ({ Token: "IBTC" }), }, + USDT: { + name: "USDT", + symbol: "USDT", + decimals: 6, + ed: "0", + toRaw: () => ({ Token: "USDT" }), + }, }, kintsugi: { KINT: { diff --git a/src/adapters/moonbeam.ts b/src/adapters/moonbeam.ts index a6bdfa0e..4de0ce99 100644 --- a/src/adapters/moonbeam.ts +++ b/src/adapters/moonbeam.ts @@ -1,25 +1,118 @@ +import { Storage } from "@acala-network/sdk/utils/storage"; import { AnyApi, FixedPointNumber as FN } from "@acala-network/sdk-core"; -import { Observable } from "rxjs"; +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 { BalanceAdapter, BalanceAdapterConfigs } from "../balance-adapter"; import { BaseCrossChainAdapter } from "../base-chain-adapter"; import { ChainId, chains } from "../configs"; -import { ApiNotFound } from "../errors"; -import { BalanceData, BasicToken, TransferParams } from "../types"; - -export const moonbeamTokensConfig: Record = { - GLMR: { - name: "GLMR", - symbol: "GLMR", - decimals: 18, - ed: "100000000000000000", +import { ApiNotFound, InvalidAddress, TokenNotFound } from "../errors"; +import { + createRouteConfigs, + getDestAccountInfo, + validateAddress, +} from "src/utils"; +import { + BalanceData, + BasicToken, + ExtendedToken, + TransferParams, +} from "src/types"; + +export const moonbeamRouteConfigs = createRouteConfigs("moonbeam", [ + { + to: "subsocial", + token: "xcSUB", + xcm: { + fee: { token: "SUB", amount: "1000000000" }, + }, + }, + { + to: "acala", + token: "GLMR", + xcm: { + fee: { token: "GLMR", amount: "1000000000" }, + }, + }, + { + to: "acala", + token: "xcACA", + xcm: { + fee: { token: "ACA", amount: "1000000000" }, + }, + }, + { + to: "acala", + token: "xcaUSD", + xcm: { + fee: { token: "AUSD", amount: "1000000000" }, + }, + }, + { + to: "acala", + token: "xcDOT", + xcm: { + fee: { token: "DOT", amount: "1000000000" }, + }, + }, + { + to: "statemint", + token: "xcUSDT", + xcm: { + fee: { token: "USDT", amount: "1000000000" }, + }, + }, +]); + +const moonbeamTokensConfig: Record = { + SUB: { + name: "SUB", + symbol: "SUB", + decimals: 10, + ed: "100000000000", + toRaw: () => ({ + ForeignAsset: "89994634370519791027168048838578580624", + }), + }, + DOT: { + name: "DOT", + symbol: "DOT", + decimals: 10, + ed: "10000000000", + toRaw: () => ({ + ForeignAsset: "42259045809535163221576417993425387648", + }), + }, + ACA: { + name: "ACA", + symbol: "ACA", + decimals: 12, + ed: "10000000000", + toRaw: () => ({ + ForeignAsset: "224821240862170613278369189818311486111", + }), + }, + AUSD: { + name: "AUSD", + symbol: "AUSD", + decimals: 12, + ed: "10000000000", + toRaw: () => ({ + ForeignAsset: "110021739665376159354538090254163045594", + }), + }, + USDT: { + name: "USDT", + symbol: "USDT", + decimals: 6, + ed: "10000000000", + toRaw: () => ({ + ForeignAsset: "311091173110107856861649819128533077277", + }), }, - ACA: { name: "ACA", symbol: "ACA", decimals: 12, ed: "100000000000" }, - AUSD: { name: "AUSD", symbol: "AUSD", decimals: 12, ed: "100000000000" }, - LDOT: { name: "LDOT", symbol: "LDOT", decimals: 10, ed: "500000000" }, - DOT: { name: "DOT", symbol: "DOT", decimals: 10, ed: "10000000000" }, }; export const moonriverTokensConfig: Record = { @@ -28,39 +121,164 @@ export const moonriverTokensConfig: Record = { KUSD: { name: "KUSD", symbol: "KUSD", decimals: 12, ed: "0" }, }; -class BaseMoonbeamAdapter extends BaseCrossChainAdapter { - public subscribeTokenBalance(_: string, __: string): Observable { - throw new ApiNotFound(this.chain.id); +// 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], + }), + }; +}; + +class MoonbeamBalanceAdapter extends BalanceAdapter { + private storages: ReturnType; + + constructor({ api, chain, tokens }: BalanceAdapterConfigs) { + super({ api, chain, tokens }); + this.storages = createBalanceStorages(api); } + public subscribeBalance( + token: string, + address: string + ): Observable { + const storage = this.storages.balances(address); + + if (token !== this.nativeToken) { + throw new TokenNotFound(token); + } + + 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 + ), + })) + ); + } +} + +class MoonbeamBaseAdapter extends BaseCrossChainAdapter { + private balanceAdapter?: MoonbeamBalanceAdapter; + public async init(api: AnyApi) { this.api = api; + + await api.isReady; + + const chain = this.chain.id as ChainId; + + this.balanceAdapter = new MoonbeamBalanceAdapter({ + chain, + api, + tokens: moonbeamTokensConfig, + }); + } + + public subscribeTokenBalance( + token: string, + address: string + ): Observable { + if (!this.balanceAdapter) { + throw new ApiNotFound(this.chain.id); + } + + return this.balanceAdapter.subscribeBalance(token, address); } public subscribeMaxInput( - _: string, - __: string, - ___: ChainId + token: string, + address: string, + to: ChainId ): Observable { - throw new ApiNotFound(this.chain.id); + if (!this.balanceAdapter) { + throw new ApiNotFound(this.chain.id); + } + + return combineLatest({ + txFee: this.estimateTxFee({ + amount: FN.ZERO, + to, + token, + address, + signer: address, + }), + 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( - _: TransferParams + params: TransferParams ): | SubmittableExtrinsic<"promise", ISubmittableResult> | SubmittableExtrinsic<"rxjs", ISubmittableResult> { - throw new ApiNotFound(this.chain.id); + if (this.api === undefined) { + throw new ApiNotFound(this.chain.id); + } + + const { address, amount, to, token } = params; + + const { accountId, accountType, addrType } = getDestAccountInfo( + address, + token, + this.api, + to + ); + + const tokenData = moonbeamTokensConfig[token.replace("xc", "")]; + + if (!validateAddress(address, addrType)) throw new InvalidAddress(address); + + const toChain = chains[to]; + + return this.api.tx.xTokens.transfer( + tokenData.toRaw(), + amount.toChainData(), + { + V3: { + parents: 1, + interior: { + X2: [ + { Parachain: toChain.paraChainId }, + { [accountType]: { id: accountId, network: undefined } }, + ], + }, + }, + } as any, + "Unlimited" + ); } } -export class MoonbeamAdapter extends BaseMoonbeamAdapter { +export class MoonbeamAdapter extends MoonbeamBaseAdapter { constructor() { - super(chains.moonbeam, [], moonbeamTokensConfig); + super(chains.moonbeam, moonbeamRouteConfigs, moonbeamTokensConfig); } } -export class MoonriverAdapter extends BaseMoonbeamAdapter { +export class MoonriverAdapter extends MoonbeamBaseAdapter { constructor() { super(chains.moonriver, [], moonriverTokensConfig); } diff --git a/src/adapters/parallel.ts b/src/adapters/parallel.ts index 2269f9e5..2e8845b0 100644 --- a/src/adapters/parallel.ts +++ b/src/adapters/parallel.ts @@ -32,6 +32,14 @@ export const parallelRouteConfigs = createRouteConfigs("parallel", [ weightLimit: DEST_WEIGHT, }, }, + { + to: "acala", + token: "AUSD", + xcm: { + fee: { token: "AUSD", amount: "6400000000" }, + weightLimit: DEST_WEIGHT, + }, + }, { to: "acala", token: "LDOT", @@ -40,6 +48,14 @@ export const parallelRouteConfigs = createRouteConfigs("parallel", [ weightLimit: DEST_WEIGHT, }, }, + { + to: "statemint", + token: "USDT", + xcm: { + fee: { token: "USDT", amount: "24037893" }, + weightLimit: DEST_WEIGHT, + }, + }, ]); export const heikoRouteConfigs = createRouteConfigs("heiko", [ @@ -95,6 +111,13 @@ export const parallelTokensConfig: Record< ed: "100000000000", toRaw: () => "104", }, + UDST: { + name: "UDST", + symbol: "UDST", + decimals: 6, + ed: "100000000000", + toRaw: () => "102", + }, LDOT: { name: "LDOT", symbol: "LDOT", diff --git a/src/adapters/statemint.ts b/src/adapters/statemint.ts index 27da85c2..0d6ebe91 100644 --- a/src/adapters/statemint.ts +++ b/src/adapters/statemint.ts @@ -59,6 +59,38 @@ export const statemintRouteConfigs = createRouteConfigs("statemint", [ weightLimit: "Unlimited", }, }, + { + to: "astar", + token: "USDT", + xcm: { + fee: { token: "USDT", amount: "808" }, + weightLimit: "Unlimited", + }, + }, + { + to: "interlay", + token: "USDT", + xcm: { + fee: { token: "USDT", amount: "808" }, + weightLimit: "Unlimited", + }, + }, + { + to: "moonbeam", + token: "USDT", + xcm: { + fee: { token: "USDT", amount: "808" }, + weightLimit: "Unlimited", + }, + }, + { + to: "parallel", + token: "USDT", + xcm: { + fee: { token: "USDT", amount: "808" }, + weightLimit: "Unlimited", + }, + }, ]); export const statemineRouteConfigs = createRouteConfigs("statemine", [ diff --git a/src/adapters/subsocial.ts b/src/adapters/subsocial.ts new file mode 100644 index 00000000..261da23b --- /dev/null +++ b/src/adapters/subsocial.ts @@ -0,0 +1,197 @@ +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 { BalanceAdapter, BalanceAdapterConfigs } from "../balance-adapter"; +import { BaseCrossChainAdapter } from "../base-chain-adapter"; +import { ChainId, chains } from "../configs"; +import { ApiNotFound, InvalidAddress, TokenNotFound } from "../errors"; +import { BalanceData, BasicToken, TransferParams } from "../types"; +import { + createPolkadotXCMAccount, + createPolkadotXCMAsset, + createPolkadotXCMDest, + createRouteConfigs, + getDestAccountType, + getValidDestAddrType, + validateAddress, +} from "../utils"; + +export const subsocialRouteConfigs = createRouteConfigs("subsocial" as any, [ + { + to: "hydradx", + token: "SUB", + xcm: { + fee: { token: "SUB", amount: "65000000" }, + }, + }, + { + to: "moonbeam", + token: "SUB", + xcm: { + fee: { token: "SUB", amount: "65000000" }, + }, + }, +]); + +export const subsocialTokensConfig: Record = { + SUB: { name: "SUB", symbol: "SUB", decimals: 10, ed: "100000000" }, +}; + +// 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], + }), + }; +}; + +class SubsocialBalanceAdapter extends BalanceAdapter { + private storages: ReturnType; + + constructor({ api, chain, tokens }: BalanceAdapterConfigs) { + super({ api, chain, tokens }); + this.storages = createBalanceStorages(api); + } + + public subscribeBalance( + token: string, + address: string + ): Observable { + const storage = this.storages.balances(address); + + if (token !== this.nativeToken) { + throw new TokenNotFound(token); + } + + 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 + ), + })) + ); + } +} + +class SubsocialBaseAdapter extends BaseCrossChainAdapter { + private balanceAdapter?: SubsocialBalanceAdapter; + + public async init(api: AnyApi) { + this.api = api; + + await api.isReady; + + const chain = this.chain.id as ChainId; + + this.balanceAdapter = new SubsocialBalanceAdapter({ + chain, + api, + tokens: subsocialTokensConfig, + }); + } + + 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: this.estimateTxFee({ + amount: FN.ZERO, + to, + token, + address, + signer: address, + }), + 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 { address, amount, to, token } = params; + + const addrType = getValidDestAddrType(address, token, to); + + const accountId = + addrType === "ethereum" + ? address + : this.api.createType("AccountId32", address).toHex(); + + const accountType = getDestAccountType(address, token, to); + + if (!validateAddress(address, addrType)) throw new InvalidAddress(address); + + const toChain = chains[to]; + + if (token !== this.balanceAdapter?.nativeToken) { + throw new TokenNotFound(token); + } + + // const accountId = this.api?.createType('AccountId32', address).toHex() + const paraChainId = toChain.paraChainId; + const rawAmount = amount.toChainData(); + + return this.api?.tx.polkadotXcm.limitedReserveTransferAssets( + createPolkadotXCMDest(this.api, paraChainId), + createPolkadotXCMAccount(this.api, accountId, accountType), + createPolkadotXCMAsset(this.api, rawAmount, "NATIVE"), + 0, + this.getDestWeight(token, to)?.toString() as any + ); + } +} + +export class SubsocialAdapter extends SubsocialBaseAdapter { + constructor() { + super(chains.subsocial, subsocialRouteConfigs, subsocialTokensConfig); + } +} diff --git a/src/configs/chains/polkadot-chains.ts b/src/configs/chains/polkadot-chains.ts index 7c157c9a..03bfb853 100644 --- a/src/configs/chains/polkadot-chains.ts +++ b/src/configs/chains/polkadot-chains.ts @@ -76,4 +76,12 @@ export const polkadotChains = { paraChainId: 2031, ss58Prefix: 36, }, + subsocial: { + id: "subsocial", + display: "Subsocial", + type: "substrate", + icon: "https://sub.id/images/subsocial.svg", + paraChainId: 2101, + ss58Prefix: 28, + }, };