WIP commit on proposals, working on a dummy button to create a txn

This commit is contained in:
Dummy Tester 123 2021-02-15 18:48:48 -06:00
parent aab7b80c32
commit af81a3f644
40 changed files with 37486 additions and 33 deletions

View File

@ -1,12 +1,27 @@
import { useLocalStorageState } from '../utils/utils';
import { Account, clusterApiUrl, Connection, Transaction, TransactionInstruction } from '@solana/web3.js';
import {
Account,
clusterApiUrl,
Connection,
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { notify } from '../utils/notifications';
import { ExplorerLink } from '../components/ExplorerLink';
import { setProgramIds } from '../utils/ids';
import { TokenInfo, TokenListProvider, ENV as ChainId } from '@solana/spl-token-registry';
import {
TokenInfo,
TokenListProvider,
ENV as ChainId,
} from '@solana/spl-token-registry';
export type ENV = 'mainnet-beta' | 'testnet' | 'devnet' | 'localnet' | 'lending';
export type ENV =
| 'mainnet-beta'
| 'testnet'
| 'devnet'
| 'localnet'
| 'lending';
export const ENDPOINTS = [
{
@ -19,9 +34,16 @@ export const ENDPOINTS = [
endpoint: clusterApiUrl('testnet'),
ChainId: ChainId.Testnet,
},
{ name: 'devnet' as ENV, endpoint: clusterApiUrl('devnet'),
ChainId: ChainId.Devnet, },
{ name: 'localnet' as ENV, endpoint: 'http://127.0.0.1:8899', ChainId: ChainId.Devnet, },
{
name: 'devnet' as ENV,
endpoint: clusterApiUrl('devnet'),
ChainId: ChainId.Devnet,
},
{
name: 'localnet' as ENV,
endpoint: 'http://127.0.0.1:8899',
ChainId: ChainId.Devnet,
},
{
name: 'Oyster Dev' as ENV,
endpoint: 'http://oyster-dev.solana.com/',
@ -62,31 +84,47 @@ const ConnectionContext = React.createContext<ConnectionConfig>({
});
export function ConnectionProvider({ children = undefined as any }) {
const [endpoint, setEndpoint] = useLocalStorageState('connectionEndpts', ENDPOINTS[0].endpoint);
const [endpoint, setEndpoint] = useLocalStorageState(
'connectionEndpts',
ENDPOINTS[0].endpoint,
);
const [slippage, setSlippage] = useLocalStorageState('slippage', DEFAULT_SLIPPAGE.toString());
const [slippage, setSlippage] = useLocalStorageState(
'slippage',
DEFAULT_SLIPPAGE.toString(),
);
const connection = useMemo(() => new Connection(endpoint, 'recent'), [endpoint]);
const sendConnection = useMemo(() => new Connection(endpoint, 'recent'), [endpoint]);
const connection = useMemo(() => new Connection(endpoint, 'recent'), [
endpoint,
]);
const sendConnection = useMemo(() => new Connection(endpoint, 'recent'), [
endpoint,
]);
const env = ENDPOINTS.find((end) => end.endpoint === endpoint)?.name || ENDPOINTS[0].name;
const env =
ENDPOINTS.find(end => end.endpoint === endpoint)?.name || ENDPOINTS[0].name;
const [tokens, setTokens] = useState<TokenInfo[]>([]);
const [tokenMap, setTokenMap] = useState<Map<string, TokenInfo>>(new Map());
useEffect(() => {
// fetch token files
new TokenListProvider().resolve()
.then((container) => {
const list = container.excludeByTag("nft").filterByChainId(ENDPOINTS.find((end) => end.endpoint === endpoint)?.ChainId || ChainId.MainnetBeta).getList();
new TokenListProvider().resolve().then(container => {
const list = container
.excludeByTag('nft')
.filterByChainId(
ENDPOINTS.find(end => end.endpoint === endpoint)?.ChainId ||
ChainId.MainnetBeta,
)
.getList();
const knownMints = [...list].reduce((map, item) => {
map.set(item.address, item);
return map;
}, new Map<string, TokenInfo>());
const knownMints = [...list].reduce((map, item) => {
map.set(item.address, item);
return map;
}, new Map<string, TokenInfo>());
setTokenMap(knownMints);
setTokens(list);
});
setTokenMap(knownMints);
setTokens(list);
});
}, [env]);
setProgramIds(env);
@ -109,7 +147,10 @@ export function ConnectionProvider({ children = undefined as any }) {
}, [connection]);
useEffect(() => {
const id = sendConnection.onAccountChange(new Account().publicKey, () => {});
const id = sendConnection.onAccountChange(
new Account().publicKey,
() => {},
);
return () => {
sendConnection.removeAccountChangeListener(id);
};
@ -128,7 +169,7 @@ export function ConnectionProvider({ children = undefined as any }) {
endpoint,
setEndpoint,
slippage: parseFloat(slippage),
setSlippage: (val) => setSlippage(val.toString()),
setSlippage: val => setSlippage(val.toString()),
connection,
sendConnection,
tokens,
@ -173,7 +214,7 @@ const getErrorForTransaction = async (connection: Connection, txid: string) => {
const errors: string[] = [];
if (tx?.meta && tx.meta.logMessages) {
tx.meta.logMessages.forEach((log) => {
tx.meta.logMessages.forEach(log => {
const regex = /Error: (.*)/gm;
let m;
while ((m = regex.exec(log)) !== null) {
@ -200,12 +241,14 @@ export const sendTransaction = async (
awaitConfirmation = true,
) => {
let transaction = new Transaction();
instructions.forEach((instruction) => transaction.add(instruction));
transaction.recentBlockhash = (await connection.getRecentBlockhash('max')).blockhash;
instructions.forEach(instruction => transaction.add(instruction));
transaction.recentBlockhash = (
await connection.getRecentBlockhash('max')
).blockhash;
transaction.setSigners(
// fee payied by the wallet owner
wallet.publicKey,
...signers.map((s) => s.publicKey)
...signers.map(s => s.publicKey),
);
if (signers.length > 0) {
transaction.partialSign(...signers);
@ -221,7 +264,10 @@ export const sendTransaction = async (
let slot = 0;
if (awaitConfirmation) {
const confirmation = (await connection.confirmTransaction(txid, options && (options.commitment as any)));
const confirmation = await connection.confirmTransaction(
txid,
options && (options.commitment as any),
);
const status = confirmation.value;
slot = confirmation.context.slot;
@ -231,16 +277,18 @@ export const sendTransaction = async (
message: 'Transaction failed...',
description: (
<>
{errors.map((err) => (
{errors.map(err => (
<div>{err}</div>
))}
<ExplorerLink address={txid} type='transaction' />
<ExplorerLink address={txid} type="transaction" />
</>
),
type: 'error',
});
throw new Error(`Raw transaction ${txid} failed (${JSON.stringify(status)})`);
throw new Error(
`Raw transaction ${txid} failed (${JSON.stringify(status)})`,
);
}
}

View File

@ -90,9 +90,9 @@ export const PROGRAM_IDS = [
name: 'devnet',
timelock: () => ({
programAccountId: new PublicKey(
'9gBhDCCKV7KELLFRY8sAJZXqDmvUfmNzFzpB2b4FUVVr',
'4GQdZW6Un8ffGhhVZ1mx2vFjgiBXoGyCqYGugVwCm6G5',
),
programId: new PublicKey('9iAeqqppjn7g1Jn8o2cQCqU5aQVV3h4q9bbWdKRbeC2w'),
programId: new PublicKey('GVRKB2HsYmq1JdQiLSSq776NcahBuMprcCDFud9jcg39'),
}),
wormhole: () => ({
pubkey: new PublicKey('WormT3McKhFJ2RkiGpdw9GKvNCrB2aB54gb2uV9MfQC'),

5
packages/proposals/.env Normal file
View File

@ -0,0 +1,5 @@
# HOST Public Key used for additional swap fees
LEND_HOST_FEE_ADDRESS=''
# Rewired variables to comply with CRA restrictions
REACT_APP_LEND_HOST_FEE_ADDRESS=$LEND_HOST_FEE_ADDRESS

View File

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

View File

@ -0,0 +1,23 @@
const CracoLessPlugin = require('craco-less');
const path = require('path');
const fs = require('fs');
// Handle relative paths to sibling packages
const appDirectory = fs.realpathSync(process.cwd());
const resolvePackage = relativePath => path.resolve(appDirectory, relativePath);
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { '@primary-color': '#2abdd2' },
javascriptEnabled: true,
},
},
},
},
],
};

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,99 @@
{
"name": "proposals",
"version": "0.0.1",
"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",
"start:lending": "craco start --verbose",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"localnet:update": "solana-localnet update",
"localnet:up": "rm client/util/store/config.json; set -x; solana-localnet down; set -e; solana-localnet up",
"localnet:down": "solana-localnet down",
"localnet:logs": "solana-localnet logs -f",
"predeploy": "git pull --ff-only && yarn && yarn build",
"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"
},
"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="Oyster Lending on Solana" />
<!--
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>Oyster Lending | Solana</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": "Swap | Serum",
"name": "Swap | Serum",
"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:

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 216 KiB

View File

@ -0,0 +1,321 @@
@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%;
}
.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-pro-basicLayout {
min-height: calc(100% - 30px) !important;
}
.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,299 @@
import {
Account,
Connection,
PublicKey,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js';
import { contexts, utils, actions } from '@oyster/common';
import { AccountLayout, MintLayout } from '@solana/spl-token';
import { initTimelockSetInstruction } from '../models/initTimelockSet';
import {
ConsensusAlgorithm,
ExecutionType,
TimelockSetLayout,
TimelockType,
} from '../models/timelock';
import { addSignatoryMintInstruction } from '../models/addSignatoryMint';
import { addVotingMintInstruction } from '../models/addVotingMint';
import { createSign } from 'crypto';
const { sendTransaction } = contexts.Connection;
const { createUninitializedMint } = actions;
const { notify } = utils;
export const createProposal = async (connection: Connection, wallet: any) => {
notify({
message: 'Initializing Proposal...',
description: 'Please wait...',
type: 'warn',
});
const PROGRAM_IDS = utils.programIds();
let signers: Account[] = [];
let instructions: TransactionInstruction[] = [];
const mintRentExempt = await connection.getMinimumBalanceForRentExemption(
MintLayout.span,
);
const accountRentExempt = await connection.getMinimumBalanceForRentExemption(
AccountLayout.span,
);
const timelockRentExempt = await connection.getMinimumBalanceForRentExemption(
TimelockSetLayout.span,
);
const timelockSetKey = new Account();
const uninitializedTimelockSetInstruction = SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: timelockSetKey.publicKey,
lamports: timelockRentExempt,
space: TimelockSetLayout.span,
programId: PROGRAM_IDS.timelock.programAccountId,
});
signers.push(timelockSetKey);
instructions.push(uninitializedTimelockSetInstruction);
const adminMint = createUninitializedMint(
instructions,
wallet.publicKey,
mintRentExempt,
signers,
);
const adminValidationKey = new Account();
const adminValidationInstruction = SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: adminValidationKey.publicKey,
lamports: mintRentExempt,
space: MintLayout.span,
programId: PROGRAM_IDS.timelock.programAccountId,
});
instructions.push(adminValidationInstruction);
signers.push(adminValidationKey);
const destinationAdminKey = new Account();
const destinationAdminInstruction = SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: destinationAdminKey.publicKey,
lamports: accountRentExempt,
space: AccountLayout.span,
programId: wallet.publicKey,
});
instructions.push(destinationAdminInstruction);
signers.push(destinationAdminKey);
instructions.push(
initTimelockSetInstruction(
timelockSetKey.publicKey,
adminMint,
adminValidationKey.publicKey,
destinationAdminKey.publicKey,
wallet.publicKey,
{
timelockType: TimelockType.CustomSingleSignerV1,
consensusAlgorithm: ConsensusAlgorithm.Majority,
executionType: ExecutionType.AllOrNothing,
},
),
);
try {
let tx = await sendTransaction(
connection,
wallet,
instructions,
signers,
true,
);
notify({
message: 'Proposal created.',
type: 'success',
description: `Transaction - ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
/*await createSignatoryMint({
connection,
wallet,
mintRentExempt,
timelockSetKey,
destinationAdminKey,
adminMint,
accountRentExempt,
});
await createVotingMint({
connection,
wallet,
mintRentExempt,
timelockSetKey,
destinationAdminKey,
adminMint,
accountRentExempt,
});*/
};
interface MintParams {
connection: Connection;
wallet: any;
mintRentExempt: number;
timelockSetKey: Account;
destinationAdminKey: Account;
adminMint: PublicKey;
accountRentExempt: number;
}
async function createSignatoryMint(args: MintParams) {
const {
connection,
wallet,
mintRentExempt,
timelockSetKey,
destinationAdminKey,
adminMint,
accountRentExempt,
} = args;
const PROGRAM_IDS = utils.programIds();
let signatoryInstructions: TransactionInstruction[] = [];
let signatorySigners: Account[] = [];
const signatoryMint = createUninitializedMint(
signatoryInstructions,
wallet.publicKey,
mintRentExempt,
signatorySigners,
);
const signatoryValidationKey = new Account();
const signatoryValidationInstruction = SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: signatoryValidationKey.publicKey,
lamports: mintRentExempt,
space: MintLayout.span,
programId: PROGRAM_IDS.timelock.programAccountId,
});
signatoryInstructions.push(signatoryValidationInstruction);
signatorySigners.push(signatoryValidationKey);
const destinationSignatoryKey = new Account();
const destinationSignatoryInstruction = SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: destinationSignatoryKey.publicKey,
lamports: accountRentExempt,
space: AccountLayout.span,
programId: wallet.publicKey,
});
signatoryInstructions.push(destinationSignatoryInstruction);
signatorySigners.push(destinationSignatoryKey);
signatoryInstructions.push(
addSignatoryMintInstruction(
timelockSetKey.publicKey,
destinationAdminKey.publicKey,
adminMint,
signatoryMint,
signatoryValidationKey.publicKey,
destinationSignatoryKey.publicKey,
),
);
notify({
message: 'Adding you as a Signatory...',
type: 'warn',
description: 'Please wait...',
});
try {
let tx = await sendTransaction(
connection,
wallet,
signatoryInstructions,
signatorySigners,
true,
);
notify({
message: 'Signatory added!',
type: 'success',
description: `Transaction - ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
}
async function createVotingMint(args: MintParams) {
const {
connection,
wallet,
mintRentExempt,
timelockSetKey,
destinationAdminKey,
adminMint,
} = args;
const PROGRAM_IDS = utils.programIds();
let votingInstructions: TransactionInstruction[] = [];
let votingSigners: Account[] = [];
const votingMint = createUninitializedMint(
votingInstructions,
wallet.publicKey,
mintRentExempt,
votingSigners,
);
const votingValidationKey = new Account();
const votingValidationInstruction = SystemProgram.createAccount({
fromPubkey: wallet.publicKey,
newAccountPubkey: votingValidationKey.publicKey,
lamports: mintRentExempt,
space: MintLayout.span,
programId: PROGRAM_IDS.timelock.programAccountId,
});
votingInstructions.push(votingValidationInstruction);
votingSigners.push(votingValidationKey);
votingInstructions.push(
addVotingMintInstruction(
timelockSetKey.publicKey,
destinationAdminKey.publicKey,
adminMint,
votingMint,
votingValidationKey.publicKey,
),
);
notify({
message: 'Adding Voting Mint...',
type: 'warn',
description: 'Please wait...',
});
try {
let tx = await sendTransaction(
connection,
wallet,
votingInstructions,
votingSigners,
true,
);
notify({
message: 'Voting Mint added!',
type: 'success',
description: `Transaction - ${tx}`,
});
} catch (ex) {
console.error(ex);
throw new Error();
}
}

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,121 @@
import React from 'react';
import './../../App.less';
import { Menu } from 'antd';
import {
PieChartOutlined,
GithubOutlined,
HomeOutlined,
ForkOutlined,
// LineChartOutlined
} from '@ant-design/icons';
import BasicLayout from '@ant-design/pro-layout';
import { Link, useLocation } from 'react-router-dom';
import { LABELS } from '../../constants';
import config from './../../../package.json';
import { contexts, components } from '@oyster/common';
const { AppBar } = components;
const { useConnectionConfig } = contexts.Connection;
export const AppLayout = React.memo((props: any) => {
const { env } = useConnectionConfig();
const location = useLocation();
const paths: { [key: string]: string } = {
'/dashboard': '2',
};
const current =
[...Object.keys(paths)].find(key => location.pathname.startsWith(key)) ||
'';
const defaultKey = paths[current] || '1';
const theme = 'light';
return (
<div className="App">
<div className="Banner">
<div className="Banner-description">{LABELS.AUDIT_WARNING}</div>
</div>
<BasicLayout
title={LABELS.APP_TITLE}
footerRender={() => (
<div className="footer" title={LABELS.FOOTER}>
{LABELS.FOOTER}
</div>
)}
navTheme={theme}
headerTheme={theme}
theme={theme}
layout="mix"
fixSiderbar={true}
primaryColor="#d83aeb"
logo={<div className="App-logo" />}
rightContentRender={() => <AppBar />}
links={[]}
menuContentRender={() => {
return (
<div className="links">
<Menu
theme={theme}
defaultSelectedKeys={[defaultKey]}
mode="inline"
>
<Menu.Item key="1" icon={<HomeOutlined />}>
<Link
to={{
pathname: '/',
}}
>
{LABELS.MENU_HOME}
</Link>
</Menu.Item>
<Menu.Item key="2" icon={<PieChartOutlined />}>
<Link
to={{
pathname: '/dashboard',
}}
>
{LABELS.MENU_DASHBOARD}
</Link>
</Menu.Item>
</Menu>
<Menu
theme={theme}
defaultSelectedKeys={[defaultKey]}
selectable={false}
mode="inline"
className="bottom-links"
>
<Menu.Item key="16" icon={<ForkOutlined />}>
<a
title="Fork"
href={`${config.repository.url}/fork`}
target="_blank"
rel="noopener noreferrer"
>
Fork
</a>
</Menu.Item>
,
<Menu.Item key="15" icon={<GithubOutlined />}>
<a
title="Gtihub"
href={config.repository.url}
target="_blank"
rel="noopener noreferrer"
>
Github
</a>
</Menu.Item>
</Menu>
</div>
);
}}
>
{props.children}
</BasicLayout>
</div>
);
});

View File

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

View File

@ -0,0 +1,15 @@
export const LABELS = {
CONNECT_LABEL: 'Connect Wallet',
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_DASHBOARD: 'Dashboard',
APP_TITLE: 'Oyster Proposals',
CONNECT_BUTTON: 'Connect',
WALLET_TOOLTIP: 'Wallet public key',
WALLET_BALANCE: 'Wallet balance',
SETTINGS_TOOLTIP: 'Settings',
DASHBOARD_INFO: 'Connect to a wallet to view proposals.',
};

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,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": "Oyster Proposals",
"short_name": "Oyster Proposals",
"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,62 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import { TimelockConfig, TimelockInstruction } from './timelock';
/// Adds signatory mint to new timelock set. Gives signatory token to admin caller.
///
/// 0. `[writable]` Uninitialized Timelock set account
/// 1. `[writable]` Initialized Admin Token account
/// 2. `[writable]` Initialized Admin mint account
/// 3. `[writable]` Uninitialized Signatory Mint account
/// 4. `[writable]` Uninitialized Signatory Validation account
/// 5. `[writable]` Uninitialized Destination account for first signatory token
/// 6. `[]` Timelock Program
/// 7. '[]` Token program id
/// 8. `[]` Rent sysvar
export const addSignatoryMintInstruction = (
timelockSetAccount: PublicKey,
adminAccount: PublicKey,
adminMintAccount: PublicKey,
signatoryMintAccount: PublicKey,
signatoryValidationAccount: PublicKey,
destinationSignatoryAccount: PublicKey,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.AddSignatoryMint,
},
data,
);
const keys = [
{ pubkey: timelockSetAccount, isSigner: true, isWritable: true },
{ pubkey: adminAccount, isSigner: true, isWritable: true },
{ pubkey: adminMintAccount, isSigner: true, isWritable: true },
{ pubkey: signatoryMintAccount, isSigner: true, isWritable: true },
{ pubkey: signatoryValidationAccount, isSigner: true, isWritable: true },
{ pubkey: destinationSignatoryAccount, isSigner: true, isWritable: true },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -0,0 +1,60 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import { TimelockConfig, TimelockInstruction } from './timelock';
import BN from 'bn.js';
/// Adds voting mint to new timelock set.
///
/// 0. `[writable]` Uninitialized Timelock set account
/// 1. `[writable]` Initialized Admin Token account
/// 2. `[writable]` Initialized Admin mint account
/// 3. `[writable]` Uninitialized Voting Mint account
/// 4. `[writable]` Uninitialized Voting Validation account
/// 5. `[]` Timelock Program
/// 6. '[]` Token program id
/// 7. `[]` Rent sysvar
export const addVotingMintInstruction = (
timelockSetAccount: PublicKey,
adminAccount: PublicKey,
adminMintAccount: PublicKey,
votingMintAccount: PublicKey,
votingValidationAccount: PublicKey,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.AddSignatoryMint,
},
data,
);
const keys = [
{ pubkey: timelockSetAccount, isSigner: true, isWritable: true },
{ pubkey: adminAccount, isSigner: true, isWritable: true },
{ pubkey: adminMintAccount, isSigner: true, isWritable: true },
{ pubkey: votingMintAccount, isSigner: true, isWritable: true },
{ pubkey: votingValidationAccount, isSigner: true, isWritable: true },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -0,0 +1,70 @@
import {
PublicKey,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from '@solana/web3.js';
import { utils } from '@oyster/common';
import * as BufferLayout from 'buffer-layout';
import { TimelockConfig, TimelockInstruction } from './timelock';
import BN from 'bn.js';
/// Initializes a new empty Timelocked set of Instructions that will be executed at various slots in the future in draft mode.
/// Grants Admin token to caller.
///
/// 1. `[writable]` Uninitialized Timelock set account
/// 2. `[writable]` Uninitialized Admin Mint account
/// 3. `[writable]` Uninitialized Admin Validation account
/// 4. `[writable]` Uninitialized Destination account for first admin token
/// 5. `[]` Wallet pubkey - owner of destination account for first admin token
/// 6. `[]` Timelock Program
/// 7. '[]` Token program id
/// 8. `[]` Rent sysvar
export const initTimelockSetInstruction = (
timelockSetAccount: PublicKey,
adminMintAccount: PublicKey,
adminValidationAccount: PublicKey,
destinationAdminAccount: PublicKey,
wallet: PublicKey,
timelockConfig: TimelockConfig,
): TransactionInstruction => {
const PROGRAM_IDS = utils.programIds();
const dataLayout = BufferLayout.struct([
BufferLayout.u8('instruction'),
BufferLayout.u8('consensusAlgorithm'),
BufferLayout.u8('executionType'),
BufferLayout.u8('timelockType'),
]);
const data = Buffer.alloc(dataLayout.span);
dataLayout.encode(
{
instruction: TimelockInstruction.InitTimelockSet,
consensusAlgorithm: new BN(timelockConfig.consensusAlgorithm),
executionType: new BN(timelockConfig.executionType),
timelockType: new BN(timelockConfig.timelockType),
},
data,
);
const keys = [
{ pubkey: timelockSetAccount, isSigner: true, isWritable: true },
{ pubkey: adminMintAccount, isSigner: true, isWritable: true },
{ pubkey: adminValidationAccount, isSigner: true, isWritable: true },
{ pubkey: destinationAdminAccount, isSigner: true, isWritable: true },
{ pubkey: wallet, isSigner: true, isWritable: false },
{
pubkey: PROGRAM_IDS.timelock.programAccountId,
isSigner: false,
isWritable: false,
},
{ pubkey: PROGRAM_IDS.token, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
];
return new TransactionInstruction({
keys,
programId: PROGRAM_IDS.timelock.programId,
data,
});
};

View File

@ -0,0 +1,158 @@
import * as Layout from '../utils/layout';
import * as BufferLayout from 'buffer-layout';
import BN from 'bn.js';
import { AccountInfo, PublicKey } from '@solana/web3.js';
export enum TimelockInstruction {
InitTimelockSet = 1,
AddSignatoryMint = 11,
AddVotingMint = 12,
}
export interface TimelockConfig {
consensusAlgorithm: ConsensusAlgorithm;
executionType: ExecutionType;
timelockType: TimelockType;
}
export enum ConsensusAlgorithm {
Majority = 0,
SuperMajority = 1,
FullConsensus = 2,
}
export enum ExecutionType {
AllOrNothing = 0,
AnyAboveVoteFinishSlot = 1,
}
export enum TimelockType {
CustomSingleSignerV1 = 0,
}
export enum TimelockStateStatus {
/// Draft
Draft = 0,
/// Taking votes
Voting = 1,
/// Votes complete, in execution phase
Executing = 2,
/// Completed, can be rebooted
Completed = 3,
/// Deleted
Deleted = 4,
}
export interface TimelockState {
status: TimelockStateStatus;
totalVotingTokensMinted: BN;
timelockTransactions: PublicKey[];
}
export const TimelockSetLayout: typeof BufferLayout.Structure = BufferLayout.struct(
[
BufferLayout.u8('version'),
Layout.publicKey('signatoryMint'),
Layout.publicKey('adminMint'),
Layout.publicKey('votingMint'),
Layout.publicKey('signatoryValidation'),
Layout.publicKey('adminValidation'),
Layout.publicKey('votingValidation'),
BufferLayout.u8('timelockStateStatus'),
Layout.uint64('totalVotingTokensMinted'),
Layout.publicKey('timelockTxn1'),
Layout.publicKey('timelockTxn2'),
Layout.publicKey('timelockTxn3'),
Layout.publicKey('timelockTxn4'),
Layout.publicKey('timelockTxn5'),
Layout.publicKey('timelockTxn6'),
Layout.publicKey('timelockTxn7'),
Layout.publicKey('timelockTxn8'),
Layout.publicKey('timelockTxn9'),
Layout.publicKey('timelockTxn10'),
BufferLayout.u8('consensusAlgorithm'),
BufferLayout.u8('executionType'),
BufferLayout.u8('timelockType'),
],
);
export interface TimelockSet {
/// Version of the struct
version: number;
/// Mint that creates signatory tokens of this instruction
/// If there are outstanding signatory tokens, then cannot leave draft state. Signatories must burn tokens (ie agree
/// to move instruction to voting state) and bring mint to net 0 tokens outstanding. Each signatory gets 1 (serves as flag)
signatoryMint: PublicKey;
/// Admin ownership mint. One token is minted, can be used to grant admin status to a new person.
adminMint: PublicKey;
/// Mint that creates voting tokens of this instruction
votingMint: PublicKey;
/// Used to validate signatory tokens in a round trip transfer
signatoryValidation: PublicKey;
/// Used to validate admin tokens in a round trip transfer
adminValidation: PublicKey;
/// Used to validate voting tokens in a round trip transfer
votingValidation: PublicKey;
/// Reserve state
state: TimelockState;
/// configuration values
config: TimelockConfig;
}
export const TimelockSetParser = (
pubKey: PublicKey,
info: AccountInfo<Buffer>,
) => {
const buffer = Buffer.from(info.data);
const data = TimelockSetLayout.decode(buffer);
const details = {
pubkey: pubKey,
account: {
...info,
},
info: {
version: data.version,
signatoryMint: data.signatoryMint,
adminMint: data.adminMint,
votingMint: data.votingMint,
signatoryValidation: data.signatoryValidation,
adminValidation: data.adminValidation,
votingValidation: data.votingValidation,
state: {
status: TimelockStateStatus[data.timelockStateStatus],
totalVotingTokensMinted: data.totalVotingTokensMinted,
timelockTransactions: [
data.timelockTxn1,
data.timelockTxn2,
data.timelockTxn3,
data.timelockTxn4,
data.timelockTxn5,
data.timelockTxn6,
data.timelockTxn7,
data.timelockTxn8,
data.timelockTxn9,
data.timelockTxn10,
],
},
config: {
consensusAlgorithm: data.consensusAlgorithm,
executionType: data.executionType,
timelockType: data.timelockType,
},
},
};
return details;
};

View File

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

View File

@ -0,0 +1,30 @@
import { HashRouter, Route, Switch } from 'react-router-dom';
import React from 'react';
import { contexts } from '@oyster/common';
import { AppLayout } from './components/Layout';
import { DashboardView, HomeView } from './views';
const { WalletProvider } = contexts.Wallet;
const { ConnectionProvider } = contexts.Connection;
const { AccountsProvider } = contexts.Accounts;
export function Routes() {
return (
<>
<HashRouter basename={'/'}>
<ConnectionProvider>
<WalletProvider>
<AccountsProvider>
<AppLayout>
<Switch>
<Route exact path="/" component={() => <HomeView />} />
<Route exact path="/dashboard" children={<DashboardView />} />
</Switch>
</AppLayout>
</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,16 @@
import { Card, Col, Row } from 'antd';
import React from 'react';
import { GUTTER, LABELS } from '../../constants';
import { contexts } from '@oyster/common';
import './style.less';
const { useWallet } = contexts.Wallet;
export const DashboardView = () => {
const { connected } = useWallet();
return (
<div className="dashboard-container">
<Row gutter={GUTTER}></Row>
</div>
);
};

View File

@ -0,0 +1,84 @@
@import '~antd/es/style/themes/dark.less';
.dashboard-item {
display: flex;
justify-content: space-between;
align-items: center;
color: @text-color;
& > :nth-child(n) {
flex: 15%;
text-align: right;
white-space: nowrap;
margin: 10px 0px;
}
& > :first-child {
flex: 50px;
}
& > :last-child {
flex: 200px
}
border-bottom: 1px solid @border-color-split;
}
.dashboard-title {
display: flex;
& > :first-child {
margin-left: 0px;
margin-right: auto;
}
& > :last-child {
span {
font-size: 10px;
color: gray
}
}
}
.dashboard-header {
& > :nth-child(n) {
flex: 15%;
text-align: right;
}
& > :first-child {
text-align: left;
flex: 50px;
}
& > :last-child {
flex: 200px;
}
}
.dashboard-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
flex: 1;
& > :first-child {
flex: 1;
}
}
.dashboard-info {
display: flex;
flex-direction: column;
align-self: center;
justify-content: center;
flex: 1;
.dashboard-splash {
max-width: 400px;
width: 100%;
align-self: center;
}
}

View File

@ -0,0 +1,21 @@
import { Row } from 'antd';
import React from 'react';
import { GUTTER } from '../../constants';
import { Button } from 'antd';
import { createProposal } from '../../actions/createProposal';
import { contexts } from '@oyster/common';
const { useWallet } = contexts.Wallet;
const { useConnection } = contexts.Connection;
export const HomeView = () => {
const wallet = useWallet();
const connection = useConnection();
return (
<div className="flexColumn">
<Row gutter={GUTTER} className="home-info-row">
<Button onClick={() => createProposal(connection, wallet.wallet)}>
Click me
</Button>
</Row>
</div>
);
};

View File

@ -0,0 +1,2 @@
export { HomeView } from './home';
export { DashboardView } from './dashboard';

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