diff --git a/bridge_ui/package-lock.json b/bridge_ui/package-lock.json index 872264967..c328bbf12 100644 --- a/bridge_ui/package-lock.json +++ b/bridge_ui/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "hasInstallScript": true, "dependencies": { + "@improbable-eng/grpc-web": "^0.13.0", "@material-ui/core": "^4.12.2", "@metamask/detect-provider": "^1.2.0", "@project-serum/sol-wallet-adapter": "^0.2.5", @@ -4011,8 +4012,6 @@ "version": "0.13.0", "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz", "integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==", - "dev": true, - "optional": true, "dependencies": { "browser-headers": "^0.4.0" }, @@ -10309,9 +10308,7 @@ "node_modules/browser-headers": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", - "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==", - "dev": true, - "optional": true + "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" }, "node_modules/browser-process-hrtime": { "version": "1.0.0", @@ -17417,9 +17414,7 @@ "node_modules/google-protobuf": { "version": "3.17.3", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz", - "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==", - "dev": true, - "optional": true + "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==" }, "node_modules/got": { "version": "9.6.0", @@ -41493,8 +41488,6 @@ "version": "0.13.0", "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz", "integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==", - "dev": true, - "optional": true, "requires": { "browser-headers": "^0.4.0" } @@ -46695,9 +46688,7 @@ "browser-headers": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/browser-headers/-/browser-headers-0.4.1.tgz", - "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==", - "dev": true, - "optional": true + "integrity": "sha512-CA9hsySZVo9371qEHjHZtYxV2cFtVj5Wj/ZHi8ooEsrtm4vOnl9Y9HmyYWk9q+05d7K3rdoAE0j3MVEFVvtQtg==" }, "browser-process-hrtime": { "version": "1.0.0", @@ -52508,9 +52499,7 @@ "google-protobuf": { "version": "3.17.3", "resolved": "https://registry.npmjs.org/google-protobuf/-/google-protobuf-3.17.3.tgz", - "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==", - "dev": true, - "optional": true + "integrity": "sha512-OVPzcSWIAJ+d5yiHyeaLrdufQtrvaBrF4JQg+z8ynTkbO3uFcujqXszTumqg1cGsAsjkWnI+M5B1xZ19yR4Wyg==" }, "got": { "version": "9.6.0", diff --git a/bridge_ui/package.json b/bridge_ui/package.json index ccadaf631..4888dc496 100644 --- a/bridge_ui/package.json +++ b/bridge_ui/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@improbable-eng/grpc-web": "^0.13.0", "@material-ui/core": "^4.12.2", "@metamask/detect-provider": "^1.2.0", "@project-serum/sol-wallet-adapter": "^0.2.5", diff --git a/bridge_ui/src/components/Transfer.tsx b/bridge_ui/src/components/Transfer.tsx index ac9986737..55ccba2c8 100644 --- a/bridge_ui/src/components/Transfer.tsx +++ b/bridge_ui/src/components/Transfer.tsx @@ -1,5 +1,6 @@ import { Button, + CircularProgress, Grid, makeStyles, MenuItem, @@ -11,6 +12,7 @@ import { useEthereumProvider } from "../contexts/EthereumProviderContext"; import { useSolanaWallet } from "../contexts/SolanaWalletContext"; import useEthereumBalance from "../hooks/useEthereumBalance"; import useSolanaBalance from "../hooks/useSolanaBalance"; +import useWrappedAsset from "../hooks/useWrappedAsset"; import { ChainId, CHAINS, @@ -105,8 +107,18 @@ function Transfer() { decimals: solDecimals, uiAmount: solBalance, } = useSolanaBalance(assetAddress, solPK, fromChain === CHAIN_ID_SOLANA); + const { isLoading: isCheckingWrapped, wrappedAsset } = useWrappedAsset( + toChain, + fromChain, + assetAddress, + provider + ); + console.log(isCheckingWrapped, wrappedAsset); + // TODO: make a helper function for this + const isWrapped = true; + //wrappedAsset && wrappedAsset !== ethers.constants.AddressZero; // TODO: dynamically get "to" wallet - const handleClick = useCallback(() => { + const handleTransferClick = useCallback(() => { // TODO: more generic way of calling these if (transferFrom[fromChain]) { if ( @@ -210,39 +222,77 @@ function Transfer() { value={assetAddress} onChange={handleAssetChange} /> - - - {canAttemptTransfer ? null : ( - - {!isTransferImplemented - ? `Transfer is not yet implemented for ${CHAINS_BY_ID[fromChain].name}` - : !isProviderConnected - ? "The source wallet is not connected" - : !isRecipientAvailable - ? "The receiving wallet is not connected" - : !isAddressDefined - ? "Please provide an asset address" - : !isAmountPositive - ? "The amount must be positive" - : !isBalanceAtLeastAmount - ? "The amount may not be greater than the balance" - : ""} - + {isWrapped ? ( + <> + + + {canAttemptTransfer ? null : ( + + {!isTransferImplemented + ? `Transfer is not yet implemented for ${CHAINS_BY_ID[fromChain].name}` + : !isProviderConnected + ? "The source wallet is not connected" + : !isRecipientAvailable + ? "The receiving wallet is not connected" + : !isAddressDefined + ? "Please provide an asset address" + : !isAmountPositive + ? "The amount must be positive" + : !isBalanceAtLeastAmount + ? "The amount may not be greater than the balance" + : ""} + + )} + + ) : ( + <> +
+ + {isCheckingWrapped ? ( + + ) : null} +
+ {isCheckingWrapped ? null : ( + +
+ This token does not exist on {CHAINS_BY_ID[toChain].name}. Someone + must attest the the token to the target chain before it can be + transferred. +
+ )} + )} ); diff --git a/bridge_ui/src/hooks/useEthereumBalance.ts b/bridge_ui/src/hooks/useEthereumBalance.ts index 1e48cea8a..8b3edcf0c 100644 --- a/bridge_ui/src/hooks/useEthereumBalance.ts +++ b/bridge_ui/src/hooks/useEthereumBalance.ts @@ -20,12 +20,10 @@ function useEthereumBalance( token .decimals() .then((decimals) => { - console.log(decimals); provider ?.getSigner() .getAddress() .then((pk) => { - console.log(pk); token.balanceOf(pk).then((n) => { if (!cancelled) { setBalance(formatUnits(n, decimals)); diff --git a/bridge_ui/src/hooks/useWrappedAsset.ts b/bridge_ui/src/hooks/useWrappedAsset.ts new file mode 100644 index 000000000..a46095a07 --- /dev/null +++ b/bridge_ui/src/hooks/useWrappedAsset.ts @@ -0,0 +1,41 @@ +import { ethers } from "ethers"; +import { useEffect, useState } from "react"; +import { ChainId, CHAIN_ID_ETH } from "../utils/consts"; +import { wrappedAssetEth } from "../utils/wrappedAsset"; + +export interface WrappedAssetState { + isLoading: boolean; + wrappedAsset: string | null; +} + +function useWrappedAsset( + checkChain: ChainId, + originChain: ChainId, + originAsset: string, + provider: ethers.providers.Web3Provider | undefined +) { + const [state, setState] = useState({ + isLoading: false, + wrappedAsset: null, + }); + useEffect(() => { + let cancelled = false; + (async () => { + if (provider && checkChain === CHAIN_ID_ETH) { + setState({ isLoading: true, wrappedAsset: null }); + const asset = await wrappedAssetEth(provider, originChain, originAsset); + if (!cancelled) { + setState({ isLoading: false, wrappedAsset: asset }); + } + } else { + setState({ isLoading: false, wrappedAsset: null }); + } + })(); + return () => { + cancelled = true; + }; + }, [checkChain, originChain, originAsset, provider]); + return state; +} + +export default useWrappedAsset; diff --git a/bridge_ui/src/muiTheme.js b/bridge_ui/src/muiTheme.js index cb992c062..b750d800f 100644 --- a/bridge_ui/src/muiTheme.js +++ b/bridge_ui/src/muiTheme.js @@ -8,7 +8,7 @@ export const theme = responsiveFontSizes( default: "#010114", paper: "#010114", }, - divider: "#FFFFFF", + divider: "#4e4e54", primary: { main: "#0074FF", }, diff --git a/bridge_ui/src/sdk/index.ts b/bridge_ui/src/sdk/index.ts new file mode 100644 index 000000000..f989e236a --- /dev/null +++ b/bridge_ui/src/sdk/index.ts @@ -0,0 +1,34 @@ +import { + GrpcWebImpl, + PublicrpcClientImpl, +} from "../proto/publicrpc/v1/publicrpc"; +import { ChainId } from "../utils/consts"; + +export async function getSignedVAA( + emitterChain: ChainId, + emitterAddress: string, + sequence: string +) { + const rpc = new GrpcWebImpl("http://localhost:8080", {}); + const api = new PublicrpcClientImpl(rpc); + // TODO: potential infinite loop, support cancellation? + let result; + while (!result) { + console.log("wait 1 second"); + await new Promise((resolve) => setTimeout(resolve, 1000)); + console.log("check for signed vaa", emitterChain, emitterAddress, sequence); + try { + result = await api.GetSignedVAA({ + messageId: { + emitterChain, + emitterAddress, + sequence, + }, + }); + console.log(result); + } catch (e) { + console.log(e); + } + } + return result; +} diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts index 771049aba..8050b0f53 100644 --- a/bridge_ui/src/utils/consts.ts +++ b/bridge_ui/src/utils/consts.ts @@ -1,3 +1,5 @@ +import { getAddress } from "ethers/lib/utils"; + export type ChainId = 1 | 2 | 3 | 4; export const CHAIN_ID_SOLANA: ChainId = 1; export const CHAIN_ID_ETH: ChainId = 2; @@ -31,10 +33,15 @@ export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain) => { return obj; }, {} as ChainsById); export const SOLANA_HOST = "http://localhost:8899"; -export const ETH_TEST_TOKEN_ADDRESS = - "0x0290FB167208Af455bB137780163b7B7a9a10C16"; -export const ETH_TOKEN_BRIDGE_ADDRESS = - "0xe982e462b094850f12af94d21d470e21be9d0e9c"; +export const ETH_TEST_TOKEN_ADDRESS = getAddress( + "0x0290FB167208Af455bB137780163b7B7a9a10C16" +); +export const ETH_BRIDGE_ADDRESS = getAddress( + "0x254dffcd3277c0b1660f6d42efbb754edababc2b" +); +export const ETH_TOKEN_BRIDGE_ADDRESS = getAddress( + "0xe982e462b094850f12af94d21d470e21be9d0e9c" +); export const SOL_TEST_TOKEN_ADDRESS = "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"; export const SOL_BRIDGE_ADDRESS = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"; diff --git a/bridge_ui/src/utils/transferFrom.ts b/bridge_ui/src/utils/transferFrom.ts index 9bde64178..70f1795e1 100644 --- a/bridge_ui/src/utils/transferFrom.ts +++ b/bridge_ui/src/utils/transferFrom.ts @@ -1,4 +1,5 @@ import Wallet from "@project-serum/sol-wallet-adapter"; +import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { AccountMeta, Connection, @@ -7,17 +8,19 @@ import { Transaction, TransactionInstruction, } from "@solana/web3.js"; -import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { ethers } from "ethers"; import { arrayify, formatUnits, parseUnits, zeroPad } from "ethers/lib/utils"; import { Bridge__factory, + Implementation__factory, TokenImplementation__factory, } from "../ethers-contracts"; +import { getSignedVAA } from "../sdk"; import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA, + ETH_BRIDGE_ADDRESS, ETH_TOKEN_BRIDGE_ADDRESS, SOLANA_HOST, SOL_BRIDGE_ADDRESS, @@ -41,46 +44,60 @@ export function transferFromEth( //TODO: don't hardcode, fetch decimals / share them with balance, how do we determine recipient chain? //TODO: more catches const amountParsed = parseUnits(amount, 18); - signer.getAddress().then((signerAddress) => { + (async () => { + const signerAddress = await signer.getAddress(); console.log("Signer:", signerAddress); console.log("Token:", tokenAddress); const token = TokenImplementation__factory.connect(tokenAddress, signer); - token - .allowance(signerAddress, ETH_TOKEN_BRIDGE_ADDRESS) - .then((allowance) => { - console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't? - token - .approve(ETH_TOKEN_BRIDGE_ADDRESS, amountParsed) - .then((transaction) => { - console.log(transaction); - const fee = 0; // for now, this won't do anything, we may add later - const nonceConst = Math.random() * 100000; - const nonceBuffer = Buffer.alloc(4); - nonceBuffer.writeUInt32LE(nonceConst, 0); - console.log("Initiating transfer"); - console.log("Amount:", formatUnits(amountParsed, 18)); - console.log("To chain:", recipientChain); - console.log("To address:", recipientAddress); - console.log("Fees:", fee); - console.log("Nonce:", nonceBuffer); - const bridge = Bridge__factory.connect( - ETH_TOKEN_BRIDGE_ADDRESS, - signer - ); - bridge - .transferTokens( - tokenAddress, - amountParsed, - recipientChain, - recipientAddress, - fee, - nonceBuffer - ) - .then((v) => console.log("Success:", v)) - .catch((r) => console.error(r)); //TODO: integrate toast messages - }); - }); - }); + const allowance = await token.allowance( + signerAddress, + ETH_TOKEN_BRIDGE_ADDRESS + ); + console.log("Allowance", allowance.toString()); //TODO: should we check that this is zero and warn if it isn't? + const transaction = await token.approve( + ETH_TOKEN_BRIDGE_ADDRESS, + amountParsed + ); + console.log(transaction); + const fee = 0; // for now, this won't do anything, we may add later + const nonceConst = Math.random() * 100000; + const nonceBuffer = Buffer.alloc(4); + nonceBuffer.writeUInt32LE(nonceConst, 0); + console.log("Initiating transfer"); + console.log("Amount:", formatUnits(amountParsed, 18)); + console.log("To chain:", recipientChain); + console.log("To address:", recipientAddress); + console.log("Fees:", fee); + console.log("Nonce:", nonceBuffer); + const bridge = Bridge__factory.connect(ETH_TOKEN_BRIDGE_ADDRESS, signer); + const v = await bridge.transferTokens( + tokenAddress, + amountParsed, + recipientChain, + recipientAddress, + fee, + nonceBuffer + ); + const receipt = await v.wait(); + // TODO: dangerous!(?) + const bridgeLog = receipt.logs.filter((l) => { + console.log(l.address, ETH_BRIDGE_ADDRESS); + return l.address === ETH_BRIDGE_ADDRESS; + })[0]; + const { + args: { sender, sequence }, + } = Implementation__factory.createInterface().parseLog(bridgeLog); + console.log(sender, sequence); + const emitterAddress = Buffer.from( + zeroPad(arrayify(ETH_TOKEN_BRIDGE_ADDRESS), 32) + ).toString("hex"); + const { vaaBytes } = await getSignedVAA( + CHAIN_ID_ETH, + emitterAddress, + sequence + ); + console.log("SIGNED VAA:", vaaBytes); + })(); } // TODO: should we share this with client? ooh, should client use the SDK ;) @@ -201,8 +218,8 @@ export function transferFromSolana( console.log("SIGNED", signed); const txid = await connection.sendRawTransaction(signed.serialize()); console.log("SENT", txid); - await connection.confirmTransaction(txid); - console.log("CONFIRMED"); + const conf = await connection.confirmTransaction(txid); + console.log("CONFIRMED", conf); })(); } diff --git a/bridge_ui/src/utils/wrappedAsset.ts b/bridge_ui/src/utils/wrappedAsset.ts new file mode 100644 index 000000000..e6425c066 --- /dev/null +++ b/bridge_ui/src/utils/wrappedAsset.ts @@ -0,0 +1,24 @@ +import { PublicKey } from "@solana/web3.js"; +import { ethers } from "ethers"; +import { arrayify, zeroPad } from "ethers/lib/utils"; +import { Bridge__factory } from "../ethers-contracts"; +import { ChainId, CHAIN_ID_SOLANA, ETH_TOKEN_BRIDGE_ADDRESS } from "./consts"; + +export function wrappedAssetEth( + provider: ethers.providers.Web3Provider, + originChain: ChainId, + originAsset: string +) { + const tokenBridge = Bridge__factory.connect( + ETH_TOKEN_BRIDGE_ADDRESS, + provider + ); + // TODO: address conversion may be more complex than this + const originAssetBytes = zeroPad( + originChain === CHAIN_ID_SOLANA + ? new PublicKey(originAsset).toBytes() + : arrayify(originAsset), + 32 + ); + return tokenBridge.wrappedAsset(originChain, originAssetBytes); +} diff --git a/bridge_ui/tsconfig.json b/bridge_ui/tsconfig.json index 16fff78a0..c11055884 100644 --- a/bridge_ui/tsconfig.json +++ b/bridge_ui/tsconfig.json @@ -17,6 +17,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, + "downlevelIteration": true, "noEmit": true, "jsx": "react-jsx" }, diff --git a/buf.gen.web.yaml b/buf.gen.web.yaml index 25efe2267..755165432 100644 --- a/buf.gen.web.yaml +++ b/buf.gen.web.yaml @@ -19,4 +19,4 @@ plugins: - env=browser - forceLong=string - outputClientImpl=grpc-web - - explorer/src/proto + - bridge_ui/src/proto diff --git a/tools/package-lock.json b/tools/package-lock.json index 1704220ae..7720111b8 100644 --- a/tools/package-lock.json +++ b/tools/package-lock.json @@ -7,7 +7,7 @@ "": { "version": "1.0.0", "devDependencies": { - "ts-proto": "^1.81.1" + "ts-proto": "^1.82.3" } }, "node_modules/@protobufjs/aspromise": { @@ -175,9 +175,9 @@ } }, "node_modules/ts-proto": { - "version": "1.82.0", - "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.0.tgz", - "integrity": "sha512-vo4QN4QhR0D4/+C/pSbRIVSV6U7dooNcuyW3SL9DvhKRQA4lnAbF5QBs77ge3JRi+aSZJm8MlzTNk7+e++fvvQ==", + "version": "1.82.3", + "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.3.tgz", + "integrity": "sha512-ODveOXK2imsgTiqkBcJu9mIOklmCTSzs7Xu+mT8Xljwh3Wenhax7bhty+x2eO4J7AfNkikXH0Xs7K3lk3UT8VA==", "dev": true, "dependencies": { "@types/object-hash": "^1.3.0", @@ -354,9 +354,9 @@ } }, "ts-proto": { - "version": "1.82.0", - "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.0.tgz", - "integrity": "sha512-vo4QN4QhR0D4/+C/pSbRIVSV6U7dooNcuyW3SL9DvhKRQA4lnAbF5QBs77ge3JRi+aSZJm8MlzTNk7+e++fvvQ==", + "version": "1.82.3", + "resolved": "https://registry.npmjs.org/ts-proto/-/ts-proto-1.82.3.tgz", + "integrity": "sha512-ODveOXK2imsgTiqkBcJu9mIOklmCTSzs7Xu+mT8Xljwh3Wenhax7bhty+x2eO4J7AfNkikXH0Xs7K3lk3UT8VA==", "dev": true, "requires": { "@types/object-hash": "^1.3.0", diff --git a/tools/package.json b/tools/package.json index 83a6c6bd0..56c3c733f 100644 --- a/tools/package.json +++ b/tools/package.json @@ -3,7 +3,6 @@ "version": "1.0.0", "description": "tooling for building web code from protobufs", "devDependencies": { - "ts-proto": "^1.81.1" - }, - "dependencies": {} + "ts-proto": "^1.82.3" + } }