├── .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 | [![Glow JS Overview](https://cdn.loom.com/sessions/thumbnails/837a218eca284292a5c69d719564ed9d-with-play.gif)](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 | ![CleanShot 2022-04-13 at 14 49 13](https://user-images.githubusercontent.com/1319079/163250841-82d2b4e9-ec6a-4efa-9e5b-0fbd6591d4ca.gif) 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 | 3 | 4 | -------------------------------------------------------------------------------- /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 | 12 | 18 | 19 | ); 20 | -------------------------------------------------------------------------------- /packages/glow-react/src/assets/GlowIcon3D.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const GlowIcon3D = (props: React.SVGProps) => ( 4 | 12 | 13 | 17 | 21 | 22 | 23 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 47 | 52 | 53 | 61 | 62 | 63 | 64 | 72 | 73 | 74 | 75 | 76 | 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 | "AQAAAP///////////zA5CQAAAADbAXqPjQcLfGvWoo7sQqGnI/j0QmrdZUkog7F93MTUlBxIAACqdu0UO1ZiXtdYtorFHQADzF6Hx4hy5kDe7ECIArsItRjm5MoBD4kVSu5X1k8Gcmz5tJguERJHbnTkA8PJgA8VvPJU+kXAp4RO9ys11ZayzFZACEPb+un1WuxxQuFjp7KmVUjv9a8TfjS0ioI6K9neIo9ecY6i+9jiN8bqhCIdLiUhWYnpxcxU5c3K680wOi/8VkDbazzKXYOzG1NKL1+C0m2ZqRXaeX2MwkhbJdip7UxBcFh37vz7KnaYl5aKaqNmG66DuhtV3H1jt8Od/nbhXsDqdpz6jAbxBk29Q/1T3Aq+OEvikW7o4gjM8Ps5Ok0c37h3302TQTevRjdReahbBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGhNtJbPrf59Wh0YzTm5bNPIjbZITBvHVoGDkRm2r/tgbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBQiXx+NpTh8nQ9s8fAniL36i4yOCVOWF8kIA/lk6LJoVcsMZJhP3qQRBGCbQ2qOsPREfGUVEPGbX3j/jwVW5FXoc2BSWIpr+dqWWCnKgGBulaEbwRG4iBoT6AKsS0UIfGTL8qcmfiXW5B17pBdqGTuFQEO1AG9qtxGpiM/vYEOApqpnRg6T+hmcYo+NEcrbhKsfP6SGjK6rHqZ+DpC50YAVF42W+8nGtdTUDZ1ZdpA2jNtwch5uxVIp6/MVaqTkedRGbMXWAdYbj9KflzQ+JDpanU7EPzMdoHpRzoAgycPELYroHT3IsnUEU8tj3CgDGYAIze5v5DIc2V6bSAdtMgAcVRBSqg7AkiK3gaNXv+rICZYBgAIZDhDzqzY3F4bifXSpe5WhcF+B87eW++YMA1BcOu+LZnwZMS7Be6Xs13n3PpkUtLQ9ZSh3dG+rZinpS/WOSWuMmZDXd5Epas/248b+hU/bp3ItoOmDCLJRBMa3Iq/uTvvrTbvzFcseD4nBVFKGTb/K6j0OEHu4vJX8qcE2RAKFPJVVvMrfSHl3r/awiKCnol2eyBDyG0bUfMTZOWtrrhh/WLnp/Rr5Nu8VcpAbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp71dJeYD3rANPhdAy+1mv1pZ+oTcN5ghO2vICHrYL1lQGm4hX/quBhPtof2NGGMA12sQ53BrrO1WYoPAAAAAAAQan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAj4EvMBljNK/7709f2F36E0WNneKdh6lXIbhIXGHeWICai5P0YQ4TJN5wbPAf8WQdl08WLzW0URX7qBrR2v9o5bWT3lsugz5F9coE36561k5aDxJsz0li1+dJ2mCB54FI6m6yc+yvrNRu7mjNQBlgwVTW6u3GsEItdWVi/9wHVuTYxUgWiWkXUkFR9Ra5rNq2nYiCfOzdDfunO9KqPX2DdH9gCxpnSqvcIGkv7htahiuR1y3vsoXqJukLM4En43Z8GwgcvdM+CXJO4V73aweraVz1iyZl7oVPA/QtNgwITTO6QhqL3ywSiF+Lnlthh5T5fMuat0e/PxmEYIBWs+JJH4UPLW4CpHr4JNCatp3ELXDLKMv6JJ+37le50lbBJ2LvBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAJjvDUtDzvh7Eb3XvYadLSfXu6FkfXVPWajFsOfRbKJTmmIa4XM7yipcx3P9ZNtevNOmWY75Av4Yfww1hnCluGzrZEqEqOUQBdM4chE7TF9aegkP3IwV7vgN6grcd4ykRwGCUU9BMw+6+/I0LCiXNCNk5CVPiIgCILhwk9tgDaWjGVDPZI8eM5Ju/jN1Jjr5XId2c5y26BWfiU51CO9+5Vbo5YK4yzGV9WOFZy0DKIrWgG0Z8bgTAj1+ipheltA6eIlnyjBukmm4l5uMmrPwGJG3I8Ruhtg3ouwAUcNE86fWSBPfkLCpCZS+n6x5Xig0s5UdF8ObEaE1N7s1aaFUf3bHHTcMNSIqCR1LwlV/0fqQMSS4V6xJj8p6V36+j6+CboUPLW4CpHr4JNCatp3ELXDLKMv6JJ+37le50lbBJ2LvBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkGp9UXGSxcUSGMyUw9SvF/WNruCJuh/UTj29mKAAAAAJjvDUtDzvh7Eb3XvYadLSfXu6FkfXVPWajFsOfRbKJTYL2wz9H2H1gL5RgDDyG6oGBg88dGD3VrijOZAaYSOiprEcCeiIJBDlBwznYhb28nPuB/Ct9fOCOR9Rjf+3qS8ULQ6rZhXAU+DgpLTWMKVwyBfLob2ujZNjhn3eZsX0XhRflyIK4UMgIAaEC1Y5k6r3K8s8ZK/ZsU8g/ODvDDvZzoB0BKuc6NbjFIO2J9yNsEJkPc9AI+KWlFwAEAaUz3heiwHm6BxaIuwIQ9qCiz52onw7eodgmSiUkYvKkJLDRNb/G6WrJW+3X5/oWpomPlEiM9D9V+lCYN3bT/81P2dWiZ9SvwDfkYyxRMmUbdkRKwyGbAJGq31zUDB1lK/OJkLuBUHowqzmvLkMawor15fn7dgGL+4yV/DR6ezru+4oXVPgP22vWOyg9V6NNTgdR4txdzoad8zPMTo833C9Razi3il9y0d9IkGgN6fimoK71S8y3BSgAMlXl35j8HWSimRwbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpmO8NS0PO+HsRvde9hp0tJ9e7oWR9dU9ZqMWw59FsolPvDYtv2izrpB2hXUCV0do5Kg0vjtDGx7wPTPrIwoC1bcIommpD0s6RxvVcrsNw9KzDii7Ud/WIEzNMbQN0n/KkmO8NS0PO+HsRvde9hp0tJ9e7oWR9dU9ZqMWw59FsolNo18H3siCD651oNTAmY7rs6kASAPT/RL5h7Wgc04TA/KTk6s43BJoUj9zovt5qg0KDUwajT5ytFjfrpX1cuAbe46PlwuQLarFj8hqCueP2jlxkDG+w03hFyWxSJdoLiDDf4bP/hIlw1S4FwW3UbVzx3Csu2bhr+5tPS+0rNxuGhQN3ojOao6yRogdR/XiPjjoIaHdGsNZvpncVZOMbEa39cXy0BprJoXx5azEM5ycM7NleETq4Rlvd8sOL8x5YG9w5U4yxHhLTmdTxMXIjKWiZNyJDx5RKGU+gD6vcwqZtx7lPwmmLpf18nafM+hKYT0bYjPWAJ+hdHhb58Mo30ACihQ8tbgKkevgk0Jq2ncQtcMsoy/okn7fuV7nSVsEnYu8G3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAmO8NS0PO+HsRvde9hp0tJ9e7oWR9dU9ZqMWw59FsolNL2UnENgLDPyB3kO0Wo1JMobmXXPEhoqkM/+x9+LaKzQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpuaEktB0AEQjsnvmTopplpLklkZbm5f7N2DFLtdkoO5RBV7BYDzHF/ORKYlgtvPnXjudZQ6CEo5OzUDaNIomTCMjVZUDyvUDVHl5+RBnji81mmFtf8RWZXpnTmF9ybfEmL4unFpd2XFV4mKWGC+NIOywHlRionXODBSbpk2epHu3wmRAwxp/4+vpgWc6gXtxs2jt9Id4vm7S2r/5vGvb2MIUPLW4CpHr4JNCatp3ELXDLKMv6JJ+37le50lbBJ2LvaNfB97Igg+udaDUwJmO67OpAEgD0/0S+Ye1oHNOEwPzf4bP/hIlw1S4FwW3UbVzx3Csu2bhr+5tPS+0rNxuGhQN3ojOao6yRogdR/XiPjjoIaHdGsNZvpncVZOMbEa3946PlwuQLarFj8hqCueP2jlxkDG+w03hFyWxSJdoLiDBxfLQGmsmhfHlrMQznJwzs2V4ROrhGW93yw4vzHlgb3DlTjLEeEtOZ1PExciMpaJk3IkPHlEoZT6APq9zCpm3HuU/CaYul/Xydp8z6EphPRtiM9YAn6F0eFvnwyjfQAKLWMhja848R4RV7eSMRWpZ6mkVX+NRl5CNBFM2z61WxHdm6jpOFTZVjCnDwePeZ42R8l9XcDkA1CXG+XPFEDqjfdM+JW1Dhin5hBpJQDVGAaB0p5yZDpBtiR/q4Cu9UbXpkAUU3a0sUZUG4zIX0qWDqMV9auO3+QaqHA6jm133xK0HPdYsyi5UmSlt2XKtG92Dk7VpYNazr86LSGO6Br3d+yyXiHqmHj72DlETErPWYL1dQAxKqBC9ynKHdksKhxr6GNB03RnD70j3mLJxTp5y4VFPXdrcCE6gtkdFxuhSFVd08dMk3JZRRZFEdpFcHucC+e/JPIIQcLf4l9gvEUwQkhQ8tbgKkevgk0Jq2ncQtcMsoy/okn7fuV7nSVsEnYu8G3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAmO8NS0PO+HsRvde9hp0tJ9e7oWR9dU9ZqMWw59FsolM+gjyymt911yAGKKn4cMAqMt9XYf1b+ccE6I4e++FVTbaoO0eDCcC1y2Xqaf+gQAth4bryn+WPjeJXXqkq6YfUcmiH/bOXaBL7Q18L2crz9N9EsXrE9ZAgUHrxygFiW0LG1+4fllw/bM+nP1Ep3FbzL0S78YQpHadb4CSRlmZ5a9f8EcxnWNBVfS1PC8zTUUJsOGLatzg/VV2fdx/RHrkQqbjftGdiR+1Pdw71BV+V0ySzHl2ZJz/sgVCk9Og+fcVHgQVkz17t+eFQhCnojKfI2u0Yl0ioGSoPYRu6qo/twu5AJCjEPmNna9IMZVUqcYJuE1yboROowt+0W4W0WR9UhQ8tbgKkevgk0Jq2ncQtcMsoy/okn7fuV7nSVsEnYu8G3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAmO8NS0PO+HsRvde9hp0tJ9e7oWR9dU9ZqMWw59FsolOVF8A64K6fCF8GCLasCiYpzf1Bnm99TcXU3hr34dku2J+O2yxdbDG1nKnX0Fmm1wacfdmO7un0WDVkngRZJ9SvNQTHzmt4+Wk5ZNFew3rjO2NZO5yeOHGQChfp4G0LlfIew+fUNH5kb54hWJ2ai6kfjtRFA+XlErN09lpalqQf5zvAO1i2425d0UogmLSSBaCEHI8wzYxmS+IcSVNHgRJNh76P6ACuJh51c6UVttVoh35Z2kZrmNCYyF5QoTCXa7nOpK49n0NxuW9UqAJFUhZPS6lyOqLZC96qkNIEHuzzZQ38cgxBfqZ4GACsz2O+CCXEaxoP2OvpAjZxPV7Vr+lghQ8tbgKkevgk0Jq2ncQtcMsoy/okn7fuV7nSVsEnYu8G3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAmO8NS0PO+HsRvde9hp0tJ9e7oWR9dU9ZqMWw59FsolNL2UnENgLDPyB3kO0Wo1JMobmXXPEhoqkM/+x9+LaKzQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp3ofSD5XHkFucfYeokkL8TNnqfBlifY3jiacyjyPv2DBBV7BYDzHF/ORKYlgtvPnXjudZQ6CEo5OzUDaNIomTCJ5jrN4Ejx5YLVJgSYzl/J7/YLmjlcRVS6gfHC0+wsOLEcTSyyLxuU555eKqZRSCJOzk2DmBcAbs6bLhPH206qeen8plw3+Dv7n5tDHTf19wmyu/rq4huXLD/PBPXHbHMIUPLW4CpHr4JNCatp3ELXDLKMv6JJ+37le50lbBJ2LvlRfAOuCunwhfBgi2rAomKc39QZ5vfU3F1N4a9+HZLtgew+fUNH5kb54hWJ2ai6kfjtRFA+XlErN09lpalqQf5zvAO1i2425d0UogmLSSBaCEHI8wzYxmS+IcSVNHgRJNNQTHzmt4+Wk5ZNFew3rjO2NZO5yeOHGQChfp4G0LlfKHvo/oAK4mHnVzpRW21WiHflnaRmuY0JjIXlChMJdruc6krj2fQ3G5b1SoAkVSFk9LqXI6otkL3qqQ0gQe7PNlDfxyDEF+pngYAKzPY74IJcRrGg/Y6+kCNnE9XtWv6WDltCvu0txrh2e1xqiKalBP2NrcDhJF5ZwEFe6BtMwEZ62dSkAaI34ASUhN4PsHK4WPYAwz8eHRJA/50QHkvAN/9nayboHuIzOznOy2HQTIw/K/0lJ8C/dFZ7Osi/ho2/83uYOvYqS2gzLL3T1PbhnktzwKkfjhESCqJY9b+jqDxZOUrnZjQZ8r2tMq9YQrWgiILp9lO1JxLfG8xkBMvDkJqdJKKgtXqSkTOGVBSwXqyyAWhKz92jyoJNB3A02kKMrRDa3raJz97kaDlTthiZ6qDHVpRwrp9dHl1l/7cbafamEDBAF9javJPAr0KNGCOc1Lc/V2JwAWR+G1PKYMqyT3hQ8tbgKkevgk0Jq2ncQtcMsoy/okn7fuV7nSVsEnYu8G3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAmO8NS0PO+HsRvde9hp0tJ9e7oWR9dU9ZqMWw59FsolMGhNtJbPrf59Wh0YzTm5bNPIjbZITBvHVoGDkRm2r/tgbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBHgLa5r76h9bbqk1C4Gns/+vIMg9fNehvo44lkvK1XxaXWfl7AIMlJN9bdPyh+pzAaXaeUZwTuLRoHOVAmHcsw4n+1uNeSDeiSa4xIDQvUh6asxsEOAM9ID5lysO1/MNm9xgUk6uIUB9WAOzNYrYbGT2EEExF8OtVGC4MU+XkXpchRoPzARWpqJKXfsloYD8kvcWXRuS66tAHsb8O3uw+EvZScQ2AsM/IHeQ7RajUkyhuZdc8SGiqQz/7H34torNBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKm0TRy6EoLodxoeBecarMdLYt0TxK0qOzNmeNjg/PtMFkFXsFgPMcX85EpiWC28+deO51lDoISjk7NQNo0iiZMIGLqwJSMuG1KKHJnGIwzyOLw73Uq1LxiE8qNHXdnXJp5trIPuX8mOXyCrlDFPT28ykdVZob1uZKyu2i4fnvoaQA6FdYZiWnWY6otKSfoqOQVOZKUPncdrEB0hgZX911EEhQ8tbgKkevgk0Jq2ncQtcMsoy/okn7fuV7nSVsEnYu/ltCvu0txrh2e1xqiKalBP2NrcDhJF5ZwEFe6BtMwEZze5g69ipLaDMsvdPU9uGeS3PAqR+OERIKolj1v6OoPFk5SudmNBnyva0yr1hCtaCIgun2U7UnEt8bzGQEy8OQn2drJuge4jM7Oc7LYdBMjD8r/SUnwL90Vns6yL+Gjb/6nSSioLV6kpEzhlQUsF6ssgFoSs/do8qCTQdwNNpCjK0Q2t62ic/e5Gg5U7YYmeqgx1aUcK6fXR5dZf+3G2n2phAwQBfY2ryTwK9CjRgjnNS3P1dicAFkfhtTymDKsk9w4DaF+OkJBT5FgSHGb1p2rtx3BqoRyC+KqVKo8reHmpBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKlrhfSCa2i3AxeuICaYH9ccyD24d88U7G6YysnWCPFBOiLTBvjpUe4KI5xV0AOHMS8B9yiGJUvgTAD8BKdx+yNpxBj4FNv7FbiPxLA16zBC2ZdHeo1MKlUsNEvdfGkyddcw5rjqqfdak7LnAgoPjqqfwYL+ztdGdSyqAOYMFbvsP6Mv5BkXgnpkDV0RTPyVZve6FRofISrC/D3WIGE/WZsG4iW19X2l9te/qmzIdbIyFtsUKz/AbIQtTGaxKloQfIkJ4c262NMPDjb7Ft1UlKOmYhEVhIVGA4hVE74vkHhyQ1/5PrmG6MsoiB+b8qVnkwBQI8sOAe7FCaEs/pKlv8skBV7RZO5PDBaHGJgVG9J6bY4bKgbxu/8GfM8vzdbZqPnhGQA+ruCb73C1gl42TxeU0JMotRMK0FGSOtpGTLIbCH0VOVI7sHbiUgp4wFwA6Dx7N3SuW59Bc+bmVcOSd8iKWOxlEqSPlD9XfDIjJmjfh5LXb/nWAXsqnC2Rr0H38ldi9pBmSZFQzpF6YWesgVmzJWAKKTB5ap0PB0i7LurJFrygleplkgYdUJoupi1AIssHWo+ByVMNXnjGA4nNkIQzc0Btt6x3jD1QHYl1MG8XE2ZLKegKLuSnj8MbZeb3i1WFDy1uAqR6+CTQmradxC1wyyjL+iSft+5XudJWwSdi7wbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpBqfVFxksXFEhjMlMPUrxf1ja7gibof1E49vZigAAAACY7w1LQ874exG9172GnS0n17uhZH11T1moxbDn0WyiU35UdxpXpvFMqeQC1UruRfc3iso2XHsWmn7IP1GCspjwBt324ddloZPZy+FGzut5rBy0he1fWzeROoz1hX7/AKkFIh/XBmOWee6Tp36BJfcY3BcOeNV0j0H4BlPJHiLsg0bX7rnRU+wfA2HlEWfWcfnqNusoi8QLjV8M4AWOpN8EJGfKuEbzcVkRM4by+cupRTwxQUnvULtAi0+sGIOZ1iZ29fwXcrkNmzUODqmzXKoBV72AL/vCFRR9Ri12mHzZGVNB0hqz7HxrPk+Cy/27TtIsAFsSgcY+63qfUyoJva1SmzDukL/a0Fhq5DZjl1452yoUKOeeeuYLXrwEf8cWELQOA2hfjpCQU+RYEhxm9adq7cdwaqEcgviqlSqPK3h5qQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpYs0OTYcJXI9uRj7WRwJ0RDiQhIzDPNZvQ2GHaI9sAlHmBBsG//rMtXmQkMhpGjNmTzDuUt4hL/A1DZet+EGoxHZrPaJahn5BIabxb+EMAA44K9vnTmcYCtu0NYUtR82AOlnvUUE/ZnLfm0MRGrjNQ8+5WbUVdqaXT8bB+wOWHJDAs36isCTP5f/Xqs7axarmZCQCjs87qtWQcHolnoEtrgo2H4HXlGqgM+eg/iC+MiRHBl21FRizGbJOMkPDcBuiOuLMoEwsTShlmaCgCFnGpT7cwjA65q+//g/EfGstSDpL2UnENgLDPyB3kO0Wo1JMobmXXPEhoqkM/+x9+LaKzQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCpv8G4l3JHZJ8Iivcu2IRjXM6h56xEtKTpSyu+AD8txK5BV7BYDzHF/ORKYlgtvPnXjudZQ6CEo5OzUDaNIomTCE19MgxpMYzygUVrb1iMIxQTNZ1DIYRL9Ky5xnX4W0TGp5lPJZ7iEPhF4rHAvcpA1kCvra63wk4jEwBtxZlCTO+yJTqw7w3lyVcTIYRTRxBxx0oErIpcIzoHb1/zmR+kLIUPLW4CpHr4JNCatp3ELXDLKMv6JJ+37le50lbBJ2LvmjAm8ERMocWHhSeninfHyj9qECImGBo0i/0xBwz8Y+ClKLp5mq63o9ryM3XJGZkkKUh5zMSjVmJhCEY10G+o93lnGbU5sHyzWxbwQmTWuWfoQr9EvFL9RzLXfv127DzdqOfBgpBXEVjcCsdHo7yY5GAeKOXsWE11CI28YB2tIfuVz+ydw7aJMRDqGH1pSBciTG5fc7+zRJh07pw3FAaKUmo6j1t3mmLthLzYZtcDmvF9LjqvtMwA29We4gc5qFaurAnuEO55OEOaRX1wvMTUjLtElKuScnNNFYtv5jrGhU+aMCbwREyhxYeFJ6eKd8fKP2oQIiYYGjSL/TEHDPxj4KOjV+LieOCe5CarPfNDDKQNukYCgi/XX1cW+rcl5zgRqOfBgpBXEVjcCsdHo7yY5GAeKOXsWE11CI28YB2tIfulKLp5mq63o9ryM3XJGZkkKUh5zMSjVmJhCEY10G+o93lnGbU5sHyzWxbwQmTWuWfoQr9EvFL9RzLXfv127Dzdlc/sncO2iTEQ6hh9aUgXIkxuX3O/s0SYdO6cNxQGilJqOo9bd5pi7YS82GbXA5rxfS46r7TMANvVnuIHOahWrqwJ7hDueThDmkV9cLzE1Iy7RJSrknJzTRWLb+Y6xoVPhQ8tbgKkevgk0Jq2ncQtcMsoy/okn7fuV7nSVsEnYu8G3fbh12Whk9nL4UbO63msHLSF7V9bN5E6jPWFfv8AqQan1RcZLFxRIYzJTD1K8X9Y2u4Im6H9ROPb2YoAAAAAmO8NS0PO+HsRvde9hp0tJ9e7oWR9dU9ZqMWw59FsolM="; 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 | --------------------------------------------------------------------------------