basic ETH interaction; WIP Solana interaction

This commit is contained in:
Hendrik Hofstadt 2020-08-10 00:01:18 +02:00
parent 70a1f24220
commit 47464d7600
4 changed files with 376 additions and 32 deletions

81
web/package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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<solanaWeb3.Connection>(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 (
<>
<p>Slot: {slot}</p>
<Space>
<Input.Group>
<Input addonAfter={`Balance: ${balance}`} name={"abc"} placeholder={"ERC20 address"}
onChange={(e) => {
setToken(e.target.value);
}}/>
<InputNumber name={"amount"} placeholder={"Amount"} type={"number"}/>
</Input.Group>
<Button type="primary">Transfer</Button>
<Form onFinish={(values) => {
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)
}
}}>
<Form.Item name="address" validateStatus={addressValid ? "success" : "error"}>
<Input addonAfter={`Balance: ${coinInfo.balance}`} name="address" placeholder={"ERC20 address"}
onBlur={(v) => {
setAddress(v.target.value)
}}/>
</Form.Item>
<Form.Item name="amount" rules={[{
required: true, validator: (rule, value, callback) => {
let big = new BigNumber(value);
callback(big.lte(coinInfo.balance) ? undefined : "Amount exceeds balance")
}
}]}>
<InputNumber name={"amount"} placeholder={"Amount"} type={"number"} onChange={value => {
// @ts-ignore
setAmount(value || 0)
}}/>
</Form.Item>
<Form.Item name="target_chain" rules={[{required: true, message: "Please choose a target chain"}]}>
<Select placeholder="Target Chain">
<Select.Option value={1}>
Solana
</Select.Option>
</Select>
</Form.Item>
<Form.Item name="recipient" rules={[{
required: true,
validator: (rule, value, callback) => {
try {
new solanaWeb3.PublicKey(value);
callback();
} catch (e) {
callback("Not a valid Solana address");
}
}
},]}>
<Input name="recipient" placeholder={"Address of the recipient"}/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
{coinInfo.allowance.toNumber() >= amount || coinInfo.isWrapped ? "Transfer" : "Approve"}
</Button>
</Form.Item>
</Form>
</Space>
</>
);

156
web/src/utils/bridge.ts Normal file
View File

@ -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<TransactionInstruction> {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
BufferLayout.blob(32, 'address'),
BufferLayout.u8('chain'),
]);
let seeds: Array<Buffer> = [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}