├── .github
├── actions
│ └── pnpm-install
│ │ └── action.yml
└── workflows
│ ├── test-glow-client.yml
│ └── test-solana-client.yml
├── .gitignore
├── README.md
├── package.json
├── packages
├── example-next-js
│ ├── .eslintrc.json
│ ├── .gitignore
│ ├── README.md
│ ├── next-env.d.ts
│ ├── next.config.js
│ ├── package.json
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── buttons.tsx
│ │ └── index.tsx
│ ├── public
│ │ ├── favicon.ico
│ │ └── vercel.svg
│ ├── styles
│ │ ├── Buttons.module.scss
│ │ └── Home.module.css
│ └── tsconfig.json
├── glow-client
│ ├── .eslintrc.json
│ ├── .release-it.json
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── __tests__
│ │ │ └── utils.test.ts
│ │ ├── glow-client.ts
│ │ ├── index.ts
│ │ ├── utils.ts
│ │ └── window-types.ts
│ └── tsconfig.json
├── glow-react
│ ├── .release-it.json
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── GlowContext.tsx
│ │ ├── assets
│ │ │ ├── GlowIcon.tsx
│ │ │ └── GlowIcon3D.tsx
│ │ ├── components
│ │ │ └── GlowSignInButton.tsx
│ │ ├── hooks
│ │ │ ├── useOnMount.tsx
│ │ │ └── usePolling.tsx
│ │ ├── index.ts
│ │ └── styles
│ │ │ └── index.scss
│ └── tsconfig.json
├── solana-client
│ ├── .eslintrc.json
│ ├── .release-it.json
│ ├── README.md
│ ├── jest.config.js
│ ├── package.json
│ ├── src
│ │ ├── EllipticCurve.ts
│ │ ├── GKeypair.ts
│ │ ├── GPublicKey.ts
│ │ ├── GTransaction.ts
│ │ ├── __tests__
│ │ │ ├── GPublicKey.test.ts
│ │ │ ├── GTransaction.test.ts
│ │ │ └── __snapshots__
│ │ │ │ └── GTransaction.test.ts.snap
│ │ ├── base-types.ts
│ │ ├── borsh
│ │ │ ├── CompactArray.ts
│ │ │ ├── GlowBorshTypes.ts
│ │ │ ├── __tests__
│ │ │ │ └── GlowBorsh.test.ts
│ │ │ ├── base.ts
│ │ │ ├── index.ts
│ │ │ ├── programs
│ │ │ │ └── token-program.ts
│ │ │ └── transaction-borsh.ts
│ │ ├── client
│ │ │ ├── client-types.ts
│ │ │ ├── error-codes.ts
│ │ │ ├── normalizers.ts
│ │ │ ├── rpc-types.ts
│ │ │ └── solana-client.ts
│ │ ├── error.ts
│ │ ├── index.ts
│ │ ├── transaction
│ │ │ ├── AddressLookupTable.ts
│ │ │ ├── LTransaction.ts
│ │ │ ├── TransactionInterface.ts
│ │ │ ├── VTransaction.ts
│ │ │ ├── XTransaction.ts
│ │ │ ├── __tests__
│ │ │ │ ├── lookup-table.test.ts
│ │ │ │ ├── transaction-utils.test.ts
│ │ │ │ ├── vtransaction-3N3xmERQotKh5of4H5Q5UEjwMKhaDR52pfJHCGRcQUD5hHTBX9hnXBbRcJ6CiFczrRtPhtx3b2ddd2kSjvZP7Cg.json
│ │ │ │ ├── vtransaction-5j9WCjiybkEDMXYyuUTy8d2kEcJeaRyzcsPpkF4kmJdVbxR483dzKsJLRTrY91bKxi9tA94vfzqjCmdP3G596kBt.json
│ │ │ │ └── vtransaction.test.ts
│ │ │ └── transaction-utils.ts
│ │ └── utils
│ │ │ └── ed25519.ts
│ ├── tsconfig.esm.json
│ └── tsconfig.json
└── wallet-standard
│ ├── .eslintignore
│ ├── .eslintrc
│ ├── .gitignore
│ ├── .prettierignore
│ ├── .release-it.json
│ ├── package.json
│ ├── src
│ ├── account.ts
│ ├── icon.ts
│ ├── index.ts
│ ├── initialize.ts
│ ├── register.ts
│ ├── solana.ts
│ └── wallet.ts
│ ├── tsconfig.esm.json
│ └── tsconfig.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
/.github/actions/pnpm-install/action.yml:
--------------------------------------------------------------------------------
1 | name: "pnpm Install"
2 | description: "Install pnpm for a particular folder in a workspace and use a cache. Intended to be run in a Node Docker container."
3 | inputs:
4 | package-name: # @lux/sass, next, etc
5 | description: "This is the package in our workspace that we want to install."
6 | required: true
7 | runs:
8 | using: "composite"
9 | steps:
10 | - uses: pnpm/action-setup@v4.0.0
11 | name: Install pnpm
12 | with:
13 | version: 7
14 | run_install: false
15 |
16 | - name: Get pnpm store directory
17 | id: pnpm-cache
18 | shell: sh
19 | run: |
20 | echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
21 |
22 | - uses: actions/cache@v4
23 | name: Setup pnpm cache
24 | with:
25 | path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
26 | key: ${{ runner.os }}-pnpm-store-2-${{ inputs.package-name}}-${{ hashFiles('**/pnpm-lock.yaml') }}
27 | restore-keys: |
28 | ${{ runner.os }}-pnpm-store-2
29 |
30 | - name: Install dependencies
31 | shell: sh
32 | run: |
33 | pnpm install --filter ${{ inputs.package-name }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/test-glow-client.yml:
--------------------------------------------------------------------------------
1 | name: Glow Client
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - "packages/glow-client/**"
7 |
8 | jobs:
9 | js_tests:
10 | name: JS Tests
11 | runs-on: ubuntu-24.04
12 | container: node:20.18.0-alpine3.19
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 1
18 | - name: Install PNPM and dependencies
19 | run: npm i pnpm@9.12.1 -g && pnpm install --filter @glow-xyz/solana-client... --filter @glow-xyz/glow-client... --frozen-lockfile
20 | - name: Build
21 | run: pnpm --filter @glow-xyz/solana-client run build
22 | - name: Lint
23 | run: pnpm --filter @glow-xyz/glow-client run lint
24 | - name: TSC
25 | run: pnpm --filter @glow-xyz/glow-client run tsc
26 | - name: Test
27 | run: pnpm --filter @glow-xyz/glow-client run test
28 |
--------------------------------------------------------------------------------
/.github/workflows/test-solana-client.yml:
--------------------------------------------------------------------------------
1 | name: Solana Client
2 |
3 | on:
4 | pull_request:
5 | paths:
6 | - "packages/solana-client/**"
7 |
8 | jobs:
9 | js_tests:
10 | name: JS Tests
11 | runs-on: ubuntu-24.04
12 | container: node:20.18.0-alpine3.19
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 1
18 | - name: Install PNPM and dependencies
19 | run: npm i pnpm@9.12.1 -g && pnpm install --filter @glow-xyz/solana-client... --frozen-lockfile
20 | - name: Lint
21 | run: pnpm --filter @glow-xyz/solana-client run lint
22 | - name: TSC
23 | run: pnpm --filter @glow-xyz/solana-client run tsc
24 | - name: Test
25 | run: pnpm --filter @glow-xyz/solana-client run test
26 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dotenv environment variables file (build for Zeit Now)
2 | .env
3 | .env.build
4 | !zoom-web/.env
5 |
6 | # Dependency directories
7 | node_modules/
8 |
9 | # Logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | lerna-debug.log
14 |
15 | # Coverage
16 | **/coverage
17 |
18 | # IDEs
19 | .idea
20 | .vscode
21 |
22 | .DS_Store
23 |
24 | .now
25 |
26 | next/.env.local
27 | .env.local
28 |
29 | postgres
30 | .docker
31 |
32 | .vercel
33 |
34 | tsconfig.tsbuildinfo
35 | dump.rdb
36 |
37 | # markdown editor
38 | .obsidian
39 |
40 | dist/
41 |
42 | .turbo
43 | build/**
44 | dist/**
45 | .next/**
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Glow JS
2 |
3 | This is a set of packages that helps you integrate the [Glow Solana Wallet](https://glow.app) into your dApp. 🤩
4 |
5 | ## Video Overview
6 |
7 | [](https://www.loom.com/share/837a218eca284292a5c69d719564ed9d)
8 |
9 | ## Packages
10 |
11 | Here is a set of packages that you can use to integrate Glow onto your dApp:
12 |
13 | - `@glow-xyz/glow-client` - this gives you a `GlowClient` instance which interacts with the Glow Chrome Extension
14 | - `@glow-xyz/glow-react` - this gives you a React Context provider `` that makes it easy to integrate Glow with React
15 | - `@glow-xyz/example-next-js` - this is a Next.js example which [you can see deployed here](https://glow-js.luma-dev.com/)
16 |
17 | We have made this as easy as possible, so you should be able to integrate Glow into your dApp in under 10 minutes.
18 |
19 | ## Developer Support
20 |
21 | You can join our Telegram chat here: [https://t.me/+-yjcsc1WStNiODA5](https://t.me/+-yjcsc1WStNiODA5)
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glow-js",
3 | "version": "1.0.0",
4 | "private": true,
5 | "devDependencies": {
6 | "prettier": "2.8.0",
7 | "release-it": "15.5.1",
8 | "typescript": "5.6.3"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/packages/example-next-js/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/packages/example-next-js/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
--------------------------------------------------------------------------------
/packages/example-next-js/README.md:
--------------------------------------------------------------------------------
1 | # Example Next.js
2 |
3 | This is an example of how you can use Next.js to integrate with Glow.
4 |
5 | ## Example on Production
6 |
7 | You can check this deployed on production here: https://glow-js.luma-dev.com/
8 |
9 | 
10 |
11 |
12 | ## Running Locally
13 |
14 | First, run the development server:
15 |
16 | ```bash
17 | pnpm dev
18 | ```
19 |
20 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
21 |
22 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
23 |
24 |
--------------------------------------------------------------------------------
/packages/example-next-js/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/packages/example-next-js/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | module.exports = nextConfig
7 |
--------------------------------------------------------------------------------
/packages/example-next-js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@glow-xyz/example-next-js",
3 | "version": "0.6.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@glow-xyz/glow-client": "1.5.x",
13 | "@glow-xyz/glow-react": "1.1.x",
14 | "@popperjs/core": "2.11.6",
15 | "bootstrap": "5.2.3",
16 | "classnames": "2.3.2",
17 | "next": "12.3.1",
18 | "react": "18.3.1",
19 | "react-dom": "18.3.1"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^18.11.10",
23 | "@types/node-fetch": "^2.6.2",
24 | "@types/react": "^18.3.12",
25 | "@types/react-dom": "^18.3.1",
26 | "eslint": "^8.29.0",
27 | "eslint-config-next": "^13.0.6",
28 | "sass": "^1.56.1",
29 | "typescript": "^5.6.3"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/packages/example-next-js/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import "@glow-xyz/glow-react/dist/styles.css";
2 | import { GlowProvider } from "@glow-xyz/glow-react";
3 | import type { AppProps } from "next/app";
4 |
5 | function MyApp({ Component, pageProps }: AppProps) {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
13 | export default MyApp;
14 |
--------------------------------------------------------------------------------
/packages/example-next-js/pages/buttons.tsx:
--------------------------------------------------------------------------------
1 | import "bootstrap/dist/css/bootstrap.css";
2 | import type { NextPage } from "next";
3 | import { GlowSignInButton } from "@glow-xyz/glow-react";
4 | import styles from "../styles/Buttons.module.scss";
5 |
6 | const ButtonsExample: NextPage = () => {
7 | return (
8 |
9 |
10 |
Black
11 |
Purple
12 |
White Outline
13 |
White Naked
14 |
15 |
Large Squared
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Medium Squared
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
Small Squared
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
Large Rounded
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
Medium Rounded
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
Small Rounded
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | );
125 | };
126 |
127 | export default ButtonsExample;
128 |
--------------------------------------------------------------------------------
/packages/example-next-js/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { GlowSignInButton, useGlowContext } from "@glow-xyz/glow-react";
2 | import "bootstrap/dist/css/bootstrap.css";
3 | import type { NextPage } from "next";
4 | import Head from "next/head";
5 | import Image from "next/image";
6 | import styles from "../styles/Home.module.css";
7 |
8 | const Home: NextPage = () => {
9 | const { user, signOut } = useGlowContext();
10 |
11 | return (
12 |
13 |
14 |
Glow SDK Example
15 |
16 |
17 |
18 |
19 |
20 | Welcome to Glow
21 |
22 |
23 |
24 | {user ? (
25 |
26 | Signed in as
27 |
28 | {user.address}
29 |
30 |
31 | ) : (
32 |
Not signed in.
33 | )}
34 |
35 |
36 |
37 | {user ? (
38 |
47 | ) : (
48 |
49 | )}
50 |
51 |
52 |
53 |
65 |
66 | );
67 | };
68 |
69 | export default Home;
70 |
--------------------------------------------------------------------------------
/packages/example-next-js/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/glow-xyz/glow-js/e5b4e93f519c58f5f574322874208982cd8aa9b2/packages/example-next-js/public/favicon.ico
--------------------------------------------------------------------------------
/packages/example-next-js/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/example-next-js/styles/Buttons.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | align-items: flex-start;
3 | background: #ebebeb;
4 | display: flex;
5 | min-height: 100vh;
6 | padding: 2rem;
7 | }
8 |
9 | .buttons {
10 | display: grid;
11 | grid-gap: 1.5rem 2rem;
12 | grid-template-columns: repeat(4, 1fr);
13 | margin: 0 auto;
14 |
15 | h2 {
16 | font-size: 1.8rem;
17 | }
18 |
19 | h3 {
20 | font-size: 1.4rem;
21 | grid-column: span 4;
22 | }
23 | }
24 |
25 | .button-group {
26 | > *:not(:last-child) {
27 | margin-bottom: 0.5rem;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/example-next-js/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | align-items: center;
13 | }
14 |
15 | .footer {
16 | display: flex;
17 | flex: 1;
18 | padding: 2rem 0;
19 | border-top: 1px solid #eaeaea;
20 | justify-content: center;
21 | align-items: center;
22 | }
23 |
24 | .footer a {
25 | display: flex;
26 | justify-content: center;
27 | align-items: center;
28 | flex-grow: 1;
29 | }
30 |
31 | .title a {
32 | color: #0070f3;
33 | text-decoration: none;
34 | }
35 |
36 | .title a:hover,
37 | .title a:focus,
38 | .title a:active {
39 | text-decoration: underline;
40 | }
41 |
42 | .title {
43 | margin: 0;
44 | line-height: 1.15;
45 | font-size: 4rem;
46 | }
47 |
48 | .title,
49 | .description {
50 | text-align: center;
51 | }
52 |
53 | .description {
54 | margin: 4rem 0;
55 | line-height: 1.5;
56 | font-size: 1.5rem;
57 | }
58 |
59 | .code {
60 | background: #fafafa;
61 | border-radius: 5px;
62 | padding: 0.75rem;
63 | font-size: 1.1rem;
64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
65 | Bitstream Vera Sans Mono, Courier New, monospace;
66 | }
67 |
68 | .grid {
69 | display: flex;
70 | align-items: center;
71 | justify-content: center;
72 | flex-wrap: wrap;
73 | max-width: 800px;
74 | }
75 |
76 | .card {
77 | margin: 1rem;
78 | padding: 1.5rem;
79 | text-align: left;
80 | color: inherit;
81 | text-decoration: none;
82 | border: 1px solid #eaeaea;
83 | border-radius: 10px;
84 | transition: color 0.15s ease, border-color 0.15s ease;
85 | max-width: 300px;
86 | }
87 |
88 | .card:hover,
89 | .card:focus,
90 | .card:active {
91 | color: #0070f3;
92 | border-color: #0070f3;
93 | }
94 |
95 | .card h2 {
96 | margin: 0 0 1rem 0;
97 | font-size: 1.5rem;
98 | }
99 |
100 | .card p {
101 | margin: 0;
102 | font-size: 1.25rem;
103 | line-height: 1.5;
104 | }
105 |
106 | .logo {
107 | height: 1em;
108 | margin-left: 0.5rem;
109 | }
110 |
111 | @media (max-width: 600px) {
112 | .grid {
113 | width: 100%;
114 | flex-direction: column;
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/packages/example-next-js/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es6",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": false,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true
17 | },
18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
19 | "exclude": ["node_modules"]
20 | }
21 |
--------------------------------------------------------------------------------
/packages/glow-client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": ["lodash"],
4 | "rules": {
5 | "curly": "error",
6 | "lodash/import-scope": [2, "method"],
7 | "no-restricted-globals": ["error", "name", "event", "origin", "status"],
8 | "prefer-const": [
9 | "error",
10 | {
11 | "destructuring": "all"
12 | }
13 | ],
14 | "no-console": 0
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/glow-client/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "commitMessage": "[glow-client]: Release v${version}",
4 | "tagName": "glow-client-v${version}"
5 | },
6 | "github": {
7 | "release": true,
8 | "releaseName": "`@glow-xyz/glow-client` v${version}`"
9 | },
10 | "npm": {
11 | "access": "public"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/glow-client/README.md:
--------------------------------------------------------------------------------
1 | # `@glow-xyz/glow-client`
2 |
3 | The `@glow-xyz/glow-client` gives you a client to interact with the Glow Desktop and Safari Extension from your website or
4 | dApp.
5 |
6 | If you're building a website that interacts with Solana, you can use the `@glow-xyz/glow-client` to ask the user to:
7 |
8 | - connect their Solana wallet
9 | - sign messages
10 | - approve transactions
11 |
12 |
13 | ## Installing
14 |
15 | ```sh
16 | # npm
17 | npm install @glow-xyz/glow-client
18 |
19 | # yarn
20 | yarn add @glow-xyz/glow-client
21 |
22 | # pnpm
23 | pnpm install @glow-xyz/glow-client
24 | ```
25 |
26 | ## Usage
27 |
28 | ```ts
29 | import { GlowClient } from "@glow-xyz/glow-client";
30 |
31 | const glowClient = new GlowClient();
32 |
33 | glowClient.on("update", () => {
34 | console.log("Current address", glowClient.address);
35 | });
36 |
37 | // Connect
38 | const { address } = await glowClient.connect();
39 |
40 | // Sign Message
41 | const { signature_base64: sig1 } = await glowClient.signMessage({
42 | message_utf8: 'Hi this is a message!'
43 | });
44 | const { signature_base64: sig2 } = await glowClient.signMessage({
45 | message_hex: 'deadbeef' // You can specify different message formats
46 | });
47 |
48 | // Sign Transaction
49 | // Transaction from @solana/web3.js
50 | const transaction = Transaction.from(Buffer.from(str, 'base64'));
51 | await glowClient.signTransaction({
52 | transaction,
53 | network: Solana.Network.Devnet,
54 | });
55 | ```
56 |
57 | ## Differences with Existing Wallets
58 |
59 | **Setting the Network**
60 |
61 | Phantom and other wallets default to the Solana network chosen in the wallet's popup. This is a security vulnerability because it leads to simulations failing.
62 |
63 | With Glow, we let the dApp specify which `network` the transaction is for. So when you call `glowClient.signTransaction` you also pass in the `network` parameter.
64 |
65 | **Localnet**
66 |
67 | Unfortunately, we aren't able to support `Localnet` at this time because we make RPC calls from our backend, not the client. Our backend cannot connect to your local machine.
68 |
69 | ## Alternatives
70 |
71 | The most popular project in the ecosystem is the [`@solana-labs/wallet-adapter`](https://github.com/solana-labs/wallet-adapter) which is great if you want to support every wallet in the ecosystem.
72 |
73 | But if you want to just support Glow and have a lighter, easier API to work with, this is a useful library for you!
74 |
75 | We also plan on adding more fun methods to the Glow JS SDK that other wallets probably won't support. So those methods will be found here and not in the `@solana-labs/wallet-adapter`.
76 |
--------------------------------------------------------------------------------
/packages/glow-client/jest.config.js:
--------------------------------------------------------------------------------
1 | // Inspired by https://github.com/arcatdmz/nextjs-with-jest-typescript/blob/master/jest.config.js
2 | module.exports = {
3 | preset: "ts-jest/presets/js-with-ts",
4 | testEnvironment: "node",
5 | moduleFileExtensions: ["ts", "tsx", "js"],
6 | transform: {
7 | "^.+\\.(ts|tsx)$": [
8 | "ts-jest",
9 | {
10 | // This helps the tests run faster
11 | // But it skips typechecking, so that should be a different step on CI
12 | // https://huafu.github.io/ts-jest/user/config/isolatedModules
13 | isolatedModules: true,
14 | },
15 | ],
16 | },
17 | testMatch: ["**/__tests__/*.test.(ts|tsx)"],
18 | testPathIgnorePatterns: ["./dist", "./node_modules/"],
19 | collectCoverage: false,
20 | };
21 |
--------------------------------------------------------------------------------
/packages/glow-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@glow-xyz/glow-client",
3 | "version": "1.5.0",
4 | "description": "",
5 | "main": "dist/index.js",
6 | "typings": "dist/index.d.ts",
7 | "files": [
8 | "dist/**/*",
9 | "src/**/*"
10 | ],
11 | "sideEffects": false,
12 | "scripts": {
13 | "lint": "eslint . --ext ts --ext tsx --quiet",
14 | "tsc": "tsc --noEmit",
15 | "test": "jest",
16 | "build": "rimraf dist && tsc",
17 | "release": "pnpm build && release-it"
18 | },
19 | "dependencies": {
20 | "@glow-xyz/solana-client": "1.8.0",
21 | "bs58": "^5.0.0",
22 | "buffer": "^6.0.3",
23 | "eventemitter3": "^5.0.0",
24 | "luxon": "^3.0.4",
25 | "tweetnacl": "^1.0.3",
26 | "zod": "^3.19.1"
27 | },
28 | "devDependencies": {
29 | "@solana/web3.js": "^1.95.4",
30 | "@types/jest": "^29.2.3",
31 | "@types/luxon": "^3.1.0",
32 | "@typescript-eslint/parser": "^5.45.0",
33 | "esbuild": "^0.15.17",
34 | "esbuild-register": "^3.4.1",
35 | "eslint": "^8.29.0",
36 | "eslint-plugin-lodash": "^7.4.0",
37 | "jest": "^29.3.1",
38 | "prettier": "^2.8.0",
39 | "rimraf": "^3.0.2",
40 | "ts-jest": "^29.0.3",
41 | "typescript": "^5.6.3"
42 | },
43 | "private": false,
44 | "license": "ISC"
45 | }
46 |
--------------------------------------------------------------------------------
/packages/glow-client/src/__tests__/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { GPublicKey, GKeypair, GTransaction } from "@glow-xyz/solana-client";
2 | import bs58 from "bs58";
3 | import { Buffer } from "buffer";
4 | import { DateTime, Duration } from "luxon";
5 | import nacl from "tweetnacl";
6 | import {
7 | constructSignInMessage,
8 | constructSignInTx,
9 | NOTE_PROGRAM,
10 | verifySignature,
11 | verifySignIn,
12 | } from "../utils";
13 |
14 | describe("verifySignature", () => {
15 | test("confirms a valid signature", async () => {
16 | const message = "hi";
17 | const messageBuff = Buffer.from(message);
18 | const keypair = nacl.sign.keyPair();
19 | const signedMessage = nacl.sign.detached(messageBuff, keypair.secretKey);
20 |
21 | verifySignature({
22 | signature: Buffer.from(signedMessage).toString("base64"),
23 | message,
24 | signer: bs58.encode(keypair.publicKey),
25 | });
26 | });
27 |
28 | test("rejects an invalid signature", async () => {
29 | const keypair = nacl.sign.keyPair();
30 | await expect(async () => {
31 | verifySignature({
32 | signature: "ab",
33 | message: "hi",
34 | signer: bs58.encode(keypair.publicKey),
35 | });
36 | }).rejects.toThrow();
37 | });
38 | });
39 |
40 | describe("verifySignIn", () => {
41 | test("parses a localhost message", () => {
42 | const keypair = nacl.sign.keyPair();
43 | const expectedAddress = bs58.encode(keypair.publicKey);
44 |
45 | const _requestedAt = DateTime.now().toUTC().toISO();
46 | const message = `would like you to sign in with your Solana account:
47 | ${expectedAddress}
48 |
49 | Domain: localhost
50 | Requested At: ${_requestedAt}
51 | Nonce: 825`;
52 |
53 | const { appName, domain, address, nonce, requestedAt } = verifySignIn({
54 | signature: signMessage(message, keypair),
55 | message,
56 | expectedAddress,
57 | expectedDomain: "localhost",
58 | });
59 | expect(appName).toEqual("");
60 | expect(domain).toEqual("localhost");
61 | expect(address).toEqual(expectedAddress);
62 | expect(nonce).toEqual("825");
63 | expect(requestedAt.toISO()).toEqual(_requestedAt);
64 | });
65 |
66 | test("approves a valid sign in", () => {
67 | const keypair = nacl.sign.keyPair();
68 | const expectedAddress = bs58.encode(keypair.publicKey);
69 |
70 | const _requestedAt = DateTime.now().toUTC().toISO();
71 | const message = `Glow Wallet would like you to sign in with your Solana account:
72 | ${expectedAddress}
73 |
74 | Domain: glow.xyz
75 | Requested At: ${_requestedAt}
76 | Nonce: 869`;
77 |
78 | const { appName, domain, address, nonce, requestedAt } = verifySignIn({
79 | signature: signMessage(message, keypair),
80 | message,
81 | expectedAddress,
82 | expectedDomain: "glow.xyz",
83 | });
84 | expect(appName).toEqual("Glow Wallet");
85 | expect(domain).toEqual("glow.xyz");
86 | expect(address).toEqual(expectedAddress);
87 | expect(nonce).toEqual("869");
88 | expect(requestedAt.toISO()).toEqual(_requestedAt);
89 |
90 | verifySignIn({
91 | signature: signMessage(message, keypair),
92 | message,
93 | expectedAddress,
94 | expectedDomain: ["glow.xyz"],
95 | });
96 | });
97 |
98 | test("rejects a missing address", () => {
99 | const keypair = nacl.sign.keyPair();
100 | const expectedAddress = bs58.encode(keypair.publicKey);
101 |
102 | const _requestedAt = DateTime.now().toUTC().toISO();
103 | const message = `Glow Wallet would like you to sign in with your Solana account:
104 | ${expectedAddress}
105 |
106 | Domain: glow.xyz
107 | Requested At: ${_requestedAt}
108 | Nonce: 869`;
109 |
110 | expect(() => {
111 | verifySignIn({
112 | signature: signMessage(message, keypair),
113 | message,
114 | expectedAddress: "",
115 | expectedDomain: "glow.xyz",
116 | });
117 | }).toThrow();
118 |
119 | expect(() => {
120 | verifySignIn({
121 | signature: signMessage(message, keypair),
122 | message,
123 | expectedAddress: null as any,
124 | expectedDomain: "glow.xyz",
125 | });
126 | }).toThrow();
127 | });
128 |
129 | test("rejects an invalid address", () => {
130 | const keypair = nacl.sign.keyPair();
131 | const expectedAddress = bs58.encode(keypair.publicKey);
132 |
133 | const _requestedAt = DateTime.now().toUTC().toISO();
134 | const message = `Glow Wallet would like you to sign in with your Solana account:
135 | ${expectedAddress}
136 |
137 | Domain: glow.xyz
138 | Requested At: ${_requestedAt}
139 | Nonce: 869`;
140 |
141 | expect(() => {
142 | verifySignIn({
143 | signature: signMessage(message, keypair),
144 | message,
145 | expectedAddress: "636Lq2zGQDYZ3i6hahVcFWJkY6Jejndy5Qe4gBdukXDi",
146 | expectedDomain: "glow.xyz",
147 | });
148 | }).toThrow();
149 | });
150 |
151 | test("rejects an invalid domain", () => {
152 | const keypair = nacl.sign.keyPair();
153 | const expectedAddress = bs58.encode(keypair.publicKey);
154 |
155 | const _requestedAt = DateTime.now().toUTC().toISO();
156 | const message = `Glow Wallet would like you to sign in with your Solana account:
157 | ${expectedAddress}
158 |
159 | Domain: glow.xyz
160 | Requested At: ${_requestedAt}
161 | Nonce: 869`;
162 |
163 | expect(() => {
164 | verifySignIn({
165 | signature: signMessage(message, keypair),
166 | message,
167 | expectedAddress,
168 | expectedDomain: "espn.com-invalid",
169 | });
170 | }).toThrow();
171 | });
172 |
173 | test("rejects a missing domain", () => {
174 | const keypair = nacl.sign.keyPair();
175 | const expectedAddress = bs58.encode(keypair.publicKey);
176 |
177 | const _requestedAt = DateTime.now().toUTC().toISO();
178 | const message = `Glow Wallet would like you to sign in with your Solana account:
179 | ${expectedAddress}
180 |
181 | Domain: glow.xyz
182 | Requested At: ${_requestedAt}
183 | Nonce: 869`;
184 |
185 | expect(() => {
186 | verifySignIn({
187 | signature: signMessage(message, keypair),
188 | message,
189 | expectedAddress,
190 | expectedDomain: null as any,
191 | });
192 | }).toThrow();
193 |
194 | expect(() => {
195 | verifySignIn({
196 | signature: signMessage(message, keypair),
197 | message,
198 | expectedAddress,
199 | expectedDomain: ["glow.app"],
200 | });
201 | }).toThrow();
202 |
203 | expect(() => {
204 | verifySignIn({
205 | signature: signMessage(message, keypair),
206 | message,
207 | expectedAddress,
208 | expectedDomain: [],
209 | });
210 | }).toThrow();
211 | });
212 |
213 | test("rejects an old time", () => {
214 | const keypair = nacl.sign.keyPair();
215 | const expectedAddress = bs58.encode(keypair.publicKey);
216 |
217 | const _requestedAt = DateTime.now().minus({ days: 1 }).toUTC().toISO();
218 | const message = `Glow Wallet would like you to sign in with your Solana account:
219 | ${expectedAddress}
220 |
221 | Domain: glow.xyz
222 | Requested At: ${_requestedAt}
223 | Nonce: 869`;
224 |
225 | expect(() => {
226 | verifySignIn({
227 | signature: signMessage(message, keypair),
228 | message,
229 | expectedAddress,
230 | expectedDomain: "glow.xyz",
231 | });
232 | }).toThrow();
233 | });
234 |
235 | test("accepts an old time if configured so", () => {
236 | const keypair = nacl.sign.keyPair();
237 | const expectedAddress = bs58.encode(keypair.publicKey);
238 |
239 | const _requestedAt = DateTime.now().minus({ days: 1 }).toUTC().toISO();
240 | const message = `Glow Wallet would like you to sign in with your Solana account:
241 | ${expectedAddress}
242 |
243 | Domain: glow.xyz
244 | Requested At: ${_requestedAt}
245 | Nonce: 869`;
246 |
247 | expect(() => {
248 | verifySignIn({
249 | signature: signMessage(message, keypair),
250 | message,
251 | expectedAddress,
252 | expectedDomain: "glow.xyz",
253 | maxAllowedTimeDiffMs: Duration.fromObject({ days: 2 }).toMillis(),
254 | });
255 | }).not.toThrow();
256 | });
257 |
258 | test("rejects a future time", () => {
259 | const keypair = nacl.sign.keyPair();
260 | const expectedAddress = bs58.encode(keypair.publicKey);
261 |
262 | const _requestedAt = DateTime.now().plus({ days: 1 }).toUTC().toISO();
263 | const message = `Glow Wallet would like you to sign in with your Solana account:
264 | ${expectedAddress}
265 |
266 | Domain: glow.xyz
267 | Requested At: ${_requestedAt}
268 | Nonce: 869`;
269 |
270 | expect(() => {
271 | verifySignIn({
272 | signature: signMessage(message, keypair),
273 | message,
274 | expectedAddress,
275 | expectedDomain: "glow.xyz",
276 | });
277 | }).toThrow();
278 | });
279 |
280 | test("rejects an invalid signature", () => {
281 | const _requestedAt = DateTime.now().toUTC().toISO();
282 | const keypair = nacl.sign.keyPair();
283 | const expectedAddress = bs58.encode(keypair.publicKey);
284 | const message = `Glow Wallet would like you to sign in with your Solana account:
285 | ${expectedAddress}
286 |
287 | Domain: glow.xyz
288 | Requested At: ${_requestedAt}
289 | Nonce: 869`;
290 |
291 | expect(() => {
292 | verifySignIn({
293 | signature: signMessage(message, nacl.sign.keyPair()),
294 | message,
295 | expectedAddress,
296 | expectedDomain: "glow.xyz",
297 | });
298 | }).toThrow();
299 | });
300 |
301 | test("parses a signed transaction", () => {
302 | const keypair = nacl.sign.keyPair();
303 | const address = bs58.encode(keypair.publicKey);
304 |
305 | const { gtransaction, message } = constructSignInTx({
306 | address,
307 | appName: "Hi app",
308 | domain: "glow.app",
309 | signer: new GKeypair(keypair),
310 | });
311 |
312 | const {
313 | appName,
314 | domain,
315 | address: _address,
316 | } = verifySignIn({
317 | signed_transaction_base64: GTransaction.toBuffer({
318 | gtransaction,
319 | }).toString("base64"),
320 | message,
321 | expectedAddress: address,
322 | expectedDomain: "glow.app",
323 | });
324 | expect(appName).toEqual("Hi app");
325 | expect(domain).toEqual("glow.app");
326 | expect(address).toEqual(_address);
327 | });
328 |
329 | test("rejects a transaction with an invalid message", () => {
330 | const keypair = nacl.sign.keyPair();
331 | const address = bs58.encode(keypair.publicKey);
332 |
333 | const message = constructSignInMessage({
334 | appName: "Hi app",
335 | domain: "glow.app",
336 | address,
337 | nonce: Math.floor(Math.random() * 1000),
338 | requestedAt: DateTime.now(),
339 | });
340 |
341 | const gtransaction = GTransaction.create({
342 | instructions: [
343 | {
344 | accounts: [
345 | { address, signer: true },
346 | { address: GPublicKey.nullString, signer: true },
347 | ],
348 | program: NOTE_PROGRAM,
349 | data_base64: Buffer.from(message + "errrrrrrrr").toString("base64"),
350 | },
351 | ],
352 | latestBlockhash: GPublicKey.nullString,
353 | feePayer: GPublicKey.nullString,
354 | signers: [new GKeypair(keypair)],
355 | });
356 |
357 | expect(() => {
358 | verifySignIn({
359 | signed_transaction_base64: GTransaction.toBuffer({
360 | gtransaction,
361 | }).toString("base64"),
362 | message,
363 | expectedAddress: address,
364 | expectedDomain: "glow.app",
365 | });
366 | }).toThrow();
367 | });
368 |
369 | test("rejects an unsigned transaction", () => {
370 | const keypair = nacl.sign.keyPair();
371 | const address = bs58.encode(keypair.publicKey);
372 |
373 | const { gtransaction, message } = constructSignInTx({
374 | address,
375 | appName: "Hi app",
376 | domain: "glow.app",
377 | });
378 |
379 | expect(() => {
380 | verifySignIn({
381 | signed_transaction_base64: GTransaction.toBuffer({
382 | gtransaction,
383 | }).toString("base64"),
384 | message,
385 | expectedAddress: address,
386 | expectedDomain: "glow.app",
387 | });
388 | }).toThrow();
389 | });
390 | });
391 |
392 | const signMessage = (message: string, keypair: nacl.SignKeyPair): string => {
393 | const signedMessage = nacl.sign.detached(
394 | Buffer.from(message),
395 | keypair.secretKey
396 | );
397 | return Buffer.from(signedMessage).toString("base64");
398 | };
399 |
--------------------------------------------------------------------------------
/packages/glow-client/src/glow-client.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "buffer";
2 | import EventEmitter from "eventemitter3";
3 | import { verifySignIn } from "./utils";
4 | import { Address, GlowAdapter, Network, SolanaWindow } from "./window-types";
5 |
6 | export class GlowClient extends EventEmitter {
7 | public _address: Address | null;
8 | public _wallet: GlowAdapter | null;
9 |
10 | public eventNames() {
11 | return ["update"] as any;
12 | }
13 |
14 | constructor() {
15 | super();
16 |
17 | this._address = null;
18 | this._wallet = null;
19 |
20 | this.registerLoadedHandler();
21 | }
22 |
23 | private registerEventHandlers() {
24 | if (!this._wallet) {
25 | return;
26 | }
27 |
28 | this._wallet.on("disconnect", () => {
29 | this._address = null;
30 | this.emitUpdate("disconnect");
31 | });
32 |
33 | // TODO: is the emitted key interesting?
34 | this._wallet.on("accountChanged", async () => {
35 | try {
36 | const connectResp = await this._wallet?.connect({
37 | onlyIfTrusted: true,
38 | });
39 |
40 | if (connectResp?.publicKey) {
41 | this._address = connectResp.publicKey.toBase58();
42 | } else {
43 | this._address = null;
44 | }
45 | } catch {
46 | this._address = null;
47 | } finally {
48 | this.emitUpdate("accountChanged");
49 | }
50 | });
51 | }
52 |
53 | private emitUpdate(reason: string) {
54 | // TODO: think about how we should define event
55 | this.emit("update");
56 | }
57 |
58 | private registerLoadedHandler() {
59 | if (typeof window === "undefined") {
60 | return;
61 | }
62 |
63 | const onGlowLoaded = async () => {
64 | const _window = window as unknown as SolanaWindow;
65 | if (_window.glow) {
66 | clearInterval(glowLoadedInterval);
67 |
68 | this._wallet = _window.glow;
69 | this.registerEventHandlers();
70 |
71 | try {
72 | const { address } = await this._wallet.connect({
73 | onlyIfTrusted: true,
74 | });
75 |
76 | this._address = address;
77 | } catch {
78 | // We ignore this error since it's likely that the wallet isn't connected yet and isn't
79 | // worth throwing a runtime error.
80 | } finally {
81 | this.emitUpdate("loaded");
82 | }
83 | }
84 | };
85 |
86 | // Poll for the window.glow to be set since the extension loads
87 | // after the webpage loads.
88 | const glowLoadedInterval = setInterval(onGlowLoaded, 250);
89 |
90 | window.addEventListener("message", (event) => {
91 | if (event.data.__glow_loaded) {
92 | onGlowLoaded();
93 | }
94 | });
95 | }
96 |
97 | get address(): Address | null {
98 | return this._address;
99 | }
100 |
101 | async signIn(): Promise<{
102 | address: Address;
103 | message: string;
104 | signature: string;
105 | signedTransactionBase64: string | null;
106 | }> {
107 | if (!this._wallet) {
108 | throw new Error("Not loaded.");
109 | }
110 |
111 | const { address, message, signatureBase64, signedTransactionBase64 } =
112 | await this._wallet.signIn();
113 |
114 | this._address = address;
115 |
116 | verifySignIn({
117 | message,
118 | expectedAddress: address,
119 | expectedDomain: window.location.hostname,
120 | signature: signatureBase64,
121 | signed_transaction_base64: signedTransactionBase64,
122 | });
123 |
124 | return {
125 | address,
126 | signature: signatureBase64,
127 | message,
128 | signedTransactionBase64: signedTransactionBase64 ?? null,
129 | };
130 | }
131 |
132 | async connect(): Promise<{ address: Address }> {
133 | if (!this._wallet) {
134 | throw new Error("Not loaded.");
135 | }
136 |
137 | const { address } = await this._wallet.connect();
138 | this._address = address;
139 |
140 | return { address };
141 | }
142 |
143 | async disconnect(): Promise {
144 | await this._wallet?.disconnect();
145 | this._address = null;
146 | }
147 |
148 | async signTransaction({
149 | transactionBase64,
150 | network,
151 | }: {
152 | transactionBase64: string;
153 | network: Network;
154 | }): Promise<{ signedTransactionBase64: string }> {
155 | if (!this._wallet) {
156 | throw new Error("Not connected.");
157 | }
158 |
159 | const wallet = this._wallet;
160 |
161 | const { signedTransactionBase64 } = await wallet.signTransaction({
162 | transactionBase64,
163 | network,
164 | });
165 | return { signedTransactionBase64 };
166 | }
167 |
168 | async signAllTransactions({
169 | transactionsBase64,
170 | network,
171 | }: {
172 | transactionsBase64: string[];
173 | network: Network;
174 | }): Promise<{
175 | signedTransactionsBase64: string[];
176 | }> {
177 | if (!this._wallet) {
178 | throw new Error("Not connected.");
179 | }
180 |
181 | const { signedTransactionsBase64 } = await this._wallet.signAllTransactions(
182 | {
183 | transactionsBase64,
184 | network,
185 | }
186 | );
187 | return { signedTransactionsBase64 };
188 | }
189 |
190 | async signMessage(
191 | params:
192 | | {
193 | messageHex: string;
194 | }
195 | | {
196 | messageBase64: string;
197 | }
198 | | {
199 | messageUint8: Uint8Array;
200 | }
201 | | {
202 | messageBuffer: Buffer;
203 | }
204 | ): Promise<{ signatureBase64: string }> {
205 | if (!this._wallet) {
206 | throw new Error("Not connected.");
207 | }
208 |
209 | let messageBase64: string;
210 | if ("messageHex" in params) {
211 | messageBase64 = Buffer.from(params.messageHex, "hex").toString("base64");
212 | } else if ("messageBase64" in params) {
213 | messageBase64 = params.messageBase64;
214 | } else if ("messageBuffer" in params) {
215 | messageBase64 = Buffer.from(params.messageBuffer).toString("base64");
216 | } else if ("messageUint8" in params) {
217 | messageBase64 = Buffer.from(params.messageUint8).toString("base64");
218 | } else {
219 | throw new Error("No message passed in.");
220 | }
221 |
222 | const { signedMessageBase64 } = await this._wallet.signMessage({
223 | messageBase64,
224 | });
225 |
226 | return {
227 | signatureBase64: signedMessageBase64,
228 | };
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/packages/glow-client/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./glow-client";
2 | export * from "./utils";
3 | export * from "./window-types";
4 |
--------------------------------------------------------------------------------
/packages/glow-client/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { GPublicKey, GTransaction, GKeypair } from "@glow-xyz/solana-client";
2 | import bs58 from "bs58";
3 | import { Buffer } from "buffer";
4 | import { DateTime, Duration } from "luxon";
5 | import { sign } from "tweetnacl";
6 | import { Address } from "./window-types";
7 |
8 | const SIGN_IN_REGEX_STR =
9 | `^(?.{0,100}?)[ ]?would like you to sign in with your Solana account:
10 | (?[5KL1-9A-HJ-NP-Za-km-z]{32,44})
11 |
12 | Domain: (?[A-Za-z0-9.\\-]+)
13 | Requested At: (?.+)
14 | Nonce: (?[A-Za-z0-9\-\.]+)$`
15 | .split("\n")
16 | .map((s) => s.trim())
17 | .join("\n");
18 | const SIGN_IN_REGEX = new RegExp(SIGN_IN_REGEX_STR);
19 | const DEFAULT_MAX_ALLOWED_TIME_DIFF_MS = Duration.fromObject({
20 | minutes: 10,
21 | }).toMillis();
22 |
23 | /**
24 | * We take in either a signature or a signed transaction.
25 | *
26 | * The signed transaction option is useful for Ledger which does not support signing messages
27 | * directly.
28 | */
29 | export const verifySignIn = ({
30 | message,
31 | expectedDomain,
32 | expectedAddress,
33 | maxAllowedTimeDiffMs = DEFAULT_MAX_ALLOWED_TIME_DIFF_MS,
34 | ...params
35 | }: {
36 | message: string;
37 | expectedDomain: string | string[];
38 | expectedAddress: Address;
39 | /**
40 | * How long is a signature valid for, both ways.
41 | * Providing value of 10 minutes will cause signatures
42 | * from [t - 10 minutes, t + 10 minutes] to be considered valid.
43 | */
44 | maxAllowedTimeDiffMs?: number;
45 | } & (
46 | | {
47 | signature: string; // base64
48 | signed_transaction_base64?: string; // base64
49 | }
50 | | {
51 | signed_transaction_base64: string; // base64
52 | signature?: string; // base64
53 | }
54 | )): {
55 | appName: string;
56 | domain: string;
57 | address: Address;
58 | nonce: string;
59 | requestedAt: DateTime;
60 | } => {
61 | if (!expectedAddress) {
62 | throw new Error("Missing expected address.");
63 | }
64 |
65 | const match = message.match(SIGN_IN_REGEX);
66 |
67 | if (!match || !match.groups) {
68 | throw new Error("Invalid message format.");
69 | }
70 |
71 | const {
72 | appName,
73 | domain,
74 | address,
75 | nonce: _nonce,
76 | requestedAt: _requestedAt,
77 | } = match.groups;
78 | const nonce = _nonce;
79 | const requestedAt = DateTime.fromISO(_requestedAt).toUTC();
80 |
81 | if (Array.isArray(expectedDomain)) {
82 | if (expectedDomain.indexOf(domain) === -1) {
83 | throw new Error("Domain does not match expected domain.");
84 | }
85 | } else {
86 | if (expectedDomain !== domain) {
87 | throw new Error("Domain does not match expected domain.");
88 | }
89 | }
90 |
91 | if (expectedAddress !== address) {
92 | throw new Error("Address does not match expected address.");
93 | }
94 |
95 | const timeDiff = DateTime.now().diff(requestedAt);
96 | if (Math.abs(timeDiff.toMillis()) > maxAllowedTimeDiffMs) {
97 | throw new Error("Message is not recent.");
98 | }
99 |
100 | if (
101 | "signed_transaction_base64" in params &&
102 | typeof params.signed_transaction_base64 === "string"
103 | ) {
104 | const gtransaction = GTransaction.parse({
105 | buffer: Buffer.from(params.signed_transaction_base64!, "base64"),
106 | });
107 | const messageFromTx = Buffer.from(
108 | gtransaction.instructions[0].data_base64,
109 | "base64"
110 | ).toString("utf-8");
111 | if (messageFromTx !== message) {
112 | throw new Error(
113 | "The transaction message does not match the message passed in."
114 | );
115 | }
116 | const signature = gtransaction.signatures.find(
117 | (s) => s.address === address
118 | )!.signature!;
119 |
120 | verifySignature({
121 | signature: Buffer.from(bs58.decode(signature)).toString("base64"),
122 | messageBuffer: Buffer.from(gtransaction.messageBase64, "base64"),
123 | signer: address,
124 | });
125 | } else {
126 | verifySignature({ signature: params.signature!, message, signer: address });
127 | }
128 |
129 | return {
130 | appName,
131 | domain,
132 | address,
133 | nonce,
134 | requestedAt,
135 | };
136 | };
137 |
138 | export const constructSignInMessage = ({
139 | appName,
140 | domain,
141 | address,
142 | nonce,
143 | requestedAt,
144 | }: {
145 | appName: string | null;
146 | domain: string;
147 | address: string;
148 | nonce: number;
149 | requestedAt: DateTime;
150 | }): string => {
151 | const message = `${
152 | appName ?? domain
153 | } would like you to sign in with your Solana account:
154 | ${address}
155 |
156 | Domain: ${domain}
157 | Requested At: ${requestedAt.toUTC().toISO()}
158 | Nonce: ${nonce}`;
159 |
160 | return message
161 | .split("\n")
162 | .map((line) => line.trim())
163 | .join("\n");
164 | };
165 |
166 | export const constructSignInTx = ({
167 | address,
168 | appName,
169 | domain,
170 | signer,
171 | }: {
172 | address: string;
173 | appName: string;
174 | domain: string;
175 | signer?: GKeypair;
176 | }): { gtransaction: GTransaction.GTransaction; message: string } => {
177 | const message = constructSignInMessage({
178 | appName,
179 | domain,
180 | address,
181 | nonce: Math.floor(Math.random() * 1000),
182 | requestedAt: DateTime.now(),
183 | });
184 |
185 | const gtransaction = GTransaction.create({
186 | instructions: [
187 | {
188 | accounts: [
189 | { address, signer: true },
190 | { address: GPublicKey.nullString, signer: true },
191 | ],
192 | program: NOTE_PROGRAM,
193 | data_base64: Buffer.from(message).toString("base64"),
194 | },
195 | ],
196 | latestBlockhash: GPublicKey.nullString,
197 | feePayer: GPublicKey.nullString,
198 | signers: signer ? [signer] : undefined,
199 | });
200 |
201 | return { gtransaction, message };
202 | };
203 |
204 | export const NOTE_PROGRAM = "noteD9tEFTDH1Jn9B1HbpoC7Zu8L9QXRo7FjZj3PT93";
205 |
206 | export const verifySignature = ({
207 | signature,
208 | signer,
209 | ...params
210 | }: {
211 | signature: string; // base64
212 | signer: Address;
213 | } & (
214 | | {
215 | message: string;
216 | }
217 | | {
218 | messageBuffer: Buffer;
219 | }
220 | )) => {
221 | const signatureUint = new Uint8Array(Buffer.from(signature, "base64"));
222 | const addressUint = bs58.decode(signer);
223 |
224 | let messageUint: Uint8Array;
225 | if ("message" in params) {
226 | messageUint = new Uint8Array(Buffer.from(params.message));
227 | } else {
228 | messageUint = new Uint8Array(params.messageBuffer);
229 | }
230 |
231 | if (!sign.detached.verify(messageUint, signatureUint, addressUint)) {
232 | console.error("Problem verifying signature...");
233 | throw new Error("The Solana signature is invalid.");
234 | }
235 | };
236 |
--------------------------------------------------------------------------------
/packages/glow-client/src/window-types.ts:
--------------------------------------------------------------------------------
1 | import { GPublicKey } from "@glow-xyz/solana-client";
2 | import type { PublicKey, Transaction } from "@solana/web3.js";
3 | import EventEmitter from "eventemitter3";
4 | import { z } from "zod";
5 |
6 | export const AddressRegex = /^[5KL1-9A-HJ-NP-Za-km-z]{32,44}$/;
7 | export const AddressZ = z.string().regex(AddressRegex);
8 | export type Address = z.infer;
9 |
10 | export enum Network {
11 | Mainnet = "mainnet",
12 | Devnet = "devnet",
13 | Localnet = "localnet",
14 | }
15 |
16 | /**
17 | * This is based off of:
18 | * https://github.com/solana-labs/wallet-adapter/blob/master/packages/wallets/phantom/src/adapter.ts#L26
19 | *
20 | * We want to be compatible with Phantom's interface. For `window.glow` we want to expose a nicer API
21 | * for people to consume. For example, instead of `.connect` we will expose `glow.signIn` which will
22 | * sign a message that includes a nonce + the time / recent blockhash + the origin.
23 | */
24 | export interface PhantomWalletEvents {
25 | connect(...args: unknown[]): unknown;
26 | disconnect(...args: unknown[]): unknown;
27 | accountChanged(publicKey: PublicKey | null): unknown;
28 | }
29 |
30 | /**
31 | * The first version of the Glow API is compatible with Phantom's API to make it easier for dApps
32 | * to be compatible with Phantom and Glow.
33 | *
34 | * We plan on deviating from the Phantom API in `window.glow` which will offer an improved API
35 | * and better developer experience.
36 | */
37 | export interface PhantomAdapter extends EventEmitter {
38 | // Properties
39 | publicKey?: { toBytes(): Uint8Array; toBase58(): string } | null;
40 | isConnected: boolean;
41 |
42 | // Methods
43 | connect: (params?: {
44 | onlyIfTrusted: true;
45 | }) => Promise<{ publicKey: PublicKey | null }>;
46 | disconnect: () => Promise;
47 | signMessage: (
48 | message: Uint8Array
49 | ) => Promise<{ signature: Uint8Array | null }>;
50 | signTransaction: (
51 | transaction: Transaction,
52 | // The network parameter is not supported on Phantom
53 | network?: Network
54 | ) => Promise;
55 | signAllTransactions(
56 | transactions: Transaction[],
57 | // The network parameter is not supported on Phantom
58 | network?: Network
59 | ): Promise;
60 | }
61 |
62 | export interface GlowAdapter extends EventEmitter {
63 | address: Address | null;
64 | publicKey: GPublicKey | null;
65 |
66 | signIn: () => Promise<{
67 | address: Address;
68 | signatureBase64: string;
69 | message: string;
70 |
71 | signedTransactionBase64?: string; // This is useful for Ledger
72 | }>;
73 | connect: (params?: { onlyIfTrusted: true }) => Promise<{
74 | publicKey: PublicKey;
75 | address: Address;
76 | }>;
77 | disconnect: () => Promise;
78 | signOut: () => Promise;
79 | signMessage: (params: {
80 | messageBase64: string;
81 | }) => Promise<{ signedMessageBase64: string }>;
82 | signAndSendTransaction: (params: {
83 | transactionBase64: string;
84 | network: Network;
85 | waitForConfirmation?: boolean;
86 | }) => Promise<{ signature: string }>;
87 | signTransaction: (params: {
88 | transactionBase64: string;
89 | network: Network;
90 | }) => Promise<{ signature: string; signedTransactionBase64: string }>;
91 | signAllTransactions: (params: {
92 | transactionsBase64: string[];
93 | network: Network;
94 | }) => Promise<{ signedTransactionsBase64: string[] }>;
95 | }
96 |
97 | export interface SolanaWindow extends Window {
98 | solana: PhantomAdapter;
99 | glowSolana: PhantomAdapter;
100 | glow: GlowAdapter;
101 | }
102 |
--------------------------------------------------------------------------------
/packages/glow-client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "baseUrl": ".",
5 | "esModuleInterop": true,
6 | "incremental": true,
7 | "jsx": "react",
8 | "lib": ["dom", "dom.iterable", "esnext"],
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "noImplicitReturns": true,
12 | "outDir": "dist/",
13 | "declaration": true,
14 | "noUnusedLocals": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ES6"
19 | },
20 | "exclude": ["node_modules", "dist"],
21 | "include": [
22 | "src/**/*.ts*"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/packages/glow-react/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "commitMessage": "[glow-react]: Release v${version}",
4 | "tagName": "glow-react-v${version}"
5 | },
6 | "github": {
7 | "release": true,
8 | "releaseName": "`@glow-xyz/glow-react` v${version}`"
9 | },
10 | "npm": {
11 | "access": "public"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/glow-react/README.md:
--------------------------------------------------------------------------------
1 | # `@glow-xyz/glow-react`
2 |
3 | The `@glow-xyz/glow-react` gives you a React interface to hook up Glow with your dApp.
4 |
5 | ## Installing
6 |
7 | ```sh
8 | # npm
9 | npm install @glow-xyz/glow-react
10 |
11 | # yarn
12 | yarn add @glow-xyz/glow-react
13 |
14 | # pnpm
15 | pnpm install @glow-xyz/glow-react
16 | ```
17 |
18 | ## Usage
19 |
20 | ```tsx
21 | // Top level app component
22 | import { GlowSignInButton, GlowProvider } from "@glow-xyz/glow-react";
23 | import "@glow-xyz/glow-react/dist/styles.css";
24 |
25 | const App = ({children}) => {
26 | return (
27 |
28 | {children}
29 |
30 | )
31 | }
32 |
33 | // Component rendered under in the tree
34 | const Home = () => {
35 | const { user } = useGlowContext();
36 |
37 | return (
38 |
39 | {user ? (
40 |
Signed in as {user.address}
41 | ) : (
42 |
43 | )}
44 |
45 | )
46 | }
47 | ```
48 |
--------------------------------------------------------------------------------
/packages/glow-react/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@glow-xyz/glow-react",
3 | "version": "1.1.0",
4 | "main": "dist/index.js",
5 | "module": "dist/index.cjs.js",
6 | "typings": "dist/index.d.ts",
7 | "files": [
8 | "dist/**/*",
9 | "src/**/*"
10 | ],
11 | "sideEffects": false,
12 | "scripts": {
13 | "build": "npm run build:js && npm run build:css",
14 | "build:js": "tsc",
15 | "build:css": "sass src/styles/index.scss:dist/styles.css",
16 | "release": "pnpm build && release-it"
17 | },
18 | "dependencies": {
19 | "@glow-xyz/glow-client": "^1.5.0",
20 | "@glow-xyz/solana-client": "^1.8.0",
21 | "classnames": "2.3.2"
22 | },
23 | "devDependencies": {
24 | "@solana/web3.js": "1.95.4",
25 | "@types/react": "18.3.12",
26 | "@types/react-dom": "18.3.1",
27 | "esbuild": "0.15.17",
28 | "esbuild-register": "3.4.1",
29 | "prettier": "2.8.0",
30 | "sass": "1.56.1"
31 | },
32 | "peerDependencies": {
33 | "react": "^17.0.0 || ^18.0.0",
34 | "react-dom": "^17.0.0 || ^18.0.0"
35 | },
36 | "private": false
37 | }
38 |
--------------------------------------------------------------------------------
/packages/glow-react/src/GlowContext.tsx:
--------------------------------------------------------------------------------
1 | import { GlowClient } from "@glow-xyz/glow-client";
2 | import { Solana } from "@glow-xyz/solana-client";
3 | import { useOnMount } from "./hooks/useOnMount";
4 | import { usePolling } from "./hooks/usePolling";
5 | import React, { createContext, useCallback, useContext, useState } from "react";
6 | import { GlowAdapter, PhantomAdapter, Address } from "@glow-xyz/glow-client";
7 |
8 | type GlowUser = { address: Address };
9 |
10 | type GlowContext = {
11 | user: GlowUser | null;
12 |
13 | signIn: () => Promise<{
14 | wallet: Solana.Address;
15 | signatureBase64: string;
16 | message: string;
17 | }>;
18 | signOut: () => Promise;
19 |
20 | glowDetected: boolean;
21 |
22 | client: GlowClient;
23 | };
24 |
25 | export const GlowContext = createContext(null);
26 |
27 | export const glowClient = new GlowClient();
28 |
29 | declare global {
30 | interface Window {
31 | glow?: GlowAdapter;
32 | solana?: PhantomAdapter;
33 | glowSolana?: PhantomAdapter;
34 | }
35 | }
36 |
37 | export const GlowProvider = ({ children }: { children: React.ReactNode }) => {
38 | const [user, setUser] = useState(null);
39 | const [glowDetected, setGlowDetected] = useState(false);
40 |
41 | usePolling(
42 | () => {
43 | if (window.glow) {
44 | setGlowDetected(true);
45 | }
46 | },
47 | glowDetected ? null : 250,
48 | { runOnMount: true }
49 | );
50 |
51 | useOnMount(() => {
52 | glowClient.on("loaded", () => {
53 | setGlowDetected(true);
54 | });
55 | glowClient.on("update", () => {
56 | setUser(glowClient.address ? { address: glowClient.address } : null);
57 | setGlowDetected(true);
58 | });
59 | if (glowClient._wallet) {
60 | setUser(glowClient.address ? { address: glowClient.address } : null);
61 | setGlowDetected(true);
62 | }
63 | });
64 |
65 | const signIn = useCallback(async () => {
66 | try {
67 | const { address, signature, message } = await glowClient.signIn();
68 | setUser({ address });
69 | return { wallet: address, signatureBase64: signature, message };
70 | } catch (e) {
71 | console.error("Connecting Glow failed.");
72 | throw e;
73 | }
74 | }, [setUser]);
75 |
76 | const signOut = useCallback(async () => {
77 | await window.glowSolana!.disconnect();
78 | setUser(null);
79 | }, [setUser]);
80 |
81 | return (
82 |
91 | {children}
92 |
93 | );
94 | };
95 |
96 | export const useGlowContext = (): GlowContext => {
97 | const value = useContext(GlowContext);
98 |
99 | if (!value) {
100 | return {} as GlowContext;
101 | }
102 |
103 | return value as GlowContext;
104 | };
105 |
--------------------------------------------------------------------------------
/packages/glow-react/src/assets/GlowIcon.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const GlowIcon = (props: React.SVGProps) => (
4 |
19 | );
20 |
--------------------------------------------------------------------------------
/packages/glow-react/src/assets/GlowIcon3D.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const GlowIcon3D = (props: React.SVGProps) => (
4 |
77 | );
78 |
--------------------------------------------------------------------------------
/packages/glow-react/src/components/GlowSignInButton.tsx:
--------------------------------------------------------------------------------
1 | import { Solana } from "@glow-xyz/solana-client";
2 | import classNames from "classnames";
3 | import React from "react";
4 | import { useGlowContext } from "../GlowContext";
5 | import { GlowIcon } from "../assets/GlowIcon";
6 | import { GlowIcon3D } from "../assets/GlowIcon3D";
7 |
8 | type Props = StyledProps | RenderProps;
9 |
10 | type RenderProps = {
11 | render: (props: {
12 | glowDetected: boolean;
13 | signIn: () => Promise<{
14 | wallet: Solana.Address;
15 | signatureBase64: string;
16 | message: string;
17 | }>;
18 | }) => React.ReactNode;
19 | };
20 |
21 | type StyledProps = {
22 | size?: "lg" | "md" | "sm";
23 | shape?: "squared" | "rounded";
24 | variant?: "black" | "purple" | "white-naked" | "white-outline";
25 | } & Omit, "onClick" | "type" | "size">;
26 |
27 | export const GlowSignInButton = (props: Props) => {
28 | if ("render" in props) {
29 | return ;
30 | }
31 |
32 | return ;
33 | };
34 |
35 | const CustomGlowSignInButton = ({ render }: RenderProps) => {
36 | const { glowDetected, signIn } = useGlowContext();
37 | return <>{render({ glowDetected, signIn })}>;
38 | };
39 |
40 | const StyledGlowSignInButton = ({
41 | className,
42 | disabled: _disabled,
43 | size = "md",
44 | shape = "squared",
45 | variant = "black",
46 | ...props
47 | }: StyledProps) => {
48 | const { glowDetected, signIn } = useGlowContext();
49 |
50 | return (
51 |
79 | );
80 | };
81 |
--------------------------------------------------------------------------------
/packages/glow-react/src/hooks/useOnMount.tsx:
--------------------------------------------------------------------------------
1 | import { EffectCallback, useEffect } from "react";
2 |
3 | export const useOnMount = (callback: EffectCallback) => {
4 | useEffect(() => {
5 | return callback();
6 | // eslint-disable-next-line react-hooks/exhaustive-deps
7 | }, []);
8 | };
9 |
--------------------------------------------------------------------------------
/packages/glow-react/src/hooks/usePolling.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 | import { useOnMount } from "./useOnMount";
3 |
4 | // From here https://overreacted.io/making-setinterval-declarative-with-react-hooks/
5 | // If intervalMs is null, then we will not run the loop
6 | export const usePolling = (
7 | callback: () => void,
8 | intervalMs: number | null,
9 | { runOnMount }: { runOnMount: boolean } = { runOnMount: false }
10 | ) => {
11 | const savedCallback = useRef<() => void>(() => null);
12 |
13 | // Remember the latest callback.
14 | useEffect(() => {
15 | savedCallback.current = callback;
16 | }, [callback]);
17 |
18 | useOnMount(() => {
19 | if (runOnMount) {
20 | callback();
21 | }
22 | });
23 |
24 | // Set up the interval.
25 | useEffect(() => {
26 | function tick() {
27 | savedCallback.current();
28 | }
29 |
30 | if (intervalMs != null) {
31 | const id = setInterval(tick, intervalMs);
32 | return () => clearInterval(id);
33 | }
34 |
35 | return undefined;
36 | }, [intervalMs]);
37 | };
38 |
--------------------------------------------------------------------------------
/packages/glow-react/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./components/GlowSignInButton";
2 | export * from "./GlowContext";
3 |
--------------------------------------------------------------------------------
/packages/glow-react/src/styles/index.scss:
--------------------------------------------------------------------------------
1 | body {
2 | --glow--black: #151516;
3 | --glow--gray-dark: #505050;
4 | --glow--gray-light: #e2e2e2;
5 | --glow--gray-regular: #a4a4a4;
6 | --glow--puprple-gradient-end-rgb: 161, 38, 209;
7 | --glow--puprple-gradient-start-rgb: 209, 64, 221;
8 | --glow--purple-gradient-end: rgb(var(--glow--puprple-gradient-end-rgb));
9 | --glow--purple-gradient-start: rgb(var(--glow--puprple-gradient-start-rgb));
10 | --glow--white: #ffffff;
11 |
12 | --glow--font: "SF Pro", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
13 | Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
14 | }
15 |
16 | .glow--sign-in-button {
17 | align-items: center;
18 | border: 0;
19 | cursor: pointer;
20 | display: flex;
21 | flex-direction: row;
22 | justify-content: center;
23 | transition: 200ms all;
24 | position: relative;
25 |
26 | .glow--button-content {
27 | align-items: center;
28 | display: flex;
29 | position: absolute;
30 | }
31 |
32 | .glow--sign-in-button-text {
33 | font-family: var(--glow--font);
34 | letter-spacing: -0.02em;
35 | }
36 |
37 | .glow--icon-3d {
38 | // Optical alignment. The svg has some px in the bottom for the shadow.
39 | --shadow-size: 8px;
40 | margin-bottom: calc(var(--shadow-size) / -2);
41 | }
42 |
43 | // SIZES
44 | // =====
45 |
46 | &.glow--size-lg {
47 | min-width: 16.5rem;
48 | min-height: 3.125rem;
49 |
50 | .glow--icon {
51 | margin-right: 0.625rem;
52 | width: 1.25rem;
53 | height: 1.25rem;
54 |
55 | &.glow--icon-3d {
56 | width: 1.5625rem;
57 | height: 1.5625rem;
58 | }
59 | }
60 |
61 | .glow--sign-in-button-text {
62 | font-size: 18px;
63 | font-weight: 590;
64 | line-height: 21px;
65 | }
66 | }
67 |
68 | &.glow--size-md {
69 | min-width: 14.375rem;
70 | min-height: 2.5rem;
71 |
72 | .glow--icon {
73 | margin-right: 0.625rem;
74 | width: 1.125rem;
75 | height: 1.125rem;
76 |
77 | &.glow--icon-3d {
78 | width: 22.5px;
79 | height: 22.5px;
80 | }
81 | }
82 |
83 | .glow--sign-in-button-text {
84 | font-size: 15px;
85 | font-weight: 590;
86 | line-height: 18px;
87 | }
88 | }
89 |
90 | &.glow--size-sm {
91 | min-width: 11.5rem;
92 | min-height: 2rem;
93 |
94 | .glow--icon {
95 | margin-right: 0.625rem;
96 | width: 0.875rem;
97 | height: 0.875rem;
98 |
99 | &.glow--icon-3d {
100 | width: 17.5px;
101 | height: 17.5px;
102 | }
103 | }
104 |
105 | .glow--sign-in-button-text {
106 | font-size: 15px;
107 | font-weight: 500;
108 | line-height: 18px;
109 | }
110 | }
111 |
112 | // SHAPES
113 | // ======
114 |
115 | &.glow--shape-squared,
116 | &::before {
117 | border-radius: 0.5rem;
118 | }
119 |
120 | &.glow--shape-rounded {
121 | &.glow--size-lg,
122 | &.glow--size-lg::before {
123 | border-radius: 1.5625rem;
124 | }
125 |
126 | &.glow--size-md,
127 | &.glow--size-md::before {
128 | border-radius: 1.25rem;
129 | }
130 |
131 | &.glow--size-sm,
132 | &.glow--size-sm::before {
133 | border-radius: 1rem;
134 | }
135 | }
136 |
137 | // VARIANTS
138 | // ========
139 |
140 | &.glow--variant-black {
141 | background: var(--glow--black);
142 | color: var(--glow--white);
143 | }
144 |
145 | &.glow--variant-purple {
146 | background: linear-gradient(
147 | 93deg,
148 | var(--glow--purple-gradient-start) 1.51%,
149 | var(--glow--purple-gradient-end) 99.28%
150 | );
151 |
152 | &::before {
153 | background: linear-gradient(
154 | 93deg,
155 | rgba(var(--glow--puprple-gradient-start-rgb), 0.7) 1.51%,
156 | rgba(var(--glow--puprple-gradient-end-rgb), 0.7) 99.28%
157 | ),
158 | #ffffff;
159 | box-shadow: inset 0 0 44.9123px
160 | rgba(var(--glow--puprple-gradient-end-rgb), 0.7);
161 | content: "";
162 | position: absolute;
163 | top: 0;
164 | left: 0;
165 | bottom: 0;
166 | right: 0;
167 | opacity: 0;
168 | transition: opacity 200ms;
169 | }
170 |
171 | color: var(--glow--white);
172 | }
173 |
174 | &.glow--variant-white-naked {
175 | background: var(--glow--white);
176 | color: var(--glow--black);
177 | }
178 |
179 | &.glow--variant-white-outline {
180 | background: var(--glow--white);
181 | border: 1px solid var(--glow--black);
182 | color: var(--glow--black);
183 | }
184 |
185 | // STATES
186 | // ======
187 |
188 | &:not(:disabled) {
189 | &:hover,
190 | &:active {
191 | &.glow--variant-black {
192 | background: var(--glow--gray-dark);
193 | }
194 |
195 | &.glow--variant-purple {
196 | &::before {
197 | opacity: 1;
198 | }
199 | }
200 |
201 | &.glow--variant-white-naked {
202 | background: var(--glow--gray-light);
203 | }
204 |
205 | &.glow--variant-white-outline {
206 | border-color: var(--glow--gray-regular);
207 | }
208 | }
209 | }
210 |
211 | &:disabled {
212 | cursor: initial;
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/packages/glow-react/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "baseUrl": ".",
5 | "esModuleInterop": true,
6 | "incremental": true,
7 | "jsx": "react",
8 | "lib": ["dom", "dom.iterable", "esnext"],
9 | "module": "CommonJS",
10 | "moduleResolution": "node",
11 | "noImplicitReturns": true,
12 | "outDir": "dist/",
13 | "declaration": true,
14 | "noUnusedLocals": true,
15 | "resolveJsonModule": true,
16 | "skipLibCheck": true,
17 | "strict": true,
18 | "target": "ES2020"
19 | },
20 | "exclude": ["node_modules", "dist"],
21 | "include": ["src/**/*.ts*"]
22 | }
23 |
--------------------------------------------------------------------------------
/packages/solana-client/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "plugins": ["lodash"],
4 | "rules": {
5 | "curly": "error",
6 | "lodash/import-scope": [2, "method"],
7 | "no-restricted-globals": ["error", "name", "event", "origin", "status"],
8 | "prefer-const": [
9 | "error",
10 | {
11 | "destructuring": "all"
12 | }
13 | ],
14 | "no-console": 0
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/packages/solana-client/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "commitMessage": "[solana-client]: Release v${version}",
4 | "tagName": "solana-client-v${version}"
5 | },
6 | "github": {
7 | "release": true,
8 | "releaseName": "`@glow-xyz/solana-client` v${version}`"
9 | },
10 | "npm": {
11 | "access": "public"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/solana-client/README.md:
--------------------------------------------------------------------------------
1 | # `@glow-xyz/solana-client`
2 |
3 | The `@glow-xyz/solana-client` gives you a client to interact with the [Solana JSON RPC API](https://docs.solana.com/developing/clients/jsonrpc-api).
4 |
5 | This is a replacement for the [`Connection` object](https://solana-labs.github.io/solana-web3.js/classes/Connection.html) in the `@solana/web3.js` library.
6 |
7 | There are a few differences between this client and `@solana/web3.js`:
8 |
9 | - the types are a bit easier to use
10 | - the requests are less opinionated
11 | - coming soon
12 | - we can add middleware to track performance / handle errors
13 |
14 | ## Installing
15 |
16 | ```sh
17 | # npm
18 | npm install @glow-xyz/solana-client
19 |
20 | # yarn
21 | yarn add @glow-xyz/solana-client
22 |
23 | # pnpm
24 | pnpm install @glow-xyz/solana-client
25 | ```
26 |
--------------------------------------------------------------------------------
/packages/solana-client/jest.config.js:
--------------------------------------------------------------------------------
1 | // Inspired by https://github.com/arcatdmz/nextjs-with-jest-typescript/blob/master/jest.config.js
2 | module.exports = {
3 | preset: "ts-jest/presets/js-with-ts",
4 | testEnvironment: "node",
5 | moduleFileExtensions: ["ts", "tsx", "js"],
6 | transform: {
7 | "^.+\\.(ts|tsx)$": [
8 | "ts-jest",
9 | {
10 | // This helps the tests run faster
11 | // But it skips typechecking, so that should be a different step on CI
12 | // https://huafu.github.io/ts-jest/user/config/isolatedModules
13 | isolatedModules: true,
14 | },
15 | ],
16 | },
17 | testMatch: ["**/__tests__/*.test.(ts|tsx)"],
18 | testPathIgnorePatterns: ["./dist", "./node_modules/"],
19 | collectCoverage: false,
20 | };
21 |
--------------------------------------------------------------------------------
/packages/solana-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@glow-xyz/solana-client",
3 | "version": "1.8.0",
4 | "sideEffects": false,
5 | "typings": "dist/types/index.d.ts",
6 | "exports": {
7 | "import": "./dist/esm/index.js",
8 | "require": "./dist/cjs/index.js"
9 | },
10 | "module": "./dist/esm/index.js",
11 | "main": "./dist/cjs/index.js",
12 | "files": [
13 | "dist/**/*",
14 | "src/**/*"
15 | ],
16 | "scripts": {
17 | "lint": "eslint . --ext ts --ext tsx --quiet",
18 | "tsc": "tsc --noEmit",
19 | "test": "jest",
20 | "build": "rimraf dist && tsc -p tsconfig.json && tsc -p tsconfig.esm.json",
21 | "release": "pnpm build && release-it"
22 | },
23 | "publishConfig": {
24 | "access": "public"
25 | },
26 | "dependencies": {
27 | "@glow-xyz/beet": "0.6.0",
28 | "@noble/ed25519": "^1.7.1",
29 | "@noble/hashes": "^1.1.3",
30 | "axios": "^0.27.2",
31 | "bignumber.js": "^9.1.1",
32 | "bn.js": "^5.2.1",
33 | "bs58": "^5.0.0",
34 | "buffer": "^6.0.3",
35 | "js-sha256": "^0.9.0",
36 | "lodash": "^4.17.21",
37 | "luxon": "^3.0.4",
38 | "p-limit": "^3.0.1",
39 | "tweetnacl": "^1.0.3",
40 | "zod": "^3.19.1"
41 | },
42 | "devDependencies": {
43 | "@solana/web3.js": "^1.63.1",
44 | "@types/bn.js": "5.1.1",
45 | "@types/jest": "29.2.3",
46 | "@types/lodash": "4.14.191",
47 | "@types/luxon": "3.1.0",
48 | "@typescript-eslint/parser": "5.45.0",
49 | "esbuild": "0.15.17",
50 | "esbuild-register": "3.4.1",
51 | "eslint": "8.29.0",
52 | "eslint-plugin-lodash": "7.4.0",
53 | "jest": "29.3.1",
54 | "prettier": "2.8.0",
55 | "rimraf": "^3.0.2",
56 | "ts-jest": "29.0.3",
57 | "typescript": "5.6.3"
58 | },
59 | "private": false,
60 | "license": "ISC"
61 | }
62 |
--------------------------------------------------------------------------------
/packages/solana-client/src/EllipticCurve.ts:
--------------------------------------------------------------------------------
1 | import { sha256 } from "@noble/hashes/sha256";
2 | import { Buffer } from "buffer";
3 | import { Solana } from "./base-types";
4 | import { GPublicKey, PublicKeyInitData } from "./GPublicKey";
5 | import { isOnCurve as _isOnCurve } from "./utils/ed25519";
6 |
7 | const MAX_SEED_LENGTH = 32;
8 |
9 | /**
10 | * We pull these out to a different namespace as GPublicKey because the crypto libraries
11 | * from `@noble` import modern features that will break older browsers.
12 | */
13 | export namespace EllipticCurve {
14 | /**
15 | * Check that a pubkey is on the ed25519 curve.
16 | */
17 | export const isOnCurve = (pubkeyData: PublicKeyInitData): boolean => {
18 | const pubkey = new GPublicKey(pubkeyData);
19 | return _isOnCurve(pubkey.toBytes());
20 | };
21 |
22 | /**
23 | * Derive a program address from seeds and a program ID.
24 | */
25 | export const createProgramAddress = (
26 | seeds: Array,
27 | programId: Solana.Address
28 | ): Solana.Address => {
29 | let buffer = Buffer.alloc(0);
30 | seeds.forEach(function (seed) {
31 | if (seed.length > MAX_SEED_LENGTH) {
32 | throw new TypeError(`Max seed length exceeded`);
33 | }
34 | buffer = Buffer.concat([buffer, seed]);
35 | });
36 | buffer = Buffer.concat([
37 | buffer,
38 | new GPublicKey(programId).toBuffer(),
39 | Buffer.from("ProgramDerivedAddress"),
40 | ]);
41 | const publicKeyBytes = sha256(buffer);
42 | if (isOnCurve(publicKeyBytes)) {
43 | throw new Error(`Invalid seeds, address must fall off the curve`);
44 | }
45 | return new GPublicKey(publicKeyBytes).toBase58();
46 | };
47 |
48 | /**
49 | * Find a valid program address
50 | *
51 | * Valid program addresses must fall off the ed25519 curve. This function
52 | * iterates a nonce until it finds one that when combined with the seeds
53 | * results in a valid program address.
54 | */
55 | export const findProgramAddress = (
56 | seeds: Array,
57 | programId: Solana.Address
58 | ): [Solana.Address, number] => {
59 | let nonce = 255;
60 | let address: Solana.Address;
61 | while (nonce != 0) {
62 | try {
63 | const seedsWithNonce = seeds.concat(Buffer.from([nonce]));
64 | address = createProgramAddress(seedsWithNonce, programId);
65 | } catch (err) {
66 | if (err instanceof TypeError) {
67 | throw err;
68 | }
69 | nonce--;
70 | continue;
71 | }
72 | return [address, nonce];
73 | }
74 | throw new Error(`Unable to find a viable program address nonce`);
75 | };
76 |
77 | export const AddressRegex = /^[5KL1-9A-HJ-NP-Za-km-z]{32,44}$/;
78 |
79 | export const isValidAddress = (address: string): boolean => {
80 | if (!AddressRegex.test(address)) {
81 | return false;
82 | }
83 |
84 | try {
85 | new GPublicKey(address);
86 | return true;
87 | } catch {
88 | return false;
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/packages/solana-client/src/GKeypair.ts:
--------------------------------------------------------------------------------
1 | import nacl from "tweetnacl";
2 | import { Solana } from "./base-types";
3 | import { GPublicKey } from "./GPublicKey";
4 |
5 | interface Ed25519Keypair {
6 | publicKey: Uint8Array;
7 | secretKey: Uint8Array;
8 | }
9 |
10 | /**
11 | * An account keypair used for signing transactions.
12 | */
13 | export class GKeypair {
14 | private _keypair: Ed25519Keypair;
15 |
16 | /**
17 | * Create a new keypair instance.
18 | * Generate random keypair if no {@link Ed25519Keypair} is provided.
19 | *
20 | * @param keypair ed25519 keypair
21 | */
22 | constructor(keypair?: Ed25519Keypair) {
23 | if (keypair) {
24 | this._keypair = keypair;
25 | } else {
26 | this._keypair = nacl.sign.keyPair();
27 | }
28 | }
29 |
30 | static generate(): GKeypair {
31 | return new GKeypair(nacl.sign.keyPair());
32 | }
33 |
34 | /**
35 | * Create a keypair from a raw secret key byte array.
36 | *
37 | * This method should only be used to recreate a keypair from a previously
38 | * generated secret key. Generating keypairs from a random seed should be done
39 | * with the {@link Keypair.fromSeed} method.
40 | *
41 | * @throws error if the provided secret key is invalid
42 | */
43 | static fromSecretKey(secretKey: Uint8Array): GKeypair {
44 | const keypair = nacl.sign.keyPair.fromSecretKey(secretKey);
45 | const encoder = new TextEncoder();
46 | const signData = encoder.encode("@glow-xyz/solana-client-validation-v1");
47 | const signature = nacl.sign.detached(signData, keypair.secretKey);
48 | if (!nacl.sign.detached.verify(signData, signature, keypair.publicKey)) {
49 | throw new Error("provided secretKey is invalid");
50 | }
51 | return new GKeypair(keypair);
52 | }
53 |
54 | static fromSeed(seed: Uint8Array): GKeypair {
55 | return new GKeypair(nacl.sign.keyPair.fromSeed(seed));
56 | }
57 |
58 | get publicKey(): GPublicKey {
59 | return new GPublicKey(this._keypair.publicKey);
60 | }
61 |
62 | get address(): Solana.Address {
63 | return new GPublicKey(this._keypair.publicKey).toString();
64 | }
65 |
66 | get secretKey(): Uint8Array {
67 | return this._keypair.secretKey;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/packages/solana-client/src/GPublicKey.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This is a lighter version of the public key in `@solana/web3.js`
3 | */
4 | import BN from "bn.js";
5 | import bs58 from "bs58";
6 | import { Buffer } from "buffer";
7 |
8 | /**
9 | * Value to be converted into public key
10 | */
11 | export type PublicKeyInitData =
12 | | number
13 | | string
14 | | Buffer
15 | | Uint8Array
16 | | Array
17 | | PublicKeyData;
18 |
19 | /**
20 | * JSON object representation of GPublicKey class
21 | */
22 | export type PublicKeyData = {
23 | _bn: BN;
24 | };
25 |
26 | function isPublicKeyData(value: PublicKeyInitData): value is PublicKeyData {
27 | return (value as PublicKeyData)._bn !== undefined;
28 | }
29 |
30 | export class GPublicKey {
31 | private _bn: BN;
32 |
33 | /**
34 | * Create a new GPublicKey object
35 | * @param value ed25519 public key as buffer or base-58 encoded string
36 | */
37 | constructor(value: PublicKeyInitData) {
38 | if (isPublicKeyData(value)) {
39 | this._bn = value._bn;
40 | } else {
41 | if (typeof value === "string") {
42 | // assume base 58 encoding by default
43 | const decoded = bs58.decode(value);
44 | if (decoded.length != 32) {
45 | throw new Error(`Invalid public key input`);
46 | }
47 | this._bn = new BN(decoded);
48 | } else {
49 | this._bn = new BN(value);
50 | }
51 |
52 | if (this._bn.byteLength() > 32) {
53 | throw new Error(`Invalid public key input`);
54 | }
55 | }
56 | }
57 |
58 | static nullString: string = "11111111111111111111111111111111";
59 |
60 | static default: GPublicKey = new GPublicKey(GPublicKey.nullString);
61 |
62 | static byteLength: number = 32;
63 |
64 | equals(publicKey: GPublicKey): boolean {
65 | return this._bn.eq(publicKey._bn);
66 | }
67 |
68 | toBase58(): string {
69 | return bs58.encode(this.toBytes());
70 | }
71 |
72 | toJSON(): string {
73 | return this.toBase58();
74 | }
75 |
76 | toBytes(): Uint8Array {
77 | return new Uint8Array(this.toBuffer());
78 | }
79 |
80 | toBuffer(): Buffer {
81 | const b = this._bn.toArrayLike(Buffer);
82 | if (b.length === 32) {
83 | return b;
84 | }
85 |
86 | const zeroPad = Buffer.alloc(32);
87 | b.copy(zeroPad, 32 - b.length);
88 | return zeroPad;
89 | }
90 |
91 | toString(): string {
92 | return this.toBase58();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/packages/solana-client/src/GTransaction.ts:
--------------------------------------------------------------------------------
1 | import bs58 from "bs58";
2 | import { Buffer } from "buffer";
3 | import keyBy from "lodash/keyBy";
4 | import sortBy from "lodash/sortBy";
5 | import nacl from "tweetnacl";
6 | import { z } from "zod";
7 | import { Base58, Solana } from "./base-types";
8 | import { FixableGlowBorsh } from "./borsh/base";
9 | import { GlowBorshTypes } from "./borsh/GlowBorshTypes";
10 | import { TRANSACTION_MESSAGE } from "./borsh/transaction-borsh";
11 |
12 | export type Signer = { secretKey: Buffer | Uint8Array };
13 |
14 | /**
15 | * This is useful for manipulating existing transactions for a few reasons:
16 | *
17 | * 1. there are some bugs with web3.js that cause it to throw errors on valid transactions
18 | * 2. web3.js is heavy and is probably slower than this (not tested)
19 | * 3. web3.js is changing all of the time so a bug could easily be introduced in an update
20 | * 4. this returns a nicer data format for us to consume
21 | *
22 | * Note: this only lets you _parse, sign and serialize_ existing, valid transactions. It does not
23 | * allow modifying a transaction.
24 | */
25 | export namespace GTransaction {
26 | const SignatureZ = z.object({
27 | signature: z.string().nullable(), // base58
28 | address: Solana.AddressZ,
29 | });
30 |
31 | const AccountZ = z.object({
32 | address: Solana.AddressZ,
33 | signer: z.boolean(),
34 | writable: z.boolean(),
35 | });
36 |
37 | const InstructionZ = z.object({
38 | accounts: z.array(Solana.AddressZ),
39 | program: Solana.AddressZ,
40 | data_base64: z.string(),
41 | });
42 | export type Instruction = z.infer;
43 |
44 | // This is useful when creating a Transaction from instructions. Each instruction
45 | // requests that an account should be writable or a signer. But when we serialize the transaction
46 | // we just store information for an account if it's a signer or writable on *any* instruction.
47 | // Solana txs don't store information about which instruction requested the account to be a
48 | // signer or writable.
49 | const InstructionFactoryZ = z.object({
50 | accounts: z.array(
51 | z.object({
52 | address: Solana.AddressZ,
53 | writable: z.boolean().optional(),
54 | signer: z.boolean().optional(),
55 | })
56 | ),
57 | program: Solana.AddressZ,
58 | data_base64: z.string(),
59 | });
60 | export type InstructionFactory = z.infer;
61 |
62 | export const GTransactionZ = z.object({
63 | signature: z.string().nullable(),
64 | signatures: z.array(SignatureZ),
65 | accounts: z.array(AccountZ),
66 | latestBlockhash: z.string(), // Base58
67 | instructions: z.array(InstructionZ),
68 | messageBase64: z.string(),
69 | });
70 | export type GTransaction = Readonly>;
71 |
72 | export const create = ({
73 | instructions,
74 | latestBlockhash,
75 | feePayer,
76 | signers = [],
77 | suppressInvalidSignerError,
78 | }: {
79 | instructions: InstructionFactory[];
80 | latestBlockhash: string;
81 | feePayer?: string;
82 | signers?: Array;
83 | suppressInvalidSignerError?: boolean;
84 | }): GTransaction => {
85 | const accountMap: Record<
86 | Solana.Address,
87 | { writable: boolean; signer: boolean }
88 | > = {};
89 |
90 | for (const { accounts, program } of instructions) {
91 | const currentProgramVal = accountMap[program];
92 | accountMap[program] = {
93 | signer: Boolean(currentProgramVal?.signer),
94 | writable: Boolean(currentProgramVal?.writable),
95 | };
96 |
97 | for (const { signer, writable, address } of accounts) {
98 | const currentVal = accountMap[address];
99 | accountMap[address] = {
100 | signer: Boolean(currentVal?.signer || signer),
101 | writable: Boolean(currentVal?.writable || writable),
102 | };
103 | }
104 | }
105 |
106 | // Fee payer needs to always be writable and a signer
107 | // https://github.com/solana-labs/solana-web3.js/blob/2f80949da901e42d5f5565c44c3b3095ac024e67/src/transaction.ts#L428-L429
108 | if (feePayer) {
109 | accountMap[feePayer] = { signer: true, writable: true };
110 | }
111 |
112 | const unsortedAccounts = Object.entries(accountMap).map(
113 | ([address, { writable, signer }]) => ({ writable, signer, address })
114 | );
115 |
116 | const accounts = sortBy(
117 | unsortedAccounts,
118 | ({ signer, address, writable }) => {
119 | if (address === feePayer) {
120 | return [0, address];
121 | }
122 | if (signer && writable) {
123 | return [2, address];
124 | }
125 | if (signer) {
126 | return [3, address];
127 | }
128 | if (writable) {
129 | return [4, address];
130 | }
131 | return [5, address];
132 | }
133 | );
134 | const signerAccounts = accounts.filter((a) => a.signer);
135 | const signatures = signerAccounts.map((a) => ({
136 | address: a.address,
137 | signature: null,
138 | }));
139 |
140 | const messageBase64 = constructMessageBase64({
141 | instructions,
142 | accounts,
143 | latestBlockhash,
144 | });
145 |
146 | const gtransaction: GTransaction.GTransaction = {
147 | signature: null,
148 | signatures,
149 | accounts,
150 | latestBlockhash,
151 | messageBase64,
152 | instructions: instructions.map(({ accounts, program, data_base64 }) => ({
153 | program,
154 | data_base64,
155 | accounts: accounts.map((a) => a.address),
156 | })),
157 | };
158 |
159 | return Object.freeze(
160 | GTransaction.sign({
161 | signers,
162 | gtransaction,
163 | suppressInvalidSignerError,
164 | })
165 | );
166 | };
167 |
168 | export const sign = ({
169 | signers,
170 | gtransaction,
171 | suppressInvalidSignerError = false,
172 | }: {
173 | gtransaction: GTransaction;
174 | signers: Array;
175 | suppressInvalidSignerError?: boolean;
176 | }): GTransaction => {
177 | for (const { secretKey } of signers) {
178 | const keypair = nacl.sign.keyPair.fromSecretKey(secretKey);
179 | const address = bs58.encode(keypair.publicKey);
180 |
181 | if (gtransaction.signatures.every((sig) => sig.address !== address)) {
182 | // Skip to next signer if suppressing unknown signer
183 | if (suppressInvalidSignerError) {
184 | console.log(
185 | `Transaction did not require a signature from ${address}, skipping.`
186 | );
187 | continue;
188 | }
189 |
190 | throw new Error(
191 | `This transaction does not require a signature from: ${address}`
192 | );
193 | }
194 |
195 | const message = Buffer.from(gtransaction.messageBase64, "base64");
196 | const signatureUint = nacl.sign.detached(message, secretKey);
197 |
198 | gtransaction = GTransaction.addSignature({
199 | gtransaction,
200 | address,
201 | signature: Buffer.from(signatureUint),
202 | });
203 | }
204 |
205 | return gtransaction;
206 | };
207 |
208 | export const parse = ({ buffer }: { buffer: Buffer }): GTransaction => {
209 | const signaturesCoder =
210 | GlowBorshTypes.transactionSignaturesSection.toFixedFromData(buffer, 0);
211 | const sigs = signaturesCoder.read(buffer, 0);
212 |
213 | const messageBuffer = buffer.slice(signaturesCoder.byteSize);
214 | const {
215 | numReadonlySigned,
216 | numReadonlyUnsigned,
217 | numRequiredSigs,
218 | latestBlockhash,
219 | instructions: rawInstructions,
220 | addresses,
221 | } = TRANSACTION_MESSAGE.parse({ buffer: messageBuffer })!;
222 |
223 | const numAccounts = addresses.length;
224 |
225 | const accounts = addresses.map((address, idx) => ({
226 | address,
227 | signer: idx < numRequiredSigs,
228 | writable:
229 | idx < numRequiredSigs - numReadonlySigned ||
230 | (idx >= numRequiredSigs &&
231 | idx < addresses.length - numReadonlyUnsigned),
232 | }));
233 |
234 | const instructions: z.infer[] = rawInstructions.map(
235 | ({ programIdx, accountIdxs, data }) => ({
236 | program: addresses[programIdx],
237 | accounts: accountIdxs.map((idx) => {
238 | if (idx >= numAccounts) {
239 | throw new Error("Account not found.");
240 | }
241 | return addresses[idx];
242 | }),
243 | data_base64: data.toString("base64"),
244 | })
245 | );
246 |
247 | const signatures: Array<{
248 | signature: Base58;
249 | address: Solana.Address;
250 | }> = sigs.map((signature, idx) => ({
251 | signature,
252 | address: addresses[idx],
253 | }));
254 |
255 | return Object.freeze(
256 | GTransactionZ.parse({
257 | signature: signatures[0].signature,
258 | signatures,
259 | latestBlockhash,
260 | instructions,
261 | accounts,
262 | messageBase64: messageBuffer.toString("base64"),
263 | })
264 | );
265 | };
266 |
267 | export const addSignature = ({
268 | gtransaction,
269 | address,
270 | signature,
271 | }: {
272 | gtransaction: GTransaction;
273 | address: Solana.Address;
274 | signature: Buffer;
275 | }): GTransaction => {
276 | const accountIndex = gtransaction.accounts.findIndex(
277 | (account) => account.address === address
278 | );
279 |
280 | if (accountIndex < 0) {
281 | throw new Error(
282 | `This transaction does not require a signature from: ${address}`
283 | );
284 | }
285 |
286 | // Copy signatures map not to mutate original transaction
287 | const signatures = gtransaction.signatures.map((sig, index) => {
288 | return {
289 | address: sig.address,
290 | signature:
291 | index === accountIndex ? bs58.encode(signature) : sig.signature,
292 | };
293 | });
294 |
295 | return Object.freeze({
296 | ...gtransaction,
297 | signatures,
298 | signature: signatures[0].signature,
299 | });
300 | };
301 |
302 | export const toBuffer = ({
303 | gtransaction,
304 | }: {
305 | gtransaction: GTransaction;
306 | }): Buffer => {
307 | const messageBuffer = Buffer.from(gtransaction.messageBase64, "base64");
308 | const signaturesFixedBeet = FixableGlowBorsh.compactArray({
309 | itemCoder: GlowBorshTypes.signatureNullable,
310 | }).toFixedFromValue(
311 | gtransaction.signatures.map(({ signature }) => signature)
312 | );
313 | const txBufferSize =
314 | signaturesFixedBeet.byteSize + messageBuffer.byteLength;
315 |
316 | const txBuffer = Buffer.alloc(txBufferSize);
317 | signaturesFixedBeet.write(
318 | txBuffer,
319 | 0,
320 | gtransaction.signatures.map(({ signature }) => signature)
321 | );
322 | messageBuffer.copy(txBuffer, signaturesFixedBeet.byteSize);
323 |
324 | return txBuffer;
325 | };
326 |
327 | export const updateBlockhash = ({
328 | gtransaction,
329 | blockhash,
330 | }: {
331 | gtransaction: GTransaction;
332 | blockhash: string;
333 | }): GTransaction => {
334 | const {
335 | signature,
336 | signatures,
337 | accounts,
338 | instructions,
339 | messageBase64: _messageBase64,
340 | } = gtransaction;
341 |
342 | const messageData = TRANSACTION_MESSAGE.parse({
343 | base64: _messageBase64,
344 | });
345 |
346 | if (!messageData) {
347 | throw new Error("Problem parsing existing message");
348 | }
349 |
350 | return Object.freeze({
351 | signature,
352 | signatures,
353 | accounts,
354 | instructions,
355 | latestBlockhash: blockhash,
356 | messageBase64: TRANSACTION_MESSAGE.toBuffer({
357 | ...messageData,
358 | latestBlockhash: blockhash,
359 | }).toString("base64"),
360 | });
361 | };
362 |
363 | export const updateFeePayer = ({
364 | gtransaction,
365 | feePayer,
366 | }: {
367 | gtransaction: GTransaction;
368 | feePayer: Solana.Address;
369 | }): GTransaction => {
370 | const accountsMap = keyBy(gtransaction.accounts, "address");
371 | // Recreate transaction not to copy old feePayer
372 | const unsignedTransaction = create({
373 | instructions: gtransaction.instructions.map((ix) => ({
374 | accounts: ix.accounts.map((address) => ({
375 | address,
376 | signer: accountsMap[address].signer,
377 | writable: accountsMap[address].writable,
378 | })),
379 | data_base64: ix.data_base64,
380 | program: ix.program,
381 | })),
382 | latestBlockhash: gtransaction.latestBlockhash,
383 | feePayer,
384 | });
385 |
386 | let signedTransaction = unsignedTransaction;
387 | for (const { address, signature } of gtransaction.signatures) {
388 | if (isSignatureEmpty(signature)) {
389 | continue;
390 | }
391 | signedTransaction = addSignature({
392 | gtransaction: signedTransaction,
393 | address,
394 | signature: Buffer.from(bs58.decode(signature!)),
395 | });
396 | }
397 | return signedTransaction;
398 | };
399 |
400 | export const verifySignatures = ({
401 | gtransaction,
402 | suppressMissingSignatureError = false,
403 | }: {
404 | gtransaction: GTransaction;
405 | suppressMissingSignatureError?: boolean;
406 | }) => {
407 | const messageBuffer = Buffer.from(gtransaction.messageBase64, "base64");
408 |
409 | for (const { address, signature } of gtransaction.signatures) {
410 | if (isSignatureEmpty(signature)) {
411 | if (suppressMissingSignatureError) {
412 | continue;
413 | }
414 | throw new Error(`Missing signature from ${address}`);
415 | }
416 |
417 | if (
418 | !nacl.sign.detached.verify(
419 | messageBuffer,
420 | bs58.decode(signature!),
421 | bs58.decode(address)
422 | )
423 | ) {
424 | throw new Error(`The Solana signature is invalid (from ${address}).`);
425 | }
426 | }
427 | };
428 | }
429 |
430 | const constructMessageBase64 = ({
431 | instructions,
432 | accounts,
433 | latestBlockhash,
434 | }: {
435 | instructions: GTransaction.InstructionFactory[];
436 | accounts: GTransaction.GTransaction["accounts"];
437 | latestBlockhash: string;
438 | }): string => {
439 | const numRequiredSigs = accounts.filter((a) => a.signer).length;
440 | const numReadOnlySigs = accounts.filter(
441 | (a) => !a.writable && a.signer
442 | ).length;
443 | const numReadonlyUnsigned = accounts.filter(
444 | (a) => !a.writable && !a.signer
445 | ).length;
446 |
447 | const accountToInfo = Object.fromEntries(
448 | accounts.map(({ address, signer, writable }, idx) => {
449 | return [address, { signer, writable, idx }];
450 | })
451 | );
452 | const addresses = accounts.map((a) => a.address);
453 |
454 | const compiledInstructions = instructions.map(
455 | ({ program, accounts, data_base64 }) => {
456 | const { idx: programIdx } = accountToInfo[program];
457 | const accountIdxs = accounts.map((a) => accountToInfo[a.address].idx);
458 | const data = Buffer.from(data_base64, "base64");
459 | return {
460 | programIdx,
461 | accountIdxs,
462 | data,
463 | };
464 | }
465 | );
466 |
467 | const messageBuffer = TRANSACTION_MESSAGE.toBuffer({
468 | numReadonlySigned: numReadOnlySigs,
469 | latestBlockhash,
470 | numReadonlyUnsigned,
471 | numRequiredSigs,
472 | instructions: compiledInstructions,
473 | addresses,
474 | });
475 |
476 | return messageBuffer.toString("base64");
477 | };
478 |
479 | const isSignatureEmpty = (signature: Base58 | null) =>
480 | !signature || bs58.decode(signature).every((byte) => byte === 0);
481 |
--------------------------------------------------------------------------------
/packages/solana-client/src/__tests__/GPublicKey.test.ts:
--------------------------------------------------------------------------------
1 | import BN from "bn.js";
2 | import { Buffer } from "buffer";
3 | import { EllipticCurve } from "../EllipticCurve";
4 | import { GKeypair } from "../GKeypair";
5 | import { GPublicKey } from "../GPublicKey";
6 |
7 | describe("GPublicKey", function () {
8 | test("invalid", async () => {
9 | await expect(async () => {
10 | new GPublicKey([
11 | 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
12 | 0, 0, 0, 0, 0, 0, 0, 0, 0,
13 | ]);
14 | }).rejects.toThrow();
15 |
16 | await expect(async () => {
17 | new GPublicKey(
18 | "0x300000000000000000000000000000000000000000000000000000000000000000000"
19 | );
20 | }).rejects.toThrow();
21 |
22 | await expect(async () => {
23 | new GPublicKey(
24 | "0x300000000000000000000000000000000000000000000000000000000000000"
25 | );
26 | }).rejects.toThrow();
27 |
28 | await expect(async () => {
29 | new GPublicKey(
30 | "135693854574979916511997248057056142015550763280047535983739356259273198796800000"
31 | );
32 | }).rejects.toThrow();
33 |
34 | await expect(async () => {
35 | new GPublicKey("12345");
36 | }).rejects.toThrow();
37 | });
38 |
39 | test("equals", () => {
40 | const arrayKey = new GPublicKey([
41 | 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
42 | 0, 0, 0, 0, 0, 0, 0,
43 | ]);
44 | const base58Key = new GPublicKey(
45 | "CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"
46 | );
47 |
48 | expect(arrayKey.equals(base58Key)).toBeTruthy();
49 | });
50 |
51 | test("toBase58", () => {
52 | const key = new GPublicKey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
53 | expect(key.toBase58()).toBe("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
54 | expect(key.toString()).toBe("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
55 |
56 | const key2 = new GPublicKey("1111111111111111111111111111BukQL");
57 | expect(key2.toBase58()).toBe("1111111111111111111111111111BukQL");
58 | expect(key2.toString()).toBe("1111111111111111111111111111BukQL");
59 |
60 | const key3 = new GPublicKey("11111111111111111111111111111111");
61 | expect(key3.toBase58()).toBe("11111111111111111111111111111111");
62 |
63 | const key4 = new GPublicKey([
64 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
65 | 0, 0, 0, 0, 0, 0, 0,
66 | ]);
67 | expect(key4.toBase58()).toBe("11111111111111111111111111111111");
68 | });
69 |
70 | test("toJSON", () => {
71 | const key = new GPublicKey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
72 | expect(key.toJSON()).toBe("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
73 | expect(JSON.stringify(key)).toBe(
74 | '"CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"'
75 | );
76 | expect(JSON.stringify({ key })).toBe(
77 | '{"key":"CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3"}'
78 | );
79 | });
80 |
81 | test("toBuffer", () => {
82 | const key = new GPublicKey("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
83 | expect(key.toBuffer().byteLength).toBe(32);
84 | expect(key.toBase58()).toBe("CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3");
85 |
86 | const key2 = new GPublicKey("11111111111111111111111111111111");
87 | expect(key2.toBuffer().byteLength).toBe(32);
88 | expect(key2.toBase58()).toBe("11111111111111111111111111111111");
89 |
90 | const key3 = new GPublicKey(0);
91 | expect(key3.toBuffer().byteLength).toBe(32);
92 | expect(key3.toBase58()).toBe("11111111111111111111111111111111");
93 | });
94 |
95 | test("equals (II)", () => {
96 | const key1 = new GPublicKey([
97 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
98 | 0, 0, 0, 0, 0, 0, 1,
99 | ]);
100 | const key2 = new GPublicKey(key1.toBuffer());
101 |
102 | expect(key1.equals(key2)).toBeTruthy();
103 | });
104 |
105 | test("createProgramAddress", async () => {
106 | const programId = new GPublicKey(
107 | "BPFLoader1111111111111111111111111111111111"
108 | );
109 | const publicKey = new GPublicKey(
110 | "SeedPubey1111111111111111111111111111111111"
111 | );
112 |
113 | let programAddress = await EllipticCurve.createProgramAddress(
114 | [Buffer.from("", "utf8"), Buffer.from([1])],
115 | programId.toBase58()
116 | );
117 | expect(programAddress).toBe("3gF2KMe9KiC6FNVBmfg9i267aMPvK37FewCip4eGBFcT");
118 |
119 | programAddress = await EllipticCurve.createProgramAddress(
120 | [Buffer.from("☉", "utf8")],
121 | programId.toBase58()
122 | );
123 | expect(programAddress).toBe("7ytmC1nT1xY4RfxCV2ZgyA7UakC93do5ZdyhdF3EtPj7");
124 |
125 | programAddress = await EllipticCurve.createProgramAddress(
126 | [Buffer.from("Talking", "utf8"), Buffer.from("Squirrels", "utf8")],
127 | programId.toBase58()
128 | );
129 | expect(programAddress).toBe("HwRVBufQ4haG5XSgpspwKtNd3PC9GM9m1196uJW36vds");
130 |
131 | programAddress = await EllipticCurve.createProgramAddress(
132 | [publicKey.toBuffer()],
133 | programId.toString()
134 | );
135 | expect(programAddress).toBe("GUs5qLUfsEHkcMB9T38vjr18ypEhRuNWiePW2LoK4E3K");
136 |
137 | const programAddress2 = await EllipticCurve.createProgramAddress(
138 | [Buffer.from("Talking", "utf8")],
139 | programId.toBase58()
140 | );
141 | expect(programAddress).not.toBe(programAddress2);
142 |
143 | await expect(async () => {
144 | EllipticCurve.createProgramAddress(
145 | [Buffer.alloc(32 + 1)],
146 | programId.toBase58()
147 | );
148 | }).rejects.toThrow();
149 |
150 | // https://github.com/solana-labs/solana/issues/11950
151 | {
152 | const seeds = [
153 | new GPublicKey(
154 | "H4snTKK9adiU15gP22ErfZYtro3aqR9BTMXiH3AwiUTQ"
155 | ).toBuffer(),
156 | new BN(2).toArrayLike(Buffer, "le", 8),
157 | ];
158 | const programId = new GPublicKey(
159 | "4ckmDgGdxQoPDLUkDT3vHgSAkzA3QRdNq5ywwY4sUSJn"
160 | );
161 | programAddress = await EllipticCurve.createProgramAddress(
162 | seeds,
163 | programId.toBase58()
164 | );
165 | expect(programAddress).toBe(
166 | "12rqwuEgBYiGhBrDJStCiqEtzQpTTiZbh7teNVLuYcFA"
167 | );
168 | }
169 | });
170 |
171 | test("findProgramAddress", async () => {
172 | const programId = new GPublicKey(
173 | "BPFLoader1111111111111111111111111111111111"
174 | );
175 | const [programAddress, nonce] = EllipticCurve.findProgramAddress(
176 | [Buffer.from("", "utf8")],
177 | programId.toString()
178 | );
179 | expect(programAddress).toBe(
180 | EllipticCurve.createProgramAddress(
181 | [Buffer.from("", "utf8"), Buffer.from([nonce])],
182 | programId.toBase58()
183 | )
184 | );
185 | });
186 |
187 | test("isOnCurve", () => {
188 | const onCurve = GKeypair.generate().publicKey;
189 | expect(EllipticCurve.isOnCurve(onCurve.toBuffer())).toBeTruthy();
190 | expect(EllipticCurve.isOnCurve(onCurve.toBase58())).toBeTruthy();
191 | // A program address, yanked from one of the above tests. This is a pretty
192 | // poor test vector since it was created by the same code it is testing.
193 | // Unfortunately, I've been unable to find a golden negative example input
194 | // for curve25519 point decompression :/
195 | const offCurve = new GPublicKey(
196 | "12rqwuEgBYiGhBrDJStCiqEtzQpTTiZbh7teNVLuYcFA"
197 | );
198 | expect(EllipticCurve.isOnCurve(offCurve.toBuffer())).toBeFalsy();
199 | expect(EllipticCurve.isOnCurve(offCurve.toBase58())).toBeFalsy();
200 | });
201 | });
202 |
--------------------------------------------------------------------------------
/packages/solana-client/src/__tests__/__snapshots__/GTransaction.test.ts.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`GTransaction create with signing with obsolete signers 1`] = `"This transaction does not require a signature from: ATBeZdCvKyCbBnk27Mnd5RfcoFUNE5XfQYRTjveiS1cW"`;
4 |
5 | exports[`GTransaction sign with obsolete signers 1`] = `"This transaction does not require a signature from: ATBeZdCvKyCbBnk27Mnd5RfcoFUNE5XfQYRTjveiS1cW"`;
6 |
--------------------------------------------------------------------------------
/packages/solana-client/src/base-types.ts:
--------------------------------------------------------------------------------
1 | import z from "zod";
2 |
3 | export namespace Solana {
4 | export const AddressRegex = /^[5KL1-9A-HJ-NP-Za-km-z]{32,44}$/;
5 | export const AddressZ = z.string().regex(AddressRegex);
6 | export type Address = z.infer;
7 |
8 | export const SignatureRegex = /^[5KL1-9A-HJ-NP-Za-km-z]{85,90}$/;
9 | export const SignatureZ = z.string().regex(SignatureRegex);
10 | export type Signature = z.infer;
11 |
12 | export const SolAmountZ = z.object({ lamports: z.string() });
13 | export type SolAmount = z.infer;
14 |
15 | export const TokenAmountZ = z.object({ units: z.string() });
16 | export type TokenAmount = z.infer;
17 | }
18 |
19 | export type Base58 = string;
20 | export type Base64 = string;
21 | export type Hex = string;
22 |
--------------------------------------------------------------------------------
/packages/solana-client/src/borsh/CompactArray.ts:
--------------------------------------------------------------------------------
1 | import { FixedSizeBeet } from "@glow-xyz/beet";
2 | import { FixableBeet } from "@glow-xyz/beet";
3 | import { Buffer } from "buffer";
4 |
5 | // https://github.com/solana-labs/solana/blob/master/web3.js/src/util/shortvec-encoding.ts
6 | export namespace CompactArray {
7 | export const Borsh: FixableBeet = {
8 | description: "CompactArrayLength",
9 | toFixedFromValue(value: number): FixedSizeBeet {
10 | const { byteSize, buffer: bufferToInsert } = CompactArray.encodeLength({
11 | value,
12 | });
13 |
14 | return {
15 | byteSize,
16 | description: "CompactArrayLength",
17 | read: () => {
18 | return value;
19 | },
20 | write: (buffer, offset) => {
21 | bufferToInsert.copy(buffer, offset, 0);
22 | },
23 | };
24 | },
25 | toFixedFromData: (
26 | buffer: Buffer,
27 | offset: number
28 | ): FixedSizeBeet => {
29 | const { value: length, byteSize } = CompactArray.decodeLength({
30 | buffer,
31 | offset,
32 | });
33 |
34 | return {
35 | byteSize,
36 | description: "CompactArrayLength",
37 | read: () => {
38 | return length;
39 | },
40 | write: (buffer, offset, value) => {
41 | const { buffer: bufferToInsert } = CompactArray.encodeLength({
42 | value,
43 | });
44 | bufferToInsert.copy(buffer, offset, 0);
45 | },
46 | };
47 | },
48 | };
49 |
50 | export function decodeLength({
51 | buffer,
52 | offset,
53 | }: {
54 | buffer: Buffer;
55 | offset: number;
56 | }): {
57 | value: number;
58 | byteSize: number;
59 | } {
60 | let length = 0;
61 | let size = 0;
62 |
63 | for (;;) {
64 | const elem = buffer[offset + size];
65 |
66 | length |= (elem & 0x7f) << (size * 7);
67 | size += 1;
68 | if ((elem & 0x80) === 0) {
69 | break;
70 | }
71 | }
72 |
73 | return { value: length, byteSize: size };
74 | }
75 |
76 | export function encodeLength({ value }: { value: number }): {
77 | buffer: Buffer;
78 | byteSize: number;
79 | } {
80 | let rem_len = value;
81 | const bytes = [];
82 |
83 | for (;;) {
84 | let elem = rem_len & 0x7f;
85 | rem_len >>= 7;
86 | if (rem_len == 0) {
87 | bytes.push(elem);
88 | break;
89 | } else {
90 | elem |= 0x80;
91 | bytes.push(elem);
92 | }
93 | }
94 |
95 | return { buffer: Buffer.from(bytes), byteSize: bytes.length };
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/packages/solana-client/src/borsh/GlowBorshTypes.ts:
--------------------------------------------------------------------------------
1 | import * as beet from "@glow-xyz/beet";
2 | import { FixableBeet, FixedSizeBeet } from "@glow-xyz/beet";
3 | import bs58 from "bs58";
4 | import { Buffer } from "buffer";
5 | import { Base58 } from "../base-types";
6 | import { FixableGlowBorsh } from "./base";
7 |
8 | export namespace GlowBorshTypes {
9 | // Specifically for transaction signatures
10 | export const signature: FixedSizeBeet = {
11 | byteSize: 64,
12 | description: "Signature",
13 | read: function (buffer, offset) {
14 | const signatureLength = 64;
15 | const signatureBeet = beet.fixedSizeUint8Array(signatureLength);
16 |
17 | return bs58.encode(signatureBeet.read(buffer, offset));
18 | },
19 | write: function (buffer, offset, value) {
20 | const signatureLength = 64;
21 | const signatureBeet = beet.fixedSizeUint8Array(signatureLength);
22 |
23 | signatureBeet.write(buffer, offset, bs58.decode(value));
24 | },
25 | };
26 |
27 | export const signatureNullable: FixedSizeBeet =
28 | {
29 | byteSize: 64,
30 | description: "SignatureNullable",
31 | read: function (buffer, offset) {
32 | const signatureLength = 64;
33 | const signatureBeet = beet.fixedSizeUint8Array(signatureLength);
34 |
35 | const signatureArray = signatureBeet.read(buffer, offset);
36 | if (signatureArray.every((byte) => byte === 0)) {
37 | return null;
38 | }
39 | return bs58.encode(signatureArray);
40 | },
41 | write: function (buffer, offset, value) {
42 | const signatureLength = 64;
43 | const signatureBeet = beet.fixedSizeUint8Array(signatureLength);
44 |
45 | signatureBeet.write(
46 | buffer,
47 | offset,
48 | value ? bs58.decode(value) : Buffer.alloc(64)
49 | );
50 | },
51 | };
52 |
53 | export const transactionSignaturesSection: FixableBeet =
54 | FixableGlowBorsh.compactArray({ itemCoder: GlowBorshTypes.signature });
55 | }
56 |
--------------------------------------------------------------------------------
/packages/solana-client/src/borsh/__tests__/GlowBorsh.test.ts:
--------------------------------------------------------------------------------
1 | import { GlowBorsh } from "../base";
2 |
3 | describe("GlowBorshTypes", () => {
4 | test("GlowBorsh with discriminator", () => {
5 | const format = new GlowBorsh<{ ix: null }>({
6 | fields: [["ix", GlowBorsh.ixDiscriminator({ ix_name: "hi" })]],
7 | });
8 | const buffer = format.toBuffer({ ix: null });
9 | expect(buffer.toString("hex")).toEqual("798e81a33796e832");
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/packages/solana-client/src/borsh/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./GlowBorshTypes";
2 | export * from "./base";
3 | export * from "./transaction-borsh";
4 |
--------------------------------------------------------------------------------
/packages/solana-client/src/borsh/programs/token-program.ts:
--------------------------------------------------------------------------------
1 | import * as beet from "@glow-xyz/beet";
2 | import BN from "bn.js";
3 | import { Solana } from "../../base-types";
4 | import { GlowBorsh } from "../base";
5 |
6 | /**
7 | * https://github.com/solana-labs/solana-program-library/blob/810c79ec32c0f169d7f5a8e1eff0f3e23aa713a0/token/program/src/state.rs#L86
8 | *
9 | * This is an SPL Token Account.
10 | *
11 | * /// The mint associated with this account
12 | * pub mint: Pubkey,
13 | * /// The owner of this account.
14 | * pub owner: Pubkey,
15 | * /// The amount of tokens this account holds.
16 | * pub amount: u64,
17 | * /// If `delegate` is `Some` then `delegated_amount` represents
18 | * /// the amount authorized by the delegate
19 | * pub delegate: COption,
20 | * /// The account's state
21 | * pub state: AccountState,
22 | * /// If is_native.is_some, this is a native token, and the value logs the rent-exempt reserve. An
23 | * /// Account is required to be rent-exempt, so the value is used by the Processor to ensure that
24 | * /// wrapped SOL accounts do not drop below this threshold.
25 | * pub is_native: COption,
26 | * /// The amount delegated
27 | * pub delegated_amount: u64,
28 | * /// Optional authority to close the account.
29 | * pub close_authority: COption,
30 | *
31 | * So the token program is a little weird and it stores the optional part of COption as 4 bytes
32 | * rather than just a binary 1 / 0.
33 | * Ref: https://github.com/solana-labs/solana-program-library/blob/801b4e59f85d673864188be8f551674506bcd13d/token/program/src/state.rs#L273
34 | */
35 | export type TOKEN_ACCOUNT_DATA = {
36 | mint: Solana.Address;
37 | owner: Solana.Address;
38 | amount: Solana.TokenAmount;
39 | delegate_exists: number;
40 | delegate: Solana.Address;
41 | state: number;
42 | is_native_exists: number;
43 | is_native: BN;
44 | delegated_amount: BN;
45 | close_authority_exists: number;
46 | close_authority: Solana.Address;
47 | };
48 |
49 | export const TOKEN_ACCOUNT = new GlowBorsh({
50 | fields: [
51 | ["mint", GlowBorsh.address],
52 | ["owner", GlowBorsh.address],
53 | ["amount", GlowBorsh.tokenAmount],
54 | ["delegate_exists", beet.u32],
55 | ["delegate", GlowBorsh.address],
56 | ["state", beet.u8],
57 | ["is_native_exists", beet.u32],
58 | ["is_native", beet.u64],
59 | ["delegated_amount", beet.u64],
60 | ["close_authority_exists", beet.u32],
61 | ["close_authority", GlowBorsh.address],
62 | ],
63 | });
64 |
65 | /**
66 | * https://github.com/solana-labs/solana-program-library/blob/810c79ec32c0f169d7f5a8e1eff0f3e23aa713a0/token/program/src/state.rs#L13
67 | */
68 | export const SPL_MINT_ACCOUNT = new GlowBorsh<{
69 | mint_authority_exists: number;
70 | mint_authority: Solana.Address | null;
71 | supply: Solana.TokenAmount;
72 | decimals: number;
73 | is_initialized: boolean;
74 | freeze_authority_exists: number;
75 | freeze_authority: Solana.Address | null;
76 | }>({
77 | fields: [
78 | ["mint_authority_exists", beet.u32],
79 | ["mint_authority", GlowBorsh.addressNullable],
80 | ["supply", GlowBorsh.tokenAmount],
81 | ["decimals", beet.u8],
82 | ["is_initialized", beet.bool],
83 | ["freeze_authority_exists", beet.u32],
84 | ["freeze_authority", GlowBorsh.addressNullable],
85 | ],
86 | });
87 |
--------------------------------------------------------------------------------
/packages/solana-client/src/borsh/transaction-borsh.ts:
--------------------------------------------------------------------------------
1 | import * as beet from "@glow-xyz/beet";
2 | import { FixableBeet, FixedSizeBeet } from "@glow-xyz/beet";
3 | import { Buffer } from "buffer";
4 | import { Solana } from "../base-types";
5 | import { FixableGlowBorsh, GlowBorsh } from "./base";
6 |
7 | export type InstructionRawType = {
8 | programIdx: number;
9 | accountIdxs: number[];
10 | data: Buffer;
11 | };
12 |
13 | export const TransactionInstructionFormat: FixableBeet<
14 | InstructionRawType,
15 | InstructionRawType
16 | > = {
17 | description: "TransactionInstruction",
18 | toFixedFromValue: (
19 | ix: InstructionRawType
20 | ): FixedSizeBeet => {
21 | const accountsCoderFixable = FixableGlowBorsh.compactArray({
22 | itemCoder: beet.u8,
23 | });
24 | const accountsCoder = accountsCoderFixable.toFixedFromValue(ix.accountIdxs);
25 |
26 | const dataCoderFixable = FixableGlowBorsh.compactArray({
27 | itemCoder: beet.u8,
28 | });
29 | const dataCoder = dataCoderFixable.toFixedFromValue(Array.from(ix.data));
30 |
31 | const byteSize = 1 + accountsCoder.byteSize + dataCoder.byteSize;
32 |
33 | return {
34 | description: "TransactionInstruction",
35 | write: function (
36 | buff: Buffer,
37 | offset: number,
38 | ix: InstructionRawType
39 | ): void {
40 | let cursor = offset;
41 | beet.u8.write(buff, cursor, ix.programIdx);
42 | cursor += beet.u8.byteSize;
43 |
44 | accountsCoder.write(buff, cursor, ix.accountIdxs);
45 | cursor += accountsCoder.byteSize;
46 |
47 | dataCoder.write(buff, cursor, Array.from(ix.data));
48 | },
49 |
50 | read: function (buff: Buffer, offset: number): InstructionRawType {
51 | let cursor = offset;
52 | const programIdx = beet.u8.read(buff, cursor);
53 | cursor += beet.u8.byteSize;
54 |
55 | const accountIdxs = accountsCoder.read(buff, cursor);
56 | cursor += accountsCoder.byteSize;
57 |
58 | const data = dataCoder.read(buff, cursor);
59 | return { programIdx, accountIdxs, data: Buffer.from(data) };
60 | },
61 | byteSize,
62 | };
63 | },
64 | toFixedFromData: (
65 | buff: Buffer,
66 | offset: number
67 | ): FixedSizeBeet => {
68 | let cursor = offset + 1; // + 1 for the programIdx which is a u8
69 |
70 | const accountsCoderFixable = FixableGlowBorsh.compactArray({
71 | itemCoder: beet.u8,
72 | });
73 | const accountsCoder = accountsCoderFixable.toFixedFromData(buff, cursor);
74 | cursor += accountsCoder.byteSize;
75 |
76 | const dataCoderFixable = FixableGlowBorsh.compactArray({
77 | itemCoder: beet.u8,
78 | });
79 | const dataCoder = dataCoderFixable.toFixedFromData(buff, cursor);
80 |
81 | const byteSize = 1 + accountsCoder.byteSize + dataCoder.byteSize;
82 |
83 | return {
84 | description: "TransactionInstruction",
85 | write: function (
86 | buf: Buffer,
87 | offset: number,
88 | ix: InstructionRawType
89 | ): void {
90 | let cursor = offset;
91 | beet.u8.write(buff, cursor, ix.programIdx);
92 | cursor += beet.u8.byteSize;
93 |
94 | accountsCoder.write(buff, cursor, ix.accountIdxs);
95 | cursor += accountsCoder.byteSize;
96 |
97 | dataCoder.write(buff, cursor, Array.from(ix.data));
98 | },
99 |
100 | read: function (buf: Buffer, offset: number): InstructionRawType {
101 | let cursor = offset;
102 | const programIdx = beet.u8.read(buff, cursor);
103 | cursor += beet.u8.byteSize;
104 |
105 | const accountIdxs = accountsCoder.read(buff, cursor);
106 | cursor += accountsCoder.byteSize;
107 |
108 | const data = dataCoder.read(buff, cursor);
109 | return { programIdx, accountIdxs, data: Buffer.from(data) };
110 | },
111 | byteSize,
112 | };
113 | },
114 | };
115 |
116 | export const TRANSACTION_MESSAGE = new FixableGlowBorsh<{
117 | numRequiredSigs: number;
118 | numReadonlySigned: number;
119 | numReadonlyUnsigned: number;
120 | addresses: Solana.Address[];
121 | latestBlockhash: string;
122 | instructions: InstructionRawType[];
123 | }>({
124 | fields: [
125 | ["numRequiredSigs", beet.u8],
126 | ["numReadonlySigned", beet.u8],
127 | ["numReadonlyUnsigned", beet.u8],
128 | [
129 | "addresses",
130 | FixableGlowBorsh.compactArray({ itemCoder: GlowBorsh.address }),
131 | ],
132 | ["latestBlockhash", GlowBorsh.address],
133 | [
134 | "instructions",
135 | FixableGlowBorsh.compactArrayFixable({
136 | elemCoder: TransactionInstructionFormat,
137 | }),
138 | ],
139 | ],
140 | });
141 |
--------------------------------------------------------------------------------
/packages/solana-client/src/client/client-types.ts:
--------------------------------------------------------------------------------
1 | import {Buffer} from "buffer";
2 | import { DateTime } from "luxon";
3 | import { Solana } from "../base-types";
4 | import { GTransaction } from "../GTransaction";
5 |
6 | /**
7 | * Here are the types that SolanaClient exports, those that are consumed by
8 | * callers of SolanaClient.
9 | *
10 | * In some cases they may differ from SolanaRpcTypes, which are the raw,
11 | * unmodified types returned by the Solana RPC, or may be exactly the same
12 | * (for example, SolanaClientTypes.PerformanceSample and
13 | * SolanaRpcTypes.PerformanceSampleZ represent the same type as of the date when
14 | * this comment was written).
15 | *
16 | * In that case, we prefer to duplicate the type rather than infer it from zod
17 | * in order to be explicit and enable both types to evolve differently.
18 | */
19 | export namespace SolanaClientTypes {
20 | export type ParsedAccount = {
21 | parsed: true;
22 | pubkey: Solana.Address;
23 | /** `true` if this account's data contains a loaded program */
24 | executable: boolean;
25 | /** Identifier of the program that owns the account, this is the _program_ owner */
26 | owner: Solana.Address;
27 | lamports: number; // TODO: replace this with a string
28 | rentEpoch?: number;
29 |
30 | data: Data;
31 | buffer?: never;
32 | };
33 |
34 | export type ParsedAccountGeneric = ParsedAccount>;
35 |
36 | export type Account = {
37 | parsed: false;
38 | pubkey: Solana.Address;
39 | /** `true` if this account's data contains a loaded program */
40 | executable: boolean;
41 | /** Identifier of the program that owns the account, this is the _program_ owner */
42 | owner: Solana.Address;
43 | lamports: number; // TODO: replace this with a string
44 | rentEpoch?: number;
45 |
46 | buffer: Buffer;
47 | data?: never;
48 | };
49 |
50 | export type TransactionWithMeta = {
51 | slot: number | null;
52 | transaction: GTransaction.GTransaction;
53 | block_time: DateTime | null;
54 | meta: TransactionMeta | null;
55 | };
56 |
57 | export type TokenAccount = {
58 | address: Solana.Address;
59 | amount: string;
60 | decimals: number;
61 | uiAmountString: string;
62 | };
63 |
64 | export type TokenBalance = {
65 | accountIndex: number;
66 | mint: string;
67 | owner?: string;
68 | uiTokenAmount: {
69 | amount: string;
70 | decimals: number;
71 | };
72 | };
73 |
74 | export type TransactionMeta = {
75 | // TODO make this unknown to force callers of SolanaClient to parse this with zod
76 | err?: any;
77 | fee: number;
78 | preBalances: number[];
79 | postBalances: number[];
80 | innerInstructions?: Array<{
81 | index: number;
82 | instructions: Array;
83 | }>;
84 | preTokenBalances: Array | null;
85 | postTokenBalances: Array | null;
86 | logMessages: string[] | null;
87 | };
88 |
89 | export type PerformanceSample = {
90 | numSlots: number;
91 | numTransactions: number;
92 | samplePeriodSecs: number;
93 | slot: number;
94 | };
95 | }
96 |
--------------------------------------------------------------------------------
/packages/solana-client/src/client/error-codes.ts:
--------------------------------------------------------------------------------
1 | export enum SolanaRpcError {
2 | // https://github.com/solana-labs/solana/blob/3114c199bde434b47f255bb6fdd6492836fd9a45/client/src/rpc_custom_error.rs#L10-L24
3 | SERVER_ERROR_BLOCK_CLEANED_UP = -32001,
4 | SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE = -32002,
5 | SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE = -32003,
6 | SERVER_ERROR_BLOCK_NOT_AVAILABLE = -32004,
7 | SERVER_ERROR_NODE_UNHEALTHY = -32005,
8 | SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE = -32006,
9 | SERVER_ERROR_SLOT_SKIPPED = -32007,
10 | SERVER_ERROR_NO_SNAPSHOT = -32008,
11 | SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED = -32009,
12 | SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX = -32010,
13 | SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE = -32011,
14 | SCAN_ERROR = -32012,
15 | SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH = -32013,
16 | SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET = -32014,
17 | SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION = -32015,
18 |
19 | INVALID_PARAM = -32602,
20 | }
21 |
--------------------------------------------------------------------------------
/packages/solana-client/src/client/normalizers.ts:
--------------------------------------------------------------------------------
1 | import * as bs58 from "bs58";
2 | import { Buffer } from "buffer";
3 | import { DateTime } from "luxon";
4 | import { Solana } from "../base-types";
5 | import { GTransaction } from "../GTransaction";
6 | import { SolanaClientTypes } from "./client-types";
7 | import { SolanaRpcTypes } from "./rpc-types";
8 |
9 | export const normalizeRpcAccountWithPubkey = ({
10 | account,
11 | pubkey,
12 | }: SolanaRpcTypes.AccountWithPubkey): SolanaClientTypes.Account => {
13 | return {
14 | buffer: Buffer.from(account.data[0], "base64"),
15 | executable: account.executable,
16 | lamports: account.lamports,
17 | owner: account.owner,
18 | parsed: false,
19 | rentEpoch: account.rentEpoch,
20 | pubkey,
21 | };
22 | };
23 |
24 | export const normalizeRpcParsedAccountWithPubkey = ({
25 | account,
26 | pubkey,
27 | }: SolanaRpcTypes.ParsedAccountWithPubkey):
28 | | SolanaClientTypes.Account
29 | | SolanaClientTypes.ParsedAccountGeneric => {
30 | if (Array.isArray(account.data) && account.data[1] === "base64") {
31 | return normalizeRpcAccountWithPubkey({
32 | account: {
33 | ...account,
34 | data: account.data as [string, "base64"],
35 | },
36 | pubkey,
37 | });
38 | }
39 |
40 | return {
41 | data: account.data,
42 | executable: account.executable,
43 | lamports: account.lamports,
44 | owner: account.owner,
45 | parsed: true,
46 | rentEpoch: account.rentEpoch,
47 | pubkey,
48 | };
49 | };
50 |
51 | export const normalizeTransactionWithMeta = (
52 | txContainer: SolanaRpcTypes.TransactionRawWithMeta
53 | ): SolanaClientTypes.TransactionWithMeta => {
54 | const {
55 | blockTime,
56 | slot,
57 | transaction: [transaction_base64],
58 | meta,
59 | } = txContainer;
60 | const transaction = GTransaction.parse({
61 | buffer: Buffer.from(transaction_base64, "base64"),
62 | });
63 | const addressAtIndex = (idx: number): Solana.Address =>
64 | transaction.accounts[idx].address;
65 |
66 | return {
67 | block_time: blockTime ? DateTime.fromSeconds(blockTime) : null,
68 | slot: slot ?? null,
69 | transaction,
70 | meta: meta && {
71 | ...meta,
72 | logMessages: meta.logMessages || null,
73 | innerInstructions: meta.innerInstructions?.map(
74 | ({ index, instructions }) => ({
75 | index,
76 | instructions: instructions.map((ix) => ({
77 | accounts: ix.accounts.map(addressAtIndex),
78 | // TODO: are we actually getting base58 inner instruction data?
79 | data_base64: ix.data
80 | ? Buffer.from(bs58.decode(ix.data)).toString("base64")
81 | : "",
82 | program: addressAtIndex(ix.programIdIndex),
83 | })),
84 | })
85 | ),
86 | },
87 | };
88 | };
89 |
--------------------------------------------------------------------------------
/packages/solana-client/src/client/rpc-types.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { Solana } from "../base-types";
3 |
4 | /**
5 | * Here are the types returned directly by the Solana RPC, with no modification.
6 | *
7 | * We want these to only be used internally within SolanaClient and never be
8 | * leaked outside to our application.
9 | *
10 | * This means that each type here should correspond with one type in
11 | * SolanaClientTypes, which are the types actually exported to our app. This
12 | * corresponding type may be exactly the same, or may be modified with some data
13 | * parsing or a nicer structure (the RPC types sometimes are quite complex).
14 | */
15 | export namespace SolanaRpcTypes {
16 | const BaseAccountZ = z.object({
17 | executable: z.boolean(),
18 | owner: Solana.AddressZ,
19 | lamports: z.number(),
20 | rentEpoch: z.number(),
21 | });
22 |
23 | export const AccountZ = BaseAccountZ.and(
24 | z.object({
25 | data: z.tuple([z.string(), z.literal("base64")]),
26 | })
27 | );
28 |
29 | export const ParsedAccountZ = BaseAccountZ.and(
30 | z.object({
31 | data: z
32 | .tuple([z.string(), z.literal("base64")])
33 | .or(z.record(z.string(), z.any())),
34 | })
35 | );
36 |
37 | export const AccountWithPubkeyZ = z.object({
38 | account: AccountZ,
39 | pubkey: Solana.AddressZ,
40 | });
41 | export type AccountWithPubkey = z.infer;
42 |
43 | export const ParsedAccountWithPubkeyZ = z.object({
44 | account: ParsedAccountZ,
45 | pubkey: Solana.AddressZ,
46 | });
47 | export type ParsedAccountWithPubkey = z.infer<
48 | typeof ParsedAccountWithPubkeyZ
49 | >;
50 |
51 | export const InstructionZ = z.object({
52 | accounts: z.array(z.number()),
53 | data: z.string().nullish(), // This is base58 data
54 | programIdIndex: z.number(),
55 | });
56 | export type Instruction = z.infer;
57 |
58 | export const TokenBalanceZ = z.object({
59 | accountIndex: z.number(),
60 | mint: z.string(),
61 | owner: Solana.AddressZ.optional(),
62 | uiTokenAmount: z.object({
63 | amount: z.string(),
64 | decimals: z.number(),
65 | }),
66 | });
67 |
68 | // encoding=base64
69 | export const TransactionRawMetaZ = z.object({
70 | // Errors can be of many types and may presumably be added as Solana evolves
71 | // The possible errors are defined at the SDK instead of at the RPC, which
72 | // means that the RPC itself is not making any guarantees about what kinds
73 | // of errors will we get here.
74 | // Because of this, it's better to not make any assumptions about errors
75 | // and leave their parsing to the callers of SolanaClient. We don't want a
76 | // newly added error to break a whole SolanaClient endpoint.
77 | err: z.any(),
78 | fee: z.number(),
79 | innerInstructions: z
80 | .array(
81 | z.object({
82 | index: z.number(),
83 | instructions: z.array(InstructionZ),
84 | })
85 | )
86 | .nullish(),
87 | logMessages: z.array(z.string()).nullish(),
88 | postBalances: z.array(z.number()),
89 | preBalances: z.array(z.number()),
90 | preTokenBalances: z.array(TokenBalanceZ).nullable(),
91 | postTokenBalances: z.array(TokenBalanceZ).nullable(),
92 | loadedAddresses: z
93 | .object({
94 | readonly: z.array(Solana.AddressZ),
95 | writable: z.array(Solana.AddressZ),
96 | })
97 | .optional(),
98 | });
99 | export type TransactionRawMeta = z.infer;
100 | export type LoadedAddresses = TransactionRawMeta["loadedAddresses"];
101 | // https://github.com/luma-team/solana/blob/6d5bbca630bd59fb64f2bc446793c83482d8fba4/transaction-status/src/lib.rs#L403
102 | export const TransactionRawWithMetaZ = z.object({
103 | slot: z.number().optional(),
104 | transaction: z.tuple([z.string(), z.literal("base64")]),
105 | blockTime: z.number().optional().nullable(),
106 | meta: TransactionRawMetaZ.nullable(),
107 | });
108 | export type TransactionRawWithMeta = z.infer;
109 |
110 | export const PerformanceSampleZ = z.object({
111 | numSlots: z.number(),
112 | numTransactions: z.number(),
113 | samplePeriodSecs: z.number(),
114 | slot: z.number(),
115 | });
116 |
117 | export type Filter =
118 | | {
119 | dataSize: number;
120 | }
121 | | {
122 | memcmp: {
123 | bytes: string;
124 | offset: number;
125 | };
126 | }
127 | | null
128 | | false;
129 | }
130 |
--------------------------------------------------------------------------------
/packages/solana-client/src/error.ts:
--------------------------------------------------------------------------------
1 | import defaults from "lodash/defaults";
2 |
3 | export interface IGlowErrorOptions {
4 | code?: string;
5 | statusCode?: number;
6 | extraData?: { [key: string]: any };
7 | }
8 |
9 | export class GlowError extends Error {
10 | public code: string | null;
11 | public statusCode: number;
12 | public extraData: { [key: string]: any };
13 |
14 | constructor(message = "", options: IGlowErrorOptions = {}) {
15 | const { statusCode, code, extraData } = defaults(options, {
16 | code: null,
17 | statusCode: 500,
18 | extraData: null,
19 | });
20 |
21 | super(message);
22 |
23 | this.name = "GlowError";
24 | this.code = code;
25 | this.statusCode = statusCode;
26 | this.extraData = extraData;
27 |
28 | Object.setPrototypeOf(this, GlowError.prototype);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/packages/solana-client/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./base-types";
2 | export * from "./borsh/index";
3 | export * from "./client/client-types";
4 | export * from "./client/error-codes";
5 | export * from "./client/rpc-types";
6 | export * from "./client/solana-client";
7 | export * from "./EllipticCurve";
8 | export * from "./error";
9 | export * from "./GKeypair";
10 | export * from "./GPublicKey";
11 | export * from "./GTransaction";
12 | export * from "./transaction/AddressLookupTable";
13 | export * from "./transaction/LTransaction";
14 | export * from "./transaction/transaction-utils";
15 | export * from "./transaction/TransactionInterface";
16 | export * from "./transaction/VTransaction";
17 | export * from "./transaction/XTransaction";
18 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/AddressLookupTable.ts:
--------------------------------------------------------------------------------
1 | import * as beet from "@glow-xyz/beet";
2 | import BN from "bn.js";
3 | import { Buffer } from "buffer";
4 | import { Solana } from "../base-types";
5 | import { FixableGlowBorsh, GlowBorsh } from "../borsh";
6 | import { GPublicKey } from "../GPublicKey";
7 |
8 | /**
9 | * The lookup table stores some information about how it was configured.
10 | *
11 | * Then it stores addresses at an offset. It does not store the number of addresses,
12 | * so when getting addresses, we just iterate over the rest of the data.
13 | *
14 | * https://github.com/luma-team/solana/blob/b05c7d91ed4e0279ec622584edb54c9ef8547ad1/programs/address-lookup-table/src/state.rs#L40
15 | */
16 | const LookupTableMetaFormat = new FixableGlowBorsh<{
17 | typeIndex: number;
18 | deactivationSlot: BN;
19 | lastExtendedSlot: BN;
20 | lastExtendedStartIndex: number;
21 | authority: Solana.Address | null;
22 | }>({
23 | fields: [
24 | ["typeIndex", beet.u32],
25 | ["deactivationSlot", GlowBorsh.u64],
26 | ["lastExtendedSlot", GlowBorsh.u64],
27 | ["lastExtendedStartIndex", beet.u8],
28 | ["authority", beet.coption(GlowBorsh.address)],
29 | ],
30 | });
31 |
32 | const LookupTableAddressesOffset = 56;
33 |
34 | type LookupTable = {
35 | typeIndex: number;
36 | deactivationSlot: BN;
37 | lastExtendedSlot: BN;
38 | lastExtendedStartIndex: number;
39 | authority: Solana.Address | null;
40 | addresses: Solana.Address[];
41 | };
42 |
43 | export const parseLookupTable = ({
44 | buffer,
45 | }: {
46 | buffer: Buffer;
47 | }): LookupTable | null => {
48 | const parsed = LookupTableMetaFormat.parse({ buffer });
49 | if (!parsed) {
50 | return null;
51 | }
52 |
53 | const addressBytes = Array.from(buffer).slice(LookupTableAddressesOffset);
54 | if (addressBytes.length % GPublicKey.byteLength !== 0) {
55 | console.error(
56 | `Invalid account size. The address section ${addressBytes.length} is not a multiple of ${GPublicKey.byteLength}.`
57 | );
58 | return null;
59 | }
60 |
61 | const addresses: Solana.Address[] = [];
62 |
63 | let idx = LookupTableAddressesOffset;
64 | while (idx < buffer.length) {
65 | addresses.push(
66 | new GPublicKey(buffer.slice(idx, idx + GPublicKey.byteLength)).toBase58()
67 | );
68 | idx += GPublicKey.byteLength;
69 | }
70 |
71 | return {
72 | ...parsed,
73 | addresses,
74 | };
75 | };
76 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/LTransaction.ts:
--------------------------------------------------------------------------------
1 | import * as beet from "@glow-xyz/beet";
2 | import bs58 from "bs58";
3 | import nacl from "tweetnacl";
4 | import { Buffer } from "buffer";
5 | import { Base64, Hex, Solana } from "../base-types";
6 | import {
7 | FixableGlowBorsh,
8 | GlowBorsh,
9 | GlowBorshTypes,
10 | InstructionRawType,
11 | TransactionInstructionFormat,
12 | } from "../borsh";
13 | import { GKeypair } from "../GKeypair";
14 | import { getTransactionVersion } from "./transaction-utils";
15 | import {
16 | SignatureInfo,
17 | TransactionAccount,
18 | TransactionInstruction,
19 | TransactionInterface,
20 | } from "./TransactionInterface";
21 |
22 | /**
23 | * This is for a legacy transactions which as of 2022-10-14 are
24 | * the most common transaction type.
25 | *
26 | * This is designed to resemble VTransaction. We are moving away from GTransaction
27 | * and I hope to phase it out by the end of the year.
28 | */
29 | export class LTransaction implements TransactionInterface {
30 | #signatureInfos: Array;
31 | readonly #byteLength: number;
32 | readonly #messageBuffer: Buffer;
33 | readonly #message: LegacyTransactionMessage;
34 |
35 | constructor({ base64 }: { base64: Base64 }) {
36 | const txBuffer = Buffer.from(base64, "base64");
37 | const { version, messageBuffer, signatures } = getTransactionVersion({
38 | buffer: txBuffer,
39 | });
40 |
41 | if (version !== "legacy") {
42 | throw new Error(
43 | `Unsupported transaction version. Expected legacy, received ${version}.`
44 | );
45 | }
46 |
47 | const message = LegacyTransactionMessageFormat.parse({
48 | buffer: messageBuffer,
49 | });
50 |
51 | if (!message) {
52 | throw new Error("Could not parse message.");
53 | }
54 |
55 | this.#byteLength = txBuffer.byteLength;
56 | this.#messageBuffer = messageBuffer;
57 | this.#message = message;
58 | this.#signatureInfos = signatures.map((signature, idx) => ({
59 | signature,
60 | address: message.addresses[idx],
61 | }));
62 | }
63 |
64 | get addresses(): Solana.Address[] {
65 | return this.accounts.map((account) => account.address);
66 | }
67 |
68 | get numRequiredSigs(): number {
69 | return this.#message.numRequiredSigs;
70 | }
71 |
72 | get signature(): Solana.Signature {
73 | return this.#signatureInfos[0].signature;
74 | }
75 |
76 | get feePayer(): Solana.Address {
77 | return this.#signatureInfos[0].address;
78 | }
79 |
80 | get latestBlockhash(): string {
81 | return this.#message.latestBlockhash;
82 | }
83 |
84 | toBuffer(): Buffer {
85 | const signatures = this.#signatureInfos.map((i) => i.signature);
86 | const signaturesCoder =
87 | GlowBorshTypes.transactionSignaturesSection.toFixedFromValue(signatures);
88 |
89 | const buffer = Buffer.alloc(this.#byteLength);
90 | signaturesCoder.write(buffer, 0, signatures);
91 |
92 | this.#messageBuffer.copy(buffer, signaturesCoder.byteSize);
93 |
94 | return buffer;
95 | }
96 |
97 | toBase64(): Base64 {
98 | return this.toBuffer().toString("base64");
99 | }
100 |
101 | toHex(): Hex {
102 | return this.toBuffer().toString("hex");
103 | }
104 |
105 | sign({ signers }: { signers: GKeypair[] }) {
106 | const newSigs = [...this.#signatureInfos];
107 |
108 | for (const { secretKey } of signers) {
109 | const keypair = nacl.sign.keyPair.fromSecretKey(secretKey);
110 | const address = bs58.encode(keypair.publicKey);
111 |
112 | const accountIndex = this.#message.addresses.findIndex(
113 | (a) => a === address
114 | );
115 | if (accountIndex === -1) {
116 | continue;
117 | }
118 |
119 | const signatureUint = nacl.sign.detached(this.#messageBuffer, secretKey);
120 |
121 | newSigs[accountIndex] = {
122 | signature: bs58.encode(signatureUint),
123 | address,
124 | };
125 | }
126 |
127 | this.#signatureInfos = newSigs;
128 |
129 | return this;
130 | }
131 |
132 | get instructions(): Array {
133 | const accounts = this.accounts;
134 |
135 | return this.#message.instructions.map((rawIx) => {
136 | return {
137 | accounts: rawIx.accountIdxs.map((idx) => accounts[idx].address),
138 | program: accounts[rawIx.programIdx].address,
139 | data_base64: rawIx.data.toString("base64"),
140 | };
141 | });
142 | }
143 |
144 | get accounts(): Array {
145 | const message = this.#message;
146 |
147 | const {
148 | numReadonlySigned,
149 | numReadonlyUnsigned,
150 | numRequiredSigs,
151 | addresses,
152 | } = message;
153 |
154 | return addresses.map((address, idx) => ({
155 | address,
156 | signer: idx < numRequiredSigs,
157 | writable:
158 | idx < numRequiredSigs - numReadonlySigned ||
159 | (idx >= numRequiredSigs &&
160 | idx < addresses.length - numReadonlyUnsigned),
161 | wasLookedUp: false,
162 | }));
163 | }
164 | }
165 |
166 | type LegacyTransactionMessage = {
167 | numRequiredSigs: number;
168 | numReadonlySigned: number;
169 | numReadonlyUnsigned: number;
170 | addresses: Solana.Address[];
171 | latestBlockhash: string;
172 | instructions: InstructionRawType[];
173 | };
174 |
175 | export const LegacyTransactionMessageFormat =
176 | new FixableGlowBorsh({
177 | fields: [
178 | ["numRequiredSigs", beet.u8],
179 | ["numReadonlySigned", beet.u8],
180 | ["numReadonlyUnsigned", beet.u8],
181 | [
182 | "addresses",
183 | FixableGlowBorsh.compactArray({ itemCoder: GlowBorsh.address }),
184 | ],
185 | ["latestBlockhash", GlowBorsh.address],
186 | [
187 | "instructions",
188 | FixableGlowBorsh.compactArrayFixable({
189 | elemCoder: TransactionInstructionFormat,
190 | }),
191 | ],
192 | ],
193 | });
194 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/TransactionInterface.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "buffer";
2 | import { Base58, Base64, Hex, Solana } from "../base-types";
3 | import { GKeypair } from "../GKeypair";
4 |
5 | export interface TransactionInterface {
6 | sign: (args: { signers: GKeypair[] }) => TransactionInterface;
7 | toBuffer: () => Buffer;
8 | toHex: () => Hex;
9 | toBase64: () => Base64;
10 |
11 | addresses: Solana.Address[];
12 | instructions: TransactionInstruction[];
13 | accounts: TransactionAccount[];
14 | signature: Solana.Signature;
15 | feePayer: Solana.Address;
16 | latestBlockhash: string;
17 |
18 | numRequiredSigs: number;
19 | }
20 |
21 | export type TransactionInstruction = {
22 | accounts: Solana.Address[];
23 | program: Solana.Address;
24 | data_base64: Base64;
25 | };
26 |
27 | export type TransactionAccount = {
28 | address: Solana.Address;
29 | signer: boolean;
30 | writable: boolean;
31 | wasLookedUp: boolean;
32 | };
33 |
34 | export type SignatureInfo = {
35 | signature: Base58;
36 | address: Solana.Address;
37 | };
38 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/VTransaction.ts:
--------------------------------------------------------------------------------
1 | import * as beet from "@glow-xyz/beet";
2 | import bs58 from "bs58";
3 | import { Buffer } from "buffer";
4 | import nacl from "tweetnacl";
5 | import { Base64, Hex, Solana } from "../base-types";
6 | import {
7 | FixableGlowBorsh,
8 | GlowBorsh,
9 | GlowBorshTypes,
10 | InstructionRawType,
11 | TransactionInstructionFormat,
12 | } from "../borsh";
13 | import { SolanaRpcTypes } from "../client/rpc-types";
14 | import { GKeypair } from "../GKeypair";
15 | import { getTransactionVersion } from "./transaction-utils";
16 | import {
17 | SignatureInfo,
18 | TransactionAccount,
19 | TransactionInstruction,
20 | TransactionInterface,
21 | } from "./TransactionInterface";
22 |
23 | /**
24 | * This creates a Version 0 transaction. You need to have the lookup
25 | * tables populated when instantiating this or certain calls will error.
26 | */
27 | export class VTransaction implements TransactionInterface {
28 | #signatureInfos: Array;
29 | readonly #loadedAddresses: SolanaRpcTypes.LoadedAddresses;
30 | readonly #byteLength: number;
31 | readonly #messageBuffer: Buffer;
32 | readonly #message: V0Message;
33 |
34 | constructor({
35 | base64,
36 | loadedAddresses,
37 | }: {
38 | base64: Base64;
39 | loadedAddresses: SolanaRpcTypes.LoadedAddresses | null;
40 | }) {
41 | const txBuffer = Buffer.from(base64, "base64");
42 | const { version, messageBuffer, signatures } = getTransactionVersion({
43 | buffer: txBuffer,
44 | });
45 |
46 | if (version !== 0) {
47 | throw new Error(
48 | `Unsupported transaction version. Expected 0, received ${version}.`
49 | );
50 | }
51 |
52 | const message = V0TransactionMessageFormat.parse({
53 | buffer: messageBuffer,
54 | });
55 |
56 | if (!message) {
57 | throw new Error("Could not parse message.");
58 | }
59 |
60 | this.#byteLength = txBuffer.byteLength;
61 | this.#loadedAddresses = loadedAddresses || { writable: [], readonly: [] };
62 | this.#messageBuffer = messageBuffer;
63 | this.#message = message;
64 | this.#signatureInfos = signatures.map((signature, idx) => ({
65 | signature,
66 | address: message.addresses[idx],
67 | }));
68 | }
69 |
70 | get addresses(): Solana.Address[] {
71 | return this.accounts.map((account) => account.address);
72 | }
73 |
74 | get latestBlockhash(): string {
75 | return this.#message.latestBlockhash;
76 | }
77 |
78 | toBuffer(): Buffer {
79 | const signatures = this.#signatureInfos.map((i) => i.signature);
80 | const signaturesCoder =
81 | GlowBorshTypes.transactionSignaturesSection.toFixedFromValue(signatures);
82 |
83 | const buffer = Buffer.alloc(this.#byteLength);
84 | signaturesCoder.write(buffer, 0, signatures);
85 |
86 | this.#messageBuffer.copy(buffer, signaturesCoder.byteSize);
87 |
88 | return buffer;
89 | }
90 |
91 | toBase64(): Base64 {
92 | return this.toBuffer().toString("base64");
93 | }
94 |
95 | toHex(): Hex {
96 | return this.toBuffer().toString("hex");
97 | }
98 |
99 | sign({ signers }: { signers: GKeypair[] }) {
100 | const newSigs = [...this.#signatureInfos];
101 |
102 | for (const { secretKey } of signers) {
103 | const keypair = nacl.sign.keyPair.fromSecretKey(secretKey);
104 | const address = bs58.encode(keypair.publicKey);
105 |
106 | const accountIndex = this.#message.addresses.findIndex(
107 | (a) => a === address
108 | );
109 | if (accountIndex === -1) {
110 | continue;
111 | }
112 |
113 | const signatureUint = nacl.sign.detached(this.#messageBuffer, secretKey);
114 |
115 | newSigs[accountIndex] = {
116 | signature: bs58.encode(signatureUint),
117 | address,
118 | };
119 | }
120 |
121 | this.#signatureInfos = newSigs;
122 |
123 | return this;
124 | }
125 |
126 | get feePayer(): Solana.Address {
127 | return this.#signatureInfos[0].address;
128 | }
129 |
130 | get signature(): Solana.Signature {
131 | return this.#signatureInfos[0].signature;
132 | }
133 |
134 | get instructions(): Array {
135 | const accounts = this.accounts;
136 |
137 | return this.#message.instructions.map((rawIx) => {
138 | return {
139 | accounts: rawIx.accountIdxs.map((idx) => accounts[idx].address),
140 | program: accounts[rawIx.programIdx].address,
141 | data_base64: rawIx.data.toString("base64"),
142 | };
143 | });
144 | }
145 |
146 | get numRequiredSigs(): number {
147 | return this.#message.numRequiredSigs;
148 | }
149 |
150 | get accounts(): Array {
151 | const message = this.#message;
152 | const loadedAddresses = this.#loadedAddresses;
153 |
154 | const {
155 | numReadonlySigned,
156 | numReadonlyUnsigned,
157 | numRequiredSigs,
158 | addresses,
159 | } = message;
160 |
161 | const out: TransactionAccount[] = addresses.map((address, idx) => ({
162 | address,
163 | signer: idx < numRequiredSigs,
164 | writable:
165 | idx < numRequiredSigs - numReadonlySigned ||
166 | (idx >= numRequiredSigs &&
167 | idx < addresses.length - numReadonlyUnsigned),
168 | wasLookedUp: false,
169 | }));
170 |
171 | for (const address of loadedAddresses?.writable ?? []) {
172 | out.push({
173 | address,
174 | writable: true,
175 | signer: false,
176 | wasLookedUp: true,
177 | });
178 | }
179 | for (const address of loadedAddresses?.readonly ?? []) {
180 | out.push({
181 | address,
182 | writable: false,
183 | signer: false,
184 | wasLookedUp: true,
185 | });
186 | }
187 |
188 | return out;
189 | }
190 | }
191 |
192 | type AddressTableLookup = {
193 | lookupTableAddress: Solana.Address;
194 | writableIndexes: number[];
195 | readonlyIndexes: number[];
196 | };
197 |
198 | const AddressTableLookup = new FixableGlowBorsh({
199 | fields: [
200 | ["lookupTableAddress", GlowBorsh.address],
201 | ["writableIndexes", FixableGlowBorsh.compactArray({ itemCoder: beet.u8 })],
202 | ["readonlyIndexes", FixableGlowBorsh.compactArray({ itemCoder: beet.u8 })],
203 | ],
204 | });
205 |
206 | type V0Message = {
207 | maskedVersion: number;
208 | numRequiredSigs: number;
209 | numReadonlySigned: number;
210 | numReadonlyUnsigned: number;
211 | addresses: Solana.Address[];
212 | latestBlockhash: string;
213 | instructions: InstructionRawType[];
214 | addressTableLookups: AddressTableLookup[];
215 | };
216 |
217 | export const V0TransactionMessageFormat = new FixableGlowBorsh({
218 | fields: [
219 | // In a very confusing naming format, they are calling the second version of txs "V0"
220 | // https://beta.docs.solana.com/proposals/transactions-v2
221 | ["maskedVersion", beet.u8], // The first bit here will indicate if it's a versioned tx
222 | ["numRequiredSigs", beet.u8],
223 | ["numReadonlySigned", beet.u8],
224 | ["numReadonlyUnsigned", beet.u8],
225 | [
226 | "addresses",
227 | FixableGlowBorsh.compactArray({ itemCoder: GlowBorsh.address }),
228 | ],
229 | ["latestBlockhash", GlowBorsh.address],
230 | [
231 | "instructions",
232 | FixableGlowBorsh.compactArrayFixable({
233 | elemCoder: TransactionInstructionFormat,
234 | }),
235 | ],
236 | [
237 | "addressTableLookups",
238 | FixableGlowBorsh.compactArrayFixable({
239 | elemCoder: AddressTableLookup,
240 | }),
241 | ],
242 | ],
243 | });
244 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/XTransaction.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "buffer";
2 | import { Base64 } from "../base-types";
3 | import { SolanaRpcTypes } from "../client/rpc-types";
4 | import { LTransaction } from "./LTransaction";
5 | import { getTransactionVersion } from "./transaction-utils";
6 | import { TransactionInterface } from "./TransactionInterface";
7 | import { VTransaction } from "./VTransaction";
8 |
9 | /**
10 | * This is intended to be a more abstract class that can be used for both old and new transaction
11 | * types.
12 | */
13 | export namespace XTransaction {
14 | export const parse = ({
15 | base64,
16 | loadedAddresses,
17 | }: {
18 | base64: Base64;
19 | loadedAddresses: SolanaRpcTypes.LoadedAddresses | null;
20 | }): TransactionInterface => {
21 | const buffer = Buffer.from(base64, "base64");
22 | const { version } = getTransactionVersion({ buffer });
23 |
24 | if (version === "legacy") {
25 | return new LTransaction({ base64 });
26 | }
27 |
28 | if (version === 0) {
29 | return new VTransaction({ base64, loadedAddresses });
30 | }
31 |
32 | throw new Error("Invalid transaction format.");
33 | };
34 | }
35 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/__tests__/lookup-table.test.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "buffer";
2 | import { Base64 } from "../../base-types";
3 | import { parseLookupTable } from "../AddressLookupTable";
4 |
5 | const LookupTable1: Base64 =
6 | "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";
7 |
8 | describe("LookupTable", () => {
9 | test("parse", () => {
10 | const buffer = Buffer.from(LookupTable1, "base64");
11 | const lookupTable = parseLookupTable({
12 | buffer,
13 | });
14 |
15 | expect(lookupTable!.typeIndex).toBe(1);
16 | expect(lookupTable!.authority).toBe(
17 | "9FRhPDoDk9JrpCqc4r51qTWgdBTxM892TdjexeErQUNs"
18 | );
19 | expect(lookupTable!.lastExtendedSlot.toNumber()).toBe(154743039);
20 | expect(lookupTable!.addresses.slice(0, 3)).toEqual([
21 | "CURVGoZn8zycx6FXwwevgBTB2gVvdbGTEpvMJDbgs2t4",
22 | "2gCzKgSTPSy4fL7z9NQhJAKvumEofTa2DFJU4wGhQ5Jt",
23 | "DiZy5F8fHGgLkFMUkTwF1s2RwnFsjGwAXKF4GfEjvRB7",
24 | ]);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/__tests__/transaction-utils.test.ts:
--------------------------------------------------------------------------------
1 | import { Buffer } from "buffer";
2 | import { Base64 } from "../../base-types";
3 | import {
4 | getTransactionVersion,
5 | TransactionVersion,
6 | } from "../transaction-utils";
7 |
8 | const TestData: Array<[TransactionVersion, Base64]> = [
9 | [
10 | "legacy",
11 | "AUzawhRvQoFaeRhdcxpgS1J9MSCCY3+1Su+lIrmSdPqMzyVFWQ3OmUyOate16qFqrxaO057yZmLubyfLXczFHgIBAAQIPtgAs76l9rprtE5Q5YTW9Q2nwM9H7Nr5p8IWavzngmMqPWLxHd9O+8OhaYF0nTEPuqZrluLhtMiyB/IF4t6CImClDomIGZRbzcENQHna3dQs9W2jOjRGkTP5Hnfw2LuwsUxFz89vVR79B7ZCixj4pajCBgZUOApOw66uCwQkGkEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1qFhtKgdUm7Dw0HMNAN6vXM1OnSN0JJRLBF+/v+ckMnAi2+wc/rYbks3949hmXGkPqZAzOAnsFfblw30+NNonxS/Za1WvSunFeRXQsIx8n1jYhz1t3jzfBokiVHRdWAo8qVmeFQtlMx60uUjXEUR/6UV35H1UnaynGnlzpIpyhAQYHAAMCAQcFBAhdoWxHnHXvOg==",
12 | ],
13 | [
14 | 0,
15 | "ATi76PGbrz0K+1uiJiVIxfsXg5SE+zj9ZR/TKbTwtU6pPV84Qg5ZR4AaUjaef0uhFEw+XO8vkSwk7FaP9XO5JwWAAQABAn9ga/qYhdDgSftxl4CLVlBlRooyjZnabjgnerV4N1a5AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmD6HHzY+RDvqxAZFPxVgCE7P6K8K2XJ1HTY4zWQ8dMgEBAgACDAIAAAABAAAAAAAAAAEfiZ3FeBS8m76OgSDN784WBijZvYu2yDuiOw5JxtU5cQEBAA==",
16 | ],
17 | ];
18 |
19 | describe("getTransactionVersion", () => {
20 | test("get transaction versions", () => {
21 | for (const [expectedVersion, base64] of TestData) {
22 | const { version } = getTransactionVersion({
23 | buffer: Buffer.from(base64, "base64"),
24 | });
25 | expect(version).toBe(expectedVersion);
26 | }
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/__tests__/vtransaction-5j9WCjiybkEDMXYyuUTy8d2kEcJeaRyzcsPpkF4kmJdVbxR483dzKsJLRTrY91bKxi9tA94vfzqjCmdP3G596kBt.json:
--------------------------------------------------------------------------------
1 | {
2 | "slot": 163620263,
3 | "transaction": [
4 | "Aexp48WyFSibH0WJrNhr8jvk4Bmixcu8RChJb4n7MjELlWDvfw/AH/KVtb/IKJLrwurkYj1laErMfTy+GSafsguAAQABAkU7VI6Ga9Ziqp9odbS68ew4qGnZQpISuVH9D6wOOXFCBUpTUPhdyILWFKVWcniKKW3fHqur0KYGeIhJMvTu9qDD+Njtk/MMAB2U6zsKvC6lb3VG9zGdqDbSbDNcEQPCCAEBGAIOAw8EEAURBhIHEwgUCRUKFgsXDBgNGTJIZWxsbywgZnJvbSB0aGUgU29sYW5hIFdhbGxldCBBZGFwdGVyIGV4YW1wbGUgYXBwIQHQniWK5s9kew4kQbQ4GLuXE8R/Y1H4e+9YH2+DRsnMKgwAAgQGCAoMDhASFBYMAQMFBwkLDQ8RExUX",
5 | "base64"
6 | ],
7 | "blockTime": 1663788553,
8 | "meta": {
9 | "err": null,
10 | "fee": 5000,
11 | "innerInstructions": [],
12 | "loadedAddresses": {
13 | "readonly": [
14 | "1111111ogCyDbaRMvkdsHB3qfdyFYaG1WtRUAfdh",
15 | "11111112cMQwSC9qirWGjZM6gLGwW69X22mqwLLGP",
16 | "11111113R2cuenjG5nFubqX9Wzuukdin2YfGQVzu5",
17 | "11111114DhpssPJgSi1YU7hCMfYt1BJ334YgsffXm",
18 | "111111152P2r5yt6odmBLPsFCLBrFisJ3aS7LqLAT",
19 | "11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo9",
20 | "11111116djSnXB2wXVGT4xDLsfTnkp1p4cCxHAfRq",
21 | "11111117SQekjmcMtR25wEPPiL6m1Mb5586NkLL4X",
22 | "11111118F5rixNBnFLmioWZSYzjjFuAL5dyoDVzhD",
23 | "111111193m4hAxmCcGXMfnjVPfNhWSjb69sDgffKu",
24 | "11111119rSGfPZLcyCGzY4uYEL1fkzJr6fke9qKxb",
25 | "1111111Af7Udc9v3L82dQM5b4zee1Xt77Be4czzbH"
26 | ],
27 | "writable": [
28 | "1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM",
29 | "11111112D1oxKts8YPdTJRG5FzxTNpMtWmq8hkVx3",
30 | "111111131h1vYVSYuKP6AhS86fbRdMw9XHiZAvAaj",
31 | "11111113pNDtm61yGF8j2ycAwLEPsuWQXobye5qDR",
32 | "11111114d3RrygbPdAtMuFnDmzsN8T5fYKVQ7FVr7",
33 | "11111115RidqCHAoz6dzmXxGcfWLNzevYqNpaRAUo",
34 | "11111116EPqoQskEM2Pddp8KTL9JdYEBZMGF3aq7V",
35 | "11111117353mdUKehx9GW6JNHznGt5oSZs9fWkVkB",
36 | "11111117qkFjr4u54stuNNUR8fRF8dNhaP35yvANs",
37 | "11111118eRTi4fUVRoeYEeeTyL4DPAwxatvWT5q1Z",
38 | "11111119T6fgHG3unjQB6vpWozhBdiXDbQovvFVeF",
39 | "1111111AFmseVrdL9f9oyCzZefL9tG6UbvhMPRAGw"
40 | ]
41 | },
42 | "logMessages": [
43 | "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo invoke [1]",
44 | "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo consumed 5566 of 200000 compute units",
45 | "Program Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo success"
46 | ],
47 | "postBalances": [
48 | 999990000, 119712000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
49 | 0, 0, 0, 0, 0, 0, 0
50 | ],
51 | "preBalances": [
52 | 999995000, 119712000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
53 | 0, 0, 0, 0, 0, 0, 0
54 | ],
55 | "preTokenBalances": [],
56 | "postTokenBalances": []
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/__tests__/vtransaction.test.ts:
--------------------------------------------------------------------------------
1 | import zip from "lodash/zip";
2 | import { Buffer } from "buffer";
3 | import { GKeypair } from "../../GKeypair";
4 | import { GPublicKey } from "../../GPublicKey";
5 | import { VTransaction } from "../VTransaction";
6 | import * as web3 from "@solana/web3.js";
7 | import vTransaction5j9 from "./vtransaction-5j9WCjiybkEDMXYyuUTy8d2kEcJeaRyzcsPpkF4kmJdVbxR483dzKsJLRTrY91bKxi9tA94vfzqjCmdP3G596kBt.json";
8 | import vTransaction3N3 from "./vtransaction-3N3xmERQotKh5of4H5Q5UEjwMKhaDR52pfJHCGRcQUD5hHTBX9hnXBbRcJ6CiFczrRtPhtx3b2ddd2kSjvZP7Cg.json";
9 |
10 | describe("vTransaction", () => {
11 | test("vTransaction5j9 does not error if we don't pass in loadedAddresses", () => {
12 | // We want to be able to get basic info about the transaction without erroring.
13 | const vTransaction = new VTransaction({
14 | base64: vTransaction5j9.transaction[0],
15 | loadedAddresses: null,
16 | });
17 | expect(async () => {
18 | vTransaction.instructions;
19 | }).rejects.toThrow();
20 | });
21 |
22 | test("vTransaction5j9", () => {
23 | // console.log(vTransaction5j9);
24 | const vTransaction = new VTransaction({
25 | base64: vTransaction5j9.transaction[0],
26 | loadedAddresses: vTransaction5j9.meta.loadedAddresses,
27 | });
28 | console.log(vTransaction.addresses);
29 |
30 | expect(vTransaction.addresses).toEqual([
31 | "5fFbz3RE24mGVceM5N2SKcVHP5nqqq28PoGwJNtMVT6y",
32 | "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo",
33 | "1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM",
34 | "11111112D1oxKts8YPdTJRG5FzxTNpMtWmq8hkVx3",
35 | "111111131h1vYVSYuKP6AhS86fbRdMw9XHiZAvAaj",
36 | "11111113pNDtm61yGF8j2ycAwLEPsuWQXobye5qDR",
37 | "11111114d3RrygbPdAtMuFnDmzsN8T5fYKVQ7FVr7",
38 | "11111115RidqCHAoz6dzmXxGcfWLNzevYqNpaRAUo",
39 | "11111116EPqoQskEM2Pddp8KTL9JdYEBZMGF3aq7V",
40 | "11111117353mdUKehx9GW6JNHznGt5oSZs9fWkVkB",
41 | "11111117qkFjr4u54stuNNUR8fRF8dNhaP35yvANs",
42 | "11111118eRTi4fUVRoeYEeeTyL4DPAwxatvWT5q1Z",
43 | "11111119T6fgHG3unjQB6vpWozhBdiXDbQovvFVeF",
44 | "1111111AFmseVrdL9f9oyCzZefL9tG6UbvhMPRAGw",
45 | "1111111ogCyDbaRMvkdsHB3qfdyFYaG1WtRUAfdh",
46 | "11111112cMQwSC9qirWGjZM6gLGwW69X22mqwLLGP",
47 | "11111113R2cuenjG5nFubqX9Wzuukdin2YfGQVzu5",
48 | "11111114DhpssPJgSi1YU7hCMfYt1BJ334YgsffXm",
49 | "111111152P2r5yt6odmBLPsFCLBrFisJ3aS7LqLAT",
50 | "11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo9",
51 | "11111116djSnXB2wXVGT4xDLsfTnkp1p4cCxHAfRq",
52 | "11111117SQekjmcMtR25wEPPiL6m1Mb5586NkLL4X",
53 | "11111118F5rixNBnFLmioWZSYzjjFuAL5dyoDVzhD",
54 | "111111193m4hAxmCcGXMfnjVPfNhWSjb69sDgffKu",
55 | "11111119rSGfPZLcyCGzY4uYEL1fkzJr6fke9qKxb",
56 | "1111111Af7Udc9v3L82dQM5b4zee1Xt77Be4czzbH",
57 | ]);
58 |
59 | expect(vTransaction.instructions).toEqual([
60 | {
61 | data_base64: Buffer.from(
62 | "48656c6c6f2c2066726f6d2074686520536f6c616e612057616c6c65742041646170746572206578616d706c652061707021",
63 | "hex"
64 | ).toString("base64"),
65 | program: "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo",
66 | accounts: [
67 | "1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM",
68 | "1111111ogCyDbaRMvkdsHB3qfdyFYaG1WtRUAfdh",
69 | "11111112D1oxKts8YPdTJRG5FzxTNpMtWmq8hkVx3",
70 | "11111112cMQwSC9qirWGjZM6gLGwW69X22mqwLLGP",
71 | "111111131h1vYVSYuKP6AhS86fbRdMw9XHiZAvAaj",
72 | "11111113R2cuenjG5nFubqX9Wzuukdin2YfGQVzu5",
73 | "11111113pNDtm61yGF8j2ycAwLEPsuWQXobye5qDR",
74 | "11111114DhpssPJgSi1YU7hCMfYt1BJ334YgsffXm",
75 | "11111114d3RrygbPdAtMuFnDmzsN8T5fYKVQ7FVr7",
76 | "111111152P2r5yt6odmBLPsFCLBrFisJ3aS7LqLAT",
77 | "11111115RidqCHAoz6dzmXxGcfWLNzevYqNpaRAUo",
78 | "11111115q4EpJaTXAZWpCg3J2zppWGSZ46KXozzo9",
79 | "11111116EPqoQskEM2Pddp8KTL9JdYEBZMGF3aq7V",
80 | "11111116djSnXB2wXVGT4xDLsfTnkp1p4cCxHAfRq",
81 | "11111117353mdUKehx9GW6JNHznGt5oSZs9fWkVkB",
82 | "11111117SQekjmcMtR25wEPPiL6m1Mb5586NkLL4X",
83 | "11111117qkFjr4u54stuNNUR8fRF8dNhaP35yvANs",
84 | "11111118F5rixNBnFLmioWZSYzjjFuAL5dyoDVzhD",
85 | "11111118eRTi4fUVRoeYEeeTyL4DPAwxatvWT5q1Z",
86 | "111111193m4hAxmCcGXMfnjVPfNhWSjb69sDgffKu",
87 | "11111119T6fgHG3unjQB6vpWozhBdiXDbQovvFVeF",
88 | "11111119rSGfPZLcyCGzY4uYEL1fkzJr6fke9qKxb",
89 | "1111111AFmseVrdL9f9oyCzZefL9tG6UbvhMPRAGw",
90 | "1111111Af7Udc9v3L82dQM5b4zee1Xt77Be4czzbH",
91 | ],
92 | },
93 | ]);
94 | });
95 |
96 | test("vTransaction3N3", () => {
97 | const transactionBase64 = vTransaction3N3.transaction[0];
98 | const txBuffer = Buffer.from(transactionBase64, "base64");
99 | const vTransaction = new VTransaction({
100 | base64: transactionBase64,
101 | loadedAddresses: vTransaction3N3.meta.loadedAddresses,
102 | });
103 |
104 | const web3VersionedTx = web3.VersionedTransaction.deserialize(txBuffer);
105 | const web3TxMessage = web3.TransactionMessage.decompile(
106 | web3VersionedTx.message,
107 | {
108 | accountKeysFromLookups: {
109 | writable: vTransaction3N3.meta.loadedAddresses.writable.map(
110 | (address) => new web3.PublicKey(address)
111 | ),
112 | readonly: vTransaction3N3.meta.loadedAddresses.readonly.map(
113 | (address) => new web3.PublicKey(address)
114 | ),
115 | },
116 | }
117 | );
118 |
119 | expect(vTransaction.instructions.length).toBe(
120 | web3TxMessage.instructions.length
121 | );
122 | for (const [web3Ix, ix] of zip(
123 | web3TxMessage.instructions,
124 | vTransaction.instructions
125 | )) {
126 | expect(ix).toEqual({
127 | accounts: web3Ix!.keys.map(({ pubkey }) => pubkey.toBase58()),
128 | program: web3Ix!.programId.toBase58(),
129 | data_base64: web3Ix!.data.toString("base64"),
130 | });
131 | }
132 | });
133 |
134 | test("signing a transaction", () => {
135 | const keypair = GKeypair.generate();
136 |
137 | // Set up web3 transaction
138 | const pubkey = new web3.PublicKey(keypair.address);
139 | const instructions = [
140 | web3.SystemProgram.transfer({
141 | fromPubkey: pubkey,
142 | toPubkey: pubkey,
143 | lamports: 100000,
144 | }),
145 | ];
146 | const messageV0 = new web3.TransactionMessage({
147 | payerKey: pubkey,
148 | recentBlockhash: GPublicKey.nullString,
149 | instructions,
150 | }).compileToV0Message();
151 | const web3Transaction = new web3.VersionedTransaction(messageV0);
152 | const initialBase64 = Buffer.from(web3Transaction.serialize()).toString(
153 | "base64"
154 | );
155 |
156 | // Set up the vtransaction
157 | const vtransaction = new VTransaction({
158 | base64: initialBase64,
159 | loadedAddresses: { writable: [], readonly: [] },
160 | });
161 | expect(vtransaction.toHex()).toBe(
162 | Buffer.from(initialBase64, "base64").toString("hex")
163 | );
164 |
165 | web3Transaction.sign([keypair as unknown as web3.Keypair]);
166 | const signedHex = Buffer.from(web3Transaction.serialize()).toString("hex");
167 | expect(vtransaction.sign({ signers: [keypair] }).toHex()).toBe(signedHex);
168 | });
169 | });
170 |
--------------------------------------------------------------------------------
/packages/solana-client/src/transaction/transaction-utils.ts:
--------------------------------------------------------------------------------
1 | import bs58 from "bs58";
2 | import { Buffer } from "buffer";
3 | import nacl from "tweetnacl";
4 | import { Base58, Base64, Solana } from "../base-types";
5 | import { GlowBorshTypes } from "../borsh";
6 | import { GKeypair } from "../GKeypair";
7 | import { LegacyTransactionMessageFormat } from "./LTransaction";
8 | import { V0TransactionMessageFormat } from "./VTransaction";
9 |
10 | const VERSION_PREFIX_MASK = 0x7f;
11 | export type TransactionVersion = "legacy" | number;
12 |
13 | export const getTransactionVersion = ({
14 | buffer,
15 | }: {
16 | buffer: Buffer;
17 | }): {
18 | version: TransactionVersion;
19 | signatures: Base58[];
20 | messageBuffer: Buffer;
21 | addresses: Solana.Address[];
22 | } => {
23 | const signaturesCoder =
24 | GlowBorshTypes.transactionSignaturesSection.toFixedFromData(buffer, 0);
25 | const signatures = signaturesCoder.read(buffer, 0);
26 |
27 | const signaturesLength = signaturesCoder.byteSize;
28 | const messageBuffer = buffer.slice(signaturesLength);
29 |
30 | const prefix = buffer[signaturesLength];
31 |
32 | const maskedPrefix = prefix & VERSION_PREFIX_MASK;
33 |
34 | if (maskedPrefix === prefix) {
35 | const message = LegacyTransactionMessageFormat.parse({
36 | buffer: messageBuffer,
37 | });
38 | if (!message) {
39 | throw new Error("Cannot parse transaction.");
40 | }
41 |
42 | return {
43 | version: "legacy",
44 | signatures,
45 | messageBuffer,
46 | addresses: message.addresses,
47 | };
48 | }
49 |
50 | if (maskedPrefix !== 0) {
51 | throw new Error("We only support 'legacy' and '0' versions.");
52 | }
53 |
54 | const message = V0TransactionMessageFormat.parse({ buffer: messageBuffer });
55 | if (!message) {
56 | throw new Error("Cannot parse transaction.");
57 | }
58 |
59 | return {
60 | version: maskedPrefix,
61 | messageBuffer,
62 | signatures,
63 | addresses: message.addresses,
64 | };
65 | };
66 |
67 | export const signXTransaction = ({
68 | base64,
69 | signer,
70 | }: {
71 | base64: Base64;
72 | signer: GKeypair;
73 | }): {
74 | signature: Buffer;
75 | signed_transaction: Buffer;
76 | } => {
77 | const txBuffer = Buffer.from(base64, "base64");
78 | const signaturesCoder =
79 | GlowBorshTypes.transactionSignaturesSection.toFixedFromData(txBuffer, 0);
80 |
81 | const {
82 | messageBuffer,
83 | addresses,
84 | signatures: oldSignatures,
85 | } = getTransactionVersion({
86 | buffer: txBuffer,
87 | });
88 |
89 | const signature = Buffer.from(
90 | nacl.sign.detached(new Uint8Array(messageBuffer), signer.secretKey)
91 | );
92 | const signatureIdx = addresses.findIndex(
93 | (address) => address === signer.address
94 | );
95 |
96 | const newSignatures = oldSignatures.map((sig, idx) => {
97 | if (idx === signatureIdx) {
98 | return bs58.encode(signature);
99 | }
100 | return sig;
101 | });
102 |
103 | const signed_transaction = Buffer.alloc(txBuffer.byteLength);
104 | signaturesCoder.write(signed_transaction, 0, newSignatures);
105 | messageBuffer.copy(signed_transaction, signaturesCoder.byteSize);
106 |
107 | return { signature, signed_transaction };
108 | };
109 |
--------------------------------------------------------------------------------
/packages/solana-client/src/utils/ed25519.ts:
--------------------------------------------------------------------------------
1 | // Pulled from https://github.com/solana-labs/solana-web3.js/blob/master/src/utils/ed25519.ts
2 | import {sha512} from '@noble/hashes/sha512';
3 | import * as ed25519 from '@noble/ed25519';
4 |
5 | /**
6 | * A 64 byte secret key, the first 32 bytes of which is the
7 | * private scalar and the last 32 bytes is the public key.
8 | * Read more: https://blog.mozilla.org/warner/2011/11/29/ed25519-keys/
9 | */
10 | type Ed25519SecretKey = Uint8Array;
11 |
12 | /**
13 | * Ed25519 Keypair
14 | */
15 | export interface Ed25519Keypair {
16 | publicKey: Uint8Array;
17 | secretKey: Ed25519SecretKey;
18 | }
19 |
20 | ed25519.utils.sha512Sync = (...m) => sha512(ed25519.utils.concatBytes(...m));
21 |
22 | export const generatePrivateKey = ed25519.utils.randomPrivateKey;
23 | export const generateKeypair = (): Ed25519Keypair => {
24 | const privateScalar = ed25519.utils.randomPrivateKey();
25 | const publicKey = getPublicKey(privateScalar);
26 | const secretKey = new Uint8Array(64);
27 | secretKey.set(privateScalar);
28 | secretKey.set(publicKey, 32);
29 | return {
30 | publicKey,
31 | secretKey,
32 | };
33 | };
34 | export const getPublicKey = ed25519.sync.getPublicKey;
35 | export function isOnCurve(publicKey: Uint8Array): boolean {
36 | try {
37 | ed25519.Point.fromHex(publicKey, true /* strict */);
38 | return true;
39 | } catch {
40 | return false;
41 | }
42 | }
43 | export const sign = (
44 | message: Parameters[0],
45 | secretKey: Ed25519SecretKey,
46 | ) => ed25519.sync.sign(message, secretKey.slice(0, 32));
47 | export const verify = ed25519.sync.verify;
48 |
49 |
--------------------------------------------------------------------------------
/packages/solana-client/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "declaration": true,
6 | "declarationDir": "./dist/types",
7 | "module": "esnext",
8 | "outDir": "./dist/esm"
9 | },
10 | "include": ["src/**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/solana-client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "baseUrl": ".",
5 | "declaration": false,
6 | "declarationDir": null,
7 | "esModuleInterop": true,
8 | "incremental": false,
9 | "jsx": "react",
10 | "lib": ["dom", "dom.iterable", "esnext"],
11 | "module": "commonjs",
12 | "moduleResolution": "node",
13 | "noImplicitReturns": true,
14 | "noUnusedLocals": true,
15 | "outDir": "dist/cjs/",
16 | "resolveJsonModule": true,
17 | "skipLibCheck": true,
18 | "strict": true,
19 | "target": "ES6"
20 | },
21 | "exclude": ["node_modules", "dist"],
22 | "include": ["src/**/*.ts*"]
23 | }
24 |
--------------------------------------------------------------------------------
/packages/wallet-standard/.eslintignore:
--------------------------------------------------------------------------------
1 | lib
2 |
--------------------------------------------------------------------------------
/packages/wallet-standard/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
4 | "parser": "@typescript-eslint/parser",
5 | "plugins": ["@typescript-eslint", "require-extensions"],
6 | "rules": {
7 | "@typescript-eslint/consistent-type-imports": "error",
8 | "curly": "error",
9 | "no-restricted-globals": ["error", "name", "event", "origin", "status"],
10 | "prefer-const": [
11 | "error",
12 | {
13 | "destructuring": "all"
14 | }
15 | ],
16 | "no-console": 0
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/packages/wallet-standard/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/packages/wallet-standard/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 |
--------------------------------------------------------------------------------
/packages/wallet-standard/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "commitMessage": "[wallet-standard]: Release v${version}",
4 | "tagName": "wallet-standard-v${version}"
5 | },
6 | "github": {
7 | "release": true,
8 | "releaseName": "`@glow-xyz/wallet-standard` v${version}`"
9 | },
10 | "npm": {
11 | "access": "public"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/packages/wallet-standard/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@glow-xyz/wallet-standard",
3 | "version": "1.1.0",
4 | "sideEffects": false,
5 | "typings": "dist/types/index.d.ts",
6 | "exports": {
7 | "import": "./dist/esm/index.js",
8 | "require": "./dist/cjs/index.js"
9 | },
10 | "module": "./dist/esm/index.js",
11 | "main": "./dist/cjs/index.js",
12 | "files": [
13 | "dist/**/*",
14 | "src/**/*"
15 | ],
16 | "private": false,
17 | "publishConfig": {
18 | "access": "public"
19 | },
20 | "scripts": {
21 | "build": "rimraf dist && tsc -p tsconfig.json && tsc -p tsconfig.esm.json",
22 | "tsc": "tsc --noEmit",
23 | "fmt": "prettier --write '{*,**/*}.{ts,tsx,js,jsx,json}'",
24 | "lint": "eslint . --ext ts --ext tsx --quiet",
25 | "release": "pnpm build && release-it"
26 | },
27 | "dependencies": {
28 | "@glow-xyz/glow-client": "^1.5.0",
29 | "@solana/wallet-standard-features": "^1.0.0",
30 | "@wallet-standard/base": "^1.0.1",
31 | "@wallet-standard/features": "^1.0.1",
32 | "bs58": "^5.0.0",
33 | "buffer": "^6.0.3"
34 | },
35 | "devDependencies": {
36 | "@types/node": "^18.11.10",
37 | "@typescript-eslint/eslint-plugin": "^5.45.0",
38 | "@typescript-eslint/parser": "^5.45.0",
39 | "eslint": "8.29.0",
40 | "eslint-config-prettier": "^8.5.0",
41 | "eslint-plugin-prettier": "^4.2.1",
42 | "eslint-plugin-require-extensions": "^0.1.1",
43 | "prettier": "^2.8.0",
44 | "rimraf": "^3.0.2",
45 | "typescript": "^5.6.3"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/packages/wallet-standard/src/account.ts:
--------------------------------------------------------------------------------
1 | // This is copied with modification from @wallet-standard/wallet
2 |
3 | import type { WalletAccount } from "@wallet-standard/base";
4 | import { SOLANA_CHAINS } from "./solana.js";
5 |
6 | const chains = SOLANA_CHAINS;
7 | const features = [
8 | "solana:signAndSendTransaction",
9 | "solana:signMessage",
10 | "solana:signTransaction",
11 | ] as const;
12 |
13 | export class GlowWalletAccount implements WalletAccount {
14 | readonly #address: WalletAccount["address"];
15 | readonly #publicKey: WalletAccount["publicKey"];
16 | readonly #chains: WalletAccount["chains"];
17 | readonly #features: WalletAccount["features"];
18 | readonly #label: WalletAccount["label"];
19 | readonly #icon: WalletAccount["icon"];
20 |
21 | get address() {
22 | return this.#address;
23 | }
24 |
25 | get publicKey() {
26 | return this.#publicKey.slice();
27 | }
28 |
29 | get chains() {
30 | return this.#chains.slice();
31 | }
32 |
33 | get features() {
34 | return this.#features.slice();
35 | }
36 |
37 | get label() {
38 | return this.#label;
39 | }
40 |
41 | get icon() {
42 | return this.#icon;
43 | }
44 |
45 | constructor({
46 | address,
47 | publicKey,
48 | label,
49 | icon,
50 | }: Omit) {
51 | if (new.target === GlowWalletAccount) {
52 | Object.freeze(this);
53 | }
54 |
55 | this.#address = address;
56 | this.#publicKey = publicKey;
57 | this.#chains = chains;
58 | this.#features = features;
59 | this.#label = label;
60 | this.#icon = icon;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/wallet-standard/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./initialize.js";
2 |
--------------------------------------------------------------------------------
/packages/wallet-standard/src/initialize.ts:
--------------------------------------------------------------------------------
1 | import type { GlowAdapter } from "@glow-xyz/glow-client";
2 | import { DEPRECATED_registerWallet } from "./register.js";
3 | import { GlowWallet } from "./wallet.js";
4 |
5 | export function initialize(glow: GlowAdapter): void {
6 | DEPRECATED_registerWallet(new GlowWallet(glow));
7 | }
8 |
--------------------------------------------------------------------------------
/packages/wallet-standard/src/register.ts:
--------------------------------------------------------------------------------
1 | // This is copied from @wallet-standard/wallet
2 |
3 | import type {
4 | DEPRECATED_WalletsWindow,
5 | Wallet,
6 | WalletEventsWindow,
7 | WindowRegisterWalletEvent,
8 | WindowRegisterWalletEventCallback,
9 | } from "@wallet-standard/base";
10 |
11 | export function registerWallet(wallet: Wallet): void {
12 | const callback: WindowRegisterWalletEventCallback = ({ register }) =>
13 | register(wallet);
14 | try {
15 | (window as WalletEventsWindow).dispatchEvent(
16 | new RegisterWalletEvent(callback)
17 | );
18 | } catch (error) {
19 | console.error(
20 | "wallet-standard:register-wallet event could not be dispatched\n",
21 | error
22 | );
23 | }
24 | try {
25 | (window as WalletEventsWindow).addEventListener(
26 | "wallet-standard:app-ready",
27 | ({ detail: api }) => callback(api)
28 | );
29 | } catch (error) {
30 | console.error(
31 | "wallet-standard:app-ready event listener could not be added\n",
32 | error
33 | );
34 | }
35 | }
36 |
37 | class RegisterWalletEvent extends Event implements WindowRegisterWalletEvent {
38 | readonly #detail: WindowRegisterWalletEventCallback;
39 |
40 | get detail() {
41 | return this.#detail;
42 | }
43 |
44 | get type() {
45 | return "wallet-standard:register-wallet" as const;
46 | }
47 |
48 | constructor(callback: WindowRegisterWalletEventCallback) {
49 | super("wallet-standard:register-wallet", {
50 | bubbles: false,
51 | cancelable: false,
52 | composed: false,
53 | });
54 | this.#detail = callback;
55 | }
56 |
57 | /** @deprecated */
58 | preventDefault(): never {
59 | throw new Error("preventDefault cannot be called");
60 | }
61 |
62 | /** @deprecated */
63 | stopImmediatePropagation(): never {
64 | throw new Error("stopImmediatePropagation cannot be called");
65 | }
66 |
67 | /** @deprecated */
68 | stopPropagation(): never {
69 | throw new Error("stopPropagation cannot be called");
70 | }
71 | }
72 |
73 | /** @deprecated */
74 | export function DEPRECATED_registerWallet(wallet: Wallet): void {
75 | registerWallet(wallet);
76 | try {
77 | ((window as DEPRECATED_WalletsWindow).navigator.wallets ||= []).push(
78 | ({ register }) => register(wallet)
79 | );
80 | } catch (error) {
81 | console.error("window.navigator.wallets could not be pushed\n", error);
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/packages/wallet-standard/src/solana.ts:
--------------------------------------------------------------------------------
1 | // This is copied with modification from @solana/wallet-standard-chains
2 |
3 | import { Network } from "@glow-xyz/glow-client";
4 | import type { IdentifierString } from "@wallet-standard/base";
5 |
6 | /** Solana Mainnet (beta) cluster, e.g. https://api.mainnet-beta.solana.com */
7 | export const SOLANA_MAINNET_CHAIN = "solana:mainnet";
8 |
9 | /** Solana Devnet cluster, e.g. https://api.devnet.solana.com */
10 | export const SOLANA_DEVNET_CHAIN = "solana:devnet";
11 |
12 | /** Solana Localnet cluster, e.g. http://localhost:8899 */
13 | export const SOLANA_LOCALNET_CHAIN = "solana:localnet";
14 |
15 | /** Array of Solana clusters (Glow doesn't support testnet) */
16 | export const SOLANA_CHAINS = [
17 | SOLANA_MAINNET_CHAIN,
18 | SOLANA_DEVNET_CHAIN,
19 | SOLANA_LOCALNET_CHAIN,
20 | ] as const;
21 |
22 | /** Type of all Solana clusters */
23 | export type SolanaChain = typeof SOLANA_CHAINS[number];
24 |
25 | /**
26 | * Check if a chain corresponds with one of the Solana clusters.
27 | */
28 | export function isSolanaChain(chain: IdentifierString): chain is SolanaChain {
29 | return SOLANA_CHAINS.includes(chain as SolanaChain);
30 | }
31 |
32 | /**
33 | * Map supported Solana clusters to supported Glow networks.
34 | */
35 | export function getNetworkForChain(chain: SolanaChain): Network {
36 | switch (chain) {
37 | case SOLANA_MAINNET_CHAIN:
38 | return Network.Mainnet;
39 | case SOLANA_DEVNET_CHAIN:
40 | return Network.Devnet;
41 | case SOLANA_LOCALNET_CHAIN:
42 | return Network.Localnet;
43 | default:
44 | return Network.Mainnet;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/packages/wallet-standard/src/wallet.ts:
--------------------------------------------------------------------------------
1 | import type { GlowAdapter } from "@glow-xyz/glow-client";
2 | import { Network } from "@glow-xyz/glow-client";
3 | import type {
4 | SolanaSignAndSendTransactionFeature,
5 | SolanaSignAndSendTransactionMethod,
6 | SolanaSignAndSendTransactionOutput,
7 | SolanaSignMessageFeature,
8 | SolanaSignMessageMethod,
9 | SolanaSignMessageOutput,
10 | SolanaSignTransactionFeature,
11 | SolanaSignTransactionMethod,
12 | SolanaSignTransactionOutput,
13 | } from "@solana/wallet-standard-features";
14 | import type { Wallet } from "@wallet-standard/base";
15 | import type {
16 | ConnectFeature,
17 | ConnectMethod,
18 | DisconnectFeature,
19 | DisconnectMethod,
20 | EventsFeature,
21 | EventsListeners,
22 | EventsNames,
23 | EventsOnMethod,
24 | } from "@wallet-standard/features";
25 | import bs58 from "bs58";
26 | import { Buffer } from "buffer";
27 | import { GlowWalletAccount } from "./account.js";
28 | import { icon } from "./icon.js";
29 | import type { SolanaChain } from "./solana.js";
30 | import { getNetworkForChain, isSolanaChain, SOLANA_CHAINS } from "./solana.js";
31 |
32 | export type GlowFeature = {
33 | "glow:": {
34 | glow: GlowAdapter;
35 | };
36 | };
37 |
38 | export class GlowWallet implements Wallet {
39 | readonly #listeners: { [E in EventsNames]?: EventsListeners[E][] } = {};
40 | readonly #version = "1.0.0" as const;
41 | readonly #name = "Glow" as const;
42 | readonly #icon = icon;
43 | #account: GlowWalletAccount | null = null;
44 | readonly #glow: GlowAdapter;
45 |
46 | get version() {
47 | return this.#version;
48 | }
49 |
50 | get name() {
51 | return this.#name;
52 | }
53 |
54 | get icon() {
55 | return this.#icon;
56 | }
57 |
58 | get chains() {
59 | return SOLANA_CHAINS.slice();
60 | }
61 |
62 | get features(): ConnectFeature &
63 | DisconnectFeature &
64 | EventsFeature &
65 | SolanaSignAndSendTransactionFeature &
66 | SolanaSignTransactionFeature &
67 | SolanaSignMessageFeature &
68 | GlowFeature {
69 | return {
70 | "standard:connect": {
71 | version: "1.0.0",
72 | connect: this.#connect,
73 | },
74 | "standard:disconnect": {
75 | version: "1.0.0",
76 | disconnect: this.#disconnect,
77 | },
78 | "standard:events": {
79 | version: "1.0.0",
80 | on: this.#on,
81 | },
82 | "solana:signAndSendTransaction": {
83 | version: "1.0.0",
84 | supportedTransactionVersions: ["legacy", 0],
85 | signAndSendTransaction: this.#signAndSendTransaction,
86 | },
87 | "solana:signTransaction": {
88 | version: "1.0.0",
89 | supportedTransactionVersions: ["legacy", 0],
90 | signTransaction: this.#signTransaction,
91 | },
92 | "solana:signMessage": {
93 | version: "1.0.0",
94 | signMessage: this.#signMessage,
95 | },
96 | "glow:": {
97 | glow: this.#glow,
98 | },
99 | };
100 | }
101 |
102 | get accounts() {
103 | return this.#account ? [this.#account] : [];
104 | }
105 |
106 | constructor(glow: GlowAdapter) {
107 | if (new.target === GlowWallet) {
108 | Object.freeze(this);
109 | }
110 |
111 | this.#glow = glow;
112 |
113 | glow.on("connect", this.#connected, this);
114 | glow.on("disconnect", this.#disconnected, this);
115 | glow.on("accountChanged", this.#reconnected, this);
116 |
117 | this.#connected();
118 | }
119 |
120 | #on: EventsOnMethod = (event, listener) => {
121 | this.#listeners[event]?.push(listener) ||
122 | (this.#listeners[event] = [listener]);
123 | return (): void => this.#off(event, listener);
124 | };
125 |
126 | #emit(
127 | event: E,
128 | ...args: Parameters
129 | ): void {
130 | // eslint-disable-next-line prefer-spread
131 | this.#listeners[event]?.forEach((listener) => listener.apply(null, args));
132 | }
133 |
134 | #off(event: E, listener: EventsListeners[E]): void {
135 | this.#listeners[event] = this.#listeners[event]?.filter(
136 | (existingListener) => listener !== existingListener
137 | );
138 | }
139 |
140 | #connected = () => {
141 | const address = this.#glow.address;
142 | const publicKey = this.#glow.publicKey;
143 | if (address && publicKey) {
144 | const account = this.#account;
145 | if (!account || account.address !== address) {
146 | this.#account = new GlowWalletAccount({
147 | address,
148 | publicKey: publicKey.toBytes(),
149 | });
150 | this.#emit("change", { accounts: this.accounts });
151 | }
152 | }
153 | };
154 |
155 | #disconnected = () => {
156 | if (this.#account) {
157 | this.#account = null;
158 | this.#emit("change", { accounts: this.accounts });
159 | }
160 | };
161 |
162 | #reconnected = () => {
163 | if (this.#glow.address && this.#glow.publicKey) {
164 | this.#connected();
165 | } else {
166 | this.#disconnected();
167 | }
168 | };
169 |
170 | #connect: ConnectMethod = async ({ silent } = {}) => {
171 | if (!this.#account) {
172 | await this.#glow.connect(silent ? { onlyIfTrusted: true } : undefined);
173 | }
174 |
175 | this.#connected();
176 |
177 | return { accounts: this.accounts };
178 | };
179 |
180 | #disconnect: DisconnectMethod = async () => {
181 | await this.#glow.disconnect();
182 | };
183 |
184 | #signAndSendTransaction: SolanaSignAndSendTransactionMethod = async (
185 | ...inputs
186 | ) => {
187 | if (!this.#account) {
188 | throw new Error("not connected");
189 | }
190 |
191 | const outputs: SolanaSignAndSendTransactionOutput[] = [];
192 |
193 | if (inputs.length === 1) {
194 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
195 | const { transaction, account, chain, options } = inputs[0]!;
196 | const { commitment } = options || {};
197 | if (account !== this.#account) {
198 | throw new Error("invalid account");
199 | }
200 | if (!isSolanaChain(chain)) {
201 | throw new Error("invalid chain");
202 | }
203 |
204 | const { signature } = await this.#glow.signAndSendTransaction({
205 | transactionBase64: Buffer.from(transaction).toString("base64"),
206 | network: getNetworkForChain(chain),
207 | waitForConfirmation: Boolean(commitment),
208 | });
209 |
210 | outputs.push({ signature: bs58.decode(signature) });
211 | } else if (inputs.length > 1) {
212 | for (const input of inputs) {
213 | outputs.push(...(await this.#signAndSendTransaction(input)));
214 | }
215 | }
216 |
217 | return outputs;
218 | };
219 |
220 | #signTransaction: SolanaSignTransactionMethod = async (...inputs) => {
221 | if (!this.#account) {
222 | throw new Error("not connected");
223 | }
224 |
225 | const outputs: SolanaSignTransactionOutput[] = [];
226 |
227 | if (inputs.length === 1) {
228 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
229 | const { transaction, account, chain } = inputs[0]!;
230 | if (account !== this.#account) {
231 | throw new Error("invalid account");
232 | }
233 | if (chain && !isSolanaChain(chain)) {
234 | throw new Error("invalid chain");
235 | }
236 |
237 | const { signedTransactionBase64 } = await this.#glow.signTransaction({
238 | transactionBase64: Buffer.from(transaction).toString("base64"),
239 | network: chain ? getNetworkForChain(chain) : Network.Mainnet,
240 | });
241 |
242 | outputs.push({
243 | signedTransaction: new Uint8Array(
244 | Buffer.from(signedTransactionBase64, "base64")
245 | ),
246 | });
247 | } else if (inputs.length > 1) {
248 | let chain: SolanaChain | undefined = undefined;
249 | for (const input of inputs) {
250 | if (input.account !== this.#account) {
251 | throw new Error("invalid account");
252 | }
253 | if (input.chain) {
254 | if (!isSolanaChain(input.chain)) {
255 | throw new Error("invalid chain");
256 | }
257 | if (chain) {
258 | if (input.chain !== chain) {
259 | throw new Error("conflicting chain");
260 | }
261 | } else {
262 | chain = input.chain;
263 | }
264 | }
265 | }
266 |
267 | const transactionsBase64 = inputs.map(({ transaction }) =>
268 | Buffer.from(transaction).toString("base64")
269 | );
270 |
271 | const { signedTransactionsBase64 } = await this.#glow.signAllTransactions(
272 | {
273 | transactionsBase64,
274 | network: chain ? getNetworkForChain(chain) : Network.Mainnet,
275 | }
276 | );
277 |
278 | outputs.push(
279 | ...signedTransactionsBase64.map((signedTransactionBase64) => ({
280 | signedTransaction: new Uint8Array(
281 | Buffer.from(signedTransactionBase64, "base64")
282 | ),
283 | }))
284 | );
285 | }
286 |
287 | return outputs;
288 | };
289 |
290 | #signMessage: SolanaSignMessageMethod = async (...inputs) => {
291 | if (!this.#account) {
292 | throw new Error("not connected");
293 | }
294 |
295 | const outputs: SolanaSignMessageOutput[] = [];
296 |
297 | if (inputs.length === 1) {
298 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
299 | const { message, account } = inputs[0]!;
300 | if (account !== this.#account) {
301 | throw new Error("invalid account");
302 | }
303 |
304 | const { signedMessageBase64 } = await this.#glow.signMessage({
305 | messageBase64: Buffer.from(message).toString("base64"),
306 | });
307 |
308 | const signature = new Uint8Array(
309 | Buffer.from(signedMessageBase64, "base64")
310 | );
311 |
312 | outputs.push({ signedMessage: message, signature });
313 | } else if (inputs.length > 1) {
314 | for (const input of inputs) {
315 | outputs.push(...(await this.#signMessage(input)));
316 | }
317 | }
318 |
319 | return outputs;
320 | };
321 | }
322 |
--------------------------------------------------------------------------------
/packages/wallet-standard/tsconfig.esm.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "baseUrl": "./",
5 | "declaration": true,
6 | "declarationDir": "./dist/types",
7 | "module": "esnext",
8 | "outDir": "./dist/esm"
9 | },
10 | "include": ["src/**/*.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/packages/wallet-standard/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "baseUrl": ".",
5 | "declaration": false,
6 | "declarationDir": null,
7 | "esModuleInterop": true,
8 | "incremental": false,
9 | "jsx": "react",
10 | "lib": ["dom", "dom.iterable", "esnext"],
11 | "module": "commonjs",
12 | "moduleResolution": "node",
13 | "noImplicitReturns": true,
14 | "noUnusedLocals": true,
15 | "outDir": "dist/cjs/",
16 | "resolveJsonModule": true,
17 | "skipLibCheck": true,
18 | "strict": true,
19 | "target": "ES6"
20 | },
21 | "exclude": ["node_modules", "dist"],
22 | "include": ["src/**/*.ts*"]
23 | }
24 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "packages/*"
3 |
--------------------------------------------------------------------------------