- {
- setActiveWallet(WalletID.RAINBOW);
+ setActiveWallet(WalletID.EVM);
openConnectModal?.();
setIsOpenFalse();
}}
- disabled={!supportedRainbow}
+ disabled={!supportedWalletEvm}
/>
@@ -195,7 +197,7 @@ function Item({
disabled={disabled}
onClick={onClick}
>
-
+
{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 (
- {`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 (
+
+
+ {`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 (
+
+
+ {`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 */}
-
+
{needSwitchNetwork ? "Switch network" : "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);
}