WIP SPL token integration
This commit is contained in:
parent
0e69aa4ddc
commit
ca4e4a3243
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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%'}}>
|
||||
<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>
|
||||
</Layout>,
|
||||
</Router>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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,6 +105,8 @@ function Transfer() {
|
|||
return (
|
||||
<>
|
||||
<p>Slot: {slot}</p>
|
||||
<Row>
|
||||
<Col>
|
||||
<Space>
|
||||
<Form onFinish={(values) => {
|
||||
let recipient = new solanaWeb3.PublicKey(values["recipient"]).toBuffer()
|
||||
|
@ -128,7 +118,8 @@ function Transfer() {
|
|||
}
|
||||
}}>
|
||||
<Form.Item name="address" validateStatus={addressValid ? "success" : "error"}>
|
||||
<Input addonAfter={`Balance: ${coinInfo.balance}`} name="address" placeholder={"ERC20 address"}
|
||||
<Input addonAfter={`Balance: ${coinInfo.balance}`} name="address"
|
||||
placeholder={"ERC20 address"}
|
||||
onBlur={(v) => {
|
||||
setAddress(v.target.value)
|
||||
}}/>
|
||||
|
@ -144,7 +135,8 @@ function Transfer() {
|
|||
setAmount(value || 0)
|
||||
}}/>
|
||||
</Form.Item>
|
||||
<Form.Item name="target_chain" rules={[{required: true, message: "Please choose a target chain"}]}>
|
||||
<Form.Item name="target_chain"
|
||||
rules={[{required: true, message: "Please choose a target chain"}]}>
|
||||
<Select placeholder="Target Chain">
|
||||
<Select.Option value={1}>
|
||||
Solana
|
||||
|
@ -171,6 +163,12 @@ function Transfer() {
|
|||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Col>
|
||||
<Col>
|
||||
<SplBalances/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue