├── .envrc ├── .eslintrc.cjs ├── .github ├── actions │ └── yarn │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── main.yml │ ├── nix.yml │ └── release.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── .size-limit.json ├── .vscode ├── extensions.json └── settings.json ├── .yarn ├── releases │ └── yarn-4.3.1.cjs └── sdks │ ├── eslint │ ├── bin │ │ └── eslint.js │ ├── lib │ │ ├── api.js │ │ └── unsupported-api.js │ └── package.json │ ├── integrations.yml │ ├── prettier │ ├── bin │ │ └── prettier.cjs │ ├── index.cjs │ └── package.json │ └── typescript │ ├── bin │ ├── tsc │ └── tsserver │ ├── lib │ ├── tsc.js │ ├── tsserver.js │ ├── tsserverlibrary.js │ └── typescript.js │ └── package.json ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── jest.config.mjs ├── package.json ├── packages ├── anchor-contrib │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── errors.ts │ │ ├── generateAccountParsers.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── accounts.ts │ │ │ ├── coder.ts │ │ │ ├── idl.ts │ │ │ ├── index.ts │ │ │ ├── programs.ts │ │ │ └── provider.ts │ ├── tsconfig.cjs.json │ └── tsconfig.json ├── chai-solana │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── debugAccountOwners.ts │ │ ├── expectTXTable.ts │ │ ├── index.ts │ │ ├── printInstructionLogs.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tsconfig.cjs.json │ └── tsconfig.json ├── option-utils │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ ├── tsconfig.cjs.json │ └── tsconfig.json ├── solana-contrib │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── broadcaster │ │ │ ├── index.ts │ │ │ ├── sendAndSpamRawTx.ts │ │ │ └── tiered.ts │ │ ├── computeBudget │ │ │ ├── index.ts │ │ │ ├── instructions.ts │ │ │ └── layouts.ts │ │ ├── constants.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── provider.ts │ │ ├── transaction │ │ │ ├── PendingTransaction.ts │ │ │ ├── TransactionEnvelope.ts │ │ │ ├── TransactionReceipt.ts │ │ │ ├── index.ts │ │ │ ├── parseTransactionLogs.test.ts │ │ │ ├── parseTransactionLogs.ts │ │ │ ├── programErr.ts │ │ │ ├── txSizer.ts │ │ │ └── utils.ts │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── instructions.ts │ │ │ ├── misc.ts │ │ │ ├── printAccountOwners.ts │ │ │ ├── printTXTable.ts │ │ │ ├── pubkeyCache.ts │ │ │ ├── publicKey.ts │ │ │ ├── simulateTransactionWithCommitment.ts │ │ │ ├── time.ts │ │ │ └── txLink.ts │ │ └── wallet.ts │ ├── tsconfig.cjs.json │ └── tsconfig.json ├── stableswap-sdk │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── calculator │ │ │ ├── amounts.ts │ │ │ ├── amounts.unit.test.ts │ │ │ ├── curve.ts │ │ │ ├── curve.unit.test.ts │ │ │ ├── index.ts │ │ │ └── price.ts │ │ ├── constants.ts │ │ ├── entities │ │ │ ├── exchange.ts │ │ │ └── index.ts │ │ ├── events.ts │ │ ├── index.ts │ │ ├── instructions │ │ │ ├── admin.ts │ │ │ ├── common.ts │ │ │ ├── index.ts │ │ │ ├── layouts.ts │ │ │ └── swap.ts │ │ ├── stable-swap.ts │ │ ├── state │ │ │ ├── fees.ts │ │ │ ├── index.ts │ │ │ └── layout.ts │ │ └── util │ │ │ ├── account.ts │ │ │ ├── index.ts │ │ │ ├── initialize.ts │ │ │ ├── initializeSimple.ts │ │ │ └── instructions.ts │ ├── tsconfig.cjs.json │ └── tsconfig.json ├── token-utils │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── ata.ts │ │ ├── common.ts │ │ ├── index.ts │ │ ├── instructions │ │ │ ├── account.ts │ │ │ ├── ata.ts │ │ │ ├── index.ts │ │ │ ├── mint.ts │ │ │ └── nft.ts │ │ ├── layout.ts │ │ ├── price.ts │ │ ├── splTokenRegistry.ts │ │ ├── token.ts │ │ ├── tokenAmount.ts │ │ ├── tokenList.ts │ │ ├── tokenOwner.ts │ │ └── tokenProvider.ts │ ├── tsconfig.cjs.json │ └── tsconfig.json ├── tuple-utils │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ │ ├── fill.ts │ │ ├── index.ts │ │ ├── map.ts │ │ └── tuple.ts │ ├── tsconfig.cjs.json │ └── tsconfig.json ├── use-solana │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── adapters │ │ │ ├── index.ts │ │ │ ├── ledger │ │ │ │ ├── core.ts │ │ │ │ └── index.ts │ │ │ ├── readonly │ │ │ │ └── index.ts │ │ │ ├── secret-key │ │ │ │ └── index.ts │ │ │ ├── solana.ts │ │ │ └── types.ts │ │ ├── context.tsx │ │ ├── error.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── providers.ts │ │ ├── storage.ts │ │ ├── typings │ │ │ └── window.d.ts │ │ └── utils │ │ │ ├── provider.ts │ │ │ ├── useConnectionInternal.ts │ │ │ ├── usePersistedKVStore.ts │ │ │ ├── useProviderInternal.ts │ │ │ └── useWalletInternal.ts │ ├── tsconfig.cjs.json │ └── tsconfig.json └── wallet-adapter-icons │ ├── LICENSE.txt │ ├── README.md │ ├── package.json │ ├── src │ ├── bravewallet.tsx │ ├── coin98.tsx │ ├── index.tsx │ └── mathwallet.tsx │ ├── tsconfig.cjs.json │ └── tsconfig.json ├── tsconfig.json ├── turbo.json └── yarn.lock /.envrc: -------------------------------------------------------------------------------- 1 | watch_file flake.nix 2 | watch_file flake.lock 3 | mkdir -p .direnv 4 | eval "$(nix print-dev-env --profile "$(direnv_layout_dir)/flake-profile")" 5 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | "use strict"; 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore 7 | require("@rushstack/eslint-patch/modern-module-resolution"); 8 | 9 | /** @type import('@typescript-eslint/utils/dist').TSESLint.Linter.ConfigType */ 10 | module.exports = { 11 | env: { 12 | browser: true, 13 | node: true, 14 | jest: true, 15 | }, 16 | settings: { react: { version: "18" } }, 17 | extends: ["@saberhq/eslint-config-react"], 18 | parserOptions: { 19 | project: ["tsconfig.json", "./**/tsconfig*.json"], 20 | EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /.github/actions/yarn/action.yml: -------------------------------------------------------------------------------- 1 | name: Yarn 2 | description: Sets up the Yarn, the Yarn Cache, and installs 3 | 4 | inputs: 5 | cachix-auth-token: 6 | required: true 7 | description: "Your Cachix auth token." 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | # Install Cachix 13 | - uses: cachix/install-nix-action@v24 14 | - name: Setup Cachix 15 | uses: cachix/cachix-action@v12 16 | with: 17 | name: saber 18 | authToken: ${{ inputs.cachix-auth-token }} 19 | 20 | # Install Node 21 | - name: Get yarn cache directory path 22 | id: yarn-cache-dir-path 23 | run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT 24 | shell: nix shell .#ci --command bash {0} 25 | - name: Yarn Cache 26 | uses: actions/cache@v3 27 | with: 28 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 29 | key: ${{ runner.os }}-mod-${{ hashFiles('**/yarn.lock') }} 30 | restore-keys: | 31 | ${{ runner.os }}-mod- 32 | - run: yarn install 33 | shell: nix shell .#ci --command bash {0} 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | defaults: 10 | run: 11 | shell: nix shell .#ci --command bash {0} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: ./.github/actions/yarn 19 | with: 20 | cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} 21 | - run: yarn build 22 | - run: yarn size 23 | 24 | typecheck: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: ./.github/actions/yarn 29 | with: 30 | cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} 31 | - run: yarn build 32 | - run: yarn typecheck 33 | 34 | test: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: ./.github/actions/yarn 39 | with: 40 | cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} 41 | - run: yarn build 42 | - run: yarn test 43 | 44 | lint: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | - uses: ./.github/actions/yarn 49 | with: 50 | cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} 51 | - run: yarn build 52 | - run: yarn lint:ci 53 | 54 | docs: 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v4 58 | - uses: ./.github/actions/yarn 59 | with: 60 | cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} 61 | - run: yarn build 62 | - run: yarn docs:generate 63 | 64 | doctor: 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@v4 68 | - uses: ./.github/actions/yarn 69 | with: 70 | cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} 71 | # workaround b/c it's broken 72 | - run: yarn plugin remove @yarnpkg/plugin-version 73 | - run: yarn doctor packages/ 74 | -------------------------------------------------------------------------------- /.github/workflows/nix.yml: -------------------------------------------------------------------------------- 1 | name: Nix environment 2 | 3 | on: 4 | push: 5 | paths: 6 | - flake.nix 7 | - shell.nix 8 | - flake.lock 9 | branches: 10 | - master 11 | pull_request: 12 | paths: 13 | - flake.nix 14 | - shell.nix 15 | - flake.lock 16 | branches: 17 | - master 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: cachix/install-nix-action@v24 25 | - name: Setup Cachix 26 | uses: cachix/cachix-action@v12 27 | with: 28 | name: saber 29 | authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 30 | - name: Check flake 31 | run: nix flake check -v --show-trace --no-update-lock-file 32 | - run: nix build .#ci 33 | - run: nix develop -c echo success 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: "Version to publish, by explicit version or semver keyword." 8 | required: true 9 | default: patch 10 | 11 | jobs: 12 | release-packages: 13 | runs-on: ubuntu-latest 14 | name: Release all packages 15 | steps: 16 | - uses: actions/checkout@v4 17 | - run: | 18 | echo "Must be on master branch to publish packages." 19 | exit 1 20 | if: github.ref != 'refs/heads/master' 21 | - name: Setup Node 22 | uses: actions/setup-node@v3.6.0 23 | with: 24 | node-version: 18.x 25 | registry-url: "https://registry.npmjs.org" 26 | - name: Get yarn cache directory path 27 | id: yarn-cache-dir-path 28 | run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT 29 | - name: Yarn Cache 30 | uses: actions/cache@v3 31 | with: 32 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 33 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-modules- 36 | - name: Install Yarn dependencies 37 | run: yarn install 38 | - run: yarn build 39 | - run: yarn workspaces foreach --all -t version ${{ github.event.inputs.version }} 40 | - run: yarn docs:generate 41 | - uses: mikepenz/release-changelog-builder-action@v3 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | - id: git-release 45 | name: Perform Git release 46 | run: | 47 | VERSION=$(node -e "console.log(require('./package.json').version);") 48 | git config user.name "GitHub Actions" 49 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 50 | git add . 51 | git commit -a -m "Release v$VERSION" 52 | git tag "v$VERSION" 53 | git push origin HEAD --tags 54 | echo "::set-output name=version::v$VERSION" 55 | - name: Release 56 | uses: softprops/action-gh-release@v1 57 | with: 58 | tag_name: ${{ steps.git-release.outputs.version }} 59 | generate_release_notes: true 60 | - name: Publish 61 | run: | 62 | echo 'npmAuthToken: "${NODE_AUTH_TOKEN}"' >> .yarnrc.yml 63 | git update-index --assume-unchanged .yarnrc.yml 64 | yarn publish:all 65 | env: 66 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 67 | - name: Deploy docs 68 | uses: JamesIves/github-pages-deploy-action@v4.4.1 69 | with: 70 | branch: gh-pages 71 | folder: docs 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional stylelint cache 57 | .stylelintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | .env*.local 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | .parcel-cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Storybook build outputs 91 | .out 92 | .storybook-out 93 | storybook-static 94 | 95 | # rollup.js default build output 96 | dist/ 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress build output 105 | .vuepress/dist 106 | 107 | # Serverless directories 108 | .serverless/ 109 | 110 | # FuseBox cache 111 | .fusebox/ 112 | 113 | # DynamoDB Local files 114 | .dynamodb/ 115 | 116 | # TernJS port file 117 | .tern-port 118 | 119 | # Stores VSCode versions used for testing VSCode extensions 120 | .vscode-test 121 | 122 | # Idea editors 123 | .idea 124 | 125 | # Temporary folders 126 | tmp/ 127 | temp/ 128 | 129 | # End of https://www.toptal.com/developers/gitignore/api/node 130 | 131 | .pnp.* 132 | .yarn/* 133 | !.yarn/patches 134 | !.yarn/plugins 135 | !.yarn/releases 136 | !.yarn/sdks 137 | 138 | docs/ 139 | 140 | .turbo/ 141 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.size-limit.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "packages/anchor-contrib/dist/esm/index.js", 4 | "limit": "12 KB", 5 | "ignore": [ 6 | "@coral-xyz/anchor", 7 | "@solana/web3.js", 8 | "@saberhq/solana-contrib" 9 | ], 10 | "webpack": true 11 | }, 12 | { 13 | "path": "packages/solana-contrib/dist/esm/index.js", 14 | "limit": "25 KB", 15 | "ignore": ["@solana/web3.js", "bn.js"], 16 | "webpack": true 17 | }, 18 | { 19 | "path": "packages/token-utils/dist/esm/index.js", 20 | "limit": "50 KB", 21 | "ignore": ["@solana/spl-token", "@solana/web3.js", "bn.js"], 22 | "webpack": true 23 | }, 24 | { 25 | "path": "packages/use-solana/dist/esm/index.js", 26 | "limit": "50 KB", 27 | "ignore": [ 28 | "@solana/spl-token", 29 | "@solana/web3.js", 30 | "bn.js", 31 | "@ledgerhq/hw-transport", 32 | "@ledgerhq/hw-transport-webusb", 33 | "@nightlylabs/wallet-solana-adapter", 34 | "@solana/wallet-adapter-base", 35 | "@solana/wallet-adapter-clover", 36 | "@solana/wallet-adapter-coin98", 37 | "@solana/wallet-adapter-exodus", 38 | "@solana/wallet-adapter-glow", 39 | "@solana/wallet-adapter-huobi", 40 | "@solana/wallet-adapter-mathwallet", 41 | "@solana/wallet-adapter-phantom", 42 | "@solana/wallet-adapter-slope", 43 | "@solana/wallet-adapter-solflare", 44 | "@solana/wallet-adapter-sollet", 45 | "@solana/wallet-adapter-solong", 46 | "@solana/wallet-adapter-walletconnect" 47 | ], 48 | "webpack": true 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": ".yarn/sdks/typescript/lib", 3 | "search.exclude": { 4 | "**/.yarn": true, 5 | "**/.pnp.*": true 6 | }, 7 | "workbench.colorCustomizations": { 8 | "titleBar.activeBackground": "#6764FB", 9 | "titleBar.inactiveBackground": "#6764FB", 10 | "titleBar.activeForeground": "#000000", 11 | "titleBar.inactiveForeground": "#000000" 12 | }, 13 | "eslint.nodePath": ".yarn/sdks", 14 | "prettier.prettierPath": ".yarn/sdks/prettier/index.cjs", 15 | "typescript.enablePromptUseWorkspaceTsdk": true, 16 | "cSpell.words": ["saberhq"] 17 | } 18 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/bin/eslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absRequire = createRequire(absPnpApiPath); 12 | 13 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 14 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 15 | 16 | if (existsSync(absPnpApiPath)) { 17 | if (!process.versions.pnp) { 18 | // Setup the environment to be able to require eslint/bin/eslint.js 19 | require(absPnpApiPath).setup(); 20 | if (isPnpLoaderEnabled && register) { 21 | register(pathToFileURL(absPnpLoaderPath)); 22 | } 23 | } 24 | } 25 | 26 | // Defer to the real eslint/bin/eslint.js your application uses 27 | module.exports = absRequire(`eslint/bin/eslint.js`); 28 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absRequire = createRequire(absPnpApiPath); 12 | 13 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 14 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 15 | 16 | if (existsSync(absPnpApiPath)) { 17 | if (!process.versions.pnp) { 18 | // Setup the environment to be able to require eslint 19 | require(absPnpApiPath).setup(); 20 | if (isPnpLoaderEnabled && register) { 21 | register(pathToFileURL(absPnpLoaderPath)); 22 | } 23 | } 24 | } 25 | 26 | // Defer to the real eslint your application uses 27 | module.exports = absRequire(`eslint`); 28 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/lib/unsupported-api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absRequire = createRequire(absPnpApiPath); 12 | 13 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 14 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 15 | 16 | if (existsSync(absPnpApiPath)) { 17 | if (!process.versions.pnp) { 18 | // Setup the environment to be able to require eslint/use-at-your-own-risk 19 | require(absPnpApiPath).setup(); 20 | if (isPnpLoaderEnabled && register) { 21 | register(pathToFileURL(absPnpLoaderPath)); 22 | } 23 | } 24 | } 25 | 26 | // Defer to the real eslint/use-at-your-own-risk your application uses 27 | module.exports = absRequire(`eslint/use-at-your-own-risk`); 28 | -------------------------------------------------------------------------------- /.yarn/sdks/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint", 3 | "version": "8.57.0-sdk", 4 | "main": "./lib/api.js", 5 | "type": "commonjs", 6 | "bin": { 7 | "eslint": "./bin/eslint.js" 8 | }, 9 | "exports": { 10 | "./package.json": "./package.json", 11 | ".": "./lib/api.js", 12 | "./use-at-your-own-risk": "./lib/unsupported-api.js" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.yarn/sdks/integrations.yml: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by @yarnpkg/sdks. 2 | # Manual changes might be lost! 3 | 4 | integrations: 5 | - vscode 6 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/bin/prettier.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absRequire = createRequire(absPnpApiPath); 12 | 13 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 14 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 15 | 16 | if (existsSync(absPnpApiPath)) { 17 | if (!process.versions.pnp) { 18 | // Setup the environment to be able to require prettier/bin/prettier.cjs 19 | require(absPnpApiPath).setup(); 20 | if (isPnpLoaderEnabled && register) { 21 | register(pathToFileURL(absPnpLoaderPath)); 22 | } 23 | } 24 | } 25 | 26 | // Defer to the real prettier/bin/prettier.cjs your application uses 27 | module.exports = absRequire(`prettier/bin/prettier.cjs`); 28 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/index.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absRequire = createRequire(absPnpApiPath); 12 | 13 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 14 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 15 | 16 | if (existsSync(absPnpApiPath)) { 17 | if (!process.versions.pnp) { 18 | // Setup the environment to be able to require prettier 19 | require(absPnpApiPath).setup(); 20 | if (isPnpLoaderEnabled && register) { 21 | register(pathToFileURL(absPnpLoaderPath)); 22 | } 23 | } 24 | } 25 | 26 | // Defer to the real prettier your application uses 27 | module.exports = absRequire(`prettier`); 28 | -------------------------------------------------------------------------------- /.yarn/sdks/prettier/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prettier", 3 | "version": "3.2.5-sdk", 4 | "main": "./index.cjs", 5 | "type": "commonjs", 6 | "bin": "./bin/prettier.cjs" 7 | } 8 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absRequire = createRequire(absPnpApiPath); 12 | 13 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 14 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 15 | 16 | if (existsSync(absPnpApiPath)) { 17 | if (!process.versions.pnp) { 18 | // Setup the environment to be able to require typescript/bin/tsc 19 | require(absPnpApiPath).setup(); 20 | if (isPnpLoaderEnabled && register) { 21 | register(pathToFileURL(absPnpLoaderPath)); 22 | } 23 | } 24 | } 25 | 26 | // Defer to the real typescript/bin/tsc your application uses 27 | module.exports = absRequire(`typescript/bin/tsc`); 28 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/bin/tsserver: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absRequire = createRequire(absPnpApiPath); 12 | 13 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 14 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 15 | 16 | if (existsSync(absPnpApiPath)) { 17 | if (!process.versions.pnp) { 18 | // Setup the environment to be able to require typescript/bin/tsserver 19 | require(absPnpApiPath).setup(); 20 | if (isPnpLoaderEnabled && register) { 21 | register(pathToFileURL(absPnpLoaderPath)); 22 | } 23 | } 24 | } 25 | 26 | // Defer to the real typescript/bin/tsserver your application uses 27 | module.exports = absRequire(`typescript/bin/tsserver`); 28 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/tsc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absRequire = createRequire(absPnpApiPath); 12 | 13 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 14 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 15 | 16 | if (existsSync(absPnpApiPath)) { 17 | if (!process.versions.pnp) { 18 | // Setup the environment to be able to require typescript/lib/tsc.js 19 | require(absPnpApiPath).setup(); 20 | if (isPnpLoaderEnabled && register) { 21 | register(pathToFileURL(absPnpLoaderPath)); 22 | } 23 | } 24 | } 25 | 26 | // Defer to the real typescript/lib/tsc.js your application uses 27 | module.exports = absRequire(`typescript/lib/tsc.js`); 28 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/lib/typescript.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const {existsSync} = require(`fs`); 4 | const {createRequire, register} = require(`module`); 5 | const {resolve} = require(`path`); 6 | const {pathToFileURL} = require(`url`); 7 | 8 | const relPnpApiPath = "../../../../.pnp.cjs"; 9 | 10 | const absPnpApiPath = resolve(__dirname, relPnpApiPath); 11 | const absRequire = createRequire(absPnpApiPath); 12 | 13 | const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`); 14 | const isPnpLoaderEnabled = existsSync(absPnpLoaderPath); 15 | 16 | if (existsSync(absPnpApiPath)) { 17 | if (!process.versions.pnp) { 18 | // Setup the environment to be able to require typescript 19 | require(absPnpApiPath).setup(); 20 | if (isPnpLoaderEnabled && register) { 21 | register(pathToFileURL(absPnpLoaderPath)); 22 | } 23 | } 24 | } 25 | 26 | // Defer to the real typescript your application uses 27 | module.exports = absRequire(`typescript`); 28 | -------------------------------------------------------------------------------- /.yarn/sdks/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript", 3 | "version": "5.4.5-sdk", 4 | "main": "./lib/typescript.js", 5 | "type": "commonjs", 6 | "bin": { 7 | "tsc": "./bin/tsc", 8 | "tsserver": "./bin/tsserver" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: pnp 6 | 7 | yarnPath: .yarn/releases/yarn-4.3.1.cjs 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `saber-common` 2 | 3 | Common libraries across Saber projects. 4 | 5 | ## Documentation 6 | 7 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 8 | 9 | Automatically generated TypeScript documentation can be found [on GitHub pages](https://saber-hq.github.io/saber-common/). 10 | 11 | ### Common Errors 12 | 13 | #### Module parse failed: Unexpected token 14 | 15 | `saber-common` [targets ES2019](packages/tsconfig/tsconfig.lib.json), which is [widely supported by modern DApp browsers](https://caniuse.com/?search=es2019). Please ensure that your build pipeline supports this version of ECMAScript. 16 | 17 | ## Packages 18 | 19 | | Package | Description | Version | 20 | | :----------------------------- | :--------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- | 21 | | `@saberhq/anchor-contrib` | TypeScript client for Anchor programs | [![npm](https://img.shields.io/npm/v/@saberhq/anchor-contrib.svg)](https://www.npmjs.com/package/@saberhq/anchor-contrib) | 22 | | `@saberhq/browserslist-config` | Saber shareable config for Browserslist. | [![npm](https://img.shields.io/npm/v/@saberhq/browserslist-config.svg)](https://www.npmjs.com/package/@saberhq/browserslist-config) | 23 | | `@saberhq/chai-solana` | Chai test helpers | [![npm](https://img.shields.io/npm/v/@saberhq/chai-solana.svg)](https://www.npmjs.com/package/@saberhq/chai-solana) | 24 | | `@saberhq/solana-contrib` | Solana TypeScript utilities | [![npm](https://img.shields.io/npm/v/@saberhq/solana-contrib.svg)](https://www.npmjs.com/package/@saberhq/solana-contrib) | 25 | | `@saberhq/stableswap-sdk` | StableSwap SDK | [![npm](https://img.shields.io/npm/v/@saberhq/stableswap-sdk.svg)](https://www.npmjs.com/package/@saberhq/stableswap-sdk) | 26 | | `@saberhq/token-utils` | SPL Token arithmetic and types | [![npm](https://img.shields.io/npm/v/@saberhq/token-utils.svg)](https://www.npmjs.com/package/@saberhq/token-utils) | 27 | | `@saberhq/use-solana` | Solana React library | [![npm](https://img.shields.io/npm/v/@saberhq/use-solana.svg)](https://www.npmjs.com/package/@saberhq/use-solana) | 28 | 29 | ## Release 30 | 31 | To release a new version of Saber Common, navigate to [the release action page](https://github.com/saber-hq/saber-common/actions/workflows/release.yml) and click "Run workflow". 32 | 33 | There, you may specify `patch`, `minor`, or `major`. 34 | 35 | ## Join Us 36 | 37 | We're looking for contributors! Reach out to team@saber.so or message **michaelhly** on [Keybase](https://keybase.io/) with any questions. 38 | 39 | ## License 40 | 41 | Saber Common is licensed under the Apache License, Version 2.0. 42 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1694529238, 9 | "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1701237617, 24 | "narHash": "sha256-Ryd8xpNDY9MJnBFDYhB37XSFIxCPVVVXAbInNPa95vs=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "85306ef2470ba705c97ce72741d56e42d0264015", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Saber-common development environment"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | flake-utils.url = "github:numtide/flake-utils"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | flake-utils.lib.eachDefaultSystem 11 | (system: 12 | let 13 | pkgs = import nixpkgs { inherit system; }; 14 | in 15 | with pkgs; 16 | rec { 17 | packages.ci = buildEnv { 18 | name = "ci"; 19 | paths = [ nodejs yarn nixpkgs-fmt bash ]; 20 | }; 21 | devShell = mkShell { buildInputs = [ packages.ci ]; }; 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /jest.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 4 | export default { 5 | preset: "ts-jest/presets/default-esm", 6 | globals: { 7 | "ts-jest": { 8 | useESM: true, 9 | }, 10 | }, 11 | moduleNameMapper: { 12 | "^(\\.{1,2}/.*)\\.js$": "$1", 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "name": "@saberhq/saber-common", 5 | "description": "Common libraries across Saber projects.", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "lint-staged": { 10 | "*.nix": "nixpkgs-fmt", 11 | "*.{ts,tsx,mts,cts}": "eslint --cache --fix", 12 | "*.{md,js,jsx,json,yml,yaml,css,md}": "prettier --write" 13 | }, 14 | "devDependencies": { 15 | "@babel/core": "^7.24.4", 16 | "@babel/preset-env": "^7.24.4", 17 | "@babel/preset-typescript": "^7.24.1", 18 | "@coral-xyz/anchor": "^0.29.0", 19 | "@jest/types": "^29.6.3", 20 | "@rushstack/eslint-patch": "^1.10.2", 21 | "@saberhq/eslint-config": "^3.3.1", 22 | "@saberhq/eslint-config-react": "^3.3.1", 23 | "@saberhq/tsconfig": "^3.3.1", 24 | "@saberhq/use-solana": "workspace:*", 25 | "@size-limit/file": "^11.1.2", 26 | "@size-limit/webpack": "^11.1.2", 27 | "@size-limit/webpack-why": "^11.1.2", 28 | "@solana/web3.js": "^1.91.1", 29 | "@types/babel__core": "^7.20.5", 30 | "@types/babel__preset-env": "^7.9.6", 31 | "@types/bn.js": "^5.1.5", 32 | "@types/eslint": "^8.56.9", 33 | "@types/jest": "^29.5.12", 34 | "@types/node": "^20.12.7", 35 | "@types/react": "^18.2.77", 36 | "@types/source-map-support": "^0.5.10", 37 | "@types/w3c-web-usb": "^1.0.10", 38 | "@typescript-eslint/utils": "^7.6.0", 39 | "@yarnpkg/doctor": "^4.0.1", 40 | "bn.js": "^5.2.1", 41 | "buffer": "^6.0.3", 42 | "eslint": "^8.57.0", 43 | "husky": "^9.0.11", 44 | "jest": "^29.7.0", 45 | "jest-runtime": "^29.7.0", 46 | "jsbi": "^4.3.0", 47 | "lint-staged": "^15.2.2", 48 | "prettier": "^3.2.5", 49 | "react": "^18.2.0", 50 | "size-limit": "^11.1.2", 51 | "source-map-support": "^0.5.21", 52 | "ts-jest": "^29.1.2", 53 | "ts-node": "^10.9.2", 54 | "turbo": "^2.0.7", 55 | "typedoc": "^0.25.13", 56 | "typescript": "^5.4.5" 57 | }, 58 | "scripts": { 59 | "build": "turbo build", 60 | "clean": "turbo clean", 61 | "publish:all": "yarn workspaces foreach --all --exclude @saberhq/saber-common -ptv npm publish", 62 | "typecheck": "tsc --build", 63 | "lint": "eslint . --cache", 64 | "lint:fix": "eslint . --cache --fix", 65 | "lint:ci": "eslint . --max-warnings=0", 66 | "prepare": "husky install", 67 | "size": "size-limit", 68 | "analyze": "size-limit --why", 69 | "docs:generate": "typedoc --excludePrivate --includeVersion --out docs/ --entryPointStrategy packages --includes packages/ $(find ./packages -mindepth 1 -maxdepth 1 | grep -v browserslist)", 70 | "test": "jest **/*.test.ts", 71 | "doctor:packages": "yarn doctor packages/" 72 | }, 73 | "version": "3.0.0", 74 | "packageManager": "yarn@4.3.1" 75 | } 76 | -------------------------------------------------------------------------------- /packages/anchor-contrib/README.md: -------------------------------------------------------------------------------- 1 | # @saberhq/anchor-contrib 2 | 3 | TypeScript client for Anchor programs. 4 | 5 | ## Documentation 6 | 7 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 8 | 9 | Automatically generated TypeScript documentation can be found [on GitHub pages](https://saber-hq.github.io/saber-common/). 10 | 11 | ## License 12 | 13 | Saber Common is licensed under the Apache License, Version 2.0. 14 | -------------------------------------------------------------------------------- /packages/anchor-contrib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saberhq/anchor-contrib", 3 | "version": "3.0.0", 4 | "description": "TypeScript client for Anchor programs.", 5 | "homepage": "https://github.com/saber-hq/saber-common/tree/master/packages/anchor-contrib#readme", 6 | "repository": "git+https://github.com/saber-hq/saber-common.git", 7 | "bugs": "https://github.com/saber-hq/saber-common/issues", 8 | "funding": "https://www.coingecko.com/en/coins/saber", 9 | "author": "Saber Team ", 10 | "license": "Apache-2.0", 11 | "exports": { 12 | ".": { 13 | "import": "./dist/esm/index.js", 14 | "require": "./dist/cjs/index.js" 15 | } 16 | }, 17 | "main": "dist/cjs/index.js", 18 | "module": "dist/esm/index.js", 19 | "scripts": { 20 | "build": "tsc && tsc -P tsconfig.cjs.json", 21 | "clean": "rm -fr dist/", 22 | "prepublishOnly": "npm run build" 23 | }, 24 | "peerDependencies": { 25 | "@coral-xyz/anchor": "^0.22 || ^0.23 || ^0.24 || ^0.28 || ^0.29", 26 | "@solana/web3.js": "^1.42", 27 | "bn.js": "^4 || ^5" 28 | }, 29 | "devDependencies": { 30 | "@coral-xyz/anchor": "^0.29.0", 31 | "@saberhq/tsconfig": "^3.3.1", 32 | "@solana/web3.js": "^1.91.1", 33 | "@types/lodash.camelcase": "^4.3.9", 34 | "@types/lodash.mapvalues": "^4.6.9", 35 | "bn.js": "^5.2.1", 36 | "typescript": "^5.4.5" 37 | }, 38 | "dependencies": { 39 | "@saberhq/solana-contrib": "workspace:^", 40 | "eventemitter3": "^4.0.7", 41 | "lodash.camelcase": "^4.3.0", 42 | "lodash.mapvalues": "^4.6.0", 43 | "tslib": "^2.6.2" 44 | }, 45 | "files": [ 46 | "dist/", 47 | "src/" 48 | ], 49 | "publishConfig": { 50 | "access": "public" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/anchor-contrib/src/errors.ts: -------------------------------------------------------------------------------- 1 | import type { Idl } from "@coral-xyz/anchor"; 2 | import type { IdlErrorCode } from "@coral-xyz/anchor/dist/esm/idl.js"; 3 | 4 | import type { AnchorError } from "./index.js"; 5 | 6 | export type ErrorMap = { 7 | [K in AnchorError["name"]]: AnchorError & { name: K }; 8 | }; 9 | 10 | /** 11 | * Generates the error mapping 12 | * @param idl 13 | * @returns 14 | */ 15 | export const generateErrorMap = (idl: T): ErrorMap => { 16 | return (idl.errors?.reduce((acc, err) => { 17 | return { 18 | ...acc, 19 | [err.name]: err, 20 | }; 21 | }, {}) ?? {}) as ErrorMap; 22 | }; 23 | 24 | /** 25 | * Returns a RegExp which matches the message of a program error. 26 | * @param err 27 | * @returns 28 | */ 29 | export const matchError = (err: IdlErrorCode): RegExp => 30 | matchErrorCode(err.code); 31 | 32 | /** 33 | * Returns a RegExp which matches the code of a custom program error. 34 | * @param err 35 | * @returns 36 | */ 37 | export const matchErrorCode = (code: number): RegExp => 38 | new RegExp(`custom program error: 0x${code.toString(16)}`); 39 | -------------------------------------------------------------------------------- /packages/anchor-contrib/src/generateAccountParsers.ts: -------------------------------------------------------------------------------- 1 | import type { AccountsCoder, Idl } from "@coral-xyz/anchor"; 2 | import { BorshAccountsCoder } from "@coral-xyz/anchor"; 3 | import camelCase from "lodash.camelcase"; 4 | 5 | /** 6 | * Parsers associated with an IDL. 7 | */ 8 | export type AccountParsers = { 9 | [K in keyof M]: (data: Buffer) => M[K]; 10 | }; 11 | 12 | /** 13 | * Creates parsers for accounts. 14 | * 15 | * This is intended to be called once at initialization. 16 | * 17 | * @param idl The IDL. 18 | */ 19 | export const generateAccountParsers = >( 20 | idl: Idl, 21 | ): AccountParsers => { 22 | const coder = new BorshAccountsCoder(idl); 23 | return generateAccountParsersFromCoder( 24 | idl.accounts?.map((a) => a.name), 25 | coder, 26 | ); 27 | }; 28 | 29 | /** 30 | * Creates parsers for accounts. 31 | * 32 | * This is intended to be called once at initialization. 33 | * 34 | * @param idl The IDL. 35 | */ 36 | export const generateAccountParsersFromCoder = ( 37 | accountNames: string[] | undefined, 38 | coder: AccountsCoder, 39 | ): AccountParsers => { 40 | return (accountNames ?? []).reduce((parsers, account) => { 41 | parsers[camelCase(account) as keyof M] = (data: Buffer) => 42 | coder.decode(account, data); 43 | return parsers; 44 | }, {} as AccountParsers); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/anchor-contrib/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:anchor-contrib/README.md]] 3 | * @module 4 | */ 5 | 6 | export * from "./errors.js"; 7 | export * from "./generateAccountParsers.js"; 8 | export * from "./types.js"; 9 | export * from "./utils/index.js"; 10 | -------------------------------------------------------------------------------- /packages/anchor-contrib/src/utils/accounts.ts: -------------------------------------------------------------------------------- 1 | import type { AccountsCoder } from "@coral-xyz/anchor"; 2 | import { BorshAccountsCoder } from "@coral-xyz/anchor"; 3 | import type { IdlTypeDef } from "@coral-xyz/anchor/dist/esm/idl.js"; 4 | import type { ProgramAccountParser, PublicKey } from "@saberhq/solana-contrib"; 5 | import camelCase from "lodash.camelcase"; 6 | 7 | /** 8 | * Account information. 9 | */ 10 | export interface AnchorAccount extends ProgramAccountParser { 11 | /** 12 | * {@link IdlTypeDef}. 13 | */ 14 | idl: IdlTypeDef; 15 | /** 16 | * Size of the account in bytes 17 | */ 18 | size: number; 19 | /** 20 | * The discriminator. 21 | */ 22 | discriminator: Buffer; 23 | /** 24 | * Encodes the value. 25 | */ 26 | encode: (value: T) => Promise; 27 | } 28 | 29 | /** 30 | * {@link ProgramAccountParser}s associated with an IDL. 31 | */ 32 | export type AnchorAccountMap = { 33 | [K in keyof M]: AnchorAccount; 34 | }; 35 | /** 36 | * Generates the metadata of accounts. 37 | * 38 | * This is intended to be called once at initialization. 39 | */ 40 | export const generateAnchorAccounts = ( 41 | programID: PublicKey, 42 | accounts: IdlTypeDef[], 43 | coder: AccountsCoder, 44 | ): AnchorAccountMap => { 45 | const parsers: Partial> = {}; 46 | accounts.forEach((account) => { 47 | parsers[camelCase(account.name) as keyof M] = { 48 | programID, 49 | name: account.name, 50 | encode: (value) => coder.encode(account.name, value), 51 | parse: (data: Buffer) => coder.decode(account.name, data), 52 | idl: account, 53 | size: coder.size(account), 54 | discriminator: BorshAccountsCoder.accountDiscriminator(account.name), 55 | }; 56 | }); 57 | return parsers as AnchorAccountMap; 58 | }; 59 | -------------------------------------------------------------------------------- /packages/anchor-contrib/src/utils/idl.ts: -------------------------------------------------------------------------------- 1 | import type { IdlType } from "@coral-xyz/anchor/dist/esm/idl.js"; 2 | 3 | /** 4 | * Formats an IDL type as a string. This comes straight from the Anchor source. 5 | * @param idlType 6 | * @returns 7 | */ 8 | export const formatIdlType = (idlType: IdlType): string => { 9 | if (typeof idlType === "string") { 10 | return idlType; 11 | } 12 | 13 | if ("vec" in idlType) { 14 | return `Vec<${formatIdlType(idlType.vec)}>`; 15 | } 16 | if ("option" in idlType) { 17 | return `Option<${formatIdlType(idlType.option)}>`; 18 | } 19 | if ("defined" in idlType) { 20 | return idlType.defined; 21 | } 22 | if ("array" in idlType) { 23 | return `Array<${formatIdlType(idlType.array[0])}; ${idlType.array[1]}>`; 24 | } 25 | throw new Error(`Unknown IDL type: ${JSON.stringify(idlType)}`); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/anchor-contrib/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./coder.js"; 2 | export * from "./idl.js"; 3 | export * from "./programs.js"; 4 | export * from "./provider.js"; 5 | -------------------------------------------------------------------------------- /packages/anchor-contrib/src/utils/programs.ts: -------------------------------------------------------------------------------- 1 | import type { Idl } from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import type { 4 | Provider as SaberProvider, 5 | ReadonlyProvider as ReadonlySaberProvider, 6 | } from "@saberhq/solana-contrib"; 7 | import type { PublicKey } from "@solana/web3.js"; 8 | import mapValues from "lodash.mapvalues"; 9 | 10 | import { makeAnchorProvider } from "./provider.js"; 11 | 12 | /** 13 | * Builds a program from its IDL. 14 | * 15 | * @param idl 16 | * @param address 17 | * @param provider 18 | * @returns 19 | */ 20 | export const newProgram =

( 21 | idl: Idl, 22 | address: PublicKey, 23 | provider: SaberProvider | ReadonlySaberProvider, 24 | ) => { 25 | return new Program( 26 | idl, 27 | address.toString(), 28 | makeAnchorProvider(provider), 29 | ) as unknown as P; 30 | }; 31 | 32 | /** 33 | * Builds a map of programs from their IDLs and addresses. 34 | * 35 | * @param provider 36 | * @param programs 37 | * @returns 38 | */ 39 | export const newProgramMap =

( 40 | provider: SaberProvider | ReadonlySaberProvider, 41 | idls: { 42 | [K in keyof P]: Idl; 43 | }, 44 | addresses: { 45 | [K in keyof P]: PublicKey; 46 | }, 47 | ): { 48 | [K in keyof P]: P[K]; 49 | } => { 50 | return mapValues(idls, (idl, k: keyof P) => 51 | newProgram(idl, addresses[k], provider), 52 | ) as unknown as { 53 | [K in keyof P]: P[K]; 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /packages/anchor-contrib/src/utils/provider.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AnchorProvider as AnchorProviderImpl, 3 | Provider as IAnchorProvider, 4 | } from "@coral-xyz/anchor"; 5 | import * as anchor from "@coral-xyz/anchor"; 6 | import type { 7 | Provider as SaberProvider, 8 | ReadonlyProvider as ReadonlySaberProvider, 9 | Wallet, 10 | } from "@saberhq/solana-contrib"; 11 | import { 12 | SolanaProvider, 13 | SolanaReadonlyProvider, 14 | } from "@saberhq/solana-contrib"; 15 | import type { ConfirmOptions, Connection } from "@solana/web3.js"; 16 | 17 | /** 18 | * Interface of an AnchorProvider. 19 | */ 20 | export interface AnchorProvider extends IAnchorProvider { 21 | wallet: Wallet; 22 | opts: ConfirmOptions; 23 | } 24 | 25 | const anchorModule = anchor; 26 | 27 | /** 28 | * Class used to create new {@link AnchorProvider}s. 29 | */ 30 | export const AnchorProviderClass: AnchorProviderCtor & 31 | typeof AnchorProviderImpl = 32 | "AnchorProvider" in anchorModule 33 | ? anchorModule.AnchorProvider 34 | : ( 35 | anchorModule as unknown as { 36 | Provider: AnchorProviderCtor & typeof AnchorProviderImpl; 37 | } 38 | ).Provider; 39 | 40 | /** 41 | * Constructor for an Anchor provider. 42 | */ 43 | export type AnchorProviderCtor = new ( 44 | connection: Connection, 45 | wallet: Wallet, 46 | opts: ConfirmOptions, 47 | ) => AnchorProvider; 48 | 49 | /** 50 | * Create a new Anchor provider. 51 | * 52 | * @param connection 53 | * @param wallet 54 | * @param opts 55 | * @returns 56 | */ 57 | export const buildAnchorProvider = ( 58 | connection: Connection, 59 | wallet: Wallet, 60 | opts: ConfirmOptions, 61 | ) => { 62 | return new AnchorProviderClass(connection, wallet, opts); 63 | }; 64 | 65 | /** 66 | * Creates a readonly Saber Provider from an Anchor provider. 67 | * @param anchorProvider The Anchor provider. 68 | * @returns 69 | */ 70 | export const makeReadonlySaberProvider = ( 71 | anchorProvider: IAnchorProvider, 72 | ): ReadonlySaberProvider => { 73 | return new SolanaReadonlyProvider(anchorProvider.connection); 74 | }; 75 | 76 | /** 77 | * Creates a Saber Provider from an Anchor provider. 78 | * @param anchorProvider The Anchor provider. 79 | * @returns 80 | */ 81 | export const makeSaberProvider = ( 82 | anchorProvider: AnchorProvider, 83 | ): SaberProvider => { 84 | return SolanaProvider.init({ 85 | connection: anchorProvider.connection, 86 | wallet: anchorProvider.wallet, 87 | opts: anchorProvider.opts, 88 | }); 89 | }; 90 | 91 | /** 92 | * Creates an Anchor Provider from a Saber provider. 93 | * @param saberProvider 94 | * @returns 95 | */ 96 | export const makeAnchorProvider = ( 97 | saberProvider: ReadonlySaberProvider, 98 | ): AnchorProvider => { 99 | return buildAnchorProvider( 100 | saberProvider.connection, 101 | saberProvider.wallet, 102 | saberProvider.opts, 103 | ); 104 | }; 105 | -------------------------------------------------------------------------------- /packages/anchor-contrib/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.cjs.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs/" 5 | }, 6 | "include": ["src/"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/anchor-contrib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.mono.json", 3 | "compilerOptions": { 4 | "rootDir": "src/", 5 | "outDir": "dist/esm/" 6 | }, 7 | "include": ["src/"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/chai-solana/README.md: -------------------------------------------------------------------------------- 1 | # @saberhq/chai-solana 2 | 3 | Chai helpers for Solana tests. 4 | 5 | - Address/`PublicKey` comparisons 6 | - `TokenAmount` comparisons 7 | - Transaction envelope validations 8 | 9 | ## Documentation 10 | 11 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 12 | 13 | Automatically generated TypeScript documentation can be found [on GitHub pages](https://saber-hq.github.io/saber-common/). 14 | 15 | ## Installation 16 | 17 | ``` 18 | yarn add @saberhq/chai-solana 19 | ``` 20 | 21 | ## Examples 22 | 23 | The best way to learn how to use `chai-solana` is by example. View examples in the integration tests here: 24 | 25 | - [Saber Merkle Distributor](https://github.com/saber-hq/merkle-distributor) 26 | - [Saber Voter Snapshots](https://github.com/saber-hq/snapshots) 27 | 28 | ## Common Issues 29 | 30 | ### Invalid Chai property: eventually 31 | 32 | Downgrade to Chai v4.3.4. Versions after this changed the way Chai bundled its dependencies, causing issues in how `chai-as-promised` is installed. 33 | 34 | ## License 35 | 36 | Apache 2.0 37 | -------------------------------------------------------------------------------- /packages/chai-solana/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saberhq/chai-solana", 3 | "version": "3.0.0", 4 | "description": "Solana Chai helpers", 5 | "homepage": "https://github.com/saber-hq/saber-common/tree/master/packages/chai-solana#readme", 6 | "repository": "git+https://github.com/saber-hq/saber-common.git", 7 | "bugs": "https://github.com/saber-hq/saber-common/issues", 8 | "author": "Saber Team ", 9 | "license": "Apache-2.0", 10 | "type": "module", 11 | "scripts": { 12 | "build": "tsc && tsc --project tsconfig.cjs.json", 13 | "clean": "rm -fr dist/", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "exports": { 17 | ".": { 18 | "import": "./dist/esm/index.js", 19 | "require": "./dist/cjs/index.js" 20 | } 21 | }, 22 | "main": "dist/cjs/index.js", 23 | "module": "dist/esm/index.js", 24 | "files": [ 25 | "dist/", 26 | "src/" 27 | ], 28 | "dependencies": { 29 | "@saberhq/anchor-contrib": "workspace:^", 30 | "@saberhq/solana-contrib": "workspace:^", 31 | "@saberhq/token-utils": "workspace:^", 32 | "@types/chai": "^4.3.14", 33 | "@types/chai-as-promised": "^7.1.8", 34 | "@types/promise-retry": "^1.1.6", 35 | "chai": "^5.1.0", 36 | "chai-as-promised": "^7.1.1", 37 | "chai-bn": "^0.3.1", 38 | "colors": "^1.4.0", 39 | "tslib": "^2.6.2" 40 | }, 41 | "gitHead": "f9fd3fbd36a7a6dd6f5e9597af5309affe50ac0e", 42 | "publishConfig": { 43 | "access": "public" 44 | }, 45 | "peerDependencies": { 46 | "@coral-xyz/anchor": ">=0.17", 47 | "@solana/web3.js": "^1.42", 48 | "bn.js": "^5.2.0", 49 | "jsbi": "*" 50 | }, 51 | "devDependencies": { 52 | "@coral-xyz/anchor": "^0.29.0", 53 | "@saberhq/tsconfig": "^3.3.1", 54 | "@solana/web3.js": "^1.91.1", 55 | "bn.js": "^5.2.1", 56 | "jsbi": "^4.3.0", 57 | "typescript": "^5.4.5" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/chai-solana/src/debugAccountOwners.ts: -------------------------------------------------------------------------------- 1 | import { AnchorProviderClass } from "@saberhq/anchor-contrib"; 2 | import { printAccountOwners } from "@saberhq/solana-contrib"; 3 | 4 | /** 5 | * A wrapper around `printAccountOwners` that loads the connection from env(). 6 | * This is useful for people who are too lazy to pass in a connection. 7 | * 8 | * -------- 9 | * 10 | * A useful tool for debugging account structs. It gives a quick glance at 11 | * addresses and owners. It also converts bignums into JS numbers. 12 | * 13 | * Types converted: 14 | * - **big numbers**: converted to native numbers 15 | * - **addresses**: format in base58, and prints the owner in parentheses if the account exists 16 | * - **plain objects**: recursively converts 17 | * 18 | * HINT: This function is mainly useful for the browser. If you are writing 19 | * Rust integration tests, use debugAccountOwners from chai-solana instead, so 20 | * that you don't have to pass in connection. 21 | * 22 | * Usage: 23 | * ``` 24 | * await debugAccountOwners(depositAccounts); // using await is recommended, due to race conditions 25 | * void debugAccountOwners(depositAccounts); // don't do this in tests, there may be race conditions 26 | * ``` 27 | * 28 | * Example output: 29 | * ``` 30 | * tests/awesomeTest.spec.ts:583:29 { 31 | * payer: 'CEGhKVeyXUrihUnNU9EchSuu6pMHEsB8MiKgvhJqYgd1 (11111111111111111111111111111111)', 32 | * foo: '61tMNVhG66QZQ4UEAoHytqaUN4G1xpk1zsS5YU7Y2Qui (135QzSyjKTKaZ7ebhLpvNA2KUahEjykMjbqz3JV1V4k9)', 33 | * bar: '9oPMxXVSm5msAecxi4zJpKDwbHS9c6Yos1ru739rVExc (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA)', 34 | * tokenProgram: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA (BPFLoader2111111111111111111111111111111111)' 35 | * } 36 | * ``` 37 | * 38 | * WARNING: This may break silently if web3 changes its api. This is only 39 | * intended for debugging purposes only. 40 | */ 41 | export function debugAccountOwners(plainObj: object): Promise { 42 | return printAccountOwners(AnchorProviderClass.env().connection, plainObj); 43 | } 44 | -------------------------------------------------------------------------------- /packages/chai-solana/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:chai-solana/README.md]] 3 | * @module 4 | */ 5 | 6 | import "./types.js"; 7 | 8 | import type { Address } from "@coral-xyz/anchor"; 9 | import { BN } from "@coral-xyz/anchor"; 10 | import { TokenAmount } from "@saberhq/token-utils"; 11 | import { PublicKey } from "@solana/web3.js"; 12 | import { default as chaiAsPromised } from "chai-as-promised"; 13 | import { default as chaiBN } from "chai-bn"; 14 | 15 | export * from "./debugAccountOwners.js"; 16 | export * from "./expectTXTable.js"; 17 | export * from "./printInstructionLogs.js"; 18 | export * from "./utils.js"; 19 | 20 | export const chaiSolana: Chai.ChaiPlugin = (chai) => { 21 | chai.use( 22 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 23 | // @ts-ignore 24 | chaiBN(BN) as Chai.ChaiPlugin, 25 | ); 26 | chai.use(chaiAsPromised); 27 | chai.config.includeStack = true; 28 | 29 | chai.use((chai) => { 30 | chai.Assertion.addProperty( 31 | "tokenAmount", 32 | function (): Chai.TokenAmountAssertion { 33 | const assert = this.assert.bind(this); 34 | const obj = this._obj as unknown; 35 | const equal: Chai.TokenAmountAssertion["equal"] = function ( 36 | value, 37 | message, 38 | ) { 39 | const amount = value instanceof TokenAmount ? value.toU64() : value; 40 | const msgPrefix = message ? `${message}: ` : ""; 41 | 42 | const myAmount = obj as TokenAmount; 43 | if (value instanceof TokenAmount) { 44 | assert( 45 | myAmount.token.equals(value.token), 46 | `${msgPrefix}token mismatch: #{this} to equal #{exp} but got #{act}`, 47 | `${msgPrefix}token mismatch: expected #{this} to not equal #{act}`, 48 | { 49 | address: value.token.address, 50 | decimals: value.token.decimals, 51 | network: value.token.network, 52 | }, 53 | { 54 | address: myAmount.token.address, 55 | decimals: myAmount.token.decimals, 56 | network: myAmount.token.network, 57 | }, 58 | ); 59 | } 60 | 61 | const otherAmt = new TokenAmount(myAmount.token, amount.toString()); 62 | assert( 63 | myAmount.equalTo(otherAmt), 64 | `${msgPrefix}expected #{this} to equal #{exp} but got #{act}`, 65 | `${msgPrefix}expected #{this} to not equal #{exp} but got #{act}`, 66 | otherAmt.format(), 67 | myAmount.format(), 68 | ); 69 | }; 70 | return { 71 | equal, 72 | equals: equal, 73 | eq: equal, 74 | zero: () => { 75 | equal(0); 76 | }, 77 | }; 78 | }, 79 | ); 80 | 81 | chai.Assertion.addMethod( 82 | "eqAddress", 83 | function (otherKey: Address, message?: string) { 84 | const obj = this._obj as unknown; 85 | 86 | this.assert( 87 | (obj as Record)?._bn || 88 | obj instanceof PublicKey || 89 | typeof obj === "string", 90 | "expected #{this} to be a PublicKey or address string", 91 | "expected #{this} to not be a PublicKey or address string", 92 | true, 93 | obj, 94 | ); 95 | const key = obj as Address; 96 | 97 | const myKey = typeof key === "string" ? new PublicKey(key) : key; 98 | const theirKey = 99 | typeof otherKey === "string" ? new PublicKey(otherKey) : otherKey; 100 | 101 | const msgPrefix = message ? `${message}: ` : ""; 102 | 103 | this.assert( 104 | myKey.equals(theirKey), 105 | `${msgPrefix}expected #{this} to equal #{exp} but got #{act}`, 106 | `${msgPrefix}expected #{this} to not equal #{act}`, 107 | otherKey.toString(), 108 | myKey.toString(), 109 | ); 110 | }, 111 | ); 112 | 113 | chai.Assertion.addMethod( 114 | "eqAmount", 115 | function (other: TokenAmount, message?: string) { 116 | const obj = this._obj as unknown; 117 | const myAmount = obj as TokenAmount; 118 | const msgPrefix = message ? `${message}: ` : ""; 119 | 120 | this.assert( 121 | myAmount.token.equals(other.token), 122 | `${msgPrefix}token mismatch: #{this} to equal #{exp} but got #{act}`, 123 | `${msgPrefix}token mismatch: expected #{this} to not equal #{act}`, 124 | myAmount.token, 125 | other.token, 126 | ); 127 | 128 | this.assert( 129 | myAmount.raw.toString() === other.raw.toString(), 130 | `${msgPrefix}expected #{this} to equal #{exp} but got #{act}`, 131 | `${msgPrefix}expected #{this} to not equal #{act}`, 132 | myAmount.toString(), 133 | other.toString(), 134 | ); 135 | }, 136 | ); 137 | }); 138 | }; 139 | -------------------------------------------------------------------------------- /packages/chai-solana/src/printInstructionLogs.ts: -------------------------------------------------------------------------------- 1 | import type { InstructionLogs } from "@saberhq/solana-contrib"; 2 | import { formatLogEntry, parseTransactionLogs } from "@saberhq/solana-contrib"; 3 | import type { SendTransactionError } from "@solana/web3.js"; 4 | import { default as colors } from "colors/safe.js"; 5 | 6 | /** 7 | * Formats instruction logs to be printed to the console. 8 | * @param logs 9 | */ 10 | export const formatInstructionLogsForConsole = ( 11 | logs: readonly InstructionLogs[], 12 | ): string => 13 | logs 14 | .map((log, i) => { 15 | return [ 16 | [ 17 | colors.bold(colors.blue("=> ")), 18 | colors.bold(colors.white(`Instruction #${i}: `)), 19 | log.programAddress 20 | ? colors.yellow(`Program ${log.programAddress}`) 21 | : "System", 22 | ].join(""), 23 | ...log.logs.map((entry) => { 24 | const entryStr = formatLogEntry(entry, true); 25 | switch (entry.type) { 26 | case "text": 27 | return colors.white(entryStr); 28 | case "cpi": 29 | return colors.cyan(entryStr); 30 | case "programError": 31 | return colors.red(entryStr); 32 | case "runtimeError": 33 | return colors.red(entryStr); 34 | case "system": 35 | return colors.white(entryStr); 36 | case "success": 37 | return colors.green(entryStr); 38 | } 39 | }), 40 | ].join("\n"); 41 | }) 42 | .join("\n"); 43 | 44 | export const printSendTransactionError = (err: SendTransactionError) => { 45 | try { 46 | const parsed = parseTransactionLogs(err.logs ?? null, err); 47 | console.log(formatInstructionLogsForConsole(parsed)); 48 | } catch (e) { 49 | console.warn( 50 | colors.yellow("Could not print logs due to error. Printing raw logs"), 51 | e, 52 | ); 53 | console.log(err.logs?.join("\n")); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /packages/chai-solana/src/types.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import "chai-bn"; 4 | import "chai-as-promised"; 5 | 6 | import type { Address } from "@coral-xyz/anchor"; 7 | import type { BigintIsh, TokenAmount } from "@saberhq/token-utils"; 8 | 9 | declare global { 10 | // eslint-disable-next-line @typescript-eslint/no-namespace 11 | namespace Chai { 12 | export interface TokenAmountComparer { 13 | (value: TokenAmount | BigintIsh, message?: string): void; 14 | } 15 | 16 | export interface TokenAmountAssertion { 17 | equal: TokenAmountComparer; 18 | equals: TokenAmountComparer; 19 | eq: TokenAmountComparer; 20 | // above: TokenAmountComparer; 21 | // greaterThan: TokenAmountComparer; 22 | // gt: TokenAmountComparer; 23 | // gte: TokenAmountComparer; 24 | // below: TokenAmountComparer; 25 | // lessThan: TokenAmountComparer; 26 | // lt: TokenAmountComparer; 27 | // lte: TokenAmountComparer; 28 | // least: TokenAmountComparer; 29 | // most: TokenAmountComparer; 30 | // closeTo: BNCloseTo; 31 | // negative: BNBoolean; 32 | zero: () => void; 33 | } 34 | 35 | interface Assertion { 36 | eqAddress: (otherKey: Address, message?: string) => Assertion; 37 | eqAmount: (otherAmount: TokenAmount, message?: string) => Assertion; 38 | tokenAmount: TokenAmountAssertion; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/chai-solana/src/utils.ts: -------------------------------------------------------------------------------- 1 | import "chai-as-promised"; 2 | 3 | import type { Idl } from "@coral-xyz/anchor"; 4 | import type { 5 | PromiseOrValue, 6 | TransactionLike, 7 | TransactionReceipt, 8 | } from "@saberhq/solana-contrib"; 9 | import { confirmTransactionLike } from "@saberhq/solana-contrib"; 10 | import { assert, expect } from "chai"; 11 | 12 | /** 13 | * Processes a transaction, expecting rejection or fulfillment. 14 | * 15 | * @param tx 16 | * @param msg 17 | * @param cb 18 | * @returns 19 | */ 20 | export const expectTX = ( 21 | tx: PromiseOrValue, 22 | msg?: string, 23 | cb?: (receipt: TransactionReceipt) => Promise, 24 | ): Chai.PromisedAssertion => { 25 | const handleReceipt = async (receipt: TransactionReceipt) => { 26 | await cb?.(receipt); 27 | return receipt; 28 | }; 29 | 30 | if (tx && "then" in tx) { 31 | return expect( 32 | tx 33 | .then(async (v) => { 34 | if (v === null) { 35 | throw new Error("transaction is null"); 36 | } 37 | return await confirmTransactionLike(v); 38 | }) 39 | .then(handleReceipt), 40 | msg, 41 | ).eventually; 42 | } else if (tx) { 43 | return expect(confirmTransactionLike(tx).then(handleReceipt), msg) 44 | .eventually; 45 | } else { 46 | return expect(Promise.reject(new Error("transaction is null")), msg) 47 | .eventually; 48 | } 49 | }; 50 | 51 | export type IDLError = NonNullable[number]; 52 | 53 | export const assertError = (error: IDLError, other: IDLError): void => { 54 | assert.strictEqual(error.code, other.code); 55 | assert.strictEqual(error.msg, other.msg); 56 | }; 57 | -------------------------------------------------------------------------------- /packages/chai-solana/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.cjs.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs/" 5 | }, 6 | "include": ["src/"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/chai-solana/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.mono.json", 3 | "compilerOptions": { 4 | "rootDir": "src/", 5 | "outDir": "dist/esm/" 6 | }, 7 | "include": ["src/"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/option-utils/README.md: -------------------------------------------------------------------------------- 1 | # @saberhq/option-utils 2 | 3 | Utilities related to optional types. 4 | 5 | ## License 6 | 7 | Saber Common is licensed under the Apache License, Version 2.0. 8 | -------------------------------------------------------------------------------- /packages/option-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saberhq/option-utils", 3 | "version": "3.0.0", 4 | "description": "Utilities for handling optional values in TypeScript.", 5 | "homepage": "https://github.com/saber-hq/saber-common/tree/master/packages/option-utils#readme", 6 | "repository": "git+https://github.com/saber-hq/saber-common.git", 7 | "bugs": "https://github.com/saber-hq/saber-common/issues", 8 | "funding": "https://www.coingecko.com/en/coins/saber", 9 | "author": "Saber Team ", 10 | "license": "Apache-2.0", 11 | "exports": { 12 | ".": { 13 | "import": "./dist/esm/index.js", 14 | "require": "./dist/cjs/index.js" 15 | } 16 | }, 17 | "main": "dist/cjs/index.js", 18 | "module": "dist/esm/index.js", 19 | "files": [ 20 | "src/", 21 | "dist/" 22 | ], 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "keywords": [ 27 | "typescript", 28 | "saber", 29 | "option" 30 | ], 31 | "scripts": { 32 | "build": "tsc && tsc --project tsconfig.cjs.json", 33 | "clean": "rm -fr dist/" 34 | }, 35 | "dependencies": { 36 | "tslib": "^2.6.2" 37 | }, 38 | "devDependencies": { 39 | "@saberhq/tsconfig": "^3.3.1", 40 | "typescript": "^5.4.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/option-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:option-utils/README.md]] 3 | * @module 4 | */ 5 | 6 | /** 7 | * Optional type. 8 | */ 9 | export type Maybe = T | null | undefined; 10 | 11 | /** 12 | * Applies a function to a list of null/undefined values, unwrapping the null/undefined value or passing it through. 13 | */ 14 | export const mapN = ( 15 | fn: ( 16 | ...a: { 17 | [K in keyof T]: NonNullable; 18 | } 19 | ) => U, 20 | ...args: T 21 | ): U | null | undefined => { 22 | if (!args.every((arg) => arg !== undefined)) { 23 | return undefined; 24 | } 25 | if (!args.every((arg) => arg !== null)) { 26 | return null; 27 | } 28 | return fn( 29 | ...(args as { 30 | [K in keyof T]: NonNullable; 31 | }), 32 | ); 33 | }; 34 | 35 | /** 36 | * Applies a function to a null/undefined inner value if it is null or undefined, 37 | * otherwise returns null/undefined. 38 | * 39 | * For consistency reasons, we recommend just using {@link mapN} in all cases. 40 | * 41 | * @deprecated use {@link mapN} 42 | * @param obj 43 | * @param fn 44 | * @returns 45 | */ 46 | export const mapSome = ( 47 | obj: NonNullable | null | undefined, 48 | fn: (obj: NonNullable) => U, 49 | ): U | null | undefined => (exists(obj) ? fn(obj) : obj); 50 | 51 | /** 52 | * Checks to see if the provided value is not null. 53 | * 54 | * Useful for preserving types in filtering out non-null values. 55 | * 56 | * @param value 57 | * @returns 58 | */ 59 | export const isNotNull = (value: TValue | null): value is TValue => { 60 | return value !== null; 61 | }; 62 | 63 | /** 64 | * Checks to see if the provided value is not undefined. 65 | * 66 | * @param value 67 | * @returns 68 | */ 69 | export const isNotUndefined = ( 70 | value: TValue | undefined, 71 | ): value is TValue => { 72 | return value !== undefined; 73 | }; 74 | 75 | /** 76 | * Checks to see if the provided value is not null or undefined. 77 | * 78 | * @param value 79 | * @returns 80 | */ 81 | export const exists = ( 82 | value: TValue | null | undefined, 83 | ): value is TValue => { 84 | return value !== null && value !== undefined; 85 | }; 86 | -------------------------------------------------------------------------------- /packages/option-utils/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.cjs.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs/" 5 | }, 6 | "include": ["src/"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/option-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.mono.json", 3 | "compilerOptions": { 4 | "rootDir": "src/", 5 | "outDir": "dist/esm/" 6 | }, 7 | "include": ["src/"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/solana-contrib/README.md: -------------------------------------------------------------------------------- 1 | # @saberhq/solana-contrib 2 | 3 | Common types and libraries for Solana development. 4 | 5 | ## Documentation 6 | 7 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 8 | 9 | Automatically generated TypeScript documentation can be found [on GitHub pages](https://saber-hq.github.io/saber-common/). 10 | 11 | ## Installation 12 | 13 | ``` 14 | yarn add @saberhq/solana-contrib 15 | ``` 16 | 17 | ## License 18 | 19 | Apache 2.0 20 | -------------------------------------------------------------------------------- /packages/solana-contrib/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saberhq/solana-contrib", 3 | "version": "3.0.0", 4 | "description": "Common types and libraries for Solana", 5 | "homepage": "https://github.com/saber-hq/saber-common/tree/master/packages/solana-contrib#readme", 6 | "repository": "git+https://github.com/saber-hq/saber-common.git", 7 | "bugs": "https://github.com/saber-hq/saber-common/issues", 8 | "funding": "https://www.coingecko.com/en/coins/saber", 9 | "author": "Saber Team ", 10 | "license": "Apache-2.0", 11 | "scripts": { 12 | "build": "tsc && tsc -P tsconfig.cjs.json", 13 | "clean": "rm -fr dist/", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "exports": { 17 | ".": { 18 | "import": "./dist/esm/index.js", 19 | "require": "./dist/cjs/index.js" 20 | } 21 | }, 22 | "main": "dist/cjs/index.js", 23 | "module": "dist/esm/index.js", 24 | "files": [ 25 | "dist/", 26 | "src/" 27 | ], 28 | "dependencies": { 29 | "@saberhq/option-utils": "workspace:^", 30 | "@solana/buffer-layout": "^4.0.1", 31 | "@types/promise-retry": "^1.1.6", 32 | "@types/retry": "^0.12.5", 33 | "promise-retry": "^2.0.1", 34 | "retry": "^0.13.1", 35 | "tiny-invariant": "^1.3.3", 36 | "tslib": "^2.6.2" 37 | }, 38 | "devDependencies": { 39 | "@saberhq/tsconfig": "^3.3.1", 40 | "@solana/web3.js": "^1.91.1", 41 | "@types/bn.js": "^5.1.5", 42 | "@types/jest": "^29.5.12", 43 | "@types/node": "^20.12.7", 44 | "bn.js": "^5.2.1", 45 | "typescript": "^5.4.5" 46 | }, 47 | "peerDependencies": { 48 | "@solana/web3.js": "^1.42", 49 | "bn.js": "^4 || ^5" 50 | }, 51 | "gitHead": "f9fd3fbd36a7a6dd6f5e9597af5309affe50ac0e", 52 | "publishConfig": { 53 | "access": "public" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/broadcaster/sendAndSpamRawTx.ts: -------------------------------------------------------------------------------- 1 | import type { Connection, SendOptions } from "@solana/web3.js"; 2 | 3 | import { sleep } from "../utils/index.js"; 4 | import type { TransactionRetryOptions } from "./index.js"; 5 | import { DEFAULT_RETRY_OPTIONS } from "./index.js"; 6 | 7 | /** 8 | * Sends and spams a raw transaction multiple times. 9 | * @param connection Connection to send the transaction to. We recommend using a public endpoint such as GenesysGo. 10 | * @param rawTx 11 | * @param opts 12 | */ 13 | export const sendAndSpamRawTx = async ( 14 | connection: Connection, 15 | rawTx: Buffer, 16 | sendOptions: SendOptions, 17 | { 18 | retryTimes = DEFAULT_RETRY_OPTIONS.retryTimes, 19 | retryInterval = DEFAULT_RETRY_OPTIONS.retryInterval, 20 | }: TransactionRetryOptions = DEFAULT_RETRY_OPTIONS, 21 | ) => { 22 | const result = await connection.sendRawTransaction(rawTx, sendOptions); 23 | // if we could send the TX with preflight, let's spam it. 24 | void (async () => { 25 | // technique stolen from Mango. 26 | for (let i = 0; i < retryTimes; i++) { 27 | try { 28 | await sleep(retryInterval); 29 | await connection.sendRawTransaction(rawTx, { 30 | ...sendOptions, 31 | skipPreflight: true, 32 | }); 33 | } catch (e) { 34 | console.warn(`[Broadcaster] sendAndSpamRawTx error`, e); 35 | } 36 | } 37 | })(); 38 | return result; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/broadcaster/tiered.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Blockhash, 3 | BlockhashWithExpiryBlockHeight, 4 | Commitment, 5 | ConfirmOptions, 6 | Connection, 7 | RpcResponseAndContext, 8 | SendOptions, 9 | SimulatedTransactionResponse, 10 | Transaction, 11 | } from "@solana/web3.js"; 12 | 13 | import type { Broadcaster } from "../interfaces.js"; 14 | import { DEFAULT_PROVIDER_OPTIONS } from "../provider.js"; 15 | import { PendingTransaction } from "../transaction/index.js"; 16 | import { suppressConsoleErrorAsync } from "../utils/index.js"; 17 | import type { BroadcastOptions } from "./index.js"; 18 | import { 19 | DEFAULT_FALLBACK_RETRY_OPTIONS, 20 | DEFAULT_RETRY_OPTIONS, 21 | SingleConnectionBroadcaster, 22 | } from "./index.js"; 23 | import { sendAndSpamRawTx } from "./sendAndSpamRawTx.js"; 24 | 25 | /** 26 | * Broadcasts transactions to multiple connections simultaneously. 27 | */ 28 | export class TieredBroadcaster implements Broadcaster { 29 | readonly premiumBroadcaster: SingleConnectionBroadcaster; 30 | 31 | constructor( 32 | readonly primaryConnection: Connection, 33 | /** 34 | * Connections to send to in addition to the primary. 35 | */ 36 | readonly fallbackConnections: readonly Connection[], 37 | readonly opts: ConfirmOptions = DEFAULT_PROVIDER_OPTIONS, 38 | ) { 39 | this.premiumBroadcaster = new SingleConnectionBroadcaster( 40 | primaryConnection, 41 | opts, 42 | ); 43 | } 44 | 45 | async getLatestBlockhash( 46 | commitment: Commitment = this.opts.preflightCommitment ?? "confirmed", 47 | ): Promise { 48 | return await this.premiumBroadcaster.getLatestBlockhash(commitment); 49 | } 50 | 51 | async getRecentBlockhash( 52 | commitment: Commitment = this.opts.preflightCommitment ?? "confirmed", 53 | ): Promise { 54 | return await this.premiumBroadcaster.getRecentBlockhash(commitment); 55 | } 56 | 57 | private async _sendRawTransaction( 58 | encoded: Buffer, 59 | options?: SendOptions & Omit, 60 | ): Promise { 61 | const pending = new PendingTransaction( 62 | this.primaryConnection, 63 | await sendAndSpamRawTx( 64 | this.primaryConnection, 65 | encoded, 66 | options ?? this.opts, 67 | options ?? DEFAULT_RETRY_OPTIONS, 68 | ), 69 | ); 70 | void (async () => { 71 | await Promise.all( 72 | this.fallbackConnections.map(async (fc) => { 73 | try { 74 | await sendAndSpamRawTx( 75 | fc, 76 | encoded, 77 | options ?? this.opts, 78 | options?.fallbackRetryOptions ?? DEFAULT_FALLBACK_RETRY_OPTIONS, 79 | ); 80 | } catch (e) { 81 | console.warn(`[Broadcaster] _sendRawTransaction error`, e); 82 | } 83 | }), 84 | ); 85 | })(); 86 | return pending; 87 | } 88 | 89 | /** 90 | * Broadcasts a signed transaction. 91 | * 92 | * @param tx 93 | * @param confirm 94 | * @param opts 95 | * @returns 96 | */ 97 | async broadcast( 98 | tx: Transaction, 99 | { printLogs = true, ...opts }: BroadcastOptions = this.opts, 100 | ): Promise { 101 | if (tx.signatures.length === 0) { 102 | throw new Error("Transaction must be signed before broadcasting."); 103 | } 104 | const rawTx = tx.serialize(); 105 | 106 | if (printLogs) { 107 | return await this._sendRawTransaction(rawTx, opts); 108 | } 109 | 110 | return await suppressConsoleErrorAsync(async () => { 111 | // hide the logs of TX errors if printLogs = false 112 | return await this._sendRawTransaction(rawTx, opts); 113 | }); 114 | } 115 | 116 | /** 117 | * Simulates a transaction with a commitment. 118 | * @param tx 119 | * @param commitment 120 | * @returns 121 | */ 122 | async simulate( 123 | tx: Transaction, 124 | { 125 | commitment = this.opts.preflightCommitment ?? "confirmed", 126 | verifySigners = true, 127 | }: { 128 | commitment?: Commitment; 129 | verifySigners?: boolean; 130 | } = { 131 | commitment: this.opts.preflightCommitment ?? "confirmed", 132 | verifySigners: true, 133 | }, 134 | ): Promise> { 135 | if (verifySigners && tx.signatures.length === 0) { 136 | throw new Error("Transaction must be signed before simulating."); 137 | } 138 | return this.premiumBroadcaster.simulate(tx, { 139 | commitment, 140 | verifySigners, 141 | }); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/computeBudget/index.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | 3 | /** 4 | * The compute budget program. 5 | * Source: https://github.com/solana-labs/solana/blob/master/program-runtime/src/compute_budget.rs#L101 6 | */ 7 | export const COMPUTE_BUDGET_PROGRAM = new PublicKey( 8 | "ComputeBudget111111111111111111111111111111", 9 | ); 10 | 11 | export * from "./instructions.js"; 12 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/computeBudget/instructions.ts: -------------------------------------------------------------------------------- 1 | import { TransactionInstruction } from "@solana/web3.js"; 2 | 3 | import { COMPUTE_BUDGET_PROGRAM } from "./index.js"; 4 | import { RequestHeapFrameLayout, RequestUnitsLayout } from "./layouts.js"; 5 | 6 | /** 7 | * Request a specific maximum number of compute units the transaction is 8 | * allowed to consume and an additional fee to pay. 9 | */ 10 | export const requestComputeUnitsInstruction = ( 11 | units: number, 12 | additionalFee: number, 13 | ): TransactionInstruction => { 14 | const data = Buffer.alloc(RequestUnitsLayout.span); 15 | RequestUnitsLayout.encode({ instruction: 0, units, additionalFee }, data); 16 | return new TransactionInstruction({ 17 | data, 18 | keys: [], 19 | programId: COMPUTE_BUDGET_PROGRAM, 20 | }); 21 | }; 22 | 23 | /** 24 | * Request a specific transaction-wide program heap region size in bytes. 25 | * The value requested must be a multiple of 1024. This new heap region 26 | * size applies to each program executed, including all calls to CPIs. 27 | */ 28 | export const requestHeapFrameInstruction = ( 29 | bytes: number, 30 | ): TransactionInstruction => { 31 | const data = Buffer.alloc(RequestHeapFrameLayout.span); 32 | RequestHeapFrameLayout.encode({ instruction: 1, bytes }, data); 33 | return new TransactionInstruction({ 34 | data, 35 | keys: [], 36 | programId: COMPUTE_BUDGET_PROGRAM, 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/computeBudget/layouts.ts: -------------------------------------------------------------------------------- 1 | import * as BufferLayout from "@solana/buffer-layout"; 2 | 3 | export const RequestUnitsLayout = BufferLayout.struct<{ 4 | instruction: number; 5 | units: number; 6 | additionalFee: number; 7 | }>([ 8 | BufferLayout.u8("instruction"), 9 | BufferLayout.u32("units"), 10 | BufferLayout.u32("additionalFee"), 11 | ]); 12 | 13 | export const RequestHeapFrameLayout = BufferLayout.struct<{ 14 | instruction: number; 15 | bytes: number; 16 | }>([BufferLayout.u8("instruction"), BufferLayout.u32("bytes")]); 17 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/constants.ts: -------------------------------------------------------------------------------- 1 | import type { Cluster, ConnectionConfig } from "@solana/web3.js"; 2 | 3 | /** 4 | * A network: a Solana cluster or localnet. 5 | */ 6 | export type Network = Cluster | "localnet"; 7 | 8 | /** 9 | * Formats the network as a string. 10 | * @param network 11 | * @returns 12 | */ 13 | export const formatNetwork = (network: Network): string => { 14 | if (network === "mainnet-beta") { 15 | return "mainnet"; 16 | } 17 | return network; 18 | }; 19 | 20 | export type NetworkConfig = Readonly< 21 | Omit & { 22 | name: string; 23 | /** 24 | * HTTP endpoint to connect to for this network. 25 | */ 26 | endpoint: string; 27 | /** 28 | * Websocket endpoint to connect to for this network. 29 | */ 30 | endpointWs?: string; 31 | } 32 | >; 33 | 34 | /** 35 | * Default configuration for all networks. 36 | */ 37 | export const DEFAULT_NETWORK_CONFIG_MAP = { 38 | "mainnet-beta": { 39 | name: "Mainnet Beta", 40 | endpoint: "https://api.mainnet-beta.solana.com/", 41 | }, 42 | devnet: { 43 | name: "Devnet", 44 | endpoint: "https://api.devnet.solana.com/", 45 | }, 46 | testnet: { 47 | name: "Testnet", 48 | endpoint: "https://api.testnet.solana.com/", 49 | }, 50 | localnet: { 51 | name: "Localnet", 52 | endpoint: "http://127.0.0.1:8899", 53 | }, 54 | } as const; 55 | 56 | export type NetworkConfigMap = { [N in Network]: NetworkConfig }; 57 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/error.ts: -------------------------------------------------------------------------------- 1 | export const firstAggregateError = (err: AggregateError) => { 2 | const errors = err.errors as Error[]; 3 | const [firstError, ...remErrors] = [errors.pop(), ...errors]; 4 | if (remErrors.length > 0) { 5 | console.error(remErrors); 6 | } 7 | return firstError; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:solana-contrib/README.md]] 3 | * @module 4 | */ 5 | 6 | export * from "./broadcaster/index.js"; 7 | export * from "./constants.js"; 8 | export * from "./interfaces.js"; 9 | export * from "./provider.js"; 10 | export * from "./transaction/index.js"; 11 | export * from "./utils/index.js"; 12 | export * from "./wallet.js"; 13 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/transaction/TransactionReceipt.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Cluster, 3 | TransactionResponse, 4 | TransactionSignature, 5 | VersionedTransactionResponse, 6 | } from "@solana/web3.js"; 7 | import { default as invariant } from "tiny-invariant"; 8 | 9 | import type { Event, EventParser } from "../interfaces.js"; 10 | import type { PromiseOrValue } from "../utils/misc.js"; 11 | import { valueAsPromise } from "../utils/misc.js"; 12 | import { generateTXLink } from "../utils/txLink.js"; 13 | import { PendingTransaction } from "./PendingTransaction.js"; 14 | import type { TransactionEnvelope } from "./TransactionEnvelope.js"; 15 | 16 | /** 17 | * A value that can be processed into a {@link TransactionReceipt}. 18 | */ 19 | export type TransactionLike = 20 | | TransactionEnvelope 21 | | PendingTransaction 22 | | TransactionReceipt; 23 | 24 | /** 25 | * Confirms a transaction, returning its receipt. 26 | * 27 | * @param tx 28 | * @returns 29 | */ 30 | export const confirmTransactionLike = async ( 31 | tx: PromiseOrValue, 32 | ): Promise => { 33 | const ish = await valueAsPromise(tx); 34 | if (ish instanceof TransactionReceipt) { 35 | return ish; 36 | } 37 | 38 | let pending: PendingTransaction; 39 | if (ish instanceof PendingTransaction) { 40 | pending = ish; 41 | } else { 42 | pending = await ish.send({ 43 | printLogs: false, 44 | }); 45 | } 46 | return await pending.wait(); 47 | }; 48 | 49 | /** 50 | * A transaction that has been processed by the cluster. 51 | */ 52 | export class TransactionReceipt { 53 | constructor( 54 | /** 55 | * Signature (id) of the transaction. 56 | */ 57 | readonly signature: TransactionSignature, 58 | /** 59 | * Raw response from web3.js 60 | */ 61 | readonly response: TransactionResponse | VersionedTransactionResponse, 62 | ) {} 63 | 64 | /** 65 | * Gets the events associated with this transaction. 66 | */ 67 | getEvents(eventParser: EventParser): readonly E[] { 68 | const logs = this.response.meta?.logMessages; 69 | if (logs && logs.length > 0) { 70 | return eventParser(logs); 71 | } 72 | return []; 73 | } 74 | 75 | /** 76 | * Prints the logs associated with this transaction. 77 | */ 78 | printLogs(): void { 79 | console.log(this.response.meta?.logMessages?.join("\n")); 80 | } 81 | 82 | /** 83 | * Gets the compute units used by the transaction. 84 | * @returns 85 | */ 86 | get computeUnits(): number { 87 | const logs = this.response.meta?.logMessages; 88 | invariant(logs, "no logs"); 89 | const consumeLog = logs[logs.length - 2]; 90 | invariant(consumeLog, "no consume log"); 91 | const amtStr = consumeLog.split(" ")[3]; 92 | invariant(amtStr, "no amount"); 93 | return parseInt(amtStr); 94 | } 95 | 96 | /** 97 | * Generates a link to view this {@link TransactionReceipt} on the official Solana explorer. 98 | * @param network 99 | * @returns 100 | */ 101 | generateSolanaExplorerLink(cluster: Cluster = "mainnet-beta"): string { 102 | return generateTXLink(this.signature, cluster); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/transaction/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./parseTransactionLogs.js"; 2 | export * from "./PendingTransaction.js"; 3 | export * from "./programErr.js"; 4 | export * from "./TransactionEnvelope.js"; 5 | export * from "./TransactionReceipt.js"; 6 | export * from "./utils.js"; 7 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/transaction/parseTransactionLogs.test.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { parseTransactionLogs } from "./parseTransactionLogs.js"; 4 | 5 | describe("parseTransactionLogs", () => { 6 | it("should parse the logs", () => { 7 | const logs = [ 8 | `Program GAGEa8FYyJNgwULDNhCBEZzW6a8Zfhs6QRxTZ9XQy151 invoke [1]`, 9 | `Program log: Instruction: GaugeCommitVote`, 10 | `Program log: Custom program error: 0x66`, 11 | `Program GAGEa8FYyJNgwULDNhCBEZzW6a8Zfhs6QRxTZ9XQy151 consumed 2778 of 200000 compute units`, 12 | `Program GAGEa8FYyJNgwULDNhCBEZzW6a8Zfhs6QRxTZ9XQy151 failed: custom program error: 0x66`, 13 | ]; 14 | 15 | const result = parseTransactionLogs(logs, null); 16 | 17 | expect(result).toEqual([ 18 | { 19 | programAddress: "GAGEa8FYyJNgwULDNhCBEZzW6a8Zfhs6QRxTZ9XQy151", 20 | logs: [ 21 | { 22 | type: "text", 23 | depth: 1, 24 | text: "Program log: Instruction: GaugeCommitVote", 25 | }, 26 | { 27 | type: "text", 28 | depth: 1, 29 | text: "Program log: Custom program error: 0x66", 30 | }, 31 | { 32 | type: "system", 33 | depth: 1, 34 | text: "Program GAGEa8FYyJNgwULDNhCBEZzW6a8Zfhs6QRxTZ9XQy151 consumed 2778 of 200000 compute units", 35 | }, 36 | { 37 | type: "programError", 38 | depth: 1, 39 | text: "custom program error: 0x66", 40 | }, 41 | ], 42 | failed: true, 43 | }, 44 | ]); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/transaction/txSizer.ts: -------------------------------------------------------------------------------- 1 | import type { Transaction } from "@solana/web3.js"; 2 | 3 | function shortvecEncodeLength(bytes: Array, len: number) { 4 | let rem_len = len; 5 | for (;;) { 6 | let elem = rem_len & 0x7f; 7 | rem_len >>= 7; 8 | if (rem_len === 0) { 9 | bytes.push(elem); 10 | break; 11 | } else { 12 | elem |= 0x80; 13 | bytes.push(elem); 14 | } 15 | } 16 | } 17 | 18 | /** 19 | * Calculates transaction size. If the transaction is too large, it does not throw. 20 | * @param tx 21 | * @returns 22 | */ 23 | export const calculateTxSizeUnsafe = (tx: Transaction): number => { 24 | // check if fee payer signed. 25 | const { feePayer } = tx; 26 | const hasFeePayerSigned = 27 | feePayer && tx.signatures.find((s) => s.publicKey.equals(feePayer)); 28 | const signData = tx.serializeMessage(); 29 | const numSigners = tx.signatures.length + (hasFeePayerSigned ? 1 : 0); 30 | const signatureCount: number[] = []; 31 | shortvecEncodeLength(signatureCount, numSigners); 32 | return signatureCount.length + numSigners * 64 + signData.length; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/transaction/utils.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AccountMeta, 3 | Cluster, 4 | TransactionInstruction, 5 | } from "@solana/web3.js"; 6 | import { PublicKey, Transaction } from "@solana/web3.js"; 7 | 8 | /** 9 | * Instruction that can be serialized to JSON. 10 | */ 11 | export interface SerializableInstruction { 12 | programId: string; 13 | keys: (Omit & { publicKey: string })[]; 14 | data: string; 15 | } 16 | 17 | /** 18 | * Stub of a recent blockhash that can be used to simulate transactions. 19 | */ 20 | export const RECENT_BLOCKHASH_STUB = 21 | "GfVcyD4kkTrj4bKc7WA9sZCin9JDbdT4Zkd3EittNR1W"; 22 | 23 | /** 24 | * Builds a transaction with a fake `recentBlockhash` and `feePayer` for the purpose 25 | * of simulating a sequence of instructions. 26 | * 27 | * @param cluster 28 | * @param ixs 29 | * @returns 30 | */ 31 | export const buildStubbedTransaction = ( 32 | cluster: Cluster, 33 | ixs: TransactionInstruction[], 34 | ): Transaction => { 35 | const tx = new Transaction(); 36 | tx.recentBlockhash = RECENT_BLOCKHASH_STUB; 37 | 38 | // random keys that always have money in them 39 | tx.feePayer = 40 | cluster === "devnet" 41 | ? new PublicKey("A2jaCHPzD6346348JoEym2KFGX9A7uRBw6AhCdX7gTWP") 42 | : new PublicKey("9u9iZBWqGsp5hXBxkVZtBTuLSGNAG9gEQLgpuVw39ASg"); 43 | tx.instructions = ixs; 44 | return tx; 45 | }; 46 | 47 | /** 48 | * Serializes a {@link Transaction} to base64 format without checking signatures. 49 | * @param tx 50 | * @returns 51 | */ 52 | export const serializeToBase64Unchecked = (tx: Transaction): string => 53 | tx 54 | .serialize({ 55 | requireAllSignatures: false, 56 | verifySignatures: false, 57 | }) 58 | .toString("base64"); 59 | 60 | /** 61 | * Generates a link for inspecting the contents of a transaction. 62 | * 63 | * @returns URL 64 | */ 65 | export const generateInspectLinkFromBase64 = ( 66 | cluster: Cluster, 67 | base64TX: string, 68 | ): string => { 69 | return `https://${ 70 | cluster === "mainnet-beta" ? "" : `${cluster}.` 71 | }anchor.so/tx/inspector?message=${encodeURIComponent(base64TX)}`; 72 | }; 73 | 74 | /** 75 | * Generates a link for inspecting the contents of a transaction, not checking for 76 | * or requiring valid signatures. 77 | * 78 | * @returns URL 79 | */ 80 | export const generateUncheckedInspectLink = ( 81 | cluster: Cluster, 82 | tx: Transaction, 83 | ): string => { 84 | return generateInspectLinkFromBase64(cluster, serializeToBase64Unchecked(tx)); 85 | }; 86 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./instructions.js"; 2 | export * from "./misc.js"; 3 | export * from "./printAccountOwners.js"; 4 | export * from "./printTXTable.js"; 5 | export * from "./pubkeyCache.js"; 6 | export * from "./publicKey.js"; 7 | export * from "./simulateTransactionWithCommitment.js"; 8 | export * from "./time.js"; 9 | export * from "./txLink.js"; 10 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/utils/instructions.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey, TransactionInstruction } from "@solana/web3.js"; 2 | 3 | /** 4 | * ID of the memo program. 5 | */ 6 | export const MEMO_PROGRAM_ID = new PublicKey( 7 | "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr", 8 | ); 9 | 10 | /** 11 | * Creates a memo program instruction. 12 | * 13 | * More info: https://spl.solana.com/memo 14 | * 15 | * @param text Text of the memo. 16 | * @param signers Optional signers to validate. 17 | * @returns 18 | */ 19 | export const createMemoInstruction = ( 20 | text: string, 21 | signers: readonly PublicKey[] = [], 22 | ): TransactionInstruction => { 23 | return new TransactionInstruction({ 24 | programId: MEMO_PROGRAM_ID, 25 | keys: signers.map((s) => ({ 26 | pubkey: s, 27 | isSigner: true, 28 | isWritable: false, 29 | })), 30 | data: Buffer.from(text, "utf8"), 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from "./publicKey"; 2 | 3 | export * from "@saberhq/option-utils"; 4 | 5 | const noop = () => { 6 | // noop 7 | }; 8 | 9 | /** 10 | * Hide the console.error because @solana/web3.js often emits noisy errors as a 11 | * side effect. 12 | */ 13 | export const suppressConsoleErrorAsync = async ( 14 | fn: () => Promise, 15 | ): Promise => { 16 | const oldConsoleError = console.error; 17 | console.error = noop; 18 | try { 19 | const result = await fn(); 20 | console.error = oldConsoleError; 21 | return result; 22 | } catch (e) { 23 | console.error = oldConsoleError; 24 | throw e; 25 | } 26 | }; 27 | 28 | /** 29 | * Hide the console.error because @solana/web3.js often emits noisy errors as a 30 | * side effect. 31 | */ 32 | export const suppressConsoleError = (fn: () => T): T => { 33 | const oldConsoleError = console.error; 34 | console.error = noop; 35 | try { 36 | const result = fn(); 37 | console.error = oldConsoleError; 38 | return result; 39 | } catch (e) { 40 | console.error = oldConsoleError; 41 | throw e; 42 | } 43 | }; 44 | 45 | export function sleep(ms: number): Promise { 46 | return new Promise((resolve) => setTimeout(resolve, ms)); 47 | } 48 | 49 | /** 50 | * Promise or its inner value. 51 | */ 52 | export type PromiseOrValue = Promise | T; 53 | 54 | /** 55 | * Awaits for a promise or value. 56 | */ 57 | export const valueAsPromise = async ( 58 | awaitable: PromiseOrValue, 59 | ): Promise => { 60 | if ("then" in awaitable) { 61 | return await awaitable; 62 | } 63 | return awaitable; 64 | }; 65 | 66 | /** 67 | * Shortens a pubkey. 68 | * @param pubkey 69 | * @returns 70 | */ 71 | export const formatPubkeyShort = ( 72 | pubkey: PublicKey, 73 | leading = 7, 74 | trailing = 7, 75 | ): string => { 76 | const str = pubkey.toString(); 77 | return str.length > 20 78 | ? `${str.substring(0, leading)}.....${str.substring( 79 | str.length - trailing, 80 | str.length, 81 | )}` 82 | : str; 83 | }; 84 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/utils/pubkeyCache.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKeyInitData } from "@solana/web3.js"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | 4 | const pubkeyCache: Record = {}; 5 | 6 | /** 7 | * PublicKey with a cached base58 value. 8 | */ 9 | export class CachedPublicKey extends PublicKey { 10 | private readonly _base58: string; 11 | 12 | constructor(value: PublicKeyInitData) { 13 | super(value); 14 | this._base58 = super.toBase58(); 15 | } 16 | 17 | override equals(other: PublicKey): boolean { 18 | if (other instanceof CachedPublicKey) { 19 | return other._base58 === this._base58; 20 | } 21 | return super.equals(other); 22 | } 23 | 24 | override toString() { 25 | return this._base58; 26 | } 27 | 28 | override toBase58(): string { 29 | return this._base58; 30 | } 31 | } 32 | 33 | const getOrCreatePublicKey = (pk: string): PublicKey => { 34 | const cached = pubkeyCache[pk]; 35 | if (!cached) { 36 | return (pubkeyCache[pk] = new CachedPublicKey(pk)); 37 | } 38 | return cached; 39 | }; 40 | 41 | /** 42 | * Gets or parses a PublicKey. 43 | * @param pk 44 | * @returns 45 | */ 46 | export const getPublicKey = ( 47 | pk: string | PublicKey | PublicKeyInitData, 48 | ): PublicKey => { 49 | if (typeof pk === "string") { 50 | return getOrCreatePublicKey(pk); 51 | } else if (pk instanceof PublicKey) { 52 | return getOrCreatePublicKey(pk.toString()); 53 | } else { 54 | return getOrCreatePublicKey(new PublicKey(pk).toString()); 55 | } 56 | }; 57 | 58 | const gpaCache: Record = {}; 59 | 60 | /** 61 | * Concatenates seeds to generate a unique number array. 62 | * @param seeds 63 | * @returns 64 | */ 65 | const concatSeeds = (seeds: Array): Uint8Array => { 66 | return Uint8Array.from( 67 | seeds.reduce((acc: number[], seed) => [...acc, ...seed], []), 68 | ); 69 | }; 70 | 71 | /** 72 | * Gets a cached program address for the given seeds. 73 | * @param seeds 74 | * @param programId 75 | * @returns 76 | */ 77 | export const getProgramAddress = ( 78 | seeds: Array, 79 | programId: PublicKey, 80 | ) => { 81 | const normalizedSeeds = concatSeeds(seeds); 82 | const cacheKey = `${normalizedSeeds.toString()}_${programId.toString()}`; 83 | const cached = gpaCache[cacheKey]; 84 | if (cached) { 85 | return cached; 86 | } 87 | const [key] = PublicKey.findProgramAddressSync(seeds, programId); 88 | return (gpaCache[cacheKey] = getPublicKey(key)); 89 | }; 90 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/utils/publicKey.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKeyData } from "@solana/web3.js"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | import BN from "bn.js"; 4 | 5 | export { PublicKey } from "@solana/web3.js"; 6 | 7 | /** 8 | * Returns a {@link PublicKey} if it can be parsed, otherwise returns null. 9 | * @param pk 10 | * @returns 11 | */ 12 | export const parsePublicKey = (pk: unknown): PublicKey | null => { 13 | if (!pk) { 14 | return null; 15 | } 16 | 17 | if (pk instanceof PublicKey) { 18 | return pk; 19 | } 20 | 21 | if ( 22 | typeof pk !== "object" || 23 | Array.isArray(pk) || 24 | ("constructor" in pk && BN.isBN(pk)) 25 | ) { 26 | return null; 27 | } 28 | 29 | try { 30 | return new PublicKey(pk as PublicKeyData); 31 | } catch (e) { 32 | return null; 33 | } 34 | }; 35 | 36 | /** 37 | * Returns true if the given value is a {@link PublicKey}. 38 | * @param pk 39 | * @returns 40 | */ 41 | export const isPublicKey = (pk: unknown): pk is PublicKey => { 42 | return !!parsePublicKey(pk); 43 | }; 44 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/utils/simulateTransactionWithCommitment.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Commitment, 3 | Connection, 4 | RpcResponseAndContext, 5 | SimulatedTransactionResponse, 6 | Transaction, 7 | } from "@solana/web3.js"; 8 | import { SendTransactionError } from "@solana/web3.js"; 9 | 10 | /** 11 | * Copy of Connection.simulateTransaction that takes a commitment parameter. 12 | */ 13 | export async function simulateTransactionWithCommitment( 14 | connection: Connection, 15 | transaction: Transaction, 16 | commitment: Commitment = "confirmed", 17 | ): Promise> { 18 | const connectionInner = connection as Connection & { 19 | _rpcRequest: ( 20 | rpc: "simulateTransaction", 21 | args: [ 22 | string, 23 | { 24 | encoding: string; 25 | commitment: Commitment; 26 | }, 27 | ], 28 | ) => Promise<{ 29 | error: Error; 30 | result: RpcResponseAndContext; 31 | }>; 32 | }; 33 | 34 | // only populate recent blockhash if it isn't on the tx 35 | if (!transaction.recentBlockhash) { 36 | const { blockhash } = await connection.getLatestBlockhash(commitment); 37 | transaction.recentBlockhash = blockhash; 38 | } 39 | 40 | const wireTransaction = transaction.serialize({ 41 | requireAllSignatures: false, 42 | }); 43 | const encodedTransaction = wireTransaction.toString("base64"); 44 | const config = { encoding: "base64", commitment }; 45 | 46 | const res = await connectionInner._rpcRequest("simulateTransaction", [ 47 | encodedTransaction, 48 | config, 49 | ]); 50 | if (res.error) { 51 | throw new SendTransactionError( 52 | "failed to simulate transaction: " + res.error.message, 53 | res.result.value.logs ?? undefined, 54 | ); 55 | } 56 | return res.result; 57 | } 58 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/utils/time.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js"; 2 | 3 | /** 4 | * Converts a {@link Date} to a {@link BN} timestamp. 5 | * @param date 6 | * @returns 7 | */ 8 | export const dateToTs = (date: Date): BN => 9 | new BN(Math.floor(date.getTime() / 1_000)); 10 | 11 | /** 12 | * Converts a {@link BN} timestamp to a {@link Date}. 13 | * @param ts 14 | * @returns 15 | */ 16 | export const tsToDate = (ts: BN): Date => new Date(ts.toNumber() * 1_000); 17 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/utils/txLink.ts: -------------------------------------------------------------------------------- 1 | import type { Cluster } from "@solana/web3.js"; 2 | 3 | export enum ExplorerType { 4 | SOLANA_EXPLORER = "solana-explorer", 5 | SOLSCAN = "solscan", 6 | } 7 | 8 | export function generateTXLink( 9 | signature: string, 10 | cluster: Cluster = "mainnet-beta", 11 | explorerType: string = ExplorerType.SOLANA_EXPLORER, 12 | ): string { 13 | switch (explorerType) { 14 | case ExplorerType.SOLANA_EXPLORER as string: 15 | return `https://explorer.solana.com/tx/${signature}?cluster=${cluster}`; 16 | case ExplorerType.SOLSCAN as string: 17 | return `https://solscan.io/tx/${signature}?cluster=${cluster}`; 18 | default: 19 | throw new Error(`Explorer type ${explorerType} is not supported.`); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/solana-contrib/src/wallet.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ConfirmOptions, 3 | Connection, 4 | PublicKey, 5 | Signer, 6 | Transaction, 7 | VersionedTransaction, 8 | } from "@solana/web3.js"; 9 | 10 | import { 11 | isVersionedTransaction, 12 | type Provider, 13 | type Wallet, 14 | } from "./interfaces.js"; 15 | import { SolanaProvider } from "./provider.js"; 16 | 17 | /** 18 | * Wallet based on a Signer. 19 | */ 20 | export class SignerWallet implements Wallet { 21 | constructor(readonly signer: Signer) {} 22 | 23 | get publicKey(): PublicKey { 24 | return this.signer.publicKey; 25 | } 26 | 27 | signAllTransactions( 28 | txs: T[], 29 | ): Promise { 30 | return Promise.resolve( 31 | txs.map((tx) => { 32 | if (isVersionedTransaction(tx)) { 33 | tx.sign([this.signer]); 34 | } else { 35 | tx.partialSign(this.signer); 36 | } 37 | return tx; 38 | }), 39 | ); 40 | } 41 | 42 | signTransaction( 43 | transaction: T, 44 | ): Promise { 45 | if (isVersionedTransaction(transaction)) { 46 | transaction.sign([this.signer]); 47 | } else { 48 | transaction.partialSign(this.signer); 49 | } 50 | return Promise.resolve(transaction); 51 | } 52 | 53 | /** 54 | * Creates a Provider from this Wallet by adding a Connection. 55 | * @param connection 56 | * @returns 57 | */ 58 | createProvider( 59 | connection: Connection, 60 | sendConnection?: Connection, 61 | opts?: ConfirmOptions, 62 | ): Provider { 63 | return SolanaProvider.load({ 64 | connection, 65 | sendConnection, 66 | wallet: this, 67 | opts, 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/solana-contrib/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.cjs.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs/" 5 | }, 6 | "include": ["src/"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/solana-contrib/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.mono.json", 3 | "compilerOptions": { 4 | "rootDir": "src/", 5 | "outDir": "dist/esm/" 6 | }, 7 | "include": ["src/"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/README.md: -------------------------------------------------------------------------------- 1 | # @saberhq/stableswap-sdk 2 | 3 | Stableswap SDK. 4 | 5 | ## Documentation 6 | 7 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 8 | 9 | Automatically generated TypeScript documentation can be found [on GitHub pages](https://saber-hq.github.io/saber-common/). 10 | 11 | ## Installation 12 | 13 | ``` 14 | yarn add @saberhq/stableswap-sdk 15 | ``` 16 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saberhq/stableswap-sdk", 3 | "version": "3.0.0", 4 | "description": "Solana SDK for Saber's StableSwap program.", 5 | "homepage": "https://github.com/saber-hq/saber-common/tree/master/packages/stableswap-sdk#readme", 6 | "repository": "git+https://github.com/saber-hq/saber-common.git", 7 | "bugs": "https://github.com/saber-hq/saber-common/issues", 8 | "author": "Saber Team ", 9 | "license": "Apache-2.0", 10 | "keywords": [ 11 | "solana", 12 | "saber" 13 | ], 14 | "type": "module", 15 | "exports": { 16 | ".": { 17 | "import": "./dist/esm/index.js", 18 | "require": "./dist/cjs/index.js" 19 | } 20 | }, 21 | "main": "dist/cjs/index.js", 22 | "module": "dist/esm/index.js", 23 | "scripts": { 24 | "build": "tsc && tsc -P tsconfig.cjs.json", 25 | "clean": "rm -rf dist" 26 | }, 27 | "engines": { 28 | "node": ">=12.x" 29 | }, 30 | "devDependencies": { 31 | "@saberhq/tsconfig": "^3.3.1", 32 | "@solana/web3.js": "^1.91.1", 33 | "@types/bn.js": "^5.1.5", 34 | "@types/lodash": "^4.17.0", 35 | "@types/node": "^20.12.7", 36 | "bn.js": "^5.2.1", 37 | "jsbi": "^4.3.0", 38 | "lodash": "^4.17.21", 39 | "typescript": "^5.4.5" 40 | }, 41 | "peerDependencies": { 42 | "@solana/web3.js": "^1.42", 43 | "bn.js": ">=5", 44 | "jsbi": "^3 || ^4" 45 | }, 46 | "dependencies": { 47 | "@saberhq/solana-contrib": "workspace:^", 48 | "@saberhq/token-utils": "workspace:^", 49 | "@solana/buffer-layout": "^4.0.1", 50 | "tiny-invariant": "^1.3.3", 51 | "tslib": "^2.6.2" 52 | }, 53 | "files": [ 54 | "dist/", 55 | "src/" 56 | ], 57 | "publishConfig": { 58 | "access": "public" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/calculator/curve.ts: -------------------------------------------------------------------------------- 1 | import { ONE, ZERO } from "@saberhq/token-utils"; 2 | 3 | const N_COINS = BigInt(2); // n 4 | 5 | const abs = (a: bigint): bigint => { 6 | if (a > ZERO) { 7 | return a; 8 | } 9 | return -a; 10 | }; 11 | 12 | // maximum iterations of newton's method approximation 13 | const MAX_ITERS = 20; 14 | 15 | /** 16 | * Compute the StableSwap invariant 17 | * @param ampFactor Amplification coefficient (A) 18 | * @param amountA Swap balance of token A 19 | * @param amountB Swap balance of token B 20 | * Reference: https://github.com/curvefi/curve-contract/blob/7116b4a261580813ef057887c5009e22473ddb7d/tests/simulation.py#L31 21 | */ 22 | export const computeD = ( 23 | ampFactor: bigint, 24 | amountA: bigint, 25 | amountB: bigint, 26 | ): bigint => { 27 | const Ann = ampFactor * N_COINS; // A*n^n 28 | const S = amountA + amountB; // sum(x_i), a.k.a S 29 | if (S === ZERO) { 30 | return ZERO; 31 | } 32 | 33 | let dPrev = ZERO; 34 | let d = S; 35 | 36 | for (let i = 0; abs(d - dPrev) > ONE && i < MAX_ITERS; i++) { 37 | dPrev = d; 38 | let dP = d; 39 | dP = (dP * d) / (amountA * N_COINS); 40 | dP = (dP * d) / (amountB * N_COINS); 41 | 42 | const dNumerator = d * (Ann * S + dP * N_COINS); 43 | const dDenominator = d * (Ann - ONE) + dP * (N_COINS + ONE); 44 | d = dNumerator / dDenominator; 45 | } 46 | 47 | return d; 48 | }; 49 | 50 | /** 51 | * Compute Y amount in respect to X on the StableSwap curve 52 | * @param ampFactor Amplification coefficient (A) 53 | * @param x The quantity of underlying asset 54 | * @param d StableSwap invariant 55 | * Reference: https://github.com/curvefi/curve-contract/blob/7116b4a261580813ef057887c5009e22473ddb7d/tests/simulation.py#L55 56 | */ 57 | export const computeY = (ampFactor: bigint, x: bigint, d: bigint): bigint => { 58 | const Ann = ampFactor * N_COINS; // A*n^n 59 | // sum' = prod' = x 60 | const b = x + d / Ann - d; // b = sum' - (A*n**n - 1) * D / (A * n**n) 61 | // c = D ** (n + 1) / (n ** (2 * n) * prod' * A) 62 | const c = (d * d * d) / (N_COINS * (N_COINS * (x * Ann))); 63 | 64 | let yPrev = ZERO; 65 | let y = d; 66 | for (let i = 0; i < MAX_ITERS && abs(y - yPrev) > ONE; i++) { 67 | yPrev = y; 68 | y = (y * y + c) / (N_COINS * y + b); 69 | } 70 | 71 | return y; 72 | }; 73 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/calculator/curve.unit.test.ts: -------------------------------------------------------------------------------- 1 | import type { BigintIsh } from "@saberhq/token-utils"; 2 | 3 | import { computeD, computeY } from "./curve.js"; 4 | 5 | const assertBN = (actual: BigintIsh, expected: BigintIsh) => { 6 | expect(actual.toString()).toEqual(expected.toString()); 7 | }; 8 | 9 | describe("Calculator tests", () => { 10 | it("computeD", () => { 11 | assertBN(computeD(BigInt(100), BigInt(0), BigInt(0)), BigInt(0)); 12 | assertBN( 13 | computeD(BigInt(100), BigInt(1000000000), BigInt(1000000000)), 14 | BigInt("2000000000"), 15 | ); 16 | assertBN(computeD(BigInt(73), BigInt(92), BigInt(81)), BigInt(173)); 17 | assertBN( 18 | computeD(BigInt(11503), BigInt(28338), BigInt(78889)), 19 | BigInt(107225), 20 | ); 21 | assertBN(computeD(BigInt(8552), BigInt(26), BigInt(69321)), BigInt(66920)); 22 | assertBN(computeD(BigInt(496), BigInt(62), BigInt(68567)), BigInt(57447)); 23 | assertBN( 24 | computeD( 25 | BigInt("17653203515214796177"), 26 | BigInt("13789683482691983066"), 27 | BigInt("3964443602730479576"), 28 | ), 29 | BigInt("17754127085422462641"), 30 | ); 31 | }); 32 | 33 | it("computeY", () => { 34 | assertBN(computeY(BigInt(100), BigInt(100), BigInt(0)), BigInt(0)); 35 | assertBN(computeY(BigInt(8), BigInt(94), BigInt(163)), BigInt(69)); 36 | assertBN( 37 | computeY(BigInt(2137), BigInt(905777403660), BigInt(830914146046)), 38 | BigInt(490376033), 39 | ); 40 | assertBN( 41 | computeY( 42 | BigInt("17095344176474858097"), 43 | BigInt(383), 44 | BigInt("2276818911077272163"), 45 | ), 46 | BigInt("2276917873767753112"), 47 | ); 48 | assertBN( 49 | computeY( 50 | BigInt("7644937799120520965"), 51 | BigInt("14818904982296505121"), 52 | BigInt("17480022366793075404"), 53 | ), 54 | BigInt("2661117384496570284"), 55 | ); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/calculator/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./amounts.js"; 2 | export * from "./curve.js"; 3 | export * from "./price.js"; 4 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/calculator/price.ts: -------------------------------------------------------------------------------- 1 | import { Price, TokenAmount } from "@saberhq/token-utils"; 2 | import BN from "bn.js"; 3 | 4 | import type { IExchangeInfo } from "../index.js"; 5 | import { calculateEstimatedSwapOutputAmount } from "../index.js"; 6 | 7 | /** 8 | * Gets the price of the second token in the swap, i.e. "Token 1", with respect to "Token 0". 9 | * 10 | * To get the price of "Token 0", use `.invert()` on the result of this function. 11 | * @returns 12 | */ 13 | export const calculateSwapPrice = (exchangeInfo: IExchangeInfo): Price => { 14 | const reserve0 = exchangeInfo.reserves[0].amount; 15 | const reserve1 = exchangeInfo.reserves[1].amount; 16 | 17 | // We try to get at least 4 decimal points of precision here 18 | // Otherwise, we attempt to swap 1% of total supply of the pool 19 | // or at most, $1 20 | const inputAmountNum = Math.max( 21 | 10_000, 22 | Math.min( 23 | 10 ** reserve0.token.decimals, 24 | Math.floor(parseInt(reserve0.toU64().div(new BN(100)).toString())), 25 | ), 26 | ); 27 | 28 | const inputAmount = new TokenAmount(reserve0.token, inputAmountNum); 29 | const outputAmount = calculateEstimatedSwapOutputAmount( 30 | exchangeInfo, 31 | inputAmount, 32 | ); 33 | 34 | const frac = outputAmount.outputAmountBeforeFees.asFraction.divide( 35 | inputAmount.asFraction, 36 | ); 37 | 38 | return new Price( 39 | reserve0.token, 40 | reserve1.token, 41 | frac.denominator, 42 | frac.numerator, 43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/constants.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@solana/web3.js"; 2 | 3 | export const DEFAULT_TOKEN_DECIMALS = 6; 4 | 5 | export const ZERO_TS = 0; 6 | 7 | export const SWAP_PROGRAM_ID = new PublicKey( 8 | "SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ", 9 | ); 10 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./exchange.js"; 2 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/events.ts: -------------------------------------------------------------------------------- 1 | import { u64 } from "@saberhq/token-utils"; 2 | 3 | export interface StableSwapEvent { 4 | type: string; 5 | tokenAAmount?: u64; 6 | tokenBAmount?: u64; 7 | poolTokenAmount?: u64; 8 | fee?: u64; 9 | } 10 | 11 | const parseUint = (str?: string): u64 | undefined => 12 | !str || str === "0x0" ? undefined : new u64(str.slice("0x".length), 16); 13 | 14 | const parseEventRaw = (type: string, msg: string): StableSwapEvent => { 15 | const parts = msg.slice("Program log: ".length).split(", "); 16 | return Object.entries({ 17 | type, 18 | tokenAAmount: parseUint(parts[1]), 19 | tokenBAmount: parseUint(parts[2]), 20 | poolTokenAmount: parseUint(parts[3]), 21 | fee: parseUint(parts[4]), 22 | }) 23 | .filter(([, v]) => !!v) 24 | .reduce((acc, [k, v]) => ({ ...acc, [k]: v }), {}) as StableSwapEvent; 25 | }; 26 | 27 | /** 28 | * Parses the log message to return the StableSwap info about the transaction. 29 | * @param logMessages 30 | * @returns 31 | */ 32 | export const parseEventLogs = ( 33 | logMessages?: string[] | null, 34 | ): readonly StableSwapEvent[] => { 35 | if (!logMessages) { 36 | return []; 37 | } 38 | return logMessages.reduce((acc, logMessage, i) => { 39 | const nextLog = logMessages[i + 1]; 40 | return logMessage.startsWith("Program log: Event: ") && nextLog 41 | ? [ 42 | ...acc, 43 | parseEventRaw( 44 | logMessage.slice("Program log: Event: ".length), 45 | nextLog, 46 | ), 47 | ] 48 | : acc; 49 | }, [] as StableSwapEvent[]); 50 | }; 51 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:stableswap-sdk/README.md]] 3 | * @module 4 | */ 5 | 6 | export * from "./calculator/index.js"; 7 | export * from "./constants.js"; 8 | export * from "./entities/index.js"; 9 | export * from "./events.js"; 10 | export * from "./instructions/index.js"; 11 | export * from "./stable-swap.js"; 12 | export * from "./state/index.js"; 13 | export * from "./util/index.js"; 14 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/instructions/common.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PublicKey, 3 | TransactionInstructionCtorFields, 4 | } from "@solana/web3.js"; 5 | import { TransactionInstruction } from "@solana/web3.js"; 6 | 7 | export const buildInstruction = ({ 8 | config: { swapProgramID }, 9 | keys, 10 | data, 11 | }: Pick & { 12 | config: StableSwapConfig; 13 | }): TransactionInstruction => { 14 | return new TransactionInstruction({ 15 | keys, 16 | programId: swapProgramID, 17 | data, 18 | }); 19 | }; 20 | 21 | export interface StableSwapConfig { 22 | /** 23 | * The public key identifying this instance of the Stable Swap. 24 | */ 25 | readonly swapAccount: PublicKey; 26 | /** 27 | * Authority 28 | */ 29 | readonly authority: PublicKey; 30 | /** 31 | * Program Identifier for the Swap program 32 | */ 33 | readonly swapProgramID: PublicKey; 34 | /** 35 | * Program Identifier for the Token program 36 | */ 37 | readonly tokenProgramID: PublicKey; 38 | } 39 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/instructions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./admin.js"; 2 | export type { StableSwapConfig } from "./common.js"; 3 | export * from "./layouts.js"; 4 | export * from "./swap.js"; 5 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/instructions/layouts.ts: -------------------------------------------------------------------------------- 1 | import { Uint64Layout } from "@saberhq/token-utils"; 2 | import * as BufferLayout from "@solana/buffer-layout"; 3 | 4 | import type { RawFees } from "../state/layout.js"; 5 | import { FeesLayout } from "../state/layout.js"; 6 | import type { AdminInstruction } from "./admin.js"; 7 | import type { StableSwapInstruction } from "./swap.js"; 8 | 9 | export const InitializeSwapIXLayout = BufferLayout.struct<{ 10 | instruction: StableSwapInstruction.INITIALIZE; 11 | nonce: number; 12 | ampFactor: Uint8Array; 13 | fees: RawFees; 14 | }>([ 15 | BufferLayout.u8("instruction"), 16 | BufferLayout.u8("nonce"), 17 | Uint64Layout("ampFactor"), 18 | FeesLayout, 19 | ]); 20 | 21 | export const SwapIXLayout = BufferLayout.struct<{ 22 | instruction: StableSwapInstruction.SWAP; 23 | amountIn: Uint8Array; 24 | minimumAmountOut: Uint8Array; 25 | }>([ 26 | BufferLayout.u8("instruction"), 27 | Uint64Layout("amountIn"), 28 | Uint64Layout("minimumAmountOut"), 29 | ]); 30 | 31 | export const DepositIXLayout = BufferLayout.struct<{ 32 | instruction: StableSwapInstruction.DEPOSIT; 33 | tokenAmountA: Uint8Array; 34 | tokenAmountB: Uint8Array; 35 | minimumPoolTokenAmount: Uint8Array; 36 | }>([ 37 | BufferLayout.u8("instruction"), 38 | Uint64Layout("tokenAmountA"), 39 | Uint64Layout("tokenAmountB"), 40 | Uint64Layout("minimumPoolTokenAmount"), 41 | ]); 42 | 43 | export const WithdrawIXLayout = BufferLayout.struct<{ 44 | instruction: StableSwapInstruction.WITHDRAW; 45 | poolTokenAmount: Uint8Array; 46 | minimumTokenA: Uint8Array; 47 | minimumTokenB: Uint8Array; 48 | }>([ 49 | BufferLayout.u8("instruction"), 50 | Uint64Layout("poolTokenAmount"), 51 | Uint64Layout("minimumTokenA"), 52 | Uint64Layout("minimumTokenB"), 53 | ]); 54 | 55 | export const WithdrawOneIXLayout = BufferLayout.struct<{ 56 | instruction: StableSwapInstruction.WITHDRAW_ONE; 57 | poolTokenAmount: Uint8Array; 58 | minimumTokenAmount: Uint8Array; 59 | }>([ 60 | BufferLayout.u8("instruction"), 61 | Uint64Layout("poolTokenAmount"), 62 | Uint64Layout("minimumTokenAmount"), 63 | ]); 64 | 65 | export const RampAIXLayout = BufferLayout.struct<{ 66 | instruction: AdminInstruction.RAMP_A; 67 | targetAmp: Uint8Array; 68 | stopRampTS: number; 69 | }>([ 70 | BufferLayout.u8("instruction"), 71 | Uint64Layout("targetAmp"), 72 | BufferLayout.ns64("stopRampTS"), 73 | ]); 74 | 75 | export const StopRampAIXLayout = BufferLayout.struct<{ 76 | instruction: AdminInstruction.STOP_RAMP_A; 77 | }>([BufferLayout.u8("instruction")]); 78 | 79 | export const PauseIXLayout = BufferLayout.struct<{ 80 | instruction: AdminInstruction.PAUSE; 81 | }>([BufferLayout.u8("instruction")]); 82 | 83 | export const UnpauseIXLayout = BufferLayout.struct<{ 84 | instruction: AdminInstruction.UNPAUSE; 85 | }>([BufferLayout.u8("instruction")]); 86 | 87 | export const SetFeeAccountIXLayout = BufferLayout.struct<{ 88 | instruction: AdminInstruction.SET_FEE_ACCOUNT; 89 | }>([BufferLayout.u8("instruction")]); 90 | 91 | export const ApplyNewAdminIXLayout = BufferLayout.struct<{ 92 | instruction: AdminInstruction.APPLY_NEW_ADMIN; 93 | }>([BufferLayout.u8("instruction")]); 94 | 95 | export const CommitNewAdminIXLayout = BufferLayout.struct<{ 96 | instruction: AdminInstruction.COMMIT_NEW_ADMIN; 97 | }>([BufferLayout.u8("instruction")]); 98 | 99 | export const SetNewFeesIXLayout = BufferLayout.struct<{ 100 | instruction: AdminInstruction.SET_NEW_FEES; 101 | fees: RawFees; 102 | }>([BufferLayout.u8("instruction"), FeesLayout]); 103 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/state/fees.ts: -------------------------------------------------------------------------------- 1 | import { Percent, u64 } from "@saberhq/token-utils"; 2 | 3 | import type { RawFees } from "./layout.js"; 4 | 5 | export type Fees = { 6 | trade: Percent; 7 | withdraw: Percent; 8 | adminTrade: Percent; 9 | adminWithdraw: Percent; 10 | }; 11 | 12 | export const DEFAULT_FEE = new Percent(0, 10000); 13 | 14 | export const ZERO_FEES: Fees = { 15 | /** 16 | * Fee per trade 17 | */ 18 | trade: DEFAULT_FEE, 19 | withdraw: DEFAULT_FEE, 20 | adminTrade: DEFAULT_FEE, 21 | adminWithdraw: DEFAULT_FEE, 22 | }; 23 | 24 | const recommendedFeesRaw = { 25 | adminTradeFeeNumerator: 50, 26 | adminTradeFeeDenominator: 100, 27 | adminWithdrawFeeNumerator: 50, 28 | adminWithdrawFeeDenominator: 100, 29 | tradeFeeNumerator: 20, 30 | tradeFeeDenominator: 10000, 31 | withdrawFeeNumerator: 50, 32 | withdrawFeeDenominator: 10000, 33 | }; 34 | 35 | export const RECOMMENDED_FEES: Fees = { 36 | trade: new Percent( 37 | recommendedFeesRaw.tradeFeeNumerator, 38 | recommendedFeesRaw.tradeFeeDenominator, 39 | ), 40 | withdraw: new Percent( 41 | recommendedFeesRaw.withdrawFeeNumerator, 42 | recommendedFeesRaw.withdrawFeeDenominator, 43 | ), 44 | adminTrade: new Percent( 45 | recommendedFeesRaw.adminTradeFeeNumerator, 46 | recommendedFeesRaw.adminTradeFeeDenominator, 47 | ), 48 | adminWithdraw: new Percent( 49 | recommendedFeesRaw.adminWithdrawFeeNumerator, 50 | recommendedFeesRaw.adminWithdrawFeeDenominator, 51 | ), 52 | }; 53 | 54 | export const encodeFees = (fees: Fees): RawFees => ({ 55 | adminTradeFeeNumerator: new u64( 56 | fees.adminTrade.numerator.toString(), 57 | ).toBuffer(), 58 | adminTradeFeeDenominator: new u64( 59 | fees.adminTrade.denominator.toString(), 60 | ).toBuffer(), 61 | adminWithdrawFeeNumerator: new u64( 62 | fees.adminWithdraw.numerator.toString(), 63 | ).toBuffer(), 64 | adminWithdrawFeeDenominator: new u64( 65 | fees.adminWithdraw.denominator.toString(), 66 | ).toBuffer(), 67 | tradeFeeNumerator: new u64(fees.trade.numerator.toString()).toBuffer(), 68 | tradeFeeDenominator: new u64(fees.trade.denominator.toString()).toBuffer(), 69 | withdrawFeeNumerator: new u64(fees.withdraw.numerator.toString()).toBuffer(), 70 | withdrawFeeDenominator: new u64( 71 | fees.withdraw.denominator.toString(), 72 | ).toBuffer(), 73 | }); 74 | 75 | export const decodeFees = (raw: RawFees): Fees => ({ 76 | adminTrade: new Percent( 77 | u64.fromBuffer(Buffer.from(raw.adminTradeFeeNumerator)).toString(), 78 | u64.fromBuffer(Buffer.from(raw.adminTradeFeeDenominator)).toString(), 79 | ), 80 | adminWithdraw: new Percent( 81 | u64.fromBuffer(Buffer.from(raw.adminWithdrawFeeNumerator)).toString(), 82 | u64.fromBuffer(Buffer.from(raw.adminWithdrawFeeDenominator)).toString(), 83 | ), 84 | trade: new Percent( 85 | u64.fromBuffer(Buffer.from(raw.tradeFeeNumerator)).toString(), 86 | u64.fromBuffer(Buffer.from(raw.tradeFeeDenominator)).toString(), 87 | ), 88 | withdraw: new Percent( 89 | u64.fromBuffer(Buffer.from(raw.withdrawFeeNumerator)).toString(), 90 | u64.fromBuffer(Buffer.from(raw.withdrawFeeDenominator)).toString(), 91 | ), 92 | }); 93 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/state/index.ts: -------------------------------------------------------------------------------- 1 | import { u64 } from "@saberhq/token-utils"; 2 | import { PublicKey } from "@solana/web3.js"; 3 | 4 | import type { SwapTokenInfo } from "../instructions/swap.js"; 5 | import type { Fees } from "./fees.js"; 6 | import { decodeFees } from "./fees.js"; 7 | import { StableSwapLayout } from "./layout.js"; 8 | 9 | export * from "./fees.js"; 10 | export * from "./layout.js"; 11 | 12 | /** 13 | * State of a StableSwap, read from the swap account. 14 | */ 15 | export interface StableSwapState { 16 | /** 17 | * Whether or not the swap is initialized. 18 | */ 19 | isInitialized: boolean; 20 | 21 | /** 22 | * Whether or not the swap is paused. 23 | */ 24 | isPaused: boolean; 25 | 26 | /** 27 | * Nonce used to generate the swap authority. 28 | */ 29 | nonce: number; 30 | 31 | /** 32 | * Mint account for pool token 33 | */ 34 | poolTokenMint: PublicKey; 35 | 36 | /** 37 | * Admin account 38 | */ 39 | adminAccount: PublicKey; 40 | 41 | tokenA: SwapTokenInfo; 42 | tokenB: SwapTokenInfo; 43 | 44 | /** 45 | * Initial amplification coefficient (A) 46 | */ 47 | initialAmpFactor: u64; 48 | 49 | /** 50 | * Target amplification coefficient (A) 51 | */ 52 | targetAmpFactor: u64; 53 | 54 | /** 55 | * Ramp A start timestamp 56 | */ 57 | startRampTimestamp: number; 58 | 59 | /** 60 | * Ramp A start timestamp 61 | */ 62 | stopRampTimestamp: number; 63 | 64 | /** 65 | * When the future admin can no longer become the admin, if applicable. 66 | */ 67 | futureAdminDeadline: number; 68 | 69 | /** 70 | * The next admin. 71 | */ 72 | futureAdminAccount: PublicKey; 73 | 74 | /** 75 | * Fee schedule 76 | */ 77 | fees: Fees; 78 | } 79 | 80 | /** 81 | * Decodes the Swap account. 82 | * @param data 83 | * @returns 84 | */ 85 | export const decodeSwap = (data: Buffer): StableSwapState => { 86 | const stableSwapData = StableSwapLayout.decode(data); 87 | if (!stableSwapData.isInitialized) { 88 | throw new Error(`Invalid token swap state`); 89 | } 90 | const adminAccount = new PublicKey(stableSwapData.adminAccount); 91 | const adminFeeAccountA = new PublicKey(stableSwapData.adminFeeAccountA); 92 | const adminFeeAccountB = new PublicKey(stableSwapData.adminFeeAccountB); 93 | const tokenAccountA = new PublicKey(stableSwapData.tokenAccountA); 94 | const tokenAccountB = new PublicKey(stableSwapData.tokenAccountB); 95 | const poolTokenMint = new PublicKey(stableSwapData.tokenPool); 96 | const mintA = new PublicKey(stableSwapData.mintA); 97 | const mintB = new PublicKey(stableSwapData.mintB); 98 | const initialAmpFactor = u64.fromBuffer( 99 | Buffer.from(stableSwapData.initialAmpFactor), 100 | ); 101 | const targetAmpFactor = u64.fromBuffer( 102 | Buffer.from(stableSwapData.targetAmpFactor), 103 | ); 104 | const startRampTimestamp = stableSwapData.startRampTs; 105 | const stopRampTimestamp = stableSwapData.stopRampTs; 106 | const fees = decodeFees(stableSwapData.fees); 107 | return { 108 | isInitialized: !!stableSwapData.isInitialized, 109 | isPaused: !!stableSwapData.isPaused, 110 | nonce: stableSwapData.nonce, 111 | futureAdminDeadline: stableSwapData.futureAdminDeadline, 112 | futureAdminAccount: new PublicKey(stableSwapData.futureAdminAccount), 113 | adminAccount, 114 | tokenA: { 115 | adminFeeAccount: adminFeeAccountA, 116 | reserve: tokenAccountA, 117 | mint: mintA, 118 | }, 119 | tokenB: { 120 | adminFeeAccount: adminFeeAccountB, 121 | reserve: tokenAccountB, 122 | mint: mintB, 123 | }, 124 | poolTokenMint, 125 | initialAmpFactor, 126 | targetAmpFactor, 127 | startRampTimestamp, 128 | stopRampTimestamp, 129 | fees, 130 | }; 131 | }; 132 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/state/layout.ts: -------------------------------------------------------------------------------- 1 | import { PublicKeyLayout, Uint64Layout } from "@saberhq/token-utils"; 2 | import * as BufferLayout from "@solana/buffer-layout"; 3 | 4 | /** 5 | * Raw representation of fees. 6 | */ 7 | export interface RawFees { 8 | adminTradeFeeNumerator: Uint8Array; 9 | adminTradeFeeDenominator: Uint8Array; 10 | adminWithdrawFeeNumerator: Uint8Array; 11 | adminWithdrawFeeDenominator: Uint8Array; 12 | tradeFeeNumerator: Uint8Array; 13 | tradeFeeDenominator: Uint8Array; 14 | withdrawFeeNumerator: Uint8Array; 15 | withdrawFeeDenominator: Uint8Array; 16 | } 17 | 18 | /** 19 | * Layout for StableSwap fees 20 | */ 21 | export const FeesLayout = BufferLayout.struct( 22 | [ 23 | Uint64Layout("adminTradeFeeNumerator"), 24 | Uint64Layout("adminTradeFeeDenominator"), 25 | Uint64Layout("adminWithdrawFeeNumerator"), 26 | Uint64Layout("adminWithdrawFeeDenominator"), 27 | Uint64Layout("tradeFeeNumerator"), 28 | Uint64Layout("tradeFeeDenominator"), 29 | Uint64Layout("withdrawFeeNumerator"), 30 | Uint64Layout("withdrawFeeDenominator"), 31 | ], 32 | "fees", 33 | ); 34 | 35 | /** 36 | * Layout for stable swap state 37 | */ 38 | export const StableSwapLayout = BufferLayout.struct<{ 39 | isInitialized: 0 | 1; 40 | isPaused: 0 | 1; 41 | nonce: number; 42 | initialAmpFactor: Uint8Array; 43 | targetAmpFactor: Uint8Array; 44 | startRampTs: number; 45 | stopRampTs: number; 46 | futureAdminDeadline: number; 47 | futureAdminAccount: Uint8Array; 48 | adminAccount: Uint8Array; 49 | tokenAccountA: Uint8Array; 50 | tokenAccountB: Uint8Array; 51 | tokenPool: Uint8Array; 52 | mintA: Uint8Array; 53 | mintB: Uint8Array; 54 | adminFeeAccountA: Uint8Array; 55 | adminFeeAccountB: Uint8Array; 56 | fees: RawFees; 57 | }>([ 58 | BufferLayout.u8("isInitialized"), 59 | BufferLayout.u8("isPaused"), 60 | BufferLayout.u8("nonce"), 61 | Uint64Layout("initialAmpFactor"), 62 | Uint64Layout("targetAmpFactor"), 63 | BufferLayout.ns64("startRampTs"), 64 | BufferLayout.ns64("stopRampTs"), 65 | BufferLayout.ns64("futureAdminDeadline"), 66 | PublicKeyLayout("futureAdminAccount"), 67 | PublicKeyLayout("adminAccount"), 68 | PublicKeyLayout("tokenAccountA"), 69 | PublicKeyLayout("tokenAccountB"), 70 | PublicKeyLayout("tokenPool"), 71 | PublicKeyLayout("mintA"), 72 | PublicKeyLayout("mintB"), 73 | PublicKeyLayout("adminFeeAccountA"), 74 | PublicKeyLayout("adminFeeAccountB"), 75 | FeesLayout, 76 | ]); 77 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/util/account.ts: -------------------------------------------------------------------------------- 1 | import type { Connection, PublicKey } from "@solana/web3.js"; 2 | 3 | /** 4 | * Loads the account info of an account owned by a program. 5 | * @param connection 6 | * @param address 7 | * @param programId 8 | * @returns 9 | */ 10 | export const loadProgramAccount = async ( 11 | connection: Connection, 12 | address: PublicKey, 13 | programId: PublicKey, 14 | ): Promise => { 15 | const accountInfo = await connection.getAccountInfo(address); 16 | if (accountInfo === null) { 17 | throw new Error("Failed to find account"); 18 | } 19 | 20 | if (!accountInfo.owner.equals(programId)) { 21 | throw new Error( 22 | `Invalid owner: expected ${programId.toBase58()}, found ${accountInfo.owner.toBase58()}`, 23 | ); 24 | } 25 | 26 | return Buffer.from(accountInfo.data); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./account.js"; 2 | export * from "./initialize.js"; 3 | export * from "./initializeSimple.js"; 4 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/src/util/instructions.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from "@saberhq/solana-contrib"; 2 | import { TransactionEnvelope } from "@saberhq/solana-contrib"; 3 | import type { 4 | ConfirmOptions, 5 | Signer, 6 | TransactionInstruction, 7 | } from "@solana/web3.js"; 8 | 9 | export interface TransactionInstructions { 10 | /** 11 | * Transaction instructions 12 | */ 13 | instructions: readonly TransactionInstruction[]; 14 | /** 15 | * Additional transaction signers if applicable 16 | */ 17 | signers: readonly Signer[]; 18 | } 19 | 20 | export interface MutableTransactionInstructions { 21 | /** 22 | * Transaction instructions 23 | */ 24 | instructions: TransactionInstruction[]; 25 | /** 26 | * Additional transaction signers if applicable 27 | */ 28 | signers: Signer[]; 29 | } 30 | 31 | export const createMutableTransactionInstructions = 32 | (): MutableTransactionInstructions => ({ 33 | instructions: [], 34 | signers: [], 35 | }); 36 | 37 | /** 38 | * Executes a TransactionInstructions 39 | * @param title 40 | * @param param1 41 | * @param param2 42 | * @returns Transaction signature 43 | */ 44 | export const executeTxInstructions = async ( 45 | title: string, 46 | { instructions, signers }: TransactionInstructions, 47 | { 48 | provider, 49 | payerSigner, 50 | options, 51 | }: { 52 | provider: Provider; 53 | payerSigner: Signer; 54 | options?: ConfirmOptions; 55 | }, 56 | ): Promise => { 57 | console.log(`Running tx ${title}`); 58 | const txEnv = new TransactionEnvelope(provider, instructions.slice(), [ 59 | // payer of the tx 60 | payerSigner, 61 | // initialize the swap 62 | ...signers, 63 | ]); 64 | 65 | const sig = await txEnv.confirm(options); 66 | console.log(`${title} done at tx: ${sig.signature}`); 67 | return sig.signature; 68 | }; 69 | 70 | export const mergeInstructions = ( 71 | mut: MutableTransactionInstructions, 72 | inst: TransactionInstructions, 73 | ): void => { 74 | mut.instructions.push(...inst.instructions); 75 | mut.signers.push(...inst.signers); 76 | }; 77 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.cjs.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs/" 5 | }, 6 | "include": ["src/"], 7 | "exclude": ["**/*.test.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/stableswap-sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.mono.json", 3 | "compilerOptions": { 4 | "rootDir": "src/", 5 | "outDir": "dist/esm/" 6 | }, 7 | "include": ["src/"], 8 | "exclude": ["**/*.test.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/token-utils/README.md: -------------------------------------------------------------------------------- 1 | # `@saberhq/token-utils` 2 | 3 | Math and utilities for SPL tokens. 4 | 5 | ## Documentation 6 | 7 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/docs/developing/overview). 8 | 9 | Automatically generated TypeScript documentation can be found [on GitHub pages](https://saber-hq.github.io/saber-common/). 10 | 11 | ## Installation 12 | 13 | ``` 14 | yarn add @saberhq/token-utils 15 | ``` 16 | 17 | ## License 18 | 19 | Apache 2.0 20 | -------------------------------------------------------------------------------- /packages/token-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saberhq/token-utils", 3 | "description": "Token-related math and transaction utilities for Solana.", 4 | "version": "3.0.0", 5 | "homepage": "https://github.com/saber-hq/saber-common/tree/master/packages/token-utils#readme", 6 | "repository": "git+https://github.com/saber-hq/saber-common.git", 7 | "bugs": "https://github.com/saber-hq/saber-common/issues", 8 | "funding": "https://www.coingecko.com/en/coins/saber", 9 | "author": "Saber Team ", 10 | "license": "Apache-2.0", 11 | "type": "module", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/esm/index.js", 15 | "require": "./dist/cjs/index.js" 16 | } 17 | }, 18 | "main": "dist/cjs/index.js", 19 | "module": "dist/esm/index.js", 20 | "keywords": [ 21 | "solana", 22 | "saber" 23 | ], 24 | "scripts": { 25 | "build": "tsc && tsc -P tsconfig.cjs.json", 26 | "clean": "rm -fr dist/", 27 | "prepublishOnly": "npm run build" 28 | }, 29 | "dependencies": { 30 | "@saberhq/solana-contrib": "workspace:^", 31 | "@solana/buffer-layout": "^4.0.1", 32 | "@solana/spl-token": "^0.1.8", 33 | "@ubeswap/token-math": "^6.0.0", 34 | "tiny-invariant": "^1.3.3", 35 | "tslib": "^2.6.2" 36 | }, 37 | "files": [ 38 | "dist/", 39 | "src/" 40 | ], 41 | "devDependencies": { 42 | "@saberhq/tsconfig": "^3.3.1", 43 | "@solana/web3.js": "^1.91.1", 44 | "@types/bn.js": "^5.1.5", 45 | "jsbi": "^4.3.0", 46 | "typescript": "^5.4.5" 47 | }, 48 | "peerDependencies": { 49 | "@solana/web3.js": "^1.42", 50 | "bn.js": "^4 || ^5", 51 | "jsbi": "^3 || ^4" 52 | }, 53 | "publishConfig": { 54 | "access": "public" 55 | }, 56 | "gitHead": "f9fd3fbd36a7a6dd6f5e9597af5309affe50ac0e" 57 | } 58 | -------------------------------------------------------------------------------- /packages/token-utils/src/ata.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from "@saberhq/solana-contrib"; 2 | import { getProgramAddress } from "@saberhq/solana-contrib"; 3 | import { 4 | ASSOCIATED_TOKEN_PROGRAM_ID, 5 | TOKEN_PROGRAM_ID, 6 | } from "@solana/spl-token"; 7 | 8 | /** 9 | * Gets an associated token account address. 10 | * 11 | * @deprecated use {@link getATAAddressSync} 12 | */ 13 | export const getATAAddress = async ({ 14 | mint, 15 | owner, 16 | }: { 17 | mint: PublicKey; 18 | owner: PublicKey; 19 | }): Promise => { 20 | return Promise.resolve(getATAAddressSync({ mint, owner })); 21 | }; 22 | 23 | /** 24 | * Gets an associated token account address synchronously. 25 | */ 26 | export const getATAAddressSync = ({ 27 | mint, 28 | owner, 29 | }: { 30 | mint: PublicKey; 31 | owner: PublicKey; 32 | }): PublicKey => { 33 | return getProgramAddress( 34 | [owner.toBuffer(), TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], 35 | ASSOCIATED_TOKEN_PROGRAM_ID, 36 | ); 37 | }; 38 | 39 | export type ATAMap = { 40 | [mint in K]: { 41 | address: PublicKey; 42 | mint: PublicKey; 43 | }; 44 | }; 45 | 46 | /** 47 | * Gets multiple associated token account addresses. 48 | * 49 | * @deprecated use {@link getATAAddressesSync} 50 | */ 51 | export const getATAAddresses = ({ 52 | mints, 53 | owner, 54 | }: { 55 | mints: { 56 | [mint in K]: PublicKey; 57 | }; 58 | owner: PublicKey; 59 | }): Promise<{ 60 | /** 61 | * All ATAs 62 | */ 63 | accounts: ATAMap; 64 | }> => { 65 | return Promise.resolve(getATAAddressesSync({ mints, owner })); 66 | }; 67 | 68 | /** 69 | * Gets multiple associated token account addresses. 70 | */ 71 | export const getATAAddressesSync = ({ 72 | mints, 73 | owner, 74 | }: { 75 | mints: { 76 | [mint in K]: PublicKey; 77 | }; 78 | owner: PublicKey; 79 | }): { 80 | /** 81 | * All ATAs 82 | */ 83 | accounts: ATAMap; 84 | } => { 85 | const result = Object.entries(mints).map( 86 | ( 87 | args, 88 | ): { 89 | address: PublicKey; 90 | name: string; 91 | mint: PublicKey; 92 | } => { 93 | const [name, mint] = args as [K, PublicKey]; 94 | const result = getATAAddressSync({ 95 | mint, 96 | owner: owner, 97 | }); 98 | return { 99 | address: result, 100 | name, 101 | mint, 102 | }; 103 | }, 104 | ); 105 | const deduped = result.reduce( 106 | (acc, { address, name, mint }) => { 107 | return { 108 | accounts: { 109 | ...acc.accounts, 110 | [name]: { address, mint }, 111 | }, 112 | }; 113 | }, 114 | { accounts: {} } as { 115 | accounts: ATAMap; 116 | }, 117 | ); 118 | return { 119 | accounts: deduped.accounts, 120 | }; 121 | }; 122 | -------------------------------------------------------------------------------- /packages/token-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:token-utils/README.md]] 3 | * @module 4 | */ 5 | 6 | export * from "./ata.js"; 7 | export * from "./instructions/index.js"; 8 | export * from "./layout.js"; 9 | export * from "./price.js"; 10 | export * from "./splTokenRegistry.js"; 11 | export * from "./token.js"; 12 | export * from "./tokenAmount.js"; 13 | export * from "./tokenList.js"; 14 | export * from "./tokenOwner.js"; 15 | export * from "./tokenProvider.js"; 16 | 17 | // re-export token-math types 18 | // so consumers don't need to use them 19 | 20 | export type { BigintIsh, IFormatUint, NumberFormat } from "@ubeswap/token-math"; 21 | export { 22 | Fraction, 23 | makeDecimalMultiplier, 24 | MAX_U64, 25 | MAX_U256, 26 | ONE, 27 | parseBigintIsh, 28 | Percent, 29 | Rounding, 30 | TEN, 31 | validateU64, 32 | validateU256, 33 | ZERO, 34 | } from "@ubeswap/token-math"; 35 | 36 | // serum common 37 | export * from "./common.js"; 38 | 39 | // re-export SPL token types 40 | export type { 41 | AuthorityType, 42 | MintInfo as MintData, 43 | MultisigInfo, 44 | } from "@solana/spl-token"; 45 | export { 46 | ASSOCIATED_TOKEN_PROGRAM_ID, 47 | NATIVE_MINT, 48 | Token as SPLToken, 49 | TOKEN_PROGRAM_ID, 50 | u64, 51 | } from "@solana/spl-token"; 52 | -------------------------------------------------------------------------------- /packages/token-utils/src/instructions/account.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from "@saberhq/solana-contrib"; 2 | import { TransactionEnvelope } from "@saberhq/solana-contrib"; 3 | import { Token as SPLToken, TOKEN_PROGRAM_ID } from "@solana/spl-token"; 4 | import type { PublicKey, Signer } from "@solana/web3.js"; 5 | import { Keypair, SystemProgram } from "@solana/web3.js"; 6 | 7 | import { TokenAccountLayout } from "../layout.js"; 8 | 9 | export const createTokenAccount = async ({ 10 | provider, 11 | mint, 12 | owner = provider.wallet.publicKey, 13 | payer = provider.wallet.publicKey, 14 | accountSigner = Keypair.generate(), 15 | }: { 16 | provider: Provider; 17 | mint: PublicKey; 18 | owner?: PublicKey; 19 | payer?: PublicKey; 20 | /** 21 | * The keypair of the account to be created. 22 | */ 23 | accountSigner?: Signer; 24 | }): Promise<{ 25 | key: PublicKey; 26 | tx: TransactionEnvelope; 27 | }> => { 28 | // Allocate memory for the account 29 | const rentExemptAccountBalance = 30 | await SPLToken.getMinBalanceRentForExemptAccount(provider.connection); 31 | return buildCreateTokenAccountTX({ 32 | provider, 33 | mint, 34 | rentExemptAccountBalance, 35 | owner, 36 | payer, 37 | accountSigner, 38 | }); 39 | }; 40 | 41 | export const buildCreateTokenAccountTX = ({ 42 | provider, 43 | mint, 44 | rentExemptAccountBalance, 45 | owner = provider.wallet.publicKey, 46 | payer = provider.wallet.publicKey, 47 | accountSigner = Keypair.generate(), 48 | }: { 49 | provider: Provider; 50 | mint: PublicKey; 51 | /** 52 | * SOL needed for a rent exempt token account. 53 | */ 54 | rentExemptAccountBalance: number; 55 | owner?: PublicKey; 56 | payer?: PublicKey; 57 | /** 58 | * The keypair of the account to be created. 59 | */ 60 | accountSigner?: Signer; 61 | }): { 62 | key: PublicKey; 63 | tx: TransactionEnvelope; 64 | } => { 65 | const tokenAccount = accountSigner.publicKey; 66 | return { 67 | key: tokenAccount, 68 | tx: new TransactionEnvelope( 69 | provider, 70 | [ 71 | SystemProgram.createAccount({ 72 | fromPubkey: payer, 73 | newAccountPubkey: accountSigner.publicKey, 74 | lamports: rentExemptAccountBalance, 75 | space: TokenAccountLayout.span, 76 | programId: TOKEN_PROGRAM_ID, 77 | }), 78 | SPLToken.createInitAccountInstruction( 79 | TOKEN_PROGRAM_ID, 80 | mint, 81 | tokenAccount, 82 | owner, 83 | ), 84 | ], 85 | [accountSigner], 86 | ), 87 | }; 88 | }; 89 | -------------------------------------------------------------------------------- /packages/token-utils/src/instructions/ata.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from "@saberhq/solana-contrib"; 2 | import { 3 | ASSOCIATED_TOKEN_PROGRAM_ID, 4 | Token, 5 | TOKEN_PROGRAM_ID, 6 | } from "@solana/spl-token"; 7 | import type { TransactionInstruction } from "@solana/web3.js"; 8 | import { PublicKey } from "@solana/web3.js"; 9 | 10 | import { getATAAddressSync } from "../ata.js"; 11 | 12 | type GetOrCreateATAResult = { 13 | /** 14 | * ATA key 15 | */ 16 | address: PublicKey; 17 | /** 18 | * Instruction to create the account if it doesn't exist. 19 | */ 20 | instruction: TransactionInstruction | null; 21 | }; 22 | 23 | type GetOrCreateATAsResult = { 24 | /** 25 | * All accounts 26 | */ 27 | accounts: { [mint in K]: PublicKey }; 28 | /** 29 | * Instructions to create accounts that don't exist. 30 | */ 31 | instructions: readonly TransactionInstruction[]; 32 | /** 33 | * Instructions, keyed. 34 | */ 35 | createAccountInstructions: { [mint in K]: TransactionInstruction | null }; 36 | }; 37 | 38 | /** 39 | * Gets an associated token account, returning a create instruction if it doesn't exist. 40 | * @param param0 41 | * @returns 42 | */ 43 | export const getOrCreateATA = async ({ 44 | provider, 45 | mint, 46 | owner = provider.wallet.publicKey, 47 | payer = provider.wallet.publicKey, 48 | }: { 49 | provider: Provider; 50 | mint: PublicKey; 51 | owner?: PublicKey; 52 | payer?: PublicKey; 53 | }): Promise => { 54 | const address = getATAAddressSync({ mint, owner }); 55 | if (await provider.getAccountInfo(address)) { 56 | return { address, instruction: null }; 57 | } else { 58 | return { 59 | address, 60 | instruction: createATAInstruction({ 61 | mint, 62 | address, 63 | owner, 64 | payer, 65 | }), 66 | }; 67 | } 68 | }; 69 | 70 | /** 71 | * Gets ATAs and creates them if they don't exist. 72 | * @param param0 73 | * @returns 74 | */ 75 | export const getOrCreateATAs = async ({ 76 | provider, 77 | mints, 78 | owner = provider.wallet.publicKey, 79 | }: { 80 | provider: Provider; 81 | mints: { 82 | [mint in K]: PublicKey; 83 | }; 84 | owner?: PublicKey; 85 | }): Promise> => { 86 | const result = await Promise.all( 87 | Object.entries(mints).map( 88 | async ([name, mint]): Promise<{ 89 | address: PublicKey; 90 | name: string; 91 | mintKey: PublicKey; 92 | instruction: TransactionInstruction | null; 93 | }> => { 94 | const mintKey = new PublicKey(mint as PublicKey); 95 | const result = await getOrCreateATA({ 96 | provider, 97 | mint: mintKey, 98 | owner: owner, 99 | payer: provider.wallet.publicKey, 100 | }); 101 | return { 102 | address: result.address, 103 | instruction: result.instruction, 104 | name, 105 | mintKey, 106 | }; 107 | }, 108 | ), 109 | ); 110 | 111 | const deduped = result.reduce( 112 | (acc, { address, name, instruction }) => { 113 | return { 114 | accounts: { 115 | ...acc.accounts, 116 | [name]: address, 117 | }, 118 | createAccountInstructions: { 119 | ...acc.createAccountInstructions, 120 | [name]: instruction, 121 | }, 122 | instructions: instruction 123 | ? { 124 | ...acc.instructions, 125 | [address.toString()]: instruction, 126 | } 127 | : acc.instructions, 128 | }; 129 | }, 130 | { accounts: {}, instructions: {}, createAccountInstructions: {} } as { 131 | accounts: { [key in K]?: PublicKey }; 132 | createAccountInstructions: { [key in K]?: TransactionInstruction | null }; 133 | instructions: { [address: string]: TransactionInstruction }; 134 | }, 135 | ); 136 | return { 137 | accounts: deduped.accounts, 138 | createAccountInstructions: deduped.createAccountInstructions, 139 | instructions: Object.values(deduped.instructions), 140 | } as GetOrCreateATAsResult; 141 | }; 142 | 143 | /** 144 | * Instruction for creating an ATA. 145 | * @returns 146 | */ 147 | export const createATAInstruction = ({ 148 | address, 149 | mint, 150 | owner, 151 | payer, 152 | }: { 153 | address: PublicKey; 154 | mint: PublicKey; 155 | owner: PublicKey; 156 | payer: PublicKey; 157 | }): TransactionInstruction => 158 | Token.createAssociatedTokenAccountInstruction( 159 | ASSOCIATED_TOKEN_PROGRAM_ID, 160 | TOKEN_PROGRAM_ID, 161 | mint, 162 | address, 163 | owner, 164 | payer, 165 | ); 166 | -------------------------------------------------------------------------------- /packages/token-utils/src/instructions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./account.js"; 2 | export * from "./ata.js"; 3 | export * from "./mint.js"; 4 | export * from "./nft.js"; 5 | -------------------------------------------------------------------------------- /packages/token-utils/src/instructions/mint.ts: -------------------------------------------------------------------------------- 1 | import type { Provider } from "@saberhq/solana-contrib"; 2 | import { TransactionEnvelope } from "@saberhq/solana-contrib"; 3 | import type { u64 } from "@solana/spl-token"; 4 | import { Token as SPLToken, TOKEN_PROGRAM_ID } from "@solana/spl-token"; 5 | import type { PublicKey, Signer } from "@solana/web3.js"; 6 | import { SystemProgram } from "@solana/web3.js"; 7 | 8 | import { MintLayout } from "../layout.js"; 9 | 10 | /** 11 | * Creates instructions for initializing a mint. 12 | * @param param0 13 | * @returns 14 | */ 15 | export const createInitMintInstructions = async ({ 16 | provider, 17 | mintKP, 18 | decimals, 19 | mintAuthority = provider.wallet.publicKey, 20 | freezeAuthority = null, 21 | }: { 22 | provider: Provider; 23 | mintKP: Signer; 24 | decimals: number; 25 | mintAuthority?: PublicKey; 26 | freezeAuthority?: PublicKey | null; 27 | }): Promise => { 28 | return createInitMintTX({ 29 | provider, 30 | mintKP, 31 | decimals, 32 | rentExemptMintBalance: await SPLToken.getMinBalanceRentForExemptMint( 33 | provider.connection, 34 | ), 35 | mintAuthority, 36 | freezeAuthority, 37 | }); 38 | }; 39 | 40 | /** 41 | * Creates instructions for initializing a mint. 42 | * @param param0 43 | * @returns 44 | */ 45 | export const createInitMintTX = ({ 46 | provider, 47 | mintKP, 48 | decimals, 49 | rentExemptMintBalance, 50 | mintAuthority = provider.wallet.publicKey, 51 | freezeAuthority = null, 52 | }: { 53 | provider: Provider; 54 | mintKP: Signer; 55 | decimals: number; 56 | rentExemptMintBalance: number; 57 | mintAuthority?: PublicKey; 58 | freezeAuthority?: PublicKey | null; 59 | }): TransactionEnvelope => { 60 | const from = provider.wallet.publicKey; 61 | return new TransactionEnvelope( 62 | provider, 63 | [ 64 | SystemProgram.createAccount({ 65 | fromPubkey: from, 66 | newAccountPubkey: mintKP.publicKey, 67 | space: MintLayout.span, 68 | lamports: rentExemptMintBalance, 69 | programId: TOKEN_PROGRAM_ID, 70 | }), 71 | SPLToken.createInitMintInstruction( 72 | TOKEN_PROGRAM_ID, 73 | mintKP.publicKey, 74 | decimals, 75 | mintAuthority, 76 | freezeAuthority, 77 | ), 78 | ], 79 | [mintKP], 80 | ); 81 | }; 82 | 83 | export const createMintToInstruction = ({ 84 | provider, 85 | mint, 86 | mintAuthorityKP, 87 | to, 88 | amount, 89 | }: { 90 | provider: Provider; 91 | mint: PublicKey; 92 | mintAuthorityKP: Signer; 93 | to: PublicKey; 94 | amount: u64; 95 | }): TransactionEnvelope => { 96 | return new TransactionEnvelope( 97 | provider, 98 | [ 99 | SPLToken.createMintToInstruction( 100 | TOKEN_PROGRAM_ID, 101 | mint, 102 | to, 103 | mintAuthorityKP.publicKey, 104 | [], 105 | amount, 106 | ), 107 | ], 108 | [mintAuthorityKP], 109 | ); 110 | }; 111 | -------------------------------------------------------------------------------- /packages/token-utils/src/instructions/nft.ts: -------------------------------------------------------------------------------- 1 | import type { Provider, TransactionEnvelope } from "@saberhq/solana-contrib"; 2 | import { Token as SPLToken, TOKEN_PROGRAM_ID, u64 } from "@solana/spl-token"; 3 | import type { PublicKey, Signer } from "@solana/web3.js"; 4 | 5 | import { getOrCreateATA } from "./ata.js"; 6 | import { createInitMintInstructions } from "./mint.js"; 7 | 8 | export const mintNFT = async ( 9 | provider: Provider, 10 | mintKP: Signer, 11 | owner: PublicKey = provider.wallet.publicKey, 12 | ): Promise => { 13 | // Temporary mint authority 14 | const tempMintAuthority = provider.wallet.publicKey; 15 | // Mint for the NFT 16 | const tx = await createInitMintInstructions({ 17 | provider, 18 | mintKP, 19 | decimals: 0, 20 | mintAuthority: tempMintAuthority, 21 | }); 22 | // Token account for the NFT 23 | const { address, instruction } = await getOrCreateATA({ 24 | provider, 25 | mint: mintKP.publicKey, 26 | owner: owner, 27 | payer: provider.wallet.publicKey, 28 | }); 29 | if (instruction) { 30 | tx.instructions.push(instruction); 31 | } 32 | // Mint to owner's ATA 33 | tx.instructions.push( 34 | SPLToken.createMintToInstruction( 35 | TOKEN_PROGRAM_ID, 36 | mintKP.publicKey, 37 | address, 38 | tempMintAuthority, 39 | [], 40 | new u64(1), 41 | ), 42 | ); 43 | // Set mint authority of the NFT to NULL 44 | tx.instructions.push( 45 | SPLToken.createSetAuthorityInstruction( 46 | TOKEN_PROGRAM_ID, 47 | mintKP.publicKey, 48 | null, 49 | "MintTokens", 50 | tempMintAuthority, 51 | [], 52 | ), 53 | ); 54 | 55 | return tx; 56 | }; 57 | -------------------------------------------------------------------------------- /packages/token-utils/src/layout.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from "@saberhq/solana-contrib"; 2 | import type { Layout } from "@solana/buffer-layout"; 3 | import * as BufferLayout from "@solana/buffer-layout"; 4 | import type { AccountInfo, MintInfo } from "@solana/spl-token"; 5 | import { 6 | AccountLayout, 7 | MintLayout as TokenMintLayout, 8 | u64, 9 | } from "@solana/spl-token"; 10 | 11 | export { 12 | Layout as TypedLayout, 13 | Structure as TypedStructure, 14 | } from "@solana/buffer-layout"; 15 | 16 | /** 17 | * Typed struct buffer layout 18 | * @param fields 19 | * @param property 20 | * @param decodePrefixes 21 | * @returns 22 | */ 23 | export const structLayout = ( 24 | fields: Layout[], 25 | property?: string | undefined, 26 | decodePrefixes?: boolean | undefined, 27 | ): BufferLayout.Structure => 28 | BufferLayout.struct(fields, property, decodePrefixes); 29 | 30 | /** 31 | * Layout for a public key 32 | */ 33 | export const PublicKeyLayout = (property = "publicKey"): BufferLayout.Blob => { 34 | return BufferLayout.blob(32, property); 35 | }; 36 | 37 | /** 38 | * Layout for a 64bit unsigned value 39 | */ 40 | export const Uint64Layout = (property = "uint64"): BufferLayout.Blob => { 41 | return BufferLayout.blob(8, property); 42 | }; 43 | 44 | /** 45 | * Layout for a TokenAccount. 46 | */ 47 | export const TokenAccountLayout = AccountLayout as Layout<{ 48 | mint: Buffer; 49 | owner: Buffer; 50 | amount: Buffer; 51 | delegateOption: number; 52 | delegate: Buffer; 53 | state: number; 54 | delegatedAmount: Buffer; 55 | isNativeOption: number; 56 | isNative: Buffer; 57 | closeAuthorityOption: number; 58 | closeAuthority: Buffer; 59 | }>; 60 | 61 | /** 62 | * Layout for a Mint. 63 | */ 64 | export const MintLayout = TokenMintLayout as Layout<{ 65 | mintAuthorityOption: number; 66 | mintAuthority: Buffer; 67 | supply: Buffer; 68 | decimals: number; 69 | isInitialized: number; 70 | freezeAuthorityOption: number; 71 | freezeAuthority: Buffer; 72 | }>; 73 | 74 | /** 75 | * Data in an SPL token account. 76 | */ 77 | export type TokenAccountData = Omit; 78 | 79 | /** 80 | * Deserializes a token account. 81 | * @param address 82 | * @param data 83 | * @returns 84 | */ 85 | export const deserializeAccount = (data: Buffer): TokenAccountData => { 86 | const accountInfo = TokenAccountLayout.decode(data); 87 | 88 | const mint = new PublicKey(accountInfo.mint); 89 | const owner = new PublicKey(accountInfo.owner); 90 | const amount = u64.fromBuffer(accountInfo.amount); 91 | 92 | let delegate: PublicKey | null; 93 | let delegatedAmount: u64; 94 | 95 | if (accountInfo.delegateOption === 0) { 96 | delegate = null; 97 | delegatedAmount = new u64(0); 98 | } else { 99 | delegate = new PublicKey(accountInfo.delegate); 100 | delegatedAmount = u64.fromBuffer(accountInfo.delegatedAmount); 101 | } 102 | 103 | const isInitialized = accountInfo.state !== 0; 104 | const isFrozen = accountInfo.state === 2; 105 | 106 | let rentExemptReserve: u64 | null; 107 | let isNative: boolean; 108 | 109 | if (accountInfo.isNativeOption === 1) { 110 | rentExemptReserve = u64.fromBuffer(accountInfo.isNative); 111 | isNative = true; 112 | } else { 113 | rentExemptReserve = null; 114 | isNative = false; 115 | } 116 | 117 | let closeAuthority: PublicKey | null; 118 | if (accountInfo.closeAuthorityOption === 0) { 119 | closeAuthority = null; 120 | } else { 121 | closeAuthority = new PublicKey(accountInfo.closeAuthority); 122 | } 123 | 124 | return { 125 | mint, 126 | owner, 127 | amount, 128 | delegate, 129 | delegatedAmount, 130 | isInitialized, 131 | isFrozen, 132 | rentExemptReserve, 133 | isNative, 134 | closeAuthority, 135 | }; 136 | }; 137 | 138 | /** 139 | * Deserialize a {@link Buffer} into a {@link MintInfo}. 140 | * @param data 141 | * @returns 142 | */ 143 | export const deserializeMint = (data: Buffer): MintInfo => { 144 | if (data.length !== MintLayout.span) { 145 | throw new Error("Not a valid Mint"); 146 | } 147 | 148 | const mintInfo = MintLayout.decode(data); 149 | 150 | let mintAuthority: PublicKey | null; 151 | if (mintInfo.mintAuthorityOption === 0) { 152 | mintAuthority = null; 153 | } else { 154 | mintAuthority = new PublicKey(mintInfo.mintAuthority); 155 | } 156 | 157 | const supply = u64.fromBuffer(mintInfo.supply); 158 | const isInitialized = mintInfo.isInitialized !== 0; 159 | 160 | let freezeAuthority: PublicKey | null; 161 | if (mintInfo.freezeAuthorityOption === 0) { 162 | freezeAuthority = null; 163 | } else { 164 | freezeAuthority = new PublicKey(mintInfo.freezeAuthority); 165 | } 166 | 167 | return { 168 | mintAuthority, 169 | supply, 170 | decimals: mintInfo.decimals, 171 | isInitialized, 172 | freezeAuthority, 173 | }; 174 | }; 175 | -------------------------------------------------------------------------------- /packages/token-utils/src/price.ts: -------------------------------------------------------------------------------- 1 | import type { BigintIsh } from "@ubeswap/token-math"; 2 | import { Price as UPrice } from "@ubeswap/token-math"; 3 | 4 | import type { Token } from "./token.js"; 5 | 6 | /** 7 | * A price of one token relative to another. 8 | */ 9 | export class Price extends UPrice { 10 | /** 11 | * Constructs a price. 12 | * @param baseCurrency 13 | * @param quoteCurrency 14 | * @param denominator 15 | * @param numerator 16 | */ 17 | constructor( 18 | baseCurrency: Token, 19 | quoteCurrency: Token, 20 | denominator: BigintIsh, 21 | numerator: BigintIsh, 22 | ) { 23 | super(baseCurrency, quoteCurrency, denominator, numerator); 24 | } 25 | 26 | new( 27 | baseCurrency: Token, 28 | quoteCurrency: Token, 29 | denominator: BigintIsh, 30 | numerator: BigintIsh, 31 | ): this { 32 | return new Price( 33 | baseCurrency, 34 | quoteCurrency, 35 | denominator, 36 | numerator, 37 | ) as this; 38 | } 39 | 40 | static fromUPrice(price: UPrice): Price { 41 | return new Price( 42 | price.baseCurrency, 43 | price.quoteCurrency, 44 | price.denominator, 45 | price.numerator, 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/token-utils/src/splTokenRegistry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * These types all come from the @solana/spl-token-registry package. 3 | * 4 | * We re-export them here so we do not have to have a hard dependency on 5 | * that package, which is massive. 6 | */ 7 | 8 | /** 9 | * Chain ID. 10 | */ 11 | export enum ENV { 12 | MainnetBeta = 101, 13 | Testnet = 102, 14 | Devnet = 103, 15 | } 16 | 17 | /** 18 | * A token list. 19 | */ 20 | export interface SPLTokenList { 21 | readonly name: string; 22 | readonly logoURI: string; 23 | readonly tags: { [tag: string]: TagDetails }; 24 | readonly timestamp: string; 25 | readonly tokens: SPLTokenInfo[]; 26 | } 27 | 28 | /** 29 | * Tag details. 30 | */ 31 | export interface TagDetails { 32 | readonly name: string; 33 | readonly description: string; 34 | } 35 | 36 | /** 37 | * TokenExtensions. 38 | */ 39 | export interface SPLTokenExtensions { 40 | readonly website?: string; 41 | readonly bridgeContract?: string; 42 | readonly assetContract?: string; 43 | readonly address?: string; 44 | readonly explorer?: string; 45 | readonly twitter?: string; 46 | readonly github?: string; 47 | readonly medium?: string; 48 | readonly tgann?: string; 49 | readonly tggroup?: string; 50 | readonly discord?: string; 51 | readonly serumV3Usdt?: string; 52 | readonly serumV3Usdc?: string; 53 | readonly coingeckoId?: string; 54 | readonly imageUrl?: string; 55 | readonly description?: string; 56 | } 57 | 58 | /** 59 | * TokenInfo. 60 | */ 61 | export interface SPLTokenInfo { 62 | readonly chainId: number; 63 | readonly address: string; 64 | readonly name: string; 65 | readonly decimals: number; 66 | readonly symbol: string; 67 | readonly logoURI?: string; 68 | readonly tags?: string[]; 69 | readonly extensions?: SPLTokenExtensions; 70 | } 71 | -------------------------------------------------------------------------------- /packages/token-utils/src/tokenAmount.ts: -------------------------------------------------------------------------------- 1 | import { u64 } from "@solana/spl-token"; 2 | import type { BigintIsh, FractionObject } from "@ubeswap/token-math"; 3 | import { 4 | parseAmountFromString, 5 | parseBigintIsh, 6 | TokenAmount as UTokenAmount, 7 | validateU64, 8 | } from "@ubeswap/token-math"; 9 | import BN from "bn.js"; 10 | 11 | import type { Token } from "./token.js"; 12 | 13 | export type { IFormatUint } from "@ubeswap/token-math"; 14 | 15 | export interface TokenAmountObject extends FractionObject { 16 | /** 17 | * Discriminator to show this is a token amount. 18 | */ 19 | _isTA: true; 20 | /** 21 | * Mint of the token. 22 | */ 23 | mint: string; 24 | /** 25 | * Amount of tokens in string representation. 26 | */ 27 | uiAmount: string; 28 | } 29 | 30 | export class TokenAmount extends UTokenAmount { 31 | // amount _must_ be raw, i.e. in the native representation 32 | constructor(token: Token, amount: BigintIsh) { 33 | super(token, amount, validateU64); 34 | } 35 | 36 | new(token: Token, amount: BigintIsh): this { 37 | // unsafe but nobody will be extending this anyway probably 38 | return new TokenAmount(token, amount) as this; 39 | } 40 | 41 | /** 42 | * Parses a token amount from a decimal representation. 43 | * @param token 44 | * @param uiAmount 45 | * @returns 46 | */ 47 | static parse(token: Token, uiAmount: string): TokenAmount { 48 | const prev = parseAmountFromString(token, uiAmount, ".", ","); 49 | return new TokenAmount(token, prev); 50 | } 51 | 52 | /** 53 | * Divides this TokenAmount by a raw integer. 54 | * @param other 55 | * @returns 56 | */ 57 | divideByInteger(other: BigintIsh): TokenAmount { 58 | return new TokenAmount( 59 | this.token, 60 | this.toU64().div(new BN(parseBigintIsh(other).toString())), 61 | ); 62 | } 63 | 64 | /** 65 | * String representation of this token amount. 66 | */ 67 | override toString(): string { 68 | return `TokenAmount[Token=(${this.token.toString()}), amount=${this.toExact()}`; 69 | } 70 | 71 | /** 72 | * JSON representation of the token amount. 73 | */ 74 | override toJSON(): TokenAmountObject { 75 | return { 76 | ...super.toJSON(), 77 | _isTA: true, 78 | mint: this.token.address, 79 | uiAmount: this.toExact(), 80 | }; 81 | } 82 | 83 | /** 84 | * Converts this to the raw u64 used by the SPL library 85 | * @returns 86 | */ 87 | toU64(): u64 { 88 | return new u64(this.raw.toString()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /packages/token-utils/src/tokenList.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | SPLTokenExtensions, 3 | SPLTokenInfo, 4 | SPLTokenList, 5 | } from "./splTokenRegistry.js"; 6 | import { Token } from "./token.js"; 7 | 8 | /** 9 | * Known origin chains. 10 | */ 11 | export const ORIGIN_CHAINS = [ 12 | "bitcoin", 13 | "ethereum", 14 | "terra", 15 | "avalanche", 16 | "binance", 17 | "celo", 18 | "polygon", 19 | "fantom", 20 | "polygon", 21 | "heco", 22 | ] as const; 23 | 24 | /** 25 | * Known origin chains. 26 | */ 27 | export type OriginChain = (typeof ORIGIN_CHAINS)[number]; 28 | 29 | /** 30 | * Token extensions with additional information. 31 | */ 32 | export type TokenExtensions = SPLTokenExtensions & { 33 | /** 34 | * Mints of the underlying tokens that make up this token. 35 | * E.g. a Saber USDC-USDT LP token would use the USDC and USDT mints. 36 | */ 37 | readonly underlyingTokens?: string[]; 38 | /** 39 | * The protocol that this token comes from. 40 | * E.g. `wormhole-v1`, `wormhole-v2`, `allbridge`, `sollet`, `saber`. 41 | */ 42 | readonly source?: string; 43 | 44 | /* 45 | ** Link to the source's website where you can acquire this token 46 | */ 47 | readonly sourceUrl?: string; 48 | /** 49 | * The currency code of what this token represents, e.g. BTC, ETH, USD. 50 | */ 51 | readonly currency?: string; 52 | /** 53 | * If this token is a bridged token, this is the chain that the asset originates from. 54 | */ 55 | readonly originChain?: OriginChain; 56 | }; 57 | 58 | /** 59 | * Token info. 60 | */ 61 | export type TokenInfo = Omit & { 62 | readonly extensions?: TokenExtensions; 63 | }; 64 | 65 | /** 66 | * A list of tokens, based off of the Uniswap standard. 67 | */ 68 | export type TokenList = Omit & { 69 | readonly tokens: TokenInfo[]; 70 | }; 71 | 72 | /** 73 | * Creates a token map from a TokenList. 74 | * @param tokens 75 | * @returns 76 | */ 77 | export const makeTokenMap = (tokenList: TokenList): Record => { 78 | const ret: Record = {}; 79 | tokenList.tokens.forEach((item) => { 80 | ret[item.address] = new Token(item); 81 | }); 82 | return ret; 83 | }; 84 | 85 | /** 86 | * Dedupes a list of tokens, picking the first instance of the token in a list. 87 | * @param tokens 88 | * @returns 89 | */ 90 | export const dedupeTokens = (tokens: TokenInfo[]): TokenInfo[] => { 91 | const seen = new Set(); 92 | return tokens.filter((token) => { 93 | const tokenID = `${token.address}_${token.chainId}`; 94 | if (seen.has(tokenID)) { 95 | return false; 96 | } else { 97 | seen.add(tokenID); 98 | return true; 99 | } 100 | }); 101 | }; 102 | -------------------------------------------------------------------------------- /packages/token-utils/src/tokenOwner.ts: -------------------------------------------------------------------------------- 1 | import type { PublicKey } from "@saberhq/solana-contrib"; 2 | import type { TransactionInstruction } from "@solana/web3.js"; 3 | 4 | import { 5 | ASSOCIATED_TOKEN_PROGRAM_ID, 6 | getATAAddress, 7 | getATAAddressSync, 8 | SPLToken, 9 | TOKEN_PROGRAM_ID, 10 | } from "./index.js"; 11 | import type { TokenAmount } from "./tokenAmount.js"; 12 | 13 | /** 14 | * Wrapper around a token account owner to create token instructions. 15 | */ 16 | export class TokenOwner { 17 | constructor(readonly owner: PublicKey) {} 18 | 19 | /** 20 | * Gets the user's ATA. 21 | * @param mint 22 | * @returns 23 | */ 24 | async getATA(mint: PublicKey): Promise { 25 | return await getATAAddress({ mint, owner: this.owner }); 26 | } 27 | 28 | /** 29 | * Gets the user's ATA. 30 | * @param mint 31 | * @returns 32 | */ 33 | getATASync(mint: PublicKey): PublicKey { 34 | return getATAAddressSync({ mint, owner: this.owner }); 35 | } 36 | 37 | /** 38 | * Transfers tokens to a token account. 39 | * @param amount Amount of tokens to transfer. 40 | * @param to Token account to transfer to. 41 | * @returns The transaction instruction. 42 | */ 43 | async transfer( 44 | amount: TokenAmount, 45 | to: PublicKey, 46 | ): Promise { 47 | return SPLToken.createTransferInstruction( 48 | TOKEN_PROGRAM_ID, 49 | await this.getATA(amount.token.mintAccount), 50 | to, 51 | this.owner, 52 | [], 53 | amount.toU64(), 54 | ); 55 | } 56 | 57 | /** 58 | * Transfers tokens to a token account, checked.. 59 | * @param amount Amount of tokens to transfer. 60 | * @param to Token account to transfer to. 61 | * @returns The transaction instruction. 62 | */ 63 | async transferChecked( 64 | amount: TokenAmount, 65 | to: PublicKey, 66 | ): Promise { 67 | return SPLToken.createTransferCheckedInstruction( 68 | TOKEN_PROGRAM_ID, 69 | await this.getATA(amount.token.mintAccount), 70 | amount.token.mintAccount, 71 | to, 72 | this.owner, 73 | [], 74 | amount.toU64(), 75 | amount.token.decimals, 76 | ); 77 | } 78 | 79 | /** 80 | * Mints tokens to a token account. 81 | * @param amount Amount of tokens to transfer. 82 | * @param to Token account to transfer to. 83 | * @returns The transaction instruction. 84 | */ 85 | mintTo(amount: TokenAmount, to: PublicKey): TransactionInstruction { 86 | return SPLToken.createMintToInstruction( 87 | TOKEN_PROGRAM_ID, 88 | amount.token.mintAccount, 89 | to, 90 | this.owner, 91 | [], 92 | amount.toU64(), 93 | ); 94 | } 95 | 96 | /** 97 | * Creates an associated token account instruction. 98 | * @param mint Mint of the ATA. 99 | * @param payer Payer to create the ATA. Defaults to the owner. 100 | * @returns The transaction instruction. 101 | */ 102 | async createATA( 103 | mint: PublicKey, 104 | payer: PublicKey = this.owner, 105 | ): Promise { 106 | return SPLToken.createAssociatedTokenAccountInstruction( 107 | ASSOCIATED_TOKEN_PROGRAM_ID, 108 | TOKEN_PROGRAM_ID, 109 | mint, 110 | await this.getATA(mint), 111 | this.owner, 112 | payer, 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/token-utils/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.cjs.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs/" 5 | }, 6 | "include": ["src/"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/token-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.mono.json", 3 | "compilerOptions": { 4 | "rootDir": "src/", 5 | "outDir": "dist/esm/" 6 | }, 7 | "include": ["src/"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/tuple-utils/README.md: -------------------------------------------------------------------------------- 1 | # @saberhq/tuple-utils 2 | 3 | Miscellaneous utilities for dealing with tuples. 4 | 5 | ## License 6 | 7 | Saber Common is licensed under the Apache License, Version 2.0. 8 | -------------------------------------------------------------------------------- /packages/tuple-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saberhq/tuple-utils", 3 | "version": "3.0.0", 4 | "description": "Utilities for handling tuples in TypeScript.", 5 | "homepage": "https://github.com/saber-hq/saber-common/tree/master/packages/tuple-utils#readme", 6 | "repository": "git+https://github.com/saber-hq/saber-common.git", 7 | "bugs": "https://github.com/saber-hq/saber-common/issues", 8 | "funding": "https://www.coingecko.com/en/coins/saber", 9 | "author": "Saber Team ", 10 | "license": "Apache-2.0", 11 | "type": "module", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/esm/index.js", 15 | "require": "./dist/cjs/index.js" 16 | } 17 | }, 18 | "main": "dist/cjs/index.js", 19 | "module": "dist/esm/index.js", 20 | "files": [ 21 | "src/", 22 | "dist/" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "keywords": [ 28 | "typescript", 29 | "saber", 30 | "tuple" 31 | ], 32 | "scripts": { 33 | "build": "tsc && tsc --project tsconfig.cjs.json", 34 | "clean": "rm -fr dist/" 35 | }, 36 | "dependencies": { 37 | "@saberhq/option-utils": "workspace:^", 38 | "tslib": "^2.6.2" 39 | }, 40 | "devDependencies": { 41 | "@saberhq/tsconfig": "^3.3.1", 42 | "typescript": "^5.4.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/tuple-utils/src/fill.ts: -------------------------------------------------------------------------------- 1 | import type { Tuple } from "./tuple.js"; 2 | 3 | /** 4 | * Replaces all of the values of a tuple with the given value. 5 | */ 6 | export const tupleFill = ( 7 | value: V, 8 | tuple: Tuple, 9 | ): Tuple => { 10 | return tuple.map(() => value) as Tuple; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/tuple-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:tuple-utils/README.md]] 3 | * @module 4 | */ 5 | 6 | export * from "./fill.js"; 7 | export * from "./map.js"; 8 | export * from "./tuple.js"; 9 | -------------------------------------------------------------------------------- /packages/tuple-utils/src/map.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from "@saberhq/option-utils"; 2 | import { mapN } from "@saberhq/option-utils"; 3 | 4 | import type { Tuple } from "./tuple.js"; 5 | 6 | /** 7 | * Applies `mapFn` to the inner value of the tuple. 8 | */ 9 | export const tupleMapInner = ( 10 | mapFn: (v: T) => U, 11 | tuple: Tuple, N>, 12 | ): Tuple, N> => { 13 | return tuple.map((v) => mapN(mapFn, v)) as Tuple, N>; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/tuple-utils/src/tuple.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A tuple of length `N` with elements of type `T`. 3 | */ 4 | export type Tuple = N extends N 5 | ? number extends N 6 | ? T[] 7 | : _TupleOf 8 | : never; 9 | type _TupleOf = R["length"] extends N 10 | ? R 11 | : _TupleOf; 12 | -------------------------------------------------------------------------------- /packages/tuple-utils/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.cjs.json", 3 | "compilerOptions": { 4 | "outDir": "dist/cjs/" 5 | }, 6 | "include": ["src/"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/tuple-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.mono.json", 3 | "compilerOptions": { 4 | "rootDir": "src/", 5 | "outDir": "dist/esm/" 6 | }, 7 | "include": ["src/"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/use-solana/README.md: -------------------------------------------------------------------------------- 1 | # @saberhq/use-solana 2 | 3 | Solana frontend library and TypeScript SDK. This SDK features: 4 | 5 | - React hooks and providers for adding a Solana connection to your app 6 | - Integrations with several popular wallets (full list [here](https://github.com/saber-hq/use-solana/blob/master/packages/use-solana/src/providers.ts)) 7 | - Helpers for fetching token account balances and performing mathematical operations on tokens 8 | 9 | ## Documentation 10 | 11 | Detailed information on how to build on Saber can be found on the [Saber developer documentation website](https://docs.saber.so/developing/overview). 12 | 13 | Automatically generated TypeScript documentation can be found [on GitHub pages](https://saber-hq.github.io/saber-common/). 14 | 15 | ## Installation 16 | 17 | First, run: 18 | 19 | ```bash 20 | # If using NPM 21 | 22 | npm install --save @saberhq/use-solana 23 | 24 | # If using Yarn 25 | 26 | yarn add @saberhq/use-solana 27 | ``` 28 | 29 | ## License 30 | 31 | Apache 2.0 32 | -------------------------------------------------------------------------------- /packages/use-solana/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saberhq/use-solana", 3 | "version": "3.0.0", 4 | "description": "Solana utilities for React applications.", 5 | "homepage": "https://github.com/saber-hq/saber-common/tree/master/packages/use-solana#readme", 6 | "repository": "git+https://github.com/saber-hq/saber-common.git", 7 | "bugs": "https://github.com/saber-hq/saber-common/issues", 8 | "author": "Saber Team ", 9 | "license": "Apache-2.0", 10 | "type": "module", 11 | "exports": { 12 | ".": { 13 | "import": "./dist/esm/index.js", 14 | "require": "./dist/cjs/index.js" 15 | } 16 | }, 17 | "scripts": { 18 | "build": "tsc && tsc -P tsconfig.cjs.json", 19 | "clean": "rm -fr dist/", 20 | "prepublishOnly": "npm run build" 21 | }, 22 | "devDependencies": { 23 | "@saberhq/tsconfig": "^3.3.1", 24 | "@solana/web3.js": "^1.91.1", 25 | "@types/bn.js": "^5.1.5", 26 | "@types/node": "^20.12.7", 27 | "@types/react": "^18.2.77", 28 | "bn.js": "^5.2.1", 29 | "react": "^18.2.0", 30 | "typescript": "^5.4.5" 31 | }, 32 | "dependencies": { 33 | "@ledgerhq/devices": "8.2.2", 34 | "@ledgerhq/hw-transport": "6.30.5", 35 | "@ledgerhq/hw-transport-webusb": "6.28.5", 36 | "@saberhq/solana-contrib": "workspace:^", 37 | "@saberhq/wallet-adapter-icons": "workspace:^", 38 | "@solana/wallet-adapter-base": "^0.9.23", 39 | "@solana/wallet-adapter-brave": "0.1.17", 40 | "@solana/wallet-adapter-clover": "^0.4.19", 41 | "@solana/wallet-adapter-coin98": "^0.5.20", 42 | "@solana/wallet-adapter-coinbase": "^0.1.19", 43 | "@solana/wallet-adapter-exodus": "^0.1.18", 44 | "@solana/wallet-adapter-glow": "^0.1.18", 45 | "@solana/wallet-adapter-huobi": "^0.1.15", 46 | "@solana/wallet-adapter-mathwallet": "^0.9.18", 47 | "@solana/wallet-adapter-nightly": "^0.1.16", 48 | "@solana/wallet-adapter-phantom": "^0.9.24", 49 | "@solana/wallet-adapter-slope": "^0.5.21", 50 | "@solana/wallet-adapter-solflare": "^0.6.28", 51 | "@solana/wallet-adapter-sollet": "^0.11.17", 52 | "@solana/wallet-adapter-solong": "^0.9.18", 53 | "@solana/wallet-adapter-walletconnect": "^0.1.16", 54 | "eventemitter3": "^4.0.7", 55 | "fast-json-stable-stringify": "^2.1.0", 56 | "tiny-invariant": "^1.3.3", 57 | "tslib": "^2.6.2", 58 | "unstated-next": "^1.1.0" 59 | }, 60 | "peerDependencies": { 61 | "@solana/web3.js": "^1.42", 62 | "bn.js": "^4 || ^5", 63 | "react": "^17.0.2 || ^18" 64 | }, 65 | "main": "dist/cjs/index.js", 66 | "module": "dist/esm/index.js", 67 | "files": [ 68 | "dist/", 69 | "src/" 70 | ], 71 | "publishConfig": { 72 | "access": "public" 73 | }, 74 | "gitHead": "f9fd3fbd36a7a6dd6f5e9597af5309affe50ac0e" 75 | } 76 | -------------------------------------------------------------------------------- /packages/use-solana/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ledger/index.js"; 2 | export * from "./solana.js"; 3 | export * from "./types.js"; 4 | -------------------------------------------------------------------------------- /packages/use-solana/src/adapters/ledger/core.ts: -------------------------------------------------------------------------------- 1 | import type { default as Transport } from "@ledgerhq/hw-transport"; 2 | import { isVersionedTransaction } from "@saberhq/solana-contrib"; 3 | import type { Transaction, VersionedTransaction } from "@solana/web3.js"; 4 | import { PublicKey } from "@solana/web3.js"; 5 | 6 | const INS_GET_PUBKEY = 0x05; 7 | const INS_SIGN_MESSAGE = 0x06; 8 | 9 | const P1_NON_CONFIRM = 0x00; 10 | const P1_CONFIRM = 0x01; 11 | 12 | const P2_EXTEND = 0x01; 13 | const P2_MORE = 0x02; 14 | 15 | const MAX_PAYLOAD = 255; 16 | 17 | const LEDGER_CLA = 0xe0; 18 | 19 | /* 20 | * Helper for chunked send of large payloads 21 | */ 22 | async function ledgerSend( 23 | transport: Transport, 24 | instruction: number, 25 | p1: number, 26 | payload: Buffer, 27 | ) { 28 | let p2 = 0; 29 | let payloadOffset = 0; 30 | 31 | if (payload.length > MAX_PAYLOAD) { 32 | while (payload.length - payloadOffset > MAX_PAYLOAD) { 33 | const chunk = payload.slice(payloadOffset, payloadOffset + MAX_PAYLOAD); 34 | payloadOffset += MAX_PAYLOAD; 35 | const reply = await transport.send( 36 | LEDGER_CLA, 37 | instruction, 38 | p1, 39 | p2 | P2_MORE, 40 | chunk, 41 | ); 42 | if (reply.length !== 2) { 43 | throw new Error("Received unexpected reply payload"); 44 | } 45 | p2 |= P2_EXTEND; 46 | } 47 | } 48 | 49 | const chunk = payload.slice(payloadOffset); 50 | const reply = await transport.send(LEDGER_CLA, instruction, p1, p2, chunk); 51 | 52 | return reply.slice(0, reply.length - 2); 53 | } 54 | 55 | const BIP32_HARDENED_BIT = (1 << 31) >>> 0; 56 | function harden(n = 0) { 57 | return (n | BIP32_HARDENED_BIT) >>> 0; 58 | } 59 | 60 | export function getSolanaDerivationPath( 61 | account?: number, 62 | change?: number, 63 | ): Buffer { 64 | let length; 65 | if (account !== undefined) { 66 | if (change !== undefined) { 67 | length = 4; 68 | } else { 69 | length = 3; 70 | } 71 | } else { 72 | length = 2; 73 | } 74 | 75 | const derivationPath = Buffer.alloc(1 + length * 4); 76 | // eslint-disable-next-line 77 | var offset = 0; 78 | offset = derivationPath.writeUInt8(length, offset); 79 | offset = derivationPath.writeUInt32BE(harden(44), offset); // Using BIP44 80 | offset = derivationPath.writeUInt32BE(harden(501), offset); // Solana's BIP44 path 81 | 82 | if (length > 2) { 83 | offset = derivationPath.writeUInt32BE(harden(account), offset); 84 | if (length === 4) { 85 | // @FIXME: https://github.com/project-serum/spl-token-wallet/issues/59 86 | // eslint-disable-next-line unused-imports/no-unused-vars,@typescript-eslint/no-unused-vars 87 | offset = derivationPath.writeUInt32BE(harden(change), offset); 88 | } 89 | } 90 | 91 | return derivationPath; 92 | } 93 | 94 | export async function signTransaction< 95 | T extends Transaction | VersionedTransaction, 96 | >( 97 | transport: Transport, 98 | transaction: T, 99 | derivationPath: Buffer = getSolanaDerivationPath(), 100 | ): Promise { 101 | const message = isVersionedTransaction(transaction) 102 | ? transaction.message.serialize() 103 | : transaction.serializeMessage(); 104 | return signBytes(transport, message, derivationPath); 105 | } 106 | 107 | export async function signBytes( 108 | transport: Transport, 109 | bytes: Uint8Array, 110 | derivationPath: Buffer = getSolanaDerivationPath(), 111 | ): Promise { 112 | const numPaths = Buffer.alloc(1); 113 | numPaths.writeUInt8(1, 0); 114 | 115 | const payload = Buffer.concat([numPaths, derivationPath, bytes]); 116 | 117 | // @FIXME: must enable blind signing in Solana Ledger App per https://github.com/project-serum/spl-token-wallet/issues/71 118 | // See also https://github.com/project-serum/spl-token-wallet/pull/23#issuecomment-712317053 119 | return ledgerSend(transport, INS_SIGN_MESSAGE, P1_CONFIRM, payload); 120 | } 121 | 122 | export async function getPublicKey( 123 | transport: Transport, 124 | derivationPath: Buffer = getSolanaDerivationPath(), 125 | ): Promise { 126 | const publicKeyBytes = await ledgerSend( 127 | transport, 128 | INS_GET_PUBKEY, 129 | P1_NON_CONFIRM, 130 | derivationPath, 131 | ); 132 | 133 | return new PublicKey(publicKeyBytes); 134 | } 135 | -------------------------------------------------------------------------------- /packages/use-solana/src/adapters/readonly/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Broadcaster, 3 | BroadcastOptions, 4 | PendingTransaction, 5 | } from "@saberhq/solana-contrib"; 6 | import type { 7 | Connection, 8 | Transaction, 9 | VersionedTransaction, 10 | } from "@solana/web3.js"; 11 | import { PublicKey } from "@solana/web3.js"; 12 | import { EventEmitter } from "eventemitter3"; 13 | 14 | import type { WalletAdapter } from "../types.js"; 15 | 16 | declare global { 17 | interface Window { 18 | /** 19 | * Allows setting the pubkey for the ReadonlyAdapter. 20 | */ 21 | USE_SOLANA_PUBKEY_OVERRIDE?: string; 22 | } 23 | } 24 | 25 | /** 26 | * Sets the readonly Solana pubkey. 27 | * @param pubkey 28 | */ 29 | export const setReadonlySolanaPubkey = (pubkey: PublicKey): void => { 30 | window.USE_SOLANA_PUBKEY_OVERRIDE = pubkey.toString(); 31 | }; 32 | 33 | /** 34 | * Adapter that cannot sign transactions. Dummy for testing. 35 | */ 36 | export class ReadonlyAdapter extends EventEmitter implements WalletAdapter { 37 | private _publicKey: PublicKey | null = null; 38 | 39 | constructor() { 40 | super(); 41 | const localPubkey = 42 | window.USE_SOLANA_PUBKEY_OVERRIDE ?? 43 | process.env.REACT_APP_LOCAL_PUBKEY ?? 44 | process.env.LOCAL_PUBKEY; 45 | if (!localPubkey) { 46 | console.warn("LOCAL_PUBKEY not set for readonly provider"); 47 | } else { 48 | this._publicKey = new PublicKey(localPubkey); 49 | } 50 | } 51 | 52 | get connected(): boolean { 53 | return true; 54 | } 55 | 56 | get autoApprove(): boolean { 57 | return false; 58 | } 59 | 60 | get publicKey(): PublicKey | null { 61 | return this._publicKey; 62 | } 63 | 64 | signAndBroadcastTransaction( 65 | _transaction: Transaction, 66 | _connection: Connection, 67 | _broadcaster: Broadcaster, 68 | _opts?: BroadcastOptions, 69 | ): Promise { 70 | throw new Error("readonly adapter cannot sign transactions"); 71 | } 72 | 73 | signAllTransactions( 74 | _transactions: T[], 75 | ): Promise { 76 | throw new Error("readonly adapter cannot sign transactions"); 77 | } 78 | 79 | signTransaction( 80 | _transaction: T, 81 | ): Promise { 82 | throw new Error("readonly adapter cannot sign transactions"); 83 | } 84 | 85 | connect = (): Promise => { 86 | this.emit("connect", this._publicKey); 87 | return Promise.resolve(); 88 | }; 89 | 90 | disconnect(): void { 91 | this.emit("disconnect"); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/use-solana/src/adapters/secret-key/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Broadcaster, 3 | PendingTransaction, 4 | SignAndBroadcastOptions, 5 | } from "@saberhq/solana-contrib"; 6 | import { 7 | doSignAndBroadcastTransaction, 8 | SignerWallet, 9 | } from "@saberhq/solana-contrib"; 10 | import type { 11 | Connection, 12 | PublicKey, 13 | Transaction, 14 | VersionedTransaction, 15 | } from "@solana/web3.js"; 16 | import { Keypair } from "@solana/web3.js"; 17 | import EventEmitter from "eventemitter3"; 18 | 19 | import type { ConnectedWallet, WalletAdapter } from "../types.js"; 20 | 21 | /** 22 | * Adapter backed by a secret key. 23 | */ 24 | export class SecretKeyAdapter extends EventEmitter implements WalletAdapter { 25 | _wallet?: SignerWallet; 26 | _publicKey?: PublicKey; 27 | 28 | _connected: boolean; 29 | 30 | constructor() { 31 | super(); 32 | this._connected = false; 33 | } 34 | 35 | get connected(): boolean { 36 | return this._connected; 37 | } 38 | 39 | get autoApprove(): boolean { 40 | return false; 41 | } 42 | 43 | async signAndBroadcastTransaction( 44 | transaction: Transaction, 45 | _connection: Connection, 46 | broadcaster: Broadcaster, 47 | opts?: SignAndBroadcastOptions, 48 | ): Promise { 49 | return await doSignAndBroadcastTransaction( 50 | this as ConnectedWallet, 51 | transaction, 52 | broadcaster, 53 | opts, 54 | ); 55 | } 56 | 57 | signAllTransactions( 58 | transactions: T[], 59 | ): Promise { 60 | const wallet = this._wallet; 61 | if (!wallet) { 62 | return Promise.resolve(transactions); 63 | } 64 | return wallet.signAllTransactions(transactions); 65 | } 66 | 67 | get publicKey(): PublicKey | null { 68 | return this._publicKey ?? null; 69 | } 70 | 71 | async signTransaction( 72 | transaction: T, 73 | ): Promise { 74 | const wallet = this._wallet; 75 | if (!wallet) { 76 | return Promise.resolve(transaction); 77 | } 78 | return wallet.signTransaction(transaction); 79 | } 80 | 81 | connect = (args?: unknown): Promise => { 82 | const argsTyped = args as 83 | | { 84 | secretKey?: number[]; 85 | } 86 | | undefined; 87 | const secretKey = argsTyped?.secretKey; 88 | if (!secretKey || !Array.isArray(secretKey)) { 89 | throw new Error("Secret key missing."); 90 | } 91 | this._wallet = new SignerWallet( 92 | Keypair.fromSecretKey(Uint8Array.from(secretKey)), 93 | ); 94 | this._publicKey = this._wallet.publicKey; 95 | this._connected = true; 96 | this.emit("connect", this.publicKey); 97 | return Promise.resolve(); 98 | }; 99 | 100 | disconnect(): void { 101 | if (this._wallet) { 102 | this._wallet = undefined; 103 | this._publicKey = undefined; 104 | this._publicKey = undefined; 105 | this._connected = false; 106 | this.emit("disconnect"); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /packages/use-solana/src/adapters/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Broadcaster, 3 | PendingTransaction, 4 | SignAndBroadcastOptions, 5 | Wallet, 6 | } from "@saberhq/solana-contrib"; 7 | import { PublicKey } from "@saberhq/solana-contrib"; 8 | import type { WalletConnectWalletAdapterConfig } from "@solana/wallet-adapter-walletconnect"; 9 | import type { 10 | Connection, 11 | PublicKey as SolanaPublicKey, 12 | Transaction, 13 | VersionedTransaction, 14 | } from "@solana/web3.js"; 15 | 16 | export interface WalletAdapter 17 | extends Omit { 18 | publicKey: Connected extends true ? SolanaPublicKey : null; 19 | autoApprove: boolean; 20 | connected: Connected; 21 | 22 | connect: (args?: unknown) => Promise; 23 | disconnect: () => void | Promise; 24 | on(event: "connect" | "disconnect", fn: () => void): void; 25 | 26 | /** 27 | * Signs and broadcasts a transaction. 28 | * 29 | * @param transaction 30 | * @param broadcaster 31 | * @param options 32 | */ 33 | signAndBroadcastTransaction( 34 | transaction: Transaction, 35 | connection: Connection, 36 | broadcaster: Broadcaster, 37 | opts?: SignAndBroadcastOptions, 38 | ): Promise; 39 | } 40 | 41 | export type ConnectedWallet = WalletAdapter & Wallet; 42 | 43 | export type WalletOptions = WalletConnectWalletAdapterConfig; 44 | 45 | export type WalletAdapterBuilder = ( 46 | providerUrl: string, 47 | endpoint: string, 48 | options?: WalletOptions, 49 | ) => WalletAdapter; 50 | 51 | /** 52 | * Wallet adapter wrapper with caching of the PublicKey built-in. 53 | */ 54 | export class WrappedWalletAdapter 55 | implements Omit, "publicKey"> 56 | { 57 | constructor(readonly adapter: WalletAdapter) {} 58 | 59 | private _prevPubkey: SolanaPublicKey | null = null; 60 | private _publicKeyCached: PublicKey | null = null; 61 | 62 | get publicKey(): Connected extends true ? PublicKey : null { 63 | if (!this.connected) { 64 | return null as Connected extends true ? PublicKey : null; 65 | } 66 | if (this.adapter.publicKey) { 67 | if (this.adapter.publicKey === this._prevPubkey) { 68 | if (this._publicKeyCached) { 69 | return this._publicKeyCached as Connected extends true 70 | ? PublicKey 71 | : null; 72 | } 73 | } 74 | this._prevPubkey = this.adapter.publicKey; 75 | this._publicKeyCached = new PublicKey(this.adapter.publicKey.toString()); 76 | return this._publicKeyCached as Connected extends true ? PublicKey : null; 77 | } 78 | throw new Error("Invalid wallet connection state"); 79 | } 80 | 81 | get autoApprove(): boolean { 82 | return this.adapter.autoApprove; 83 | } 84 | 85 | get connected(): Connected { 86 | return ( 87 | this.adapter.connected && 88 | // need this branch b/c Solflare adapter does not respect the connected state properly 89 | (!!this.adapter.publicKey as Connected) 90 | ); 91 | } 92 | 93 | signAndBroadcastTransaction( 94 | transaction: Transaction, 95 | connection: Connection, 96 | broadcaster: Broadcaster, 97 | opts?: SignAndBroadcastOptions, 98 | ): Promise { 99 | return this.adapter.signAndBroadcastTransaction( 100 | transaction, 101 | connection, 102 | broadcaster, 103 | opts, 104 | ); 105 | } 106 | 107 | signTransaction( 108 | tx: T, 109 | ): Promise { 110 | return this.adapter.signTransaction(tx); 111 | } 112 | 113 | signAllTransactions( 114 | transaction: T[], 115 | ): Promise { 116 | return this.adapter.signAllTransactions(transaction); 117 | } 118 | 119 | connect(args?: unknown): Promise { 120 | return this.adapter.connect(args); 121 | } 122 | 123 | async disconnect(): Promise { 124 | await this.adapter.disconnect(); 125 | this._prevPubkey = null; 126 | this._publicKeyCached = null; 127 | } 128 | 129 | on(event: "connect" | "disconnect", fn: () => void): void { 130 | this.adapter.on(event, fn); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /packages/use-solana/src/error.ts: -------------------------------------------------------------------------------- 1 | import type { WalletProviderInfo, WalletTypeEnum } from "./index.js"; 2 | 3 | export enum ErrorLevel { 4 | WARN = "warn", 5 | ERROR = "error", 6 | } 7 | 8 | /** 9 | * Error thrown by the use-solana library. 10 | */ 11 | export abstract class UseSolanaError extends Error { 12 | abstract readonly level: ErrorLevel; 13 | 14 | constructor(name: string, message: string) { 15 | super(message); 16 | this.name = name; 17 | } 18 | } 19 | 20 | /** 21 | * Error derived from another error. 22 | */ 23 | export abstract class UseSolanaDerivedError extends UseSolanaError { 24 | constructor( 25 | name: string, 26 | readonly description: string, 27 | readonly originalError: unknown, 28 | ) { 29 | super( 30 | name, 31 | `${description}: ${ 32 | originalError instanceof Error ? originalError.message : "unknown" 33 | }`, 34 | ); 35 | if (originalError instanceof Error) { 36 | this.stack = originalError.stack; 37 | } 38 | } 39 | } 40 | 41 | /** 42 | * Thrown when the automatic connection to a wallet errors. 43 | */ 44 | export class WalletAutomaticConnectionError extends UseSolanaDerivedError { 45 | level = ErrorLevel.WARN; 46 | 47 | constructor( 48 | originalError: unknown, 49 | readonly info: WalletProviderInfo, 50 | ) { 51 | super( 52 | "WalletAutomaticConnectionError", 53 | `Error attempting to automatically connect to wallet ${info.name}`, 54 | originalError, 55 | ); 56 | } 57 | } 58 | 59 | /** 60 | * Thrown when a wallet disconnection errors. 61 | */ 62 | export class WalletDisconnectError extends UseSolanaDerivedError { 63 | level = ErrorLevel.WARN; 64 | 65 | constructor( 66 | originalError: unknown, 67 | readonly info?: WalletProviderInfo, 68 | ) { 69 | super( 70 | "WalletDisconnectError", 71 | `Error disconnecting wallet ${info?.name ?? "(unknown)"}`, 72 | originalError, 73 | ); 74 | } 75 | } 76 | 77 | /** 78 | * Thrown when a wallet activation errors. 79 | */ 80 | export class WalletActivateError< 81 | WalletType extends WalletTypeEnum, 82 | > extends UseSolanaDerivedError { 83 | level = ErrorLevel.ERROR; 84 | 85 | constructor( 86 | originalError: unknown, 87 | readonly walletType: WalletType[keyof WalletType], 88 | readonly walletArgs?: Record, 89 | ) { 90 | super( 91 | "WalletActivateError", 92 | `Error activating wallet ${walletType as unknown as string}`, 93 | originalError, 94 | ); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /packages/use-solana/src/hooks.ts: -------------------------------------------------------------------------------- 1 | import type { Connection } from "@solana/web3.js"; 2 | 3 | import type { ConnectedWallet } from "./adapters/types.js"; 4 | import type { UseSolana } from "./context.js"; 5 | import { useSolana } from "./context.js"; 6 | import type { 7 | DefaultWalletType, 8 | UnknownWalletType, 9 | WalletTypeEnum, 10 | } from "./index.js"; 11 | import type { ConnectionContext } from "./utils/useConnectionInternal.js"; 12 | 13 | /** 14 | * Gets the current Solana wallet. 15 | */ 16 | export function useWallet< 17 | WalletType extends WalletTypeEnum = typeof DefaultWalletType, 18 | >(): UseSolana { 19 | const context = useSolana(); 20 | if (!context) { 21 | throw new Error("wallet not loaded"); 22 | } 23 | return context; 24 | } 25 | 26 | /** 27 | * Gets the current Solana wallet, returning null if it is not connected. 28 | */ 29 | export const useConnectedWallet = (): ConnectedWallet | null => { 30 | const { wallet, connected, walletActivating } = 31 | useWallet(); 32 | if ( 33 | !wallet?.connected || 34 | !connected || 35 | !wallet.publicKey || 36 | walletActivating 37 | ) { 38 | return null; 39 | } 40 | return wallet as ConnectedWallet; 41 | }; 42 | 43 | /** 44 | * Loads the connection context 45 | * @returns 46 | */ 47 | export function useConnectionContext(): ConnectionContext { 48 | const context = useSolana(); 49 | if (!context) { 50 | throw new Error("Not in context"); 51 | } 52 | return context; 53 | } 54 | 55 | /** 56 | * Gets the read connection 57 | * @returns 58 | */ 59 | export function useConnection(): Connection { 60 | return useConnectionContext().connection; 61 | } 62 | 63 | /** 64 | * Gets the send connection 65 | * @returns 66 | */ 67 | export function useSendConnection(): Connection { 68 | return useConnectionContext().sendConnection; 69 | } 70 | -------------------------------------------------------------------------------- /packages/use-solana/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * [[include:use-solana/README.md]] 3 | * @module 4 | */ 5 | 6 | export * from "./adapters/index.js"; 7 | export * from "./context.js"; 8 | export * from "./error.js"; 9 | export * from "./hooks.js"; 10 | export * from "./providers.js"; 11 | export * from "./storage.js"; 12 | export * from "./utils/provider.js"; 13 | export * as icons from "@saberhq/wallet-adapter-icons"; 14 | 15 | // re-export solana utils 16 | export * as solana from "@saberhq/solana-contrib"; 17 | -------------------------------------------------------------------------------- /packages/use-solana/src/storage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows storing and persisting user settings. 3 | */ 4 | export interface StorageAdapter { 5 | get(key: string): Promise; 6 | set(key: string, value: string): Promise; 7 | remove(key: string): Promise; 8 | } 9 | 10 | /** 11 | * Adapter to use `localStorage` for storage. 12 | */ 13 | export const LOCAL_STORAGE_ADAPTER: StorageAdapter = { 14 | get(key) { 15 | return Promise.resolve(localStorage.getItem(key)); 16 | }, 17 | set(key, value) { 18 | localStorage.setItem(key, value); 19 | return Promise.resolve(); 20 | }, 21 | remove(key) { 22 | localStorage.removeItem(key); 23 | return Promise.resolve(); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/use-solana/src/utils/provider.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Broadcaster, 3 | PendingTransaction, 4 | SignAndBroadcastOptions, 5 | } from "@saberhq/solana-contrib"; 6 | import { 7 | DEFAULT_PROVIDER_OPTIONS, 8 | SingleConnectionBroadcaster, 9 | SolanaProvider, 10 | SolanaTransactionSigner, 11 | TieredBroadcaster, 12 | } from "@saberhq/solana-contrib"; 13 | import type { 14 | Commitment, 15 | ConfirmOptions, 16 | Connection, 17 | Transaction, 18 | } from "@solana/web3.js"; 19 | import invariant from "tiny-invariant"; 20 | 21 | import type { ConnectedWallet } from "../adapters/index.js"; 22 | 23 | export class WalletAdapterTransactionSigner extends SolanaTransactionSigner { 24 | constructor( 25 | readonly connection: Connection, 26 | override readonly wallet: ConnectedWallet, 27 | broadcaster: Broadcaster, 28 | preflightCommitment: Commitment = "confirmed", 29 | ) { 30 | super(wallet, broadcaster, preflightCommitment); 31 | } 32 | 33 | override async signAndBroadcastTransaction( 34 | transaction: Transaction, 35 | opts?: SignAndBroadcastOptions, 36 | ): Promise { 37 | return await this.wallet.signAndBroadcastTransaction( 38 | transaction, 39 | this.connection, 40 | this.broadcaster, 41 | opts, 42 | ); 43 | } 44 | } 45 | 46 | export class WalletAdapterProvider extends SolanaProvider { 47 | /** 48 | * @param connection The cluster connection where the program is deployed. 49 | * @param sendConnection The connection where transactions are sent to. 50 | * @param wallet The wallet used to pay for and sign all transactions. 51 | * @param opts Transaction confirmation options to use by default. 52 | */ 53 | constructor( 54 | connection: Connection, 55 | broadcaster: Broadcaster, 56 | override readonly wallet: ConnectedWallet, 57 | opts: ConfirmOptions = DEFAULT_PROVIDER_OPTIONS, 58 | ) { 59 | super( 60 | connection, 61 | broadcaster, 62 | wallet, 63 | opts, 64 | new WalletAdapterTransactionSigner( 65 | connection, 66 | wallet, 67 | broadcaster, 68 | opts.preflightCommitment, 69 | ), 70 | ); 71 | } 72 | 73 | /** 74 | * Initializes a new SolanaProvider. 75 | */ 76 | static override init({ 77 | connection, 78 | broadcastConnections = [connection], 79 | wallet, 80 | opts = DEFAULT_PROVIDER_OPTIONS, 81 | }: { 82 | /** 83 | * Connection used for general reads 84 | */ 85 | readonly connection: Connection; 86 | /** 87 | * Connections used for broadcasting transactions. Defaults to the read connection. 88 | */ 89 | readonly broadcastConnections?: readonly Connection[]; 90 | /** 91 | * Wallet used for signing transactions 92 | */ 93 | readonly wallet: ConnectedWallet; 94 | /** 95 | * Confirmation options 96 | */ 97 | readonly opts?: ConfirmOptions; 98 | }): WalletAdapterProvider { 99 | const firstBroadcastConnection = broadcastConnections[0]; 100 | invariant( 101 | firstBroadcastConnection, 102 | "must have at least one broadcast connection", 103 | ); 104 | return new WalletAdapterProvider( 105 | connection, 106 | broadcastConnections.length > 1 107 | ? new TieredBroadcaster(connection, broadcastConnections, opts) 108 | : new SingleConnectionBroadcaster(firstBroadcastConnection, opts), 109 | wallet, 110 | opts, 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /packages/use-solana/src/utils/useConnectionInternal.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Network, 3 | NetworkConfig, 4 | NetworkConfigMap, 5 | } from "@saberhq/solana-contrib"; 6 | import { DEFAULT_NETWORK_CONFIG_MAP } from "@saberhq/solana-contrib"; 7 | import type { Commitment } from "@solana/web3.js"; 8 | import { Connection } from "@solana/web3.js"; 9 | import { useMemo } from "react"; 10 | 11 | import type { StorageAdapter } from "../storage.js"; 12 | import { usePersistedKVStore } from "./usePersistedKVStore.js"; 13 | 14 | export type PartialNetworkConfigMap = { 15 | [N in Network]?: Partial; 16 | }; 17 | 18 | export interface ConnectionContext { 19 | connection: Connection; 20 | sendConnection: Connection; 21 | network: Network; 22 | setNetwork: (val: Network) => void | Promise; 23 | endpoint: string; 24 | setEndpoints: ( 25 | endpoints: Omit, 26 | ) => void | Promise; 27 | } 28 | 29 | const makeNetworkConfigMap = ( 30 | partial: PartialNetworkConfigMap, 31 | ): NetworkConfigMap => 32 | Object.entries(DEFAULT_NETWORK_CONFIG_MAP).reduce( 33 | (acc, [k, v]) => ({ 34 | ...acc, 35 | [k as Network]: { 36 | ...v, 37 | ...partial[k as Network], 38 | }, 39 | }), 40 | DEFAULT_NETWORK_CONFIG_MAP, 41 | ); 42 | 43 | export interface ConnectionArgs { 44 | defaultNetwork?: Network; 45 | networkConfigs?: PartialNetworkConfigMap; 46 | commitment?: Commitment; 47 | storageAdapter: StorageAdapter; 48 | } 49 | 50 | /** 51 | * Handles the connection to the Solana nodes. 52 | * @returns 53 | */ 54 | export const useConnectionInternal = ({ 55 | // default to mainnet-beta 56 | defaultNetwork = "mainnet-beta", 57 | networkConfigs = DEFAULT_NETWORK_CONFIG_MAP, 58 | commitment = "confirmed", 59 | storageAdapter, 60 | }: ConnectionArgs): ConnectionContext => { 61 | const [network, setNetwork] = usePersistedKVStore( 62 | "use-solana/network", 63 | defaultNetwork, 64 | storageAdapter, 65 | ); 66 | const configMap = makeNetworkConfigMap(networkConfigs); 67 | const config = configMap[network]; 68 | const [{ endpoint, endpointWs, ...connectionConfigArgs }, setEndpoints] = 69 | usePersistedKVStore>( 70 | `use-solana/rpc-endpoint/${network}`, 71 | config, 72 | storageAdapter, 73 | ); 74 | 75 | const connection = useMemo( 76 | () => 77 | new Connection(endpoint, { 78 | ...connectionConfigArgs, 79 | commitment: connectionConfigArgs.commitment ?? commitment, 80 | wsEndpoint: endpointWs, 81 | }), 82 | [commitment, connectionConfigArgs, endpoint, endpointWs], 83 | ); 84 | const sendConnection = useMemo( 85 | () => 86 | new Connection(endpoint, { 87 | ...connectionConfigArgs, 88 | commitment: connectionConfigArgs.commitment ?? commitment, 89 | wsEndpoint: endpointWs, 90 | }), 91 | [commitment, connectionConfigArgs, endpoint, endpointWs], 92 | ); 93 | 94 | return { 95 | connection, 96 | sendConnection, 97 | network, 98 | setNetwork, 99 | endpoint, 100 | setEndpoints, 101 | }; 102 | }; 103 | -------------------------------------------------------------------------------- /packages/use-solana/src/utils/usePersistedKVStore.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | import type { StorageAdapter } from "../storage.js"; 4 | 5 | export function usePersistedKVStore( 6 | key: string, 7 | defaultState: T, 8 | storageAdapter: StorageAdapter, 9 | ): [T, (newState: T | null) => Promise] { 10 | const [state, setState] = useState(null); 11 | 12 | useEffect(() => { 13 | void (async () => { 14 | const storedState = await storageAdapter.get(key); 15 | if (storedState) { 16 | console.debug(`Restoring user settings for ${key}`); 17 | setState(JSON.parse(storedState) as T); 18 | } 19 | })(); 20 | }, [key, storageAdapter]); 21 | 22 | const setLocalStorageState = useCallback( 23 | async (newState: T | null) => { 24 | const changed = state !== newState; 25 | if (!changed) { 26 | return; 27 | } 28 | if (newState === null) { 29 | await storageAdapter.remove(key); 30 | setState(defaultState); 31 | } else { 32 | await storageAdapter.set(key, JSON.stringify(newState)); 33 | setState(newState); 34 | } 35 | }, 36 | [state, defaultState, storageAdapter, key], 37 | ); 38 | 39 | return [state ?? defaultState, setLocalStorageState]; 40 | } 41 | -------------------------------------------------------------------------------- /packages/use-solana/src/utils/useProviderInternal.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AugmentedProvider, 3 | ReadonlyProvider, 4 | } from "@saberhq/solana-contrib"; 5 | import { 6 | DEFAULT_PROVIDER_OPTIONS, 7 | SolanaAugmentedProvider, 8 | SolanaReadonlyProvider, 9 | } from "@saberhq/solana-contrib"; 10 | import type { Commitment, ConfirmOptions, Connection } from "@solana/web3.js"; 11 | import { useMemo } from "react"; 12 | 13 | import type { ConnectedWallet, WalletAdapter } from "../adapters/types.js"; 14 | import { WalletAdapterProvider } from "./provider.js"; 15 | 16 | /** 17 | * Wallet-related information. 18 | */ 19 | export interface UseProvider { 20 | /** 21 | * Read-only provider. 22 | */ 23 | provider: ReadonlyProvider; 24 | /** 25 | * {@link Provider} of the currently connected wallet. 26 | */ 27 | providerMut: AugmentedProvider | null; 28 | } 29 | 30 | export interface UseProviderArgs { 31 | /** 32 | * Connection. 33 | */ 34 | connection: Connection; 35 | /** 36 | * Send connection. 37 | */ 38 | sendConnection?: Connection; 39 | /** 40 | * Broadcast connections. 41 | */ 42 | broadcastConnections?: Connection[]; 43 | /** 44 | * Wallet. 45 | */ 46 | wallet?: WalletAdapter; 47 | /** 48 | * Commitment for the read-only provider. 49 | */ 50 | commitment?: Commitment; 51 | /** 52 | * Confirm options for the mutable provider. 53 | */ 54 | confirmOptions?: ConfirmOptions; 55 | } 56 | 57 | export const useProviderInternal = ({ 58 | connection, 59 | sendConnection = connection, 60 | broadcastConnections = [sendConnection], 61 | wallet, 62 | commitment = "confirmed", 63 | confirmOptions = DEFAULT_PROVIDER_OPTIONS, 64 | }: UseProviderArgs): UseProvider => { 65 | const provider = useMemo( 66 | () => 67 | new SolanaReadonlyProvider(connection, { 68 | commitment, 69 | }), 70 | [commitment, connection], 71 | ); 72 | 73 | const connected = wallet?.connected; 74 | const publicKey = wallet?.publicKey; 75 | const providerMut = useMemo( 76 | () => 77 | wallet && connected && publicKey 78 | ? new SolanaAugmentedProvider( 79 | WalletAdapterProvider.init({ 80 | connection, 81 | broadcastConnections, 82 | wallet: wallet as ConnectedWallet, 83 | opts: confirmOptions, 84 | }), 85 | ) 86 | : null, 87 | [ 88 | wallet, 89 | connected, 90 | publicKey, 91 | connection, 92 | broadcastConnections, 93 | confirmOptions, 94 | ], 95 | ); 96 | 97 | return { 98 | provider, 99 | providerMut, 100 | }; 101 | }; 102 | -------------------------------------------------------------------------------- /packages/use-solana/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "dist/cjs/" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/use-solana/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.react.json", 3 | "compilerOptions": { 4 | "rootDir": "src/", 5 | "composite": true, 6 | "jsxImportSource": "react", 7 | "lib": ["ES2022", "DOM"], 8 | "outDir": "dist/esm/", 9 | "types": [] 10 | }, 11 | "include": ["src/"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/wallet-adapter-icons/README.md: -------------------------------------------------------------------------------- 1 | # @saberhq/wallet-adapter-icons 2 | 3 | Icons of wallet adapters. 4 | 5 | ## License 6 | 7 | Saber Common is licensed under the Apache License, Version 2.0. 8 | -------------------------------------------------------------------------------- /packages/wallet-adapter-icons/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@saberhq/wallet-adapter-icons", 3 | "version": "3.0.0", 4 | "description": "Solana wallet adapter icons.", 5 | "homepage": "https://github.com/saber-hq/saber-common/tree/master/packages/solana-wallet-adapters#readme", 6 | "repository": "git+https://github.com/saber-hq/saber-common.git", 7 | "bugs": "https://github.com/saber-hq/saber-common/issues", 8 | "funding": "https://www.coingecko.com/en/coins/saber", 9 | "author": "Saber Team ", 10 | "license": "Apache-2.0", 11 | "sideEffects": false, 12 | "type": "module", 13 | "exports": { 14 | ".": { 15 | "import": "./dist/esm/index.js", 16 | "require": "./dist/cjs/index.js" 17 | } 18 | }, 19 | "main": "dist/cjs/index.js", 20 | "module": "dist/esm/index.js", 21 | "files": [ 22 | "src/", 23 | "dist/" 24 | ], 25 | "publishConfig": { 26 | "access": "public" 27 | }, 28 | "keywords": [ 29 | "typescript", 30 | "saber", 31 | "solana", 32 | "wallet" 33 | ], 34 | "scripts": { 35 | "build": "tsc && tsc -P tsconfig.cjs.json", 36 | "clean": "rm -fr dist/" 37 | }, 38 | "dependencies": { 39 | "tslib": "^2.6.2" 40 | }, 41 | "peerDependencies": { 42 | "react": "^17.0.2 || ^18" 43 | }, 44 | "devDependencies": { 45 | "@saberhq/tsconfig": "^3.3.1", 46 | "@types/react": "^18.2.77", 47 | "react": "^18.2.0", 48 | "typescript": "^5.4.5" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/wallet-adapter-icons/src/coin98.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export const COIN98: React.FC> = (props) => ( 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /packages/wallet-adapter-icons/src/mathwallet.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const MATHWALLET: React.FC> = (props) => ( 4 | 11 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /packages/wallet-adapter-icons/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "moduleResolution": "node", 6 | "outDir": "dist/cjs/" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/wallet-adapter-icons/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.module.react.json", 3 | "compilerOptions": { 4 | "rootDir": "src/", 5 | "module": "Node16", 6 | "composite": true, 7 | "jsxImportSource": "react", 8 | "outDir": "dist/esm/", 9 | "skipLibCheck": false, 10 | "types": [] 11 | }, 12 | "include": ["src/"] 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@saberhq/tsconfig/tsconfig.react.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "noEmit": true, 6 | "target": "ES2022", 7 | "jsxImportSource": "react", 8 | "types": ["jest"] 9 | }, 10 | "files": ["jest.config.mjs", ".eslintrc.cjs"], 11 | "include": ["./**/*.test.ts"], 12 | "references": [ 13 | { "path": "packages/option-utils/" }, 14 | { "path": "packages/tuple-utils/" }, 15 | { "path": "packages/solana-contrib/" }, 16 | { "path": "packages/anchor-contrib/" }, 17 | { "path": "packages/chai-solana/" }, 18 | { "path": "packages/option-utils/" }, 19 | { "path": "packages/stableswap-sdk/" }, 20 | { "path": "packages/token-utils/" }, 21 | { "path": "packages/tuple-utils/" }, 22 | { "path": "packages/use-solana/" }, 23 | { "path": "packages/wallet-adapter-icons/" } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "dependsOn": ["^build"] 6 | }, 7 | "clean": { 8 | "cache": false 9 | } 10 | } 11 | } 12 | --------------------------------------------------------------------------------