feat: add bridge
This commit is contained in:
parent
9c92d884c4
commit
4b5954d627
|
@ -0,0 +1 @@
|
|||
GENERATE_SOURCEMAP = false
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import React from "react";
|
||||
import "./App.less";
|
||||
import { Routes } from "./routes";
|
||||
|
||||
function App() {
|
||||
return <Routes />;
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1 @@
|
|||
export const nop = () => {};
|
|
@ -0,0 +1,5 @@
|
|||
@import '~antd/es/style/themes/dark.less';
|
||||
@import "~antd/dist/antd.dark.less";
|
||||
|
||||
@primary-color: #ff00a8;
|
||||
@popover-background: #1a2029;
|
|
@ -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;
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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} />
|
||||
);
|
||||
};
|
|
@ -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}; */
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
});
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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"
|
||||
>
|
||||
{' '}
|
||||
{hasBalance
|
||||
? balance < 0.001
|
||||
? '<0.001'
|
||||
: balance.toFixed(3)
|
||||
: '-'}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from './labels';
|
||||
export * from './style';
|
|
@ -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',
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export const nop = () => {};
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export * from './market';
|
|
@ -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;
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './dex';
|
|
@ -0,0 +1,2 @@
|
|||
// use to override serum market to use specifc mint
|
||||
export const MINT_TO_MARKET: { [key: string]: string } = {};
|
|
@ -0,0 +1,12 @@
|
|||
export interface TotalItem {
|
||||
key: string;
|
||||
marketSize: number;
|
||||
nativeSize: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Totals {
|
||||
marketSize: number;
|
||||
numberOfAssets: number;
|
||||
items: TotalItem[];
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="react-scripts" />
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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';
|
|
@ -0,0 +1,9 @@
|
|||
declare module 'buffer-layout' {
|
||||
const bl: any;
|
||||
export = bl;
|
||||
}
|
||||
|
||||
declare module 'jazzicon' {
|
||||
const jazzicon: any;
|
||||
export = jazzicon;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
declare module '@project-serum/sol-wallet-adapter' {
|
||||
const adapter: any;
|
||||
export = adapter;
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export { HomeView } from "./home";
|
||||
export { FaucetView } from "./faucet";
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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"]
|
||||
}
|
|
@ -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
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue