diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..e073dab --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,31 @@ +name: Deploy development + +on: + pull_request: + workflow_dispatch: + +jobs: + deploy-package: + name: Deploy package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions/checkout@v2 + with: + repository: darwinia-network/devops + path: .github + + - uses: ./.github/actions/smart-vercel + name: Deploy app + with: + vercel_token: ${{ secrets.VERCEL_TOKEN }} + vercel_group: itering + preview_output: true + alias_domain: "crosschain-dev" + project_name: "crosschain-ui" + script_run: false + dist_path: . + enable_notify_slack: true + slack_channel: darwinia-apps + slack_webhook: ${{ secrets.SLACK_INCOMING_WEBHOOK_URL }} diff --git a/public/images/asset/pink.jpg b/public/images/asset/pink.jpg new file mode 100644 index 0000000..14000f7 Binary files /dev/null and b/public/images/asset/pink.jpg differ diff --git a/public/images/network/darwinia.png b/public/images/network/darwinia.png new file mode 100644 index 0000000..874eb29 Binary files /dev/null and b/public/images/network/darwinia.png differ diff --git a/public/images/warning.svg b/public/images/warning.svg new file mode 100644 index 0000000..6047dbf --- /dev/null +++ b/public/images/warning.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index 26e1882..e77b20f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -28,7 +28,7 @@ body { @layer components { .border-radius { - @apply rounded-lg; + @apply rounded-2xl; } .app-header { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a1817de..fe3c319 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -10,8 +10,8 @@ import TransferProvider from "@/providers/transfer-provider"; const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Darwinia Cross-Chain", - description: "Darwinia USDT Cross-Chain", + title: "Asset Hub Bridge - Darwinia", + description: "Assets cross-chain between Darwinia and Asset Hub.", }; export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/src/components/address-input.tsx b/src/components/address-input.tsx index 58b6223..d897796 100644 --- a/src/components/address-input.tsx +++ b/src/components/address-input.tsx @@ -71,7 +71,7 @@ export default function AddressInput({ clickable={!!options?.length || !canInput} canInput={canInput} innerSuffix={} - wrapClassName={`h-10 bg-transparent flex items-center justify-between p-1 border border-radius transition-colors duration-200 ${ + wrapClassName={`h-12 bg-transparent flex items-center justify-between p-1 border border-radius transition-colors duration-200 ${ value?.valid === false ? "border-alert" : "border-transparent" }`} inputClassName="w-full border-radius h-full bg-transparent px-1" @@ -100,8 +100,8 @@ export default function AddressInput({ )) ) : ( -
- No data +
+ No data
)} diff --git a/src/components/asset-select.tsx b/src/components/asset-select.tsx index 07b118a..ecd742e 100644 --- a/src/components/asset-select.tsx +++ b/src/components/asset-select.tsx @@ -14,30 +14,31 @@ export default function AssetSelect({ value, options, disabled, onChange = () => return ( diff --git a/src/components/balance-input.tsx b/src/components/balance-input.tsx index f2e4a0b..4adc5ff 100644 --- a/src/components/balance-input.tsx +++ b/src/components/balance-input.tsx @@ -1,4 +1,4 @@ -import { Asset } from "@/types"; +import { Asset, Cross } from "@/types"; import Input from "@/ui/input"; import { BN, BN_ZERO, bnToBn } from "@polkadot/util"; import AssetSelect from "./asset-select"; @@ -18,7 +18,7 @@ interface Props { disabled?: boolean; placeholder?: string; balance?: BN; - min?: BN; + cross?: Cross; asset?: Asset; assetSupply?: BN; assetLimit?: BN; @@ -32,7 +32,7 @@ export default function BalanceInput({ disabled, placeholder, balance, - min, + cross, asset, assetSupply, assetLimit, @@ -52,6 +52,13 @@ export default function BalanceInput({ return placeholder ?? "Enter an amount"; }, [balance, asset, placeholder]); + const min = useMemo(() => { + if (cross && cross.fee.asset.native) { + return cross.fee.amount; + } + return undefined; + }, [cross]); + const handleInputChange = useCallback>( (e) => { if (e.target.value) { @@ -77,9 +84,7 @@ export default function BalanceInput({ const inputWidth = inputRef.current?.clientWidth || 1; const spanWidth = spanRef.current?.clientWidth || 0; const percent = (spanWidth / inputWidth) * 100; - if (percent < 10) { - setDynamicStyle("text-[3rem] font-extralight"); - } else if (percent < 20) { + if (percent < 20) { setDynamicStyle("text-[2.25rem] font-light"); } else if (percent < 30) { setDynamicStyle("text-[1.875rem] font-light"); diff --git a/src/components/chain-select.tsx b/src/components/chain-select.tsx index a601da7..1774708 100644 --- a/src/components/chain-select.tsx +++ b/src/components/chain-select.tsx @@ -15,30 +15,31 @@ export default function ChainSelect({ value, options, disabled, onChange = () => diff --git a/src/components/transfer.tsx b/src/components/transfer.tsx index 48d7ea5..9d777b1 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 { isAssetExcess, parseCross } from "@/utils"; +import { formatBalance, isAssetExcess, parseCross } from "@/utils"; import { useTalisman, useTransfer } from "@/hooks"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import SwitchCross from "./switch-cross"; @@ -13,6 +13,7 @@ import { useAccount, useNetwork, useSwitchNetwork } from "wagmi"; import { Asset, ChainConfig, WalletID } from "@/types"; import { BN_ZERO } from "@polkadot/util"; import notification from "@/ui/notification"; +import Image from "next/image"; const { defaultSourceChainOptions, @@ -33,6 +34,7 @@ export default function Transfer() { targetChain, sourceAsset, targetAsset, + usdtBalance, sourceBalance, targetBalance, transferAmount, @@ -76,6 +78,23 @@ export default function Transfer() { [chain, sourceChain, activeSenderWallet], ); + const alert = useMemo(() => { + const fee = bridgeInstance?.getCrossInfo()?.fee; + const balance = usdtBalance?.asset.value; + + if (fee && balance && fee.amount.gt(balance)) { + return ( +
+ Warning + {`You need at least ${formatBalance(fee.amount, fee.asset.decimals)} ${ + fee.asset.symbol + } in your account to cover cross-chain fees.`} +
+ ); + } + return null; + }, [bridgeInstance, usdtBalance?.asset.value]); + const sourceChainRef = useRef(sourceChain); const targetChainRef = useRef(targetChain); const sourceAssetRef = useRef(sourceAsset); @@ -219,7 +238,7 @@ export default function Transfer() { : []; return ( -
+
{/* From */} {/* Send */} - + + {alert}
); } diff --git a/src/config/chains/assethub-polkadot-chain.ts b/src/config/chains/assethub-polkadot-chain.ts new file mode 100644 index 0000000..ece7e54 --- /dev/null +++ b/src/config/chains/assethub-polkadot-chain.ts @@ -0,0 +1,75 @@ +import { ChainConfig, ChainID, ParachainID, WalletID } from "@/types"; +import { bnToBn } from "@polkadot/util"; + +export const assethubPolkadotChain: ChainConfig = { + /** + * Chain + */ + id: ChainID.INVALID, + network: "assethub-polkadot", + name: "Polkadot AssetHub", + nativeCurrency: { + name: "DOT", + symbol: "DOT", + decimals: 10, + }, + rpcUrls: { + default: { + http: [], + webSocket: [], + }, + public: { + http: [], + webSocket: [], + }, + }, + blockExplorers: { + default: { + name: "Subscan", + url: "https://assethub-polkadot.subscan.io", + }, + }, + + /** + * Custom + */ + logo: "asset-hub.svg", + assets: [ + { + icon: "usdt.svg", + id: 1984, + name: "Tether USD", + symbol: "USDT", + decimals: 6, + cross: [ + { + isReserve: true, + target: { network: "darwinia", symbol: "ahUSDT" }, + fee: { amount: bnToBn(20000), asset: { id: 1984, decimals: 6, symbol: "USDT", native: true } }, // 0.02 USDT + }, + ], + }, + { + icon: "pink.jpg", + id: 23, + name: "PINK", + symbol: "PINK", + decimals: 10, + cross: [ + { + isReserve: true, + target: { network: "darwinia", symbol: "ahPINK" }, + fee: { amount: bnToBn(20000), asset: { id: 1984, decimals: 6, symbol: "USDT", native: false } }, // 0.02 USDT + }, + ], + }, + ], + wallets: [WalletID.TALISMAN], + addressType: "substrate", + + /** + * Substrate + */ + endpoint: "wss://polkadot-asset-hub-rpc.polkadot.io", + parachainId: ParachainID.ASSETHUB_POLKADOT, +}; diff --git a/src/config/chains/assethub-rococo-chain.ts b/src/config/chains/assethub-rococo-chain.ts index 8eec5d3..2bdd097 100644 --- a/src/config/chains/assethub-rococo-chain.ts +++ b/src/config/chains/assethub-rococo-chain.ts @@ -1,4 +1,4 @@ -import { ChainConfig, ChainID, WalletID } from "@/types"; +import { ChainConfig, ChainID, ParachainID, WalletID } from "@/types"; import { bnToBn } from "@polkadot/util"; export const assethubRococoChain: ChainConfig = { @@ -42,16 +42,21 @@ export const assethubRococoChain: ChainConfig = { name: "Tether USD Test", symbol: "USDT", decimals: 6, - cross: [{ target: { network: "pangolin", symbol: "ahUSDT" }, isReserve: true }], + cross: [ + { + isReserve: true, + target: { network: "pangolin", symbol: "ahUSDT" }, + fee: { amount: bnToBn(125000), asset: { id: 7777, decimals: 6, symbol: "USDT", native: true } }, // 0.125 USDT + }, + ], }, ], wallets: [WalletID.TALISMAN], addressType: "substrate", - minCross: bnToBn(125000), // 0.125 USDT /** * Substrate */ endpoint: "wss://rococo-asset-hub-rpc.polkadot.io", - parachainId: 1000, + parachainId: ParachainID.ASSETHUB_ROCOCO, }; diff --git a/src/config/chains/darwinia-chain.ts b/src/config/chains/darwinia-chain.ts new file mode 100644 index 0000000..d140aee --- /dev/null +++ b/src/config/chains/darwinia-chain.ts @@ -0,0 +1,75 @@ +import { ChainConfig, ChainID, ParachainID, WalletID } from "@/types"; +import { bnToBn } from "@polkadot/util"; + +export const darwiniaChain: ChainConfig = { + /** + * Chain + */ + id: ChainID.DARWINIA, + network: "darwinia", + name: "Darwinia", + nativeCurrency: { + name: "RING", + symbol: "RING", + decimals: 18, + }, + rpcUrls: { + default: { + http: ["https://rpc.darwinia.network"], + webSocket: ["wss://rpc.darwinia.network"], + }, + public: { + http: ["https://rpc.darwinia.network"], + webSocket: ["wss://rpc.darwinia.network"], + }, + }, + blockExplorers: { + default: { + name: "Subscan", + url: "https://darwinia.subscan.io", + }, + }, + + /** + * Custom + */ + logo: "darwinia.png", + assets: [ + { + icon: "usdt.svg", + id: 1027, + name: "Tether USD", + symbol: "ahUSDT", + decimals: 6, + 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 + }, + ], + }, + { + icon: "pink.jpg", + id: 1028, + name: "PINK", + symbol: "ahPINK", + decimals: 10, + 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 + }, + ], + }, + ], + wallets: [WalletID.RAINBOW, WalletID.TALISMAN], + addressType: "evm", + + /** + * Substrate + */ + endpoint: "wss://rpc.darwinia.network", + parachainId: ParachainID.DARWINIA, +}; diff --git a/src/config/chains/index.ts b/src/config/chains/index.ts index e154efc..fa4b4d7 100644 --- a/src/config/chains/index.ts +++ b/src/config/chains/index.ts @@ -1,2 +1,4 @@ export * from "./pangolin-chain"; +export * from "./darwinia-chain"; export * from "./assethub-rococo-chain"; +export * from "./assethub-polkadot-chain"; diff --git a/src/config/chains/pangolin-chain.ts b/src/config/chains/pangolin-chain.ts index 4c67104..e010311 100644 --- a/src/config/chains/pangolin-chain.ts +++ b/src/config/chains/pangolin-chain.ts @@ -1,4 +1,4 @@ -import { ChainConfig, ChainID, WalletID } from "@/types"; +import { ChainConfig, ChainID, ParachainID, WalletID } from "@/types"; import { bnToBn } from "@polkadot/util"; export const pangolinChain: ChainConfig = { @@ -44,20 +44,20 @@ export const pangolinChain: ChainConfig = { decimals: 6, cross: [ { - target: { network: "assethub-rococo", symbol: "USDT" }, isReserve: false, + target: { network: "assethub-rococo", symbol: "USDT" }, + fee: { amount: bnToBn(3600000), asset: { id: 7777, decimals: 6, symbol: "ahUSDT", native: true } }, // 3.6 USDT }, ], }, ], wallets: [WalletID.RAINBOW, WalletID.TALISMAN], addressType: "evm", - minCross: bnToBn(3600000), // 3.6 USDT hasAssetLimit: true, /** * Substrate */ endpoint: "wss://pangolin-rpc.darwinia.network", - parachainId: 2105, + parachainId: ParachainID.PANGOLIN, }; diff --git a/src/hooks/use-balance.ts b/src/hooks/use-balance.ts index 6e87a82..d980a3d 100644 --- a/src/hooks/use-balance.ts +++ b/src/hooks/use-balance.ts @@ -7,19 +7,21 @@ import { forkJoin, EMPTY } from "rxjs"; export function useBalance( bridge: EvmBridge | undefined, value: { address: string; valid: boolean } | undefined, - position: "source" | "target", + type: "source" | "target" | "usdt", ) { const [balance, setBalance] = useState<{ asset: { value: BN; asset: Asset } }>(); const updateBalance = useCallback(() => { if (bridge && value?.address && value.valid) { return forkJoin([ - position === "source" + type === "usdt" + ? bridge.getSourceUsdtBalance(value.address) + : type === "source" ? bridge.getSourceAssetBalance(value.address) : bridge.getTargetAssetBalance(value.address), ]).subscribe({ next: ([asset]) => { - setBalance({ asset }); + setBalance(asset ? { asset } : undefined); }, error: (err) => { console.error(err); @@ -31,7 +33,7 @@ export function useBalance( } return EMPTY.subscribe(); - }, [bridge, value, position]); + }, [bridge, value, type]); useEffect(() => { const sub$$ = updateBalance(); diff --git a/src/libs/bridge/base.ts b/src/libs/bridge/base.ts index 9d0f78b..7aaf2d1 100644 --- a/src/libs/bridge/base.ts +++ b/src/libs/bridge/base.ts @@ -96,6 +96,14 @@ export abstract class BaseBridge { return { value, asset }; } + async getSourceUsdtBalance(address: string) { + const asset = this.sourceChain.assets.find(({ symbol }) => symbol.toLowerCase().includes("usdt")); + if (asset) { + const value = await this.getAssetBalance(this.sourceApi, asset, address); + return { value, asset }; + } + } + /** * Supply */ diff --git a/src/libs/bridge/evm.ts b/src/libs/bridge/evm.ts index 1b3a9d9..f7f869b 100644 --- a/src/libs/bridge/evm.ts +++ b/src/libs/bridge/evm.ts @@ -32,8 +32,8 @@ export class EvmBridge extends SubstrateBridge { return; } - async transferAssetWithPrecompile(sender: string, recipient: string, amount: BN) { - const extrinsic = await this.transferAsset(recipient, amount); + async transferAssetsWithPrecompile(sender: string, recipient: string, amount: BN) { + const extrinsic = await this.transferAssets(recipient, amount); const account = sender as Address; // const estimateGas = await this.publicClient.estimateGas({ diff --git a/src/libs/bridge/substrate.ts b/src/libs/bridge/substrate.ts index d0b93d7..90535ee 100644 --- a/src/libs/bridge/substrate.ts +++ b/src/libs/bridge/substrate.ts @@ -1,5 +1,5 @@ import { BaseBridge } from "./base"; -import { BN, BN_ZERO, bnToBn, u8aToHex } from "@polkadot/util"; +import { BN, bnToBn, u8aToHex } from "@polkadot/util"; import { Asset, ChainConfig } from "@/types"; import { ApiPromise } from "@polkadot/api"; import { decodeAddress } from "@polkadot/util-crypto"; @@ -25,20 +25,19 @@ export class SubstrateBridge extends BaseBridge { } /** - * From Pangolin to Asset-Hub + * To Asset-Hub * @param recipient Address * @param amount Transfer amount * @returns Promise> */ - async transferAsset(recipient: string, amount: BN) { + async transferAssets(recipient: string, amount: BN) { const section = "xTokens"; - const method = "transferMultiasset"; + const method = "transferMultiassets"; const fn = this.sourceApi.tx[section][method]; const Parachain = bnToBn(this.targetChain.parachainId); - - const _asset = { - V3: { + const assetItems = [ + { id: { Concrete: { parents: 1, @@ -49,7 +48,23 @@ export class SubstrateBridge extends BaseBridge { }, fun: { Fungible: amount }, }, - }; + ]; + if (this.cross && !this.cross.fee.asset.native) { + assetItems.push({ + id: { + Concrete: { + parents: 1, + interior: { + X3: [{ Parachain }, { PalletInstance: 50 }, { GeneralIndex: bnToBn(this.cross.fee.asset.id) }], + }, + }, + }, + fun: { Fungible: this.cross.fee.amount }, + }); + } + + const _assets = { V3: assetItems }; + const _feeAssetItem = bnToBn(assetItems.length - 1); const _dest = { V3: { parents: 1, @@ -60,39 +75,49 @@ export class SubstrateBridge extends BaseBridge { }; const _destWeightLimit = { Unlimited: null }; - const extrinsic = fn(_asset, _dest, _destWeightLimit); + const extrinsic = fn(_assets, _feeAssetItem, _dest, _destWeightLimit); return extrinsic; } /** - * From Asset-Hub to Pangolin + * From Asset-Hub * @param recipient Address * @param amount Transfer amount * @returns Promise> */ - async limitedReserveTransferAsset(recipient: string, amount: BN) { + async limitedReserveTransferAssets(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) }] }, + }, + }, + fun: { Fungible: amount }, + }, + ]; + if (this.cross && !this.cross.fee.asset.native) { + assetItems.push({ + id: { + Concrete: { + parents: 0, + interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: bnToBn(this.cross.fee.asset.id) }] }, + }, + }, + fun: { Fungible: this.cross.fee.amount }, + }); + } const _dest = { V3: { parents: 1, interior: { X1: { Parachain } } } }; const _beneficiary = { V3: { parents: 0, interior: { X1: { AccountKey20: { network: null, key: recipient } } } } }; - const _assets = { - V3: [ - { - id: { - Concrete: { - parents: 0, - interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: bnToBn(this.sourceAsset.id) }] }, - }, - }, - fun: { Fungible: amount }, - }, - ], - }; - const _feeAssetItem = BN_ZERO; + const _assets = { V3: assetItems }; + const _feeAssetItem = bnToBn(assetItems.length - 1); const _weightLimit = { Unlimited: null }; const extrinsic = fn(_dest, _beneficiary, _assets, _feeAssetItem, _weightLimit); diff --git a/src/providers/rainbow-provider.tsx b/src/providers/rainbow-provider.tsx index baa4beb..641d0c1 100644 --- a/src/providers/rainbow-provider.tsx +++ b/src/providers/rainbow-provider.tsx @@ -6,13 +6,13 @@ import { darkTheme, getDefaultWallets, RainbowKitProvider } from "@rainbow-me/ra import { configureChains, createConfig, WagmiConfig } from "wagmi"; import { publicProvider } from "wagmi/providers/public"; import { PropsWithChildren } from "react"; -import { pangolinChain } from "@/config/chains"; +import { darwiniaChain } from "@/config/chains"; import { APP_NAME } from "@/config"; const projectId = process.env.NEXT_PUBLIC_WALLET_CONNECT_ID || ""; const appName = APP_NAME; -const { chains, publicClient } = configureChains([pangolinChain], [publicProvider()]); +const { chains, publicClient } = configureChains([darwiniaChain], [publicProvider()]); const { connectors } = getDefaultWallets({ appName, projectId, chains }); diff --git a/src/providers/transfer-provider.tsx b/src/providers/transfer-provider.tsx index 72fc060..0089fba 100644 --- a/src/providers/transfer-provider.tsx +++ b/src/providers/transfer-provider.tsx @@ -16,6 +16,7 @@ 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; transferAmount: { valid: boolean; input: string; amount: BN }; @@ -66,6 +67,7 @@ const defaultValue: TransferCtx = { assetLimit: undefined, targetAssetDetails: undefined, bridgeInstance: undefined, + usdtBalance: undefined, sourceBalance: undefined, targetBalance: undefined, transferAmount: { valid: true, input: "", amount: BN_ZERO }, @@ -142,6 +144,7 @@ export default function TransferProvider({ children }: PropsWithChildren { try { - const receipt = await _bridge.transferAssetWithPrecompile(_sender, _recipient, _amount); + const receipt = await _bridge.transferAssetsWithPrecompile(_sender, _recipient, _amount); notifyTransaction(receipt, _bridge.getSourceChain()); if (receipt?.status === "success") { options.successCb(); @@ -176,8 +179,8 @@ export default function TransferProvider({ children }: PropsWithChildren