Skip to content

Commit fbcf1fd

Browse files
authored
An example React app that exercises the new @solana/web3.js and the new Wallet Standard wallet adapter in @solana/react (#2817)
1 parent 17b0b99 commit fbcf1fd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3825
-270
lines changed

.github/workflows/publish-legacy-docs.yml .github/workflows/publish-gh-pages.yml

+12-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Publish Legacy Documentation
1+
name: Publish GitHub Pages
22

33
on:
44
workflow_dispatch:
@@ -18,7 +18,7 @@ env:
1818
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
1919

2020
jobs:
21-
compile-docs-and-publish:
21+
compile-github-pages-and-publish:
2222
runs-on: ubuntu-latest
2323
steps:
2424
- name: Checkout
@@ -27,11 +27,17 @@ jobs:
2727
- name: Install Dependencies
2828
uses: ./.github/workflows/actions/install-dependencies
2929

30-
- name: Compile Documentation
31-
run: pnpm turbo run compile:docs --concurrency=100% --filter=@solana/web3.js
30+
- name: Compile
31+
run: pnpm turbo run compile:ghpages --concurrency=100%
3232

33-
- name: Deploy Documentation to Github Pages
33+
- name: Assemble deploy directory
34+
run: |
35+
mkdir -p .ghpages-deploy
36+
cp -r ./packages/library-legacy/doc/* .ghpages-deploy
37+
cp -r ./examples/react-app/dist/ .ghpages-deploy/example/
38+
39+
- name: Deploy to Github Pages
3440
uses: peaceiris/actions-gh-pages@v3
3541
with:
3642
github_token: ${{ secrets.GITHUB_TOKEN }}
37-
publish_dir: ./packages/library-legacy/doc
43+
publish_dir: .ghpages-deploy

.github/workflows/pull-requests.yml

+3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ jobs:
4646
- name: Build & Test
4747
run: pnpm build
4848

49+
- name: Build GitHub Pages
50+
run: pnpm turbo run compile:ghpages --concurrency=100%
51+
4952
- name: Stop Test Validator
5053
if: always() && steps.start-test-validator.outcome == 'success'
5154
run: kill ${{ steps.start-test-validator.outputs.pid }}

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ yarn-error.log*
2424
# `solana-test-validator`
2525
.agave/
2626
test-ledger/
27+
28+
# GitHub Pages deploy directory
29+
.ghpages-deploy

examples/react-app/.eslintrc.cjs

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = {
2+
root: true,
3+
env: { browser: true, es2020: true },
4+
extends: ['../../.eslintrc.js', '@solana/eslint-config-solana/react'],
5+
ignorePatterns: ['dist', '.eslintrc.cjs'],
6+
plugins: ['react-refresh'],
7+
rules: {
8+
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
9+
},
10+
};

examples/react-app/.gitignore

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?

examples/react-app/LICENSE

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Copyright (c) 2023 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.

examples/react-app/README.md

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# @solana/example-react-app
2+
3+
This is an example of how to use `@solana/web3.js` and `@solana/react` to build a React web application.
4+
5+
The latest version of this code is automatically deployed to https://solana-labs.github.io/solana-web3.js/example/
6+
7+
## Features
8+
9+
- Connects to browser wallets that support the Wallet Standard; one or more at a time
10+
- Fetches and subscribes to the balance of the selected wallet
11+
- Allows you to sign an arbitrary message using a wallet account
12+
- Allows you to make a transfer from the selected wallet to any other connected wallet
13+
14+
## Developing
15+
16+
Start a server in development mode.
17+
18+
```shell
19+
pnpm install
20+
pnpm turbo compile:js compile:typedefs
21+
pnpm dev
22+
```
23+
24+
Press <kbd>o</kbd> + <kbd>Enter</kbd> to open the app in a browser. Edits to the source code will automatically reload the app.
25+
26+
## Building for deployment
27+
28+
Build a static bundle and HTML for deployment to a webserver.
29+
30+
```shell
31+
pnpm install
32+
pnpm turbo build
33+
```
34+
35+
The contents of the `dist/` directory can now be uploaded to a webserver.
36+
37+
## Enabling Mainnet-Beta
38+
39+
Access to this cluster is typically blocked by [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) rules, so it is disabled in the example app by default. To enable it, start the server or compile the application with the `REACT_EXAMPLE_APP_ENABLE_MAINNET` environment variable set to `"true"`.
40+
41+
```shell
42+
REACT_EXAMPLE_APP_ENABLE_MAINNET=true pnpm dev
43+
REACT_EXAMPLE_APP_ENABLE_MAINNET=true pnpm build
44+
```

examples/react-app/index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/solanaLogoMark.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Solana React Example App</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>

examples/react-app/package.json

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@solana/example-react-app",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10+
"preview": "vite preview"
11+
},
12+
"dependencies": {
13+
"@radix-ui/react-dropdown-menu": "^2.0.6",
14+
"@radix-ui/react-icons": "^1.3.0",
15+
"@radix-ui/themes": "^3.0.5",
16+
"@solana-program/system": "^0.3.2",
17+
"@solana/react": "workspace:*",
18+
"@solana/web3.js": "workspace:@solana/web3.js-experimental@*",
19+
"@wallet-standard/core": "pre",
20+
"@wallet-standard/react": "pre",
21+
"react": "^18.3.0",
22+
"react-dom": "^18.3.0",
23+
"react-error-boundary": "^4.0.13",
24+
"swr": "^2.2.5"
25+
},
26+
"devDependencies": {
27+
"@solana/wallet-standard-features": "^1.2.0",
28+
"@types/react": "^18.3",
29+
"@types/react-dom": "^18.3",
30+
"@typescript-eslint/eslint-plugin": "^7.13.1",
31+
"@typescript-eslint/parser": "^7.13.1",
32+
"@vitejs/plugin-react-swc": "^3.7.0",
33+
"eslint-plugin-react-refresh": "^0.4.7",
34+
"vite": "^5.3.1"
35+
}
36+
}
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
2+
import { Text, Tooltip } from '@radix-ui/themes';
3+
import { address } from '@solana/web3.js';
4+
import type { UiWalletAccount } from '@wallet-standard/react';
5+
import { useContext, useMemo } from 'react';
6+
import useSWRSubscription from 'swr/subscription';
7+
8+
import { ChainContext } from '../context/ChainContext';
9+
import { RpcContext } from '../context/RpcContext';
10+
import { getErrorMessage } from '../errors';
11+
import { balanceSubscribe } from '../functions/balance';
12+
import { ErrorDialog } from './ErrorDialog';
13+
14+
type Props = Readonly<{
15+
account: UiWalletAccount;
16+
}>;
17+
18+
export function Balance({ account }: Props) {
19+
const { chain } = useContext(ChainContext);
20+
const { rpc, rpcSubscriptions } = useContext(RpcContext);
21+
const subscribe = useMemo(() => balanceSubscribe.bind(null, rpc, rpcSubscriptions), [rpc, rpcSubscriptions]);
22+
const { data: lamports, error } = useSWRSubscription({ address: address(account.address), chain }, subscribe);
23+
if (error) {
24+
return (
25+
<>
26+
<ErrorDialog
27+
error={error}
28+
key={`${account.address}:${chain}`}
29+
title="Failed to fetch account balance"
30+
/>
31+
<Text>
32+
<Tooltip content={`Could not fetch balance: ${getErrorMessage(error, 'Unknown reason')}`}>
33+
<ExclamationTriangleIcon
34+
color="red"
35+
style={{ height: 16, verticalAlign: 'text-bottom', width: 16 }}
36+
/>
37+
</Tooltip>
38+
</Text>
39+
</>
40+
);
41+
} else if (lamports == null) {
42+
return <Text>&ndash;</Text>;
43+
} else {
44+
const formattedSolValue = new Intl.NumberFormat(undefined, { maximumFractionDigits: 5 }).format(
45+
// @ts-expect-error This format string is 100% allowed now.
46+
`${lamports}E-9`,
47+
);
48+
return <Text>{`${formattedSolValue} \u25CE`}</Text>;
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Pencil1Icon } from '@radix-ui/react-icons';
2+
import { Blockquote, Box, Button, Code, DataList, Dialog, Flex, TextField } from '@radix-ui/themes';
3+
import { getBase64Decoder } from '@solana/web3.js';
4+
import type { ReadonlyUint8Array } from '@wallet-standard/core';
5+
import type { SyntheticEvent } from 'react';
6+
import { useRef, useState } from 'react';
7+
8+
import { ErrorDialog } from '../components/ErrorDialog';
9+
10+
type Props = Readonly<{
11+
signMessage(message: ReadonlyUint8Array): Promise<ReadonlyUint8Array>;
12+
}>;
13+
14+
export function BaseSignMessageFeaturePanel({ signMessage }: Props) {
15+
const { current: NO_ERROR } = useRef(Symbol());
16+
const [isSigningMessage, setIsSigningMessage] = useState(false);
17+
const [error, setError] = useState<unknown | typeof NO_ERROR>(NO_ERROR);
18+
const [lastSignature, setLastSignature] = useState<ReadonlyUint8Array | undefined>();
19+
const [text, setText] = useState<string>();
20+
return (
21+
<Flex asChild gap="2" direction={{ initial: 'column', sm: 'row' }} style={{ width: '100%' }}>
22+
<form
23+
onSubmit={async e => {
24+
e.preventDefault();
25+
setError(NO_ERROR);
26+
setIsSigningMessage(true);
27+
try {
28+
const signature = await signMessage(new TextEncoder().encode(text));
29+
setLastSignature(signature);
30+
} catch (e) {
31+
setLastSignature(undefined);
32+
setError(e);
33+
} finally {
34+
setIsSigningMessage(false);
35+
}
36+
}}
37+
>
38+
<Box flexGrow="1">
39+
<TextField.Root
40+
placeholder="Write a message to sign"
41+
onChange={(e: SyntheticEvent<HTMLInputElement>) => setText(e.currentTarget.value)}
42+
value={text}
43+
>
44+
<TextField.Slot>
45+
<Pencil1Icon />
46+
</TextField.Slot>
47+
</TextField.Root>
48+
</Box>
49+
<Dialog.Root
50+
open={!!lastSignature}
51+
onOpenChange={open => {
52+
if (!open) {
53+
setLastSignature(undefined);
54+
}
55+
}}
56+
>
57+
<Dialog.Trigger>
58+
<Button
59+
color={error ? undefined : 'red'}
60+
disabled={!text}
61+
loading={isSigningMessage}
62+
type="submit"
63+
>
64+
Sign Message
65+
</Button>
66+
</Dialog.Trigger>
67+
{lastSignature ? (
68+
<Dialog.Content
69+
onClick={e => {
70+
e.stopPropagation();
71+
}}
72+
>
73+
<Dialog.Title>You Signed a Message!</Dialog.Title>
74+
<DataList.Root orientation={{ initial: 'vertical', sm: 'horizontal' }}>
75+
<DataList.Item>
76+
<DataList.Label minWidth="88px">Message</DataList.Label>
77+
<DataList.Value>
78+
<Blockquote>{text}</Blockquote>
79+
</DataList.Value>
80+
</DataList.Item>
81+
<DataList.Item>
82+
<DataList.Label minWidth="88px">Signature</DataList.Label>
83+
<DataList.Value>
84+
<Code truncate>{getBase64Decoder().decode(lastSignature)}</Code>
85+
</DataList.Value>
86+
</DataList.Item>
87+
</DataList.Root>
88+
<Flex gap="3" mt="4" justify="end">
89+
<Dialog.Close>
90+
<Button>Cool!</Button>
91+
</Dialog.Close>
92+
</Flex>
93+
</Dialog.Content>
94+
) : null}
95+
</Dialog.Root>
96+
{error !== NO_ERROR ? (
97+
<ErrorDialog error={error} onClose={() => setError(NO_ERROR)} title="Failed to sign message" />
98+
) : null}
99+
</form>
100+
</Flex>
101+
);
102+
}

0 commit comments

Comments
 (0)