Parcourir la source

WIP SPL token integration

Hendrik Hofstadt il y a 5 ans
Parent
commit
ca4e4a3243

+ 110 - 12
web/package-lock.json

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

+ 4 - 2
web/package.json

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

+ 31 - 12
web/src/App/App.tsx

@@ -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>
     );
 }

+ 48 - 0
web/src/components/SplBalances.tsx

@@ -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 - 0
web/src/config.ts

@@ -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
+}

+ 72 - 74
web/src/pages/Transfer.tsx

@@ -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>
+
         </>
     );
 }

+ 107 - 0
web/src/pages/TransferSolana.tsx

@@ -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;

+ 20 - 0
web/src/providers/BridgeContext.tsx

@@ -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>
+    )
+}

+ 7 - 0
web/src/providers/KeyContext.ts

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

+ 22 - 0
web/src/providers/SlotContext.tsx

@@ -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>
+    )
+}

+ 60 - 0
web/src/providers/SolanaTokenContext.tsx

@@ -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>
+    )
+}

+ 111 - 8
web/src/utils/bridge.ts

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

+ 0 - 0
web/src/utils/validation.ts