feat: add bridge

This commit is contained in:
bartosz-lipinski 2021-02-11 15:04:28 -06:00
parent 9c92d884c4
commit 4b5954d627
54 changed files with 35579 additions and 13500 deletions

0
packages/bridge/.env Normal file
View File

View File

@ -0,0 +1 @@
GENERATE_SOURCEMAP = false

View File

@ -0,0 +1,57 @@
const CracoLessPlugin = require('craco-less');
const CracoAlias = require('craco-alias');
const CracoBabelLoader = require('craco-babel-loader');
const path = require('path');
const fs = require('fs');
//console.log('qualified', pnp.resolveRequest('@babel/preset-typescript'), path.resolve(__dirname, '/') + 'src/');
// Handle relative paths to sibling packages
const appDirectory = fs.realpathSync(process.cwd());
const resolvePackage = relativePath => path.resolve(appDirectory, relativePath);
module.exports = {
/* webpack: {
configure: webpackConfig => {
const scopePluginIndex = webpackConfig.resolve.plugins.findIndex(
({ constructor }) =>
constructor && constructor.name === 'ModuleScopePlugin',
);
webpackConfig.resolve.plugins.splice(scopePluginIndex, 1);
return webpackConfig;
},
},*/
plugins: [
/*{
plugin: CracoBabelLoader,
options: {
includes: [
// No "unexpected token" error importing components from these lerna siblings:
resolvePackage('../packages'),
],
},
},*/
/*{
plugin: CracoAlias,
options: {
source: 'tsconfig',
// baseUrl SHOULD be specified
// plugin does not take it from tsconfig
baseUrl: '../../',
// tsConfigPath should point to the file where "baseUrl" and "paths" are specified
tsConfigPath: '../../tsconfig.json',
},
},*/
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { '@primary-color': '#2abdd2' },
javascriptEnabled: true,
},
},
},
},
],
};

33304
packages/bridge/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,93 @@
{
"name": "bridge",
"version": "0.1.0",
"dependencies": {
"@ant-design/icons": "^4.4.0",
"@ant-design/pro-layout": "^6.7.0",
"@babel/preset-typescript": "^7.12.13",
"@craco/craco": "^5.7.0",
"@oyster/common": "0.0.1",
"@project-serum/serum": "^0.13.11",
"@project-serum/sol-wallet-adapter": "^0.1.4",
"@solana/spl-token": "0.0.13",
"@solana/spl-token-swap": "0.1.0",
"@solana/web3.js": "^0.86.2",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"@types/chart.js": "^2.9.29",
"@types/echarts": "^4.9.0",
"@types/react-router-dom": "^5.1.6",
"@types/testing-library__react": "^10.2.0",
"@welldone-software/why-did-you-render": "^6.0.5",
"antd": "^4.6.6",
"bn.js": "^5.1.3",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.0",
"chart.js": "^2.9.4",
"craco-alias": "^2.1.1",
"craco-babel-loader": "^0.1.4",
"craco-less": "^1.17.0",
"echarts": "^4.9.0",
"eventemitter3": "^4.0.7",
"identicon.js": "^2.3.3",
"jazzicon": "^1.5.0",
"lodash": "^4.17.20",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-github-btn": "^1.2.0",
"react-intl": "^5.10.2",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.3",
"typescript": "^4.1.3"
},
"scripts": {
"prestart": "npm-link-shared ../common/node_modules/ . react",
"start": "craco start --verbose",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"deploy": "gh-pages -d build",
"deploy:ar": "arweave deploy-dir build --key-file ",
"format:fix": "prettier --write \"**/*.+(js|jsx|ts|tsx|json|css|md)\""
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"repository": {
"type": "git",
"url": "https://github.com/solana-labs/oyster-lending"
},
"homepage": ".",
"devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/bs58": "^4.0.1",
"@types/identicon.js": "^2.3.0",
"@types/jest": "^24.9.1",
"@types/node": "^12.12.62",
"arweave-deploy": "^1.9.1",
"gh-pages": "^3.1.0",
"prettier": "^2.1.2",
"npm-link-shared": "0.5.6"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
},
"resolutions": {
"react": "16.13.1",
"react-dom": "16.13.1"
}
}

View File

@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/logo.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Wormhole Solana Bridge" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Wormhole Solana Bridge</title>
<style type="text/css">
#root {
height: 100%;
}
#root::before {
content: "";
position: absolute;
top: 0;
left: 0;
min-width: 100%;
min-height: 100%;
filter: grayscale(100%);
background-repeat: no-repeat;
background-size: cover;
}
.App {
position: relative;
height: 100%;
text-align: center;
min-width: 100%;
display: flex;
flex-direction: column;
}
</style>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css"
integrity="sha512-+4zCK9k+qNFUR5X+cKL9EIR+ZOhtIloNl9GIKS57V1MyNsYpYcUrUeQc9vNfzsWfV28IaLL3i96P9sdNyeRssA=="
crossorigin="anonymous"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
<script
async
src="https://platform.twitter.com/widgets.js"
charset="utf-8"
></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "Wormhole Solana Bridge",
"name": "Wormhole Solana Bridge",
"icons": [
{
"src": "icon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,376 @@
@import "~antd/dist/antd.dark.less";
@import "./ant-custom.less";
body {
--row-highlight: @background-color-base;
}
.App-logo {
background-image: url("data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjZDgzYWViIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMTAwIDEwMCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHBhdGggZD0iTTQwLjM2LDUwLjkxYzAuMDA3LTguMTc0LDMuODM2LTExLjUyNSw3LjA0OC0xMi44OThjNi41NTEtMi44MDEsMTYuNzksMC4xNDEsMjMuODA5LDYuODQyICBjMS4yNDYsMS4xODksMi4zNjEsMi4zMDksMy4zNjUsMy4zNjhjLTUuNjg0LTguMzcyLTE1LjAyNS0xNy41NjYtMjkuMDY0LTE4Ljg1OWMtNy43OTQtMC43MTYtMTMuNzk0LDIuNzk5LTE2LjAzMyw5LjQwOCAgYy0yLjY0OSw3LjgyMSwwLjM0MSwxOS4zMDUsMTEuMTgxLDI2LjEyMmM2LjE1MywzLjg2OSwxMi4zLDYuODY5LDE3LjM0MSw5LjA0NWMtMC41NTEtMC4zNTQtMS4xMDUtMC43MTYtMS42Ni0xLjA5MSAgQzQ1LjczMyw2NS42NjIsNDAuMzU0LDU4LjI4MSw0MC4zNiw1MC45MXoiPjwvcGF0aD48cGF0aCBkPSJNNjAuMDI3LDYzLjc2MWMtMC4wNzgtNC43MTUsMS44OTgtOC4yNSw1LjQyMi05LjY5OGM0LjEzOS0xLjcsOS40OS0wLjAwNCwxMy42MzMsNC4zMjMgIGMwLjY5MSwwLjcyMywxLjMwMywxLjQ1MywxLjg3NSwyLjE4NGMtMS42NzQtMy42OTktNC41MS03Ljk1OC0xMS4xMjEtMTQuMjY5Yy02LjM3MS02LjA4MS0xNS44NzktOC45MTItMjEuNjQyLTYuNDUgIGMtMy44MTIsMS42MjktNS44MjksNS40NTQtNS44MzQsMTEuMDYxYy0wLjAxLDExLjgxNSwxNi4zMTIsMjEuNjQ2LDI1LjA3MiwyNi4wNzJDNjMuNzc1LDczLjc0Niw2MC4xMTUsNjkuMTY4LDYwLjAyNyw2My43NjF6Ij48L3BhdGg+PHBhdGggZD0iTTI3LjU5MSwzOC4xM2MyLjU1Ni03LjU0NSw5LjMzMS0xMS41NjgsMTguMTExLTEwLjc1OGMxMS41MjksMS4wNjEsMjAuMDE1LDcuMTQ4LDI2LjAxMywxMy45MiAgQzYxLjUsMjYuMDU0LDQ4Ljk2MywyMC4zMzksNDguODE3LDIwLjI3NGMtMy4yOTYtMS42ODgtNi43OTctMi41MzEtMTAuNDU3LTIuNTMxYy0xMi43NzQsMC0yMy4xNjcsMTAuNTgtMjMuMTY3LDIzLjU4MyAgYzAsNy45NjEsNC4yMDEsMTUuNTIxLDExLjIzOCwyMC4yMjJjMy43ODksMi41MywxMS40ODgsNS44MjQsMjAuMDQ2LDkuMDM4Yy0yLjI1NC0xLjIxNS00LjU2NC0yLjU0Ny02Ljg3NS00ICBDMjcuODg1LDU5LjIxOSwyNC42OSw0Ni42OTQsMjcuNTkxLDM4LjEzeiI+PC9wYXRoPjxwYXRoIGQ9Ik03Ny42MzcsNTkuNzY5Yy0zLjU2OC0zLjcyOS04LjA1Ny01LjI0Mi0xMS40MjgtMy44NTVjLTIuNzIxLDEuMTE4LTQuMjQ2LDMuOTY3LTQuMTgyLDcuODE0ICBjMC4xNDgsOS4wMzUsMTEuMzEzLDE1LjMxOCwxMy41ODgsMTYuNTkyYzMuNDg5LDEuOTU0LDcuNjI1LDIuMDg3LDcuOTA0LDEuOTM4czAuMjc5LTAuMTQ5LDAuNTMxLTAuNjUxICBjMC42Ni0xLjMwOSwxLjA1My00LjI3NSwwLjM2MS04Ljk2NkM4My43NzcsNjkuNDg5LDgyLjA5Niw2NC40MjcsNzcuNjM3LDU5Ljc2OXoiPjwvcGF0aD48L3N2Zz4=");
height: 32px;
pointer-events: none;
background-repeat: no-repeat;
background-size: 32px;
width: 32px;
}
.footer {
background-color: black;
color: lightgray;
padding: 10px 10px;
max-height: 60px;
overflow: hidden;
text-overflow: ellipsis;
}
.action-spinner {
position: absolute;
right: 5px;
}
.Banner {
min-height: 30px;
width: 100%;
background-color: #fff704;
display: flex;
flex-direction: column;
justify-content: center;
// z-index: 10;
}
.Banner-description {
color: black;
text-align: center;
display: flex;
align-self: center;
align-items: center;
height: 100%;
}
.App-Bar {
display: grid;
grid-template-columns: 1fr 120px;
-webkit-box-pack: justify;
justify-content: space-between;
-webkit-box-align: center;
align-items: center;
flex-direction: row;
width: 100%;
top: 0px;
position: relative;
padding: 1rem;
z-index: 2;
.ant-menu-horizontal {
border-bottom-color: transparent;
background-color: transparent;
line-height: inherit;
font-size: 16px;
margin: 0 10px;
.ant-menu-item {
margin: 0 10px;
color: lightgrey;
height: 35px;
line-height: 35px;
border-width: 0px !important;
}
.ant-menu-item:hover {
color: white;
border-width: 0px !important;
}
.ant-menu-item-selected {
font-weight: bold;
}
}
}
.App-Bar-left {
box-sizing: border-box;
margin: 0px;
min-width: 0px;
display: flex;
padding: 0px;
-webkit-box-align: center;
align-items: center;
width: fit-content;
}
.App-Bar-right {
display: flex;
flex-direction: row;
-webkit-box-align: center;
align-items: center;
justify-self: flex-end;
}
.ant-tabs-nav-scroll {
display: flex;
justify-content: center;
}
.discord {
font-size: 30px;
color: #7289da;
}
.discord:hover {
color: #8ea1e1;
}
.telegram {
color: #32afed;
font-size: 28px;
background-color: white;
border-radius: 30px;
display: flex;
width: 27px;
height: 27px;
}
em {
font-weight: bold;
font-style: normal;
text-decoration: none;
}
.telegram:hover {
color: #2789de !important;
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
.social-buttons {
margin-top: auto;
margin-left: auto;
margin-bottom: 0.5rem;
margin-right: 1rem;
gap: 0.3rem;
display: flex;
}
.wallet-wrapper {
padding-left: 0.7rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
white-space: nowrap;
}
.wallet-key {
padding: 0.1rem 0.5rem 0.1rem 0.7rem;
margin-left: 0.3rem;
border-radius: 0.5rem;
display: flex;
align-items: center;
}
.flash-positive {
color: rgba(0, 255, 0, 0.6);
}
.flash-negative {
color: rgba(255, 0, 0, 0.6);
}
.ant-table-cell {
padding: 6px 16px !important;
}
.ant-table {
margin: 0px 30px;
}
.ant-menu-inline-collapsed > .ant-menu-item {
padding-left: 16px !important;
}
.ant-pagination-options {
display: none;
}
.ant-table-container table > thead > tr th {
text-align: center;
}
.ant-notification {
a {
color: blue;
text-decoration: underline;
cursor: pointer;
}
}
.ant-layout-content {
display: flex;
overflow: auto;
}
.flexColumn {
display: flex;
flex-direction: column;
flex: 1;
}
.card-fill {
height: 100%;
}
.card-row {
box-sizing: border-box;
margin: 5px 0px;
min-width: 0px;
width: 100%;
display: flex;
flex-direction: row;
padding: 0px;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: justify;
justify-content: space-between;
.card-cell {
display: flex;
flex-direction: column;
align-items: flex-end;
box-sizing: border-box;
text-align: left;
margin: 0px;
min-width: 0px;
font-size: 14px;
}
.left {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.small {
font-size: 11px;
}
}
.ant-slider {
margin: 20px 15px 40px 15px;
}
.ant-pro-sider.ant-layout-sider-collapsed .ant-pro-sider-logo {
padding: 8px 8px;
}
.ant-pro-global-header {
.ant-pro-global-header-logo a h1 {
color: white !important;
}
background-color: black !important;
color: white !important;
.ant-btn {
color: white !important;
}
}
.ant-statistic {
user-select: none;
}
.ant-statistic-content {
font-weight: bold;
}
.ant-select-selection-item {
.token-balance {
display: none;
};
};
.token-input {
display: flex;
align-items: center;
border: 1px solid grey;
padding: 0px 10px;
margin: 5px 0px;
}
.token-balance {
margin-left: auto;
margin-right: 5px;
color: @text-color-secondary;
}
[class="ant-layout-header"] {
height: 16px !important;
}
.dashboard-amount-quote {
font-size: 10px;
font-style: normal;
text-align: right;
}
.small-statisitc {
.ant-statistic-title {
font-size: 12px;
}
.ant-statistic-content {
max-height: 20px;
line-height: 2px;;
}
.ant-statistic-content-value-int {
font-size: 12px;
}
.ant-statistic-content-value-decimal {
font-size: 10px;
}
}
.links {
position: relative;
height: 100%;
.bottom-links {
position: absolute;
bottom: 0px;
}
}
.ant-pro-sider {
background: transparent !important;
.ant-menu {
background: transparent !important;
}
}
.dashboard-amount-quote-stat {
font-size: 10px;
font-style: normal;
text-align: center;
font-weight: normal;
}
@media only screen and (max-width: 600px) {
.exchange-card {
width: 360px;
}
}

View File

@ -0,0 +1,9 @@
import React from "react";
import "./App.less";
import { Routes } from "./routes";
function App() {
return <Routes />;
}
export default App;

View File

@ -0,0 +1 @@
export const nop = () => {};

View File

@ -0,0 +1,5 @@
@import '~antd/es/style/themes/dark.less';
@import "~antd/dist/antd.dark.less";
@primary-color: #ff00a8;
@popover-background: #1a2029;

View File

@ -0,0 +1,42 @@
import React from 'react';
import { Button, Popover } from 'antd';
import { contexts, ConnectButton } from '@oyster/common';
import { CurrentUserBadge } from '../CurrentUserBadge';
import { SettingOutlined } from '@ant-design/icons';
import { Settings } from '../Settings';
import { LABELS } from '../../constants';
const { useWallet } = contexts.Wallet;
export const AppBar = (props: { left?: JSX.Element; right?: JSX.Element }) => {
const { connected, wallet } = useWallet();
const TopBar = (
<div className="App-Bar-right">
{connected ? (
<CurrentUserBadge />
) : (
<ConnectButton
type="text"
size="large"
allowWalletChange={true}
style={{ color: "#2abdd2" }}/>
)}
<Popover
placement="topRight"
title={LABELS.SETTINGS_TOOLTIP}
content={<Settings />}
trigger="click"
>
<Button
shape="circle"
size="large"
type="text"
icon={<SettingOutlined />}
/>
</Popover>
{props.right}
</div>
);
return TopBar;
};

View File

@ -0,0 +1,13 @@
import React from "react";
import { Button } from "antd";
import { LABELS } from "../../constants";
import { useHistory } from "react-router-dom";
export const BackButton = () => {
const history = useHistory();
return (
<Button type="text" onClick={history.goBack}>
{LABELS.GO_BACK_ACTION}
</Button>
);
};

View File

@ -0,0 +1,34 @@
import React from 'react';
import { contexts, utils } from '@oyster/common';
import { Identicon } from '../Identicon';
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
const { useWallet } = contexts.Wallet;
const { useNativeAccount } = contexts.Accounts;
const { formatNumber, shortenAddress } = utils;
export const CurrentUserBadge = (props: {}) => {
const { wallet } = useWallet();
const { account } = useNativeAccount();
if (!wallet || !wallet.publicKey) {
return null;
}
// should use SOL ◎ ?
return (
<div className="wallet-wrapper">
<span>
{formatNumber.format((account?.lamports || 0) / LAMPORTS_PER_SOL)} SOL
</span>
<div className="wallet-key">
{shortenAddress(`${wallet.publicKey}`)}
<Identicon
address={wallet.publicKey?.toBase58()}
style={{ marginLeft: '0.5rem', display: 'flex' }}
/>
</div>
</div>
);
};

View File

@ -0,0 +1,20 @@
import { Button, Popover } from "antd";
import React from "react";
import { InfoCircleOutlined } from "@ant-design/icons";
export const Info = (props: {
text: React.ReactElement;
style?: React.CSSProperties;
}) => {
return (
<Popover
trigger="hover"
content={<div style={{ width: 300 }}>{props.text}</div>}
>
<Button type="text" shape="circle">
<InfoCircleOutlined style={props.style} />
</Button>
</Popover>
);
};

View File

@ -0,0 +1,36 @@
import React, { useEffect, useRef } from "react";
import Jazzicon from "jazzicon";
import bs58 from "bs58";
import "./style.less";
import { PublicKey } from "@solana/web3.js";
export const Identicon = (props: {
address?: string | PublicKey;
style?: React.CSSProperties;
className?: string;
}) => {
const { style, className } = props;
const address =
typeof props.address === "string"
? props.address
: props.address?.toBase58();
const ref = useRef<HTMLDivElement>();
useEffect(() => {
if (address && ref.current) {
ref.current.innerHTML = "";
ref.current.className = className || "";
ref.current.appendChild(
Jazzicon(
style?.width || 16,
parseInt(bs58.decode(address).toString("hex").slice(5, 15), 16)
)
);
}
}, [address, style, className]);
return (
<div className="identicon-wrapper" ref={ref as any} style={props.style} />
);
};

View File

@ -0,0 +1,8 @@
.identicon-wrapper {
display: flex;
height: 1rem;
width: 1rem;
border-radius: 1.125rem;
margin: 0.2rem 0.2rem 0.2rem 0.1rem;
/* background-color: ${({ theme }) => theme.bg4}; */
}

View File

@ -0,0 +1,43 @@
import React from "react";
import { Input } from "antd";
export class NumericInput extends React.Component<any, any> {
onChange = (e: any) => {
const { value } = e.target;
const reg = /^-?\d*(\.\d*)?$/;
if (reg.test(value) || value === "" || value === "-") {
this.props.onChange(value);
}
};
// '.' at the end or only '-' in the input box.
onBlur = () => {
const { value, onBlur, onChange } = this.props;
let valueTemp = value;
if (value === undefined || value === null) return;
if (
value.charAt &&
(value.charAt(value.length - 1) === "." || value === "-")
) {
valueTemp = value.slice(0, -1);
}
if (value.startsWith && (value.startsWith(".") || value.startsWith("-."))) {
valueTemp = valueTemp.replace(".", "0.");
}
if (valueTemp.replace) onChange?.(valueTemp.replace(/0*(\d+)/, "$1"));
if (onBlur) {
onBlur();
}
};
render() {
return (
<Input
{...this.props}
onChange={this.onChange}
onBlur={this.onBlur}
maxLength={25}
/>
);
}
}

View File

@ -0,0 +1,62 @@
import React from 'react';
import './../../App.less';
import { Menu } from 'antd';
import {
PieChartOutlined,
GithubOutlined,
BankOutlined,
LogoutOutlined,
ShoppingOutlined,
HomeOutlined,
RocketOutlined,
ForkOutlined,
// LineChartOutlined
} from '@ant-design/icons';
import BasicLayout from '@ant-design/pro-layout';
import { AppBar } from './../AppBar';
import { Link, useLocation } from 'react-router-dom';
import { LABELS } from '../../constants';
import config from './../../../package.json';
import { contexts } from '@oyster/common';
const { useConnectionConfig } = contexts.Connection;
export const AppLayout = React.memo((props: any) => {
const { env } = useConnectionConfig();
const location = useLocation();
const paths: { [key: string]: string } = {
'/faucet': '7',
};
const current =
[...Object.keys(paths)].find(key => location.pathname.startsWith(key)) ||
'';
const defaultKey = paths[current] || '1';
const theme = 'dark';
return (
<div className="App">
<BasicLayout
title={LABELS.APP_TITLE}
footerRender={() => (
<div className="footer" title={LABELS.FOOTER}>
{LABELS.FOOTER}
</div>
)}
navTheme={theme}
headerTheme={theme}
theme={theme}
layout="top"
primaryColor="#d83aeb"
logo={<div className="App-logo" />}
rightContentRender={() => <AppBar />}
links={[]}
>
{props.children}
</BasicLayout>
</div>
);
});

View File

@ -0,0 +1,31 @@
import React from 'react';
import { Button, Select } from 'antd';
import { contexts } from '@oyster/common';
const { useWallet, WALLET_PROVIDERS } = contexts.Wallet;
const { ENDPOINTS, useConnectionConfig } = contexts.Connection;
export const Settings = () => {
const { connected, disconnect } = useWallet();
const { endpoint, setEndpoint } = useConnectionConfig();
return (
<>
<div style={{ display: "grid" }}>
Network:{" "}
<Select
onSelect={setEndpoint}
value={endpoint}
style={{ marginBottom: 20 }}
>
{ENDPOINTS.map(({ name, endpoint }) => (
<Select.Option value={endpoint} key={endpoint}>
{name}
</Select.Option>
))}
</Select>
{connected && <Button type="primary" onClick={disconnect}>Disconnect</Button>}
</div>
</>
);
};

View File

@ -0,0 +1,58 @@
import React from 'react';
import { contexts } from '@oyster/common';
import { TokenIcon } from '../TokenIcon';
const { useMint, useAccountByMint } = contexts.Accounts;
export const TokenDisplay = (props: {
name: string;
mintAddress: string;
icon?: JSX.Element;
showBalance?: boolean;
}) => {
const { showBalance, mintAddress, name, icon } = props;
const tokenMint = useMint(mintAddress);
const tokenAccount = useAccountByMint(mintAddress);
let balance: number = 0;
let hasBalance: boolean = false;
if (showBalance) {
if (tokenAccount && tokenMint) {
balance =
tokenAccount.info.amount.toNumber() / Math.pow(10, tokenMint.decimals);
hasBalance = balance > 0;
}
}
return (
<>
<div
title={mintAddress}
key={mintAddress}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', alignItems: 'center' }}>
{icon || <TokenIcon mintAddress={mintAddress} />}
{name}
</div>
{showBalance ? (
<span
title={balance.toString()}
key={mintAddress}
className="token-balance"
>
&nbsp;{' '}
{hasBalance
? balance < 0.001
? '<0.001'
: balance.toFixed(3)
: '-'}
</span>
) : null}
</div>
</>
);
};

View File

@ -0,0 +1,69 @@
import { Identicon } from '../Identicon';
import React from 'react';
import { utils, contexts } from '@oyster/common';
import { PublicKey } from '@solana/web3.js';
const { getTokenIcon } = utils;
const { useConnectionConfig } = contexts.Connection;
export const TokenIcon = (props: {
mintAddress?: string | PublicKey;
style?: React.CSSProperties;
size?: number;
className?: string;
}) => {
const { tokenMap } = useConnectionConfig();
const icon = getTokenIcon(tokenMap, props.mintAddress);
const size = props.size || 20;
if (icon) {
return (
<img
alt="Token icon"
className={props.className}
key={icon}
width={props.style?.width || size.toString()}
height={props.style?.height || size.toString()}
src={icon}
style={{
marginRight: '0.5rem',
marginTop: '0.11rem',
borderRadius: '10rem',
backgroundColor: 'white',
backgroundClip: 'padding-box',
...props.style,
}}
/>
);
}
return (
<Identicon
address={props.mintAddress}
style={{
marginRight: '0.5rem',
width: size,
height: size,
marginTop: 2,
...props.style,
}}
/>
);
};
export const PoolIcon = (props: {
mintA: string;
mintB: string;
style?: React.CSSProperties;
className?: string;
}) => {
return (
<div className={props.className} style={{ display: 'flex' }}>
<TokenIcon
mintAddress={props.mintA}
style={{ marginRight: '-0.5rem', ...props.style }}
/>
<TokenIcon mintAddress={props.mintB} />
</div>
);
};

View File

@ -0,0 +1,2 @@
export * from './labels';
export * from './style';

View File

@ -0,0 +1,20 @@
export const LABELS = {
CONNECT_LABEL: 'Connect Wallet',
GIVE_SOL: 'Give me SOL',
FAUCET_INFO:
'This faucet will help you fund your accounts outside of Solana main network.',
ACCOUNT_FUNDED: 'Account funded.',
AUDIT_WARNING:
'Oyster is an unaudited software project used for internal purposes at the Solana Foundation. This app is not for public use.',
FOOTER:
'This page was produced by the Solana Foundation ("SF") for internal educational and inspiration purposes only. SF does not encourage, induce or sanction the deployment, integration or use of Oyster or any similar application (including its code) in violation of applicable laws or regulations and hereby prohibits any such deployment, integration or use. Anyone using this code or a derivation thereof must comply with applicable laws and regulations when releasing related software.',
MENU_HOME: 'Home',
MENU_FAUCET: 'Faucet',
APP_TITLE: 'Wormhole',
CONNECT_BUTTON: 'Connect',
WALLET_TOOLTIP: 'Wallet public key',
WALLET_BALANCE: 'Wallet balance',
SETTINGS_TOOLTIP: 'Settings',
GO_BACK_ACTION: 'Go back',
TOTAL_TITLE: 'Total',
};

View File

@ -0,0 +1,5 @@
export const GUTTER = [16, { xs: 8, sm: 16, md: 16, lg: 16 }] as any;
export const SMALL_STATISTIC: React.CSSProperties = {
fontSize: 10,
};

View File

@ -0,0 +1,367 @@
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { Market, MARKETS, Orderbook, TOKEN_MINTS } from '@project-serum/serum';
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import { useMemo } from 'react';
import {
contexts,
utils,
ParsedAccount,
KnownTokenMap,
EventEmitter,
} from '@oyster/common';
import { DexMarketParser } from './../models/dex';
import { MINT_TO_MARKET } from '../models/marketOverrides';
const { useConnectionConfig } = contexts.Connection;
const { convert, fromLamports, getTokenName, STABLE_COINS } = utils;
const { cache, getMultipleAccounts } = contexts.Accounts;
const INITAL_LIQUIDITY_DATE = new Date('2020-10-27');
export const BONFIDA_POOL_INTERVAL = 30 * 60_000; // 30 min
interface RecentPoolData {
pool_identifier: string;
volume24hA: number;
}
export interface MarketsContextState {
midPriceInUSD: (mint: string) => number;
marketEmitter: EventEmitter;
accountsToObserve: Map<string, number>;
marketByMint: Map<string, SerumMarket>;
subscribeToMarket: (mint: string) => () => void;
precacheMarkets: (mints: string[]) => void;
dailyVolume: Map<string, RecentPoolData>;
}
const REFRESH_INTERVAL = 30_000;
const MarketsContext = React.createContext<MarketsContextState | null>(null);
const marketEmitter = new EventEmitter();
export function MarketProvider({ children = null as any }) {
const { endpoint } = useConnectionConfig();
const accountsToObserve = useMemo(() => new Map<string, number>(), []);
const [marketMints, setMarketMints] = useState<string[]>([]);
const [dailyVolume, setDailyVolume] = useState<Map<string, RecentPoolData>>(
new Map(),
);
const connection = useMemo(() => new Connection(endpoint, 'recent'), [
endpoint,
]);
const marketByMint = useMemo(() => {
return [...new Set(marketMints).values()].reduce((acc, key) => {
const mintAddress = key;
const SERUM_TOKEN = TOKEN_MINTS.find(
a => a.address.toBase58() === mintAddress,
);
const marketAddress = MINT_TO_MARKET[mintAddress];
const marketName = `${SERUM_TOKEN?.name}/USDC`;
const marketInfo = MARKETS.find(
m => m.name === marketName || m.address.toBase58() === marketAddress,
);
if (marketInfo) {
acc.set(mintAddress, {
marketInfo,
});
}
return acc;
}, new Map<string, SerumMarket>()) as Map<string, SerumMarket>;
}, [marketMints]);
useEffect(() => {
let timer = 0;
const updateData = async () => {
await refreshAccounts(connection, [...accountsToObserve.keys()]);
marketEmitter.raiseMarketUpdated(new Set([...marketByMint.keys()]));
timer = window.setTimeout(() => updateData(), REFRESH_INTERVAL);
};
const initalQuery = async () => {
const reverseSerumMarketCache = new Map<string, string>();
[...marketByMint.keys()].forEach(mint => {
const m = marketByMint.get(mint);
if (m) {
reverseSerumMarketCache.set(m.marketInfo.address.toBase58(), mint);
}
});
const allMarkets = [...marketByMint.values()].map(m => {
return m.marketInfo.address.toBase58();
});
await getMultipleAccounts(
connection,
// only query for markets that are not in cahce
allMarkets.filter(a => cache.get(a) === undefined),
'single',
).then(({ keys, array }) => {
allMarkets.forEach(() => {});
return array.map((item, index) => {
const marketAddress = keys[index];
const mintAddress = reverseSerumMarketCache.get(marketAddress);
if (mintAddress) {
const market = marketByMint.get(mintAddress);
if (market) {
const id = market.marketInfo.address;
cache.add(id, item, DexMarketParser);
}
}
return item;
});
});
const toQuery = new Set<string>();
allMarkets.forEach(m => {
const market = cache.get(m);
if (!market) {
return;
}
const decoded = market;
if (!cache.get(decoded.info.baseMint)) {
toQuery.add(decoded.info.baseMint.toBase58());
}
if (!cache.get(decoded.info.baseMint)) {
toQuery.add(decoded.info.quoteMint.toBase58());
}
toQuery.add(decoded.info.bids.toBase58());
toQuery.add(decoded.info.asks.toBase58());
});
await refreshAccounts(connection, [...toQuery.keys()]);
marketEmitter.raiseMarketUpdated(new Set([...marketByMint.keys()]));
// start update loop
updateData();
};
initalQuery();
return () => {
window.clearTimeout(timer);
};
}, [marketByMint, accountsToObserve, connection]);
const midPriceInUSD = useCallback(
(mintAddress: string) => {
return getMidPrice(
marketByMint.get(mintAddress)?.marketInfo.address.toBase58(),
mintAddress,
);
},
[marketByMint],
);
const subscribeToMarket = useCallback(
(mintAddress: string) => {
const info = marketByMint.get(mintAddress);
const market = cache.get(info?.marketInfo.address.toBase58() || '');
if (!market) {
return () => {};
}
// TODO: get recent volume
const bid = market.info.bids.toBase58();
const ask = market.info.asks.toBase58();
accountsToObserve.set(bid, (accountsToObserve.get(bid) || 0) + 1);
accountsToObserve.set(ask, (accountsToObserve.get(ask) || 0) + 1);
// TODO: add event queue to query for last trade
return () => {
accountsToObserve.set(bid, (accountsToObserve.get(bid) || 0) - 1);
accountsToObserve.set(ask, (accountsToObserve.get(ask) || 0) - 1);
// cleanup
[...accountsToObserve.keys()].forEach(key => {
if ((accountsToObserve.get(key) || 0) <= 0) {
accountsToObserve.delete(key);
}
});
};
},
[marketByMint, accountsToObserve],
);
const precacheMarkets = useCallback(
(mints: string[]) => {
const newMints = [...new Set([...marketMints, ...mints]).values()];
if (marketMints.length !== newMints.length) {
setMarketMints(newMints);
}
},
[setMarketMints, marketMints],
);
return (
<MarketsContext.Provider
value={{
midPriceInUSD,
marketEmitter,
accountsToObserve,
marketByMint,
subscribeToMarket,
precacheMarkets,
dailyVolume,
}}
>
{children}
</MarketsContext.Provider>
);
}
export const useMarkets = () => {
const context = useContext(MarketsContext);
return context as MarketsContextState;
};
export const useMidPriceInUSD = (mint: string) => {
const { midPriceInUSD, subscribeToMarket, marketEmitter } = useContext(
MarketsContext,
) as MarketsContextState;
const [price, setPrice] = useState<number>(0);
useEffect(() => {
let subscription = subscribeToMarket(mint);
const update = () => {
if (midPriceInUSD) {
setPrice(midPriceInUSD(mint));
}
};
update();
const dispose = marketEmitter.onMarket(update);
return () => {
subscription();
dispose();
};
}, [midPriceInUSD, mint, marketEmitter, subscribeToMarket]);
return { price, isBase: price === 1.0 };
};
export const usePrecacheMarket = () => {
const context = useMarkets();
return context.precacheMarkets;
};
const bbo = (bidsBook: Orderbook, asksBook: Orderbook) => {
const bestBid = bidsBook.getL2(1);
const bestAsk = asksBook.getL2(1);
if (bestBid.length > 0 && bestAsk.length > 0) {
return (bestBid[0][0] + bestAsk[0][0]) / 2.0;
}
return 0;
};
const getMidPrice = (marketAddress?: string, mintAddress?: string) => {
const SERUM_TOKEN = TOKEN_MINTS.find(
a => a.address.toBase58() === mintAddress,
);
if (STABLE_COINS.has(SERUM_TOKEN?.name || '')) {
return 1.0;
}
if (!marketAddress) {
return 0.0;
}
const marketInfo = cache.get(marketAddress);
if (!marketInfo) {
return 0.0;
}
const decodedMarket = marketInfo.info;
const baseMintDecimals =
cache.get(decodedMarket.baseMint)?.info.decimals || 0;
const quoteMintDecimals =
cache.get(decodedMarket.quoteMint)?.info.decimals || 0;
const market = new Market(
decodedMarket,
baseMintDecimals,
quoteMintDecimals,
undefined,
decodedMarket.programId,
);
const bids = cache.get(decodedMarket.bids)?.info;
const asks = cache.get(decodedMarket.asks)?.info;
if (bids && asks) {
const bidsBook = new Orderbook(market, bids.accountFlags, bids.slab);
const asksBook = new Orderbook(market, asks.accountFlags, asks.slab);
return bbo(bidsBook, asksBook);
}
return 0;
};
const refreshAccounts = async (connection: Connection, keys: string[]) => {
if (keys.length === 0) {
return [];
}
return getMultipleAccounts(connection, keys, 'single').then(
({ keys, array }) => {
return array.map((item, index) => {
const address = keys[index];
return cache.add(new PublicKey(address), item);
});
},
);
};
interface SerumMarket {
marketInfo: {
address: PublicKey;
name: string;
programId: PublicKey;
deprecated: boolean;
};
// 1st query
marketAccount?: AccountInfo<Buffer>;
// 2nd query
mintBase?: AccountInfo<Buffer>;
mintQuote?: AccountInfo<Buffer>;
bidAccount?: AccountInfo<Buffer>;
askAccount?: AccountInfo<Buffer>;
eventQueue?: AccountInfo<Buffer>;
swap?: {
dailyVolume: number;
};
midPrice?: (mint?: PublicKey) => number;
}

View File

@ -0,0 +1 @@
export const nop = () => {};

View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View File

@ -0,0 +1,18 @@
import "./wdyr";
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

View File

@ -0,0 +1,22 @@
{
"name": "Wormhole Solana Bridge",
"short_name": "Wormhole Solana Bridge",
"display": "standalone",
"start_url": "./",
"theme_color": "#002140",
"background_color": "#001529",
"icons": [
{
"src": "icons/icon-192x192.png",
"sizes": "192x192"
},
{
"src": "icons/icon-128x128.png",
"sizes": "128x128"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512"
}
]
}

View File

@ -0,0 +1 @@
export * from './market';

View File

@ -0,0 +1,47 @@
import { contexts, ParsedAccountBase } from '@oyster/common';
import { Market, MARKETS, Orderbook } from '@project-serum/serum';
import { AccountInfo, PublicKey } from '@solana/web3.js';
const { MintParser, cache } = contexts.Accounts;
export const OrderBookParser = (id: PublicKey, acc: AccountInfo<Buffer>) => {
const decoded = Orderbook.LAYOUT.decode(acc.data);
const details = {
pubkey: id,
account: {
...acc,
},
info: decoded,
} as ParsedAccountBase;
return details;
};
const DEFAULT_DEX_ID = new PublicKey(
'EUqojwWA2rd19FZrzeBncJsm38Jm1hEhE3zsmX3bRc2o',
);
export const DexMarketParser = (
pubkey: PublicKey,
acc: AccountInfo<Buffer>,
) => {
const market = MARKETS.find(m => m.address.equals(pubkey));
const decoded = Market.getLayout(market?.programId || DEFAULT_DEX_ID).decode(
acc.data,
);
const details = {
pubkey,
account: {
...acc,
},
info: decoded,
} as ParsedAccountBase;
cache.registerParser(details.info.baseMint, MintParser);
cache.registerParser(details.info.quoteMint, MintParser);
cache.registerParser(details.info.bids, OrderBookParser);
cache.registerParser(details.info.asks, OrderBookParser);
return details;
};

View File

@ -0,0 +1 @@
export * from './dex';

View File

@ -0,0 +1,2 @@
// use to override serum market to use specifc mint
export const MINT_TO_MARKET: { [key: string]: string } = {};

View File

@ -0,0 +1,12 @@
export interface TotalItem {
key: string;
marketSize: number;
nativeSize: number;
name: string;
}
export interface Totals {
marketSize: number;
numberOfAssets: number;
items: TotalItem[];
}

View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -0,0 +1,36 @@
import { HashRouter, Route, Switch } from 'react-router-dom';
import React from 'react';
import { contexts } from '@oyster/common';
import { MarketProvider } from './contexts/market';
import { AppLayout } from './components/Layout';
import {
FaucetView,
HomeView,
} from './views';
const { WalletProvider } = contexts.Wallet;
const { ConnectionProvider } = contexts.Connection;
const { AccountsProvider } = contexts.Accounts;
export function Routes() {
return (
<>
<HashRouter basename={'/'}>
<ConnectionProvider>
<WalletProvider>
<AccountsProvider>
<MarketProvider>
<AppLayout>
<Switch>
<Route exact path="/" component={() => <HomeView />} />
<Route exact path="/faucet" children={<FaucetView />} />
</Switch>
</AppLayout>
</MarketProvider>
</AccountsProvider>
</WalletProvider>
</ConnectionProvider>
</HashRouter>
</>
);
}

View File

@ -0,0 +1,146 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/,
),
);
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
};
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA',
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl: string, config?: Config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.',
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.',
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

View File

@ -0,0 +1,9 @@
declare module 'buffer-layout' {
const bl: any;
export = bl;
}
declare module 'jazzicon' {
const jazzicon: any;
export = jazzicon;
}

View File

@ -0,0 +1,4 @@
declare module '@project-serum/sol-wallet-adapter' {
const adapter: any;
export = adapter;
}

View File

@ -0,0 +1,121 @@
import { PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
import * as BufferLayout from 'buffer-layout';
/**
* Layout for a public key
*/
export const publicKey = (property = 'publicKey'): unknown => {
const publicKeyLayout = BufferLayout.blob(32, property);
const _decode = publicKeyLayout.decode.bind(publicKeyLayout);
const _encode = publicKeyLayout.encode.bind(publicKeyLayout);
publicKeyLayout.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset);
return new PublicKey(data);
};
publicKeyLayout.encode = (key: PublicKey, buffer: Buffer, offset: number) => {
return _encode(key.toBuffer(), buffer, offset);
};
return publicKeyLayout;
};
/**
* Layout for a 64bit unsigned value
*/
export const uint64 = (property = 'uint64'): unknown => {
const layout = BufferLayout.blob(8, property);
const _decode = layout.decode.bind(layout);
const _encode = layout.encode.bind(layout);
layout.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset);
return new BN(
[...data]
.reverse()
.map(i => `00${i.toString(16)}`.slice(-2))
.join(''),
16,
);
};
layout.encode = (num: BN, buffer: Buffer, offset: number) => {
const a = num.toArray().reverse();
let b = Buffer.from(a);
if (b.length !== 8) {
const zeroPad = Buffer.alloc(8);
b.copy(zeroPad);
b = zeroPad;
}
return _encode(b, buffer, offset);
};
return layout;
};
// TODO: wrap in BN (what about decimals?)
export const uint128 = (property = 'uint128'): unknown => {
const layout = BufferLayout.blob(16, property);
const _decode = layout.decode.bind(layout);
const _encode = layout.encode.bind(layout);
layout.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset);
return new BN(
[...data]
.reverse()
.map(i => `00${i.toString(16)}`.slice(-2))
.join(''),
16,
);
};
layout.encode = (num: BN, buffer: Buffer, offset: number) => {
const a = num.toArray().reverse();
let b = Buffer.from(a);
if (b.length !== 16) {
const zeroPad = Buffer.alloc(16);
b.copy(zeroPad);
b = zeroPad;
}
return _encode(b, buffer, offset);
};
return layout;
};
/**
* Layout for a Rust String type
*/
export const rustString = (property = 'string'): unknown => {
const rsl = BufferLayout.struct(
[
BufferLayout.u32('length'),
BufferLayout.u32('lengthPadding'),
BufferLayout.blob(BufferLayout.offset(BufferLayout.u32(), -8), 'chars'),
],
property,
);
const _decode = rsl.decode.bind(rsl);
const _encode = rsl.encode.bind(rsl);
rsl.decode = (buffer: Buffer, offset: number) => {
const data = _decode(buffer, offset);
return data.chars.toString('utf8');
};
rsl.encode = (str: string, buffer: Buffer, offset: number) => {
const data = {
chars: Buffer.from(str, 'utf8'),
};
return _encode(data, buffer, offset);
};
return rsl;
};

View File

@ -0,0 +1,58 @@
import React, { useCallback } from 'react';
import { Card } from 'antd';
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
import { LABELS } from '../../constants';
import { contexts, utils, ConnectButton } from '@oyster/common';
const { useConnection } = contexts.Connection;
const { useWallet } = contexts.Wallet;
const { notify } = utils;
export const FaucetView = () => {
const connection = useConnection();
const { wallet } = useWallet();
const airdrop = useCallback(() => {
if (!wallet?.publicKey) {
return;
}
connection
.requestAirdrop(wallet.publicKey, 2 * LAMPORTS_PER_SOL)
.then(() => {
notify({
message: LABELS.ACCOUNT_FUNDED,
type: "success",
});
});
}, [wallet, wallet?.publicKey, connection]);
const bodyStyle: React.CSSProperties = {
display: "flex",
flex: 1,
justifyContent: "center",
alignItems: "center",
height: "100%",
};
return (
<div className="flexColumn" style={{ flex: 1 }}>
<Card title={"Faucet"} bodyStyle={bodyStyle} style={{ flex: 1 }}>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-around",
alignItems: "center",
}}
>
<div className="deposit-input-title" style={{ margin: 10 }}>
{LABELS.FAUCET_INFO}
</div>
<ConnectButton type="primary" onClick={airdrop}>
{LABELS.GIVE_SOL}
</ConnectButton>
</div>
</Card>
</div>
);
};

View File

@ -0,0 +1,118 @@
import { MintInfo } from '@solana/spl-token';
import { Card, Col, Row, Statistic } from 'antd';
import React, { useEffect, useState } from 'react';
import { GUTTER, LABELS } from '../../constants';
import { contexts, ParsedAccount, utils } from '@oyster/common';
import { useMarkets } from '../../contexts/market';
import { LendingReserveItem } from './item';
import './itemStyle.less';
import { Totals } from '../../models/totals';
const { fromLamports, getTokenName, wadToLamports } = utils;
const { cache } = contexts.Accounts;
const { useConnectionConfig } = contexts.Connection;
export const HomeView = () => {
const { marketEmitter, midPriceInUSD } = useMarkets();
const { tokenMap } = useConnectionConfig();
const [totals, setTotals] = useState<Totals>({
marketSize: 0,
numberOfAssets: 0,
items: [],
});
useEffect(() => {
const refreshTotal = () => {
let newTotals: Totals = {
marketSize: 0,
numberOfAssets: 0,
items: [],
};
[].forEach(item => {
const address = ''; // item.pubkey.toBase58()
const localCache = cache;
const liquidityMint = localCache.get(
address,
) as ParsedAccount<MintInfo>;
if (!liquidityMint) {
return;
}
const price = midPriceInUSD(liquidityMint?.pubkey.toBase58());
const marketCapLamports = 0;
const marketSize = fromLamports(marketCapLamports, liquidityMint?.info) * price;
let leaf = {
key: address,
marketSize,
nativeSize: 0,
name: getTokenName(tokenMap, address),
};
newTotals.items.push(leaf);
newTotals.marketSize = newTotals.marketSize + leaf.marketSize;
});
newTotals.items = newTotals.items.sort(
(a, b) => b.marketSize - a.marketSize,
);
setTotals(newTotals);
};
const dispose = marketEmitter.onMarket(() => {
refreshTotal();
});
refreshTotal();
return () => {
dispose();
};
}, [marketEmitter, midPriceInUSD, setTotals, tokenMap]);
return (
<div className="flexColumn">
<Row gutter={GUTTER} className="home-info-row">
<Col xs={24} xl={5}>
<Card>
<Statistic
title="Current market size"
value={totals.marketSize}
precision={2}
valueStyle={{ color: '#3fBB00' }}
prefix="$"
/>
</Card>
</Col>
<Col xs={24} xl={5}>
<Card>
<Statistic
title="Assets"
value={totals.numberOfAssets}
precision={2}
prefix="$"
/>
</Card>
</Col>
<Col xs={24} xl={5}>
<Card>
</Card>
</Col>
<Col xs={24} xl={9}>
<Card>
</Card>
</Col>
</Row>
<Card>
</Card>
</div>
);
};

View File

@ -0,0 +1,40 @@
import React, { useMemo } from 'react';
import { TokenIcon } from '../../components/TokenIcon';
import { Link } from 'react-router-dom';
import { PublicKey } from '@solana/web3.js';
import { contexts, hooks, utils } from '@oyster/common';
import { TotalItem } from '../../models/totals';
const { wadToLamports, formatNumber, fromLamports, formatPct } = utils;
const { useMint } = contexts.Accounts;
const { useTokenName } = hooks;
export const LendingReserveItem = (props: {
address: PublicKey;
item?: TotalItem;
}) => {
const name = ''; //useTokenName(props.reserve.liquidityMint);
const marketSize = 0;
return (
<Link to={`/reserve/${props.address.toBase58()}`}>
<div className="home-item">
<span style={{ display: 'flex' }}>
<TokenIcon mintAddress={props.address.toBase58()} />
{name}
</span>
<div title={marketSize.toString()}>
<div>
<div>
<em>{formatNumber.format(marketSize)}</em> {name}
</div>
<div className="dashboard-amount-quote">
${formatNumber.format(props.item?.marketSize)}
</div>
</div>
</div>
</div>
</Link>
);
};

View File

@ -0,0 +1,36 @@
@import '~antd/es/style/themes/dark.less';
.home-item {
display: flex;
justify-content: space-between;
align-items: center;
color: @text-color;
& > :nth-child(n) {
flex: 20%;
text-align: right;
margin: 10px 0px;
}
& > :first-child {
flex: 80px
}
border-bottom: 1px solid @border-color-split;
}
.home-header {
& > div {
flex: 20%;
text-align: right;
}
& > :first-child {
text-align: left;
flex: 80px
}
}
.home-info-row {
margin-bottom: 10px;
}

View File

@ -0,0 +1,2 @@
export { HomeView } from "./home";
export { FaucetView } from "./faucet";

View File

@ -0,0 +1,8 @@
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}

View File

@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"downlevelIteration": true,
"noEmit": true,
"jsx": "react",
"typeRoots": ["../../types"]
},
"include": ["src"]
}

View File

@ -0,0 +1,9 @@
declare module "buffer-layout" {
const bl: any;
export = bl;
}
declare module "jazzicon" {
const jazzicon: any;
export = jazzicon;
}

File diff suppressed because it is too large Load Diff

View File

@ -1527,8 +1527,7 @@
"@oyster/common": {
"version": "0.0.1",
"requires": {
"@ant-design/pro-layout": "^6.7.0",
"@craco/craco": "^5.7.0",
"@ledgerhq/hw-transport-webusb": "^5.41.0",
"@project-serum/serum": "^0.13.11",
"@project-serum/sol-wallet-adapter": "^0.1.4",
"@solana/spl-token": "0.0.13",
@ -1545,19 +1544,12 @@
"bn.js": "^5.1.3",
"bs58": "^4.0.1",
"buffer-layout": "^1.2.0",
"chart.js": "^2.9.4",
"craco-less": "^1.17.0",
"echarts": "^4.9.0",
"eventemitter3": "^4.0.7",
"identicon.js": "^2.3.3",
"jazzicon": "^1.5.0",
"lodash": "^4.17.20",
"react": "16.13.1",
"react-dom": "16.13.1",
"react-github-btn": "^1.2.0",
"react-intl": "^5.10.2",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.3",
"typescript": "^4.1.3"
},
"dependencies": {
@ -3089,6 +3081,58 @@
"@types/yargs": "^13.0.0"
}
},
"@ledgerhq/devices": {
"version": "5.43.0",
"resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-5.43.0.tgz",
"integrity": "sha512-/M5ZLUBdBK7Vl2T4yNJbES3Z4w55LbPdxD9rcOBAKH/5V3V0obQv6MUasP9b7DSkwGSSLCOGZLohoT2NxK2D2A==",
"requires": {
"@ledgerhq/errors": "^5.43.0",
"@ledgerhq/logs": "^5.43.0",
"rxjs": "^6.6.3",
"semver": "^7.3.4"
},
"dependencies": {
"semver": {
"version": "7.3.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz",
"integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==",
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"@ledgerhq/errors": {
"version": "5.43.0",
"resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-5.43.0.tgz",
"integrity": "sha512-ZjKlUQbIn/DHXAefW3Y1VyDrlVhVqqGnXzrqbOXuDbZ2OAIfSe/A1mrlCbWt98jP/8EJQBuCzBOtnmpXIL/nYg=="
},
"@ledgerhq/hw-transport": {
"version": "5.43.0",
"resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-5.43.0.tgz",
"integrity": "sha512-0S+TGmiEJOqgM2MWnolZQPVKU3oRtoDj4yUFUZts9Owbgby+hmo4dIKTvv0vs8mwknQbOZByUgh3MQOQiK70MQ==",
"requires": {
"@ledgerhq/devices": "^5.43.0",
"@ledgerhq/errors": "^5.43.0",
"events": "^3.2.0"
}
},
"@ledgerhq/hw-transport-webusb": {
"version": "5.43.0",
"resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport-webusb/-/hw-transport-webusb-5.43.0.tgz",
"integrity": "sha512-Mf/qRn8cvK20cqqNtxFfpKVut8BvSvXkq/9HSArV7AUk+a6wga2VEvPlfk8xC551dkJlfln6+nECZ9KIEq9hFw==",
"requires": {
"@ledgerhq/devices": "^5.43.0",
"@ledgerhq/errors": "^5.43.0",
"@ledgerhq/hw-transport": "^5.43.0",
"@ledgerhq/logs": "^5.43.0"
}
},
"@ledgerhq/logs": {
"version": "5.43.0",
"resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-5.43.0.tgz",
"integrity": "sha512-QWfQjea3ekh9ZU+JeL2tJC9cTKLZ/JrcS0JGatLejpRYxQajvnHvHfh0dbHOKXEaXfCskEPTZ3f1kzuts742GA=="
},
"@mrmlnc/readdir-enhanced": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz",