WIP SPL token integration

This commit is contained in:
Hendrik Hofstadt 2020-08-14 21:33:46 +02:00
parent 0e69aa4ddc
commit ca4e4a3243
13 changed files with 603 additions and 108 deletions

122
web/package-lock.json generated
View File

@ -1670,9 +1670,9 @@
}
},
"@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==",
"version": "0.70.3",
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-0.70.3.tgz",
"integrity": "sha512-Q9byc2doeycUHai47IVkdM2DAWhpnV+K7OPxNivTph1a7bDRcULp0n9X7QTycJCz5E9+qx3KUduD1J2Xk/zH0Q==",
"requires": {
"@babel/runtime": "^7.3.1",
"bn.js": "^5.0.0",
@ -1691,15 +1691,6 @@
"ws": "^7.0.0"
},
"dependencies": {
"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",
@ -2043,6 +2034,11 @@
"@types/node": "*"
}
},
"@types/history": {
"version": "4.7.7",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.7.tgz",
"integrity": "sha512-2xtoL22/3Mv6a70i4+4RB7VgbDDORoWwjcqeNysojZA0R7NK17RbY5Gof/2QiFfJgX+KkWghbwJ+d/2SB8Ndzg=="
},
"@types/istanbul-lib-coverage": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz",
@ -2158,6 +2154,25 @@
"@types/react": "*"
}
},
"@types/react-router": {
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.8.tgz",
"integrity": "sha512-HzOyJb+wFmyEhyfp4D4NYrumi+LQgQL/68HvJO+q6XtuHSDvw6Aqov7sCAhjbNq3bUPgPqbdvjXC5HeB2oEAPg==",
"requires": {
"@types/history": "*",
"@types/react": "*"
}
},
"@types/react-router-dom": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.5.tgz",
"integrity": "sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw==",
"requires": {
"@types/history": "*",
"@types/react": "*",
"@types/react-router": "*"
}
},
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
@ -7438,6 +7453,19 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -9576,6 +9604,15 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
},
"mini-create-react-context": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz",
"integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==",
"requires": {
"@babel/runtime": "^7.5.5",
"tiny-warning": "^1.0.3"
}
},
"mini-css-extract-plugin": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz",
@ -16102,6 +16139,52 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
}
}
}
},
"react-router-dom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-scripts": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.4.1.tgz",
@ -16469,6 +16552,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
},
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@ -18182,6 +18270,11 @@
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tinycolor2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
@ -18691,6 +18784,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"varint": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",

View File

@ -10,6 +10,7 @@
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-router-dom": "^5.1.5",
"antd": "^4.4.1",
"ethers": "^4.0.44",
"react": "^16.13.1",
@ -17,10 +18,11 @@
"react-scripts": "3.4.1",
"typescript": "~3.7.2",
"web3": "^1.2.9",
"@solana/web3.js": "^0.66.3",
"@solana/web3.js": "^0.70.3",
"@solana/spl-token": "^0.0.5",
"buffer-layout": "^1.2.0",
"buffer": "^5.6.0"
"buffer": "^5.6.0",
"react-router-dom": "^5.2.0"
},
"devDependencies": {
"npm": "^6.14.6",

View File

@ -4,6 +4,10 @@ import * as solanaWeb3 from '@solana/web3.js';
import ClientContext from '../providers/ClientContext';
import Transfer from "../pages/Transfer";
import {Layout} from 'antd';
import {SolanaTokenProvider} from "../providers/SolanaTokenContext";
import {SlotProvider} from "../providers/SlotContext";
import {BrowserRouter as Router, Link, Route, Switch} from 'react-router-dom';
import TransferSolana from "../pages/TransferSolana";
const {Header, Content, Footer} = Layout;
@ -12,18 +16,33 @@ function App() {
return (
<div className="App">
<Layout style={{height: '100%'}}>
<Header style={{position: 'fixed', zIndex: 1, width: '100%'}}>
<div className="logo"/>
</Header>
<Content style={{padding: '0 50px', marginTop: 64}}>
<div style={{padding: 24}}>
<ClientContext.Provider value={c}>
<Transfer/>
</ClientContext.Provider>
</div>
</Content>
<Footer style={{textAlign: 'center'}}>nexantic GmbH 2020</Footer>
</Layout>,
<Router>
<Header style={{position: 'fixed', zIndex: 1, width: '100%'}}>
<Link to="/" style={{paddingRight: 20}}>Ethereum</Link>
<Link to="/solana">Solana</Link>
<div className="logo"/>
</Header>
<Content style={{padding: '0 50px', marginTop: 64}}>
<div style={{padding: 24}}>
<ClientContext.Provider value={c}>
<SlotProvider>
<SolanaTokenProvider>
<Switch>
<Route path="/solana">
<TransferSolana/>
</Route>
<Route path="/">
<Transfer/>
</Route>
</Switch>
</SolanaTokenProvider>
</SlotProvider>
</ClientContext.Provider>
</div>
</Content>
<Footer style={{textAlign: 'center'}}>nexantic GmbH 2020</Footer>
</Router>
</Layout>
</div>
);
}

View File

@ -0,0 +1,48 @@
import React, {useContext} from "react"
import {BalanceInfo, SolanaTokenContext} from "../providers/SolanaTokenContext";
import {Table} from "antd";
function SplBalances() {
let t = useContext(SolanaTokenContext);
const dataSource = [
{
key: '1',
name: 'Mike',
age: 32,
address: '10 Downing Street',
},
{
key: '2',
name: 'John',
age: 42,
address: '10 Downing Street',
},
];
const columns = [
{
title: 'Mint',
dataIndex: 'mint',
key: 'mint',
},
{
title: 'Account',
key: 'account',
render: (n: any, v: BalanceInfo) => v.account.toString()
},
{
title: 'Balance',
key: 'balance',
render: (n: any, v: BalanceInfo) => v.balance.div(Math.pow(10, v.decimals)).toString()
},
];
return (<>
<h3>SPL Holdings</h3>
<Table dataSource={t.balances} columns={columns} pagination={false} scroll={{y: 400}}/>
</>
)
}
export default SplBalances

11
web/src/config.ts Normal file
View File

@ -0,0 +1,11 @@
const BRIDGE_ADDRESS = "0xac3eB48829fFC3C37437ce4459cE63F1F4d4E0b4";
const SOLANA_BRIDGE_PROGRAM = "TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
const TOKEN_PROGRAM = "TokenSVp5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
export {
BRIDGE_ADDRESS,
TOKEN_PROGRAM,
SOLANA_BRIDGE_PROGRAM
}

View File

@ -2,13 +2,16 @@ 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 {Button, Form, Input, InputNumber, message, Select, Space} from "antd";
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";
// @ts-ignore
@ -21,7 +24,7 @@ async function lockAssets(asset: string,
amount: BigNumberish,
recipient: Arrayish,
target_chain: BigNumberish) {
let wh = WormholeFactory.connect("0xac3eB48829fFC3C37437ce4459cE63F1F4d4E0b4", signer);
let wh = WormholeFactory.connect(BRIDGE_ADDRESS, signer);
try {
message.loading({content: "Signing transaction...", key: "eth_tx", duration: 1000},)
let res = await wh.lockAssets(asset, amount, recipient, target_chain)
@ -38,7 +41,7 @@ async function approveAssets(asset: string,
let e = Erc20Factory.connect(asset, signer);
try {
message.loading({content: "Signing transaction...", key: "eth_tx", duration: 1000})
let res = await e.approve("0xac3eB48829fFC3C37437ce4459cE63F1F4d4E0b4", amount)
let res = await e.approve(BRIDGE_ADDRESS, 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"})
@ -49,13 +52,7 @@ async function approveAssets(asset: string,
function Transfer() {
let c = useContext<solanaWeb3.Connection>(ClientContext);
let [slot, setSlot] = useState(0);
useEffect(() => {
c.onSlotChange(value => {
setSlot(value.slot);
});
})
let slot = useContext(SlotContext);
let [coinInfo, setCoinInfo] = useState({
balance: new BigNumber(0),
@ -73,22 +70,13 @@ function Transfer() {
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 allowance = await e.allowance(addr, BRIDGE_ADDRESS);
let info = {
balance: balance.div(new BigNumber(10).pow(decimals)),
@ -99,7 +87,7 @@ function Transfer() {
wrappedAddress: ""
}
let b = WormholeFactory.connect("0xac3eB48829fFC3C37437ce4459cE63F1F4d4E0b4", provider);
let b = WormholeFactory.connect(BRIDGE_ADDRESS, provider);
let isWrapped = await b.isWrappedAsset(token)
if (isWrapped) {
@ -117,60 +105,70 @@ function Transfer() {
return (
<>
<p>Slot: {slot}</p>
<Space>
<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");
<Row>
<Col>
<Space>
<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)
}
}
},]}>
<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>
}}>
<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>
</Col>
<Col>
<SplBalances/>
</Col>
</Row>
</>
);
}

View File

@ -0,0 +1,107 @@
import React, {useContext, useEffect, useState} from 'react';
import ClientContext from "../providers/ClientContext";
import * as solanaWeb3 from '@solana/web3.js';
import {Button, Col, Form, Input, InputNumber, Row, Select, Space} from "antd";
import {BigNumber} from "ethers/utils";
import SplBalances from "../components/SplBalances";
import {SlotContext} from "../providers/SlotContext";
import {SolanaTokenContext} from "../providers/SolanaTokenContext";
function TransferSolana() {
let c = useContext<solanaWeb3.Connection>(ClientContext);
let slot = useContext(SlotContext);
let b = useContext(SolanaTokenContext);
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 getCoinInfo(): Promise<BigNumber> {
let acc = b.balances.find(value => value.account.toString() == address)
if (!acc) {
return new BigNumber(0)
}
return acc.balance
}
}, [address])
return (
<>
<p>Slot: {slot}</p>
<Row>
<Col>
<Space>
<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={"Token account Pubkey"}
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={2}>
Ethereum
</Select.Option>
</Select>
</Form.Item>
<Form.Item name="recipient" rules={[{
required: true,
validator: (rule, value, callback) => {
if (value.length !== 42 || value.indexOf("0x") != 0) {
callback("Invalid address")
} else {
callback()
}
}
},]}>
<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>
</Col>
<Col>
<SplBalances/>
</Col>
</Row>
</>
);
}
export default TransferSolana;

View File

@ -0,0 +1,20 @@
import React, {createContext, FunctionComponent, useContext} from "react"
import ClientContext from "../providers/ClientContext";
import solanaWeb3, {PublicKey} from "@solana/web3.js";
import {SolanaBridge} from "../utils/bridge";
import {SOLANA_BRIDGE_PROGRAM, TOKEN_PROGRAM} from "../config";
// TODO
export const BridgeContext = createContext<SolanaBridge>()
export const BridgeProvider: FunctionComponent = ({children}) => {
let c = useContext<solanaWeb3.Connection>(ClientContext);
let bridge = new SolanaBridge(c, new PublicKey(SOLANA_BRIDGE_PROGRAM), new PublicKey(TOKEN_PROGRAM))
return (
<BridgeContext.Provider value={bridge}>
{children}
</BridgeContext.Provider>
)
}

View File

@ -0,0 +1,7 @@
import React from 'react'
import * as solanaWeb3 from '@solana/web3.js';
import {Account} from "@solana/web3.js";
const KeyContext = React.createContext<Account>(new Account([97,215,234,123,197,228,56,3,210,182,139,102,127,246,235,213,211,40,93,149,16,226,130,1,29,196,87,105,185,115,179,53,123,232,195,48,5,229,144,176,217,8,1,27,185,162,160,157,137,210,99,173,135,148,20,232,241,43,238,229,1,61,122,183]));
export default KeyContext

View File

@ -0,0 +1,22 @@
import React, {createContext, FunctionComponent, useContext, useEffect, useState} from "react"
import ClientContext from "../providers/ClientContext";
import solanaWeb3 from "@solana/web3.js";
export const SlotContext = createContext(0)
export const SlotProvider: FunctionComponent = ({children}) => {
let c = useContext<solanaWeb3.Connection>(ClientContext);
let [slot, setSlot] = useState(0);
useEffect(() => {
c.onSlotChange(value => {
setSlot(value.slot);
});
})
return (
<SlotContext.Provider value={slot}>
{children}
</SlotContext.Provider>
)
}

View File

@ -0,0 +1,60 @@
import React, {createContext, FunctionComponent, useContext, useEffect, useState} from "react"
import ClientContext from "../providers/ClientContext";
import KeyContext from "../providers/KeyContext";
import {AccountInfo, ParsedAccountData, PublicKey, RpcResponseAndContext} from "@solana/web3.js";
import {message} from "antd";
import {BigNumber} from "ethers/utils";
import {SlotContext} from "./SlotContext";
import {TOKEN_PROGRAM} from "../config";
export interface BalanceInfo {
mint: string,
account: PublicKey,
balance: BigNumber,
decimals: number
}
export interface TokenInfo {
balances: Array<BalanceInfo>
loading: boolean
}
export const SolanaTokenContext = createContext<TokenInfo>({
balances: [],
loading: false
})
export const SolanaTokenProvider: FunctionComponent = ({children}) => {
let k = useContext(KeyContext)
let c = useContext(ClientContext);
let slot = useContext(SlotContext);
let [loading, setLoading] = useState(true)
let [accounts, setAccounts] = useState<Array<{ pubkey: PublicKey; account: AccountInfo<ParsedAccountData> }>>([]);
useEffect(() => {
// @ts-ignore
setLoading(true)
c.getParsedTokenAccountsByOwner(k.publicKey, {programId: new PublicKey(TOKEN_PROGRAM)},"single").then((res: RpcResponseAndContext<Array<{ pubkey: PublicKey; account: AccountInfo<ParsedAccountData> }>>) => {
setAccounts(res.value)
setLoading(false)
}).catch(() => {
setLoading(false)
message.error("Failed to load token accounts")
})
}, [slot])
let balances: Array<BalanceInfo> = accounts.map((v) => {
return {
mint: v.account.data.parsed.info.mint,
account: v.pubkey,
balance: new BigNumber(v.account.data.parsed.info.tokenAmount.amount),
decimals: v.account.data.parsed.info.tokenAmount.decimals
}
})
return (
<SolanaTokenContext.Provider value={{balances, loading}}>
{children}
</SolanaTokenContext.Provider>
)
}

View File

@ -10,18 +10,20 @@ export interface AssetMeta {
address: Buffer
}
const CHAIN_ID_SOLANA = 1;
class SolanaBridge {
connection: solanaWeb3.Connection;
programID: PublicKey;
configKey: PublicKey;
tokenProgram: PublicKey;
constructor(programID: PublicKey, configKey: PublicKey, tokenProgram: PublicKey) {
constructor(connection: solanaWeb3.Connection, programID: PublicKey, tokenProgram: PublicKey) {
this.programID = programID;
this.configKey = configKey;
this.tokenProgram = tokenProgram;
this.connection = connection;
}
async createWrappedAsset(
async createWrappedAssetInstruction(
payer: PublicKey,
amount: number | u64,
asset: AssetMeta,
@ -32,17 +34,19 @@ class SolanaBridge {
BufferLayout.u8('chain'),
]);
let seeds: Array<Buffer> = [Buffer.from("wrapped"), this.configKey.toBuffer(), Buffer.of(asset.chain),
// @ts-ignore
let configKey = (await solanaWeb3.PublicKey.findProgramAddress([Buffer.from("bridge"), this.programID.toBuffer()], this.programID))[0];
let seeds: Array<Buffer> = [Buffer.from("wrapped"), 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];
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
instruction: 5, // CreateWrapped instruction
address: asset.address,
chain: asset.chain,
},
@ -52,7 +56,7 @@ class SolanaBridge {
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: configKey, isSigner: false, isWritable: false},
{pubkey: payer, isSigner: true, isWritable: true},
{pubkey: wrappedKey, isSigner: false, isWritable: true},
{pubkey: wrappedMetaKey, isSigner: false, isWritable: true},
@ -64,6 +68,105 @@ class SolanaBridge {
});
}
async createLockAssetInstruction(
payer: PublicKey,
tokenAccount: PublicKey,
mint: PublicKey,
amount: number | u64,
targetChain: number,
targetAddress: Buffer,
asset: AssetMeta,
nonce: number,
): Promise<TransactionInstruction> {
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
uint256('amount'),
BufferLayout.u8('targetChain'),
BufferLayout.blob(32, 'assetAddress'),
BufferLayout.u8('assetChain'),
BufferLayout.blob(32, 'targetAddress'),
BufferLayout.u32('nonce'),
]);
let nonceBuffer = Buffer.alloc(4);
nonceBuffer.writeUInt32BE(nonce, 0);
// @ts-ignore
let configKey = (await solanaWeb3.PublicKey.findProgramAddress([Buffer.from("bridge"), this.programID.toBuffer()], this.programID))[0];
let seeds: Array<Buffer> = [Buffer.from("transfer"), configKey.toBuffer(), Buffer.of(asset.chain),
asset.address, Buffer.of(targetChain), targetAddress, tokenAccount.toBuffer(),
nonceBuffer,
];
// @ts-ignore
let transferKey = (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0];
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: 1, // TransferOut instruction
amount: amount,
targetChain: targetChain,
assetAddress: asset.address,
assetChain: asset.chain,
targetAddress: targetAddress,
nonce: nonce,
},
data,
);
const keys = [
{pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false},
{pubkey: this.tokenProgram, isSigner: false, isWritable: false},
{pubkey: tokenAccount, isSigner: false, isWritable: true},
{pubkey: configKey, isSigner: false, isWritable: false},
{pubkey: transferKey, isSigner: false, isWritable: true},
{pubkey: mint, isSigner: false, isWritable: false},
{pubkey: payer, isSigner: true, isWritable: false},
];
//TODO replace chainID
if (asset.chain == CHAIN_ID_SOLANA) {
// @ts-ignore
let custodyKey = (await solanaWeb3.PublicKey.findProgramAddress([Buffer.from("custody"), this.configKey.toBuffer(), mint.toBuffer()], this.programID))[0];
keys.push({pubkey: custodyKey, isSigner: false, isWritable: true})
}
return new TransactionInstruction({
keys,
programId: this.programID,
data,
});
}
// fetchAssetMeta fetches the AssetMeta for an SPL token
async fetchAssetMeta(
mint: PublicKey,
): Promise<AssetMeta> {
// @ts-ignore
let configKey = (await solanaWeb3.PublicKey.findProgramAddress([Buffer.from("bridge"), this.programID.toBuffer()], this.programID))[0];
let seeds: Array<Buffer> = [Buffer.from("meta"), configKey.toBuffer(), mint.toBuffer()];
// @ts-ignore
let metaKey = (await solanaWeb3.PublicKey.findProgramAddress(seeds, this.programID))[0];
let metaInfo = await this.connection.getAccountInfo(metaKey);
if (metaInfo == null || metaInfo.lamports == 0) {
return {
address: mint.toBuffer(),
chain: CHAIN_ID_SOLANA,
}
} else {
const dataLayout = BufferLayout.struct([
BufferLayout.blob(32, 'assetAddress'),
BufferLayout.u8('assetChain'),
]);
let wrappedMeta = dataLayout.decode(metaInfo?.data);
return {
address: wrappedMeta.assetAddress,
chain: wrappedMeta.assetChain
}
}
}
}
// Taken from https://github.com/solana-labs/solana-program-library

View File