Skip to content

Commit

Permalink
feat: Add support for Snowbridge transfers (Ethereum -> Polkadot) ❄️
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeldev5 authored and dudo50 committed Jul 15, 2024
1 parent 631cd55 commit 2f16524
Show file tree
Hide file tree
Showing 14 changed files with 874 additions and 58 deletions.
3 changes: 2 additions & 1 deletion apps/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@
"@polkadot/util": "^12.6.2",
"@tabler/icons-react": "^3.5.0",
"axios": "^1.7.2",
"ethers": "^6.13.0",
"ethers": "^6.13.1",
"react": "^18.3.1",
"react-confetti": "^6.1.0",
"react-dom": "^18.3.1",
"react-router-dom": "^6.23.1",
"web3-validator": "^2.0.6"
},
"devDependencies": {
"@metamask/providers": "^17.1.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
Expand Down
153 changes: 153 additions & 0 deletions apps/playground/src/components/eth-bridge/EthBridgeTransfer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */

import { Stack, Title, Box, Button } from "@mantine/core";
import { useDisclosure, useScrollIntoView } from "@mantine/hooks";
import { useState, useEffect } from "react";
import { BrowserProvider, ethers } from "ethers";
import ErrorAlert from "../ErrorAlert";
import EthBridgeTransferForm, { FormValues } from "./EthBridgeTransferForm";
import { EvmBuilder } from "@paraspell/sdk";

const EthBridgeTransfer = () => {
const [selectedAccount, setSelectedAccount] = useState<string | null>(null);
const [provider, setProvider] = useState<BrowserProvider | null>(null);

const [alertOpened, { open: openAlert, close: closeAlert }] =
useDisclosure(false);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(false);

const { scrollIntoView, targetRef } = useScrollIntoView<HTMLDivElement>({
offset: 0,
});

useEffect(() => {
const handleAccountsChanged = (accounts: string[]) => {
if (accounts.length === 0) {
console.log("Please connect to MetaMask.");
setSelectedAccount(null);
} else {
setSelectedAccount(accounts[0]);
}
};

if (window.ethereum) {
window.ethereum.on(
"accountsChanged",
handleAccountsChanged as (...args: unknown[]) => void
);
}

return () => {
if (window.ethereum) {
window.ethereum.removeListener(
"accountsChanged",
handleAccountsChanged
);
}
};
}, []);

const connectWallet = async () => {
if (!window.ethereum) {
alert("Please install MetaMask!");
return;
}

const tempProvider = new ethers.BrowserProvider(window.ethereum);
setProvider(tempProvider);
try {
await tempProvider.send("eth_requestAccounts", []);
const tempSigner = await tempProvider.getSigner();
const account = await tempSigner.getAddress();
setSelectedAccount(account);
console.log("Wallet connected:", account);
} catch (error) {
console.error("Error connecting to MetaMask:", error);
}
};

useEffect(() => {
if (error) {
scrollIntoView();
}
}, [error, scrollIntoView]);

const submitEthTransaction = async ({
to,
amount,
currency,
address,
}: FormValues) => {
if (!provider) {
throw new Error("Provider not initialized");
}

const signer = await provider.getSigner();

if (!signer) {
throw new Error("Signer not initialized");
}

await EvmBuilder(provider)
.to(to)
.amount(amount)
.currency(currency)
.address(address)
.signer(signer)
.build();
};

const onSubmit = async (formValues: FormValues) => {
if (!selectedAccount) {
alert("No account selected, connect wallet first");
throw new Error("No account selected!");
}

setLoading(true);

try {
await submitEthTransaction(formValues);
alert("Transaction was successful!");
} catch (e) {
if (e instanceof Error) {
console.error(e);
setError(e);
openAlert();
}
} finally {
setLoading(false);
}
};

const onAlertCloseClick = () => {
closeAlert();
};

return (
<Stack gap="xl">
<Stack w="100%" maw={400} mx="auto" gap="lg">
<Title order={3}>Ethereum Bridge Transfer</Title>
<Button size="xs" variant="outline" onClick={connectWallet}>
{selectedAccount
? `Connected: ${selectedAccount.substring(0, 6)}...${selectedAccount.substring(selectedAccount.length - 4)}`
: "Connect Ethereum Wallet"}
</Button>
<EthBridgeTransferForm onSubmit={onSubmit} loading={loading} />
</Stack>
<Box ref={targetRef}>
{alertOpened && (
<ErrorAlert onAlertCloseClick={onAlertCloseClick}>
{error?.message
.split("\n\n")
.map((line, index) => <p key={index}>{line}</p>)}{" "}
</ErrorAlert>
)}
</Box>
</Stack>
);
};

export default EthBridgeTransfer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useForm } from "@mantine/form";
import { FC } from "react";
import { Button, Select, Stack, TextInput } from "@mantine/core";
import { NODES_WITH_RELAY_CHAINS, TNodePolkadotKusama } from "@paraspell/sdk";
import { isValidPolkadotAddress } from "../../utils";

export type FormValues = {
to: TNodePolkadotKusama;
currency: string;
address: string;
amount: string;
};

type Props = {
onSubmit: (values: FormValues) => void;
loading: boolean;
};

const EthBridgeTransferForm: FC<Props> = ({ onSubmit, loading }) => {
const form = useForm<FormValues>({
initialValues: {
to: "AssetHubPolkadot",
currency: "WETH",
amount: "1000000000",
address: "5F5586mfsnM6durWRLptYt3jSUs55KEmahdodQ5tQMr9iY96",
},

validate: {
address: (value) =>
isValidPolkadotAddress(value) ? null : "Invalid address",
},
});

return (
<form onSubmit={form.onSubmit(onSubmit)}>
<Stack>
<Select
label="From"
placeholder="Pick value"
data={[...NODES_WITH_RELAY_CHAINS]}
searchable
disabled
value="Ethereum"
/>

<Select
label="To"
placeholder="Pick value"
data={[...NODES_WITH_RELAY_CHAINS]}
searchable
required
{...form.getInputProps("to")}
/>

<TextInput
label="Currency"
placeholder="WETH"
required
{...form.getInputProps("currency")}
/>

<TextInput
label="Recipient address"
placeholder="0x0000000"
required
{...form.getInputProps("address")}
/>

<TextInput
label="Amount"
placeholder="0"
required
{...form.getInputProps("amount")}
/>

<Button type="submit" loading={loading}>
Submit transaction
</Button>
</Stack>
</form>
);
};

export default EthBridgeTransferForm;
11 changes: 11 additions & 0 deletions apps/playground/src/routes/XcmSdkSandbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import PalletsQueries from "../components/pallets/PalletsQueries";
import ChannelsQueries from "../components/channels/ChannelsQueries";
import TransferInfo from "../components/TransferInfo";
import AssetClaim from "../components/asset-claim/AssetClaim";
import EthBridgeTransfer from "../components/eth-bridge/EthBridgeTransfer";

const XcmSdkSandbox = () => {
const iconStyle = { width: rem(12), height: rem(12) };
Expand Down Expand Up @@ -55,6 +56,12 @@ const XcmSdkSandbox = () => {
>
Asset Claim
</Tabs.Tab>
<Tabs.Tab
value="eth-bridge"
leftSection={<IconWallet style={iconStyle} />}
>
ETH Bridge
</Tabs.Tab>
</Tabs.List>

<Container p="xl">
Expand All @@ -81,6 +88,10 @@ const XcmSdkSandbox = () => {
<Tabs.Panel value="asset-claim">
<AssetClaim />
</Tabs.Panel>

<Tabs.Panel value="eth-bridge">
<EthBridgeTransfer />
</Tabs.Panel>
</Container>
</Tabs>
);
Expand Down
8 changes: 8 additions & 0 deletions apps/playground/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
/// <reference types="vite/client" />

import { MetaMaskInpageProvider } from "@metamask/providers";

declare global {
interface Window {
ethereum?: MetaMaskInpageProvider;
}
}
3 changes: 2 additions & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
"test:e2e": "vitest run --config ./vitest.config.e2e.ts --sequence.concurrent"
},
"dependencies": {
"ethers": "^6.13.0"
"@snowbridge/api": "^0.1.16",
"ethers": "^6.13.1"
},
"peerDependencies": {
"@polkadot/api": "^12.1.1",
Expand Down
7 changes: 2 additions & 5 deletions packages/sdk/src/builder/builders/Builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import {
type TVersionClaimAssets,
type Version
} from '../../types'
import CloseChannelBuilder, { type InboundCloseChannelBuilder } from './CloseChannelBuilder'
import OpenChannelBuilder, { type MaxSizeOpenChannelBuilder } from './OpenChannelBuilder'
import { CloseChannelBuilder, type InboundCloseChannelBuilder } from './CloseChannelBuilder'
import { OpenChannelBuilder, type MaxSizeOpenChannelBuilder } from './OpenChannelBuilder'
import RelayToParaBuilder from './RelayToParaBuilder'
import ParaToParaBuilder from './ParaToParaBuilder'
import ParaToRelayBuilder from './ParaToRelayBuilder'
Expand Down Expand Up @@ -145,6 +145,3 @@ export interface AccountBuilder {
export interface VersionBuilder extends FinalBuilderAsync {
xcmVersion: (version: TVersionClaimAssets) => FinalBuilderAsync
}

export * from './CloseChannelBuilder'
export * from './OpenChannelBuilder'
4 changes: 1 addition & 3 deletions packages/sdk/src/builder/builders/CloseChannelBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface InboundCloseChannelBuilder {
inbound: (inbound: number) => OutboundCloseChannelBuilder
}

class CloseChannelBuilder
export class CloseChannelBuilder
implements InboundCloseChannelBuilder, OutboundCloseChannelBuilder, FinalBuilder
{
private readonly api: ApiPromise
Expand Down Expand Up @@ -65,5 +65,3 @@ class CloseChannelBuilder
return closeChannelSerializedApiCall(options)
}
}

export default CloseChannelBuilder
4 changes: 1 addition & 3 deletions packages/sdk/src/builder/builders/OpenChannelBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export interface MaxSizeOpenChannelBuilder {
maxSize: (size: number) => MaxMessageSizeOpenChannelBuilder
}

class OpenChannelBuilder
export class OpenChannelBuilder
implements MaxSizeOpenChannelBuilder, MaxMessageSizeOpenChannelBuilder, FinalBuilder
{
private readonly api: ApiPromise
Expand Down Expand Up @@ -72,5 +72,3 @@ class OpenChannelBuilder
return openChannelSerializedApiCall(options)
}
}

export default OpenChannelBuilder
Loading

0 comments on commit 2f16524

Please sign in to comment.