diff --git a/web/package-lock.json b/web/package-lock.json index d049c409..531034ca 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1613,10 +1613,66 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" }, + "@solana/spl-token": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.0.5.tgz", + "integrity": "sha512-OXW/zHzMQqVGcSNrNt8sRaHlKT5vjdcUcmUHi8d4ssG8ChbZVA2lkJK10XDXlcnMIiSTindpEjiFmooYc9K3uQ==", + "requires": { + "@babel/runtime": "^7.10.5", + "@solana/web3.js": "^0.64.0", + "bn.js": "^5.0.0", + "buffer-layout": "^1.2.0", + "dotenv": "8.2.0", + "mkdirp-promise": "^5.0.1" + }, + "dependencies": { + "@solana/web3.js": { + "version": "0.64.3", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.64.3.tgz", + "integrity": "sha512-a20CbWzsA/cRQiPeuwwv21YfcFyHxywpQzAuGmRU7jVv905YzeNgkJ0CxZfN1IZdlgU1SWcFCQVv+DXXCkgWmQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "bn.js": "^5.0.0", + "bs58": "^4.0.1", + "buffer": "^5.4.3", + "buffer-layout": "^1.2.0", + "crypto-hash": "^1.2.2", + "esdoc-inject-style-plugin": "^1.0.0", + "jayson": "^3.0.1", + "mz": "^2.7.0", + "node-fetch": "^2.2.0", + "npm-run-all": "^4.1.5", + "rpc-websockets": "^5.0.8", + "superstruct": "^0.8.3", + "tweetnacl": "^1.0.0", + "ws": "^7.0.0" + } + }, + "buffer": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" + } + } + }, "@solana/web3.js": { - "version": "0.66.1", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.66.1.tgz", - "integrity": "sha512-AorappmEktL8k0wgJ8nlxbdM3YG+LeeSBBUZUtk+JA2uiRh5pFexsvvViTuTHuzYQbdp66JJyCLc2YcMz8LwEw==", + "version": "0.66.3", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.66.3.tgz", + "integrity": "sha512-HYM1z9E6qVZKEHoLAn5tvL4SioruNB/mvNQhW2v7Va1WbhFArMIEh24SPC23LuEFZVe3PKwlT47zzUhdyua8tQ==", "requires": { "@babel/runtime": "^7.3.1", "bn.js": "^5.0.0", @@ -3722,13 +3778,12 @@ } }, "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", + "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", "requires": { "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" + "ieee754": "^1.1.4" } }, "buffer-from": { @@ -9939,6 +9994,16 @@ "vm-browserify": "^1.0.1" }, "dependencies": { + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", diff --git a/web/package.json b/web/package.json index 7e1518d4..eb1cbc76 100644 --- a/web/package.json +++ b/web/package.json @@ -17,7 +17,10 @@ "react-scripts": "3.4.1", "typescript": "~3.7.2", "web3": "^1.2.9", - "@solana/web3.js": "^0.66.1" + "@solana/web3.js": "^0.66.3", + "@solana/spl-token": "^0.0.5", + "buffer-layout": "^1.2.0", + "buffer": "^5.6.0" }, "devDependencies": { "npm": "^6.14.6", diff --git a/web/src/pages/Transfer.tsx b/web/src/pages/Transfer.tsx index 3d190a5e..a7b48440 100644 --- a/web/src/pages/Transfer.tsx +++ b/web/src/pages/Transfer.tsx @@ -1,10 +1,14 @@ import React, {useContext, useEffect, useState} from 'react'; import ClientContext from "../providers/ClientContext"; import * as solanaWeb3 from '@solana/web3.js'; -import {Button, Input, InputNumber, Space} from "antd"; +import {PublicKey} from '@solana/web3.js'; +import {Button, Form, Input, InputNumber, message, Select, Space} from "antd"; import {ethers} from "ethers"; import {Erc20Factory} from "../contracts/Erc20Factory"; -import {BigNumber} from "ethers/utils"; +import {Arrayish, BigNumber, BigNumberish} from "ethers/utils"; +import {WormholeFactory} from "../contracts/WormholeFactory"; +import {WrappedAssetFactory} from "../contracts/WrappedAssetFactory"; +import {SolanaBridge} from "../utils/bridge"; // @ts-ignore @@ -13,43 +17,159 @@ window.ethereum.enable(); const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner(); +async function lockAssets(asset: string, + amount: BigNumberish, + recipient: Arrayish, + target_chain: BigNumberish) { + let wh = WormholeFactory.connect("0xac3eB48829fFC3C37437ce4459cE63F1F4d4E0b4", signer); + try { + message.loading({content: "Signing transaction...", key: "eth_tx", duration: 1000},) + let res = await wh.lockAssets(asset, amount, recipient, target_chain) + message.loading({content: "Waiting for transaction to be mined...", key: "eth_tx", duration: 1000}) + await res.wait(1); + message.success({content: "Transfer on ETH succeeded!", key: "eth_tx"}) + } catch (e) { + message.error({content: "Transfer failed", key: "eth_tx"}) + } +} + +async function approveAssets(asset: string, + amount: BigNumberish) { + let e = Erc20Factory.connect(asset, signer); + try { + message.loading({content: "Signing transaction...", key: "eth_tx", duration: 1000}) + let res = await e.approve("0xac3eB48829fFC3C37437ce4459cE63F1F4d4E0b4", amount) + message.loading({content: "Waiting for transaction to be mined...", key: "eth_tx", duration: 1000}) + await res.wait(1); + message.success({content: "Approval on ETH succeeded!", key: "eth_tx"}) + } catch (e) { + message.error({content: "Approval failed", key: "eth_tx"}) + } +} + function Transfer() { let c = useContext(ClientContext); - let [token, setToken] = useState(""); - let [balance, setBalance] = useState("0"); let [slot, setSlot] = useState(0); useEffect(() => { c.onSlotChange(value => { setSlot(value.slot); }); }) - useEffect(() => { - async function fetchBalance(){ - let e = Erc20Factory.connect(token, provider); - try { - let addr = await signer.getAddress(); - let balance = await e.balanceOf(addr); - let decimals = await e.decimals(); - setBalance(balance.div(new BigNumber(10).pow(decimals)).toString()); - }catch (e) { + let [coinInfo, setCoinInfo] = useState({ + balance: new BigNumber(0), + decimals: 0, + allowance: new BigNumber(0), + isWrapped: false, + chainID: 0, + wrappedAddress: "" + }); + let [amount, setAmount] = useState(0); + let [address, setAddress] = useState(""); + let [addressValid, setAddressValid] = useState(false) + + useEffect(() => { + fetchBalance(address) + }, [address]) + + + + async function fetchBalance(token: string) { + let p = new SolanaBridge(new PublicKey("FHbUryAag7ZfkFKbaCZaqWYsRgEtu7EWFrniy3VQ9Z3w"), new PublicKey("FHbUryAag7ZfkFKbaCZaqWYsRgEtu7EWFrniy3VQ9Z3w"), new PublicKey("FHbUryAag7ZfkFKbaCZaqWYsRgEtu7EWFrniy3VQ9Z3w")) + console.log(p.programID.toBuffer()) + console.log(await p.createWrappedAsset(new PublicKey("FHbUryAag7ZfkFKbaCZaqWYsRgEtu7EWFrniy3VQ9Z3w"), 2000, { + chain: 200, + + address: Buffer.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1), + })) + try { + let e = WrappedAssetFactory.connect(token, provider); + let addr = await signer.getAddress(); + let balance = await e.balanceOf(addr); + let decimals = await e.decimals(); + let allowance = await e.allowance(addr, "0xac3eB48829fFC3C37437ce4459cE63F1F4d4E0b4"); + + let info = { + balance: balance.div(new BigNumber(10).pow(decimals)), + allowance: allowance.div(new BigNumber(10).pow(decimals)), + decimals: decimals, + isWrapped: false, + chainID: 0, + wrappedAddress: "" } + + let b = WormholeFactory.connect("0xac3eB48829fFC3C37437ce4459cE63F1F4d4E0b4", provider); + + let isWrapped = await b.isWrappedAsset(token) + if (isWrapped) { + info.chainID = await e.assetChain() + info.wrappedAddress = await e.assetAddress() + info.isWrapped = true + } + setCoinInfo(info) + setAddressValid(true) + } catch (e) { + setAddressValid(false) } - fetchBalance(); - }, [token]) + } + return ( <>

Slot: {slot}

- - { - setToken(e.target.value); - }}/> - - - +
{ + 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) + } + }}> + + { + 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"); + } + } + },]}> + + + + + +
); diff --git a/web/src/utils/bridge.ts b/web/src/utils/bridge.ts new file mode 100644 index 00000000..5ca1630e --- /dev/null +++ b/web/src/utils/bridge.ts @@ -0,0 +1,156 @@ +import * as solanaWeb3 from "@solana/web3.js"; +import {PublicKey, TransactionInstruction} from "@solana/web3.js"; +import BN from 'bn.js'; +import assert from "assert"; +// @ts-ignore +import * as BufferLayout from 'buffer-layout'; + +export interface AssetMeta { + chain: number, + address: Buffer +} + +class SolanaBridge { + programID: PublicKey; + configKey: PublicKey; + tokenProgram: PublicKey; + + constructor(programID: PublicKey, configKey: PublicKey, tokenProgram: PublicKey) { + this.programID = programID; + this.configKey = configKey; + this.tokenProgram = tokenProgram; + } + + async createWrappedAsset( + payer: PublicKey, + amount: number | u64, + asset: AssetMeta, + ): Promise { + const dataLayout = BufferLayout.struct([ + BufferLayout.u8('instruction'), + BufferLayout.blob(32, 'address'), + BufferLayout.u8('chain'), + ]); + + let seeds: Array = [Buffer.from("wrapped"), this.configKey.toBuffer(), Buffer.of(asset.chain), + asset.address]; + // @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]; + + const data = Buffer.alloc(dataLayout.span); + dataLayout.encode( + { + instruction: 1, // Swap instruction + address: asset.address, + chain: asset.chain, + }, + data, + ); + + const keys = [ + {pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false}, + {pubkey: this.tokenProgram, isSigner: false, isWritable: false}, + {pubkey: this.configKey, isSigner: false, isWritable: false}, + {pubkey: payer, isSigner: true, isWritable: true}, + {pubkey: wrappedKey, isSigner: false, isWritable: true}, + {pubkey: wrappedMetaKey, isSigner: false, isWritable: true}, + ]; + return new TransactionInstruction({ + keys, + programId: this.programID, + data, + }); + } + +} + +// Taken from https://github.com/solana-labs/solana-program-library +// Licensed under Apache 2.0 + +export class u64 extends BN { + /** + * Convert to Buffer representation + */ + toBuffer(): Buffer { + const a = super.toArray().reverse(); + const b = Buffer.from(a); + if (b.length === 8) { + return b; + } + assert(b.length < 8, 'u64 too large'); + + const zeroPad = Buffer.alloc(8); + b.copy(zeroPad); + return zeroPad; + } + + /** + * Construct a u64 from Buffer representation + */ + static fromBuffer(buffer: Buffer): u64 { + assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`); + return new BN( + // @ts-ignore + [...buffer] + .reverse() + .map(i => `00${i.toString(16)}`.slice(-2)) + .join(''), + 16, + ); + } +} + +/** + * Layout for a public key + */ +export const publicKey = (property: string = 'publicKey'): Object => { + return BufferLayout.blob(32, property); +}; + +/** + * Layout for a 64bit unsigned value + */ +export const uint64 = (property: string = 'uint64'): Object => { + return BufferLayout.blob(8, property); +}; + +/** + * Layout for a 256-bit unsigned value + */ +export const uint256 = (property: string = 'uint256'): Object => { + return BufferLayout.blob(32, property); +}; + +/** + * Layout for a Rust String type + */ +export const rustString = (property: string = 'string') => { + const rsl = BufferLayout.struct( + [ + BufferLayout.u32('length'), + BufferLayout.u32('lengthPadding'), + BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), 'chars'), + ], + property, + ); + const _decode = rsl.decode.bind(rsl); + const _encode = rsl.encode.bind(rsl); + + rsl.decode = (buffer: Buffer, offset: number) => { + const data = _decode(buffer, offset); + return data.chars.toString('utf8'); + }; + + rsl.encode = (str: string, buffer: Buffer, offset: number) => { + const data = { + chars: Buffer.from(str, 'utf8'), + }; + return _encode(data, buffer, offset); + }; + + return rsl; +}; + +export {SolanaBridge}