├── .github
├── dependabot.yml
└── workflows
│ └── break_action.yml
├── .gitignore
├── .mergify.yml
├── LICENSE
├── Procfile
├── README.md
├── app.json
├── client
├── .eslintignore
├── .gitignore
├── .prettierignore
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── google.svg
│ ├── index.html
│ ├── manifest.json
│ ├── rainbow192.png
│ ├── rainbow512.png
│ └── robots.txt
├── setupJest.ts
├── src
│ ├── __tests__
│ │ └── bytes.ts
│ ├── app.tsx
│ ├── components
│ │ ├── ClusterModal.tsx
│ │ ├── ClusterStatusButton.tsx
│ │ ├── ConfigurationSidebar.tsx
│ │ ├── Header.tsx
│ │ ├── LoadingModal.tsx
│ │ ├── PaymentCard.tsx
│ │ ├── SlotTableRow.tsx
│ │ ├── TxContainer.tsx
│ │ ├── TxModal.tsx
│ │ ├── TxSquare.tsx
│ │ └── TxTableRow.tsx
│ ├── fonts
│ │ ├── cerebrisans
│ │ │ ├── cerebrisans-bold.eot
│ │ │ ├── cerebrisans-bold.svg
│ │ │ ├── cerebrisans-bold.ttf
│ │ │ ├── cerebrisans-bold.woff
│ │ │ ├── cerebrisans-book.eot
│ │ │ ├── cerebrisans-book.svg
│ │ │ ├── cerebrisans-book.ttf
│ │ │ ├── cerebrisans-book.woff
│ │ │ ├── cerebrisans-medium.eot
│ │ │ ├── cerebrisans-medium.svg
│ │ │ ├── cerebrisans-medium.ttf
│ │ │ ├── cerebrisans-medium.woff
│ │ │ ├── cerebrisans-regular.eot
│ │ │ ├── cerebrisans-regular.svg
│ │ │ ├── cerebrisans-regular.ttf
│ │ │ ├── cerebrisans-regular.woff
│ │ │ ├── cerebrisans-semibold.eot
│ │ │ ├── cerebrisans-semibold.svg
│ │ │ ├── cerebrisans-semibold.ttf
│ │ │ └── cerebrisans-semibold.woff
│ │ └── feather
│ │ │ ├── feather.css
│ │ │ └── fonts
│ │ │ ├── Feather.svg
│ │ │ ├── Feather.ttf
│ │ │ └── Feather.woff
│ ├── images
│ │ ├── SOLANA-preview.png
│ │ ├── break.svg
│ │ ├── dashkit
│ │ │ ├── covers
│ │ │ │ ├── auth-side-cover.jpg
│ │ │ │ ├── header-cover.jpg
│ │ │ │ ├── profile-cover-1.jpg
│ │ │ │ ├── profile-cover-2.jpg
│ │ │ │ ├── profile-cover-3.jpg
│ │ │ │ ├── profile-cover-4.jpg
│ │ │ │ ├── profile-cover-5.jpg
│ │ │ │ ├── profile-cover-6.jpg
│ │ │ │ ├── profile-cover-7.jpg
│ │ │ │ ├── profile-cover-8.jpg
│ │ │ │ ├── sidebar-cover.jpg
│ │ │ │ └── team-cover.jpg
│ │ │ └── masks
│ │ │ │ ├── avatar-group-hover-last.svg
│ │ │ │ ├── avatar-group-hover.svg
│ │ │ │ ├── avatar-group.svg
│ │ │ │ ├── avatar-status.svg
│ │ │ │ └── icon-status.svg
│ │ ├── graphic.svg
│ │ ├── hero.svg
│ │ ├── icons
│ │ │ ├── arrows.svg
│ │ │ ├── capacity.svg
│ │ │ ├── close.svg
│ │ │ ├── cup.svg
│ │ │ ├── loader.svg
│ │ │ ├── processing.svg
│ │ │ ├── table-cup.svg
│ │ │ ├── tap.svg
│ │ │ ├── timer.svg
│ │ │ └── user.svg
│ │ ├── logo.svg
│ │ ├── share-facebook-2.svg
│ │ ├── share-facebook.svg
│ │ ├── share-twitter.svg
│ │ └── solana.svg
│ ├── index.tsx
│ ├── pages
│ │ ├── GamePage.tsx
│ │ ├── HomePage.tsx
│ │ ├── ResultsPage.tsx
│ │ ├── SlotsPage.tsx
│ │ ├── StartPage.tsx
│ │ └── WalletPage.tsx
│ ├── providers
│ │ ├── accounts.tsx
│ │ ├── config.tsx
│ │ ├── game.tsx
│ │ ├── rpc
│ │ │ ├── balance.tsx
│ │ │ ├── blockhash.tsx
│ │ │ └── index.tsx
│ │ ├── server
│ │ │ ├── http
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.tsx
│ │ │ │ └── request.tsx
│ │ │ ├── index.tsx
│ │ │ └── socket.tsx
│ │ ├── slot.tsx
│ │ ├── torus.tsx
│ │ ├── transactions
│ │ │ ├── confirmed.tsx
│ │ │ ├── create.tsx
│ │ │ ├── index.tsx
│ │ │ ├── selected.tsx
│ │ │ └── tps.tsx
│ │ └── wallet.tsx
│ ├── react-app-env.d.ts
│ ├── serviceWorker.ts
│ ├── setupTests.ts
│ ├── styles
│ │ ├── animate.scss
│ │ ├── custom-variables.scss
│ │ ├── custom.scss
│ │ ├── custom
│ │ │ ├── _app.scss
│ │ │ ├── _game.scss
│ │ │ ├── _header.scss
│ │ │ ├── _home.scss
│ │ │ ├── _payment-card.scss
│ │ │ ├── _results.scss
│ │ │ ├── _sidebar.scss
│ │ │ ├── _tx-container.scss
│ │ │ └── _tx-square.scss
│ │ ├── dashkit
│ │ │ ├── _alert.scss
│ │ │ ├── _avatar.scss
│ │ │ ├── _badge.scss
│ │ │ ├── _breadcrumb.scss
│ │ │ ├── _buttons.scss
│ │ │ ├── _card.scss
│ │ │ ├── _chart.scss
│ │ │ ├── _checklist.scss
│ │ │ ├── _close.scss
│ │ │ ├── _code.scss
│ │ │ ├── _comment.scss
│ │ │ ├── _custom-forms.scss
│ │ │ ├── _dashkit.scss
│ │ │ ├── _dropdowns.scss
│ │ │ ├── _forms.scss
│ │ │ ├── _header.scss
│ │ │ ├── _icon.scss
│ │ │ ├── _jumbotron.scss
│ │ │ ├── _kanban.scss
│ │ │ ├── _list-group.scss
│ │ │ ├── _main-content.scss
│ │ │ ├── _mixins.scss
│ │ │ ├── _modal.scss
│ │ │ ├── _nav.scss
│ │ │ ├── _navbar.scss
│ │ │ ├── _popover.scss
│ │ │ ├── _progress.scss
│ │ │ ├── _reboot.scss
│ │ │ ├── _root.scss
│ │ │ ├── _tables.scss
│ │ │ ├── _toasts.scss
│ │ │ ├── _type.scss
│ │ │ ├── _utilities.scss
│ │ │ ├── _variables.scss
│ │ │ ├── _vendors.scss
│ │ │ ├── dark
│ │ │ │ ├── _overrides-dark.scss
│ │ │ │ └── _variables-dark.scss
│ │ │ ├── mixins
│ │ │ │ ├── _badge.scss
│ │ │ │ └── _breakpoints.scss
│ │ │ ├── utilities
│ │ │ │ ├── _background.scss
│ │ │ │ ├── _borders.scss
│ │ │ │ ├── _lift.scss
│ │ │ │ ├── _sizing.scss
│ │ │ │ └── _type.scss
│ │ │ └── vendors
│ │ │ │ ├── _dropzone.scss
│ │ │ │ ├── _flatpickr.scss
│ │ │ │ ├── _list.scss
│ │ │ │ ├── _quill.scss
│ │ │ │ └── _select2.scss
│ │ ├── index.scss
│ │ └── tools
│ │ │ ├── shadows.scss
│ │ │ └── transitions.scss
│ ├── utils
│ │ ├── bytes.ts
│ │ └── index.ts
│ ├── worker-loader.d.ts
│ └── workers
│ │ ├── create-transaction-rpc.ts
│ │ └── create-transaction-worker-script.ts
├── svgTransform.js
└── tsconfig.json
├── package-lock.json
├── package.json
├── package.sh
├── program
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── Xargo.toml
├── dist
│ └── break_solana_program.so
└── src
│ └── lib.rs
└── server
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .prettierignore
├── package-lock.json
├── package.json
├── src
├── api.ts
├── available_nodes.ts
├── index.ts
├── leader_schedule.ts
├── leader_tracker.ts
├── program.ts
├── tpu_proxy.ts
├── urls.ts
├── utils.ts
└── websocket.ts
├── tsconfig.json
└── webpack.config.js
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: npm
9 | directory: "/client"
10 | schedule:
11 | interval: daily
12 | time: "01:00"
13 | timezone: America/Los_Angeles
14 | labels:
15 | - "automerge"
16 | open-pull-requests-limit: 3
17 |
18 | - package-ecosystem: npm
19 | directory: "/server"
20 | schedule:
21 | interval: daily
22 | time: "01:00"
23 | timezone: America/Los_Angeles
24 | labels:
25 | - "automerge"
26 | open-pull-requests-limit: 3
27 |
--------------------------------------------------------------------------------
/.github/workflows/break_action.yml:
--------------------------------------------------------------------------------
1 | name: node_js
2 | on:
3 | push:
4 | branches: [main]
5 | pull_request:
6 | branches: [main]
7 |
8 | jobs:
9 | client:
10 | runs-on: ubuntu-latest
11 | env:
12 | PACKAGE_LOCATION: client
13 | steps:
14 | - name: Checkout repo
15 | uses: actions/checkout@v2
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v2
18 | with:
19 | node-version: lts/*
20 |
21 | - name: Build
22 | run: |
23 | cd $PACKAGE_LOCATION
24 | npm ci
25 | npm run format
26 | npm run lint
27 | npm run build
28 | npm run test
29 | server:
30 | runs-on: ubuntu-latest
31 | env:
32 | PACKAGE_LOCATION: server
33 | steps:
34 | - name: Checkout repository
35 | uses: actions/checkout@v2
36 | - name: Use Node.js ${{ matrix.node-version }}
37 | uses: actions/setup-node@v2
38 | with:
39 | node-version: lts/*
40 | - name: Build
41 | run: |
42 | cd $PACKAGE_LOCATION
43 | npm ci
44 | npm run format
45 | npm run lint
46 | npm run build
47 | npm run test
48 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | build/
3 | *.iml
4 | *.log
5 | /dist/
6 | node_modules
7 |
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: automatic merge on CI success
3 | conditions:
4 | - status-success=client
5 | - status-success=server
6 | - label=automerge
7 | actions:
8 | merge:
9 | method: rebase
10 | - name: remove automerge label on CI failure
11 | conditions:
12 | - label=automerge
13 | - or:
14 | - status-failure=client
15 | - status-failure=server
16 | actions:
17 | label:
18 | remove:
19 | - automerge
20 | - name: remove outdated reviews
21 | conditions:
22 | - base=main
23 | actions:
24 | dismiss_reviews:
25 | changes_requested: true
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019 Solana Labs, Inc
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm run start
2 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildpacks": [
3 | {
4 | "url": "heroku/nodejs"
5 | }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/client/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | build
3 | webpack
4 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | build
3 | node_modules
--------------------------------------------------------------------------------
/client/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | build
3 | dashkit
4 | animate.scss
5 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@solana/break",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "react-scripts start",
7 | "build": "react-scripts build",
8 | "test": "react-scripts test",
9 | "eject": "react-scripts eject",
10 | "lint": "echo \"lint is run during build\"",
11 | "format": "prettier -c \"**/*.+(js|jsx|ts|tsx|json|css|scss|md)\"",
12 | "format:fix": "prettier --write \"**/*.+(js|jsx|ts|tsx|json|css|scss|md)\""
13 | },
14 | "testnetDefaultChannel": "edge",
15 | "dependencies": {
16 | "@react-hook/debounce": "^4.0.0",
17 | "@react-hook/throttle": "^2.2.0",
18 | "@sentry/react": "^5.26.0",
19 | "@sentry/tracing": "^5.26.0",
20 | "@solana/web3.js": "^1.43.4",
21 | "@testing-library/jest-dom": "^5.16.4",
22 | "@testing-library/react": "^13.3.0",
23 | "@testing-library/user-event": "^14.2.0",
24 | "@toruslabs/fetch-node-details": "^6.0.1",
25 | "@toruslabs/torus.js": "^6.0.0",
26 | "@types/bs58": "^4.0.1",
27 | "@types/chai": "^4.3.1",
28 | "@types/chart.js": "^2.9.34",
29 | "@types/jest": "^27.5.1",
30 | "@types/node": "^17.0.35",
31 | "@types/qrcode.react": "^1.0.2",
32 | "@types/react": "^18.0.9",
33 | "@types/react-dom": "^17.0.14",
34 | "@types/react-router-dom": "^5.3.1",
35 | "@types/ua-parser-js": "^0.7.36",
36 | "bootstrap": "^4.6.0",
37 | "bs58": "^5.0.0",
38 | "buffer": "^6.0.3",
39 | "chai": "^4.3.6",
40 | "chart.js": "^2.9.4",
41 | "lodash.debounce": "^4.0.8",
42 | "node-sass": "^7.0.1",
43 | "prettier": "^2.6.2",
44 | "qrcode.react": "^3.0.2",
45 | "react": "^18.1.0",
46 | "react-chartjs-2": "^2.11.2",
47 | "react-dom": "^18.1.0",
48 | "react-google-login": "^5.2.2",
49 | "react-router-dom": "^5.3.0",
50 | "react-scripts": "^4.0.3",
51 | "tweetnacl": "^1.0.3",
52 | "typescript": "^4.7.2",
53 | "ua-parser-js": "^1.0.2",
54 | "worker-loader": "^3.0.8"
55 | },
56 | "eslintConfig": {
57 | "extends": "react-app"
58 | },
59 | "browserslist": {
60 | "production": [
61 | ">0.2%",
62 | "not dead",
63 | "not op_mini all"
64 | ],
65 | "development": [
66 | "last 1 chrome version",
67 | "last 1 firefox version",
68 | "last 1 safari version"
69 | ]
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/public/google.svg:
--------------------------------------------------------------------------------
1 | Google icon
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
17 |
21 |
22 |
26 |
27 |
36 | Break | Solana
37 |
41 |
42 |
43 | You need to enable JavaScript to run this app.
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/client/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Solana Explorer",
3 | "name": "Explorer for Solana clusters",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "rainbow192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "rainbow512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#75FBB4",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/client/public/rainbow192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/public/rainbow192.png
--------------------------------------------------------------------------------
/client/public/rainbow512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/public/rainbow512.png
--------------------------------------------------------------------------------
/client/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/client/setupJest.ts:
--------------------------------------------------------------------------------
1 | import { GlobalWithFetchMock } from "jest-fetch-mock";
2 |
3 | const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;
4 | customGlobal.fetch = require("jest-fetch-mock");
5 | customGlobal.fetchMock = customGlobal.fetch;
6 |
--------------------------------------------------------------------------------
/client/src/__tests__/bytes.ts:
--------------------------------------------------------------------------------
1 | import * as Bytes from "../utils/bytes";
2 | import { expect } from "chai";
3 |
4 | describe("programDataToIds", () => {
5 | it("ids = []", () => {
6 | const ids = Bytes.programDataToIds(new Uint8Array([0]));
7 | expect(ids).to.eql([]);
8 | });
9 |
10 | it("ids = [0]", () => {
11 | const ids = Bytes.programDataToIds(new Uint8Array([128]));
12 | expect(ids).to.eql([0]);
13 | });
14 |
15 | it("ids = [8]", () => {
16 | const ids = Bytes.programDataToIds(new Uint8Array([0, 128]));
17 | expect(ids).to.eql([8]);
18 | });
19 |
20 | it("ids = [7, 8]", () => {
21 | const ids = Bytes.programDataToIds(new Uint8Array([1, 128]));
22 | expect(ids).to.eql([7, 8]);
23 | });
24 | });
25 |
26 | describe("instructionDataFromId", () => {
27 | it("id = 0", () => {
28 | const data = Bytes.instructionDataFromId(0);
29 | expect(data).to.eql(new Uint8Array([0, 0]));
30 | });
31 |
32 | it("id = 8", () => {
33 | const data = Bytes.instructionDataFromId(8);
34 | expect(data).to.eql(new Uint8Array([0, 8]));
35 | });
36 |
37 | it("id = 257", () => {
38 | const data = Bytes.instructionDataFromId(257);
39 | expect(data).to.eql(new Uint8Array([1, 1]));
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/client/src/app.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Route, Switch, Redirect, useRouteMatch } from "react-router-dom";
3 |
4 | import { HomePage } from "pages/HomePage";
5 | import { GamePage } from "pages/GamePage";
6 | import { SlotsPage } from "pages/SlotsPage";
7 | import { WalletPage } from "pages/WalletPage";
8 | import { ResultsPage } from "pages/ResultsPage";
9 | import { StartPage } from "pages/StartPage";
10 |
11 | import { ClusterModal } from "components/ClusterModal";
12 | import { LoadingModal } from "components/LoadingModal";
13 | import { useGameState } from "providers/game";
14 | import { useClusterModal } from "providers/server";
15 | import { Header } from "components/Header";
16 | import { ConfigurationSidebar } from "components/ConfigurationSidebar";
17 |
18 | export default function App() {
19 | const isHomePage = !!useRouteMatch("/")?.isExact;
20 | const isWalletPage = !!useRouteMatch("/wallet")?.isExact;
21 | const isSlotsPage = !!useRouteMatch("/slots")?.isExact;
22 | const [showClusterModal] = useClusterModal();
23 | const gameState = useGameState();
24 |
25 | if (isHomePage) {
26 | return (
27 |
28 |
29 |
30 | );
31 | }
32 |
33 | const isLoading = gameState.loadingPhase !== "complete";
34 | const isInitializing =
35 | gameState.loadingPhase === "config" && !isWalletPage && !isSlotsPage;
36 | const showLoadingModal = isLoading && !isWalletPage && !isSlotsPage;
37 | const showConfigSidebar = !isHomePage && !isWalletPage;
38 | return (
39 |
40 |
41 |
42 |
43 | {showConfigSidebar && }
44 | {!isInitializing && (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | )}
54 |
55 |
56 |
57 |
58 |
59 |
60 | );
61 | }
62 |
63 | function Overlay({ show }: { show: boolean }) {
64 | if (show) return
;
65 | return
;
66 | }
67 |
--------------------------------------------------------------------------------
/client/src/components/ClusterModal.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 | import { useDebounceCallback } from "@react-hook/debounce";
4 | import { Location } from "history";
5 | import {
6 | useServer,
7 | serverName,
8 | useClusterModal,
9 | SERVERS,
10 | DEFAULT_SERVER,
11 | useCustomUrl,
12 | } from "../providers/server";
13 | import { useRpcUrlState } from "providers/rpc";
14 | import { useServerConfig } from "providers/server/http";
15 |
16 | export function ClusterModal() {
17 | const [show, setShow] = useClusterModal();
18 | const onClose = () => setShow(false);
19 | const { server } = useServer();
20 | return (
21 | <>
22 |
26 |
27 |
28 |
e.stopPropagation()}>
29 |
30 | ×
31 |
32 |
33 |
Choose a Cluster
34 |
35 |
36 |
37 |
38 | Override {serverName(server)} RPC
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | >
47 | );
48 | }
49 |
50 | function CustomRpcInput() {
51 | const [rpcUrl, setRpcUrl] = useRpcUrlState();
52 | const [editing, setEditing] = React.useState(false);
53 | const configRpcUrl = useServerConfig()?.rpcUrl;
54 | const active = configRpcUrl !== rpcUrl;
55 |
56 | const customClass = (prefix: string) => (active ? `${prefix}-info` : "");
57 | const onUrlInput = useDebounceCallback((url: string) => {
58 | if (url.length > 0) {
59 | try {
60 | new URL(url);
61 | setRpcUrl(url);
62 | } catch (err) {
63 | // ignore bad url
64 | }
65 | } else if (configRpcUrl) {
66 | setRpcUrl(configRpcUrl);
67 | }
68 | }, 500);
69 |
70 | const defaultValue = active ? rpcUrl : "";
71 | const inputTextClass = editing ? "" : "text-muted";
72 | return (
73 | setEditing(true)}
81 | onBlur={() => setEditing(false)}
82 | onInput={(e) => onUrlInput(e.currentTarget.value)}
83 | />
84 | );
85 | }
86 |
87 | function CustomClusterInput() {
88 | const [customUrl, setCustomUrl] = useCustomUrl();
89 | const [editing, setEditing] = React.useState(false);
90 |
91 | const onUrlInput = useDebounceCallback((url: string) => {
92 | setCustomUrl(url);
93 | }, 500);
94 |
95 | const inputTextClass = editing ? "" : "text-muted";
96 | return (
97 | setEditing(true)}
102 | onBlur={() => setEditing(false)}
103 | onInput={(e) => onUrlInput(e.currentTarget.value)}
104 | />
105 | );
106 | }
107 |
108 | function ClusterToggle() {
109 | const { server } = useServer();
110 |
111 | return (
112 | <>
113 |
114 | {SERVERS.map((next) => {
115 | const active = next === server;
116 | const btnClass = active
117 | ? `active btn-dark border-info text-white`
118 | : "btn-dark";
119 |
120 | const clusterLocation = (location: Location) => {
121 | const params = new URLSearchParams(location.search);
122 | if (next !== DEFAULT_SERVER) {
123 | params.set("cluster", next);
124 | } else {
125 | params.delete("cluster");
126 | }
127 | return {
128 | ...location,
129 | search: params.toString(),
130 | };
131 | };
132 |
133 | return (
134 |
139 | {serverName(next)}
140 |
141 | );
142 | })}
143 |
144 | {server === "custom" && (
145 | <>
146 | Break Server URL
147 |
148 | >
149 | )}
150 | >
151 | );
152 | }
153 |
--------------------------------------------------------------------------------
/client/src/components/ClusterStatusButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useServer, useClusterModal } from "../providers/server";
3 |
4 | function ClusterStatusButton() {
5 | const [, setShow] = useClusterModal();
6 | const { name } = useServer();
7 | return (
8 | setShow(true)}
11 | >
12 | {name}
13 |
14 | );
15 | }
16 |
17 | export default ClusterStatusButton;
18 |
--------------------------------------------------------------------------------
/client/src/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import breakSvg from "images/break.svg";
4 | import solanaSvg from "images/solana.svg";
5 | import { useGameState } from "providers/game";
6 | import ClusterStatusButton from "./ClusterStatusButton";
7 | import { useClientConfig } from "providers/config";
8 |
9 | export function Header() {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
36 |
37 |
42 |
43 |
44 |
45 | );
46 | }
47 |
48 | function HeaderCTA() {
49 | const gameState = useGameState();
50 | const [, setRefresh] = React.useState(false);
51 | const countdownStart = gameState.countdownStartTime;
52 | const gameStatus = gameState.status;
53 | const [{ countdownSeconds }] = useClientConfig();
54 |
55 | React.useEffect(() => {
56 | if (countdownStart !== undefined) {
57 | const timerId = setInterval(() => {
58 | setRefresh((r) => !r);
59 | }, 1000);
60 | return () => clearTimeout(timerId);
61 | }
62 | }, [countdownStart]);
63 |
64 | if (gameStatus === "loading" || gameStatus === "setup") {
65 | return null;
66 | }
67 |
68 | if (gameStatus === "finished") {
69 | return (
70 |
71 | Play Again
72 |
73 | );
74 | }
75 |
76 | let secondsRemaining = countdownSeconds;
77 | if (countdownStart !== undefined) {
78 | secondsRemaining = Math.max(
79 | 0,
80 | countdownSeconds - Math.floor((performance.now() - countdownStart) / 1000)
81 | );
82 | }
83 |
84 | return (
85 |
86 |
87 |
88 |
89 |
90 | {secondsRemaining}s
91 |
92 |
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/client/src/components/LoadingModal.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { LoadingPhase } from "providers/game";
4 |
5 | export function LoadingModal({
6 | show,
7 | wallet,
8 | phase,
9 | }: {
10 | show: boolean;
11 | wallet?: boolean;
12 | phase?: LoadingPhase;
13 | }) {
14 | const renderContent = () => {
15 | if (!show) return null;
16 | let loadingText = "Loading";
17 | if (wallet) {
18 | loadingText = "Fetching wallet";
19 | } else if (phase) {
20 | switch (phase) {
21 | case "config":
22 | loadingText = "Initializing";
23 | break;
24 | case "blockhash":
25 | loadingText = "Connecting to cluster";
26 | break;
27 | case "costs":
28 | loadingText = "Calculating game cost";
29 | break;
30 | case "socket":
31 | loadingText = "Connecting to transaction forwarder";
32 | break;
33 | case "creating-accounts":
34 | loadingText = "Creating game accounts";
35 | break;
36 | }
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
{loadingText}...
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | return (
54 | {renderContent()}
55 | );
56 | }
57 |
--------------------------------------------------------------------------------
/client/src/components/SlotTableRow.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { SlotTiming, useLeaderSchedule } from "providers/slot";
4 |
5 | interface Props {
6 | timing?: SlotTiming;
7 | slot: number;
8 | }
9 |
10 | export function timeElapsed(
11 | sentAt: number | undefined,
12 | receivedAt: number | undefined
13 | ): string | undefined {
14 | if (sentAt === undefined || receivedAt === undefined) return;
15 | return (Math.max(0, receivedAt - sentAt) / 1000).toFixed(3) + "s";
16 | }
17 |
18 | export function timestamp(sentAt: number | undefined): string | undefined {
19 | if (sentAt === undefined) return;
20 | const date = new Date(sentAt);
21 | const pad = (num: number, length: number) =>
22 | num.toString().padStart(length, "0");
23 | return `${pad(date.getHours(), 2)}:${pad(date.getMinutes(), 2)}:${pad(
24 | date.getSeconds(),
25 | 2
26 | )}.${pad(date.getMilliseconds(), 3)}`;
27 | }
28 |
29 | export function SlotTableRow({ slot, timing }: Props) {
30 | const schedule = useLeaderSchedule().current;
31 | function TdTimestamp({ time }: { time: number | undefined }) {
32 | return (
33 |
34 |
35 |
36 | {timeElapsed(timing?.firstShred, time)}
37 |
38 |
{timestamp(time) || "-"}
39 |
40 |
41 | );
42 | }
43 |
44 | const leader = React.useMemo(() => {
45 | if (schedule) {
46 | const [offset, leaderSchedule] = schedule;
47 | for (let [leader, slots] of Object.entries(leaderSchedule)) {
48 | if (slots.indexOf(slot - offset) >= 0) {
49 | return leader;
50 | }
51 | }
52 | }
53 | }, [slot, schedule]);
54 |
55 | let txCount, txSuccessRate, avgTpe;
56 | if (timing?.stats) {
57 | txCount =
58 | timing.stats.numSuccessfulTransactions +
59 | timing.stats.numFailedTransactions;
60 | const rawTxRate = timing.stats.numSuccessfulTransactions / txCount;
61 | txSuccessRate = `${(100 * rawTxRate).toFixed(1)}%`;
62 | avgTpe = (txCount / timing?.stats.numTransactionEntries).toFixed(1);
63 | }
64 |
65 | return (
66 |
67 | {leader ? leader.slice(0, 7) : "-"}
68 |
69 |
70 |
71 | {timing?.parent + "┐" || "-"}
72 |
73 |
{slot}
74 |
75 |
76 | {txCount || "-"}
77 | {txSuccessRate || "-"}
78 | {timing?.stats?.numTransactionEntries || "-"}
79 | {avgTpe || "-"}
80 | {timing?.stats?.maxTransactionsPerEntry || "-"}
81 |
82 |
83 |
84 | {timing?.err === undefined ? (
85 | <>
86 |
87 |
88 |
89 | >
90 | ) : (
91 | <>
92 |
93 | {timing?.err}
94 | >
95 | )}
96 |
97 | );
98 | }
99 |
--------------------------------------------------------------------------------
/client/src/components/TxSquare.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import "styles/animate.scss";
4 | import { TransactionState } from "providers/transactions";
5 | import { useSelectTransaction } from "providers/transactions/selected";
6 |
7 | interface Props {
8 | transaction: TransactionState;
9 | }
10 |
11 | export function TransactionSquare({ transaction }: Props) {
12 | const { status, details } = transaction;
13 | const selectTransaction = useSelectTransaction();
14 |
15 | let statusClass = "";
16 | if (transaction.status === "success") {
17 | statusClass = "primary";
18 | } else if (status === "failed") {
19 | statusClass = "danger";
20 | } else if (status === "timeout") {
21 | statusClass = "warning";
22 | } else {
23 | statusClass = "dark";
24 | }
25 |
26 | return (
27 | selectTransaction(details.signature)}
29 | className={`btn d-flex flex-column justify-content-center align-items-center square slideInRight btn-${statusClass}`}
30 | >
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/components/TxTableRow.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import "styles/animate.scss";
4 | import { TransactionState } from "providers/transactions";
5 | import { useSelectTransaction } from "providers/transactions/selected";
6 | import { useSlotTiming } from "providers/slot";
7 | import type { SlotTiming } from "providers/slot";
8 |
9 | interface Props {
10 | transaction: TransactionState;
11 | }
12 |
13 | export function timeElapsed(
14 | sentAt: number | undefined,
15 | receivedAt: number | undefined
16 | ): string | undefined {
17 | if (sentAt === undefined || receivedAt === undefined) return;
18 | return (Math.max(0, receivedAt - sentAt) / 1000).toFixed(3) + "s";
19 | }
20 |
21 | export function TxTableRow({ transaction }: Props) {
22 | const signature = transaction.details.signature;
23 | const selectTransaction = useSelectTransaction();
24 | const slotMetrics = useSlotTiming();
25 |
26 | let targetSlot;
27 | let landedSlot: number | undefined;
28 | let timing;
29 | let received;
30 | let targetSlotOverride: string | undefined;
31 | if (transaction.status === "success") {
32 | targetSlot = transaction.slot.target;
33 | landedSlot = transaction.slot.landed;
34 | timing = transaction.timing;
35 | received = transaction.received;
36 | } else if (
37 | transaction.status === "timeout" ||
38 | transaction.status === "failed"
39 | ) {
40 | targetSlotOverride = transaction.status.toUpperCase();
41 | } else {
42 | targetSlot = transaction.pending.targetSlot;
43 | timing = transaction.timing;
44 | received = transaction.received;
45 | }
46 |
47 | let slotTiming: SlotTiming | undefined;
48 | let landedTime: number | undefined;
49 | if (landedSlot !== undefined) {
50 | landedTime = received?.find((r) => r.slot === landedSlot)?.timestamp;
51 | slotTiming = slotMetrics.current.get(landedSlot);
52 | }
53 |
54 | let txCount, txSuccessRate, avgTpe;
55 | if (slotTiming?.stats) {
56 | txCount =
57 | slotTiming.stats.numSuccessfulTransactions +
58 | slotTiming.stats.numFailedTransactions;
59 | const rawTxRate = slotTiming.stats.numSuccessfulTransactions / txCount;
60 | txSuccessRate = `${(100 * rawTxRate).toFixed(1)}%`;
61 | avgTpe = (txCount / slotTiming?.stats.numTransactionEntries).toFixed(1);
62 | }
63 |
64 | return (
65 | selectTransaction(signature)}
68 | >
69 | {signature.slice(0, 7)}…
70 | {targetSlot || targetSlotOverride || "-"}
71 | {landedSlot || "-"}
72 | {txCount || "-"}
73 | {txSuccessRate || "-"}
74 | {slotTiming?.stats?.numTransactionEntries || "-"}
75 | {avgTpe || "-"}
76 | {slotTiming?.stats?.maxTransactionsPerEntry || "-"}
77 | {timeElapsed(timing?.subscribed, slotTiming?.firstShred) || "-"}
78 | {timeElapsed(timing?.subscribed, landedTime) || "-"}
79 | {timeElapsed(timing?.subscribed, slotTiming?.fullSlot) || "-"}
80 | {timeElapsed(timing?.subscribed, slotTiming?.createdBank) || "-"}
81 | {timeElapsed(timing?.subscribed, slotTiming?.frozen) || "-"}
82 | {timeElapsed(timing?.subscribed, slotTiming?.confirmed) || "-"}
83 | {timeElapsed(timing?.subscribed, slotTiming?.rooted) || "-"}
84 |
85 | );
86 | }
87 |
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-bold.eot
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-bold.ttf
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-bold.woff
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-book.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-book.eot
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-book.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-book.ttf
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-book.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-book.woff
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-medium.eot
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-medium.ttf
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-medium.woff
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-regular.eot
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-regular.ttf
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-regular.woff
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-semibold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-semibold.eot
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-semibold.ttf
--------------------------------------------------------------------------------
/client/src/fonts/cerebrisans/cerebrisans-semibold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/cerebrisans/cerebrisans-semibold.woff
--------------------------------------------------------------------------------
/client/src/fonts/feather/fonts/Feather.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/feather/fonts/Feather.ttf
--------------------------------------------------------------------------------
/client/src/fonts/feather/fonts/Feather.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/fonts/feather/fonts/Feather.woff
--------------------------------------------------------------------------------
/client/src/images/SOLANA-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/SOLANA-preview.png
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/auth-side-cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/auth-side-cover.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/header-cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/header-cover.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/profile-cover-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/profile-cover-1.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/profile-cover-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/profile-cover-2.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/profile-cover-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/profile-cover-3.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/profile-cover-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/profile-cover-4.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/profile-cover-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/profile-cover-5.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/profile-cover-6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/profile-cover-6.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/profile-cover-7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/profile-cover-7.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/profile-cover-8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/profile-cover-8.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/sidebar-cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/sidebar-cover.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/covers/team-cover.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/client/src/images/dashkit/covers/team-cover.jpg
--------------------------------------------------------------------------------
/client/src/images/dashkit/masks/avatar-group-hover-last.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/client/src/images/dashkit/masks/avatar-group-hover.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/client/src/images/dashkit/masks/avatar-group.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/client/src/images/dashkit/masks/avatar-status.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/client/src/images/dashkit/masks/icon-status.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/client/src/images/icons/arrows.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/client/src/images/icons/capacity.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/src/images/icons/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/client/src/images/icons/cup.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/client/src/images/icons/loader.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/client/src/images/icons/processing.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/client/src/images/icons/table-cup.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/client/src/images/icons/tap.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
15 |
16 |
17 |
23 |
29 |
30 |
31 |
37 |
38 |
39 |
45 |
46 |
47 |
53 |
54 |
55 |
61 |
62 |
63 |
69 |
70 |
--------------------------------------------------------------------------------
/client/src/images/icons/timer.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/client/src/images/icons/user.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/client/src/images/share-facebook-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/client/src/images/share-facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/client/src/images/share-twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import ReactDOM from "react-dom";
3 | import { BrowserRouter } from "react-router-dom";
4 | // import * as Sentry from "@sentry/react";
5 | // import { Integrations } from "@sentry/tracing";
6 |
7 | import "styles/index.scss";
8 |
9 | import App from "./app";
10 | import { WalletProvider } from "providers/wallet";
11 | import { TransactionsProvider } from "providers/transactions";
12 | import { GameStateProvider } from "providers/game";
13 | import { ServerProvider } from "providers/server";
14 | import { RpcProvider } from "providers/rpc";
15 | import { SlotProvider } from "providers/slot";
16 | import { AccountsProvider } from "providers/accounts";
17 | import { TorusProvider } from "providers/torus";
18 | import { ConfigProvider } from "providers/config";
19 |
20 | // if (process.env.NODE_ENV === "production") {
21 | // Sentry.init({
22 | // dsn:
23 | // "https://727cd3fff6f949449c1ce5030928e667@o434108.ingest.sentry.io/5411826",
24 | // integrations: [new Integrations.BrowserTracing()],
25 | // tracesSampleRate: 1.0,
26 | // });
27 | // }
28 |
29 | ReactDOM.render(
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ,
51 | document.getElementById("root")
52 | );
53 |
--------------------------------------------------------------------------------
/client/src/pages/GamePage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Redirect, useLocation } from "react-router-dom";
3 |
4 | import { TransactionContainer } from "components/TxContainer";
5 | import { TransactionModal } from "components/TxModal";
6 | import {
7 | useTps,
8 | useCreatedCount,
9 | useAvgConfirmationTime,
10 | } from "providers/transactions";
11 | import { useWalletState } from "providers/wallet";
12 | import { useActiveUsers } from "providers/server/socket";
13 | import { useGameState } from "providers/game";
14 |
15 | export function GamePage() {
16 | const gameState = useGameState();
17 | const gameStatus = gameState.status;
18 | const countdownStart = gameState.countdownStartTime;
19 | const showSetup = gameStatus === "setup";
20 | const showStats = gameStatus === "play" || countdownStart !== undefined;
21 | const payer = useWalletState().wallet;
22 | const location = useLocation();
23 |
24 | if (showSetup) {
25 | if (!payer) {
26 | return ;
27 | } else {
28 | return ;
29 | }
30 | }
31 |
32 | return (
33 |
34 | {showStats &&
}
35 |
40 |
41 |
42 | );
43 | }
44 |
45 | export function EmptyCard() {
46 | return (
47 |
51 | );
52 | }
53 |
54 | function Stats() {
55 | const createdCount = useCreatedCount();
56 | const avgConfTime = useAvgConfirmationTime().toFixed(2);
57 | const tps = useTps();
58 | const activeUsers = useActiveUsers();
59 |
60 | return (
61 |
62 |
63 |
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | type StatProps = {
75 | label: React.ReactNode;
76 | value: React.ReactNode;
77 | icon: string;
78 | };
79 | function StatCard({ label, value, icon }: StatProps) {
80 | return (
81 |
82 |
83 |
84 |
85 |
86 |
{label}
87 | {value}
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | );
97 | }
98 |
--------------------------------------------------------------------------------
/client/src/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import graphic from "images/graphic.svg";
4 | import breakSvg from "images/break.svg";
5 | import solanaSvg from "images/solana.svg";
6 | import { Link } from "react-router-dom";
7 |
8 | export class HomePage extends React.Component {
9 | render() {
10 | return (
11 |
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
24 |
25 | Solana is the world’s most performant blockchain — currently, it
26 | can handle 50,000 transactions per second with 400
27 | millisecond block times.
28 |
29 |
30 | To see just how fast it is, you're invited to come and Break
31 | Solana. During the next fifteen seconds, send as many
32 | transactions as you can by mashing your keyboard.
33 |
34 |
35 |
36 |
40 | Play the game
41 |
42 |
43 |
44 |
45 |
46 |
47 | );
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/pages/SlotsPage.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { useSlotTiming } from "providers/slot";
4 | import { SlotTableRow } from "components/SlotTableRow";
5 |
6 | export function SlotsPage() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 | Live Slot Stats
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | function SlotTable() {
26 | const slotTiming = useSlotTiming();
27 | let min = Number.MAX_SAFE_INTEGER,
28 | max = 0;
29 | for (const slot of slotTiming.current.keys()) {
30 | min = Math.min(min, slot);
31 | max = Math.max(max, slot);
32 | }
33 |
34 | const slots = [];
35 | for (let i = min; i <= max; i++) {
36 | slots.push(i);
37 | }
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 | Leader
47 | Slot + Parent
48 | Tx Count
49 | Tx Success %
50 | Tx Entries
51 | Avg Tx Per Entry
52 | Max Tx Per Entry
53 | First Shred
54 | Shreds Full
55 | Bank Created
56 | Bank Frozen / Dead
57 | Confirmed
58 | Rooted
59 |
60 |
61 |
62 | {slots.map((slot) => (
63 |
68 | ))}
69 |
70 |
71 |
72 |
73 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/client/src/pages/StartPage.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useWalletState } from "providers/wallet";
4 | import { Redirect, useLocation } from "react-router-dom";
5 | import { PaymentCard } from "components/PaymentCard";
6 |
7 | export function StartPage() {
8 | const payer = useWalletState().wallet;
9 | const location = useLocation();
10 |
11 | if (!payer) {
12 | return ;
13 | }
14 |
15 | return (
16 |
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/client/src/providers/config.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { TrackedCommitment } from "./transactions";
3 |
4 | export interface ClientConfig {
5 | computeUnitPrice?: number;
6 | parallelization: number;
7 | trackedCommitment: TrackedCommitment;
8 | showDebugTable: boolean;
9 | countdownSeconds: number;
10 | retryTransactionEnabled: boolean;
11 | autoSendTransactions: boolean;
12 | useTpu: boolean;
13 | rpcUrl?: string;
14 | extraWriteAccount?: string;
15 | }
16 |
17 | const DEFAULT_CONFIG: ClientConfig = {
18 | parallelization: 4,
19 | trackedCommitment: "confirmed",
20 | showDebugTable: false,
21 | retryTransactionEnabled: false,
22 | autoSendTransactions: false,
23 | countdownSeconds: 15,
24 | useTpu: false,
25 | };
26 |
27 | type SetConfig = React.Dispatch>;
28 | const ConfigContext = React.createContext<
29 | [ClientConfig, SetConfig] | undefined
30 | >(undefined);
31 |
32 | type Props = { children: React.ReactNode };
33 | export function ConfigProvider({ children }: Props) {
34 | const stateHook = React.useState(DEFAULT_CONFIG);
35 | return (
36 |
37 | {children}
38 |
39 | );
40 | }
41 |
42 | export function useClientConfig() {
43 | const context = React.useContext(ConfigContext);
44 | if (!context) {
45 | throw new Error(`useClientConfig must be used within a ConfigProvider`);
46 | }
47 | return context;
48 | }
49 |
--------------------------------------------------------------------------------
/client/src/providers/game.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useHistory, useLocation } from "react-router-dom";
3 |
4 | import { useServerConfig } from "providers/server/http";
5 | import { useSocket } from "providers/server/socket";
6 | import { useBlockhash } from "providers/rpc/blockhash";
7 | import { useDispatch as useTransactionsDispatch } from "providers/transactions";
8 | import { useAccountsState } from "./accounts";
9 | import { useConnection } from "./rpc";
10 | import { useClientConfig } from "./config";
11 |
12 | type GameStatus = "loading" | "setup" | "play" | "finished";
13 | export type LoadingPhase =
14 | | "blockhash"
15 | | "socket"
16 | | "config"
17 | | "costs"
18 | | "creating-accounts"
19 | | "complete";
20 |
21 | export interface GameState {
22 | status: GameStatus;
23 | loadingPhase: LoadingPhase;
24 | countdownStartTime: number | undefined;
25 | prepareGame: () => void;
26 | resetGame: () => void;
27 | startGame: () => void;
28 | }
29 |
30 | const GameStateContext = React.createContext(undefined);
31 |
32 | type Props = { children: React.ReactNode };
33 | export function GameStateProvider({ children }: Props) {
34 | const [status, setGameStatus] = React.useState("loading");
35 | const [countdownStartTime, setCountdownStart] = React.useState();
36 | const connection = useConnection();
37 | const blockhash = useBlockhash();
38 | const serverConfig = useServerConfig();
39 | const [clientConfig] = useClientConfig();
40 | const socket = useSocket();
41 | const accountsState = useAccountsState();
42 | const loadingPhase: LoadingPhase = React.useMemo(() => {
43 | if (!serverConfig) return "config";
44 | if (!blockhash) return "blockhash";
45 | if (!accountsState.creationCost) return "costs";
46 | if (!socket) return "socket";
47 | if (accountsState.status === "creating") return "creating-accounts";
48 | return "complete";
49 | }, [blockhash, serverConfig, socket, accountsState]);
50 |
51 | React.useEffect(() => {
52 | setGameStatus("loading");
53 | setCountdownStart(undefined);
54 | }, [
55 | connection,
56 | clientConfig.showDebugTable,
57 | clientConfig.parallelization,
58 | clientConfig.trackedCommitment,
59 | ]);
60 |
61 | React.useEffect(() => {
62 | if (status === "loading" && loadingPhase === "complete") {
63 | setGameStatus("setup");
64 | }
65 | }, [status, loadingPhase]);
66 |
67 | const history = useHistory();
68 | const location = useLocation();
69 | const resultsTimerRef = React.useRef();
70 | React.useEffect(() => {
71 | if (countdownStartTime !== undefined) {
72 | if (!resultsTimerRef.current) {
73 | resultsTimerRef.current = setTimeout(() => {
74 | setGameStatus("finished");
75 | history.push({ ...location, pathname: "/results" });
76 | }, clientConfig.countdownSeconds * 1000);
77 | }
78 | } else if (resultsTimerRef.current) {
79 | clearTimeout(resultsTimerRef.current);
80 | resultsTimerRef.current = undefined;
81 | }
82 | }, [countdownStartTime, history, location, clientConfig.countdownSeconds]);
83 |
84 | const startGame = React.useCallback(() => {
85 | setCountdownStart(performance.now());
86 | }, []);
87 |
88 | const transactionsDispatch = useTransactionsDispatch();
89 | const resetGame = React.useCallback(() => {
90 | setGameStatus("setup");
91 | setCountdownStart(undefined);
92 | transactionsDispatch({ type: "reset" });
93 | accountsState.deactivate();
94 | history.push({ ...location, pathname: "/start" });
95 | }, [accountsState, history, location, transactionsDispatch]);
96 |
97 | const prepareGame = React.useCallback(() => {
98 | setGameStatus("play");
99 | accountsState.createAccounts();
100 | history.push({ ...location, pathname: "/game" });
101 | }, [accountsState, history, location]);
102 |
103 | const accountsStatus = accountsState.status;
104 | React.useEffect(() => {
105 | const shouldReset = status === "play" && accountsStatus === "inactive";
106 | if (shouldReset) {
107 | resetGame();
108 | }
109 | }, [status, accountsStatus, resetGame]);
110 |
111 | const gameState: GameState = React.useMemo(
112 | () => ({
113 | status,
114 | loadingPhase,
115 | countdownStartTime,
116 | resetGame,
117 | prepareGame,
118 | startGame,
119 | }),
120 | [
121 | status,
122 | loadingPhase,
123 | countdownStartTime,
124 | prepareGame,
125 | startGame,
126 | resetGame,
127 | ]
128 | );
129 |
130 | return (
131 |
132 | {children}
133 |
134 | );
135 | }
136 |
137 | export function useGameState() {
138 | const context = React.useContext(GameStateContext);
139 | if (!context) {
140 | throw new Error(`useGameState must be used within a GameStateProvider`);
141 | }
142 | return context;
143 | }
144 |
--------------------------------------------------------------------------------
/client/src/providers/rpc/balance.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { AccountInfo } from "@solana/web3.js";
3 |
4 | import { useWalletState } from "providers/wallet";
5 | import { useConnection } from "providers/rpc";
6 | import { getFeePayers, reportError } from "utils";
7 | import { useClientConfig } from "providers/config";
8 |
9 | type Balance = number | "loading";
10 |
11 | interface State {
12 | payer: Balance;
13 | feePayers: Array;
14 | }
15 |
16 | const StateContext = React.createContext(undefined);
17 |
18 | type Props = { children: React.ReactNode };
19 | export function BalanceProvider({ children }: Props) {
20 | const [balance, setBalance] = React.useState("loading");
21 | const [{ parallelization }] = useClientConfig();
22 | const [feePayerBalances, setFeePayerBalances] = React.useState(
23 | Array(parallelization).fill(0)
24 | );
25 | const payer = useWalletState().wallet;
26 | const connection = useConnection();
27 |
28 | const refreshBalance = React.useCallback(() => {
29 | if (payer === undefined || connection === undefined) {
30 | setBalance("loading");
31 | return;
32 | }
33 |
34 | (async () => {
35 | try {
36 | const balance = await connection.getBalance(payer.publicKey);
37 | setBalance(balance);
38 | } catch (err) {
39 | reportError(err, "Failed to refresh balance");
40 | }
41 | })();
42 | }, [payer, connection]);
43 |
44 | React.useEffect(() => {
45 | refreshBalance();
46 | const onChange = () => {
47 | if (document.visibilityState !== "visible") return;
48 | refreshBalance();
49 | };
50 |
51 | document.addEventListener("visibilitychange", onChange);
52 | return () => document.removeEventListener("visibilitychange", onChange);
53 | }, [refreshBalance]);
54 |
55 | React.useEffect(() => {
56 | if (!payer || !connection) return;
57 | const subscription = connection.onAccountChange(
58 | payer.publicKey,
59 | (accountInfo: AccountInfo) => setBalance(accountInfo.lamports)
60 | );
61 |
62 | return () => {
63 | connection.removeAccountChangeListener(subscription);
64 | };
65 | }, [payer, connection]);
66 |
67 | const feePayers = React.useMemo(
68 | () => getFeePayers(parallelization),
69 | [parallelization]
70 | );
71 | const feePayerCounter = React.useRef(0);
72 | React.useEffect(() => {
73 | if (!connection) return;
74 | feePayerCounter.current++;
75 | const currentCounter = feePayerCounter.current;
76 |
77 | (async () => {
78 | const balances = await Promise.all(
79 | feePayers.map((feePayer) => {
80 | return connection.getBalance(feePayer.publicKey);
81 | })
82 | );
83 | if (feePayerCounter.current === currentCounter) {
84 | setFeePayerBalances(balances);
85 | }
86 | })();
87 |
88 | const subscriptions = feePayers.map((feePayer, index) => {
89 | return connection.onAccountChange(
90 | feePayer.publicKey,
91 | (accountInfo: AccountInfo) => {
92 | setFeePayerBalances((balances) => {
93 | const copy = [...balances];
94 | copy[index] = accountInfo.lamports;
95 | return copy;
96 | });
97 | }
98 | );
99 | });
100 |
101 | return () => {
102 | subscriptions.forEach((subscription) => {
103 | connection.removeAccountChangeListener(subscription);
104 | });
105 | };
106 | }, [connection, feePayers]);
107 |
108 | const state = React.useMemo(
109 | () => ({
110 | payer: balance,
111 | feePayers: feePayerBalances,
112 | }),
113 | [balance, feePayerBalances]
114 | );
115 |
116 | return (
117 | {children}
118 | );
119 | }
120 |
121 | export function useBalanceState(): State {
122 | const state = React.useContext(StateContext);
123 | if (state === undefined) {
124 | throw new Error(`useBalanceState must be used within a BalanceProvider`);
125 | }
126 | return state;
127 | }
128 |
--------------------------------------------------------------------------------
/client/src/providers/rpc/blockhash.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Blockhash, Commitment, Connection } from "@solana/web3.js";
3 | import { sleep, reportError } from "utils";
4 | import { useConnection } from ".";
5 |
6 | const POLL_INTERVAL_MS = 2000;
7 |
8 | export enum ActionType {
9 | Start,
10 | Stop,
11 | Update,
12 | }
13 |
14 | interface Stop {
15 | type: ActionType.Stop;
16 | }
17 |
18 | interface Update {
19 | type: ActionType.Update;
20 | blockhash: Blockhash;
21 | }
22 |
23 | interface State {
24 | blockhash?: Blockhash;
25 | }
26 |
27 | type Action = Stop | Update;
28 | type Dispatch = (action: Action) => void;
29 |
30 | function reducer(state: State, action: Action): State {
31 | switch (action.type) {
32 | case ActionType.Stop: {
33 | return {};
34 | }
35 | case ActionType.Update: {
36 | return Object.assign({}, state, {
37 | blockhash: action.blockhash,
38 | });
39 | }
40 | }
41 | }
42 |
43 | const StateContext = React.createContext(undefined);
44 | const DispatchContext = React.createContext(undefined);
45 |
46 | type BlockhashProviderProps = { children: React.ReactNode };
47 | export function BlockhashProvider({ children }: BlockhashProviderProps) {
48 | const [state, dispatch] = React.useReducer(reducer, {});
49 | const connection = useConnection();
50 | const connectionRef = React.useRef(connection);
51 | const refreshingRef = React.useRef(false);
52 |
53 | React.useEffect(() => {
54 | if (connection === undefined) return;
55 |
56 | connectionRef.current = connection;
57 | refresh(dispatch, connectionRef, refreshingRef);
58 | const timerId = window.setInterval(
59 | () => refresh(dispatch, connectionRef, refreshingRef),
60 | POLL_INTERVAL_MS
61 | );
62 |
63 | return () => {
64 | clearInterval(timerId);
65 | dispatch({ type: ActionType.Stop });
66 | };
67 | }, [connection]);
68 |
69 | return (
70 |
71 |
72 | {children}
73 |
74 |
75 | );
76 | }
77 |
78 | export function useBlockhash() {
79 | const state = React.useContext(StateContext);
80 | if (!state) {
81 | throw new Error(`useBlockhash must be used within a BlockhashProvider`);
82 | }
83 |
84 | return state.blockhash;
85 | }
86 |
87 | async function nodeProgress(
88 | connection: Connection,
89 | commitment: Commitment
90 | ): Promise<{ blockhash: Blockhash; slot: number }> {
91 | const [{ blockhash }, slot] = await Promise.all([
92 | connection.getLatestBlockhash(commitment),
93 | connection.getSlot(commitment),
94 | ]);
95 |
96 | return { blockhash, slot };
97 | }
98 |
99 | async function refresh(
100 | dispatch: Dispatch,
101 | connectionRef: React.MutableRefObject,
102 | refreshingRef: React.MutableRefObject
103 | ) {
104 | let blockhash = undefined;
105 | const connection = connectionRef.current;
106 | if (connection === undefined) return;
107 |
108 | if (refreshingRef.current) return;
109 | refreshingRef.current = true;
110 |
111 | let reported = false;
112 | while (blockhash === undefined && connection === connectionRef.current) {
113 | try {
114 | const processedProgress = await nodeProgress(connection, "processed");
115 | const confirmedProgress = await nodeProgress(connection, "confirmed");
116 | const finalizedProgress = await nodeProgress(connection, "finalized");
117 | console.log(
118 | `[${processedProgress.slot}, ${confirmedProgress.slot}, ${
119 | finalizedProgress.slot
120 | }], [${processedProgress.blockhash.slice(
121 | 0,
122 | 5
123 | )}, ${confirmedProgress.blockhash.slice(
124 | 0,
125 | 5
126 | )}, ${finalizedProgress.blockhash.slice(0, 5)}]`
127 | );
128 | blockhash = finalizedProgress.blockhash;
129 | dispatch({ type: ActionType.Update, blockhash });
130 | } catch (err) {
131 | if (!reported) reportError(err, "Failed to refresh blockhash");
132 | reported = true;
133 | await sleep(1000);
134 | }
135 | }
136 |
137 | refreshingRef.current = false;
138 | }
139 |
--------------------------------------------------------------------------------
/client/src/providers/rpc/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Connection } from "@solana/web3.js";
3 | import { useServerConfig } from "providers/server/http";
4 | import { BlockhashProvider } from "./blockhash";
5 | import { BalanceProvider } from "./balance";
6 |
7 | type SetUrl = (url: string) => void;
8 | type State = [string | undefined, SetUrl];
9 |
10 | type ConnectionState = {
11 | connection?: Connection;
12 | };
13 |
14 | const StateContext = React.createContext(undefined);
15 | const ConnectionContext = React.createContext(
16 | undefined
17 | );
18 |
19 | type ProviderProps = { children: React.ReactNode };
20 | export function RpcProvider({ children }: ProviderProps) {
21 | const state = React.useState();
22 | const [rpcUrl, setRpcUrl] = state;
23 |
24 | // Reset rpc url whenever config is fetched
25 | const configRpcUrl = useServerConfig()?.rpcUrl;
26 | React.useEffect(() => {
27 | setRpcUrl(configRpcUrl);
28 | }, [configRpcUrl, setRpcUrl]);
29 |
30 | const connection: ConnectionState = React.useMemo(() => {
31 | if (rpcUrl === undefined) return {};
32 | try {
33 | const url = new URL(rpcUrl).toString();
34 | return { connection: new Connection(url, "confirmed") };
35 | } catch (err) {
36 | console.error(err);
37 | return {};
38 | }
39 | }, [rpcUrl]);
40 |
41 | return (
42 |
43 |
44 |
45 | {children}
46 |
47 |
48 |
49 | );
50 | }
51 |
52 | export function useRpcUrlState(): State {
53 | const state = React.useContext(StateContext);
54 | if (state === undefined) {
55 | throw new Error(`useRpcUrlState must be used within a RpcProvider`);
56 | }
57 | return state;
58 | }
59 |
60 | export function useConnection(): Connection | undefined {
61 | const state = React.useContext(ConnectionContext);
62 | if (state === undefined) {
63 | throw new Error(`useConnection must be used within a RpcProvider`);
64 | }
65 | return state.connection;
66 | }
67 |
--------------------------------------------------------------------------------
/client/src/providers/server/http/config.ts:
--------------------------------------------------------------------------------
1 | import { PublicKey, Cluster } from "@solana/web3.js";
2 |
3 | export interface Config {
4 | cluster: Cluster | undefined;
5 | rpcUrl: string;
6 | programId: PublicKey;
7 | airdropEnabled: boolean;
8 | }
9 |
10 | function stringToCluster(str: string | undefined): Cluster | undefined {
11 | switch (str) {
12 | case "devnet":
13 | case "testnet":
14 | case "mainnet-beta": {
15 | return str;
16 | }
17 | default:
18 | return undefined;
19 | }
20 | }
21 |
22 | export function configFromInit(response: any): Config {
23 | const cluster = stringToCluster(response.cluster);
24 | return {
25 | cluster,
26 | rpcUrl: response.clusterUrl,
27 | programId: new PublicKey(response.programId),
28 | airdropEnabled: !response.paymentRequired,
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/client/src/providers/server/http/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Config as ServerConfig } from "./config";
3 | import { useServer } from "providers/server";
4 | import { fetchWithRetry } from "./request";
5 | import { Connection } from "@solana/web3.js";
6 |
7 | export enum ConfigStatus {
8 | Initialized,
9 | Fetching,
10 | Ready,
11 | Failure,
12 | }
13 |
14 | interface State {
15 | status: ConfigStatus;
16 | config?: ServerConfig;
17 | connection?: Connection;
18 | }
19 |
20 | interface Initialized {
21 | status: ConfigStatus.Initialized;
22 | config: ServerConfig;
23 | }
24 |
25 | interface Fetching {
26 | status: ConfigStatus.Fetching;
27 | }
28 |
29 | interface Ready {
30 | status: ConfigStatus.Ready;
31 | }
32 |
33 | interface Failure {
34 | status: ConfigStatus.Failure;
35 | config?: undefined;
36 | }
37 |
38 | export type Action = Initialized | Fetching | Ready | Failure;
39 | export type Dispatch = (action: Action) => void;
40 |
41 | function configReducer(state: State, action: Action): State {
42 | switch (action.status) {
43 | case ConfigStatus.Ready:
44 | case ConfigStatus.Initialized: {
45 | return { ...state, ...action };
46 | }
47 | case ConfigStatus.Failure: {
48 | if (state.status === ConfigStatus.Fetching) {
49 | return { ...state, ...action };
50 | } else {
51 | return state;
52 | }
53 | }
54 | case ConfigStatus.Fetching: {
55 | return {
56 | ...state,
57 | ...action,
58 | };
59 | }
60 | }
61 | }
62 |
63 | const StateContext = React.createContext(undefined);
64 | const RefContext = React.createContext<
65 | React.MutableRefObject | undefined
66 | >(undefined);
67 | const DispatchContext = React.createContext(undefined);
68 |
69 | type ApiProviderProps = { children: React.ReactNode };
70 | export function HttpProvider({ children }: ApiProviderProps) {
71 | const [state, dispatch] = React.useReducer(configReducer, {
72 | status: ConfigStatus.Fetching,
73 | });
74 |
75 | const { httpUrl } = useServer();
76 | const httpUrlRef = React.useRef(httpUrl);
77 | React.useEffect(() => {
78 | httpUrlRef.current = httpUrl;
79 | initConfig(dispatch, httpUrlRef);
80 | }, [httpUrl]);
81 |
82 | React.useEffect(() => {
83 | httpUrlRef.current = httpUrl;
84 | }, [httpUrl]);
85 |
86 | return (
87 |
88 |
89 | {children}
90 |
91 |
92 | );
93 | }
94 |
95 | async function initConfig(
96 | dispatch: Dispatch,
97 | httpUrlRef: React.MutableRefObject
98 | ): Promise {
99 | return fetchWithRetry(dispatch, httpUrlRef);
100 | }
101 |
102 | export function useServerConfig() {
103 | const context = React.useContext(StateContext);
104 | if (!context) {
105 | throw new Error(`useServerConfig must be used within a ApiProvider`);
106 | }
107 | return context.config;
108 | }
109 |
110 | export function useIsFetching() {
111 | const context = React.useContext(StateContext);
112 | if (!context) {
113 | throw new Error(`useIsFetching must be used within a ApiProvider`);
114 | }
115 | return context.status === ConfigStatus.Fetching;
116 | }
117 |
118 | export function useClusterParam(): string {
119 | const context = React.useContext(StateContext);
120 | if (!context) {
121 | throw new Error(`useClusterParam must be used within a ApiProvider`);
122 | }
123 | const cluster = context.config?.cluster;
124 | const rpcUrl = context.config?.rpcUrl;
125 | if (!cluster && rpcUrl) {
126 | return `cluster=custom&customUrl=${rpcUrl}`;
127 | } else if (cluster && cluster !== "mainnet-beta") {
128 | return `cluster=${cluster}`;
129 | } else {
130 | return "";
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/client/src/providers/server/http/request.tsx:
--------------------------------------------------------------------------------
1 | import { configFromInit } from "./config";
2 | import { sleep, reportError } from "utils";
3 | import { Action, Dispatch, ConfigStatus } from "./index";
4 |
5 | export async function fetchWithRetry(
6 | dispatch: Dispatch,
7 | httpUrlRef: React.MutableRefObject
8 | ) {
9 | dispatch({
10 | status: ConfigStatus.Fetching,
11 | });
12 |
13 | const httpUrl = httpUrlRef.current;
14 | while (httpUrl === httpUrlRef.current) {
15 | let response: Action | "retry" = await fetchInit(httpUrl);
16 | if (httpUrl !== httpUrlRef.current) break;
17 | if (response === "retry") {
18 | await sleep(2000);
19 | } else {
20 | dispatch(response);
21 | break;
22 | }
23 | }
24 | }
25 |
26 | async function fetchInit(httpUrl: string): Promise {
27 | try {
28 | const body = JSON.stringify({});
29 | const response = await fetch(
30 | new Request(httpUrl + "/init", {
31 | method: "POST",
32 | headers: {
33 | "Content-Type": "application/json",
34 | },
35 | body,
36 | })
37 | );
38 | const data = await response.json();
39 | if (!("clusterUrl" in data) || !("programId" in data)) {
40 | throw new Error("Received invalid response");
41 | }
42 |
43 | return {
44 | status: ConfigStatus.Initialized,
45 | config: configFromInit(data),
46 | };
47 | } catch (err) {
48 | reportError(err, "/init failed");
49 | return "retry";
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/client/src/providers/server/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Cluster } from "@solana/web3.js";
3 | import { useLocation } from "react-router-dom";
4 | import { isLocalHost } from "../../utils";
5 | import { HttpProvider } from "./http";
6 | import { SocketProvider } from "./socket";
7 |
8 | type Server = Cluster | "custom";
9 | export const DEFAULT_SERVER = isLocalHost() ? "custom" : "mainnet-beta";
10 | export const SERVERS: Server[] = isLocalHost()
11 | ? ["custom"]
12 | : ["mainnet-beta", "testnet", "devnet", "custom"];
13 |
14 | const DEFAULT_CUSTOM_URL = `http://${window.location.hostname}:${
15 | process.env.PORT || 8080
16 | }`;
17 |
18 | export function serverName(server: Server): string {
19 | switch (server) {
20 | case "mainnet-beta":
21 | return "Mainnet Beta";
22 | case "testnet":
23 | return "Testnet";
24 | case "devnet":
25 | return "Devnet";
26 | case "custom":
27 | return "Custom";
28 | }
29 | }
30 |
31 | function parseQuery(query: URLSearchParams): Server {
32 | const clusterParam = query.get("cluster");
33 | switch (clusterParam) {
34 | case "devnet":
35 | return "devnet";
36 | case "testnet":
37 | return "testnet";
38 | case "mainnet-beta":
39 | return "mainnet-beta";
40 | case "custom":
41 | return "custom";
42 | default:
43 | return DEFAULT_SERVER;
44 | }
45 | }
46 |
47 | type SetShowModal = React.Dispatch>;
48 | type ModalState = [boolean, SetShowModal];
49 | const ModalContext = React.createContext(undefined);
50 | type SetCustomUrl = React.Dispatch>;
51 | type SetServer = React.Dispatch>;
52 | type ServerState = {
53 | server: Server;
54 | setServer: SetServer;
55 | customUrl: string;
56 | setCustomUrl: SetCustomUrl;
57 | };
58 | const ServerContext = React.createContext(undefined);
59 |
60 | type ProviderProps = { children: React.ReactNode };
61 | export function ServerProvider({ children }: ProviderProps) {
62 | const query = new URLSearchParams(useLocation().search);
63 | const serverParam = parseQuery(query);
64 | const [server, setServer] = React.useState(serverParam);
65 | const [customUrl, setCustomUrl] = React.useState(DEFAULT_CUSTOM_URL);
66 | const [showModal, setShowModal] = React.useState(false);
67 |
68 | // Update state when query params change
69 | React.useEffect(() => {
70 | setServer(serverParam);
71 | }, [serverParam]);
72 |
73 | const modalState: ModalState = React.useMemo(() => {
74 | return [showModal, setShowModal];
75 | }, [showModal]);
76 |
77 | return (
78 |
81 |
82 |
83 | {children}
84 |
85 |
86 |
87 | );
88 | }
89 |
90 | function getServerUrl(server: Server, customUrl: string) {
91 | switch (server) {
92 | case "custom": {
93 | return customUrl;
94 | }
95 | default: {
96 | const useHttp = isLocalHost();
97 | let slug: string = server;
98 | if (server === "mainnet-beta") {
99 | slug = "mainnet";
100 | }
101 | return `${
102 | useHttp ? "http" : "https"
103 | }://break-solana-${slug}.herokuapp.com`;
104 | }
105 | }
106 | }
107 |
108 | export function useServer() {
109 | const context = React.useContext(ServerContext);
110 | if (!context) {
111 | throw new Error(`useServer must be used within a ServerProvider`);
112 | }
113 | const { server, customUrl } = context;
114 | const httpUrl = getServerUrl(server, customUrl);
115 | const webSocketUrl = httpUrl.replace("http", "ws");
116 |
117 | return {
118 | server,
119 | httpUrl,
120 | webSocketUrl,
121 | name: serverName(server),
122 | };
123 | }
124 |
125 | export function useCustomUrl(): [string, SetCustomUrl] {
126 | const context = React.useContext(ServerContext);
127 | if (!context) {
128 | throw new Error(`useCustomUrl must be used within a ServerProvider`);
129 | }
130 | return [context.customUrl, context.setCustomUrl];
131 | }
132 |
133 | export function useClusterModal() {
134 | const context = React.useContext(ModalContext);
135 | if (!context) {
136 | throw new Error(`useClusterModal must be used within a ServerProvider`);
137 | }
138 | return context;
139 | }
140 |
--------------------------------------------------------------------------------
/client/src/providers/server/socket.tsx:
--------------------------------------------------------------------------------
1 | import { useClientConfig } from "providers/config";
2 | import * as React from "react";
3 | import { useServer } from ".";
4 |
5 | type SetSocket = React.Dispatch>;
6 | const SocketContext = React.createContext(undefined);
7 |
8 | const FailureCallbackContext = React.createContext<
9 | React.MutableRefObject | undefined
10 | >(undefined);
11 |
12 | type SetActiveUsers = React.Dispatch>;
13 | const ActiveUsersContext = React.createContext(undefined);
14 |
15 | const SWITCH_URL_CODE = 4444;
16 |
17 | type ServerSocket = {
18 | socket: WebSocket;
19 | id: number;
20 | };
21 |
22 | let socketCounter = 0;
23 |
24 | type FailureCallback = (signature: string, reason: string) => void;
25 |
26 | type SocketProviderProps = { children: React.ReactNode };
27 | export function SocketProvider({ children }: SocketProviderProps) {
28 | let [socket, setSocket] = React.useState(undefined);
29 | let failureCallbackRef = React.useRef(() => {});
30 | let [activeUsers, setActiveUsers] = React.useState(1);
31 | let [{ useTpu, rpcUrl }] = useClientConfig();
32 |
33 | const { webSocketUrl } = useServer();
34 | React.useEffect(() => {
35 | newSocket(webSocketUrl, setSocket, setActiveUsers, failureCallbackRef);
36 | }, [webSocketUrl]);
37 |
38 | React.useEffect(() => {
39 | if (socket) {
40 | socket.socket.send(useTpu ? "tpu" : "rpc");
41 | }
42 | }, [socket, useTpu]);
43 |
44 | React.useEffect(() => {
45 | if (socket && rpcUrl) {
46 | socket.socket.send(rpcUrl);
47 | }
48 | }, [socket, rpcUrl]);
49 |
50 | return (
51 |
52 |
53 |
54 | {children}
55 |
56 |
57 |
58 | );
59 | }
60 |
61 | function newSocket(
62 | webSocketUrl: string,
63 | setSocket: SetSocket,
64 | setActiveUsers: SetActiveUsers,
65 | failureCallbackRef: React.MutableRefObject
66 | ): WebSocket | undefined {
67 | socketCounter++;
68 | const id = socketCounter;
69 |
70 | let socket: WebSocket;
71 | try {
72 | socket = new WebSocket(webSocketUrl);
73 | } catch (err) {
74 | return;
75 | }
76 |
77 | socket.onopen = () =>
78 | setSocket((serverSocket) => {
79 | if (!serverSocket || serverSocket.id <= id) {
80 | if (serverSocket && serverSocket.socket.readyState === WebSocket.OPEN) {
81 | serverSocket.socket.close(SWITCH_URL_CODE);
82 | }
83 | return { socket, id };
84 | } else {
85 | socket.close(SWITCH_URL_CODE);
86 | return serverSocket;
87 | }
88 | });
89 |
90 | socket.onmessage = (e) => {
91 | const data = JSON.parse(e.data);
92 | if ("activeUsers" in data) {
93 | setActiveUsers(data.activeUsers);
94 | }
95 |
96 | if (data?.type === "failure") {
97 | let signature = data?.signature;
98 | let reason = data?.reason;
99 | if (typeof signature === "string" && typeof reason === "string") {
100 | failureCallbackRef.current(signature, reason);
101 | }
102 | }
103 | };
104 |
105 | socket.onclose = async (event) => {
106 | setSocket((serverSocket) => {
107 | // Socket may have been updated already
108 | if (!serverSocket || serverSocket.id === id) {
109 | // Reconnect if close was not explicit
110 | if (event.code !== SWITCH_URL_CODE) {
111 | console.error("Socket closed, reconnecting...");
112 | // TODO: Re-enable
113 | // reportError(new Error("Socket was closed"), "Socket closed");
114 | setTimeout(() => {
115 | newSocket(
116 | webSocketUrl,
117 | setSocket,
118 | setActiveUsers,
119 | failureCallbackRef
120 | );
121 | }, 5000);
122 | }
123 | return undefined;
124 | }
125 | return serverSocket;
126 | });
127 | };
128 |
129 | socket.onerror = async () => {
130 | socket.close();
131 | };
132 |
133 | return socket;
134 | }
135 |
136 | export function useSocket() {
137 | return React.useContext(SocketContext);
138 | }
139 |
140 | export function useActiveUsers() {
141 | const context = React.useContext(ActiveUsersContext);
142 | if (!context) {
143 | throw new Error(`useActiveUsers must be used within a SocketProvider`);
144 | }
145 |
146 | return context;
147 | }
148 |
149 | export function useFailureCallback() {
150 | const context = React.useContext(FailureCallbackContext);
151 | if (!context) {
152 | throw new Error(`useFailureCallback must be used within a SocketProvider`);
153 | }
154 |
155 | return context;
156 | }
157 |
--------------------------------------------------------------------------------
/client/src/providers/transactions/confirmed.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { useDispatch, TrackedCommitment } from "./index";
4 | import { useConnection } from "providers/rpc";
5 | import * as Bytes from "utils/bytes";
6 | import { AccountInfo } from "@solana/web3.js";
7 | import { useAccountsState } from "providers/accounts";
8 | import { useClientConfig } from "providers/config";
9 |
10 | // Determine commitment levels to subscribe to. "singleGossip" is used
11 | // to stop tx send retries so it must be returned
12 | export const subscribedCommitments = (
13 | trackedCommitment: TrackedCommitment,
14 | showDebugTable: boolean
15 | ): TrackedCommitment[] => {
16 | if (showDebugTable) return ["confirmed"];
17 | switch (trackedCommitment) {
18 | case "processed": {
19 | return ["processed", "confirmed"];
20 | }
21 | default: {
22 | return ["confirmed"];
23 | }
24 | }
25 | };
26 |
27 | type Props = { children: React.ReactNode };
28 | export function ConfirmedHelper({ children }: Props) {
29 | const dispatch = useDispatch();
30 | const connection = useConnection();
31 | const accounts = useAccountsState().accounts;
32 | const [{ showDebugTable, trackedCommitment }] = useClientConfig();
33 |
34 | React.useEffect(() => {
35 | if (connection === undefined || accounts === undefined) return;
36 | if (showDebugTable) return;
37 |
38 | const commitments = subscribedCommitments(
39 | trackedCommitment,
40 | showDebugTable
41 | );
42 | const partitionCount = accounts.programAccounts.length;
43 |
44 | const accountSubscriptions = accounts.programAccounts.map(
45 | (account, partition) =>
46 | commitments.map((commitment) =>
47 | connection.onAccountChange(
48 | account,
49 | (accountInfo: AccountInfo, { slot }) => {
50 | const ids = new Set(Bytes.programDataToIds(accountInfo.data));
51 | const activeIdPartition = {
52 | ids,
53 | partition,
54 | partitionCount,
55 | };
56 | dispatch({
57 | type: "update",
58 | activeIdPartition,
59 | commitment,
60 | estimatedSlot: slot,
61 | receivedAt: performance.now(),
62 | });
63 | },
64 | commitment
65 | )
66 | )
67 | );
68 |
69 | return () => {
70 | accountSubscriptions.forEach((listeners) => {
71 | listeners.forEach((listener: any) => {
72 | connection.removeAccountChangeListener(listener);
73 | });
74 | });
75 | };
76 | }, [dispatch, connection, accounts, showDebugTable, trackedCommitment]);
77 |
78 | return <>{children}>;
79 | }
80 |
--------------------------------------------------------------------------------
/client/src/providers/transactions/selected.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useTransactions, TransactionState } from "./index";
3 |
4 | type SetSelected = React.Dispatch>;
5 | type SelectedState = [TransactionState | undefined, SetSelected];
6 | export const SelectedContext = React.createContext(
7 | undefined
8 | );
9 | type ProviderProps = { children: React.ReactNode };
10 | export function SelectedTxProvider({ children }: ProviderProps) {
11 | const transactions = useTransactions();
12 | const [signature, selectSignature] = React.useState(
13 | undefined
14 | );
15 | const [transaction, selectTransaction] = React.useState<
16 | TransactionState | undefined
17 | >(undefined);
18 |
19 | React.useEffect(() => {
20 | selectTransaction(
21 | transactions.find((tx) => tx.details.signature === signature)
22 | );
23 | }, [transactions, signature]);
24 |
25 | const selectedState: SelectedState = React.useMemo(() => {
26 | return [transaction, selectSignature];
27 | }, [transaction]);
28 |
29 | return (
30 |
31 | {children}
32 |
33 | );
34 | }
35 |
36 | export function useSelectedTransaction() {
37 | const state = React.useContext(SelectedContext);
38 | if (!state) {
39 | throw new Error(`useSelectedTx must be used within a TransactionsProvider`);
40 | }
41 | return state[0];
42 | }
43 |
44 | export function useSelectTransaction() {
45 | const state = React.useContext(SelectedContext);
46 | if (!state) {
47 | throw new Error(`useSelectTx must be used within a TransactionsProvider`);
48 | }
49 | return state[1];
50 | }
51 |
--------------------------------------------------------------------------------
/client/src/providers/transactions/tps.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useCreatedCount } from "./index";
3 |
4 | const TPS_REFRESH_MS = 100;
5 | const TPS_LOOK_BACK = 10;
6 |
7 | export const TpsContext = React.createContext(undefined);
8 | type ProviderProps = { children: React.ReactNode };
9 | export function TpsProvider({ children }: ProviderProps) {
10 | const [tps, setTps] = React.useState(0);
11 | const createdCount = useCreatedCount();
12 | const createdCountRef = React.useRef(0);
13 | createdCountRef.current = createdCount;
14 |
15 | React.useEffect(() => {
16 | const recentCounts: number[] = [];
17 | const timerId = setInterval(() => {
18 | if (createdCountRef.current === 0) {
19 | recentCounts.splice(0);
20 | setTps(0);
21 | return;
22 | }
23 |
24 | recentCounts.push(createdCountRef.current);
25 | while (recentCounts.length - 1 > TPS_LOOK_BACK) {
26 | recentCounts.shift();
27 | }
28 |
29 | const ticksElapsed = recentCounts.length - 1;
30 | if (ticksElapsed <= 0) return;
31 |
32 | const oldTxCount = recentCounts[0];
33 | const latestTxCount = recentCounts[ticksElapsed];
34 | const tps =
35 | (latestTxCount - oldTxCount) / ((TPS_REFRESH_MS / 1000) * ticksElapsed);
36 | setTps(Math.floor(tps));
37 | }, TPS_REFRESH_MS);
38 | return () => {
39 | clearInterval(timerId);
40 | };
41 | }, []);
42 |
43 | return {children} ;
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/providers/wallet.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Keypair } from "@solana/web3.js";
3 | import { getLocalStorageKeypair } from "utils";
4 | import { useHistory, useLocation } from "react-router";
5 |
6 | interface State {
7 | wallet?: Keypair;
8 | selectWallet: (wallet: Keypair | undefined) => void;
9 | }
10 |
11 | const StateContext = React.createContext(undefined);
12 |
13 | type Props = { children: React.ReactNode };
14 | export function WalletProvider({ children }: Props) {
15 | const [wallet, setWallet] = React.useState();
16 |
17 | const history = useHistory();
18 | const location = useLocation();
19 | const selectWallet = React.useCallback(
20 | (keypair: Keypair | undefined) => {
21 | setWallet(keypair);
22 | if (keypair === undefined) {
23 | history.push({ ...location, pathname: "/wallet" });
24 | } else {
25 | history.push({ ...location, pathname: "/start" });
26 | }
27 | },
28 | [history, location]
29 | );
30 |
31 | const state = React.useMemo(
32 | () => ({
33 | wallet,
34 | selectWallet,
35 | }),
36 | [wallet, selectWallet]
37 | );
38 |
39 | return (
40 | {children}
41 | );
42 | }
43 |
44 | export const LOCAL_WALLET = (() => {
45 | return getLocalStorageKeypair("paymentKey");
46 | })();
47 |
48 | export function useWalletState(): State {
49 | const state = React.useContext(StateContext);
50 | if (state === undefined) {
51 | throw new Error(`usePayerState must be used within a WalletProvider`);
52 | }
53 | return state;
54 | }
55 |
--------------------------------------------------------------------------------
/client/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/client/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom/extend-expect";
6 |
--------------------------------------------------------------------------------
/client/src/styles/custom-variables.scss:
--------------------------------------------------------------------------------
1 | $path-to-img: "../../images/dashkit" !default;
2 | $path-to-fonts: "../../fonts" !default;
3 |
4 | $font-family-sans-serif: "Exo 2", sans-serif !default;
5 | $font-family-base: $font-family-sans-serif !default;
6 |
7 | $border-radius: 0.125rem !default;
8 | $gray-700: #888 !default;
9 | $gray-600-dark: #555 !default;
10 | $gray-700-dark: #444 !default;
11 | $gray-750-dark: #333 !default;
12 | $gray-800-dark: #222 !default;
13 | $gray-900-dark: #111 !default;
14 | $black-dark: #000000;
15 |
16 | $black: #000000;
17 | $white: #ffffff;
18 | $green: #00ffad;
19 | $pink: #ff00a8;
20 | $purple: #ac62dd;
21 | $red: #ea134d;
22 | $orange: #e8873f;
23 |
24 | $info: $purple;
25 | $danger: $red;
26 | $warning: $pink;
27 | $primary: $green;
28 |
29 | $card-border-radius: 0.25rem !default;
30 | $card-outline-color: $gray-600-dark !default;
31 | $spacer: 1rem !default;
32 |
33 | $modal-md: 400px !default;
34 |
--------------------------------------------------------------------------------
/client/src/styles/custom.scss:
--------------------------------------------------------------------------------
1 | @import "custom/app";
2 | @import "custom/game";
3 | @import "custom/home";
4 | @import "custom/header";
5 | @import "custom/payment-card";
6 | @import "custom/results";
7 | @import "custom/sidebar";
8 | @import "custom/tx-container";
9 | @import "custom/tx-square";
10 |
11 | .card-body,
12 | .card-header,
13 | .card-footer {
14 | padding: 1rem;
15 | }
16 |
17 | .card-header {
18 | height: 50px;
19 | }
20 |
21 | .card {
22 | margin-bottom: 0.75rem;
23 | }
24 |
25 | .header {
26 | margin-bottom: 1rem;
27 | }
28 |
29 | .header .header-body {
30 | padding: 1rem 0;
31 | }
32 |
33 | .sticky {
34 | position: sticky;
35 | top: 0px;
36 | border-top: 0px solid black !important;
37 | }
38 |
39 | .btn {
40 | letter-spacing: 0.1em;
41 | }
42 |
43 | .btn.input-group {
44 | input {
45 | cursor: pointer;
46 |
47 | &:focus {
48 | cursor: text;
49 | }
50 | }
51 | }
52 |
53 | .border-dark-purple {
54 | border-color: darken($purple, 20%);
55 | }
56 |
57 | .border-top-dark {
58 | border-top: 1px solid $dark;
59 | }
60 |
61 | .btn-pink {
62 | &.btn-secondary {
63 | border-color: darken($pink, 10%);
64 | }
65 |
66 | background-color: $pink;
67 |
68 | @include hover-focus {
69 | background-color: darken($pink, 10%);
70 | }
71 | }
72 |
73 | .cluster-modal {
74 | z-index: 3000;
75 | }
76 |
77 | .header {
78 | z-index: 2000;
79 | }
80 |
81 | .modal-backdrop {
82 | pointer-events: none;
83 | }
84 |
85 | .modal.show {
86 | display: block;
87 | }
88 |
89 | .b-white {
90 | background: $white;
91 | }
92 |
93 | .b-black {
94 | background: $black !important;
95 | }
96 |
97 | .c-pointer {
98 | cursor: pointer;
99 | }
100 |
101 | .touch-action-none {
102 | touch-action: none;
103 | }
104 |
105 | .popover-container {
106 | position: relative;
107 |
108 | .popover {
109 | max-width: fit-content;
110 |
111 | &.right {
112 | left: -1.5rem;
113 | }
114 |
115 | &.bs-popover-bottom {
116 | background: $gray-750-dark;
117 | top: 1.5rem;
118 | }
119 |
120 | .popover-body {
121 | color: white;
122 | min-width: max-content;
123 | }
124 |
125 | .arrow::after {
126 | border-top-color: $gray-750-dark;
127 | border-bottom-color: $gray-750-dark;
128 | }
129 | }
130 | }
131 |
132 | .flex-basis-auto {
133 | flex-basis: auto !important;
134 | }
135 |
136 | .z-auto {
137 | z-index: auto !important;
138 | }
139 |
140 | .debug-row {
141 | cursor: pointer;
142 |
143 | &:hover {
144 | background: #111;
145 | }
146 | }
147 |
148 | .min-width-0 {
149 | min-width: 0;
150 | }
151 |
--------------------------------------------------------------------------------
/client/src/styles/custom/_app.scss:
--------------------------------------------------------------------------------
1 | img.graphic {
2 | position: fixed;
3 | top: 0;
4 | left: 0;
5 | height: 100vh;
6 | z-index: -1;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/styles/custom/_game.scss:
--------------------------------------------------------------------------------
1 | @include media-breakpoint-up(lg) {
2 | .stat-card {
3 | padding: 0px 10px;
4 |
5 | &:first-child {
6 | padding-left: 12px;
7 | }
8 |
9 | &:last-child {
10 | padding-right: 12px;
11 | }
12 | }
13 | }
14 |
15 | @include media-breakpoint-down(md) {
16 | .stat-card {
17 | &:nth-child(odd) {
18 | padding-right: 6px;
19 | }
20 |
21 | &:nth-child(even) {
22 | padding-left: 6px;
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/styles/custom/_header.scss:
--------------------------------------------------------------------------------
1 | .solana-header.header {
2 | margin-bottom: 0;
3 | }
4 |
5 | .solana-header .btn-group {
6 | pointer-events: none;
7 | cursor: default;
8 |
9 | &.countdown {
10 | min-width: 60px;
11 | }
12 | }
13 |
14 | .solana-header img.break {
15 | height: 40px;
16 |
17 | @include media-breakpoint-down(md) {
18 | height: 25px;
19 | }
20 | }
21 |
22 | .solana-header img.solana {
23 | height: 15px;
24 |
25 | @include media-breakpoint-down(md) {
26 | height: 10px;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/styles/custom/_home.scss:
--------------------------------------------------------------------------------
1 | .introduction {
2 | line-height: 3rem;
3 | font-weight: 100;
4 | font-size: 1.5rem;
5 |
6 | @include media-breakpoint-down(sm) {
7 | line-height: 2rem;
8 | font-size: 1.2rem;
9 | }
10 | }
11 |
12 | @include media-breakpoint-down(md) {
13 | .home-page {
14 | max-width: 768px;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/styles/custom/_payment-card.scss:
--------------------------------------------------------------------------------
1 | .qr-code {
2 | width: 250px;
3 | height: 250px;
4 | }
5 |
--------------------------------------------------------------------------------
/client/src/styles/custom/_results.scss:
--------------------------------------------------------------------------------
1 | .stats {
2 | overflow-x: scroll;
3 | flex-wrap: nowrap;
4 | max-width: 800px;
5 |
6 | @include media-breakpoint-down(sm) {
7 | padding-bottom: 10px !important;
8 | }
9 |
10 | scrollbar-color: #2a2a2a transparent;
11 | scrollbar-width: thin;
12 |
13 | &::-webkit-scrollbar {
14 | height: 0.4rem;
15 | }
16 |
17 | &::-webkit-scrollbar-track {
18 | background: transparent;
19 | }
20 |
21 | &::-webkit-scrollbar-thumb {
22 | border-radius: 4px;
23 | background: #2a2a2a;
24 | }
25 | }
26 |
27 | .stat-circle {
28 | height: 200px;
29 | width: 200px;
30 | @include media-breakpoint-down(sm) {
31 | height: 175px;
32 | width: 175px;
33 | }
34 | }
35 |
36 | .results-summary h3 {
37 | line-height: 1.5rem;
38 | font-weight: 100;
39 | }
40 |
41 | .donut-content {
42 | &.allow-events {
43 | pointer-events: none;
44 | }
45 |
46 | position: absolute;
47 | top: 0px;
48 | left: 0px;
49 | right: 0px;
50 | bottom: 0px;
51 | display: flex;
52 | flex-direction: column;
53 | justify-content: center;
54 | align-items: center;
55 | padding: 30px;
56 | }
57 |
--------------------------------------------------------------------------------
/client/src/styles/custom/_sidebar.scss:
--------------------------------------------------------------------------------
1 | .page-sidebar {
2 | min-width: 250px;
3 | max-width: 300px;
4 | width: 25vw;
5 | background-color: $gray-800-dark;
6 | border-right: 1px solid $purple;
7 | }
8 |
9 | .sidebar-body {
10 | margin: 1rem;
11 | }
12 |
13 | @media screen and (max-width: 768px) {
14 | .page-sidebar {
15 | display: none;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/client/src/styles/custom/_tx-container.scss:
--------------------------------------------------------------------------------
1 | .main {
2 | flex: 1;
3 | }
4 |
5 | .content {
6 | display: flex;
7 | flex-direction: column;
8 | height: 100%;
9 | }
10 |
11 | .debug-wrapper {
12 | overflow-y: auto;
13 | flex-grow: 1;
14 | height: 100px;
15 |
16 | outline: none;
17 | outline-color: transparent;
18 |
19 | scrollbar-color: rgba(0, 255, 173, 0.4) transparent;
20 | scrollbar-width: thin;
21 |
22 | &::-webkit-scrollbar {
23 | width: 0.5rem;
24 | }
25 |
26 | &::-webkit-scrollbar-track {
27 | background: transparent;
28 | }
29 |
30 | &::-webkit-scrollbar-thumb {
31 | background: rgba(0, 255, 173, 0.4);
32 | }
33 | }
34 |
35 | .tx-wrapper {
36 | min-height: 200px;
37 |
38 | h2 {
39 | line-height: 2rem;
40 | }
41 |
42 | .square-container {
43 | position: absolute;
44 | top: 0px;
45 | left: 0px;
46 | right: 0px;
47 | bottom: 0px;
48 | padding-right: 1rem;
49 | overflow-x: hidden;
50 | overflow-y: scroll;
51 | display: flex;
52 | flex-wrap: wrap;
53 | align-content: flex-start;
54 |
55 | outline: none;
56 | outline-color: transparent;
57 |
58 | scrollbar-color: rgba(0, 255, 173, 0.4) transparent;
59 | scrollbar-width: thin;
60 |
61 | &::-webkit-scrollbar {
62 | width: 0.5rem;
63 | }
64 |
65 | &::-webkit-scrollbar-track {
66 | background: transparent;
67 | }
68 |
69 | &::-webkit-scrollbar-thumb {
70 | background: rgba(0, 255, 173, 0.4);
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/client/src/styles/custom/_tx-square.scss:
--------------------------------------------------------------------------------
1 | .square {
2 | width: 0.8rem;
3 | height: 0.8rem;
4 | margin: 0.2rem;
5 | padding: 0;
6 | border-radius: 2px;
7 | font-size: 10px;
8 |
9 | &.legend {
10 | border-radius: 3px;
11 | width: 1.5rem;
12 | height: 1.5rem;
13 | margin: 0.2rem;
14 | }
15 |
16 | @include media-breakpoint-down(sm) {
17 | border-radius: 3px;
18 | width: 1.5rem;
19 | height: 1.5rem;
20 | margin: 0.2rem;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_alert.scss:
--------------------------------------------------------------------------------
1 | //
2 | // alerts
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | // Allow for a text-decoration since links are the same color as the alert text.
11 |
12 | .alert-link {
13 | text-decoration: $alert-link-text-decoration;
14 | }
15 |
16 | // Color variants
17 | //
18 | // Using Bootstrap's core alert-variant mixin to generate solid background color + yiq colorized text (and making close/links match those colors)
19 |
20 | @each $color, $value in $theme-colors {
21 | .alert-#{$color} {
22 | @include alert-variant(theme-color-level($color, $alert-bg-level), theme-color-level($color, $alert-border-level), color-yiq(theme-color-level($color, $alert-bg-level)));
23 | .close,
24 | .alert-link {
25 | color: color-yiq(theme-color-level($color, $alert-bg-level));
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_avatar.scss:
--------------------------------------------------------------------------------
1 | //
2 | // avatar.scss
3 | // Dashkit component
4 | //
5 |
6 | // General
7 |
8 | .avatar {
9 | position: relative;
10 | display: inline-block;
11 | width: $avatar-size-base;
12 | height: $avatar-size-base;
13 | font-size: $avatar-size-base / 3;
14 |
15 | // Loads mask images so they don't lag on hover
16 |
17 | &:after {
18 | content: '';
19 | position: absolute;
20 | width: 0;
21 | height: 0;
22 | background-image: url(#{$path-to-img}/masks/avatar-status.svg),
23 | url(#{$path-to-img}/masks/avatar-group.svg),
24 | url(#{$path-to-img}/masks/avatar-group-hover.svg),
25 | url(#{$path-to-img}/masks/avatar-group-hover-last.svg);
26 | }
27 | }
28 |
29 | .avatar-img {
30 | width: 100%;
31 | height: 100%;
32 | object-fit: cover;
33 | }
34 |
35 | .avatar-title {
36 | display: flex;
37 | align-items: center;
38 | justify-content: center;
39 | width: 100%;
40 | height: 100%;
41 | line-height: 0;
42 | background-color: $avatar-title-bg;
43 | color: $avatar-title-color;
44 | }
45 |
46 |
47 | // Status
48 |
49 | .avatar-online,
50 | .avatar-offline {
51 |
52 | &::before {
53 | content: '';
54 | position: absolute;
55 | bottom: 5%;
56 | right: 5%;
57 | width: 20%;
58 | height: 20%;
59 | border-radius: 50%;
60 | }
61 |
62 | .avatar-img {
63 | mask-image: url(#{$path-to-img}/masks/avatar-status.svg);
64 | mask-size: 100% 100%;
65 | }
66 | }
67 |
68 | .avatar-online::before {
69 | background-color: $success;
70 | }
71 |
72 | .avatar-offline::before {
73 | background-color: $gray-500;
74 | }
75 |
76 |
77 | // Sizing
78 |
79 | .avatar-xs {
80 | width: $avatar-size-xs;
81 | height: $avatar-size-xs;
82 | font-size: $avatar-size-xs / 3;
83 | }
84 |
85 | .avatar-sm {
86 | width: $avatar-size-sm;
87 | height: $avatar-size-sm;
88 | font-size: $avatar-size-sm / 3;
89 | }
90 |
91 | .avatar-lg {
92 | width: $avatar-size-lg;
93 | height: $avatar-size-lg;
94 | font-size: $avatar-size-lg / 3;
95 | }
96 |
97 | .avatar-xl {
98 | width: $avatar-size-xl;
99 | height: $avatar-size-xl;
100 | font-size: $avatar-size-xl / 3;
101 | }
102 |
103 | .avatar-xxl {
104 | width: $avatar-size-xl;
105 | height: $avatar-size-xl;
106 | font-size: $avatar-size-xl / 3;
107 |
108 | @include media-breakpoint-up(md) {
109 | width: $avatar-size-xxl;
110 | height: $avatar-size-xxl;
111 | font-size: $avatar-size-xxl / 3;
112 | }
113 | }
114 |
115 |
116 | // Ratio
117 |
118 | .avatar.avatar-4by3 {
119 | width: $avatar-size-base * 4 / 3;
120 | }
121 |
122 | .avatar-xs.avatar-4by3 {
123 | width: $avatar-size-xs * 4 / 3;
124 | }
125 |
126 | .avatar-sm.avatar-4by3 {
127 | width: $avatar-size-sm * 4 / 3;
128 | }
129 |
130 | .avatar-lg.avatar-4by3 {
131 | width: $avatar-size-lg * 4 / 3;
132 | }
133 |
134 | .avatar-xl.avatar-4by3 {
135 | width: $avatar-size-xl * 4 / 3;
136 | }
137 |
138 | .avatar-xxl.avatar-4by3 {
139 | width: $avatar-size-xxl * 4 / 3;
140 | }
141 |
142 |
143 | // Group
144 |
145 | .avatar-group {
146 | display: inline-flex;
147 |
148 | // Shift every next avatar left
149 |
150 | .avatar + .avatar {
151 | margin-left: -$avatar-size-base / 4;
152 | }
153 |
154 | .avatar-xs + .avatar-xs {
155 | margin-left: -$avatar-size-xs / 4;
156 | }
157 |
158 | .avatar-sm + .avatar-sm {
159 | margin-left: -$avatar-size-sm / 4;
160 | }
161 |
162 | .avatar-lg + .avatar-lg {
163 | margin-left: -$avatar-size-lg / 4;
164 | }
165 |
166 | .avatar-xl + .avatar-xl {
167 | margin-left: -$avatar-size-xl / 4;
168 | }
169 |
170 | .avatar-xxl + .avatar-xxl {
171 | margin-left: -$avatar-size-xxl / 4;
172 | }
173 |
174 | // Add some spacing between avatars
175 |
176 | .avatar:not(:last-child) {
177 | mask-image: url(#{$path-to-img}/masks/avatar-group.svg);
178 | mask-size: 100% 100%;
179 | }
180 |
181 | // Bring an avatar to front on hover
182 |
183 | .avatar:hover {
184 | mask-image: none;
185 | z-index: 1;
186 |
187 | + .avatar {
188 | mask-image: url(#{$path-to-img}/masks/avatar-group-hover.svg);
189 | mask-size: 100% 100%;
190 |
191 | &:last-child {
192 | mask-image: url(#{$path-to-img}/masks/avatar-group-hover-last.svg);
193 | }
194 | }
195 | }
196 |
197 | }
198 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_badge.scss:
--------------------------------------------------------------------------------
1 | //
2 | // badge.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | .badge {
11 | vertical-align: middle;
12 | }
13 |
14 | // Quick fix for badges in buttons
15 | .btn .badge {
16 | top: -2px;
17 | }
18 |
19 | //
20 | // Dashkit =====================================
21 | //
22 |
23 | // Creates the "soft" badge variant
24 | @each $color, $value in $theme-colors {
25 | .badge-soft-#{$color} {
26 | @include badge-variant-soft(theme-color-level($color, $badge-soft-bg-level), $value);
27 | }
28 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_breadcrumb.scss:
--------------------------------------------------------------------------------
1 | //
2 | // breadcrumb.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | .breadcrumb-item {
11 | // The separator between breadcrumbs
12 | + .breadcrumb-item::before {
13 | width: .3rem;
14 | height: .6rem;
15 | margin-right: $breadcrumb-item-padding;
16 | -webkit-mask: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIxMHB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxMCAxNiIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4gICAgICAgIDx0aXRsZT5TaGFwZTwvdGl0bGU+ICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiAgICA8ZGVmcz48L2RlZnM+ICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+ICAgICAgICA8ZyBpZD0iY2hldnJvbi1yaWdodCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMi4wMDAwMDAsIDIuMDAwMDAwKSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIuNSI+ICAgICAgICAgICAgPHBvbHlsaW5lIGlkPSJTaGFwZSIgcG9pbnRzPSIwIDEyIDYgNiAwIDAiPjwvcG9seWxpbmU+ICAgICAgICA8L2c+ICAgIDwvZz48L3N2Zz4=) no-repeat 50% 50%;
17 | mask: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIxMHB4IiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCAxMCAxNiIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4gICAgICAgIDx0aXRsZT5TaGFwZTwvdGl0bGU+ICAgIDxkZXNjPkNyZWF0ZWQgd2l0aCBTa2V0Y2guPC9kZXNjPiAgICA8ZGVmcz48L2RlZnM+ICAgIDxnIGlkPSJQYWdlLTEiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+ICAgICAgICA8ZyBpZD0iY2hldnJvbi1yaWdodCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMi4wMDAwMDAsIDIuMDAwMDAwKSIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIuNSI+ICAgICAgICAgICAgPHBvbHlsaW5lIGlkPSJTaGFwZSIgcG9pbnRzPSIwIDEyIDYgNiAwIDAiPjwvcG9seWxpbmU+ICAgICAgICA8L2c+ICAgIDwvZz48L3N2Zz4=) no-repeat 50% 50%;
18 | -webkit-mask-size: contain;
19 | mask-size: contain;
20 | background: $breadcrumb-divider-color;
21 | }
22 | }
23 |
24 |
25 | //
26 | // Dashkit =====================================
27 | //
28 |
29 | // Small
30 | //
31 | // Reduces font size
32 |
33 | .breadcrumb-sm {
34 | font-size: $breadcrumb-font-size-sm;
35 | }
36 |
37 |
38 | // Overflow
39 | //
40 | // Allows the breadcrumb to be overflown horizontally
41 |
42 | .breadcrumb-overflow {
43 | display: flex;
44 | flex-direction: row;
45 | flex-wrap: nowrap;
46 | overflow-x: auto;
47 |
48 | &::-webkit-scrollbar {
49 | display: none;
50 | }
51 | }
52 |
53 | .breadcrumb-overflow .breadcrumb-item {
54 | white-space: nowrap;
55 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_buttons.scss:
--------------------------------------------------------------------------------
1 | //
2 | // buttons.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 |
7 | //
8 | // Dashkit ===================================
9 | //
10 |
11 | // Button white
12 |
13 | .btn-white {
14 | border-color: $gray-300;
15 |
16 | @include hover-focus {
17 | background-color: $gray-100;
18 | border-color: $gray-400;
19 | }
20 | }
21 |
22 | .btn-group-toggle .btn-white:not(:disabled):not(.disabled):active,
23 | .btn-group-toggle .btn-white:not(:disabled):not(.disabled).active {
24 | background-color: $input-bg;
25 | border-color: $input-focus-border-color;
26 | color: $primary;
27 | }
28 |
29 | .btn-group-toggle .btn-white:focus,
30 | .btn-group-toggle .btn-white.focus {
31 | box-shadow: none;
32 | }
33 |
34 |
35 | // Button outline secondary
36 |
37 | .btn-outline-secondary {
38 | &:not(:hover):not([aria-expanded="true"]):not([aria-pressed="true"]){
39 | border-color: $gray-400;
40 | }
41 | }
42 |
43 |
44 | // Button rounded
45 | //
46 | // Creates circle button variations
47 |
48 | .btn-rounded-circle {
49 | width: calc(1em * #{$btn-line-height} + #{$input-btn-padding-y * 2 } + #{$btn-border-width} * 2);
50 | padding-left: 0;
51 | padding-right: 0;
52 | border-radius: 50%;
53 | }
54 | .btn-rounded-circle.btn-lg {
55 | width: calc(1em * #{$btn-line-height-lg} + #{$input-btn-padding-y-lg * 2 } + #{$btn-border-width} * 2);
56 | }
57 | .btn-rounded-circle.btn-sm {
58 | width: calc(1em * #{$btn-line-height-sm} + #{$input-btn-padding-y-sm * 2 } + #{$btn-border-width} * 2);
59 | }
60 |
61 |
62 | // Button group
63 | //
64 | // Prevent buttons from jittering on hover
65 |
66 | .btn-group .btn + .btn {
67 | margin-left: 0;
68 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_chart.scss:
--------------------------------------------------------------------------------
1 | //
2 | // chart.scss
3 | // Dashkit component
4 | //
5 |
6 | // Chart
7 | //
8 | // General styles
9 |
10 | .chart {
11 | position: relative;
12 | height: $chart-height;
13 | }
14 |
15 | .chart.chart-appended {
16 | height: calc(#{$chart-height} - #{$chart-legend-height});
17 | }
18 |
19 | .chart-sm {
20 | height: $chart-height-sm;
21 | }
22 |
23 | .chart-sm.chart-appended {
24 | height: calc(#{$chart-height-sm} - #{$chart-legend-height});
25 | }
26 |
27 |
28 | // Sparkline
29 |
30 | .chart-sparkline {
31 | width: $chart-sparkline-width;
32 | height: $chart-sparkline-height;
33 | }
34 |
35 |
36 | // Legend
37 | //
38 | // Custom legend
39 |
40 | .chart-legend {
41 | display: flex;
42 | justify-content: center;
43 | margin-top: $chart-legend-margin-top;
44 | font-size: $chart-legend-font-size;
45 | text-align: center;
46 | color: $chart-legend-color;
47 | }
48 |
49 | .chart-legend-item {
50 | display: inline-flex;
51 | align-items: center;
52 |
53 | + .chart-legend-item {
54 | margin-left: 1rem;
55 | }
56 | }
57 |
58 | .chart-legend-indicator {
59 | display: inline-block;
60 | width: .5rem;
61 | height: .5rem;
62 | margin-right: 0.375rem;
63 | border-radius: 50%;
64 | }
65 |
66 |
67 | // Tooltip
68 | //
69 | // Custom tooltip
70 |
71 | #chart-tooltip {
72 | z-index: 0;
73 | }
74 |
75 | #chart-tooltip .arrow {
76 | top: 100%;
77 | left: 50%;
78 | transform: translateX(-50%) translateX(-.5rem);
79 | }
80 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_checklist.scss:
--------------------------------------------------------------------------------
1 | //
2 | // checklist.scss
3 | // Dashkit component
4 | //
5 |
6 | .checklist {
7 | outline: none;
8 | }
9 |
10 | .checklist-control {
11 | display: flex;
12 | flex-wrap: nowrap;
13 | outline: none;
14 | user-select: none;
15 | }
16 |
17 | .checklist-control .custom-control-input:checked ~ .custom-control-caption {
18 | text-decoration: line-through;
19 | color: $checklist-control-checked-color;
20 | }
21 |
22 | .checklist-control + .checklist-control {
23 | margin-top: $checklist-control-spacer;
24 | }
25 |
26 | .checklist-control:first-child[style*="display: none"] + .checklist-control {
27 | margin-top: 0;
28 | }
29 |
30 | .checklist-control.draggable-mirror {
31 | z-index: $zindex-fixed;
32 | }
33 |
34 | .checklist-control.draggable-source--is-dragging {
35 | opacity: .2;
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_close.scss:
--------------------------------------------------------------------------------
1 | //
2 | // close.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | // Small
7 |
8 | .close-sm {
9 | font-size: $close-font-size-sm;
10 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_code.scss:
--------------------------------------------------------------------------------
1 | //
2 | // code.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Dashkit =================================
8 | //
9 |
10 | // Highlight
11 | //
12 | // Hightlight.js overrides
13 |
14 | .highlight {
15 | padding: 0;
16 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_comment.scss:
--------------------------------------------------------------------------------
1 | //
2 | // comment.scss
3 | // Dashkit component
4 | //
5 |
6 | // Comment
7 | //
8 | // General styles
9 |
10 | .comment {
11 | margin-bottom: $comment-margin-bottom;
12 | }
13 |
14 | .comment-body {
15 | display: inline-block;
16 | padding: $comment-body-padding-y $comment-body-padding-x;
17 | background-color: $comment-body-bg;
18 | border-radius: $comment-body-border-radius;
19 | }
20 |
21 | .comment-time {
22 | display: block;
23 | margin-bottom: $comment-time-margin-bottom;
24 | font-size: $comment-time-font-size;
25 | color: $comment-time-color;
26 | }
27 |
28 | .comment-text {
29 | font-size: $comment-body-font-size;
30 | }
31 |
32 | .comment-text:last-child {
33 | margin-bottom: 0;
34 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_custom-forms.scss:
--------------------------------------------------------------------------------
1 | //
2 | // custom-forms.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides ===================================
8 | //
9 |
10 | // Switch
11 |
12 | .custom-switch {
13 | min-height: $custom-switch-height;
14 |
15 | .custom-control-label {
16 |
17 | &::before {
18 | top: 0;
19 | height: $custom-switch-height;
20 | border-radius: $custom-switch-height / 2;
21 | }
22 |
23 | &::after {
24 | top: $custom-switch-spacing;
25 | left: $custom-switch-spacing - $custom-control-gutter - $custom-switch-width;
26 | background-color: $custom-switch-indicator-bg;
27 | }
28 | }
29 |
30 | .custom-control-input:checked ~ .custom-control-label {
31 |
32 | &::after {
33 | background-color: $custom-switch-indicator-active-bg;
34 | transform: translateX($custom-switch-width - $custom-switch-spacing * 2 - $custom-switch-indicator-size);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_dashkit.scss:
--------------------------------------------------------------------------------
1 | // Extended Bootstrap components
2 |
3 | @import 'mixins';
4 | @import 'alert';
5 | @import 'badge';
6 | @import 'breadcrumb';
7 | @import 'buttons';
8 | @import 'card';
9 | @import 'close';
10 | @import 'code';
11 | @import 'custom-forms';
12 | @import 'dropdowns';
13 | @import 'forms';
14 | @import 'jumbotron';
15 | @import 'list-group';
16 | @import 'modal';
17 | @import 'nav';
18 | @import 'navbar';
19 | @import 'popover';
20 | @import 'progress';
21 | @import 'reboot';
22 | @import 'root';
23 | @import 'tables';
24 | @import 'toasts';
25 | @import 'type';
26 | @import 'utilities';
27 |
28 | // Dashkit only components
29 |
30 | @import 'avatar';
31 | @import 'chart';
32 | @import 'comment';
33 | @import 'checklist';
34 | @import 'header';
35 | @import 'icon';
36 | @import 'kanban';
37 | @import 'main-content';
38 | @import 'vendors';
39 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_dropdowns.scss:
--------------------------------------------------------------------------------
1 | //
2 | // dropdowns.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap overrides ==================================
8 | //
9 |
10 | // Dropdown arrows
11 | //
12 | // Replace original arrows with Feather icons
13 |
14 | .dropdown-toggle {
15 |
16 | &::after {
17 | width: auto;
18 | height: auto;
19 | border: none !important;
20 | vertical-align: middle;
21 | font-family: 'Feather';
22 | }
23 |
24 | &::after {
25 | content: '\e92e';
26 | }
27 | }
28 |
29 | .dropup > .dropdown-toggle {
30 |
31 | &::after {
32 | content: '\e931';
33 | }
34 | }
35 |
36 | .dropright > .dropdown-toggle {
37 |
38 | &::after {
39 | content: '\e930';
40 | }
41 | }
42 |
43 | .dropleft > .dropdown-toggle {
44 |
45 | &::before {
46 | content: '\e92f';
47 | width: auto;
48 | height: auto;
49 | border: none !important;
50 | vertical-align: middle;
51 | font-family: 'Feather';
52 | }
53 | }
54 |
55 |
56 | // Dropdown toggle
57 | //
58 | // Right align arrows
59 |
60 | .dropdown-item.dropdown-toggle {
61 | display: flex;
62 | justify-content: space-between;
63 | }
64 |
65 |
66 | // Dropdown menu animation
67 | //
68 | // Animate dropdown menu appearance
69 |
70 | .dropdown-menu {
71 | animation: dropdownMenu .15s;
72 | }
73 |
74 | @keyframes dropdownMenu {
75 | from {
76 | opacity: 0;
77 | }
78 |
79 | to {
80 | opacity: 1;
81 | }
82 | }
83 |
84 |
85 |
86 | //
87 | // Dashkit ===================================
88 | //
89 |
90 |
91 | // Dropdown ellipses
92 | //
93 | // Styles the ellipses icon and removes the dropdown arrow
94 |
95 | .dropdown-ellipses {
96 | font-size: $font-size-lg;
97 | color: $gray-400;
98 | }
99 |
100 | .dropdown-ellipses::after {
101 | display: none;
102 | }
103 |
104 |
105 | // Dropdown card
106 | //
107 | // Makes the dropdown menu act like a card
108 |
109 | .dropdown-menu-card {
110 | min-width: $dropdown-card-min-width;
111 | padding-top: 0;
112 | padding-bottom: 0;
113 | background-color: $card-bg;
114 | border-color: $dropdown-card-border-color;
115 |
116 | .card-header {
117 | min-height: $dropdown-card-header-min-height;
118 | }
119 |
120 | .card-body {
121 | max-height: $dropdown-card-body-max-height;
122 | overflow-y: auto;
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_header.scss:
--------------------------------------------------------------------------------
1 | //
2 | // header.scss
3 | // Dashkit component
4 | //
5 |
6 | // Header
7 | //
8 | // General styles
9 |
10 | .header {
11 | margin-bottom: $header-margin-bottom;
12 | }
13 |
14 | .header-img-top {
15 | width: 100%; height: auto;
16 | }
17 |
18 | .header-body {
19 | padding-top: $header-spacing-y;
20 | padding-bottom: $header-spacing-y;
21 | border-bottom: $header-body-border-width solid $header-body-border-color;
22 | }
23 |
24 | .header.bg-dark .header-body,
25 | .header.bg-hero .header-body {
26 | border-bottom-color: $header-body-border-color-dark;
27 | }
28 |
29 | .header-footer {
30 | padding-top: $header-spacing-y;
31 | padding-bottom: $header-spacing-y;
32 | }
33 |
34 | .header-pretitle {
35 | text-transform: uppercase;
36 | letter-spacing: .08em;
37 | color: $text-muted;
38 | }
39 |
40 | .header-title {
41 | margin-bottom: 0;
42 | }
43 |
44 | .header-subtitle {
45 | margin-top: map-get($spacers, 2);
46 | margin-bottom: 0;
47 | color: $text-muted;
48 | }
49 |
50 | .header-tabs {
51 | margin-bottom: -$header-spacing-y;
52 | border-bottom-width: 0;
53 |
54 | .nav-link {
55 | padding-top: $header-spacing-y;
56 | padding-bottom: $header-spacing-y;
57 | }
58 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_icon.scss:
--------------------------------------------------------------------------------
1 | //
2 | // icon.scss
3 | // Dashkit component
4 | //
5 |
6 | // Icon
7 | //
8 | // General styles
9 |
10 | .icon {
11 | display: inline-block;
12 |
13 | // Feather icon
14 |
15 | > .fe {
16 | display: block;
17 | min-width: 1em * $line-height-base;
18 | min-height: 1em * $line-height-base;
19 | text-align: center;
20 | font-size: $font-size-lg;
21 | }
22 |
23 | // Active state
24 |
25 | &.active {
26 | position: relative;
27 |
28 | // Feather icon
29 |
30 | > .fe {
31 | mask-image: url(#{$path-to-img}/masks/icon-status.svg);
32 | mask-size: 100% 100%;
33 | }
34 |
35 | // Indicator
36 |
37 | &::after {
38 | content: "";
39 | position: absolute;
40 | top: 10%; right: 20%;
41 | width: 20%; height: 20%;
42 | border-radius: 50%;
43 | background-color: $primary;
44 | }
45 | }
46 | }
47 |
48 | // Feather icons
49 | //
50 | // Fixes icon / font vertical alignment issue
51 |
52 | .fe {
53 | line-height: inherit;
54 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_jumbotron.scss:
--------------------------------------------------------------------------------
1 | //
2 | // jumbotron.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | .jumbotron {
11 | padding: ($jumbotron-padding / 2);
12 | @include media-breakpoint-up(sm) {
13 | padding: $jumbotron-padding;
14 | }
15 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_kanban.scss:
--------------------------------------------------------------------------------
1 | //
2 | // kanban.scss
3 | // Dashkit component
4 | //
5 |
6 | // Container
7 |
8 | .container-fluid.kanban-container {
9 | min-height: calc(100vh - 129px);
10 | }
11 |
12 | .container.kanban-container {
13 | min-height: calc(100vh - 129px - 69px);
14 | }
15 |
16 | .kanban-container {
17 | overflow-x: scroll;
18 | -webkit-overflow-scrolling: touch;
19 | }
20 |
21 | .kanban-container > .row {
22 | flex-wrap: nowrap;
23 | }
24 |
25 | .kanban-container > .row > [class*="col"] {
26 | max-width: $kanban-col-width;
27 | }
28 |
29 |
30 | // Category
31 |
32 | .kanban-category {
33 | min-height: 1rem;
34 | }
35 |
36 |
37 | // Item
38 |
39 | .kanban-item {
40 | outline: none;
41 | user-select: none;
42 | }
43 |
44 | .kanban-item.draggable-source--is-dragging {
45 | opacity: .2;
46 | }
47 |
48 | .kanban-item.draggable-mirror {
49 | z-index: $zindex-fixed;
50 | }
51 |
52 | .card-body .kanban-item.draggable-mirror > .card {
53 | transform: rotateZ(-3deg);
54 | }
55 |
56 |
57 | // Card
58 |
59 | .kanban-item > .card[data-toggle="modal"] {
60 | cursor: pointer;
61 | }
62 |
63 |
64 | // Add form
65 |
66 | .kanban-add-form .form-control[data-toggle="flatpickr"] {
67 | width: 12ch; // there is no CSS way to set input's width to auto so hardcoding this value
68 | }
69 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_list-group.scss:
--------------------------------------------------------------------------------
1 | //
2 | // list-group.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | // Contextual variants
11 | //
12 | // Changing the Bootstrap color modifier classes to be full opacity background with yiq calculated font color
13 |
14 | @each $color, $value in $theme-colors {
15 | @include list-group-item-variant($color, $value, color-yiq($value));
16 | }
17 |
18 |
19 | // List group large
20 |
21 | .list-group-lg .list-group-item {
22 | padding-top: $list-group-item-padding-y-lg;
23 | padding-bottom: $list-group-item-padding-y-lg;
24 | }
25 |
26 |
27 | // List group flush
28 |
29 | .list-group-flush > .list-group-item {
30 | padding-left: 0;
31 | padding-right: 0;
32 | }
33 |
34 |
35 | //
36 | // Dashkit ===================================
37 | //
38 |
39 |
40 | // Activity
41 |
42 | .list-group-activity .list-group-item {
43 | border: 0;
44 | }
45 |
46 | .list-group-activity .list-group-item:not(:last-child)::before {
47 | content: '';
48 | position: absolute;
49 | top: $list-group-item-padding-y;
50 | left: $avatar-size-sm / 2;
51 | height: 100%;
52 | border-left: $border-width solid $border-color;
53 | }
54 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_main-content.scss:
--------------------------------------------------------------------------------
1 | //
2 | // main-content.scss
3 | // Dashkit component
4 | //
5 |
6 | // Main content
7 | //
8 | // General styles
9 |
10 | .navbar-vertical:not([style*="display: none"]) ~ .main-content,
11 | .navbar-vertical-sm:not([style*="display: none"]) ~ .main-content {
12 |
13 | .container,
14 | .container-fluid {
15 |
16 | @include media-breakpoint-up(md) {
17 | padding-left: ($main-content-padding-x + $grid-gutter-width / 2) !important;
18 | padding-right: ($main-content-padding-x + $grid-gutter-width / 2) !important;
19 | }
20 | }
21 | }
22 |
23 |
24 | // Main content offset
25 | //
26 | // Offsets the main content depending on the sidebar positioning
27 |
28 | .navbar-vertical.navbar-expand {
29 |
30 | @each $breakpoint, $value in $grid-breakpoints {
31 | &-#{$breakpoint} {
32 | @include media-breakpoint-up(#{$breakpoint}) {
33 |
34 | // Left
35 |
36 | &.fixed-left:not([style*="display: none"]) ~ .main-content {
37 | margin-left: $navbar-vertical-width;
38 | }
39 |
40 | // Right
41 |
42 | &.fixed-right:not([style*="display: none"]) ~ .main-content {
43 | margin-right: $navbar-vertical-width;
44 | }
45 | }
46 | }
47 | }
48 | }
49 |
50 | .navbar-vertical-sm.navbar-expand {
51 |
52 | @each $breakpoint, $value in $grid-breakpoints {
53 | &-#{$breakpoint} {
54 | @include media-breakpoint-up(#{$breakpoint}) {
55 |
56 | // Left
57 |
58 | &.fixed-left:not([style*="display: none"]) ~ .main-content {
59 | margin-left: $navbar-vertical-width-sm;
60 | }
61 |
62 | // Right
63 |
64 | &.fixed-right:not([style*="display: none"]) ~ .main-content {
65 | margin-right: $navbar-vertical-width-sm;
66 | }
67 | }
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_mixins.scss:
--------------------------------------------------------------------------------
1 | // Mixins
2 | //
3 | //
4 |
5 | // Utilities
6 | @import "mixins/breakpoints";
7 | @import "mixins/badge";
8 |
9 | // Components
10 | // ...
11 |
12 | // Skins
13 | // ...
14 |
15 | // Layout
16 | // ...
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_modal.scss:
--------------------------------------------------------------------------------
1 | //
2 | // modal.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =======================
8 | //
9 |
10 | .modal-dialog {
11 |
12 | // When fading in the modal, animate it to slide down
13 | .modal.fade & {
14 | transform: translate(0, -150px);
15 | }
16 |
17 | .modal.show & {
18 | transform: translate(0, 0);
19 | }
20 | }
21 |
22 | .modal-header .close {
23 | margin: -1.5rem -1.5rem -1.5rem auto;
24 | }
25 |
26 |
27 | //
28 | // Dashkit ===================================
29 | //
30 |
31 | // Modal dialog vertical
32 | //
33 | // Creates a vertically aligned version of the modal dialog
34 |
35 | .modal-dialog-vertical {
36 | height: 100%;
37 | max-width: $modal-dialog-vertical-width;
38 | margin: 0;
39 |
40 | .modal-content {
41 | height: inherit;
42 | border-width: 0 $modal-content-border-width 0 0;
43 | border-radius: 0;
44 | }
45 |
46 | .modal-header {
47 | border-radius: inherit;
48 | }
49 |
50 | .modal-body {
51 | height: inherit;
52 | overflow-y: auto;
53 | }
54 | }
55 |
56 | .modal.fade .modal-dialog-vertical {
57 | transform: translateX(-100%);
58 | }
59 |
60 | .modal.show .modal-dialog-vertical {
61 | transform: translateX(0);
62 | }
63 |
64 |
65 | // Positioning
66 |
67 | .modal.fixed-right {
68 | padding-right: 0 !important;
69 | }
70 |
71 | .modal.fixed-right .modal-dialog-vertical {
72 | margin-left: auto;
73 | }
74 |
75 | .modal.fixed-right.fade .modal-dialog-vertical {
76 | transform: translateX(100%);
77 | }
78 |
79 | .modal.fixed-right.show .modal-dialog-vertical {
80 | transform: translateX(0);
81 | }
82 |
83 |
84 | // Modal card
85 |
86 | .modal-card {
87 | margin-bottom: 0;
88 |
89 | .card-body {
90 | max-height: $modal-card-body-max-height;
91 | overflow-y: auto;
92 | }
93 | }
94 |
95 |
96 | // Modal tabs
97 |
98 | .modal-header-tabs {
99 | margin-top: -$modal-header-padding-y;
100 | margin-bottom: calc(-#{$modal-header-padding-y} - #{$border-width});
101 | }
102 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_nav.scss:
--------------------------------------------------------------------------------
1 | //
2 | // nav.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | // Changing nav tabs to be bottom highlight style
11 |
12 | .nav-tabs {
13 |
14 | .nav-item {
15 | margin-left: $nav-tabs-link-margin-x;
16 | margin-right: $nav-tabs-link-margin-x;
17 | }
18 |
19 | .nav-link {
20 | padding: $nav-tabs-link-padding-y 0;
21 | border-bottom: $nav-tabs-link-active-border-width solid transparent;
22 | border-left-width: 0;
23 | border-right-width: 0;
24 | border-top-width: 0;
25 |
26 | &:not(.active) {
27 | color: $gray-600;
28 |
29 | &:hover {
30 | color: $gray-700;
31 | }
32 | }
33 |
34 | }
35 |
36 | .nav-item:first-child {
37 | margin-left: 0;
38 | }
39 |
40 | .nav-item:last-child {
41 | margin-right: 0;
42 | }
43 |
44 | // Removes the primary color underline from dropdowns in .nav-tabs
45 | .nav-item.show .nav-link {
46 | border-color: transparent;
47 | }
48 |
49 | }
50 |
51 |
52 | //
53 | // Dashkit =====================================
54 | //
55 |
56 | // Nav overflow
57 | //
58 | // Allow links to overflow and make horizontally scrollable
59 |
60 | .nav-overflow {
61 | display: flex;
62 | flex-wrap: nowrap;
63 | overflow-x: auto;
64 | padding-bottom: 1px; // to prevent active links border bottom from hiding
65 |
66 | // Hide scrollbar
67 |
68 | &::-webkit-scrollbar {
69 | display: none;
70 | }
71 | }
72 |
73 |
74 | // Creates a small version of the .nav-tabs
75 |
76 | .nav-tabs-sm {
77 | font-size: $nav-tabs-sm-font-size;
78 |
79 | .nav-item {
80 | margin-left: $nav-tabs-sm-link-margin-x;
81 | margin-right: $nav-tabs-sm-link-margin-x;
82 | }
83 |
84 | .nav-link {
85 | // Calculates the exact padding necessary to vertically fill the .card-header
86 | padding-top: (($font-size-base / $nav-tabs-sm-font-size) * $nav-tabs-link-padding-y);
87 | padding-bottom: (($font-size-base / $nav-tabs-sm-font-size) * $nav-tabs-link-padding-y);
88 | }
89 | }
90 |
91 | // Creates a small version of the .nab
92 |
93 | .nav-sm {
94 |
95 | .nav-link {
96 | font-size: $font-size-sm;
97 | }
98 | }
99 |
100 |
101 | // Nav + button group
102 | //
103 | // Change the look of .btn-white when .active
104 |
105 | .nav.btn-group {
106 |
107 | .btn-white.active {
108 | background-color: $primary;
109 | border-color: $primary;
110 | color: $white;
111 | }
112 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_popover.scss:
--------------------------------------------------------------------------------
1 | //
2 | // popover.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | .popover {
11 | padding: $popover-padding-y $popover-padding-x;
12 |
13 | &:hover {
14 | visibility: visible !important;
15 | }
16 | }
17 |
18 | .popover-header {
19 | margin-bottom: $popover-header-margin-bottom;
20 | border-bottom: 0;
21 | }
22 |
23 | .popover-body-label {
24 | margin-left: .25rem;
25 | }
26 |
27 | .popover-body-value {
28 | margin-left: .25rem;
29 | }
30 |
31 | .popover-body-indicator {
32 | display: inline-block;
33 | width: .5rem;
34 | height: .5rem;
35 | border-radius: 50%;
36 | }
37 |
38 |
39 | // Large
40 |
41 | .popover-lg {
42 | max-width: $popover-lg-max-width;
43 | }
44 |
45 |
46 | // Dark
47 |
48 | .popover-dark {
49 | background-color: $popover-dark-bg;
50 | border-color: $popover-dark-border-color;
51 | }
52 |
53 | .popover-dark .popover-header {
54 | font-weight: $font-weight-normal;
55 | background-color: $popover-dark-header-bg;
56 | color: $popover-dark-header-color;
57 | }
58 |
59 | .popover-dark.bs-popover-top .arrow {
60 |
61 | &::before {
62 | border-top-color: $popover-dark-border-color;
63 | }
64 |
65 | &::after {
66 | border-top-color: $popover-dark-bg;
67 | }
68 | }
69 |
70 | .popover-dark.bs-popover-right .arrow {
71 |
72 | &::before {
73 | border-right-color: $popover-dark-border-color;
74 | }
75 |
76 | &::after {
77 | border-right-color: $popover-dark-bg;
78 | }
79 | }
80 |
81 | .popover-dark.bs-popover-bottom .arrow {
82 |
83 | &::before {
84 | border-bottom-color: $popover-dark-border-color;
85 | }
86 |
87 | &::after {
88 | border-bottom-color: $popover-dark-bg;
89 | }
90 | }
91 |
92 | .popover-dark.bs-popover-left .arrow {
93 |
94 | &::before {
95 | border-left-color: $popover-dark-border-color;
96 | }
97 |
98 | &::after {
99 | border-left-color: $popover-dark-bg;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_progress.scss:
--------------------------------------------------------------------------------
1 | //
2 | // progress.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | // Rounds the progress bar, even for "multiple bar" progress bars
11 | .progress-bar:first-child {
12 | border-top-left-radius: $progress-border-radius;
13 | border-bottom-left-radius: $progress-border-radius;
14 | }
15 | .progress-bar:last-child {
16 | border-top-right-radius: $progress-border-radius;
17 | border-bottom-right-radius: $progress-border-radius;
18 | }
19 |
20 |
21 | //
22 | // Dashkit ===================================
23 | //
24 |
25 | .progress-sm {
26 | height: $progress-height-sm;
27 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_reboot.scss:
--------------------------------------------------------------------------------
1 | //
2 | // reboot.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Remove the cancel buttons in Chrome and Safari on macOS.
8 | //
9 |
10 | [type="search"]::-webkit-search-cancel-button {
11 | -webkit-appearance: none;
12 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_root.scss:
--------------------------------------------------------------------------------
1 | //
2 | // root.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | html {
7 | height: 100%;
8 | }
9 |
10 | body {
11 | min-height: 100%;
12 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_tables.scss:
--------------------------------------------------------------------------------
1 | //
2 | // tables.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | .table {
11 |
12 | thead th {
13 | background-color: $table-head-bg;
14 | text-transform: uppercase;
15 | font-size: $font-size-sm;
16 | font-weight: $font-weight-bold;
17 | letter-spacing: .08em;
18 | color: $table-head-color;
19 | border-bottom-width: $table-border-width;
20 | }
21 |
22 | thead th, tbody th, tbody td {
23 | vertical-align: middle;
24 | }
25 | }
26 |
27 | .table-sm {
28 | font-size: $font-size-sm;
29 |
30 | thead th {
31 | font-size: $font-size-xs;
32 | }
33 | }
34 |
35 |
36 | //
37 | // Dashkit =====================================
38 | //
39 |
40 | // No wrap
41 | //
42 | // Prevents table content from wrapping to the next line
43 |
44 | .table-nowrap {
45 |
46 | th, td {
47 | white-space: nowrap;
48 | }
49 | }
50 |
51 |
52 | // Sort
53 | //
54 | // Adds sorting icons
55 |
56 | .table [data-sort] {
57 | white-space: nowrap;
58 |
59 | &::after {
60 | content: str-replace(url("data:image/svg+xml;utf8, "), "#", "%23");
61 | margin-left: .25rem;
62 | }
63 | }
64 |
65 |
66 | // Table checkbox
67 |
68 | .table-checkbox {
69 | min-height: 0;
70 | }
71 |
72 | .table-checkbox .custom-control-label::before,
73 | .table-checkbox .custom-control-label::after {
74 | top: 50%;
75 | transform: translateY(-50%);
76 | }
77 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_toasts.scss:
--------------------------------------------------------------------------------
1 | //
2 | // toasts.scss
3 | // Extended from Bootstrap
4 | //
5 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_type.scss:
--------------------------------------------------------------------------------
1 | //
2 | // type.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | //
7 | // Bootstrap Overrides =====================================
8 | //
9 |
10 | h1, .h1 {
11 | margin-bottom: $headings-margin-bottom;
12 | font-size: 1.5rem;
13 |
14 | @include media-breakpoint-up(md) {
15 | font-size: $h1-font-size;
16 | }
17 | }
18 |
19 | h2, .h2 {
20 | margin-bottom: $headings-margin-bottom;
21 | }
22 |
23 | h3, .h3 {
24 | margin-bottom: ($headings-margin-bottom * .75);
25 | }
26 |
27 | h4, .h4 {
28 | margin-bottom: ($headings-margin-bottom * .5);
29 | }
30 |
31 | h5, .h5 {
32 | margin-bottom: ($headings-margin-bottom * .5);
33 | }
34 |
35 | h6, .h6 {
36 | margin-bottom: ($headings-margin-bottom * .5);
37 | }
38 |
39 |
40 | // Links
41 |
42 | h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 {
43 |
44 | > a {
45 | color: inherit;
46 | }
47 | }
48 |
49 | // Type display classes
50 |
51 | .display-1,
52 | .display-2,
53 | .display-3,
54 | .display-4 {
55 | letter-spacing: $display-letter-spacing;
56 | }
57 |
58 | // Headings
59 |
60 | h1, .h1, h2, .h2, h3, .h3, h4, .h4, h5, .h5, h6, .h6 {
61 | letter-spacing: $headings-letter-spacing;
62 | }
63 |
64 | h6.text-uppercase {
65 | letter-spacing: .08em;
66 | }
67 |
68 | // Bold, strong
69 |
70 | b, strong {
71 | font-weight: $font-weight-bold;
72 | }
73 |
74 |
75 | // Links, buttons
76 | //
77 | // Removes focus outline
78 |
79 | a, button {
80 |
81 | &:focus {
82 | outline: none !important;
83 | }
84 | }
85 |
86 |
87 | //
88 | // Dashkit =====================================
89 | //
90 |
91 | // Include Cerebri Sans
92 |
93 | @font-face {
94 | font-family: 'Cerebri Sans';
95 | src: url('#{$path-to-fonts}/cerebrisans/cerebrisans-regular.eot');
96 | src: url('#{$path-to-fonts}/cerebrisans/cerebrisans-regular.eot?#iefix') format('embedded-opentype'), url('#{$path-to-fonts}/cerebrisans/cerebrisans-regular.woff') format('woff'), url('#{$path-to-fonts}/cerebrisans/cerebrisans-regular.ttf') format('truetype');
97 | font-weight: 400;
98 | font-style: normal;
99 | }
100 |
101 | @font-face {
102 | font-family: 'Cerebri Sans';
103 | src: url('#{$path-to-fonts}/cerebrisans/cerebrisans-medium.eot');
104 | src: url('#{$path-to-fonts}/cerebrisans/cerebrisans-medium.eot?#iefix') format('embedded-opentype'), url('#{$path-to-fonts}/cerebrisans/cerebrisans-medium.woff') format('woff'), url('#{$path-to-fonts}/cerebrisans/cerebrisans-medium.ttf') format('truetype');
105 | font-weight: 500;
106 | font-style: normal;
107 | }
108 |
109 | @font-face {
110 | font-family: 'Cerebri Sans';
111 | src: url('#{$path-to-fonts}/cerebrisans/cerebrisans-semibold.eot');
112 | src: url('#{$path-to-fonts}/cerebrisans/cerebrisans-semibold.eot?#iefix') format('embedded-opentype'), url('#{$path-to-fonts}/cerebrisans/cerebrisans-semibold.woff') format('woff'), url('#{$path-to-fonts}/cerebrisans/cerebrisans-semibold.ttf') format('truetype');
113 | font-weight: 600;
114 | font-style: normal;
115 | }
116 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_utilities.scss:
--------------------------------------------------------------------------------
1 | @import 'utilities/background';
2 | @import 'utilities/borders';
3 | @import 'utilities/lift';
4 | @import 'utilities/sizing';
5 | @import 'utilities/type';
--------------------------------------------------------------------------------
/client/src/styles/dashkit/_vendors.scss:
--------------------------------------------------------------------------------
1 | @import 'vendors/dropzone';
2 | @import 'vendors/flatpickr';
3 | @import 'vendors/quill';
4 | @import 'vendors/list';
5 | @import 'vendors/select2';
6 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/dark/_overrides-dark.scss:
--------------------------------------------------------------------------------
1 | //
2 | // overrides.scss
3 | // Dark mode overrides
4 | //
5 |
6 | //
7 | // Table of contents
8 | //
9 | // 1. Buttons
10 | // 2. Forms
11 | // 3. Input groups
12 | // 4. Quill
13 | // 5. Select2
14 | //
15 |
16 |
17 | // Buttons
18 |
19 | .btn-white, .btn-light {
20 | @include button-variant($gray-800-dark, $gray-600-dark);
21 |
22 | &:not(:disabled):not(.disabled):hover,
23 | &:not(:disabled):not(.disabled):focus,
24 | &:not(:disabled):not(.disabled):active,
25 | &:not(:disabled):not(.disabled).active,
26 | &:not(:disabled):not(.disabled):active:focus,
27 | &:not(:disabled):not(.disabled).active:focus,
28 | .show > &.dropdown-toggle {
29 | background-color: $black-dark;
30 | border-color: $gray-700-dark;
31 | color: $white;
32 | }
33 | }
34 |
35 |
36 | // Forms
37 |
38 | .form-control {
39 | border-color: $input-bg;
40 | }
41 |
42 |
43 | // Input groups
44 |
45 | .input-group .input-group-text {
46 | border-color: $input-bg;
47 | }
48 |
49 |
50 | // Quill
51 |
52 | .ql-toolbar {
53 | border-color: $input-bg;
54 | }
55 |
56 | .ql-editor {
57 | border-left-color: $input-bg;
58 | border-right-color: $input-bg;
59 | border-bottom-color: $input-bg;
60 | }
61 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/mixins/_badge.scss:
--------------------------------------------------------------------------------
1 | // Badge Mixins
2 | //
3 | // This is a custom mixin for badge-#{color}-soft variant of Bootstrap's .badge class
4 |
5 | @mixin badge-variant-soft($bg, $color) {
6 | color: $color;
7 | background-color: $bg;
8 |
9 | &[href] {
10 | @include hover-focus {
11 | color: $color;
12 | text-decoration: none;
13 | background-color: darken($bg, 5%);
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/mixins/_breakpoints.scss:
--------------------------------------------------------------------------------
1 | //
2 | // breakpoint.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | @function breakpoint-prev($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) {
7 | $n: index($breakpoint-names, $name);
8 | @return if($n != null and $n != 1, nth($breakpoint-names, $n - 1), null);
9 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/utilities/_background.scss:
--------------------------------------------------------------------------------
1 | //
2 | // background.scss
3 | //
4 |
5 |
6 | // Fixed at the bottom
7 |
8 | .bg-fixed-bottom {
9 | background-repeat: no-repeat;
10 | background-position: right bottom;
11 | background-size: 100% auto;
12 | background-attachment: fixed;
13 | }
14 |
15 | // Calculate the width of the main container because
16 | // the background-attachment property will use 100vw instead
17 |
18 | .navbar-vertical ~ .main-content.bg-fixed-bottom {
19 | background-size: 100%;
20 |
21 | @include media-breakpoint-up(md) {
22 | background-size: calc(100% - #{$navbar-vertical-width});
23 | }
24 | }
25 |
26 |
27 | // Cover
28 |
29 | .bg-cover {
30 | background-repeat: no-repeat;
31 | background-position: center center;
32 | background-size: cover;
33 | }
34 |
35 |
36 | // Auth
37 |
38 | .bg-auth {
39 | background-color: $auth-bg;
40 | }
41 |
42 |
43 | // Ellipses
44 |
45 | @each $color, $value in $theme-colors {
46 |
47 | .bg-ellipses.bg-#{$color} {
48 | background-color: transparent !important;
49 | background-repeat: no-repeat;
50 | background-image: radial-gradient(#{$value}, #{$value} 70%, transparent 70.1%);
51 | background-size: 200% 150%;
52 | background-position: center bottom;
53 | }
54 | }
55 |
56 |
57 | // Hero
58 |
59 | .bg-hero {
60 | background-image: linear-gradient(to bottom, fade-out($black, .15), fade-out($black, .15)), url(../#{$path-to-img}/covers/header-cover.jpg);
61 | background-repeat: no-repeat, no-repeat;
62 | background-position: center center, center center;
63 | background-size: cover, cover;
64 | }
65 |
66 |
67 | // Colors
68 |
69 | .bg-lighter {
70 | background-color: $lighter !important;
71 | }
72 |
73 |
74 | // Soft colors
75 |
76 | @each $color, $value in $theme-colors {
77 | .bg-#{$color}-soft {
78 | background-color: theme-color-level($color, $bg-soft-level) !important;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/utilities/_borders.scss:
--------------------------------------------------------------------------------
1 | //
2 | // borders.scss
3 | //
4 |
5 |
6 | // Sizing
7 |
8 | $border-sizing: () !default;
9 | $border-sizing: map-merge((
10 | "2": 2,
11 | "3": 3,
12 | "4": 4,
13 | "5": 5
14 | ), $border-sizing);
15 |
16 | @each $size, $value in $border-sizing {
17 |
18 | .border-#{$size} {
19 | border-width: $border-width * $value !important;
20 | }
21 |
22 | .border-top-#{$size} {
23 | border-top-width: $border-width * $value !important;
24 | }
25 |
26 | .border-right-#{$size} {
27 | border-right-width: $border-width * $value !important;
28 | }
29 |
30 | .border-bottom-#{$size} {
31 | border-bottom-width: $border-width * $value !important;
32 | }
33 |
34 | .border-left-#{$size} {
35 | border-left-width: $border-width * $value !important;
36 | }
37 | }
38 |
39 |
40 | // Contextual classes
41 |
42 | .border-body {
43 | border-color: $body-bg !important;
44 | }
45 |
46 | .border-card {
47 | border-color: $card-bg !important;
48 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/utilities/_lift.scss:
--------------------------------------------------------------------------------
1 | //
2 | // lift.scss
3 | // Theme utility
4 | //
5 |
6 | .lift {
7 | transition: box-shadow .25s ease, transform .25s ease;
8 | }
9 |
10 | .lift:hover,
11 | .lift:focus {
12 | box-shadow: $box-shadow-lift !important;
13 | transform: translate3d(0, -3px, 0);
14 | }
15 |
16 | .lift-lg:hover,
17 | .lift-lg:focus {
18 | box-shadow: $box-shadow-lift-lg !important;
19 | transform: translate3d(0, -5px, 0);
20 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/utilities/_sizing.scss:
--------------------------------------------------------------------------------
1 | //
2 | // sizing.scss
3 | //
4 |
5 | .vw-100 {
6 | width: 100vw !important;
7 | }
8 | .vh-100 {
9 | height: 100vh !important;
10 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/utilities/_type.scss:
--------------------------------------------------------------------------------
1 | //
2 | // type.scss
3 | // Extended from Bootstrap
4 | //
5 |
6 | // Font size
7 |
8 | .font-size-base {
9 | font-size: $font-size-base !important;
10 | }
11 |
12 | .font-size-sm {
13 | font-size: $font-size-sm !important;
14 | }
15 |
16 | .font-size-lg {
17 | font-size: $font-size-lg !important;
18 | }
19 |
20 |
21 | // Decoration
22 |
23 | .text-decoration-underline {
24 | text-decoration: underline !important;
25 | }
26 |
27 |
28 | // Gray colors
29 |
30 | @each $color, $value in $grays {
31 | .text-gray-#{$color} {
32 | color: $value !important;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/vendors/_dropzone.scss:
--------------------------------------------------------------------------------
1 | //
2 | // dropzone.scss
3 | // Dropzone plugin overrides
4 | //
5 |
6 | .dropzone {
7 | position: relative;
8 | display: flex;
9 | flex-direction: column;
10 | }
11 |
12 | .dz-message {
13 | padding: 5rem 1rem;
14 | background-color: $input-bg;
15 | border: $input-border-width dashed $input-border-color;
16 | border-radius: $border-radius;
17 | text-align: center;
18 | color: $text-muted;
19 | transition: $transition-base;
20 | order: -1;
21 | cursor: pointer;
22 | z-index: 999;
23 |
24 | &:hover {
25 | border-color: $text-muted;
26 | color: $body-color;
27 | }
28 | }
29 |
30 | .dz-drag-hover .dz-message {
31 | border-color: $primary;
32 | color: $primary;
33 | }
34 |
35 | .dropzone-multiple .dz-message {
36 | padding-top: 2rem;
37 | padding-bottom: 2rem;
38 | }
39 |
40 | .dropzone-single.dz-max-files-reached .dz-message {
41 | background-color: fade-out($black, .1);
42 | color: white;
43 | opacity: 0;
44 |
45 | &:hover {
46 | opacity: 1;
47 | }
48 | }
49 |
50 | .dz-preview-single {
51 | position: absolute;
52 | top: 0; right: 0; bottom: 0; left: 0;
53 | border-radius: $border-radius;
54 | }
55 |
56 | .dz-preview-cover {
57 | position: absolute;
58 | top: 0; right: 0; bottom: 0; left: 0;
59 | border-radius: $border-radius;
60 | }
61 |
62 | .dz-preview-img {
63 | object-fit: cover;
64 | width: 100%; height: 100%;
65 | border-radius: $border-radius;
66 | }
67 |
68 | .dz-preview-multiple .list-group-item:last-child {
69 | padding-bottom: 0;
70 | border-bottom: 0;
71 | }
72 |
73 | [data-dz-size] strong {
74 | font-weight: $font-weight-normal;
75 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/vendors/_flatpickr.scss:
--------------------------------------------------------------------------------
1 | //
2 | // flatpickr.scss
3 | // Flatpickr plugin overrides
4 | //
5 |
6 | .flatpickr-calendar {
7 | background-color: $input-bg;
8 | border: $input-border-width solid $input-border-color;
9 | color: $input-color;
10 | box-shadow: none;
11 |
12 | * {
13 | color: inherit !important;
14 | fill: currentColor !important;
15 | }
16 |
17 | &.arrowTop:before {
18 | border-bottom-color: $input-border-color;
19 | }
20 |
21 | &.arrowTop:after {
22 | border-bottom-color: $input-bg;
23 | }
24 |
25 | .flatpickr-months {
26 | padding-top: .625rem;
27 | padding-bottom: .625rem;
28 | }
29 |
30 | .flatpickr-prev-month,
31 | .flatpickr-next-month {
32 | top: .625rem;
33 | }
34 |
35 | .flatpickr-current-month {
36 | font-size: 115%;
37 | }
38 |
39 | .flatpickr-day {
40 | border-radius: $border-radius;
41 |
42 | &:hover {
43 | background-color: $light;
44 | border-color: $input-border-color;
45 | }
46 | }
47 |
48 | .flatpickr-day.prevMonthDay {
49 | color: $text-muted !important;
50 | }
51 |
52 | .flatpickr-day.today {
53 | border-color: $border-color;
54 | }
55 |
56 | .flatpickr-day.selected {
57 | background-color: $primary;
58 | border-color: $primary;
59 | color: $white !important;
60 | }
61 |
62 | .flatpickr-day.inRange {
63 | background-color: $light;
64 | border: none;
65 | border-radius: 0;
66 | box-shadow: -5px 0 0 $light, 5px 0 0 $light;
67 | }
68 |
69 | }
--------------------------------------------------------------------------------
/client/src/styles/dashkit/vendors/_list.scss:
--------------------------------------------------------------------------------
1 | //
2 | // list.scss
3 | //
4 |
5 | [data-toggle="lists"] .pagination > li {
6 | @extend .page-item;
7 | }
8 |
9 | [data-toggle="lists"] .pagination .page {
10 | @extend .page-link;
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/styles/dashkit/vendors/_select2.scss:
--------------------------------------------------------------------------------
1 | //
2 | // select2.scss
3 | // Select2 plugin overrides
4 | //
5 |
6 | [class*="select2"] {
7 | display: block;
8 | }
9 |
10 | .select2 {
11 | width: 100% !important;
12 | }
13 |
14 | .select2-hidden-accessible {
15 | display: none;
16 | }
17 |
18 | .select2-selection[aria-expanded="true"] {
19 | border-bottom-left-radius: 0;
20 | border-bottom-right-radius: 0;
21 | }
22 |
23 | .select2-container {
24 | display: block;
25 | }
26 |
27 | .select2-dropdown {
28 | margin-top: -$input-border-width;
29 | border-top-left-radius: 0;
30 | border-top-right-radius: 0;
31 | }
32 |
33 | .select2-search--dropdown {
34 | padding: $dropdown-item-padding-y $input-padding-x;
35 | }
36 |
37 | .select2-search--dropdown .select2-search__field {
38 | width: 100%;
39 | height: $input-height-sm;
40 | padding: $input-padding-y-sm $input-padding-x-sm;
41 | background-color: $input-bg;
42 | border: $input-border-width solid $input-border-color;
43 | border-radius: $input-border-radius-sm;
44 | line-height: $input-line-height-sm;
45 | font-size: $input-font-size-sm;
46 | color: $input-color;
47 | transition: $input-transition;
48 |
49 | &:focus {
50 | border-color: $input-focus-border-color;
51 | box-shadow: $input-focus-box-shadow;
52 | outline: none;
53 | }
54 | }
55 |
56 | .select2-results__options {
57 | padding-left: 0;
58 | margin-bottom: 0;
59 | }
60 |
61 | .select2-results__option {
62 | padding: $dropdown-item-padding-y $input-padding-x;
63 | color: $dropdown-link-color;
64 |
65 | &:not(.select2-results__message) {
66 | cursor: pointer;
67 |
68 | @include hover-focus {
69 | color: $dropdown-link-hover-color;
70 | }
71 | }
72 | }
73 |
74 | .select2-results__option[aria-selected="true"],
75 | .select2-results__option--highlighted {
76 | color: $dropdown-link-active-color;
77 | }
78 |
79 | .select2-selection--multiple {
80 | height: auto;
81 | }
82 |
83 | .select2-selection__rendered {
84 | display: flex;
85 | flex-wrap: wrap;
86 | padding-left: 0;
87 | margin: 0 -.25rem -.25rem 0;
88 | }
89 |
90 | .select2-selection__choice {
91 | display: inline-flex;
92 | align-items: center;
93 | padding-left: .375rem;
94 | padding-right: .375rem;
95 | margin: 0 .25rem .25rem 0;
96 | font-size: $font-size-sm;
97 | background-color: $light;
98 | border-radius: $border-radius-xs;
99 | }
100 |
101 | .select2-selection__choice__remove {
102 | order: 2;
103 | margin-left: .5rem;
104 | color: $text-muted;
105 | cursor: pointer;
106 |
107 | @include hover {
108 | color: $body-color;
109 | }
110 | }
111 |
112 | .select2-search--inline .select2-search__field {
113 | height: calc(1em * #{$input-line-height});
114 | padding-bottom: .25rem;
115 | background-color: transparent;
116 | border: 0;
117 | box-shadow: none;
118 | outline: none;
119 | color: $input-color;
120 |
121 | &::placeholder {
122 | color: $input-placeholder-color;
123 | }
124 | }
125 |
126 | .select2-selection__placeholder {
127 | color: $input-placeholder-color;
128 | }
129 |
--------------------------------------------------------------------------------
/client/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | // Icon font
2 | @import "../fonts/feather/feather";
3 |
4 | // Bootstrap functions
5 | @import "~bootstrap/scss/functions.scss";
6 |
7 | // Custom variables
8 | @import "custom-variables";
9 |
10 | // Dark mode variables
11 | @import "dashkit/dark/variables-dark";
12 |
13 | // Dashkit variables
14 | @import "dashkit/variables";
15 |
16 | // Bootstrap core
17 | @import "~bootstrap/scss/bootstrap.scss";
18 |
19 | // Dashkit core
20 | @import "dashkit/dashkit";
21 |
22 | // Dark mode overrides
23 | @import "dashkit/dark/overrides-dark";
24 |
25 | // Custom core
26 | @import "custom";
27 |
--------------------------------------------------------------------------------
/client/src/styles/tools/shadows.scss:
--------------------------------------------------------------------------------
1 | //shadows
2 |
3 | //example
4 | // @include box-shadow(3);
5 |
6 | @mixin box-shadow($level) {
7 | @if $level == 1 {
8 | box-shadow: 1px 1px 8px rgba(0, 7, 43, 0.04);
9 | } @else if $level == 2 {
10 | box-shadow: 4px 8px 24px rgba(0, 7, 43, 0.04);
11 | } @else if $level == 3 {
12 | box-shadow: 8px 24px 48px rgba(0, 7, 43, 0.08);
13 | } @else if $level == 4 {
14 | box-shadow: 40px 40px 96px rgba(0, 7, 43, 0.08);
15 | }
16 | }
17 |
18 | //Modal Overlay
19 | @mixin modal-overlay {
20 | background: rgba(0, 7, 43, 0.4);
21 | }
22 |
23 | //Active Inputs Shadow
24 | @mixin active-input-shadow {
25 | box-shadow: 1px 1px 4px rgba(0, 85, 255, 0.08);
26 | }
27 |
28 | //Avatar Inner Shadow
29 | @mixin avatar-inner-shadow {
30 | box-shadow: inset 0px 4px 4px rgba(0, 7, 43, 0.25);
31 | }
32 |
--------------------------------------------------------------------------------
/client/src/styles/tools/transitions.scss:
--------------------------------------------------------------------------------
1 | @mixin transition() {
2 | transition: 0.2s ease-in-out;
3 | }
4 |
5 | @mixin transition-hover() {
6 | transition: 0.5s ease-in-out;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/utils/bytes.ts:
--------------------------------------------------------------------------------
1 | // Program data is stored as little-endian
2 | // [0x1, 0x0] -> id #0, [0x0, 0x1] -> id #8
3 | export function programDataToIds(bytes: Uint8Array): Array {
4 | const ids = new Array();
5 | bytes.forEach((byte, i) => {
6 | for (let j = 7; j >= 0; j--) {
7 | if ((byte & (1 << j)) === 1 << j) {
8 | ids.push(8 * i + (7 - j));
9 | }
10 | }
11 | });
12 | return ids;
13 | }
14 |
15 | // Instruction data uses big-endian
16 | // id #0 -> [0x0, 0x0], id #256 -> [0x1, 0x0]
17 | const MAX_ID = Math.pow(2, 16) - 1;
18 | export function instructionDataFromId(id: number): Uint8Array {
19 | if (id > MAX_ID || id < 0 || !Number.isInteger(id)) {
20 | throw new Error("invalid id");
21 | }
22 |
23 | const bytes = new Uint8Array(2);
24 | bytes[0] = Math.floor(id / 256);
25 | bytes[1] = id % 256;
26 | return bytes;
27 | }
28 |
29 | export function xor(a: Uint8Array, b: Uint8Array): Uint8Array {
30 | if (a.length !== b.length) throw new Error("bytes are not the same length");
31 | const bytes = new Uint8Array(a);
32 | for (let i = 0; i < b.length; i++) {
33 | bytes[i] ^= b[i];
34 | }
35 | return bytes;
36 | }
37 |
--------------------------------------------------------------------------------
/client/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { Keypair } from "@solana/web3.js";
2 | // import * as Sentry from "@sentry/react";
3 |
4 | export function sleep(ms: number): Promise {
5 | return new Promise((resolve) => setTimeout(resolve, ms));
6 | }
7 |
8 | export function reportError(err: unknown, context: string) {
9 | if (err instanceof Error) {
10 | console.error(context, err);
11 | }
12 | // if (process.env.NODE_ENV === "production") {
13 | // const query = new URLSearchParams(window.location.search);
14 | // if (query.get("cluster") === "custom") return;
15 | // Sentry.captureException(err, {
16 | // tags: { context },
17 | // });
18 | // }
19 | }
20 |
21 | export function isLocalHost() {
22 | return window.location.hostname === "localhost";
23 | }
24 |
25 | export const getLocalStorageKeypair = (key: string): Keypair => {
26 | const base64Keypair = window.localStorage.getItem(key);
27 | if (base64Keypair) {
28 | return Keypair.fromSecretKey(Buffer.from(base64Keypair, "base64"));
29 | } else {
30 | const keypair = new Keypair();
31 | window.localStorage.setItem(
32 | key,
33 | Buffer.from(keypair.secretKey).toString("base64")
34 | );
35 | return keypair;
36 | }
37 | };
38 |
39 | export const getFeePayers = (num: number) => {
40 | const accounts = [];
41 | for (let i = 0; i < num; i++) {
42 | accounts.push(getLocalStorageKeypair(`feePayerKey${i + 1}`));
43 | }
44 | return accounts;
45 | };
46 |
--------------------------------------------------------------------------------
/client/src/worker-loader.d.ts:
--------------------------------------------------------------------------------
1 | declare module "worker-loader!*" {
2 | class WebpackWorker extends Worker {
3 | constructor();
4 | }
5 |
6 | export default WebpackWorker;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/workers/create-transaction-rpc.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-webpack-loader-syntax
2 | import CreateTransactionWorker from "worker-loader!./create-transaction-worker-script";
3 |
4 | import { Blockhash } from "@solana/web3.js";
5 |
6 | export interface CreateTransactionMessage {
7 | trackingId: number;
8 | blockhash: Blockhash;
9 | programId: string;
10 | bitId: number;
11 | feeAccountSecretKey: Uint8Array;
12 | programDataAccount: string;
13 | computeUnitPrice?: number;
14 | extraWriteAccount?: string;
15 | }
16 |
17 | export interface CreateTransactionResponseMessage {
18 | trackingId: number;
19 | signature: Buffer;
20 | serializedTransaction: Buffer;
21 | }
22 |
23 | export interface CreateTransactionErrorMessage {
24 | trackingId: string;
25 | error: Error;
26 | }
27 |
28 | export class CreateTransactionRPC {
29 | private worker: CreateTransactionWorker;
30 |
31 | private callbacks: { [trackingId: string]: Function[] } = {};
32 |
33 | constructor() {
34 | this.worker = new CreateTransactionWorker();
35 | this.worker.onmessage = this.handleMessages.bind(this);
36 | }
37 |
38 | handleMessages(event: MessageEvent) {
39 | let message = event.data;
40 |
41 | if (message.trackingId in this.callbacks) {
42 | let callbacks = this.callbacks[message.trackingId];
43 | delete this.callbacks[message.trackingId];
44 |
45 | if ("error" in message) {
46 | callbacks[1](message.error);
47 | return;
48 | }
49 |
50 | callbacks[0](message);
51 | }
52 | }
53 |
54 | createTransaction(
55 | message: CreateTransactionMessage
56 | ): Promise {
57 | return new Promise((resolve, reject) => {
58 | this.callbacks[message.trackingId] = [resolve, reject];
59 | this.worker.postMessage(message);
60 | });
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/client/src/workers/create-transaction-worker-script.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Transaction,
3 | PublicKey,
4 | Keypair,
5 | ComputeBudgetProgram,
6 | } from "@solana/web3.js";
7 | import * as Bytes from "utils/bytes";
8 | import { CreateTransactionMessage } from "./create-transaction-rpc";
9 |
10 | const self: any = globalThis;
11 |
12 | function createTransaction(message: CreateTransactionMessage) {
13 | const {
14 | trackingId,
15 | blockhash,
16 | programId,
17 | bitId,
18 | feeAccountSecretKey,
19 | programDataAccount,
20 | computeUnitPrice,
21 | extraWriteAccount,
22 | } = message;
23 |
24 | const transaction = new Transaction();
25 | if (computeUnitPrice) {
26 | const units = 1000;
27 | const MICRO_LAMPORTS_PER_LAMPORT = 1_000_000;
28 | const additionalFee = Math.ceil(
29 | (computeUnitPrice * units) / MICRO_LAMPORTS_PER_LAMPORT
30 | );
31 | transaction.add(
32 | ComputeBudgetProgram.requestUnits({
33 | units,
34 | additionalFee,
35 | })
36 | );
37 | }
38 | const breakAccountInputs = [
39 | {
40 | pubkey: new PublicKey(programDataAccount),
41 | isWritable: true,
42 | isSigner: false,
43 | },
44 | ];
45 | if (extraWriteAccount) {
46 | breakAccountInputs.push({
47 | pubkey: new PublicKey(extraWriteAccount),
48 | isWritable: true,
49 | isSigner: false,
50 | });
51 | }
52 | transaction.add({
53 | keys: breakAccountInputs,
54 | programId: new PublicKey(programId),
55 | data: Buffer.from(Bytes.instructionDataFromId(bitId)),
56 | });
57 | transaction.recentBlockhash = blockhash;
58 | transaction.sign(Keypair.fromSecretKey(feeAccountSecretKey));
59 |
60 | const signatureBuffer = transaction.signature;
61 |
62 | self.postMessage({
63 | trackingId: trackingId,
64 | signature: signatureBuffer,
65 | serializedTransaction: transaction.serialize(),
66 | });
67 | }
68 |
69 | self.onmessage = (event: any) => {
70 | const message = event.data;
71 |
72 | try {
73 | createTransaction(message);
74 | } catch (error) {
75 | self.postMessage({
76 | trackingId: message.trackingId,
77 | error: error,
78 | });
79 | }
80 | };
81 |
--------------------------------------------------------------------------------
/client/svgTransform.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | process(): string {
3 | return "module.exports = {};";
4 | },
5 | getCacheKey(): string {
6 | // The output is always the same.
7 | return "svgTransform";
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext", "webworker"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "downlevelIteration": true,
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "baseUrl": "src",
19 | "noFallthroughCasesInSwitch": true
20 | },
21 | "include": ["src"]
22 | }
23 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@solana/break-app",
3 | "lockfileVersion": 2,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "name": "@solana/break-app",
8 | "hasInstallScript": true
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@solana/break-app",
3 | "cacheDirectories": [
4 | "client/node_modules",
5 | "server/node_modules"
6 | ],
7 | "scripts": {
8 | "client:build": "./package.sh client run build",
9 | "client:install": "./package.sh client install",
10 | "server:build": "./package.sh server run build",
11 | "server:start": "./package.sh server run start",
12 | "server:install": "./package.sh server install",
13 | "program:build": "./program/do.sh build",
14 | "program:clean": "./program/do.sh clean",
15 | "start": "npm run server:start",
16 | "build": "./package.sh both run build",
17 | "lint": "./package.sh both run lint",
18 | "lint:fix": "./package.sh both run lint:fix",
19 | "format": "./package.sh both run format",
20 | "format:fix": "./package.sh both run format:fix",
21 | "postinstall": "./package.sh both install"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/package.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | dir="$1"
4 | shift
5 | if [ "$dir" = "both" ]; then
6 | set -ex
7 | (cd client && npm "$@" )
8 | (cd server && npm "$@" )
9 | else
10 | set -ex
11 | (cd $dir && npm "$@" )
12 | fi
13 |
--------------------------------------------------------------------------------
/program/.gitignore:
--------------------------------------------------------------------------------
1 | # dist is intentionally not ignored
2 | target
3 |
--------------------------------------------------------------------------------
/program/Cargo.toml:
--------------------------------------------------------------------------------
1 | # Note: This crate must be built using build.sh
2 |
3 | [package]
4 | name = "break-solana-program"
5 | version = "0.1.0"
6 | description = "Break Solana program"
7 | authors = ["Solana Maintainers "]
8 | repository = "https://github.com/solana-labs/break"
9 | license = "Apache-2.0"
10 | homepage = "https://break.solana.com/"
11 | edition = "2018"
12 |
13 | [features]
14 | no-entrypoint = []
15 |
16 | [dependencies]
17 | solana-program = "=1.6.6"
18 |
19 | [lib]
20 | crate-type = ["cdylib", "lib"]
21 | name = "break_solana_program"
22 |
--------------------------------------------------------------------------------
/program/Xargo.toml:
--------------------------------------------------------------------------------
1 | [target.bpfel-unknown-unknown.dependencies.std]
2 | features = []
--------------------------------------------------------------------------------
/program/dist/break_solana_program.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solana-labs/break/c97989858498f056954649c0f8ae03862a805a4d/program/dist/break_solana_program.so
--------------------------------------------------------------------------------
/program/src/lib.rs:
--------------------------------------------------------------------------------
1 | use solana_program::{
2 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
3 | };
4 |
5 | entrypoint!(process_instruction);
6 | fn process_instruction<'a>(
7 | _program_id: &Pubkey,
8 | accounts: &'a [AccountInfo<'a>],
9 | instruction_data: &[u8],
10 | ) -> ProgramResult {
11 | // Assume a writable account is at index 0
12 | let mut account_data = accounts[0].try_borrow_mut_data()?;
13 |
14 | // xor with the account data using byte and bit from ix data
15 | let index = u16::from_be_bytes([instruction_data[0], instruction_data[1]]);
16 | let byte = index >> 3;
17 | let bit = (index & 0x7) as u8;
18 | account_data[byte as usize] ^= 1 << (7 - bit);
19 |
20 | Ok(())
21 | }
22 |
23 | // Sanity tests
24 | #[cfg(test)]
25 | mod test {
26 | use super::*;
27 | use solana_program::clock::Epoch;
28 |
29 | #[test]
30 | fn test_xor() {
31 | let program_id = Pubkey::default();
32 | let key = Pubkey::default();
33 | let mut lamports = 0;
34 | let mut data = vec![0; 4];
35 | let owner = Pubkey::default();
36 | let account = AccountInfo::new(
37 | &key,
38 | false,
39 | true,
40 | &mut lamports,
41 | &mut data,
42 | &owner,
43 | false,
44 | Epoch::default(),
45 | );
46 |
47 | let accounts = vec![account];
48 |
49 | process_instruction(&program_id, &accounts, &[1, 1, 1, 1]).unwrap();
50 | assert_eq!(*accounts[0].data.borrow(), &[1, 1, 1, 1]);
51 |
52 | process_instruction(&program_id, &accounts, &[1, 1, 1, 1]).unwrap();
53 | assert_eq!(*accounts[0].data.borrow(), &[0, 0, 0, 0]);
54 | }
55 |
56 | #[test]
57 | #[should_panic]
58 | fn test_bad_instruction_data() {
59 | let program_id = Pubkey::default();
60 | let key = Pubkey::default();
61 | let mut lamports = 0;
62 | let mut data = vec![0; 2];
63 | let owner = Pubkey::default();
64 | let account = AccountInfo::new(
65 | &key,
66 | false,
67 | true,
68 | &mut lamports,
69 | &mut data,
70 | &owner,
71 | false,
72 | Epoch::default(),
73 | );
74 |
75 | let accounts = vec![account];
76 | process_instruction(&program_id, &accounts, &[0, 1, 2]).unwrap();
77 | }
78 |
79 | #[test]
80 | #[should_panic]
81 | fn test_bad_account() {
82 | let program_id = Pubkey::default();
83 | let accounts = vec![];
84 | process_instruction(&program_id, &accounts, &[0, 1, 2, 4]).unwrap();
85 | }
86 | #[test]
87 | #[should_panic]
88 | fn test_bad_account_data() {
89 | let program_id = Pubkey::default();
90 | let key = Pubkey::default();
91 | let mut lamports = 0;
92 | let mut data = vec![0; 3];
93 | let owner = Pubkey::default();
94 | let account = AccountInfo::new(
95 | &key,
96 | false,
97 | true,
98 | &mut lamports,
99 | &mut data,
100 | &owner,
101 | false,
102 | Epoch::default(),
103 | );
104 |
105 | let accounts = vec![account];
106 | process_instruction(&program_id, &accounts, &[0, 1, 2, 4]).unwrap();
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/server/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | webpack.config.js
3 | src/types
4 |
--------------------------------------------------------------------------------
/server/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/eslint-recommended",
9 | "plugin:@typescript-eslint/recommended"
10 | ],
11 | "globals": {
12 | "Atomics": "readonly",
13 | "SharedArrayBuffer": "readonly"
14 | },
15 | "parser": "@typescript-eslint/parser",
16 | "parserOptions": {
17 | "ecmaVersion": 2018,
18 | "sourceType": "module"
19 | },
20 | "plugins": ["@typescript-eslint"],
21 | "rules": {}
22 | }
23 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/server/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@solana/break-backend",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "Try to break Solana's network",
6 | "author": "Solana Maintainers ",
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/solana-labs/break"
11 | },
12 | "scripts": {
13 | "build": "webpack --mode production",
14 | "test": "echo 'no test specified'",
15 | "start": "node ./dist/server.js",
16 | "start:dev": "ts-node src/index.ts",
17 | "build:dev": "webpack --mode development",
18 | "format": "prettier --check \"**/*.{js,jsx,ts,tsx,json,md}\"",
19 | "format:fix": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
20 | "lint": "set -ex; eslint --ext .js,.ts,.tsx .",
21 | "lint:fix": "npm run lint -- --fix"
22 | },
23 | "testnetDefaultChannel": "edge",
24 | "dependencies": {
25 | "@sentry/node": "^7.1.1",
26 | "@sentry/tracing": "^7.0.0",
27 | "@solana/web3.js": "^1.43.6",
28 | "@types/cors": "^2.8.10",
29 | "@types/express": "^4.17.12",
30 | "@types/node": "^17.0.41",
31 | "@types/ws": "^8.5.3",
32 | "@typescript-eslint/eslint-plugin": "^4.33.0",
33 | "@typescript-eslint/parser": "^4.33.0",
34 | "bs58": "^5.0.0",
35 | "cors": "^2.8.5",
36 | "eslint": "^7.27.0",
37 | "express": "^4.18.1",
38 | "prettier": "^2.6.2",
39 | "ts-loader": "^9.3.0",
40 | "ts-node": "^10.8.1",
41 | "typescript": "^4.7.3",
42 | "webpack": "^5.73.0",
43 | "webpack-cli": "^4.9.2",
44 | "webpack-node-externals": "^3.0.0",
45 | "ws": "^8.7.0"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/server/src/api.ts:
--------------------------------------------------------------------------------
1 | import { Connection } from "@solana/web3.js";
2 | import { cluster, url, urlTls } from "./urls";
3 | import { PROGRAM_ID } from "./program";
4 | import TpuProxy from "./tpu_proxy";
5 | import WebSocketServer from "./websocket";
6 | import { Express } from "express";
7 | import http from "http";
8 |
9 | export default class ApiServer {
10 | static async start(app: Express, httpServer: http.Server): Promise {
11 | const connection = new Connection(url, "confirmed");
12 | const tpuProxy = await TpuProxy.create(connection);
13 | WebSocketServer.start(httpServer, tpuProxy);
14 |
15 | await tpuProxy.connect();
16 |
17 | const programId = PROGRAM_ID?.toBase58();
18 | if (!programId) {
19 | throw new Error("Internal error: program id is missing");
20 | }
21 |
22 | app.post("/init", async (req, res) => {
23 | res
24 | .send(
25 | JSON.stringify({
26 | programId,
27 | clusterUrl: urlTls,
28 | cluster,
29 | paymentRequired: process.env.REQUIRE_PAYMENT === "true",
30 | })
31 | )
32 | .end();
33 | });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/server/src/available_nodes.ts:
--------------------------------------------------------------------------------
1 | import { Connection } from "@solana/web3.js";
2 | import { endlessRetry } from "./utils";
3 |
4 | type NodeAddress = string;
5 | type TpuEndpoint = string;
6 | type AvailableNodes = Map;
7 | type DelinquentNodes = Set;
8 |
9 | // Polls cluster to determine which nodes are available
10 | export default class AvailableNodesService {
11 | refreshing = false;
12 |
13 | constructor(
14 | private connection: Connection,
15 | public nodes: AvailableNodes,
16 | public delinquents: DelinquentNodes
17 | ) {
18 | // Refresh every 5min in case nodes leave the cluster or change port configuration
19 | setInterval(() => this.refresh(), 5 * 60 * 1000);
20 | }
21 |
22 | static start = async (
23 | connection: Connection
24 | ): Promise => {
25 | const nodes = await AvailableNodesService.getAvailableNodes(connection);
26 | return new AvailableNodesService(connection, nodes, new Set());
27 | };
28 |
29 | private static getAvailableNodes = async (
30 | connection: Connection
31 | ): Promise => {
32 | const availableNodes = new Map();
33 | const nodes = await endlessRetry("getClusterNodes", async () =>
34 | connection.getClusterNodes()
35 | );
36 | for (const node of nodes) {
37 | if (node.tpu) {
38 | availableNodes.set(node.pubkey, node.tpu);
39 | }
40 | }
41 | return availableNodes;
42 | };
43 |
44 | private refresh = async (): Promise => {
45 | if (this.refreshing) return;
46 | this.refreshing = true;
47 | this.nodes = await AvailableNodesService.getAvailableNodes(this.connection);
48 | this.delinquents.clear();
49 | this.refreshing = false;
50 | };
51 | }
52 |
--------------------------------------------------------------------------------
/server/src/index.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import http from "http";
3 | import cors from "cors";
4 | import path from "path";
5 | import ApiServer from "./api";
6 | import * as Sentry from "@sentry/node";
7 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
8 | import * as Tracing from "@sentry/tracing";
9 |
10 | Sentry.init({
11 | dsn: "https://f74dafc67c914776b018c3be136bca19@o434108.ingest.sentry.io/5411826",
12 | // send 10% of all errors to Sentry
13 | tracesSampleRate: 0.1,
14 | });
15 |
16 | const app = express();
17 |
18 | // Redirect to https on Heroku
19 | if (
20 | process.env.NODE_ENV === "production" &&
21 | process.env.FORCE_HTTPS !== undefined
22 | ) {
23 | app.use((req, res, next) => {
24 | if (req.header("x-forwarded-proto") !== "https") {
25 | res.redirect(`https://${req.header("host")}${req.url}`);
26 | } else {
27 | next();
28 | }
29 | });
30 | }
31 |
32 | const rootPath = path.join(__dirname, "..", "..");
33 | const staticPath = path.join(rootPath, "client", "build");
34 | console.log(`Serving static files from: ${staticPath}`);
35 | app.use("/", express.static(staticPath));
36 | app.get("/*", (req, res) => {
37 | res.sendFile(path.join(staticPath, "/index.html"));
38 | });
39 |
40 | const httpServer = http.createServer(app);
41 | const port = process.env.PORT || 8080;
42 | httpServer.listen(port);
43 | console.log(`Server listening on port: ${port}`);
44 |
45 | if (!process.env.DISABLE_API) {
46 | app.use(cors());
47 | app.use(express.json());
48 | ApiServer.start(app, httpServer);
49 | }
50 |
--------------------------------------------------------------------------------
/server/src/leader_schedule.ts:
--------------------------------------------------------------------------------
1 | import { Connection } from "@solana/web3.js";
2 | import { endlessRetry, reportError } from "./utils";
3 |
4 | // Number of upcoming slots to include when building upcoming node set
5 | export const UPCOMING_SLOT_SEARCH = parseInt(
6 | process.env.LEADER_SLOT_FANOUT || "40"
7 | );
8 |
9 | // Number of past slots to include when building upcoming node set
10 | export const PAST_SLOT_SEARCH = 4;
11 |
12 | // Updates the leader schedule every epoch and provides a set of the
13 | // upcoming nodes in the schedule
14 | export default class LeaderScheduleService {
15 | refreshing = false;
16 |
17 | constructor(
18 | private connection: Connection,
19 | private leaderAddresses: Array,
20 | private scheduleFirstSlot: number
21 | ) {}
22 |
23 | static start = async (
24 | connection: Connection,
25 | currentSlot: number
26 | ): Promise => {
27 | const leaderService = new LeaderScheduleService(
28 | connection,
29 | [],
30 | currentSlot
31 | );
32 |
33 | leaderService.leaderAddresses = await endlessRetry("getSlotLeaders", () =>
34 | leaderService.fetchLeaders(currentSlot)
35 | );
36 |
37 | return leaderService;
38 | };
39 |
40 | private async fetchLeaders(startSlot: number): Promise> {
41 | const leaders = await this.connection.getSlotLeaders(
42 | startSlot,
43 | 2 * UPCOMING_SLOT_SEARCH
44 | );
45 | return leaders.map((l) => l.toBase58());
46 | }
47 |
48 | getFirstSlot = (): number => {
49 | return this.scheduleFirstSlot;
50 | };
51 |
52 | getSlotLeader = (slot: number): string | null => {
53 | const firstSlot = this.scheduleFirstSlot;
54 | const lastSlot = this.lastSlot();
55 | if (slot < firstSlot) {
56 | console.error(
57 | `getSlotLeader failed: Tried to get ${slot} before first schedule slot ${firstSlot}`
58 | );
59 | } else if (slot > lastSlot) {
60 | console.error(
61 | `getSlotLeader failed: Tried to get ${slot} after last schedule slot ${lastSlot}`
62 | );
63 | } else {
64 | return this.leaderAddresses[slot - this.scheduleFirstSlot];
65 | }
66 | return null;
67 | };
68 |
69 | private lastSlot = (): number => {
70 | return this.scheduleFirstSlot + this.leaderAddresses.length - 1;
71 | };
72 |
73 | shouldRefresh = (currentSlot: number): boolean => {
74 | const shouldRefreshAt = this.lastSlot() - UPCOMING_SLOT_SEARCH;
75 | return currentSlot >= shouldRefreshAt;
76 | };
77 |
78 | refresh = async (currentSlot: number): Promise => {
79 | if (this.refreshing) return;
80 | this.refreshing = true;
81 | try {
82 | const firstSlot = Math.max(0, currentSlot - PAST_SLOT_SEARCH);
83 | const leaderAddresses = await this.fetchLeaders(firstSlot);
84 | this.scheduleFirstSlot = firstSlot;
85 | this.leaderAddresses = leaderAddresses;
86 | } catch (err) {
87 | reportError(err, "failed to refresh slot leaders");
88 | } finally {
89 | this.refreshing = false;
90 | }
91 | };
92 | }
93 |
--------------------------------------------------------------------------------
/server/src/leader_tracker.ts:
--------------------------------------------------------------------------------
1 | import { Connection } from "@solana/web3.js";
2 |
3 | const MAX_RECENT_SLOTS_LENGTH = 12;
4 |
5 | // 48 chosen because it's unlikely that 12 leaders in a row will miss their slots
6 | const MAX_SLOT_SKIP_DISTANCE = 48;
7 |
8 | export default class LeaderTrackerService {
9 | private recentSlots: Array = [];
10 |
11 | constructor(
12 | private connection: Connection,
13 | private currentSlot: number,
14 | callback: (slot: number) => Promise
15 | ) {
16 | this.recentSlots.push(currentSlot);
17 |
18 | let receivedShredNotification = false;
19 | let processingCallback = false;
20 | this.connection.onSlotUpdate((update) => {
21 | const previousCurrentSlot = this.currentSlot;
22 | let newCurrentSlot = this.currentSlot;
23 | switch (update.type) {
24 | case "firstShredReceived": {
25 | receivedShredNotification = true;
26 | newCurrentSlot = this.updateRecentSlots(update.slot);
27 | break;
28 | }
29 | case "completed": {
30 | receivedShredNotification = true;
31 | newCurrentSlot = this.updateRecentSlots(update.slot + 1);
32 | break;
33 | }
34 | case "createdBank": {
35 | // Fallback to bank created slot updates if no shred notifications
36 | // are received (ie. connected to single node cluster leader).
37 | if (!receivedShredNotification) {
38 | newCurrentSlot = this.updateRecentSlots(update.slot);
39 | }
40 | break;
41 | }
42 | }
43 |
44 | if (newCurrentSlot != previousCurrentSlot) {
45 | // console.debug(`Leader tracker detected new slot: ${newCurrentSlot}`);
46 | if (!processingCallback) {
47 | processingCallback = true;
48 | callback(newCurrentSlot)
49 | .then(() => {
50 | // console.debug(
51 | // `Leader tracker handled new slot: ${newCurrentSlot}`
52 | // );
53 | })
54 | .catch((err) => {
55 | console.error("Failed to handle new slot", err);
56 | })
57 | .finally(() => {
58 | processingCallback = false;
59 | });
60 | }
61 | }
62 | });
63 | }
64 |
65 | private updateRecentSlots = (slot: number) => {
66 | this.recentSlots.push(slot);
67 | while (this.recentSlots.length > MAX_RECENT_SLOTS_LENGTH) {
68 | this.recentSlots.shift();
69 | }
70 |
71 | // After updating recent slots, calculate the current slot
72 |
73 | const recentSlots = this.recentSlots.slice(0);
74 | recentSlots.sort();
75 |
76 | // Validators can broadcast invalid blocks that are far in the future
77 | // so check if the current slot is in line with the recent progression.
78 | const maxIndex = recentSlots.length - 1;
79 | const medianIndex = Math.floor(maxIndex / 2);
80 | const medianRecentSlot = recentSlots[medianIndex];
81 | const expectedCurrentSlot = medianRecentSlot + (maxIndex - medianIndex);
82 | const maxReasonableCurrentSlot =
83 | expectedCurrentSlot + MAX_SLOT_SKIP_DISTANCE;
84 |
85 | // Return the highest slot that doesn't exceed what we believe is a
86 | // reasonable slot.
87 | recentSlots.reverse();
88 | for (const slot of recentSlots) {
89 | if (slot <= maxReasonableCurrentSlot) {
90 | return slot;
91 | }
92 | }
93 |
94 | // This fall back is impossible
95 | return this.currentSlot;
96 | };
97 | }
98 |
--------------------------------------------------------------------------------
/server/src/program.ts:
--------------------------------------------------------------------------------
1 | import { Keypair, PublicKey } from "@solana/web3.js";
2 | import path from "path";
3 | import fs from "fs";
4 |
5 | const DEPLOYED_PROGRAM_ADDRESS = process.env.DEPLOYED_PROGRAM_ADDRESS;
6 |
7 | const PROGRAM_KEYPAIR_PATH = path.resolve(
8 | "..",
9 | "program",
10 | "target",
11 | "deploy",
12 | "break_solana_program-keypair.json"
13 | );
14 |
15 | export const PROGRAM_ID = (() => {
16 | if (DEPLOYED_PROGRAM_ADDRESS) {
17 | return new PublicKey(DEPLOYED_PROGRAM_ADDRESS);
18 | } else if (!process.env.DISABLE_API) {
19 | return readKeypairFromFile(PROGRAM_KEYPAIR_PATH).publicKey;
20 | }
21 | })();
22 |
23 | /**
24 | * Create a Keypair from a keypair file
25 | */
26 | function readKeypairFromFile(filePath: string): Keypair {
27 | const keypairString = fs.readFileSync(filePath, { encoding: "utf8" });
28 | const keypairBuffer = Buffer.from(JSON.parse(keypairString));
29 | return Keypair.fromSecretKey(keypairBuffer);
30 | }
31 |
--------------------------------------------------------------------------------
/server/src/urls.ts:
--------------------------------------------------------------------------------
1 | // To connect to a public cluster, set `export LIVE=1` in your
2 | // environment. By default, `LIVE=1` will connect to the devnet cluster.
3 |
4 | import { clusterApiUrl, Cluster } from "@solana/web3.js";
5 |
6 | function chooseCluster(): Cluster | undefined {
7 | if (!process.env.LIVE) return;
8 | switch (process.env.CLUSTER) {
9 | case "devnet":
10 | case "testnet":
11 | case "mainnet-beta": {
12 | return process.env.CLUSTER;
13 | }
14 | }
15 | return "devnet";
16 | }
17 |
18 | export const cluster = chooseCluster();
19 |
20 | export const url =
21 | process.env.RPC_URL ||
22 | (process.env.LIVE ? clusterApiUrl(cluster, false) : "http://localhost:8899");
23 |
24 | export const urlTls =
25 | process.env.RPC_URL ||
26 | (process.env.LIVE ? clusterApiUrl(cluster, true) : "http://localhost:8899");
27 |
--------------------------------------------------------------------------------
/server/src/utils.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from "@sentry/node";
2 |
3 | export function sleep(ms: number): Promise {
4 | return new Promise((resolve) => setTimeout(resolve, ms));
5 | }
6 |
7 | export function notUndefined(x: T | undefined): x is T {
8 | return x !== undefined;
9 | }
10 |
11 | export async function endlessRetry(
12 | name: string,
13 | call: () => Promise
14 | ): Promise {
15 | let result: T | undefined;
16 | while (result == undefined) {
17 | try {
18 | console.log(name, "fetching");
19 | result = await call();
20 | } catch (err) {
21 | reportError(err, `Request ${name} failed, retrying`);
22 | await sleep(1000);
23 | }
24 | }
25 | console.log(name, "fetched!");
26 | return result;
27 | }
28 |
29 | const cluster = process.env.CLUSTER;
30 | export function reportError(err: unknown, context: string): void {
31 | if (err instanceof Error) {
32 | console.error(context, err);
33 | if (process.env.NODE_ENV === "production") {
34 | Sentry.captureException(err, {
35 | tags: { context, cluster },
36 | });
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/server/src/websocket.ts:
--------------------------------------------------------------------------------
1 | import WebSocket from "ws";
2 | import http from "http";
3 | import TpuProxy from "./tpu_proxy";
4 |
5 | // eslint-disable-next-line @typescript-eslint/no-empty-function
6 | function noop() {}
7 |
8 | export default class WebSocketServer {
9 | static start(httpServer: http.Server, tpuProxy: TpuProxy): void {
10 | // Start websocket server
11 | let activeUsers = 0;
12 | const wss = new WebSocket.Server({ server: httpServer });
13 | wss.on("connection", function connection(ws) {
14 | let isAlive = true;
15 | let useRpc = true;
16 | let rpcOverride: string;
17 |
18 | function heartbeat() {
19 | isAlive = true;
20 | }
21 |
22 | const interval = setInterval(function ping() {
23 | if (isAlive === false) return ws.terminate();
24 | isAlive = false;
25 | ws.ping(noop);
26 | }, 30000);
27 |
28 | activeUsers++;
29 | ws.on("close", () => {
30 | clearInterval(interval);
31 | activeUsers--;
32 | });
33 | ws.on("message", (data: Buffer, isBinary: boolean) => {
34 | const message = isBinary ? data : data.toString();
35 | if (typeof message === "string") {
36 | if (message === "tpu") {
37 | console.log("Client switched to TPU mode");
38 | useRpc = false;
39 | } else if (message === "rpc") {
40 | console.log("Client switched to RPC mode");
41 | useRpc = true;
42 | } else {
43 | try {
44 | const rpcEndpoint = new URL(message);
45 | rpcOverride = rpcEndpoint.toString();
46 | console.log("Client overrode RPC endpoint to", rpcEndpoint.href);
47 | } catch (err) {
48 | console.warn("Ignoring client message", message);
49 | }
50 | }
51 | } else {
52 | tpuProxy.sendRawTransaction(
53 | message,
54 | useRpc,
55 | rpcOverride,
56 | (message: string) => {
57 | ws.send(message);
58 | }
59 | );
60 | }
61 | });
62 | ws.on("pong", heartbeat);
63 | });
64 |
65 | // Start active user broadcast loop
66 | setInterval(() => {
67 | wss.clients.forEach((client) => {
68 | if (client.readyState === WebSocket.OPEN) {
69 | client.send(JSON.stringify({ type: "heartbeat", activeUsers }));
70 | }
71 | });
72 | }, 1000);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/server/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "esModuleInterop": true,
5 | "downlevelIteration": true,
6 | "module": "commonjs",
7 | "moduleResolution": "node",
8 | "baseUrl": "./",
9 | "typeRoots": ["./src/types", "./node_modules/@types"],
10 | "paths": {
11 | "@/*": ["./src/*"]
12 | }
13 | },
14 | "include": ["src"]
15 | }
16 |
--------------------------------------------------------------------------------
/server/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require("webpack");
2 | const path = require("path");
3 | const nodeExternals = require("webpack-node-externals");
4 |
5 | module.exports = (env, argv) => {
6 | let mode = argv.mode;
7 | let config = {
8 | mode,
9 | target: "node",
10 | entry: ["./src/index.ts"],
11 | node: {
12 | __dirname: false,
13 | },
14 | output: {
15 | path: path.resolve(__dirname, "dist"),
16 | filename: "server.js",
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.ts$/,
22 | use: "ts-loader",
23 | exclude: /node_modules/,
24 | },
25 | ],
26 | },
27 | resolve: {
28 | extensions: [".ts", ".js", ".json"],
29 | },
30 | externals: [nodeExternals()],
31 | };
32 |
33 | if (mode === "development") {
34 | config.watch = true;
35 | }
36 |
37 | return config;
38 | };
39 |
--------------------------------------------------------------------------------