basic ETH interaction; WIP Solana interaction
This commit is contained in:
parent
70a1f24220
commit
47464d7600
|
@ -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.66.1",
|
||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.66.1.tgz",
|
||||
"integrity": "sha512-AorappmEktL8k0wgJ8nlxbdM3YG+LeeSBBUZUtk+JA2uiRh5pFexsvvViTuTHuzYQbdp66JJyCLc2YcMz8LwEw==",
|
||||
"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.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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
})
|
||||
|
||||
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(() => {
|
||||
async function fetchBalance(){
|
||||
let e = Erc20Factory.connect(token, provider);
|
||||
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();
|
||||
setBalance(balance.div(new BigNumber(10).pow(decimals)).toString());
|
||||
}catch (e) {
|
||||
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);
|
||||
<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)
|
||||
}}/>
|
||||
<InputNumber name={"amount"} placeholder={"Amount"} type={"number"}/>
|
||||
</Input.Group>
|
||||
<Button type="primary">Transfer</Button>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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}
|
Loading…
Reference in New Issue