diff --git a/bridge_ui/package-lock.json b/bridge_ui/package-lock.json index 77839986b..872264967 100644 --- a/bridge_ui/package-lock.json +++ b/bridge_ui/package-lock.json @@ -12,6 +12,7 @@ "@material-ui/core": "^4.12.2", "@metamask/detect-provider": "^1.2.0", "@project-serum/sol-wallet-adapter": "^0.2.5", + "@solana/spl-token": "^0.1.6", "@solana/wallet-base": "^0.0.1", "@solana/web3.js": "^1.22.0", "@typechain/ethers-v5": "^7.0.1", @@ -5367,6 +5368,53 @@ "ieee754": "^1.2.1" } }, + "node_modules/@solana/spl-token": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.6.tgz", + "integrity": "sha512-fYj+a3w1bqWN6Ibf85XF3h2JkuxevI3Spvqi+mjsNqVUEo2AgxxTZmujNLn/jIzQDNdWkBfF/wYzH5ikcGHmfw==", + "dependencies": { + "@babel/runtime": "^7.10.5", + "@solana/web3.js": "^1.12.0", + "bn.js": "^5.1.0", + "buffer": "6.0.3", + "buffer-layout": "^1.2.0", + "dotenv": "10.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@solana/spl-token/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/@solana/spl-token/node_modules/dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/@solana/wallet-base": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@solana/wallet-base/-/wallet-base-0.0.1.tgz", @@ -42465,6 +42513,35 @@ } } }, + "@solana/spl-token": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.1.6.tgz", + "integrity": "sha512-fYj+a3w1bqWN6Ibf85XF3h2JkuxevI3Spvqi+mjsNqVUEo2AgxxTZmujNLn/jIzQDNdWkBfF/wYzH5ikcGHmfw==", + "requires": { + "@babel/runtime": "^7.10.5", + "@solana/web3.js": "^1.12.0", + "bn.js": "^5.1.0", + "buffer": "6.0.3", + "buffer-layout": "^1.2.0", + "dotenv": "10.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "dotenv": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", + "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" + } + } + }, "@solana/wallet-base": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@solana/wallet-base/-/wallet-base-0.0.1.tgz", diff --git a/bridge_ui/package.json b/bridge_ui/package.json index fecff3617..ccadaf631 100644 --- a/bridge_ui/package.json +++ b/bridge_ui/package.json @@ -6,6 +6,7 @@ "@material-ui/core": "^4.12.2", "@metamask/detect-provider": "^1.2.0", "@project-serum/sol-wallet-adapter": "^0.2.5", + "@solana/spl-token": "^0.1.6", "@solana/wallet-base": "^0.0.1", "@solana/web3.js": "^1.22.0", "@typechain/ethers-v5": "^7.0.1", diff --git a/bridge_ui/src/components/KeyAndBalance.tsx b/bridge_ui/src/components/KeyAndBalance.tsx index 670a33ef4..3680aee52 100644 --- a/bridge_ui/src/components/KeyAndBalance.tsx +++ b/bridge_ui/src/components/KeyAndBalance.tsx @@ -23,7 +23,7 @@ function KeyAndBalance({ ); const { wallet: solWallet } = useSolanaWallet(); const solPK = solWallet?.publicKey; - const solBalance = useSolanaBalance( + const { uiAmountString: solBalance } = useSolanaBalance( tokenAddress, solPK, chainId === CHAIN_ID_SOLANA diff --git a/bridge_ui/src/components/Transfer.tsx b/bridge_ui/src/components/Transfer.tsx index c7893e034..ac9986737 100644 --- a/bridge_ui/src/components/Transfer.tsx +++ b/bridge_ui/src/components/Transfer.tsx @@ -100,6 +100,11 @@ function Transfer() { const provider = useEthereumProvider(); const { wallet } = useSolanaWallet(); const solPK = wallet?.publicKey; + const { + tokenAccount: solTokenPK, + decimals: solDecimals, + uiAmount: solBalance, + } = useSolanaBalance(assetAddress, solPK, fromChain === CHAIN_ID_SOLANA); // TODO: dynamically get "to" wallet const handleClick = useCallback(() => { // TODO: more generic way of calling these @@ -121,27 +126,35 @@ function Transfer() { transferFrom[fromChain] === transferFromSolana ) { transferFromSolana( + wallet, solPK?.toString(), + solTokenPK?.toString(), assetAddress, amount, + solDecimals, provider, toChain ); } } - }, [fromChain, provider, solPK, assetAddress, amount, toChain]); + }, [ + fromChain, + provider, + wallet, + solPK, + solTokenPK, + assetAddress, + amount, + solDecimals, + toChain, + ]); // update this as we develop, just setting expectations with the button state const ethBalance = useEthereumBalance( assetAddress, provider, fromChain === CHAIN_ID_ETH ); - const solBalance = useSolanaBalance( - assetAddress, - solPK, - fromChain === CHAIN_ID_SOLANA - ); - const balance = Number(ethBalance) || Number(solBalance); + const balance = Number(ethBalance) || solBalance; const isTransferImplemented = !!transferFrom[fromChain]; const isProviderConnected = !!provider; const isRecipientAvailable = !!solPK; diff --git a/bridge_ui/src/hooks/useSolanaBalance.ts b/bridge_ui/src/hooks/useSolanaBalance.ts index 6bb8a3aa4..123e87039 100644 --- a/bridge_ui/src/hooks/useSolanaBalance.ts +++ b/bridge_ui/src/hooks/useSolanaBalance.ts @@ -2,20 +2,46 @@ import { Connection, PublicKey } from "@solana/web3.js"; import { useEffect, useState } from "react"; import { SOLANA_HOST } from "../utils/consts"; +export interface Balance { + tokenAccount: PublicKey | undefined; + amount: string; + decimals: number; + uiAmount: number; + uiAmountString: string; +} + +function createBalance( + tokenAccount: PublicKey | undefined, + amount: string, + decimals: number, + uiAmount: number, + uiAmountString: string +) { + return { + tokenAccount, + amount, + decimals, + uiAmount, + uiAmountString, + }; +} + function useSolanaBalance( tokenAddress: string | undefined, ownerAddress: PublicKey | null | undefined, shouldCalculate?: boolean ) { //TODO: should connection happen in a context? - const [balance, setBalance] = useState(""); + const [balance, setBalance] = useState( + createBalance(undefined, "", 0, 0, "") + ); useEffect(() => { if (!tokenAddress || !ownerAddress || !shouldCalculate) { - setBalance(""); + setBalance(createBalance(undefined, "", 0, 0, "")); return; } let cancelled = false; - const connection = new Connection(SOLANA_HOST); + const connection = new Connection(SOLANA_HOST, "finalized"); connection .getParsedTokenAccountsByOwner(ownerAddress, { mint: new PublicKey(tokenAddress), @@ -23,18 +49,23 @@ function useSolanaBalance( .then(({ value }) => { if (!cancelled) { if (value.length) { - console.log(value[0].account.data.parsed); setBalance( - value[0].account.data.parsed?.info?.tokenAmount?.uiAmountString + createBalance( + value[0].pubkey, + value[0].account.data.parsed?.info?.tokenAmount?.amount, + value[0].account.data.parsed?.info?.tokenAmount?.decimals, + value[0].account.data.parsed?.info?.tokenAmount?.uiAmount, + value[0].account.data.parsed?.info?.tokenAmount?.uiAmountString + ) ); } else { - setBalance("0"); + setBalance(createBalance(undefined, "0", 0, 0, "0")); } } }) .catch(() => { if (!cancelled) { - setBalance(""); + setBalance(createBalance(undefined, "", 0, 0, "")); } }); return () => { diff --git a/bridge_ui/src/utils/consts.ts b/bridge_ui/src/utils/consts.ts index 3d8b3a894..771049aba 100644 --- a/bridge_ui/src/utils/consts.ts +++ b/bridge_ui/src/utils/consts.ts @@ -1,34 +1,42 @@ -export type ChainId = 1 | 2 | 3 | 4 -export const CHAIN_ID_SOLANA: ChainId = 1 -export const CHAIN_ID_ETH: ChainId = 2 -export const CHAIN_ID_TERRA: ChainId = 3 -export const CHAIN_ID_BSC: ChainId = 4 +export type ChainId = 1 | 2 | 3 | 4; +export const CHAIN_ID_SOLANA: ChainId = 1; +export const CHAIN_ID_ETH: ChainId = 2; +export const CHAIN_ID_TERRA: ChainId = 3; +export const CHAIN_ID_BSC: ChainId = 4; export interface ChainInfo { - id: ChainId - name: string + id: ChainId; + name: string; } export const CHAINS = [ { id: CHAIN_ID_BSC, - name: 'Binance Smart Chain' + name: "Binance Smart Chain", }, { id: CHAIN_ID_ETH, - name: 'Ethereum' + name: "Ethereum", }, { id: CHAIN_ID_SOLANA, - name: 'Solana' + name: "Solana", }, { id: CHAIN_ID_TERRA, - name: 'Terra' + name: "Terra", }, -] -export type ChainsById = {[key in ChainId]: ChainInfo} -export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain)=>{obj[chain.id]=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 SOL_TEST_TOKEN_ADDRESS = "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ" -export const SOL_TOKEN_BRIDGE_ADDRESS = "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE" \ No newline at end of file +]; +export type ChainsById = { [key in ChainId]: ChainInfo }; +export const CHAINS_BY_ID: ChainsById = CHAINS.reduce((obj, chain) => { + obj[chain.id] = 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 SOL_TEST_TOKEN_ADDRESS = + "2WDq7wSs9zYrpx2kbHDA4RUTRch2CCTP6ZWaH4GNfnQQ"; +export const SOL_BRIDGE_ADDRESS = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"; +export const SOL_TOKEN_BRIDGE_ADDRESS = + "B6RHG3mfcckmrYN1UhmJzyS1XX3fZKbkeUcpJe9Sy3FE"; diff --git a/bridge_ui/src/utils/transferFrom.ts b/bridge_ui/src/utils/transferFrom.ts index d93db6da4..9bde64178 100644 --- a/bridge_ui/src/utils/transferFrom.ts +++ b/bridge_ui/src/utils/transferFrom.ts @@ -1,12 +1,39 @@ +import Wallet from "@project-serum/sol-wallet-adapter"; +import { + AccountMeta, + Connection, + PublicKey, + SystemProgram, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; +import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token"; import { ethers } from "ethers"; -import { arrayify, formatUnits, parseUnits } from "ethers/lib/utils"; -import { Bridge__factory, TokenImplementation__factory } from "../ethers-contracts"; -import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA, ETH_TOKEN_BRIDGE_ADDRESS, SOL_TOKEN_BRIDGE_ADDRESS } from "./consts"; +import { arrayify, formatUnits, parseUnits, zeroPad } from "ethers/lib/utils"; +import { + Bridge__factory, + TokenImplementation__factory, +} from "../ethers-contracts"; +import { + ChainId, + CHAIN_ID_ETH, + CHAIN_ID_SOLANA, + ETH_TOKEN_BRIDGE_ADDRESS, + SOLANA_HOST, + SOL_BRIDGE_ADDRESS, + SOL_TOKEN_BRIDGE_ADDRESS, +} from "./consts"; // TODO: this should probably be extended from the context somehow so that the signatures match // TODO: allow for / handle cancellation? // TODO: overall better input checking and error handling -export function transferFromEth(provider: ethers.providers.Web3Provider | undefined, tokenAddress: string, amount: string, recipientChain: ChainId, recipientAddress: Uint8Array | undefined) { +export function transferFromEth( + provider: ethers.providers.Web3Provider | undefined, + tokenAddress: string, + amount: string, + recipientChain: ChainId, + recipientAddress: Uint8Array | undefined +) { if (!provider || !recipientAddress) return; const signer = provider.getSigner(); if (!signer) return; @@ -16,11 +43,8 @@ export function transferFromEth(provider: ethers.providers.Web3Provider | undefi const amountParsed = parseUnits(amount, 18); signer.getAddress().then((signerAddress) => { console.log("Signer:", signerAddress); - console.log("Token:", tokenAddress) - const token = TokenImplementation__factory.connect( - tokenAddress, - signer - ); + console.log("Token:", tokenAddress); + const token = TokenImplementation__factory.connect(tokenAddress, signer); token .allowance(signerAddress, ETH_TOKEN_BRIDGE_ADDRESS) .then((allowance) => { @@ -59,41 +83,132 @@ export function transferFromEth(provider: ethers.providers.Web3Provider | undefi }); } +// TODO: should we share this with client? ooh, should client use the SDK ;) +// begin from clients\solana\main.ts +function ixFromRust(data: any): TransactionInstruction { + let keys: Array = data.accounts.map(accountMetaFromRust); + return new TransactionInstruction({ + programId: new PublicKey(data.program_id), + data: Buffer.from(data.data), + keys: keys, + }); +} + +function accountMetaFromRust(meta: any): AccountMeta { + return { + pubkey: new PublicKey(meta.pubkey), + isSigner: meta.is_signer, + isWritable: meta.is_writable, + }; +} +// end from clients\solana\main.ts + // TODO: need to check transfer native vs transfer wrapped // TODO: switch out targetProvider for generic address (this likely involves getting these in their respective contexts) -export function transferFromSolana(fromAddress: string | undefined, tokenAddress: string, amount: string, targetProvider: ethers.providers.Web3Provider | undefined, targetChain: ChainId) { - if (!fromAddress || !targetProvider) return; +export function transferFromSolana( + wallet: Wallet | undefined, + payerAddress: string | undefined, //TODO: we may not need this since we have wallet + fromAddress: string | undefined, + mintAddress: string, + amount: string, + decimals: number, + targetProvider: ethers.providers.Web3Provider | undefined, + targetChain: ChainId +) { + if ( + !wallet || + !wallet.publicKey || + !payerAddress || + !fromAddress || + !targetProvider + ) + return; const targetSigner = targetProvider.getSigner(); if (!targetSigner) return; - targetSigner.getAddress().then(targetAddressStr => { - const targetAddress = arrayify(targetAddressStr) + (async () => { + const targetAddressStr = await targetSigner.getAddress(); + const targetAddress = zeroPad(arrayify(targetAddressStr), 32); const nonceConst = Math.random() * 100000; const nonceBuffer = Buffer.alloc(4); nonceBuffer.writeUInt32LE(nonceConst, 0); - const nonce = nonceBuffer.readUInt32LE(0) - // TODO: check decimals - // should we avoid BigInt? - const amountParsed = BigInt(amount) - const fee = BigInt(0) // for now, this won't do anything, we may add later - console.log('bridge:',SOL_TOKEN_BRIDGE_ADDRESS) - console.log('from:',fromAddress) - console.log('token:',tokenAddress) - console.log('nonce:',nonce) - console.log('amount:',amountParsed) - console.log('fee:',fee) - console.log('target:',targetAddressStr,targetAddress) - console.log('chain:',targetChain) - // TODO: program_id vs bridge_id? - import("token-bridge").then(({transfer_native_ix})=>{ - const ix = transfer_native_ix(SOL_TOKEN_BRIDGE_ADDRESS,SOL_TOKEN_BRIDGE_ADDRESS,fromAddress,fromAddress,tokenAddress,nonce,amountParsed,fee,targetAddress,targetChain) - console.log(ix) - }) - }) + const nonce = nonceBuffer.readUInt32LE(0); + const amountParsed = parseUnits(amount, decimals).toBigInt(); + const fee = BigInt(0); // for now, this won't do anything, we may add later + console.log("program:", SOL_TOKEN_BRIDGE_ADDRESS); + console.log("bridge:", SOL_BRIDGE_ADDRESS); + console.log("payer:", payerAddress); + console.log("from:", fromAddress); + console.log("token:", mintAddress); + console.log("nonce:", nonce); + console.log("amount:", amountParsed); + console.log("fee:", fee); + console.log("target:", targetAddressStr, targetAddress); + console.log("chain:", targetChain); + const bridge = await import("bridge"); + const feeAccount = await bridge.fee_collector_address(SOL_BRIDGE_ADDRESS); + const bridgeStatePK = new PublicKey( + bridge.state_address(SOL_BRIDGE_ADDRESS) + ); + const connection = new Connection(SOLANA_HOST, "confirmed"); + const bridgeStateAccountInfo = await connection.getAccountInfo( + bridgeStatePK + ); + if (bridgeStateAccountInfo?.data === undefined) { + throw new Error("bridge state not found"); + } + const bridgeState = bridge.parse_state( + new Uint8Array(bridgeStateAccountInfo?.data) + ); + const transferIx = SystemProgram.transfer({ + fromPubkey: new PublicKey(payerAddress), + toPubkey: new PublicKey(feeAccount), + lamports: bridgeState.config.fee, + }); + // TODO: pass in connection + // Add transfer instruction to transaction + const { transfer_native_ix, approval_authority_address } = await import( + "token-bridge" + ); + const approvalIx = Token.createApproveInstruction( + TOKEN_PROGRAM_ID, + new PublicKey(fromAddress), + new PublicKey(approval_authority_address(SOL_TOKEN_BRIDGE_ADDRESS)), + new PublicKey(payerAddress), + [], + Number(amountParsed) + ); + const ix = ixFromRust( + transfer_native_ix( + SOL_TOKEN_BRIDGE_ADDRESS, + SOL_BRIDGE_ADDRESS, + payerAddress, + fromAddress, + mintAddress, + nonce, + amountParsed, + fee, + targetAddress, + targetChain + ) + ); + console.log(ix); + const transaction = new Transaction().add(transferIx, approvalIx, ix); + const { blockhash } = await connection.getRecentBlockhash(); + transaction.recentBlockhash = blockhash; + transaction.feePayer = new PublicKey(payerAddress); + // Sign transaction, broadcast, and confirm + const signed = await wallet.signTransaction(transaction); + console.log("SIGNED", signed); + const txid = await connection.sendRawTransaction(signed.serialize()); + console.log("SENT", txid); + await connection.confirmTransaction(txid); + console.log("CONFIRMED"); + })(); } const transferFrom = { [CHAIN_ID_ETH]: transferFromEth, - [CHAIN_ID_SOLANA]: transferFromSolana -} + [CHAIN_ID_SOLANA]: transferFromSolana, +}; -export default transferFrom \ No newline at end of file +export default transferFrom;