diff --git a/public/images/asset/hdx.svg b/public/images/asset/hdx.svg new file mode 100644 index 0000000..091e31b --- /dev/null +++ b/public/images/asset/hdx.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/public/images/asset/ring.png b/public/images/asset/ring.png new file mode 100644 index 0000000..dfcbe8b Binary files /dev/null and b/public/images/asset/ring.png differ diff --git a/public/images/network/hydration.svg b/public/images/network/hydration.svg new file mode 100644 index 0000000..78ec9fd --- /dev/null +++ b/public/images/network/hydration.svg @@ -0,0 +1,5 @@ + + + diff --git a/public/images/wallet/evm.png b/public/images/wallet/evm.png new file mode 100644 index 0000000..7ebae0b Binary files /dev/null and b/public/images/wallet/evm.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..9efac90 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,5 @@ +{ + "name": "AssetHub Bridge - Darwinia", + "description": "Assets cross-chain between Darwinia and AssetHub.", + "icons": [{ "src": "icon.svg", "sizes": "any" }] +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 28bf927..42bd1db 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,6 +12,7 @@ const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "AssetHub Bridge - Darwinia", description: "Assets cross-chain between Darwinia and AssetHub.", + manifest: "/manifest.json", }; export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/src/components/balance-input.tsx b/src/components/balance-input.tsx index 92f454d..d7fbc9e 100644 --- a/src/components/balance-input.tsx +++ b/src/components/balance-input.tsx @@ -52,11 +52,11 @@ export default function BalanceInput({ }, [balance, asset, placeholder]); const min = useMemo(() => { - if (cross && cross.fee.asset.native) { + if (cross && cross.fee.asset.local.id === asset.id) { return cross.fee.amount; } return undefined; - }, [cross]); + }, [cross, asset.id]); const handleInputChange = useCallback>( (e) => { @@ -133,12 +133,14 @@ export default function BalanceInput({ text={`* Limit: ${formatBalance(assetLimit ?? BN_ZERO, asset?.decimals ?? 0)}, supply: ${formatBalance( (assetSupply ?? BN_ZERO).add(value?.amount ?? BN_ZERO), asset?.decimals ?? 0, - )}`} + )}.`} /> ) : requireMin ? ( - + ) : insufficient ? ( - + ) : null} {/* Invisible */} diff --git a/src/components/connect-wallet.tsx b/src/components/connect-wallet.tsx index 202cd7b..e10fa2b 100644 --- a/src/components/connect-wallet.tsx +++ b/src/components/connect-wallet.tsx @@ -77,21 +77,21 @@ export default function ConnectWallet({ who, kind = "component", height = "paddi [clearValue, setActiveWallet, setActiveAccount], ); - const [supportedRainbow, supportedTalisman] = useMemo(() => { - return [supported.some((id) => id === WalletID.RAINBOW), supported.some((id) => id === WalletID.TALISMAN)]; + const [supportedWalletEvm, supportedWalletTalisman] = useMemo(() => { + return [supported.some((id) => id === WalletID.EVM), supported.some((id) => id === WalletID.TALISMAN)]; }, [supported]); useEffect(() => { if (!supported.some((id) => id === WalletID.TALISMAN) && activeWallet === WalletID.TALISMAN) { setActiveWallet(undefined); clearValue(undefined); - } else if (!supported.some((id) => id === WalletID.RAINBOW) && activeWallet === WalletID.RAINBOW) { + } else if (!supported.some((id) => id === WalletID.EVM) && activeWallet === WalletID.EVM) { setActiveWallet(undefined); clearValue(undefined); } }, [supported, activeWallet, clearValue, setActiveWallet]); - const walletIcon = kind === "primary" ? null : activeWallet === WalletID.RAINBOW ? "rainbow.svg" : "talisman-red.svg"; + const walletIcon = kind === "primary" ? null : activeWallet === WalletID.EVM ? "evm.png" : "talisman-red.svg"; // Major for page header if (kind === "primary" && sender?.address) { @@ -105,7 +105,9 @@ export default function ConnectWallet({ who, kind = "component", height = "paddi return (talismanAccounts.length || activeAddress) && activeWallet ? ( ) : ( @@ -129,18 +131,18 @@ export default function ConnectWallet({ who, kind = "component", height = "paddi connectTalisman(); setIsOpenFalse(); }} - disabled={!supportedTalisman} + disabled={!supportedWalletTalisman} /> { - setActiveWallet(WalletID.RAINBOW); + setActiveWallet(WalletID.EVM); openConnectModal?.(); setIsOpenFalse(); }} - disabled={!supportedRainbow} + disabled={!supportedWalletEvm} /> @@ -195,7 +197,7 @@ function Item({ disabled={disabled} onClick={onClick} > - Wallet icon + Wallet icon {name} diff --git a/src/components/transfer.tsx b/src/components/transfer.tsx index 0436aca..8692833 100644 --- a/src/components/transfer.tsx +++ b/src/components/transfer.tsx @@ -4,7 +4,7 @@ import Button from "@/ui/button"; import BalanceInput from "./balance-input"; import ChainSelect from "./chain-select"; import TransferSection from "./transfer-section"; -import { formatBalance, isAssetExcess, parseCross } from "@/utils"; +import { formatBalance, isExceedingCrossChainLimit, parseCross } from "@/utils"; import { useTalisman, useTransfer } from "@/hooks"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import SwitchCross from "./switch-cross"; @@ -28,17 +28,20 @@ export default function Transfer() { const { sourceApi, targetApi, - assetLimit, - targetAssetDetails, + assetLimitOnTargetChain, + targetAssetSupply, sender, recipient, sourceChain, targetChain, sourceAsset, targetAsset, - usdtBalance, - sourceBalance, - targetBalance, + feeBalanceOnSourceChain, + existentialDepositOnTargetChain, + sourceAssetBalance, + targetAssetBalance, + sourceNativeBalance, + targetNativeBalance, transferAmount, bridgeInstance, activeSenderAccount, @@ -53,11 +56,13 @@ export default function Transfer() { setSourceAsset, setTargetAsset, setTransferAmount, - evmTransfer, - substrateTransfer, - refetchSourceBalance, - refetchTargetBalance, - refetchTargetAssetDetails, + transfer, + updateSourceAssetBalance, + updateTargetAssetBalance, + updateTargetAssetSupply, + updateSourceNativeBalance, + updateTargetNativeBalance, + updateFeeBalanceOnSourceChain, } = useTransfer(); const [sourceChainOptions, _setSourceChainOptions] = useState(defaultSourceChainOptions); const [targetChainOptions, setTargetChainOptions] = useState(defaultTargetChainOptions); @@ -76,31 +81,78 @@ export default function Transfer() { }, [sourceChain, targetChain, targetAsset]); const needSwitchNetwork = useMemo( - () => activeSenderWallet === WalletID.RAINBOW && chain && chain.id !== sourceChain.id, + () => activeSenderWallet === WalletID.EVM && chain && chain.id !== sourceChain.id, [chain, sourceChain, activeSenderWallet], ); - const alert = useMemo(() => { - const fee = bridgeInstance?.getCrossInfo()?.fee; - const balance = usdtBalance?.asset.value; + const sourceChainRef = useRef(sourceChain); + const targetChainRef = useRef(targetChain); + const sourceAssetRef = useRef(sourceAsset); + const targetAssetRef = useRef(targetAsset); - if (fee && balance && fee.amount.gt(balance)) { + const feeAlert = useMemo(() => { + const fee = bridgeInstance?.getCrossInfo()?.fee; + if (fee && feeBalanceOnSourceChain && fee.amount.gt(feeBalanceOnSourceChain.amount)) { return (
Warning - {`You need at least ${formatBalance(fee.amount, fee.asset.decimals)} ${ - fee.asset.symbol - } in your account to cover cross-chain fees.`} + {`You need at least ${formatBalance( + fee.amount, + feeBalanceOnSourceChain.currency.decimals, + )} ${feeBalanceOnSourceChain.currency.symbol} in your Sender on ${ + sourceChainRef.current.name + } to cover cross-chain fees.`}
); } return null; - }, [bridgeInstance, usdtBalance?.asset.value]); - - const sourceChainRef = useRef(sourceChain); - const targetChainRef = useRef(targetChain); - const sourceAssetRef = useRef(sourceAsset); - const targetAssetRef = useRef(targetAsset); + }, [bridgeInstance, feeBalanceOnSourceChain]); + const existentialAlertOnSourceChain = useMemo(() => { + if ( + sourceChain.existential && + sourceNativeBalance && + sourceNativeBalance.amount.lt(sourceChain.existential.minBalance) + ) { + return ( +
+ Warning + {`You need at least ${formatBalance( + sourceChain.existential.minBalance, + sourceChain.nativeCurrency.decimals, + )} ${sourceChain.nativeCurrency.symbol} in your Sender on ${ + sourceChain.name + } to keep an account alive.`} +
+ ); + } + return null; + }, [ + sourceChain.existential, + sourceChain.name, + sourceChain.nativeCurrency.decimals, + sourceChain.nativeCurrency.symbol, + sourceNativeBalance, + ]); + const existentialAlertOnTargetChain = useMemo(() => { + if ( + targetNativeBalance && + existentialDepositOnTargetChain && + targetNativeBalance.amount.lt(existentialDepositOnTargetChain.amount) + ) { + return ( +
+ Warning + {`You need at least ${formatBalance( + existentialDepositOnTargetChain.amount, + existentialDepositOnTargetChain.currency.decimals, + )} ${existentialDepositOnTargetChain.currency.symbol} in your Recipient on ${ + targetChainRef.current.name + } to keep an account alive.`} +
+ ); + } + return null; + }, [targetNativeBalance, existentialDepositOnTargetChain]); const _setSourceChain = useCallback( (chain: ChainConfig | undefined) => { @@ -157,47 +209,46 @@ export default function Transfer() { successCb: () => { setBusy(false); setTransferAmount({ valid: true, input: "", amount: BN_ZERO }); - refetchSourceBalance(); - refetchTargetBalance(); - refetchTargetAssetDetails(); + updateSourceAssetBalance(); + updateTargetAssetBalance(); + updateTargetAssetSupply(); + updateSourceNativeBalance(); + updateTargetNativeBalance(); + updateFeeBalanceOnSourceChain(); }, failedCb: () => { setBusy(false); }, }; + const sender_ = activeSenderAccount ?? (address && sender?.address === address ? address : undefined); setBusy(true); - if (await isAssetExcess(bridgeInstance, transferAmount.amount)) { - notification.error({ title: "Transaction failed", description: "Asset limit exceeded" }); - refetchTargetAssetDetails(); + if (await isExceedingCrossChainLimit(bridgeInstance, transferAmount.input)) { + notification.error({ title: "Transaction failed", description: "Exceeding the cross-chain limit." }); + updateTargetAssetSupply(); setBusy(false); - } else if (address && sender?.address === address) { - await evmTransfer(bridgeInstance, address, recipient.address, transferAmount.amount, callback); - } else if (activeSenderAccount) { - await substrateTransfer( - bridgeInstance, - activeSenderAccount, - recipient.address, - transferAmount.amount, - callback, - ); + } else if (sender_) { + await transfer(bridgeInstance, sender_, recipient.address, transferAmount.amount, callback); } } }, [ - sender, - address, activeSenderAccount, - needSwitchNetwork, + address, bridgeInstance, + needSwitchNetwork, recipient, - transferAmount, - sourceChain, + sender?.address, setTransferAmount, + sourceChain.id, switchNetwork, - evmTransfer, - substrateTransfer, - refetchSourceBalance, - refetchTargetBalance, - refetchTargetAssetDetails, + transfer, + transferAmount.amount, + transferAmount.input, + updateFeeBalanceOnSourceChain, + updateSourceAssetBalance, + updateTargetAssetBalance, + updateTargetAssetSupply, + updateSourceNativeBalance, + updateTargetNativeBalance, ]); useEffect(() => { @@ -218,22 +269,24 @@ export default function Transfer() { }, [sourceChain, targetChain, sourceAsset, _setTargetAsset]); const disabledSend = - !( - sender?.address && - sender.valid && - recipient?.address && - recipient.valid && - transferAmount.input && - transferAmount.valid - ) && !needSwitchNetwork; + !sender?.address || + !sender.valid || + !recipient?.address || + !recipient?.valid || + !transferAmount.input || + !transferAmount.valid || + needSwitchNetwork || + !!feeAlert || + !!existentialAlertOnSourceChain || + !!existentialAlertOnTargetChain; const senderOptions = - activeSenderWallet === WalletID.RAINBOW && address + activeSenderWallet === WalletID.EVM && address ? [{ address }] : activeSenderWallet === WalletID.TALISMAN ? talismanAccounts : []; const recipientOptions = - activeRecipientWallet === WalletID.RAINBOW && address + activeRecipientWallet === WalletID.EVM && address ? [{ address }] : activeRecipientWallet === WalletID.TALISMAN ? talismanAccounts @@ -282,9 +335,9 @@ export default function Transfer() { value={transferAmount} asset={sourceAsset} cross={bridgeInstance?.getCrossInfo()} - assetLimit={assetLimit} - assetSupply={targetAssetDetails?.supply} - balance={sourceBalance?.asset.value} + assetLimit={assetLimitOnTargetChain?.amount} + assetSupply={targetAssetSupply?.amount} + balance={sourceAssetBalance?.amount} assetOptions={sourceAssetOptions} onChange={setTransferAmount} onAssetChange={_setSourceAsset} @@ -303,23 +356,17 @@ export default function Transfer() { {/* Send */} - - {alert} + {feeAlert ?? existentialAlertOnSourceChain ?? existentialAlertOnTargetChain} ); } diff --git a/src/config/chains/assethub-polkadot-chain.ts b/src/config/chains/assethub-polkadot-chain.ts index ece7e54..12cf260 100644 --- a/src/config/chains/assethub-polkadot-chain.ts +++ b/src/config/chains/assethub-polkadot-chain.ts @@ -41,11 +41,27 @@ export const assethubPolkadotChain: ChainConfig = { name: "Tether USD", symbol: "USDT", decimals: 6, + origin: { + parachainId: ParachainID.ASSETHUB_POLKADOT, + palletInstance: 50, + id: 1984, + }, cross: [ { - isReserve: true, target: { network: "darwinia", symbol: "ahUSDT" }, - fee: { amount: bnToBn(20000), asset: { id: 1984, decimals: 6, symbol: "USDT", native: true } }, // 0.02 USDT + fee: { + amount: bnToBn(20000), + asset: { + local: { id: 1984 }, + origin: { + parachainId: ParachainID.ASSETHUB_POLKADOT, + palletInstance: 50, + id: 1984, + }, + }, + }, + section: "polkadotXcm", + method: "limitedReserveTransferAssets", }, ], }, @@ -55,11 +71,27 @@ export const assethubPolkadotChain: ChainConfig = { name: "PINK", symbol: "PINK", decimals: 10, + origin: { + parachainId: ParachainID.ASSETHUB_POLKADOT, + palletInstance: 50, + id: 23, + }, cross: [ { - isReserve: true, target: { network: "darwinia", symbol: "ahPINK" }, - fee: { amount: bnToBn(20000), asset: { id: 1984, decimals: 6, symbol: "USDT", native: false } }, // 0.02 USDT + fee: { + amount: bnToBn(20000), + asset: { + local: { id: 1984 }, + origin: { + parachainId: ParachainID.ASSETHUB_POLKADOT, + palletInstance: 50, + id: 1984, + }, + }, + }, + section: "polkadotXcm", + method: "limitedReserveTransferAssets", }, ], }, @@ -72,4 +104,5 @@ export const assethubPolkadotChain: ChainConfig = { */ endpoint: "wss://polkadot-asset-hub-rpc.polkadot.io", parachainId: ParachainID.ASSETHUB_POLKADOT, + existential: { minBalance: bnToBn("500000000") }, // 0.05 DOT }; diff --git a/src/config/chains/assethub-rococo-chain.ts b/src/config/chains/assethub-rococo-chain.ts index 2bdd097..d5d0244 100644 --- a/src/config/chains/assethub-rococo-chain.ts +++ b/src/config/chains/assethub-rococo-chain.ts @@ -42,11 +42,27 @@ export const assethubRococoChain: ChainConfig = { name: "Tether USD Test", symbol: "USDT", decimals: 6, + origin: { + parachainId: ParachainID.ASSETHUB_ROCOCO, + palletInstance: 50, + id: 7777, + }, cross: [ { - isReserve: true, target: { network: "pangolin", symbol: "ahUSDT" }, - fee: { amount: bnToBn(125000), asset: { id: 7777, decimals: 6, symbol: "USDT", native: true } }, // 0.125 USDT + fee: { + amount: bnToBn(125000), + asset: { + local: { id: 7777 }, + origin: { + parachainId: ParachainID.ASSETHUB_ROCOCO, + palletInstance: 50, + id: 7777, + }, + }, + }, + section: "polkadotXcm", + method: "limitedReserveTransferAssets", }, ], }, diff --git a/src/config/chains/darwinia-chain.ts b/src/config/chains/darwinia-chain.ts index bf565f7..0c4aa21 100644 --- a/src/config/chains/darwinia-chain.ts +++ b/src/config/chains/darwinia-chain.ts @@ -1,4 +1,4 @@ -import { ChainConfig, ChainID, ParachainID, WalletID } from "@/types"; +import { AssetID, ChainConfig, ChainID, ParachainID, WalletID } from "@/types"; import { bnToBn } from "@polkadot/util"; export const darwiniaChain: ChainConfig = { @@ -35,17 +35,63 @@ export const darwiniaChain: ChainConfig = { */ logo: "darwinia.png", assets: [ + { + icon: "ring.png", + id: AssetID.SYSTEM, + name: "RING", + symbol: "RING", + decimals: 18, + origin: { + parachainId: ParachainID.DARWINIA, + palletInstance: 5, + id: AssetID.SYSTEM, + }, + cross: [ + { + target: { network: "hydradx", symbol: "RING" }, + fee: { + amount: bnToBn("1000000000000000000"), // 1 RING + asset: { + local: { id: AssetID.SYSTEM }, + origin: { + parachainId: ParachainID.DARWINIA, + palletInstance: 5, + id: AssetID.SYSTEM, + }, + }, + }, + section: "polkadotXcm", + method: "reserveTransferAssets", + }, + ], + }, { icon: "usdt.svg", id: 1027, name: "Tether USD", symbol: "ahUSDT", decimals: 6, + origin: { + parachainId: ParachainID.ASSETHUB_POLKADOT, + palletInstance: 50, + id: 1984, + }, cross: [ { - isReserve: false, target: { network: "assethub-polkadot", symbol: "USDT" }, - fee: { amount: bnToBn(700000), asset: { id: 1984, decimals: 6, symbol: "ahUSDT", native: true } }, // 0.7 USDT + fee: { + amount: bnToBn(700000), + asset: { + local: { id: 1027 }, + origin: { + parachainId: ParachainID.ASSETHUB_POLKADOT, + palletInstance: 50, + id: 1984, + }, + }, + }, + section: "xTokens", + method: "transferMultiassets", }, ], }, @@ -55,18 +101,33 @@ export const darwiniaChain: ChainConfig = { name: "PINK", symbol: "ahPINK", decimals: 10, + origin: { + parachainId: ParachainID.ASSETHUB_POLKADOT, + palletInstance: 50, + id: 23, + }, cross: [ { - isReserve: false, target: { network: "assethub-polkadot", symbol: "PINK" }, - fee: { amount: bnToBn(700000), asset: { id: 1984, decimals: 6, symbol: "ahUSDT", native: false } }, // 0.7 USDT + fee: { + amount: bnToBn(700000), + asset: { + local: { id: 1027 }, + origin: { + parachainId: ParachainID.ASSETHUB_POLKADOT, + palletInstance: 50, + id: 1984, + }, + }, + }, + section: "xTokens", + method: "transferMultiassets", }, ], }, ], - wallets: [WalletID.RAINBOW, WalletID.TALISMAN], + wallets: [WalletID.EVM, WalletID.TALISMAN], addressType: "evm", - hasAssetLimit: true, /** * Substrate diff --git a/src/config/chains/hydradx-chain.ts b/src/config/chains/hydradx-chain.ts new file mode 100644 index 0000000..06431d1 --- /dev/null +++ b/src/config/chains/hydradx-chain.ts @@ -0,0 +1,77 @@ +import { AssetID, ChainConfig, ChainID, ParachainID, WalletID } from "@/types"; +import { bnToBn } from "@polkadot/util"; + +export const hydradxChain: ChainConfig = { + /** + * Chain + */ + id: ChainID.HYDRADX, + network: "hydradx", + name: "HydraDX", + nativeCurrency: { + name: "HDX", + symbol: "HDX", + decimals: 12, + }, + rpcUrls: { + default: { + http: ["https://rpc.hydradx.cloud"], + webSocket: ["wss://rpc.hydradx.cloud"], + }, + public: { + http: ["https://rpc.hydradx.cloud"], + webSocket: ["wss://rpc.hydradx.cloud"], + }, + }, + blockExplorers: { + default: { + name: "Subscan", + url: "https://hydration.subscan.io/", + }, + }, + + /** + * Custom + */ + logo: "hydration.svg", + assets: [ + { + icon: "ring.png", + id: 31, + name: "Darwinia Network RING", + symbol: "RING", + decimals: 18, + origin: { + parachainId: ParachainID.DARWINIA, + palletInstance: 5, + id: AssetID.SYSTEM, + }, + cross: [ + { + target: { network: "darwinia", symbol: "RING" }, + fee: { + amount: bnToBn("5000000000000000000"), // 5 RING + asset: { + local: { id: 31 }, + origin: { + parachainId: ParachainID.DARWINIA, + palletInstance: 5, + id: AssetID.SYSTEM, + }, + }, + }, + section: "xTokens", + method: "transferMultiasset", + }, + ], + }, + ], + wallets: [WalletID.TALISMAN], + addressType: "substrate", + + /** + * Substrate + */ + endpoint: "wss://rpc.hydradx.cloud", + parachainId: ParachainID.HYDRADX, +}; diff --git a/src/config/chains/index.ts b/src/config/chains/index.ts index fa4b4d7..e065e9b 100644 --- a/src/config/chains/index.ts +++ b/src/config/chains/index.ts @@ -2,3 +2,4 @@ export * from "./pangolin-chain"; export * from "./darwinia-chain"; export * from "./assethub-rococo-chain"; export * from "./assethub-polkadot-chain"; +export * from "./hydradx-chain"; diff --git a/src/config/chains/pangolin-chain.ts b/src/config/chains/pangolin-chain.ts index e010311..5335523 100644 --- a/src/config/chains/pangolin-chain.ts +++ b/src/config/chains/pangolin-chain.ts @@ -42,18 +42,33 @@ export const pangolinChain: ChainConfig = { name: "Tether USD", symbol: "ahUSDT", decimals: 6, + origin: { + parachainId: ParachainID.ASSETHUB_ROCOCO, + palletInstance: 50, + id: 7777, + }, cross: [ { - isReserve: false, target: { network: "assethub-rococo", symbol: "USDT" }, - fee: { amount: bnToBn(3600000), asset: { id: 7777, decimals: 6, symbol: "ahUSDT", native: true } }, // 3.6 USDT + fee: { + amount: bnToBn(3600000), + asset: { + local: { id: 1027 }, + origin: { + parachainId: ParachainID.ASSETHUB_ROCOCO, + palletInstance: 50, + id: 7777, + }, + }, + }, + section: "xTokens", + method: "transferMultiassets", }, ], }, ], - wallets: [WalletID.RAINBOW, WalletID.TALISMAN], + wallets: [WalletID.EVM, WalletID.TALISMAN], addressType: "evm", - hasAssetLimit: true, /** * Substrate diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 385ed7b..5a6af5d 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,8 +1,11 @@ export * from "./use-talisman"; export * from "./use-transfer"; export * from "./use-api"; -export * from "./use-balance"; +export * from "./use-asset-balance"; export * from "./use-toggle"; export * from "./use-asset-limit"; -export * from "./use-asset-details"; +export * from "./use-asset-supply"; export * from "./use-account-name"; +export * from "./use-existential-deposit"; +export * from "./use-fee-balance"; +export * from "./use-native-balance"; diff --git a/src/hooks/use-asset-balance.ts b/src/hooks/use-asset-balance.ts new file mode 100644 index 0000000..e5e0dc5 --- /dev/null +++ b/src/hooks/use-asset-balance.ts @@ -0,0 +1,41 @@ +import { BaseBridge } from "@/libs"; +import { useCallback, useEffect, useState } from "react"; +import { BN } from "@polkadot/util"; +import { Currency } from "@/types"; +import { from, EMPTY } from "rxjs"; + +export function useAssetBalance( + bridge: BaseBridge | undefined, + account: { address: string; valid: boolean } | undefined, + position: "source" | "target", +) { + const [value, setValue] = useState<{ currency: Currency; amount: BN }>(); + + const update = useCallback(() => { + if (bridge && account?.address && account.valid) { + return from( + position === "source" + ? bridge.getSourceAssetBalance(account.address) + : bridge.getTargetAssetBalance(account.address), + ).subscribe({ + next: setValue, + error: (err) => { + console.error(err); + }, + }); + } else { + setValue(undefined); + } + + return EMPTY.subscribe(); + }, [bridge, account, position]); + + useEffect(() => { + const sub$$ = update(); + return () => { + sub$$?.unsubscribe(); + }; + }, [update]); + + return { value, update }; +} diff --git a/src/hooks/use-asset-details.ts b/src/hooks/use-asset-details.ts deleted file mode 100644 index 8260689..0000000 --- a/src/hooks/use-asset-details.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { EvmBridge } from "@/libs"; -import type { PalletAssetsAssetDetails } from "@polkadot/types/lookup"; -import { useCallback, useEffect, useState } from "react"; -import { from, EMPTY } from "rxjs"; - -export function useAssetDetails(bridge: EvmBridge | undefined, position: "source" | "target") { - const [assetDetails, setAssetDetails] = useState(); - - const updateBalance = useCallback(() => { - if (bridge) { - return from(position === "source" ? bridge.getSourceAssetDetails() : bridge.getTargetAssetDetails()).subscribe({ - next: setAssetDetails, - error: (err) => { - console.error(err); - setAssetDetails(undefined); - }, - }); - } else { - setAssetDetails(undefined); - } - - return EMPTY.subscribe(); - }, [bridge, position]); - - useEffect(() => { - const sub$$ = updateBalance(); - return () => sub$$.unsubscribe(); - }, [updateBalance]); - - return { assetDetails, refetch: updateBalance }; -} diff --git a/src/hooks/use-asset-limit.ts b/src/hooks/use-asset-limit.ts index 8bf7cee..9429015 100644 --- a/src/hooks/use-asset-limit.ts +++ b/src/hooks/use-asset-limit.ts @@ -1,28 +1,31 @@ -import { EvmBridge } from "@/libs"; -import { useEffect, useState } from "react"; -import { from, Subscription } from "rxjs"; +import { BaseBridge, UniversalBridge } from "@/libs"; +import { useCallback, useEffect, useState } from "react"; +import { from } from "rxjs"; import { BN } from "@polkadot/util"; +import { Currency } from "@/types"; -export function useAssetLimit(bridge: EvmBridge | undefined) { - const [assetLimit, setAssetLimit] = useState(); +export function useAssetLimit(bridge: BaseBridge | undefined, position: "source" | "target") { + const [value, setValue] = useState<{ currency: Currency; amount: BN }>(); - useEffect(() => { - let sub$$: Subscription | undefined; - - if (bridge) { - sub$$ = from(bridge.getAssetLimit()).subscribe({ - next: setAssetLimit, + const update = useCallback(() => { + if (bridge && position === "target") { + return from(bridge.getAssetLimitOnTargetChain()).subscribe({ + next: setValue, error: (err) => { console.error(err); - setAssetLimit(undefined); }, }); } else { - setAssetLimit(undefined); + setValue(undefined); } + }, [bridge, position]); - return () => sub$$?.unsubscribe(); - }, [bridge]); + useEffect(() => { + const sub$$ = update(); + return () => { + sub$$?.unsubscribe(); + }; + }, [update]); - return { assetLimit }; + return { value, update }; } diff --git a/src/hooks/use-asset-supply.ts b/src/hooks/use-asset-supply.ts new file mode 100644 index 0000000..145673a --- /dev/null +++ b/src/hooks/use-asset-supply.ts @@ -0,0 +1,33 @@ +import { BaseBridge } from "@/libs"; +import { Currency } from "@/types"; +import type { BN } from "@polkadot/util"; +import { useCallback, useEffect, useState } from "react"; +import { from, EMPTY } from "rxjs"; + +export function useAssetSupply(bridge: BaseBridge | undefined, position: "source" | "target") { + const [value, setValue] = useState<{ currency: Currency; amount: BN }>(); + + const update = useCallback(() => { + if (bridge) { + return from(position === "source" ? bridge.getSourceAssetSupply() : bridge.getTargetAssetSupply()).subscribe({ + next: setValue, + error: (err) => { + console.error(err); + }, + }); + } else { + setValue(undefined); + } + + return EMPTY.subscribe(); + }, [bridge, position]); + + useEffect(() => { + const sub$$ = update(); + return () => { + sub$$?.unsubscribe(); + }; + }, [update]); + + return { value, update }; +} diff --git a/src/hooks/use-balance.ts b/src/hooks/use-balance.ts deleted file mode 100644 index d980a3d..0000000 --- a/src/hooks/use-balance.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { EvmBridge } from "@/libs"; -import { useCallback, useEffect, useState } from "react"; -import { BN } from "@polkadot/util"; -import { Asset } from "@/types"; -import { forkJoin, EMPTY } from "rxjs"; - -export function useBalance( - bridge: EvmBridge | undefined, - value: { address: string; valid: boolean } | undefined, - type: "source" | "target" | "usdt", -) { - const [balance, setBalance] = useState<{ asset: { value: BN; asset: Asset } }>(); - - const updateBalance = useCallback(() => { - if (bridge && value?.address && value.valid) { - return forkJoin([ - type === "usdt" - ? bridge.getSourceUsdtBalance(value.address) - : type === "source" - ? bridge.getSourceAssetBalance(value.address) - : bridge.getTargetAssetBalance(value.address), - ]).subscribe({ - next: ([asset]) => { - setBalance(asset ? { asset } : undefined); - }, - error: (err) => { - console.error(err); - setBalance(undefined); - }, - }); - } else { - setBalance(undefined); - } - - return EMPTY.subscribe(); - }, [bridge, value, type]); - - useEffect(() => { - const sub$$ = updateBalance(); - return () => sub$$.unsubscribe(); - }, [updateBalance]); - - return { balance, refetch: updateBalance }; -} diff --git a/src/hooks/use-existential-deposit.ts b/src/hooks/use-existential-deposit.ts new file mode 100644 index 0000000..6b0feae --- /dev/null +++ b/src/hooks/use-existential-deposit.ts @@ -0,0 +1,33 @@ +import { BaseBridge } from "@/libs"; +import { useCallback, useEffect, useState } from "react"; +import type { BN } from "@polkadot/util"; +import { from } from "rxjs"; +import { Currency } from "@/types"; + +export function useExistentialDeposit(bridge: BaseBridge | undefined, position: "source" | "target") { + const [value, setValue] = useState<{ currency: Currency; amount: BN }>(); + + const update = useCallback(() => { + if (bridge) { + return from( + position === "source" ? bridge.getSourceExistentialDeposit() : bridge.getTargetExistentialDeposit(), + ).subscribe({ + next: setValue, + error: (err) => { + console.error(err); + }, + }); + } else { + setValue(undefined); + } + }, [bridge, position]); + + useEffect(() => { + const sub$$ = update(); + return () => { + sub$$?.unsubscribe(); + }; + }, [update]); + + return { value, update }; +} diff --git a/src/hooks/use-fee-balance.ts b/src/hooks/use-fee-balance.ts new file mode 100644 index 0000000..8946536 --- /dev/null +++ b/src/hooks/use-fee-balance.ts @@ -0,0 +1,37 @@ +import { BaseBridge } from "@/libs"; +import { useCallback, useEffect, useState } from "react"; +import { BN } from "@polkadot/util"; +import { Currency } from "@/types"; +import { from, EMPTY } from "rxjs"; + +export function useFeeBalance( + bridge: BaseBridge | undefined, + account: { address: string; valid: boolean } | undefined, + position: "source" | "target", +) { + const [value, setValue] = useState<{ currency: Currency; amount: BN }>(); + + const update = useCallback(() => { + if (bridge && account?.address && account.valid && position === "source") { + return from(bridge.getFeeBalanceOnSourceChain(account.address)).subscribe({ + next: setValue, + error: (err) => { + console.error(err); + }, + }); + } else { + setValue(undefined); + } + + return EMPTY.subscribe(); + }, [bridge, account, position]); + + useEffect(() => { + const sub$$ = update(); + return () => { + sub$$?.unsubscribe(); + }; + }, [update]); + + return { value, update }; +} diff --git a/src/hooks/use-native-balance.ts b/src/hooks/use-native-balance.ts new file mode 100644 index 0000000..ad92f17 --- /dev/null +++ b/src/hooks/use-native-balance.ts @@ -0,0 +1,41 @@ +import { BaseBridge } from "@/libs"; +import { useCallback, useEffect, useState } from "react"; +import { BN } from "@polkadot/util"; +import { Currency } from "@/types"; +import { from, EMPTY } from "rxjs"; + +export function useNativeBalance( + bridge: BaseBridge | undefined, + account: { address: string; valid: boolean } | undefined, + position: "source" | "target", +) { + const [value, setValue] = useState<{ currency: Currency; amount: BN }>(); + + const update = useCallback(() => { + if (bridge && account?.address && account.valid) { + return from( + position === "source" + ? bridge.getSourceNativeBalance(account.address) + : bridge.getTargetNativeBalance(account.address), + ).subscribe({ + next: setValue, + error: (err) => { + console.error(err); + }, + }); + } else { + setValue(undefined); + } + + return EMPTY.subscribe(); + }, [bridge, account, position]); + + useEffect(() => { + const sub$$ = update(); + return () => { + sub$$?.unsubscribe(); + }; + }, [update]); + + return { value, update }; +} diff --git a/src/libs/bridge/base.ts b/src/libs/bridge/base.ts index 7aaf2d1..25f0bec 100644 --- a/src/libs/bridge/base.ts +++ b/src/libs/bridge/base.ts @@ -1,7 +1,8 @@ -import { Asset, ChainConfig, Cross } from "@/types"; +import { Asset, AssetID, ChainConfig, Cross } from "@/types"; import { ApiPromise } from "@polkadot/api"; -import { BN_ZERO, BN, bnToBn } from "@polkadot/util"; +import { BN_ZERO, bnToBn, isFunction } from "@polkadot/util"; import { Option, u128 } from "@polkadot/types"; +import { parseUnits } from "viem"; export abstract class BaseBridge { protected readonly cross: Cross | undefined; @@ -53,95 +54,122 @@ export abstract class BaseBridge { } /** + * Native Balance * Token decimals and symbol: api.rpc.system.properties */ - private async getNativeBalance(api: ApiPromise, address: string) { - const balancesAll = await api.derive.balances.all(address); + private async getNativeBalance(api: ApiPromise, account: string) { + const balancesAll = await api.derive.balances.all(account); const locked = balancesAll.lockedBalance; const transferrable = balancesAll.availableBalance; const total = balancesAll.freeBalance.add(balancesAll.reservedBalance); return { transferrable, locked, total }; } - - async getSourceNativeBalance(address: string) { - const balances = await this.getNativeBalance(this.sourceApi, address); - return { ...balances, currency: this.sourceChain.nativeCurrency }; + async getSourceNativeBalance(account: string) { + const { transferrable } = await this.getNativeBalance(this.sourceApi, account); + return { amount: transferrable.toBn(), currency: this.sourceChain.nativeCurrency }; } - - async getTargetNativeBalance(address: string) { - const balances = await this.getNativeBalance(this.targetApi, address); - return { ...balances, currency: this.targetChain.nativeCurrency }; + async getTargetNativeBalance(account: string) { + const { transferrable } = await this.getNativeBalance(this.targetApi, account); + return { amount: transferrable.toBn(), currency: this.targetChain.nativeCurrency }; } /** + * Asset Balance * Token name, symbol and decimals: api.query.assets.metadata */ - private async getAssetBalance(api: ApiPromise, asset: Asset, address: string) { - const assetOption = await api.query.assets.account(asset.id, address); - if (assetOption.isSome) { - return assetOption.unwrap().balance as BN; + private async getAssetBalance(api: ApiPromise, asset: Asset, account: string) { + let amount = BN_ZERO; + + if (asset.id === AssetID.SYSTEM) { + amount = (await this.getNativeBalance(api, account)).transferrable; + } else if (isFunction(api.query.assets?.account)) { + const assetOption = await api.query.assets.account(asset.id, account); + amount = assetOption.isSome ? assetOption.unwrap().balance.toBn() : BN_ZERO; + } else if (isFunction(api.query.tokens?.accounts)) { + const { free } = (await api.query.tokens.accounts(account, asset.id)).toJSON() as { free?: string }; + amount = bnToBn(free ?? 0); } - return BN_ZERO; - } - async getSourceAssetBalance(address: string) { - const asset = this.sourceAsset; - const value = await this.getAssetBalance(this.sourceApi, asset, address); - return { value, asset }; + const { symbol, name, decimals } = asset; + return { currency: { symbol, name, decimals }, amount }; } - - async getTargetAssetBalance(address: string) { - const asset = this.targetAsset; - const value = await this.getAssetBalance(this.targetApi, asset, address); - return { value, asset }; + async getSourceAssetBalance(account: string) { + return this.getAssetBalance(this.sourceApi, this.sourceAsset, account); + } + async getTargetAssetBalance(account: string) { + return this.getAssetBalance(this.targetApi, this.targetAsset, account); } - async getSourceUsdtBalance(address: string) { - const asset = this.sourceChain.assets.find(({ symbol }) => symbol.toLowerCase().includes("usdt")); + async getFeeBalanceOnSourceChain(account: string) { + const asset = this.sourceChain.assets.find(({ id }) => id === this.cross?.fee.asset.local.id); if (asset) { - const value = await this.getAssetBalance(this.sourceApi, asset, address); - return { value, asset }; + return this.getAssetBalance(this.sourceApi, asset, account); } } /** * Supply */ - private async getAssetDetails(api: ApiPromise, asset: Asset) { - const detailsOption = await api.query.assets.asset(asset.id); - return detailsOption.isSome ? detailsOption.unwrap() : undefined; + private async getAssetSupply(api: ApiPromise, asset: Asset) { + let amount = BN_ZERO; + if (asset.id === AssetID.SYSTEM) { + } else if (isFunction(api.query.assets?.asset)) { + const detailsOption = await api.query.assets.asset(asset.id); + amount = detailsOption.isSome ? detailsOption.unwrap().supply.toBn() : BN_ZERO; + } else if (isFunction(api.query.tokens?.totalIssuance)) { + amount = bnToBn((await api.query.tokens.totalIssuance(asset.id)).toString()); + } + const { symbol, name, decimals } = asset; + return { currency: { symbol, name, decimals }, amount }; } - - async getSourceAssetDetails() { - return this.getAssetDetails(this.sourceApi, this.sourceAsset); + async getSourceAssetSupply() { + return this.getAssetSupply(this.sourceApi, this.sourceAsset); } - - async getTargetAssetDetails() { - return this.getAssetDetails(this.targetApi, this.targetAsset); + async getTargetAssetSupply() { + return this.getAssetSupply(this.targetApi, this.targetAsset); } - async getAssetLimit() { + async getAssetLimitOnTargetChain() { const section = "assetLimit"; const method = "foreignAssetLimit"; const fn = this.targetApi.query[section]?.[method]; - if (this.targetChain.hasAssetLimit && fn) { + const { symbol, name, decimals, origin } = this.targetAsset; + let amount = bnToBn(parseUnits(Number.MAX_SAFE_INTEGER.toString(), decimals)); + + if (isFunction(fn) && origin.id !== AssetID.SYSTEM) { const limitOption = await (fn({ Xcm: { - parents: 1, + parents: origin.parachainId === this.targetChain.parachainId ? 0 : 1, interior: { X3: [ - { Parachain: bnToBn(this.sourceChain.parachainId) }, - { PalletInstance: 50 }, - { GeneralIndex: bnToBn(this.sourceAsset.id) }, + { Parachain: origin.parachainId }, + { PalletInstance: origin.palletInstance }, + { GeneralIndex: origin.id }, ], }, }, }) as Promise>); - - return limitOption.isSome ? limitOption.unwrap() : undefined; + amount = limitOption.isSome ? limitOption.unwrap().toBn() : amount; } + return { currency: { symbol, name, decimals }, amount }; } - abstract transfer(): Promise; + /** + * Existential Deposit + */ + private getExistentialDeposit(api: ApiPromise) { + const section = "balances"; + const method = "existentialDeposit"; + const c = api.consts[section]?.[method]; + return c ? c.toBn() : BN_ZERO; + } + async getSourceExistentialDeposit() { + const amount = await this.getExistentialDeposit(this.sourceApi); + return { currency: this.sourceChain.nativeCurrency, amount }; + } + async getTargetExistentialDeposit() { + const amount = await this.getExistentialDeposit(this.targetApi); + return { currency: this.targetChain.nativeCurrency, amount }; + } } diff --git a/src/libs/bridge/index.ts b/src/libs/bridge/index.ts index b1f320b..eab43ac 100644 --- a/src/libs/bridge/index.ts +++ b/src/libs/bridge/index.ts @@ -1,3 +1,3 @@ export * from "./base"; -export * from "./evm"; +export * from "./universal"; export * from "./substrate"; diff --git a/src/libs/bridge/substrate.ts b/src/libs/bridge/substrate.ts index 90535ee..bc6ef26 100644 --- a/src/libs/bridge/substrate.ts +++ b/src/libs/bridge/substrate.ts @@ -1,13 +1,9 @@ import { BaseBridge } from "./base"; -import { BN, bnToBn, u8aToHex } from "@polkadot/util"; +import { BN, u8aToHex } from "@polkadot/util"; import { Asset, ChainConfig } from "@/types"; import { ApiPromise } from "@polkadot/api"; import { decodeAddress } from "@polkadot/util-crypto"; -/** - * Supported wallets: Talisman - */ - export class SubstrateBridge extends BaseBridge { constructor(args: { sourceApi: ApiPromise; @@ -20,42 +16,58 @@ export class SubstrateBridge extends BaseBridge { super(args); } - async transfer(): Promise { - return; + async transfer(recipient: string, amount: BN) { + switch (`${this.cross?.section}.${this.cross?.method}`) { + case "xTokens.transferMultiassets": + return this.xTokensTransferMultiassets(recipient, amount); + case "polkadotXcm.limitedReserveTransferAssets": + return this.polkadotXcmLimitedReserveTransferAssets(recipient, amount); + case "polkadotXcm.reserveTransferAssets": + return this.polkadotXcmReserveTransferAssets(recipient, amount); + case "xTokens.transferMultiasset": + return this.xTokensTransferMultiasset(recipient, amount); + } } /** - * To Asset-Hub + * To Assethub * @param recipient Address * @param amount Transfer amount * @returns Promise> */ - async transferAssets(recipient: string, amount: BN) { + private async xTokensTransferMultiassets(recipient: string, amount: BN) { const section = "xTokens"; const method = "transferMultiassets"; const fn = this.sourceApi.tx[section][method]; - const Parachain = bnToBn(this.targetChain.parachainId); const assetItems = [ { id: { Concrete: { - parents: 1, + parents: this.sourceAsset.origin.parachainId === this.sourceChain.parachainId ? 0 : 1, interior: { - X3: [{ Parachain }, { PalletInstance: 50 }, { GeneralIndex: bnToBn(this.targetAsset.id) }], + X3: [ + { Parachain: this.sourceAsset.origin.parachainId }, + { PalletInstance: this.sourceAsset.origin.palletInstance }, + { GeneralIndex: this.sourceAsset.origin.id }, + ], }, }, }, fun: { Fungible: amount }, }, ]; - if (this.cross && !this.cross.fee.asset.native) { + if (this.cross && this.cross.fee.asset.local.id !== this.sourceAsset.id) { assetItems.push({ id: { Concrete: { - parents: 1, + parents: this.cross.fee.asset.origin.parachainId === this.sourceChain.parachainId ? 0 : 1, interior: { - X3: [{ Parachain }, { PalletInstance: 50 }, { GeneralIndex: bnToBn(this.cross.fee.asset.id) }], + X3: [ + { Parachain: this.cross.fee.asset.origin.parachainId }, + { PalletInstance: this.cross.fee.asset.origin.palletInstance }, + { GeneralIndex: this.cross.fee.asset.origin.id }, + ], }, }, }, @@ -64,12 +76,15 @@ export class SubstrateBridge extends BaseBridge { } const _assets = { V3: assetItems }; - const _feeAssetItem = bnToBn(assetItems.length - 1); + const _feeAssetItem = assetItems.length - 1; const _dest = { V3: { parents: 1, interior: { - X2: [{ Parachain }, { AccountId32: { network: null, id: u8aToHex(decodeAddress(recipient)) } }], + X2: [ + { Parachain: this.targetChain.parachainId }, + { AccountId32: { network: null, id: u8aToHex(decodeAddress(recipient)) } }, + ], }, }, }; @@ -80,47 +95,130 @@ export class SubstrateBridge extends BaseBridge { } /** - * From Asset-Hub + * From Assethub * @param recipient Address * @param amount Transfer amount * @returns Promise> */ - async limitedReserveTransferAssets(recipient: string, amount: BN) { + private async polkadotXcmLimitedReserveTransferAssets(recipient: string, amount: BN) { const section = "polkadotXcm"; const method = "limitedReserveTransferAssets"; const fn = this.sourceApi.tx[section][method]; - const Parachain = bnToBn(this.targetChain.parachainId); const assetItems = [ { id: { Concrete: { - parents: 0, - interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: bnToBn(this.sourceAsset.id) }] }, + parents: this.sourceAsset.origin.parachainId === this.sourceChain.parachainId ? 0 : 1, + interior: { + X2: [ + { PalletInstance: this.sourceAsset.origin.palletInstance }, + { GeneralIndex: this.sourceAsset.origin.id }, + ], + }, }, }, fun: { Fungible: amount }, }, ]; - if (this.cross && !this.cross.fee.asset.native) { + if (this.cross && this.cross.fee.asset.local.id !== this.sourceAsset.id) { assetItems.push({ id: { Concrete: { - parents: 0, - interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: bnToBn(this.cross.fee.asset.id) }] }, + parents: this.cross.fee.asset.origin.parachainId === this.sourceChain.parachainId ? 0 : 1, + interior: { + X2: [ + { PalletInstance: this.cross.fee.asset.origin.palletInstance }, + { GeneralIndex: this.cross.fee.asset.origin.id }, + ], + }, }, }, fun: { Fungible: this.cross.fee.amount }, }); } - const _dest = { V3: { parents: 1, interior: { X1: { Parachain } } } }; + const _dest = { V3: { parents: 1, interior: { X1: { Parachain: this.targetChain.parachainId } } } }; const _beneficiary = { V3: { parents: 0, interior: { X1: { AccountKey20: { network: null, key: recipient } } } } }; const _assets = { V3: assetItems }; - const _feeAssetItem = bnToBn(assetItems.length - 1); + const _feeAssetItem = assetItems.length - 1; const _weightLimit = { Unlimited: null }; const extrinsic = fn(_dest, _beneficiary, _assets, _feeAssetItem, _weightLimit); return extrinsic; } + + /** + * Transfer RING from Darwinia to HydraDX + * Refer https://github.com/darwinia-network/assethub-bridge-ui/issues/29#issuecomment-2167207048 + * @param recipient Address + * @param amount Transfer amount + * @returns Promise> + */ + private async polkadotXcmReserveTransferAssets(recipient: string, amount: BN) { + const section = "polkadotXcm"; + const method = "reserveTransferAssets"; + const fn = this.sourceApi.tx[section][method]; + + const _dest = { V2: { parents: 1, interior: { X1: { Parachain: this.targetChain.parachainId } } } }; + const _beneficiary = { + V2: { parents: 0, interior: { X1: { AccountId32: { network: null, id: u8aToHex(decodeAddress(recipient)) } } } }, + }; + const _assets = { + V2: [ + { + id: { + Concrete: { parents: 0, interior: { X1: { PalletInstance: this.sourceAsset.origin.palletInstance } } }, + }, + fun: { Fungible: amount }, + }, + ], + }; + const _feeAssetItem = 0; + + const extrinsic = fn(_dest, _beneficiary, _assets, _feeAssetItem); + return extrinsic; + } + + /** + * Transfer RING from HydraDX to Darwinia + * Refer https://github.com/darwinia-network/assethub-bridge-ui/issues/29#issuecomment-2167207048 + * @param recipient Address + * @param amount Transfer amount + * @returns Promise> + */ + private async xTokensTransferMultiasset(recipient: string, amount: BN) { + const section = "xTokens"; + const method = "transferMultiasset"; + const fn = this.sourceApi.tx[section][method]; + + const _asset = { + V2: { + id: { + Concrete: { + parents: this.sourceAsset.origin.parachainId === this.sourceChain.parachainId ? 0 : 1, + interior: { + X2: [ + { Parachain: this.sourceAsset.origin.parachainId }, + { PalletInstance: this.sourceAsset.origin.palletInstance }, + ], + }, + }, + }, + fun: { Fungible: amount }, + }, + }; + const _dest = { + V2: { + parents: 1, + interior: { + X2: [{ Parachain: this.targetChain.parachainId }, { AccountKey20: { network: null, key: recipient } }], + }, + }, + }; + const _weightLimit = { Unlimited: null }; + + const extrinsic = fn(_asset, _dest, _weightLimit); + return extrinsic; + } } diff --git a/src/libs/bridge/evm.ts b/src/libs/bridge/universal.ts similarity index 56% rename from src/libs/bridge/evm.ts rename to src/libs/bridge/universal.ts index f7f869b..9e5b368 100644 --- a/src/libs/bridge/evm.ts +++ b/src/libs/bridge/universal.ts @@ -2,14 +2,10 @@ import { ApiPromise } from "@polkadot/api"; import { BN, u8aToHex } from "@polkadot/util"; import { SubstrateBridge } from "./substrate"; import { Asset, ChainConfig } from "@/types"; -import { Address, PublicClient, WalletClient } from "wagmi"; +import { PublicClient, WalletClient } from "wagmi"; import { DISPATCH_PRECOMPILE_ADDRESS } from "@/config"; -/** - * Supported wallets: MetaMask, etc. - */ - -export class EvmBridge extends SubstrateBridge { +export class UniversalBridge extends SubstrateBridge { private readonly publicClient: PublicClient; private readonly walletClient: WalletClient | null | undefined; @@ -28,25 +24,21 @@ export class EvmBridge extends SubstrateBridge { this.walletClient = args.walletClient; } - async transfer(): Promise { - return; - } - - async transferAssetsWithPrecompile(sender: string, recipient: string, amount: BN) { - const extrinsic = await this.transferAssets(recipient, amount); - const account = sender as Address; + async transferWithPrecompile(recipient: string, amount: BN) { + const extrinsic = await this.transfer(recipient, amount); + if (extrinsic && this.walletClient) { + const sender = (await this.walletClient.getAddresses())[0]; - // const estimateGas = await this.publicClient.estimateGas({ - // account, - // to: DISPATCH_PRECOMPILE_ADDRESS, - // data: u8aToHex(extrinsic.method.toU8a()), - // }); - // const { maxFeePerGas } = await this.publicClient.estimateFeesPerGas(); - // const estimateGasFee = estimateGas * (maxFeePerGas || 0n); + // const estimateGas = await this.publicClient.estimateGas({ + // account: sender, + // to: DISPATCH_PRECOMPILE_ADDRESS, + // data: u8aToHex(extrinsic.method.toU8a()), + // }); + // const { maxFeePerGas } = await this.publicClient.estimateFeesPerGas(); + // const estimateGasFee = estimateGas * (maxFeePerGas || 0n); - if (this.walletClient) { const hash = await this.walletClient.sendTransaction({ - account, + account: sender, to: DISPATCH_PRECOMPILE_ADDRESS, data: u8aToHex(extrinsic.method.toU8a()), }); diff --git a/src/providers/rainbow-provider.tsx b/src/providers/rainbow-provider.tsx index 641d0c1..ee26434 100644 --- a/src/providers/rainbow-provider.tsx +++ b/src/providers/rainbow-provider.tsx @@ -2,19 +2,25 @@ import "@rainbow-me/rainbowkit/styles.css"; -import { darkTheme, getDefaultWallets, RainbowKitProvider } from "@rainbow-me/rainbowkit"; +import { connectorsForWallets, darkTheme, getDefaultWallets, RainbowKitProvider } from "@rainbow-me/rainbowkit"; import { configureChains, createConfig, WagmiConfig } from "wagmi"; import { publicProvider } from "wagmi/providers/public"; import { PropsWithChildren } from "react"; import { darwiniaChain } from "@/config/chains"; import { APP_NAME } from "@/config"; +import { safeWallet } from "@rainbow-me/rainbowkit/wallets"; const projectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_ID || ""; const appName = APP_NAME; -const { chains, publicClient } = configureChains([darwiniaChain], [publicProvider()]); +const { chains, publicClient } = configureChains( + [darwiniaChain].map(({ assets, existential, ...chain }) => chain), + [publicProvider()], +); -const { connectors } = getDefaultWallets({ appName, projectId, chains }); +const { wallets } = getDefaultWallets({ appName, projectId, chains }); + +const connectors = connectorsForWallets([...wallets, { groupName: "More", wallets: [safeWallet({ chains })] }]); const wagmiConfig = createConfig({ autoConnect: true, diff --git a/src/providers/transfer-provider.tsx b/src/providers/transfer-provider.tsx index 510f5f0..a87cb94 100644 --- a/src/providers/transfer-provider.tsx +++ b/src/providers/transfer-provider.tsx @@ -2,24 +2,34 @@ import { Dispatch, PropsWithChildren, SetStateAction, createContext, useCallback, useMemo, useState } from "react"; import { BN, BN_ZERO } from "@polkadot/util"; -import type { PalletAssetsAssetDetails } from "@polkadot/types/lookup"; -import { Asset, ChainConfig, WalletID } from "@/types"; +import { Asset, ChainConfig, Currency, WalletID } from "@/types"; import { usePublicClient, useWalletClient } from "wagmi"; -import { EvmBridge } from "@/libs"; +import { UniversalBridge } from "@/libs"; import { WalletAccount } from "@talismn/connect-wallets"; import { Signer } from "@polkadot/api/types"; import { notifyError, notifyTransaction, parseCross, signAndSendExtrinsic } from "@/utils"; -import { useApi, useAssetDetails, useAssetLimit, useBalance } from "@/hooks"; +import { + useApi, + useAssetSupply, + useAssetLimit, + useAssetBalance, + useFeeBalance, + useExistentialDeposit, + useNativeBalance, +} from "@/hooks"; import { ApiPromise } from "@polkadot/api"; interface TransferCtx { - assetLimit: BN | undefined; - targetAssetDetails: PalletAssetsAssetDetails | undefined; - bridgeInstance: EvmBridge | undefined; - usdtBalance: { asset: { value: BN; asset: Asset } } | undefined; - sourceBalance: { asset: { value: BN; asset: Asset } } | undefined; - targetBalance: { asset: { value: BN; asset: Asset } } | undefined; + assetLimitOnTargetChain: { currency: Currency; amount: BN } | undefined; + existentialDepositOnTargetChain: { currency: Currency; amount: BN } | undefined; + targetAssetSupply: { currency: Currency; amount: BN } | undefined; + bridgeInstance: UniversalBridge | undefined; + sourceAssetBalance: { currency: Currency; amount: BN } | undefined; + targetAssetBalance: { currency: Currency; amount: BN } | undefined; + sourceNativeBalance: { currency: Currency; amount: BN } | undefined; + targetNativeBalance: { currency: Currency; amount: BN } | undefined; + feeBalanceOnSourceChain: { currency: Currency; amount: BN } | undefined; transferAmount: { valid: boolean; input: string; amount: BN }; sourceChain: ChainConfig; targetChain: ChainConfig; @@ -45,85 +55,42 @@ interface TransferCtx { setActiveRecipientAccount: Dispatch>; setActiveSenderWallet: Dispatch>; setActiveRecipientWallet: Dispatch>; - evmTransfer: ( - _bridge: EvmBridge, - _sender: string, + transfer: ( + _bridge: UniversalBridge, + _sender: string | WalletAccount, _recipient: string, _amount: BN, options?: { successCb: () => void; failedCb: () => void }, ) => Promise; - substrateTransfer: ( - _bridge: EvmBridge, - _account: WalletAccount, - _recipient: string, - _amount: BN, - options?: { successCb: () => void; failedCb: () => void }, - ) => Promise; - refetchSourceBalance: () => void; - refetchTargetBalance: () => void; - refetchTargetAssetDetails: () => void; + updateSourceAssetBalance: () => void; + updateTargetAssetBalance: () => void; + updateSourceNativeBalance: () => void; + updateTargetNativeBalance: () => void; + updateTargetAssetSupply: () => void; + updateFeeBalanceOnSourceChain: () => void; } const { defaultSourceChain, defaultTargetChain, defaultSourceAsset, defaultTargetAsset } = parseCross(); -const defaultValue: TransferCtx = { - assetLimit: undefined, - targetAssetDetails: undefined, - bridgeInstance: undefined, - usdtBalance: undefined, - sourceBalance: undefined, - targetBalance: undefined, - transferAmount: { valid: true, input: "", amount: BN_ZERO }, - sourceChain: defaultSourceChain, - targetChain: defaultTargetChain, - sourceAsset: defaultSourceAsset, - targetAsset: defaultTargetAsset, - sender: undefined, - recipient: undefined, - activeSenderAccount: undefined, - activeRecipientAccount: undefined, - activeSenderWallet: undefined, - activeRecipientWallet: undefined, - sourceApi: undefined, - targetApi: undefined, - - setTransferAmount: () => undefined, - setSourceChain: () => undefined, - setTargetChain: () => undefined, - setSourceAsset: () => undefined, - setTargetAsset: () => undefined, - setSender: () => undefined, - setRecipient: () => undefined, - setActiveSenderAccount: () => undefined, - setActiveRecipientAccount: () => undefined, - setActiveSenderWallet: () => undefined, - setActiveRecipientWallet: () => undefined, - refetchSourceBalance: () => undefined, - refetchTargetBalance: () => undefined, - refetchTargetAssetDetails: () => undefined, - evmTransfer: async () => undefined, - substrateTransfer: async () => undefined, -}; - const transferCb = { successCb: () => {}, failedCb: () => {}, }; -export const TransferContext = createContext(defaultValue); +export const TransferContext = createContext({} as TransferCtx); export default function TransferProvider({ children }: PropsWithChildren) { - const [transferAmount, setTransferAmount] = useState(defaultValue.transferAmount); - const [sourceChain, setSourceChain] = useState(defaultValue.sourceChain); - const [targetChain, setTargetChain] = useState(defaultValue.targetChain); - const [sourceAsset, setSourceAsset] = useState(defaultValue.sourceAsset); - const [targetAsset, setTargetAsset] = useState(defaultValue.targetAsset); - const [sender, setSender] = useState(defaultValue.sender); - const [recipient, setRecipient] = useState(defaultValue.recipient); - const [activeSenderAccount, setActiveSenderAccount] = useState(defaultValue.activeSenderAccount); - const [activeRecipientAccount, setActiveRecipientAccount] = useState(defaultValue.activeRecipientAccount); - const [activeSenderWallet, setActiveSenderWallet] = useState(defaultValue.activeSenderWallet); - const [activeRecipientWallet, setActiveRecipientWallet] = useState(defaultValue.activeRecipientWallet); + const [transferAmount, setTransferAmount] = useState({ valid: true, input: "", amount: BN_ZERO }); + const [sourceChain, setSourceChain] = useState(defaultSourceChain); + const [targetChain, setTargetChain] = useState(defaultTargetChain); + const [sourceAsset, setSourceAsset] = useState(defaultSourceAsset); + const [targetAsset, setTargetAsset] = useState(defaultTargetAsset); + const [sender, setSender] = useState(); + const [recipient, setRecipient] = useState(); + const [activeSenderAccount, setActiveSenderAccount] = useState(); + const [activeRecipientAccount, setActiveRecipientAccount] = useState(); + const [activeSenderWallet, setActiveSenderWallet] = useState(); + const [activeRecipientWallet, setActiveRecipientWallet] = useState(); const { api: sourceApi } = useApi(sourceChain); const { api: targetApi } = useApi(targetChain); @@ -134,7 +101,7 @@ export default function TransferProvider({ children }: PropsWithChildren sourceApi && targetApi - ? new EvmBridge({ + ? new UniversalBridge({ sourceApi, targetApi, publicClient, @@ -148,19 +115,42 @@ export default function TransferProvider({ children }: PropsWithChildren { + async (_bridge: UniversalBridge, _sender: string, _recipient: string, _amount: BN, options = transferCb) => { try { - const receipt = await _bridge.transferAssetsWithPrecompile(_sender, _recipient, _amount); + const receipt = await _bridge.transferWithPrecompile(_recipient, _amount); notifyTransaction(receipt, _bridge.getSourceChain()); if (receipt?.status === "success") { options.successCb(); @@ -175,18 +165,23 @@ export default function TransferProvider({ children }: PropsWithChildren { + async ( + _bridge: UniversalBridge, + _account: WalletAccount, + _recipient: string, + _amount: BN, + options = transferCb, + ) => { const crossInfo = _bridge.getCrossInfo(); if (crossInfo) { const _sender = _account.address; const _signer = _account.signer as Signer; try { - const _extrinsic = await (crossInfo.isReserve - ? _bridge.limitedReserveTransferAssets(_recipient, _amount) - : _bridge.transferAssets(_recipient, _amount)); - await signAndSendExtrinsic(_extrinsic, _signer, _sender, _bridge.getSourceChain(), options); + const _extrinsic = await _bridge.transfer(_recipient, _amount); + if (_extrinsic) { + await signAndSendExtrinsic(_extrinsic, _signer, _sender, _bridge.getSourceChain(), options); + } } catch (err) { console.error(err); notifyError(err); @@ -196,18 +191,37 @@ export default function TransferProvider({ children }: PropsWithChildren void; failedCb: () => void }, + ) => { + if (typeof _sender === "string") { + return evmTransfer(_bridge, _sender, _recipient, _amount, options); + } else { + return substrateTransfer(_bridge, _sender, _recipient, _amount, options); + } + }, + [evmTransfer, substrateTransfer], + ); return ( {children} diff --git a/src/types/asset.ts b/src/types/asset.ts index 7fd7b64..6fe27d7 100644 --- a/src/types/asset.ts +++ b/src/types/asset.ts @@ -1,12 +1,23 @@ import { Cross } from "./cross"; -export type AssetSymbol = "DOT" | "ROC" | "USDT" | "PRING" | "ahUSDT" | "PINK" | "ahPINK" | "RING"; +export type AssetSymbol = "DOT" | "ROC" | "USDT" | "PRING" | "ahUSDT" | "PINK" | "ahPINK" | "RING" | "HDX"; + +export enum AssetID { + SYSTEM = -1, +} export interface Asset { icon: string; // File name - id: number; + id: number | AssetID.SYSTEM; // The GeneralIndex of assets issued on the Assethub uses this id value name: string; symbol: AssetSymbol; decimals: number; cross: Cross[]; + + // Defines where the asset is issued + origin: { + id: number | AssetID.SYSTEM; + parachainId: number; // Indicates on which chain the token is issued + palletInstance: number; // Use the pallet instance of the chain where the asset is issued + }; } diff --git a/src/types/chain.ts b/src/types/chain.ts index 20503e9..6d627b6 100644 --- a/src/types/chain.ts +++ b/src/types/chain.ts @@ -2,11 +2,13 @@ import { Chain } from "wagmi"; import { Asset } from "./asset"; import { AddressType } from "./misc"; import { WalletID } from "."; +import { BN } from "@polkadot/util"; export enum ChainID { INVALID = -1, PANGOLIN = 43, DARWINIA = 46, + HYDRADX = 222222, } export enum ParachainID { @@ -14,9 +16,10 @@ export enum ParachainID { ASSETHUB_POLKADOT = 1000, PANGOLIN = 2105, DARWINIA = 2046, + HYDRADX = 2034, } -export type Network = "pangolin" | "darwinia" | "assethub-rococo" | "assethub-polkadot"; +export type Network = "pangolin" | "darwinia" | "assethub-rococo" | "assethub-polkadot" | "hydradx"; export interface ChainConfig extends Chain { /** @@ -33,11 +36,13 @@ export interface ChainConfig extends Chain { assets: Asset[]; wallets: WalletID[]; // Supported wallets addressType: AddressType; - hasAssetLimit?: boolean; /** * Substrate */ endpoint: string; parachainId: ParachainID; + existential?: { + minBalance: BN; + }; } diff --git a/src/types/cross.ts b/src/types/cross.ts index 5a26b2e..fe04c4f 100644 --- a/src/types/cross.ts +++ b/src/types/cross.ts @@ -6,8 +6,16 @@ export interface Cross { network: Network; symbol: AssetSymbol; }; - isReserve: boolean; - fee: { amount: BN; asset: { id: number; decimals: number; symbol: AssetSymbol; native: boolean } }; + + // If the fee is paid in RING and the fee is 0.5RING, then if the cross-chain transaction + // is 10RING, the target chain will receive 9.5RING. + fee: { + amount: BN; + asset: { local: { id: number }; origin: { id: number; parachainId: number; palletInstance: number } }; + }; + + section: string; + method: string; } export type AvailableSourceAssetOptions = { diff --git a/src/types/currency.ts b/src/types/currency.ts new file mode 100644 index 0000000..b41e636 --- /dev/null +++ b/src/types/currency.ts @@ -0,0 +1,7 @@ +import { AssetSymbol } from "./asset"; + +export interface Currency { + symbol: AssetSymbol; + name: string; + decimals: number; +} diff --git a/src/types/index.ts b/src/types/index.ts index 238902a..39cc74e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,3 +4,4 @@ export * from "./asset"; export * from "./cross"; export * from "./wallet"; export * from "./misc"; +export * from "./currency"; diff --git a/src/types/wallet.ts b/src/types/wallet.ts index ecb72a2..da9b00a 100644 --- a/src/types/wallet.ts +++ b/src/types/wallet.ts @@ -1,4 +1,4 @@ export enum WalletID { - RAINBOW = 1, + EVM = 1, TALISMAN, } diff --git a/src/ui/tooltip.tsx b/src/ui/tooltip.tsx index 460e027..4974393 100644 --- a/src/ui/tooltip.tsx +++ b/src/ui/tooltip.tsx @@ -69,7 +69,7 @@ export default function Tooltip({
{typeof content === "string" ? {content} : content}
diff --git a/src/utils/chain.ts b/src/utils/chain.ts index b4353cc..70e45c1 100644 --- a/src/utils/chain.ts +++ b/src/utils/chain.ts @@ -1,4 +1,10 @@ -import { assethubPolkadotChain, assethubRococoChain, darwiniaChain, pangolinChain } from "@/config/chains"; +import { + assethubPolkadotChain, + assethubRococoChain, + darwiniaChain, + hydradxChain, + pangolinChain, +} from "@/config/chains"; import { ChainID, Network } from "@/types"; export function getChainConfig(chainIdOrNetwork: ChainID | Network | undefined) { @@ -13,9 +19,12 @@ export function getChainConfig(chainIdOrNetwork: ChainID | Network | undefined) return darwiniaChain; case "assethub-polkadot": return assethubPolkadotChain; + case ChainID.HYDRADX: + case "hydradx": + return hydradxChain; } } export function getChainsConfig() { - return [assethubPolkadotChain, darwiniaChain]; + return [hydradxChain, darwiniaChain, assethubPolkadotChain]; } diff --git a/src/utils/cross.ts b/src/utils/cross.ts index 68226c2..92bf955 100644 --- a/src/utils/cross.ts +++ b/src/utils/cross.ts @@ -1,14 +1,14 @@ -import { assethubPolkadotChain, darwiniaChain } from "@/config/chains"; +import { darwiniaChain, hydradxChain } from "@/config/chains"; import { AvailableSourceAssetOptions, AvailableTargetAssetOptions, AvailableTargetChainOptions } from "@/types"; import { getChainConfig, getChainsConfig } from "."; -let defaultSourceChain = assethubPolkadotChain; -let defaultTargetChain = darwiniaChain; +let defaultSourceChain = darwiniaChain; +let defaultTargetChain = hydradxChain; let defaultSourceAsset = defaultSourceChain.assets[0]; let defaultTargetAsset = defaultTargetChain.assets[0]; -let defaultSourceChainOptions = [defaultSourceChain, defaultTargetChain]; +let defaultSourceChainOptions = getChainsConfig(); let defaultTargetChainOptions = [defaultTargetChain]; let defaultSourceAssetOptions = [defaultSourceAsset]; diff --git a/src/utils/misc.ts b/src/utils/misc.ts index ac8537c..92de9d7 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -1,5 +1,6 @@ -import { EvmBridge } from "@/libs"; -import { BN_ZERO } from "@polkadot/util"; +import { BaseBridge } from "@/libs"; +import { bnToBn } from "@polkadot/util"; +import { parseUnits } from "viem"; export function getChainLogoSrc(fileName: string) { return `/images/network/${fileName}`; @@ -9,11 +10,10 @@ export function getAssetIconSrc(fileName: string) { return `/images/asset/${fileName}`; } -export async function isAssetExcess(bridge: EvmBridge, amount = BN_ZERO) { - const limit = await bridge.getAssetLimit(); - if (limit) { - const details = await bridge.getTargetAssetDetails(); - return (details?.supply ?? BN_ZERO).add(amount).gt(limit); - } - return false; +export async function isExceedingCrossChainLimit(bridge: BaseBridge, acrossAmount = "0") { + const { amount: limit, currency } = await bridge.getAssetLimitOnTargetChain(); + const { amount: supply } = await bridge.getTargetAssetSupply(); + + const amount = bnToBn(parseUnits(acrossAmount, currency.decimals)); // We use the decimals on the target chain + return supply.add(amount).gt(limit); }