diff --git a/web/src/config.ts b/web/src/config.ts index 00e185e4..cbb0e81a 100644 --- a/web/src/config.ts +++ b/web/src/config.ts @@ -1,7 +1,9 @@ +import {PublicKey} from "@solana/web3.js"; + const BRIDGE_ADDRESS = "0xac3eB48829fFC3C37437ce4459cE63F1F4d4E0b4"; -const SOLANA_BRIDGE_PROGRAM = "9Y7DSe9VGMZPmev93o2JhaAD25ZwQ7d8pPswe9LzL7Eb"; -const TOKEN_PROGRAM = "TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"; +const SOLANA_BRIDGE_PROGRAM = new PublicKey("C2MtMo3upLePs5Sumkw61aBgcgpCw9Q84KyJMovLbXu6"); +const TOKEN_PROGRAM = new PublicKey("TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o"); export { diff --git a/web/src/pages/Transfer.tsx b/web/src/pages/Transfer.tsx index 9e6371e5..7d241288 100644 --- a/web/src/pages/Transfer.tsx +++ b/web/src/pages/Transfer.tsx @@ -1,17 +1,20 @@ import React, {useContext, useEffect, useState} from 'react'; import ClientContext from "../providers/ClientContext"; import * as solanaWeb3 from '@solana/web3.js'; -import {PublicKey} from '@solana/web3.js'; +import {Account, Connection, PublicKey, Transaction} from '@solana/web3.js'; import {Button, Col, Form, Input, InputNumber, message, Row, Select, Space} from "antd"; import {ethers} from "ethers"; import {Erc20Factory} from "../contracts/Erc20Factory"; import {Arrayish, BigNumber, BigNumberish} from "ethers/utils"; import {WormholeFactory} from "../contracts/WormholeFactory"; import {WrappedAssetFactory} from "../contracts/WrappedAssetFactory"; -import {SolanaBridge} from "../utils/bridge"; import {BRIDGE_ADDRESS} from "../config"; -import SplBalances from "../components/SplBalances"; import {SlotContext} from "../providers/SlotContext"; +import {SolanaTokenContext} from "../providers/SolanaTokenContext"; +import {BridgeContext} from "../providers/BridgeContext"; +import SplBalances from "../components/SplBalances"; +import {AssetMeta, SolanaBridge} from "../utils/bridge"; +import KeyContext from "../providers/KeyContext"; // @ts-ignore @@ -50,9 +53,35 @@ async function approveAssets(asset: string, } } +async function createWrapped(c: Connection, b: SolanaBridge, key: Account, meta: AssetMeta, mint: PublicKey) { + try { + let tx = new Transaction(); + let mintAccount = await c.getAccountInfo(mint); + if (!mintAccount) { + let ix = await b.createWrappedAssetInstruction(key.publicKey, meta); + tx.add(ix) + } + + // @ts-ignore + let [ix_account, newSigner] = await b.createWrappedAssetAndAccountInstructions(key.publicKey, mint); + let recentHash = await c.getRecentBlockhash(); + tx.recentBlockhash = recentHash.blockhash + tx.add(...ix_account) + tx.sign(key, newSigner) + message.loading({content: "Waiting for transaction to be confirmed...", key: "tx", duration: 1000}) + await c.sendTransaction(tx, [key, newSigner]) + message.success({content: "Creation succeeded!", key: "tx"}) + } catch (e) { + message.error({content: "Creation failed", key: "tx"}) + } +} + function Transfer() { let c = useContext(ClientContext); + let tokenAccounts = useContext(SolanaTokenContext); let slot = useContext(SlotContext); + let bridge = useContext(BridgeContext); + let k = useContext(KeyContext); let [coinInfo, setCoinInfo] = useState({ balance: new BigNumber(0), @@ -60,16 +89,43 @@ function Transfer() { allowance: new BigNumber(0), isWrapped: false, chainID: 0, - wrappedAddress: "" + assetAddress: new Buffer("") }); let [amount, setAmount] = useState(0); let [address, setAddress] = useState(""); let [addressValid, setAddressValid] = useState(false) + let [solanaAccount, setSolanaAccount] = useState(false) + let [wrappedMint, setWrappedMint] = useState("") useEffect(() => { fetchBalance(address) }, [address]) + useEffect(() => { + if (!addressValid) { + setWrappedMint("") + setSolanaAccount(false) + return + } + + let getWrappedInfo = async () => { + let wrappedMint = await bridge.getWrappedAssetMint({ + chain: coinInfo.chainID, + address: coinInfo.assetAddress + }); + setWrappedMint(wrappedMint.toString()) + + for (let account of tokenAccounts.balances) { + if (account.mint == wrappedMint.toString()) { + setSolanaAccount(true) + return; + } + } + setSolanaAccount(false) + } + getWrappedInfo(); + }, [address, addressValid, tokenAccounts, bridge]) + async function fetchBalance(token: string) { try { let e = WrappedAssetFactory.connect(token, provider); @@ -83,8 +139,8 @@ function Transfer() { allowance: allowance.div(new BigNumber(10).pow(decimals)), decimals: decimals, isWrapped: false, - chainID: 0, - wrappedAddress: "" + chainID: 2, + assetAddress: new Buffer(token.slice(2), "hex") } let b = WormholeFactory.connect(BRIDGE_ADDRESS, provider); @@ -92,7 +148,7 @@ function Transfer() { let isWrapped = await b.isWrappedAsset(token) if (isWrapped) { info.chainID = await e.assetChain() - info.wrappedAddress = await e.assetAddress() + info.assetAddress = new Buffer((await e.assetAddress()).slice(2), "hex") info.isWrapped = true } setCoinInfo(info) @@ -106,66 +162,63 @@ function Transfer() { <>

Slot: {slot}

- - -
{ - let recipient = new solanaWeb3.PublicKey(values["recipient"]).toBuffer() - let transferAmount = new BigNumber(values["amount"]).mul(new BigNumber(10).pow(coinInfo.decimals)); - if (coinInfo.allowance.toNumber() >= amount || coinInfo.isWrapped) { - lockAssets(values["address"], transferAmount, recipient, values["target_chain"]) - } else { - approveAssets(values["address"], transferAmount) + + { + let recipient = new solanaWeb3.PublicKey(values["recipient"]).toBuffer() + let transferAmount = new BigNumber(values["amount"]).mul(new BigNumber(10).pow(coinInfo.decimals)); + if (coinInfo.allowance.toNumber() >= amount || coinInfo.isWrapped) { + lockAssets(values["address"], transferAmount, recipient, values["target_chain"]) + } else { + approveAssets(values["address"], transferAmount) + } + }} style={{width: "100%"}}> + + { + setAddress(v.target.value) + }}/> + + { + let big = new BigNumber(value); + callback(big.lte(coinInfo.balance) ? undefined : "Amount exceeds balance") } - }}> - - { - setAddress(v.target.value) - }}/> - - { - let big = new BigNumber(value); - callback(big.lte(coinInfo.balance) ? undefined : "Amount exceeds balance") - } - }]}> - { - // @ts-ignore - setAmount(value || 0) - }}/> - - - - - { - try { - new solanaWeb3.PublicKey(value); - callback(); - } catch (e) { - callback("Not a valid Solana address"); - } - } - },]}> - - - - - - -
+ }]}> + { + // @ts-ignore + setAmount(value || 0) + }}/> + + + + + + + + + + +
- + + +

Mint: {wrappedMint}

+ + ); } diff --git a/web/src/pages/TransferSolana.tsx b/web/src/pages/TransferSolana.tsx index 2a743766..f98e6afa 100644 --- a/web/src/pages/TransferSolana.tsx +++ b/web/src/pages/TransferSolana.tsx @@ -3,7 +3,7 @@ import ClientContext from "../providers/ClientContext"; import * as solanaWeb3 from '@solana/web3.js'; import {PublicKey, Transaction} from '@solana/web3.js'; import * as spl from '@solana/spl-token'; -import {Button, Col, Form, Input, InputNumber, message, Row, Select, Space} from "antd"; +import {Button, Col, Form, Input, InputNumber, message, Row, Select} from "antd"; import {BigNumber} from "ethers/utils"; import SplBalances from "../components/SplBalances"; import {SlotContext} from "../providers/SlotContext"; @@ -60,86 +60,84 @@ function TransferSolana() { <>

Slot: {slot}

- - -
{ - let recipient = new Buffer(values["recipient"].slice(2), "hex"); + + { + let recipient = new Buffer(values["recipient"].slice(2), "hex"); - let transferAmount = new BN(values["amount"]).mul(new BN(10).pow(new BN(coinInfo.decimals))); - let fromAccount = new PublicKey(values["address"]) + let transferAmount = new BN(values["amount"]).mul(new BN(10).pow(new BN(coinInfo.decimals))); + let fromAccount = new PublicKey(values["address"]) - let send = async () => { - message.loading({content: "Transferring tokens...", key: "transfer"}, 1000) + let send = async () => { + message.loading({content: "Transferring tokens...", key: "transfer"}, 1000) - let lock_ix = await bridge.createLockAssetInstruction(k.publicKey, fromAccount, new PublicKey(coinInfo.mint), transferAmount, values["target_chain"], recipient, - { - chain: coinInfo.chainID, - address: coinInfo.wrappedAddress - }, 2); - let ix = spl.Token.createApproveInstruction(new PublicKey(TOKEN_PROGRAM), fromAccount, await bridge.getConfigKey(), k.publicKey, [], transferAmount.toNumber()) + let lock_ix = await bridge.createLockAssetInstruction(k.publicKey, fromAccount, new PublicKey(coinInfo.mint), transferAmount, values["target_chain"], recipient, + { + chain: coinInfo.chainID, + address: coinInfo.wrappedAddress + }, 2); + let ix = spl.Token.createApproveInstruction(TOKEN_PROGRAM, fromAccount, await bridge.getConfigKey(), k.publicKey, [], transferAmount.toNumber()) - let recentHash = await c.getRecentBlockhash(); - let tx = new Transaction(); - tx.recentBlockhash = recentHash.blockhash - tx.add(ix) - tx.add(lock_ix) - tx.sign(k) - try { - await c.sendTransaction(tx, [k]) - message.success({content: "Transfer succeeded", key: "transfer"}) - } catch (e) { - message.error({content: "Transfer failed", key: "transfer"}) + let recentHash = await c.getRecentBlockhash(); + let tx = new Transaction(); + tx.recentBlockhash = recentHash.blockhash + tx.add(ix) + tx.add(lock_ix) + tx.sign(k) + try { + await c.sendTransaction(tx, [k]) + message.success({content: "Transfer succeeded", key: "transfer"}) + } catch (e) { + message.error({content: "Transfer failed", key: "transfer"}) + } + } + send() + }}> + + { + setAddress(v.target.value) + }}/> + + { + let big = new BigNumber(value).mul(new BigNumber(10).pow(coinInfo.decimals)); + callback(big.lte(coinInfo.balance) ? undefined : "Amount exceeds balance") + } + }]}> + { + // @ts-ignore + setAmount(value || 0) + }}/> + + + + + { + if (value.length !== 42 || value.indexOf("0x") != 0) { + callback("Invalid address") + } else { + callback() } } - send() - }}> - - { - setAddress(v.target.value) - }}/> - - { - let big = new BigNumber(value).mul(new BigNumber(10).pow(coinInfo.decimals)); - callback(big.lte(coinInfo.balance) ? undefined : "Amount exceeds balance") - } - }]}> - { - // @ts-ignore - setAmount(value || 0) - }}/> - - - - - { - if (value.length !== 42 || value.indexOf("0x") != 0) { - callback("Invalid address") - } else { - callback() - } - } - },]}> - - - - - - -
+ },]}> + + + + + + diff --git a/web/src/providers/BridgeContext.tsx b/web/src/providers/BridgeContext.tsx index d631a28a..95b85784 100644 --- a/web/src/providers/BridgeContext.tsx +++ b/web/src/providers/BridgeContext.tsx @@ -4,12 +4,12 @@ import solanaWeb3, {Connection, PublicKey} from "@solana/web3.js"; import {SolanaBridge} from "../utils/bridge"; import {SOLANA_BRIDGE_PROGRAM, TOKEN_PROGRAM} from "../config"; -export const BridgeContext = createContext(new SolanaBridge(new Connection(""), new PublicKey(SOLANA_BRIDGE_PROGRAM), new PublicKey(TOKEN_PROGRAM))); +export const BridgeContext = createContext(new SolanaBridge(new Connection(""),SOLANA_BRIDGE_PROGRAM, TOKEN_PROGRAM)); export const BridgeProvider: FunctionComponent = ({children}) => { let c = useContext(ClientContext); - let bridge = new SolanaBridge(c, new PublicKey(SOLANA_BRIDGE_PROGRAM), new PublicKey(TOKEN_PROGRAM)) + let bridge = new SolanaBridge(c, SOLANA_BRIDGE_PROGRAM,TOKEN_PROGRAM) return ( diff --git a/web/src/providers/SolanaTokenContext.tsx b/web/src/providers/SolanaTokenContext.tsx index 7afac223..0b9ddc77 100644 --- a/web/src/providers/SolanaTokenContext.tsx +++ b/web/src/providers/SolanaTokenContext.tsx @@ -40,8 +40,7 @@ export const SolanaTokenProvider: FunctionComponent = ({children}) => { // @ts-ignore setLoading(true) let getAccounts = async () => { - try { - let res: RpcResponseAndContext }>> = await c.getParsedTokenAccountsByOwner(k.publicKey, {programId: new PublicKey(TOKEN_PROGRAM)}, "single") + let res: RpcResponseAndContext }>> = await c.getParsedTokenAccountsByOwner(k.publicKey, {programId: TOKEN_PROGRAM}, "single") let meta: AssetMeta[] = []; for (let acc of res.value) { let am = await b?.fetchAssetMeta(new PublicKey(acc.account.data.parsed.info.mint)) @@ -61,10 +60,7 @@ export const SolanaTokenProvider: FunctionComponent = ({children}) => { }) setBalances(balances) setLoading(false) - } catch (e) { - setLoading(false) - message.error("Failed to load token accounts") - } + } getAccounts(); }, diff --git a/web/src/utils/bridge.ts b/web/src/utils/bridge.ts index f2323f54..66422bff 100644 --- a/web/src/utils/bridge.ts +++ b/web/src/utils/bridge.ts @@ -4,6 +4,8 @@ import BN from 'bn.js'; import assert from "assert"; // @ts-ignore import * as BufferLayout from 'buffer-layout' +import {Token} from "@solana/spl-token"; +import {TOKEN_PROGRAM} from "../config"; export interface AssetMeta { chain: number, @@ -25,7 +27,6 @@ class SolanaBridge { async createWrappedAssetInstruction( payer: PublicKey, - amount: number | u64, asset: AssetMeta, ): Promise { const dataLayout = BufferLayout.struct([ @@ -36,12 +37,12 @@ class SolanaBridge { // @ts-ignore let configKey = await this.getConfigKey(); - let seeds: Array = [Buffer.from("wrapped"), configKey.toBuffer(), Buffer.of(asset.chain), + let seeds: Array = [Buffer.from("wrapped"), configKey.toBuffer(), Buffer.from([asset.chain]), padBuffer(asset.address, 32)]; // @ts-ignore let wrappedKey = (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0]; // @ts-ignore - let wrappedMetaKey = (await solanaWeb3.PublicKey.findProgramAddress([Buffer.from("wrapped"), this.configKey.toBuffer(), wrappedKey.toBuffer()], this.programID))[0]; + let wrappedMetaKey = (await solanaWeb3.PublicKey.findProgramAddress([Buffer.from("meta"), configKey.toBuffer(), wrappedKey.toBuffer()], this.programID))[0]; const data = Buffer.alloc(dataLayout.span); dataLayout.encode( @@ -54,6 +55,7 @@ class SolanaBridge { ); const keys = [ + {pubkey: this.programID, isSigner: false, isWritable: false}, {pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false}, {pubkey: this.tokenProgram, isSigner: false, isWritable: false}, {pubkey: configKey, isSigner: false, isWritable: false}, @@ -133,7 +135,6 @@ class SolanaBridge { keys.push({pubkey: custodyKey, isSigner: false, isWritable: true}) } - console.log(data) return new TransactionInstruction({ keys, @@ -171,10 +172,61 @@ class SolanaBridge { } } + AccountLayout = BufferLayout.struct([publicKey('mint'), publicKey('owner'), uint64('amount'), BufferLayout.u32('option'), publicKey('delegate'), BufferLayout.u8('is_initialized'), BufferLayout.u8('is_native'), BufferLayout.u16('padding'), uint64('delegatedAmount')]); + + async createWrappedAssetAndAccountInstructions(owner: PublicKey, mint: PublicKey): Promise<[TransactionInstruction[], solanaWeb3.Account]> { + const newAccount = new solanaWeb3.Account(); + + // @ts-ignore + const balanceNeeded = await Token.getMinBalanceRentForExemptAccount(this.connection); + let transaction = solanaWeb3.SystemProgram.createAccount({ + fromPubkey: owner, + newAccountPubkey: newAccount.publicKey, + lamports: balanceNeeded, + space: this.AccountLayout.span, + programId: TOKEN_PROGRAM, + }); // create the new account + + const keys = [{ + pubkey: newAccount.publicKey, + isSigner: false, + isWritable: true + }, { + pubkey: mint, + isSigner: false, + isWritable: false + }, { + pubkey: owner, + isSigner: false, + isWritable: false + }]; + const dataLayout = BufferLayout.struct([BufferLayout.u8('instruction')]); + const data = Buffer.alloc(dataLayout.span); + dataLayout.encode({ + instruction: 1 // InitializeAccount instruction + + }, data); + let ix_init = { + keys, + programId: TOKEN_PROGRAM, + data + } + + return [[transaction.instructions[0], ix_init], newAccount] + } + async getConfigKey(): Promise { // @ts-ignore return (await solanaWeb3.PublicKey.findProgramAddress([Buffer.from("bridge")], this.programID))[0] } + + async getWrappedAssetMint(asset: AssetMeta): Promise { + let configKey = await this.getConfigKey(); + let seeds: Array = [Buffer.from("wrapped"), configKey.toBuffer(), Buffer.of(asset.chain), + padBuffer(asset.address, 32)]; + // @ts-ignore + return (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0]; + } } // Taken from https://github.com/solana-labs/solana-program-library @@ -215,7 +267,6 @@ export class u64 extends BN { function padBuffer(b: Buffer, len: number): Buffer { const zeroPad = Buffer.alloc(len); - console.log(len - b.length) b.copy(zeroPad, len - b.length); return zeroPad; }