Repository: anza-xyz/solana-web3.js Branch: main Commit: 5e1644db15cf Files: 2013 Total size: 10.1 MB Directory structure: gitextract_q5tmvg6z/ ├── .agents/ │ └── skills/ │ ├── changesets/ │ │ ├── INJECT.md │ │ └── SKILL.md │ ├── shipping-git/ │ │ ├── INJECT.md │ │ └── SKILL.md │ ├── shipping-graphite/ │ │ ├── INJECT.md │ │ └── SKILL.md │ ├── ts-docblocks/ │ │ ├── INJECT.md │ │ └── SKILL.md │ └── ts-readme/ │ ├── INJECT.md │ └── SKILL.md ├── .bundlemonrc.json ├── .changeset/ │ ├── bold-drinks-strive.md │ ├── brown-candles-relax.md │ ├── clever-spies-shout.md │ ├── config.json │ ├── some-views-pick.md │ └── thin-cats-drop.md ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── 0_bug.md │ │ ├── 1_feature.md │ │ └── config.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ ├── label-actions.yml │ └── workflows/ │ ├── actions/ │ │ ├── install-dependencies/ │ │ │ └── action.yml │ │ └── setup-validator/ │ │ └── action.yml │ ├── autolock-inactive-threads.yml │ ├── backport.yml │ ├── bundlesize.yml │ ├── codeql.yml │ ├── deploy-docs.yml │ ├── dismiss-stale-pr-reviews.yml │ ├── label-actions.yml │ ├── manage-stale-threads.yml │ ├── preview-docs.yml │ ├── publish-packages.yml │ ├── pull-requests.yml │ └── update-docs-lockfile.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .skills-inject.json ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── docs/ │ ├── .eslintrc.json │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── README.md │ ├── build-api-docs.sh │ ├── build-api-index.js │ ├── cli.json │ ├── content/ │ │ ├── .prettierrc │ │ ├── docs/ │ │ │ ├── advanced-guides/ │ │ │ │ ├── codecs.mdx │ │ │ │ ├── errors.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── instruction-plans.mdx │ │ │ │ ├── keypairs.mdx │ │ │ │ ├── kit-without-a-client.mdx │ │ │ │ ├── meta.json │ │ │ │ ├── offchain-messages.mdx │ │ │ │ ├── signers.mdx │ │ │ │ └── transactions.mdx │ │ │ ├── getting-started.mdx │ │ │ ├── guides/ │ │ │ │ ├── fetching-accounts.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── meta.json │ │ │ │ ├── rpc-subscriptions.mdx │ │ │ │ ├── rpc.mdx │ │ │ │ ├── sending-multiple-transactions.mdx │ │ │ │ ├── sending-transactions.mdx │ │ │ │ ├── setting-up-signers.mdx │ │ │ │ ├── testing-and-local-development.mdx │ │ │ │ └── using-program-plugins.mdx │ │ │ ├── index.mdx │ │ │ ├── meta.json │ │ │ ├── plugins/ │ │ │ │ ├── available-plugins.mdx │ │ │ │ ├── creating-custom-plugins.mdx │ │ │ │ ├── generating-program-plugins.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── tree-shaking.tsx │ │ │ └── upgrade-guide.mdx │ │ ├── home/ │ │ │ ├── example-codecs.mdx │ │ │ ├── example-rpc-subscriptions.mdx │ │ │ ├── example-rpc.mdx │ │ │ ├── example-signers.mdx │ │ │ ├── example-solana-programs.mdx │ │ │ └── example-transactions.mdx │ │ └── recipes/ │ │ ├── airdropping-tokens.mdx │ │ ├── creating-a-token.mdx │ │ ├── index.mdx │ │ ├── meta.json │ │ └── transferring-sol.mdx │ ├── next.config.mjs │ ├── package.json │ ├── postcss.config.mjs │ ├── source.config.ts │ ├── src/ │ │ ├── app/ │ │ │ ├── (home)/ │ │ │ │ ├── layout.tsx │ │ │ │ ├── page/ │ │ │ │ │ ├── code.tsx │ │ │ │ │ ├── cta.tsx │ │ │ │ │ ├── example-context.tsx │ │ │ │ │ ├── features.tsx │ │ │ │ │ ├── footer.tsx │ │ │ │ │ └── hero.tsx │ │ │ │ └── page.tsx │ │ │ ├── api/ │ │ │ │ ├── [[...slug]]/ │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── docs/ │ │ │ │ ├── [[...slug]]/ │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── navbar.tsx │ │ │ │ └── sidebar.tsx │ │ │ ├── global.css │ │ │ ├── layout.config.tsx │ │ │ ├── layout.tsx │ │ │ ├── llms-full.txt/ │ │ │ │ └── route.ts │ │ │ ├── llms.mdx/ │ │ │ │ ├── api/ │ │ │ │ │ └── [[...slug]]/ │ │ │ │ │ └── route.ts │ │ │ │ ├── docs/ │ │ │ │ │ └── [[...slug]]/ │ │ │ │ │ └── route.ts │ │ │ │ └── recipes/ │ │ │ │ └── [[...slug]]/ │ │ │ │ └── route.ts │ │ │ ├── logo.tsx │ │ │ ├── recipes/ │ │ │ │ ├── [[...slug]]/ │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ └── styles/ │ │ │ ├── brand.css │ │ │ ├── fumadocs-overrides.css │ │ │ └── typography.css │ │ ├── components/ │ │ │ ├── card-tabs.tsx │ │ │ ├── page-actions.tsx │ │ │ └── ui/ │ │ │ ├── button.tsx │ │ │ └── popover.tsx │ │ └── lib/ │ │ ├── InKeepSearchDialog.tsx │ │ ├── Overrides.tsx │ │ ├── Spread.tsx │ │ ├── ThemedImage.tsx │ │ ├── cn.ts │ │ ├── get-llm-text.ts │ │ └── source.ts │ └── tsconfig.json ├── eslint.config.mjs ├── examples/ │ ├── README.md │ ├── deserialize-transaction/ │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ └── example.ts │ │ └── tsconfig.json │ ├── react-app/ │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── eslint.config.mjs │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── components/ │ │ │ │ ├── AirdropButton.tsx │ │ │ │ ├── Balance.tsx │ │ │ │ ├── BaseSignMessageFeaturePanel.tsx │ │ │ │ ├── ConnectWalletMenu.tsx │ │ │ │ ├── ConnectWalletMenuItem.tsx │ │ │ │ ├── DisconnectButton.tsx │ │ │ │ ├── ErrorDialog.tsx │ │ │ │ ├── FeatureNotSupportedCallout.tsx │ │ │ │ ├── FeaturePanel.tsx │ │ │ │ ├── Nav.tsx │ │ │ │ ├── SignInMenu.tsx │ │ │ │ ├── SignInMenuItem.tsx │ │ │ │ ├── SlotIndicator.tsx │ │ │ │ ├── SolanaPartialSignTransactionFeaturePanel.tsx │ │ │ │ ├── SolanaSignAndSendTransactionFeaturePanel.tsx │ │ │ │ ├── SolanaSignMessageFeaturePanel.tsx │ │ │ │ ├── SolanaSignTransactionFeaturePanel.tsx │ │ │ │ ├── UnconnectableWalletMenuItem.tsx │ │ │ │ ├── WalletAccountIcon.tsx │ │ │ │ └── WalletMenuItemContent.tsx │ │ │ ├── context/ │ │ │ │ ├── ChainContext.tsx │ │ │ │ ├── ChainContextProvider.tsx │ │ │ │ ├── RpcContext.tsx │ │ │ │ └── RpcContextProvider.tsx │ │ │ ├── errors.tsx │ │ │ ├── functions/ │ │ │ │ └── balance.ts │ │ │ ├── hooks/ │ │ │ │ └── useStable.ts │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── reset.css │ │ │ ├── routes/ │ │ │ │ └── root.tsx │ │ │ ├── signerBytes.json │ │ │ ├── storage.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── rpc-custom-api/ │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ └── example.ts │ │ └── tsconfig.json │ ├── rpc-transport-throttled/ │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ └── example.ts │ │ └── tsconfig.json │ ├── signers/ │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ ├── example-keypair.json │ │ │ └── example.ts │ │ └── tsconfig.json │ ├── token-airdrop/ │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ └── example.ts │ │ └── tsconfig.json │ ├── transfer-lamports/ │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ └── example.ts │ │ └── tsconfig.json │ └── utils/ │ ├── .gitignore │ ├── .prettierignore │ ├── LICENSE │ ├── createLogger.ts │ ├── package.json │ ├── pressAnyKeyPrompt.ts │ ├── tsconfig.declarations.json │ └── tsconfig.json ├── package.json ├── packages/ │ ├── accounts/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── decode-account-test.ts │ │ │ │ ├── fetch-account-test.ts │ │ │ │ ├── maybe-account-test.ts │ │ │ │ └── parse-account-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── decode-account-typetest.ts │ │ │ │ ├── fetch-account-typetest.ts │ │ │ │ ├── maybe-account-typetest.ts │ │ │ │ └── parse-account-typetest.ts │ │ │ ├── account.ts │ │ │ ├── decode-account.ts │ │ │ ├── fetch-account.ts │ │ │ ├── index.ts │ │ │ ├── maybe-account.ts │ │ │ ├── parse-account.ts │ │ │ └── rpc-api/ │ │ │ ├── common.ts │ │ │ ├── getAccountInfo.ts │ │ │ ├── getMultipleAccounts.ts │ │ │ └── index.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── addresses/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── address-test.ts │ │ │ │ ├── coercions-test.ts │ │ │ │ ├── curve-test.ts │ │ │ │ ├── program-derived-address-test.ts │ │ │ │ └── public-key-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── coercions-typetest.ts │ │ │ ├── address.ts │ │ │ ├── curve-internal.ts │ │ │ ├── curve.ts │ │ │ ├── index.ts │ │ │ ├── program-derived-address.ts │ │ │ ├── public-key.ts │ │ │ └── vendor/ │ │ │ └── noble/ │ │ │ └── ed25519.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── assertions/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── .vscode/ │ │ │ └── settings.json │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── crypto-test.ts │ │ │ │ └── subtle-crypto-test.ts │ │ │ ├── crypto.ts │ │ │ ├── index.ts │ │ │ └── subtle-crypto.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── build-scripts/ │ │ ├── .npmrc │ │ ├── README.md │ │ ├── build-time-constants.d.ts │ │ ├── constants.ts │ │ ├── create-github-release.ts │ │ ├── current-linked-version.ts │ │ ├── dev-flag.ts │ │ ├── getBaseConfig.ts │ │ ├── github-api.ts │ │ ├── maybe-tag-latest.ts │ │ ├── package.json │ │ ├── prior-release.ts │ │ ├── register-node-globals.cjs │ │ ├── tag-release-manually.ts │ │ ├── tsconfig.json │ │ ├── tsup.config.browser.ts │ │ ├── tsup.config.library.ts │ │ ├── tsup.config.package.bundled_8gpidqojr8.mjs │ │ └── tsup.config.package.ts │ ├── codecs/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ └── index.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── codecs-core/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __benchmarks__/ │ │ │ │ └── run.ts │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── add-codec-sentinel-test.ts │ │ │ │ ├── add-codec-size-prefix-test.ts │ │ │ │ ├── array-buffers-test.ts │ │ │ │ ├── bytes-test.ts │ │ │ │ ├── codec-test.ts │ │ │ │ ├── combine-codec.ts │ │ │ │ ├── decoder-entire-byte-array-test.ts │ │ │ │ ├── fix-codec-size-test.ts │ │ │ │ ├── offset-codec-test.ts │ │ │ │ ├── pad-codec-test.ts │ │ │ │ ├── resize-codec-test.ts │ │ │ │ ├── reverse-codec-test.ts │ │ │ │ └── transform-codec-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── add-codec-sentinel-typetest.ts │ │ │ │ ├── add-codec-size-prefix-typetest.ts │ │ │ │ ├── array-buffers-typetest.ts │ │ │ │ ├── bytes-typetest.ts │ │ │ │ ├── codec-typetest.ts │ │ │ │ ├── combine-codec-typetest.ts │ │ │ │ ├── fix-codec-size-typetest.ts │ │ │ │ ├── offset-codec-typetest.ts │ │ │ │ ├── pad-codec-typetest.ts │ │ │ │ ├── resize-codec-typetest.ts │ │ │ │ ├── reverse-codec-typetest.ts │ │ │ │ └── transform-codec-typetest.ts │ │ │ ├── add-codec-sentinel.ts │ │ │ ├── add-codec-size-prefix.ts │ │ │ ├── array-buffers.ts │ │ │ ├── assertions.ts │ │ │ ├── bytes.ts │ │ │ ├── codec.ts │ │ │ ├── combine-codec.ts │ │ │ ├── decoder-entire-byte-array.ts │ │ │ ├── fix-codec-size.ts │ │ │ ├── index.ts │ │ │ ├── offset-codec.ts │ │ │ ├── pad-codec.ts │ │ │ ├── readonly-uint8array.ts │ │ │ ├── resize-codec.ts │ │ │ ├── reverse-codec.ts │ │ │ └── transform-codec.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── codecs-data-structures/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── array-test.ts │ │ │ │ ├── bit-array-test.ts │ │ │ │ ├── boolean-test.ts │ │ │ │ ├── bytes-test.ts │ │ │ │ ├── constant-test.ts │ │ │ │ ├── discriminated-union-test.ts │ │ │ │ ├── enum-helpers-test.ts │ │ │ │ ├── enum-test.ts │ │ │ │ ├── hidden-prefix-test.ts │ │ │ │ ├── hidden-suffix-test.ts │ │ │ │ ├── literal-union-test.ts │ │ │ │ ├── map-test.ts │ │ │ │ ├── nullable-test.ts │ │ │ │ ├── pattern-match-test.ts │ │ │ │ ├── predicate-test.ts │ │ │ │ ├── set-test.ts │ │ │ │ ├── struct-test.ts │ │ │ │ ├── tuple-test.ts │ │ │ │ ├── union-test.ts │ │ │ │ └── unit-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── array-typetest.ts │ │ │ │ ├── bit-array-typetest.ts │ │ │ │ ├── boolean-typetest.ts │ │ │ │ ├── bytes-typetest.ts │ │ │ │ ├── constant-typetest.ts │ │ │ │ ├── discriminated-union-typetest.ts │ │ │ │ ├── enum-typetest.ts │ │ │ │ ├── hidden-prefix-typetest.ts │ │ │ │ ├── hidden-suffix-typetest.ts │ │ │ │ ├── literal-union-typetest.ts │ │ │ │ ├── map-typetest.ts │ │ │ │ ├── nullable-typetest.ts │ │ │ │ ├── pattern-match-typetest.ts │ │ │ │ ├── predicate-typetest.ts │ │ │ │ ├── set-typetest.ts │ │ │ │ ├── struct-typetest.ts │ │ │ │ ├── tuple-typetest.ts │ │ │ │ └── union-typetest.ts │ │ │ ├── array.ts │ │ │ ├── assertions.ts │ │ │ ├── bit-array.ts │ │ │ ├── boolean.ts │ │ │ ├── bytes.ts │ │ │ ├── constant.ts │ │ │ ├── discriminated-union.ts │ │ │ ├── enum-helpers.ts │ │ │ ├── enum.ts │ │ │ ├── hidden-prefix.ts │ │ │ ├── hidden-suffix.ts │ │ │ ├── index.ts │ │ │ ├── literal-union.ts │ │ │ ├── map.ts │ │ │ ├── nullable.ts │ │ │ ├── pattern-match.ts │ │ │ ├── predicate.ts │ │ │ ├── set.ts │ │ │ ├── struct.ts │ │ │ ├── tuple.ts │ │ │ ├── union.ts │ │ │ ├── unit.ts │ │ │ └── utils.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── codecs-numbers/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── f32-test.ts │ │ │ │ ├── f64-test.ts │ │ │ │ ├── i128-test.ts │ │ │ │ ├── i16-test.ts │ │ │ │ ├── i32-test.ts │ │ │ │ ├── i64-test.ts │ │ │ │ ├── i8-test.ts │ │ │ │ ├── short-u16-test.ts │ │ │ │ ├── u128-test.ts │ │ │ │ ├── u16-test.ts │ │ │ │ ├── u32-test.ts │ │ │ │ ├── u64-test.ts │ │ │ │ └── u8-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── codecs-typetest.ts │ │ │ ├── assertions.ts │ │ │ ├── common.ts │ │ │ ├── f32.ts │ │ │ ├── f64.ts │ │ │ ├── i128.ts │ │ │ ├── i16.ts │ │ │ ├── i32.ts │ │ │ ├── i64.ts │ │ │ ├── i8.ts │ │ │ ├── index.ts │ │ │ ├── short-u16.ts │ │ │ ├── u128.ts │ │ │ ├── u16.ts │ │ │ ├── u32.ts │ │ │ ├── u64.ts │ │ │ ├── u8.ts │ │ │ └── utils.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── codecs-strings/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __benchmarks__/ │ │ │ │ └── run.ts │ │ │ ├── __tests__/ │ │ │ │ ├── base10-test.ts │ │ │ │ ├── base16-test.ts │ │ │ │ ├── base58-test.ts │ │ │ │ ├── base64-test.ts │ │ │ │ ├── baseX-reslice-test.ts │ │ │ │ ├── string-test.ts │ │ │ │ └── utf8-test.ts │ │ │ ├── assertions.ts │ │ │ ├── base10.ts │ │ │ ├── base16.ts │ │ │ ├── base58.ts │ │ │ ├── base64.ts │ │ │ ├── baseX-reslice.ts │ │ │ ├── baseX.ts │ │ │ ├── index.ts │ │ │ ├── null-characters.ts │ │ │ └── utf8.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── compat/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── address-test.ts │ │ │ │ ├── instruction-test.ts │ │ │ │ ├── keypair-test.ts │ │ │ │ └── transaction-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── address-typetest.ts │ │ │ │ ├── instruction-typetest.ts │ │ │ │ ├── keypair-typetest.ts │ │ │ │ └── transaction-typetest.ts │ │ │ ├── address.ts │ │ │ ├── index.ts │ │ │ ├── instruction.ts │ │ │ ├── keypair.ts │ │ │ └── transaction.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── crypto-impl/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.browser.ts │ │ │ └── index.node.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── errors/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bin/ │ │ │ └── cli.mjs │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── RPC_INTEGER_OVERFLOW-test.ts │ │ │ │ ├── context-test.ts │ │ │ │ ├── error-test.ts │ │ │ │ ├── instruction-error-test.ts │ │ │ │ ├── json-rpc-error-test.ts │ │ │ │ ├── message-formatter-test.ts │ │ │ │ ├── simulation-errors-test.ts │ │ │ │ └── transaction-error-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── error-typetest.ts │ │ │ ├── cli.ts │ │ │ ├── codes.ts │ │ │ ├── context.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ ├── instruction-error.ts │ │ │ ├── json-rpc-error.ts │ │ │ ├── message-formatter.ts │ │ │ ├── messages.ts │ │ │ ├── rpc-enum-errors.ts │ │ │ ├── simulation-errors.ts │ │ │ ├── stack-trace.ts │ │ │ └── transaction-error.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── eslint-config/ │ │ ├── .npmrc │ │ ├── eslint.config.mjs │ │ ├── eslint.config.react.mjs │ │ ├── package.json │ │ └── tsconfig.json │ ├── event-target-impl/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.browser.ts │ │ │ └── index.node.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── fast-stable-stringify/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ └── index-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── index-typetest.ts │ │ │ └── index.ts │ │ ├── tsconfig.declarations.json │ │ └── tsconfig.json │ ├── fetch-impl/ │ │ ├── .npmrc │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ └── __benchmarks__/ │ │ │ └── run.ts │ │ └── tsconfig.json │ ├── fixed-points/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── assertions-test.ts │ │ │ │ ├── binary-arithmetics-test.ts │ │ │ │ ├── binary-codec-test.ts │ │ │ │ ├── binary-comparisons-test.ts │ │ │ │ ├── binary-conversions-test.ts │ │ │ │ ├── binary-core-test.ts │ │ │ │ ├── binary-formatting-test.ts │ │ │ │ ├── binary-guards-test.ts │ │ │ │ ├── decimal-arithmetics-test.ts │ │ │ │ ├── decimal-codec-test.ts │ │ │ │ ├── decimal-comparisons-test.ts │ │ │ │ ├── decimal-conversions-test.ts │ │ │ │ ├── decimal-core-test.ts │ │ │ │ ├── decimal-formatting-test.ts │ │ │ │ ├── decimal-guards-test.ts │ │ │ │ ├── parsing-test.ts │ │ │ │ └── rounding-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── binary-arithmetics-typetest.ts │ │ │ │ ├── binary-codec-typetest.ts │ │ │ │ ├── binary-conversions-typetest.ts │ │ │ │ ├── binary-core-typetest.ts │ │ │ │ ├── binary-guards-typetest.ts │ │ │ │ ├── decimal-arithmetics-typetest.ts │ │ │ │ ├── decimal-codec-typetest.ts │ │ │ │ ├── decimal-conversions-typetest.ts │ │ │ │ ├── decimal-core-typetest.ts │ │ │ │ └── decimal-guards-typetest.ts │ │ │ ├── assertions.ts │ │ │ ├── binary/ │ │ │ │ ├── arithmetics.ts │ │ │ │ ├── codecs.ts │ │ │ │ ├── comparisons.ts │ │ │ │ ├── conversions.ts │ │ │ │ ├── core.ts │ │ │ │ ├── formatting.ts │ │ │ │ ├── guards.ts │ │ │ │ └── index.ts │ │ │ ├── codecs.ts │ │ │ ├── decimal/ │ │ │ │ ├── arithmetics.ts │ │ │ │ ├── codecs.ts │ │ │ │ ├── comparisons.ts │ │ │ │ ├── conversions.ts │ │ │ │ ├── core.ts │ │ │ │ ├── formatting.ts │ │ │ │ ├── guards.ts │ │ │ │ └── index.ts │ │ │ ├── formatting.ts │ │ │ ├── index.ts │ │ │ ├── parsing.ts │ │ │ ├── rounding.ts │ │ │ └── signedness.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── fs-impl/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.browser.ts │ │ │ └── index.node.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── functional/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ └── pipe-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── pipe-typetest.ts │ │ │ ├── index.ts │ │ │ └── pipe.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── instruction-plans/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── append-instruction-plan-test.ts │ │ │ │ ├── instruction-plan-input-test.ts │ │ │ │ ├── instruction-plan-test.ts │ │ │ │ ├── transaction-plan-errors-test.ts │ │ │ │ ├── transaction-plan-executor-test.ts │ │ │ │ ├── transaction-plan-result-test.ts │ │ │ │ ├── transaction-plan-test.ts │ │ │ │ └── transaction-planner-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── append-instruction-plan-typetest.ts │ │ │ │ ├── instruction-plan-input-typetest.ts │ │ │ │ ├── instruction-plan-typetest.ts │ │ │ │ ├── transaction-plan-executor-typetest.ts │ │ │ │ ├── transaction-plan-result-typetest.ts │ │ │ │ ├── transaction-plan-typetest.ts │ │ │ │ └── transaction-planner-typetest.ts │ │ │ ├── append-instruction-plan.ts │ │ │ ├── index.ts │ │ │ ├── instruction-plan-input.ts │ │ │ ├── instruction-plan.ts │ │ │ ├── transaction-plan-errors.ts │ │ │ ├── transaction-plan-executor.ts │ │ │ ├── transaction-plan-result.ts │ │ │ ├── transaction-plan.ts │ │ │ └── transaction-planner.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── instructions/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── instruction-test.ts │ │ │ │ └── roles-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── instruction-typetest.ts │ │ │ ├── accounts.ts │ │ │ ├── index.ts │ │ │ ├── instruction.ts │ │ │ └── roles.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── keys/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __benchmarks__/ │ │ │ │ └── run.ts │ │ │ ├── __tests__/ │ │ │ │ ├── coercions-test.ts │ │ │ │ ├── grind-keypair-test.ts │ │ │ │ ├── key-pair-test.ts │ │ │ │ ├── private-key-test.ts │ │ │ │ ├── public-key-test.ts │ │ │ │ ├── signatures-test.ts │ │ │ │ ├── write-keypair-test.browser.ts │ │ │ │ └── write-keypair-test.node.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── key-pair-typetest.ts │ │ │ │ ├── private-key-typetest.ts │ │ │ │ ├── public-key-typetest.ts │ │ │ │ ├── signatures-typetest.ts │ │ │ │ └── write-keypair-typetest.ts │ │ │ ├── algorithm.ts │ │ │ ├── grind-keypair.ts │ │ │ ├── index.ts │ │ │ ├── key-pair.ts │ │ │ ├── private-key.ts │ │ │ ├── public-key.ts │ │ │ ├── signatures.ts │ │ │ └── write-keypair.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── kit/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── airdrop-internal-test.ts │ │ │ │ ├── compute-unit-limit-estimation-test.ts │ │ │ │ ├── create-async-generator-with-initial-value-and-slot-tracking-test.ts │ │ │ │ ├── create-reactive-store-with-initial-value-and-slot-tracking-test.ts │ │ │ │ ├── decompile-transaction-message-fetching-lookup-tables-test.ts │ │ │ │ ├── fetch-lookup-tables-test.ts │ │ │ │ ├── get-minimum-balance-for-rent-exemption-test.ts │ │ │ │ ├── program-client-core-subpath-export-test.node.ts │ │ │ │ ├── send-and-confirm-durable-nonce-transaction-test.ts │ │ │ │ └── send-transaction-internal-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── airdrop-typetest.ts │ │ │ │ ├── decompile-transaction-message-fetching-lookup-tables-typetest.ts │ │ │ │ ├── scenarios/ │ │ │ │ │ ├── transaction-message-decompile-modify-typetest.ts │ │ │ │ │ └── transaction-signers-typetest.ts │ │ │ │ ├── send-and-confirm-durable-nonce-transaction-typetest.ts │ │ │ │ └── send-and-confirm-transaction-typetest.ts │ │ │ ├── airdrop-internal.ts │ │ │ ├── airdrop.ts │ │ │ ├── compute-unit-limit-estimation.ts │ │ │ ├── create-async-generator-with-initial-value-and-slot-tracking.ts │ │ │ ├── create-reactive-store-with-initial-value-and-slot-tracking.ts │ │ │ ├── decompile-transaction-message-fetching-lookup-tables.ts │ │ │ ├── fetch-lookup-tables.ts │ │ │ ├── get-minimum-balance-for-rent-exemption.ts │ │ │ ├── index.ts │ │ │ ├── program-client-core.ts │ │ │ ├── send-and-confirm-durable-nonce-transaction.ts │ │ │ ├── send-and-confirm-transaction.ts │ │ │ ├── send-transaction-internal.ts │ │ │ └── send-transaction-without-confirming.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── nominal-types/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __typetests__/ │ │ │ │ ├── brand-typetest.ts │ │ │ │ └── nominal-type-typetest.ts │ │ │ └── index.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── offchain-messages/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── content-test.ts │ │ │ │ ├── envelope-codec-test.ts │ │ │ │ ├── message-codec-test.ts │ │ │ │ ├── message-v0-codec-test.ts │ │ │ │ ├── message-v1-codec-test.ts │ │ │ │ ├── preamble-test.ts │ │ │ │ └── signatures-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── message-codec-typetest.ts │ │ │ │ ├── message-typetest.ts │ │ │ │ ├── message-v0-codec-typetest.ts │ │ │ │ └── message-v1-codec-typetest.ts │ │ │ ├── application-domain.ts │ │ │ ├── codecs/ │ │ │ │ ├── application-domain.ts │ │ │ │ ├── content.ts │ │ │ │ ├── envelope.ts │ │ │ │ ├── message-v0.ts │ │ │ │ ├── message-v1.ts │ │ │ │ ├── message.ts │ │ │ │ ├── preamble-common.ts │ │ │ │ ├── preamble-v0.ts │ │ │ │ ├── preamble-v1.ts │ │ │ │ ├── signatures.ts │ │ │ │ ├── signing-domain.ts │ │ │ │ └── version.ts │ │ │ ├── content.ts │ │ │ ├── envelope-common.ts │ │ │ ├── envelope-v0.ts │ │ │ ├── envelope-v1.ts │ │ │ ├── envelope.ts │ │ │ ├── index.ts │ │ │ ├── message-v0.ts │ │ │ ├── message-v1.ts │ │ │ ├── message.ts │ │ │ ├── preamble-v0.ts │ │ │ ├── preamble-v1.ts │ │ │ ├── signatures.ts │ │ │ └── version.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── options/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── option-codec-test.ts │ │ │ │ ├── option-test.ts │ │ │ │ ├── unwrap-option-recursively-test.ts │ │ │ │ └── unwrap-option-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── option-codec-typetest.ts │ │ │ ├── index.ts │ │ │ ├── option-codec.ts │ │ │ ├── option.ts │ │ │ ├── unwrap-option-recursively.ts │ │ │ └── unwrap-option.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── plugin-core/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ └── client-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── client-typetest.ts │ │ │ ├── client.ts │ │ │ └── index.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── plugin-interfaces/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __typetests__/ │ │ │ │ ├── airdrop-typetest.ts │ │ │ │ ├── get-minimum-balance-typetest.ts │ │ │ │ ├── identity-typetest.ts │ │ │ │ ├── instruction-plans-typetest.ts │ │ │ │ ├── payer-typetest.ts │ │ │ │ ├── rpc-typetest.ts │ │ │ │ └── subscribe-to-typetest.ts │ │ │ ├── airdrop.ts │ │ │ ├── get-minimum-balance.ts │ │ │ ├── identity.ts │ │ │ ├── index.ts │ │ │ ├── instruction-plans.ts │ │ │ ├── payer.ts │ │ │ ├── rpc.ts │ │ │ └── subscribe-to.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── program-client-core/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── instruction-input-resolution-test.ts │ │ │ │ ├── self-fetch-functions-test.ts │ │ │ │ └── self-plan-and-send-functions-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── instruction-input-resolution-typetest.ts │ │ │ │ ├── instructions-typetest.ts │ │ │ │ ├── self-fetch-functions-typetest.ts │ │ │ │ └── self-plan-and-send-functions-typetest.ts │ │ │ ├── index.ts │ │ │ ├── instruction-input-resolution.ts │ │ │ ├── instructions.ts │ │ │ ├── self-fetch-functions.ts │ │ │ └── self-plan-and-send-functions.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── programs/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ └── program-error-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── program-error-typetest.ts │ │ │ ├── index.ts │ │ │ └── program-error.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── promises/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── abortable-test.browser.ts │ │ │ │ ├── abortable-test.ts │ │ │ │ └── race-test.ts │ │ │ ├── abortable.ts │ │ │ ├── index.ts │ │ │ └── race.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── react/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── eslint.config.mjs │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── SelectedWalletAccountContextProvider.tsx │ │ │ ├── __tests__/ │ │ │ │ ├── SelectedWalletAccountContextProvider-test.browser.tsx │ │ │ │ ├── useSignAndSendTransaction-test.ts │ │ │ │ ├── useSignIn-test.ts │ │ │ │ ├── useSignMessage-test.ts │ │ │ │ ├── useSignTransaction-test.ts │ │ │ │ ├── useWalletAccountMessageSigner-test.ts │ │ │ │ ├── useWalletAccountTransactionSendingSigner-test.ts │ │ │ │ └── useWalletAccountTransactionSigner-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── selectedWalletAccountContextProvider-typetest.ts │ │ │ │ ├── useSignAndSendTransaction-typetest.ts │ │ │ │ ├── useSignIn-typetest.ts │ │ │ │ └── useSignTransaction-typetest.ts │ │ │ ├── chain.ts │ │ │ ├── index.ts │ │ │ ├── selectedWalletAccountContext.ts │ │ │ ├── test-renderer.tsx │ │ │ ├── useSignAndSendTransaction.ts │ │ │ ├── useSignIn.ts │ │ │ ├── useSignMessage.ts │ │ │ ├── useSignTransaction.ts │ │ │ ├── useWalletAccountMessageSigner.ts │ │ │ ├── useWalletAccountTransactionSendingSigner.ts │ │ │ └── useWalletAccountTransactionSigner.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── rpc-integer-overflow-error-test.ts │ │ │ │ ├── rpc-integer-overflow-test.ts │ │ │ │ ├── rpc-request-coalescer-test.ts │ │ │ │ ├── rpc-request-deduplication-test.ts │ │ │ │ └── rpc-transport-header-config-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── rpc-clusters-typetest.ts │ │ │ ├── index.ts │ │ │ ├── rpc-clusters.ts │ │ │ ├── rpc-default-config.ts │ │ │ ├── rpc-integer-overflow-error.ts │ │ │ ├── rpc-request-coalescer.ts │ │ │ ├── rpc-request-deduplication.ts │ │ │ ├── rpc-transport.ts │ │ │ └── rpc.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-api/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── get-account-info-test.ts │ │ │ │ ├── get-balance-test.ts │ │ │ │ ├── get-block-commitment-test.ts │ │ │ │ ├── get-block-height.ts │ │ │ │ ├── get-block-production-test.ts │ │ │ │ ├── get-block-test.ts │ │ │ │ ├── get-block-time-test.ts │ │ │ │ ├── get-blocks-with-limit-test.ts │ │ │ │ ├── get-cluster-nodes-test.ts │ │ │ │ ├── get-epoch-info-test.ts │ │ │ │ ├── get-epoch-schedule-test.ts │ │ │ │ ├── get-fee-for-message-test.ts │ │ │ │ ├── get-first-available-block-test.ts │ │ │ │ ├── get-genesis-hash-test.ts │ │ │ │ ├── get-health-test.ts │ │ │ │ ├── get-highest-snapshot-slot-test.ts │ │ │ │ ├── get-identity-test.ts │ │ │ │ ├── get-inflation-governor-test.ts │ │ │ │ ├── get-inflation-rate-test.ts │ │ │ │ ├── get-inflation-reward-test.ts │ │ │ │ ├── get-largest-accounts-test.ts │ │ │ │ ├── get-latest-blockhash-test.ts │ │ │ │ ├── get-leader-schedule-test.ts │ │ │ │ ├── get-max-retransmit-slot-test.ts │ │ │ │ ├── get-max-shred-insert-slot-test.ts │ │ │ │ ├── get-minimum-balance-for-rent-exemption-test.ts │ │ │ │ ├── get-multiple-accounts-test.ts │ │ │ │ ├── get-program-accounts-test.ts │ │ │ │ ├── get-recent-performance-samples-test.ts │ │ │ │ ├── get-recent-prioritization-fees-test.ts │ │ │ │ ├── get-signature-statuses-test.ts │ │ │ │ ├── get-signatures-for-address-test.ts │ │ │ │ ├── get-slot-leader-test.ts │ │ │ │ ├── get-slot-leaders-test.ts │ │ │ │ ├── get-slot-test.ts │ │ │ │ ├── get-stake-minimum-delegation-test.ts │ │ │ │ ├── get-supply-test.ts │ │ │ │ ├── get-token-account-balance-test.ts │ │ │ │ ├── get-token-accounts-by-delegate-test.ts │ │ │ │ ├── get-token-accounts-by-owner-test.ts │ │ │ │ ├── get-token-largest-accounts-test.ts │ │ │ │ ├── get-token-supply-test.ts │ │ │ │ ├── get-transaction-count-test.ts │ │ │ │ ├── get-transaction-test.ts │ │ │ │ ├── get-version-test.ts │ │ │ │ ├── get-vote-accounts.ts │ │ │ │ ├── is-blockhash-valid-test.ts │ │ │ │ ├── minimum-ledger-slot-test.ts │ │ │ │ ├── request-airdrop-test.ts │ │ │ │ ├── send-transaction-test.ts │ │ │ │ └── simulate-transaction-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── get-block-production-typetest.ts │ │ │ │ ├── get-block-typetest.ts │ │ │ │ ├── get-leader-schedule-typetest.ts │ │ │ │ ├── get-supply-typetest.ts │ │ │ │ ├── get-token-accounts-by-delegate-typetest.ts │ │ │ │ ├── get-token-accounts-by-owner-typetest.ts │ │ │ │ ├── rpc-api-typetest.ts │ │ │ │ └── simulate-transaction-typetest.ts │ │ │ ├── getAccountInfo.ts │ │ │ ├── getBalance.ts │ │ │ ├── getBlock.ts │ │ │ ├── getBlockCommitment.ts │ │ │ ├── getBlockHeight.ts │ │ │ ├── getBlockProduction.ts │ │ │ ├── getBlockTime.ts │ │ │ ├── getBlocks.ts │ │ │ ├── getBlocksWithLimit.ts │ │ │ ├── getClusterNodes.ts │ │ │ ├── getEpochInfo.ts │ │ │ ├── getEpochSchedule.ts │ │ │ ├── getFeeForMessage.ts │ │ │ ├── getFirstAvailableBlock.ts │ │ │ ├── getGenesisHash.ts │ │ │ ├── getHealth.ts │ │ │ ├── getHighestSnapshotSlot.ts │ │ │ ├── getIdentity.ts │ │ │ ├── getInflationGovernor.ts │ │ │ ├── getInflationRate.ts │ │ │ ├── getInflationReward.ts │ │ │ ├── getLargestAccounts.ts │ │ │ ├── getLatestBlockhash.ts │ │ │ ├── getLeaderSchedule.ts │ │ │ ├── getMaxRetransmitSlot.ts │ │ │ ├── getMaxShredInsertSlot.ts │ │ │ ├── getMinimumBalanceForRentExemption.ts │ │ │ ├── getMultipleAccounts.ts │ │ │ ├── getProgramAccounts.ts │ │ │ ├── getRecentPerformanceSamples.ts │ │ │ ├── getRecentPrioritizationFees.ts │ │ │ ├── getSignatureStatuses.ts │ │ │ ├── getSignaturesForAddress.ts │ │ │ ├── getSlot.ts │ │ │ ├── getSlotLeader.ts │ │ │ ├── getSlotLeaders.ts │ │ │ ├── getStakeMinimumDelegation.ts │ │ │ ├── getSupply.ts │ │ │ ├── getTokenAccountBalance.ts │ │ │ ├── getTokenAccountsByDelegate.ts │ │ │ ├── getTokenAccountsByOwner.ts │ │ │ ├── getTokenLargestAccounts.ts │ │ │ ├── getTokenSupply.ts │ │ │ ├── getTransaction.ts │ │ │ ├── getTransactionCount.ts │ │ │ ├── getVersion.ts │ │ │ ├── getVoteAccounts.ts │ │ │ ├── index.ts │ │ │ ├── isBlockhashValid.ts │ │ │ ├── minimumLedgerSlot.ts │ │ │ ├── requestAirdrop.ts │ │ │ ├── sendTransaction.ts │ │ │ └── simulateTransaction.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-graphql/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── account-test.ts │ │ │ │ ├── block-tests.ts │ │ │ │ ├── program-accounts-test.ts │ │ │ │ └── transaction-tests.ts │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── loaders/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── account-loader-test.ts │ │ │ │ │ ├── block-loader-test.ts │ │ │ │ │ ├── program-accounts-loader-test.ts │ │ │ │ │ └── transaction-loader-test.ts │ │ │ │ ├── account.ts │ │ │ │ ├── block.ts │ │ │ │ ├── coalescer.ts │ │ │ │ ├── index.ts │ │ │ │ ├── loader.ts │ │ │ │ ├── program-accounts.ts │ │ │ │ └── transaction.ts │ │ │ ├── resolvers/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── account-resolver-test.ts │ │ │ │ │ ├── block-inputs-test.ts │ │ │ │ │ ├── block-resolver-test.ts │ │ │ │ │ ├── program-accounts-resolver-test.ts │ │ │ │ │ └── transaction-resolver-test.ts │ │ │ │ ├── account.ts │ │ │ │ ├── block.ts │ │ │ │ ├── program-accounts.ts │ │ │ │ ├── resolve-info/ │ │ │ │ │ ├── account.ts │ │ │ │ │ ├── block.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── program-accounts.ts │ │ │ │ │ ├── transaction.ts │ │ │ │ │ └── visitor.ts │ │ │ │ └── transaction.ts │ │ │ └── schema/ │ │ │ ├── type-defs/ │ │ │ │ ├── account.ts │ │ │ │ ├── block.ts │ │ │ │ ├── index.ts │ │ │ │ ├── instruction.ts │ │ │ │ ├── root.ts │ │ │ │ ├── transaction.ts │ │ │ │ └── types.ts │ │ │ └── type-resolvers/ │ │ │ ├── account.ts │ │ │ ├── block.ts │ │ │ ├── index.ts │ │ │ ├── instruction.ts │ │ │ ├── root.ts │ │ │ ├── transaction.ts │ │ │ └── types.ts │ │ ├── tsconfig.declarations.json │ │ └── tsconfig.json │ ├── rpc-parsed-types/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __typetests__/ │ │ │ │ ├── address-lookup-table-accounts-typetest.ts │ │ │ │ ├── bpf-upgradeable-loader-accounts-typetest.ts │ │ │ │ ├── config-accounts-typetest.ts │ │ │ │ ├── nonce-accounts-typetest.ts │ │ │ │ ├── stake-accounts-typetest.ts │ │ │ │ ├── sysvar-accounts-typetest.ts │ │ │ │ ├── token-accounts-typetest.ts │ │ │ │ └── vote-accounts-typetest.ts │ │ │ ├── address-lookup-table-accounts.ts │ │ │ ├── bpf-upgradeable-loader-accounts.ts │ │ │ ├── config-accounts.ts │ │ │ ├── index.ts │ │ │ ├── nonce-accounts.ts │ │ │ ├── rpc-parsed-type.ts │ │ │ ├── stake-accounts.ts │ │ │ ├── sysvar-accounts.ts │ │ │ ├── token-accounts.ts │ │ │ └── vote-accounts.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-spec/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── rpc-api-test.ts │ │ │ │ ├── rpc-test.ts │ │ │ │ └── rpc-transport-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── rpc-api-typetest.ts │ │ │ │ ├── rpc-transport-typetest.ts │ │ │ │ └── rpc-typetest.ts │ │ │ ├── index.ts │ │ │ ├── rpc-api.ts │ │ │ ├── rpc-transport.ts │ │ │ └── rpc.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-spec-types/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── large-json-file.json │ │ │ │ ├── parse-json-with-bigints-test.ts │ │ │ │ ├── rpc-message-test.ts │ │ │ │ └── stringify-json-with-bigints-test.ts │ │ │ ├── index.ts │ │ │ ├── overloads.ts │ │ │ ├── parse-json-with-bigints.ts │ │ │ ├── rpc-message.ts │ │ │ ├── rpc-request.ts │ │ │ ├── rpc-response.ts │ │ │ ├── stringify-json-with-bigints.ts │ │ │ └── type-helpers.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-subscriptions/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── rpc-integer-overflow-error-test.ts │ │ │ │ ├── rpc-subscriptions-autopinger-test.ts │ │ │ │ ├── rpc-subscriptions-channel-pool-test.ts │ │ │ │ ├── rpc-subscriptions-coalescer-test.ts │ │ │ │ ├── rpc-subscriptions-functional-test.ts │ │ │ │ ├── rpc-subscriptions-json-bigint-test.ts │ │ │ │ ├── rpc-subscriptions-json-test.ts │ │ │ │ └── rpc-subscriptions-transport-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── rpc-subscriptions-clusters-typetest.ts │ │ │ ├── index.ts │ │ │ ├── rpc-default-config.ts │ │ │ ├── rpc-integer-overflow-error.ts │ │ │ ├── rpc-subscriptions-autopinger.ts │ │ │ ├── rpc-subscriptions-channel-pool-internal.ts │ │ │ ├── rpc-subscriptions-channel-pool.ts │ │ │ ├── rpc-subscriptions-channel.ts │ │ │ ├── rpc-subscriptions-clusters.ts │ │ │ ├── rpc-subscriptions-coalescer.ts │ │ │ ├── rpc-subscriptions-json-bigint.ts │ │ │ ├── rpc-subscriptions-json.ts │ │ │ ├── rpc-subscriptions-transport.ts │ │ │ └── rpc-subscriptions.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-subscriptions-api/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── account-notifications-test.ts │ │ │ │ ├── block-notifications-test.ts │ │ │ │ ├── index-test.ts │ │ │ │ ├── logs-notifications-test.ts │ │ │ │ ├── program-notifications-test.ts │ │ │ │ ├── root-notifications-test.ts │ │ │ │ ├── signature-notifications-test.ts │ │ │ │ ├── slot-notifications-test.ts │ │ │ │ ├── slots-updates-notifications-test.ts │ │ │ │ └── vote-notifications-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── account-notifications-typetest.ts │ │ │ │ ├── block-notifications-typetest.ts │ │ │ │ ├── logs-notifications-typetest.ts │ │ │ │ ├── program-notifications-typetest.ts │ │ │ │ ├── root-notifications-typetest.ts │ │ │ │ ├── rpc-subscriptions-api-typetest.ts │ │ │ │ ├── signature-notifications-typetest.ts │ │ │ │ ├── slot-notifications-typetest.ts │ │ │ │ ├── slots-updates-notifications-typetest.ts │ │ │ │ └── vote-notifications-typetest.ts │ │ │ ├── account-notifications.ts │ │ │ ├── block-notifications.ts │ │ │ ├── index.ts │ │ │ ├── logs-notifications.ts │ │ │ ├── program-notifications.ts │ │ │ ├── root-notifications.ts │ │ │ ├── signature-notifications.ts │ │ │ ├── slot-notifications.ts │ │ │ ├── slots-updates-notifications.ts │ │ │ └── vote-notifications.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-subscriptions-channel-websocket/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __mocks__/ │ │ │ │ └── @solana/ │ │ │ │ └── ws-impl.ts │ │ │ ├── __tests__/ │ │ │ │ └── websocket-channel-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── websocket-channel-typetest.ts │ │ │ ├── index.ts │ │ │ └── websocket-channel.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-subscriptions-spec/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── rpc-subscriptions-api-test.ts │ │ │ │ ├── rpc-subscriptions-channel-test.ts │ │ │ │ ├── rpc-subscriptions-pubsub-plan-test.ts │ │ │ │ └── rpc-subscriptions-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── rpc-subscriptions-api-typetest.ts │ │ │ │ └── rpc-subscriptions-typetest.ts │ │ │ ├── index.ts │ │ │ ├── rpc-subscriptions-api.ts │ │ │ ├── rpc-subscriptions-channel.ts │ │ │ ├── rpc-subscriptions-pubsub-plan.ts │ │ │ ├── rpc-subscriptions-request.ts │ │ │ ├── rpc-subscriptions-transport.ts │ │ │ └── rpc-subscriptions.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-transformers/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── request-transformer-bigint-downcast-test.ts │ │ │ │ ├── request-transformer-default-commitment-test.ts │ │ │ │ ├── request-transformer-integer-overflow-test.ts │ │ │ │ ├── request-transformer-test.ts │ │ │ │ ├── response-transformer-bigint-upcast-test.ts │ │ │ │ ├── response-transformer-test.ts │ │ │ │ └── response-transformer-throw-solana-error-test.ts │ │ │ ├── index.ts │ │ │ ├── request-transformer-bigint-downcast-internal.ts │ │ │ ├── request-transformer-bigint-downcast.ts │ │ │ ├── request-transformer-default-commitment-internal.ts │ │ │ ├── request-transformer-default-commitment.ts │ │ │ ├── request-transformer-integer-overflow-internal.ts │ │ │ ├── request-transformer-integer-overflow.ts │ │ │ ├── request-transformer-options-object-position-config.ts │ │ │ ├── request-transformer.ts │ │ │ ├── response-transformer-allowed-numeric-values.ts │ │ │ ├── response-transformer-bigint-upcast-internal.ts │ │ │ ├── response-transformer-bigint-upcast.ts │ │ │ ├── response-transformer-result.ts │ │ │ ├── response-transformer-throw-solana-error.ts │ │ │ ├── response-transformer.ts │ │ │ └── tree-traversal.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-transport-http/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __benchmarks__/ │ │ │ │ └── run.ts │ │ │ ├── __tests__/ │ │ │ │ ├── http-transport-abort-test.ts │ │ │ │ ├── http-transport-dispatcher-test.browser.ts │ │ │ │ ├── http-transport-for-solana-rpc-test.ts │ │ │ │ ├── http-transport-from-json-test.ts │ │ │ │ ├── http-transport-headers-test.ts │ │ │ │ ├── http-transport-test.ts │ │ │ │ ├── http-transport-to-json-test.ts │ │ │ │ └── is-solana-request-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── http-transport-typetest.ts │ │ │ ├── http-transport-config.ts │ │ │ ├── http-transport-for-solana-rpc.ts │ │ │ ├── http-transport-headers.ts │ │ │ ├── http-transport.ts │ │ │ ├── index.ts │ │ │ └── is-solana-request.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── rpc-types/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── blockhash-test.ts │ │ │ │ ├── coercions-test.ts │ │ │ │ ├── commitment-test.ts │ │ │ │ ├── lamports-test.ts │ │ │ │ ├── sol-test.ts │ │ │ │ ├── stringified-bigint-test.ts │ │ │ │ ├── stringified-number-test.ts │ │ │ │ └── unix-timestamp-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── blockhash-typetests.ts │ │ │ │ ├── coercions-typetest.ts │ │ │ │ ├── encoded-bytes-typetests.ts │ │ │ │ ├── lamports-typetest.ts │ │ │ │ └── sol-typetest.ts │ │ │ ├── account-filters.ts │ │ │ ├── account-info.ts │ │ │ ├── blockhash.ts │ │ │ ├── cluster-url.ts │ │ │ ├── commitment.ts │ │ │ ├── encoded-bytes.ts │ │ │ ├── index.ts │ │ │ ├── lamports.ts │ │ │ ├── rpc-api.ts │ │ │ ├── sol.ts │ │ │ ├── stringified-bigint.ts │ │ │ ├── stringified-number.ts │ │ │ ├── token-amount.ts │ │ │ ├── token-balance.ts │ │ │ ├── transaction-error.ts │ │ │ ├── transaction.ts │ │ │ ├── typed-numbers.ts │ │ │ └── unix-timestamp.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── signers/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── account-signer-meta-test.ts │ │ │ │ ├── add-signers-test.ts │ │ │ │ ├── deduplicate-signers-test.ts │ │ │ │ ├── fee-payer-signer-test.ts │ │ │ │ ├── grind-keypair-signer-test.ts │ │ │ │ ├── keypair-signer-test.ts │ │ │ │ ├── message-modifying-signer-test.ts │ │ │ │ ├── message-partial-signer-test.ts │ │ │ │ ├── message-signer-test.ts │ │ │ │ ├── noop-signer-test.ts │ │ │ │ ├── offchain-message-signer-test.ts │ │ │ │ ├── sign-offchain-message-test.ts │ │ │ │ ├── sign-transaction-test.ts │ │ │ │ ├── signable-message-test.ts │ │ │ │ ├── transaction-modifying-signer-test.ts │ │ │ │ ├── transaction-partial-signer-test.ts │ │ │ │ ├── transaction-sending-signer-test.ts │ │ │ │ ├── transaction-signer-test.ts │ │ │ │ ├── transaction-with-single-sending-signer.ts │ │ │ │ ├── write-keypair-signer-test.browser.ts │ │ │ │ └── write-keypair-signer-test.node.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── account-signer-meta-typetest.ts │ │ │ │ ├── add-signers-typetest.ts │ │ │ │ ├── fee-payer-typetest.ts │ │ │ │ ├── keypair-signer-typetest.ts │ │ │ │ ├── message-modifying-signer-typetest.ts │ │ │ │ ├── message-partial-signer-typetest.ts │ │ │ │ ├── message-signer-typetest.ts │ │ │ │ ├── offchain-message-signer-typetest.ts │ │ │ │ ├── sign-offchain-message-typetest.ts │ │ │ │ ├── sign-transaction-typetest.ts │ │ │ │ ├── transaction-modifying-signer-typetest.ts │ │ │ │ ├── transaction-partial-signer-typetest.ts │ │ │ │ ├── transaction-sending-signer-typetest.ts │ │ │ │ └── transaction-signer-typetest.ts │ │ │ ├── account-signer-meta.ts │ │ │ ├── add-signers.ts │ │ │ ├── deduplicate-signers.ts │ │ │ ├── fee-payer-signer.ts │ │ │ ├── grind-keypair-signer.ts │ │ │ ├── index.ts │ │ │ ├── keypair-signer.ts │ │ │ ├── message-modifying-signer.ts │ │ │ ├── message-partial-signer.ts │ │ │ ├── message-signer.ts │ │ │ ├── noop-signer.ts │ │ │ ├── offchain-message-signer.ts │ │ │ ├── sign-offchain-message.ts │ │ │ ├── sign-transaction.ts │ │ │ ├── signable-message.ts │ │ │ ├── transaction-modifying-signer.ts │ │ │ ├── transaction-partial-signer.ts │ │ │ ├── transaction-sending-signer.ts │ │ │ ├── transaction-signer.ts │ │ │ ├── transaction-with-single-sending-signer.ts │ │ │ ├── types.ts │ │ │ └── write-keypair-signer.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── subscribable/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── async-iterable-test.ts │ │ │ │ ├── data-publisher-test.ts │ │ │ │ ├── demultiplex-test.ts │ │ │ │ ├── reactive-action-store-test.ts │ │ │ │ └── reactive-stream-store-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── data-publisher-typetest.ts │ │ │ │ └── event-emitter-typetest.ts │ │ │ ├── async-iterable.ts │ │ │ ├── data-publisher.ts │ │ │ ├── demultiplex.ts │ │ │ ├── event-emitter.ts │ │ │ ├── index.ts │ │ │ ├── reactive-action-store.ts │ │ │ └── reactive-stream-store.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── sysvars/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── __setup__.ts │ │ │ │ ├── clock-test.ts │ │ │ │ ├── epoch-rewards-test.ts │ │ │ │ ├── epoch-schedule-test.ts │ │ │ │ ├── last-restart-slot-test.ts │ │ │ │ ├── recent-blockhashes-test.ts │ │ │ │ ├── rent-test.ts │ │ │ │ ├── slot-hashes-test.ts │ │ │ │ ├── slot-history-test.ts │ │ │ │ ├── stake-history-test.ts │ │ │ │ └── sysvar-test.ts │ │ │ ├── __typetests__/ │ │ │ │ └── sysvar-typetest.ts │ │ │ ├── clock.ts │ │ │ ├── epoch-rewards.ts │ │ │ ├── epoch-schedule.ts │ │ │ ├── index.ts │ │ │ ├── last-restart-slot.ts │ │ │ ├── recent-blockhashes.ts │ │ │ ├── rent.ts │ │ │ ├── slot-hashes.ts │ │ │ ├── slot-history.ts │ │ │ ├── stake-history.ts │ │ │ └── sysvar.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── test-config/ │ │ ├── .npmrc │ │ ├── browser-environment.ts │ │ ├── global.d.ts │ │ ├── jest-dev.config.js │ │ ├── jest-dev.config.ts │ │ ├── jest-lint.config.js │ │ ├── jest-lint.config.ts │ │ ├── jest-prettier.config.js │ │ ├── jest-prettier.config.ts │ │ ├── jest-unit.config.browser.js │ │ ├── jest-unit.config.common.js │ │ ├── jest-unit.config.node.js │ │ ├── package.json │ │ ├── setup-define-version-constant.ts │ │ ├── setup-dev-mode.ts │ │ ├── setup-secure-context.ts │ │ ├── setup-text-encoder.ts │ │ ├── setup-undici-fetch.ts │ │ ├── setup-web-buffer-global.ts │ │ ├── setup-webcrypto.ts │ │ ├── setup-whatwg-fetch.ts │ │ ├── test-validator-setup.js │ │ ├── test-validator-teardown.js │ │ └── tsconfig.json │ ├── test-matchers/ │ │ ├── .npmrc │ │ ├── package.json │ │ ├── toBeFrozenObject.ts │ │ ├── toEqualArrayBuffer.ts │ │ └── tsconfig.json │ ├── text-encoding-impl/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── LICENSE │ │ ├── package.json │ │ ├── src/ │ │ │ ├── index.browser.ts │ │ │ ├── index.native.ts │ │ │ └── index.node.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── tsup.config.ts │ ├── transaction-confirmation/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── confirmation-strategy-blockheight-test.ts │ │ │ │ ├── confirmation-strategy-nonce-test.ts │ │ │ │ ├── confirmation-strategy-racer-test.ts │ │ │ │ ├── confirmation-strategy-signature-test.ts │ │ │ │ ├── confirmation-strategy-timeout-test.ts │ │ │ │ └── waiters-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── confirmation-strategy-blockheight-typetest.ts │ │ │ │ ├── confirmation-strategy-nonce-typetest.ts │ │ │ │ └── confirmation-strategy-recent-signature-typetest.ts │ │ │ ├── confirmation-strategy-blockheight.ts │ │ │ ├── confirmation-strategy-nonce.ts │ │ │ ├── confirmation-strategy-racer.ts │ │ │ ├── confirmation-strategy-recent-signature.ts │ │ │ ├── confirmation-strategy-timeout.ts │ │ │ ├── index.ts │ │ │ └── waiters.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── transaction-messages/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── blockhash-test.ts │ │ │ │ ├── compress-transaction-message-test.ts │ │ │ │ ├── compute-budget-instruction-test.ts │ │ │ │ ├── compute-unit-limit-test.ts │ │ │ │ ├── compute-unit-price-test.ts │ │ │ │ ├── create-transaction-message-test.ts │ │ │ │ ├── durable-nonce-test.ts │ │ │ │ ├── fee-payer-test.ts │ │ │ │ ├── heap-size-test.ts │ │ │ │ ├── instructions-test.ts │ │ │ │ ├── loaded-accounts-data-size-limit-test.ts │ │ │ │ ├── priority-fee-lamports-test.ts │ │ │ │ └── v1-transaction-config-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── blockhash-typetest.ts │ │ │ │ ├── compress-transaction-message-typetest.ts │ │ │ │ ├── compute-unit-limit-typetest.ts │ │ │ │ ├── compute-unit-price-typetest.ts │ │ │ │ ├── create-transaction-message-typetest.ts │ │ │ │ ├── durable-nonce-typetest.ts │ │ │ │ ├── fee-payer-typetest.ts │ │ │ │ ├── heap-size-typetest.ts │ │ │ │ ├── instructions-typetest.ts │ │ │ │ ├── loaded-accounts-data-size-limit-typetest.ts │ │ │ │ ├── priority-fee-lamports-typetest.ts │ │ │ │ ├── scenarios/ │ │ │ │ │ └── message-modifications-typetest.ts │ │ │ │ └── transaction-config-typetest.ts │ │ │ ├── addresses-by-lookup-table-address.ts │ │ │ ├── blockhash.ts │ │ │ ├── codecs/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── message-test.ts │ │ │ │ │ └── transaction-version-test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── legacy/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ ├── header-test.ts │ │ │ │ │ │ ├── instruction-test.ts │ │ │ │ │ │ ├── lifetime-token-test.ts │ │ │ │ │ │ └── message-test.ts │ │ │ │ │ ├── header.ts │ │ │ │ │ ├── instruction.ts │ │ │ │ │ ├── lifetime-token.ts │ │ │ │ │ └── message.ts │ │ │ │ ├── message.ts │ │ │ │ ├── transaction-version.ts │ │ │ │ ├── v0/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ ├── address-table-lookup-test.ts │ │ │ │ │ │ └── message-test.ts │ │ │ │ │ ├── address-table-lookup.ts │ │ │ │ │ └── message.ts │ │ │ │ └── v1/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── config-test.ts │ │ │ │ │ ├── instruction-test.ts │ │ │ │ │ └── message-test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── instruction.ts │ │ │ │ └── message.ts │ │ │ ├── compile/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── message-test.ts │ │ │ │ ├── __typetests__/ │ │ │ │ │ └── message-typetest.ts │ │ │ │ ├── index.ts │ │ │ │ ├── legacy/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ ├── accounts-test.ts │ │ │ │ │ │ ├── header-test.ts │ │ │ │ │ │ ├── instructions-test.ts │ │ │ │ │ │ ├── lifetime-token-test.ts │ │ │ │ │ │ └── message-test.ts │ │ │ │ │ ├── accounts.ts │ │ │ │ │ ├── header.ts │ │ │ │ │ ├── instructions.ts │ │ │ │ │ ├── lifetime-token.ts │ │ │ │ │ └── message.ts │ │ │ │ ├── message-types.ts │ │ │ │ ├── message.ts │ │ │ │ ├── v0/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ ├── accounts-test.ts │ │ │ │ │ │ ├── address-table-lookups-test.ts │ │ │ │ │ │ ├── instructions-test.ts │ │ │ │ │ │ ├── message-test.ts │ │ │ │ │ │ └── static-accounts-test.ts │ │ │ │ │ ├── accounts.ts │ │ │ │ │ ├── address-table-lookups.ts │ │ │ │ │ ├── instructions.ts │ │ │ │ │ ├── message.ts │ │ │ │ │ └── static-accounts.ts │ │ │ │ └── v1/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── config-test.ts │ │ │ │ │ ├── instructions-test.ts │ │ │ │ │ └── message-test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── instructions.ts │ │ │ │ └── message.ts │ │ │ ├── compress-transaction-message.ts │ │ │ ├── compute-budget-instruction.ts │ │ │ ├── compute-unit-limit.ts │ │ │ ├── compute-unit-price.ts │ │ │ ├── create-transaction-message.ts │ │ │ ├── decompile/ │ │ │ │ ├── __tests__/ │ │ │ │ │ └── message-test.ts │ │ │ │ ├── __typetests__/ │ │ │ │ │ └── message-typetest.ts │ │ │ │ ├── index.ts │ │ │ │ ├── legacy/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ ├── account-metas-test.ts │ │ │ │ │ │ ├── convert-instruction-test.ts │ │ │ │ │ │ ├── fee-payer-test.ts │ │ │ │ │ │ ├── lifetime-constraint-test.ts │ │ │ │ │ │ └── message-test.ts │ │ │ │ │ ├── account-metas.ts │ │ │ │ │ ├── convert-instruction.ts │ │ │ │ │ ├── fee-payer.ts │ │ │ │ │ ├── lifetime-constraint.ts │ │ │ │ │ └── message.ts │ │ │ │ ├── message.ts │ │ │ │ ├── v0/ │ │ │ │ │ ├── __tests__/ │ │ │ │ │ │ ├── address-lookup-metas-test.ts │ │ │ │ │ │ └── message-test.ts │ │ │ │ │ ├── address-lookup-metas.ts │ │ │ │ │ └── message.ts │ │ │ │ └── v1/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── config-test.ts │ │ │ │ │ ├── instructions-test.ts │ │ │ │ │ └── message-test.ts │ │ │ │ ├── config.ts │ │ │ │ ├── instructions.ts │ │ │ │ └── message.ts │ │ │ ├── durable-nonce-instruction.ts │ │ │ ├── durable-nonce.ts │ │ │ ├── fee-payer.ts │ │ │ ├── heap-size.ts │ │ │ ├── index.ts │ │ │ ├── instructions.ts │ │ │ ├── lifetime.ts │ │ │ ├── loaded-accounts-data-size-limit.ts │ │ │ ├── priority-fee-lamports.ts │ │ │ ├── transaction-message-size.ts │ │ │ ├── transaction-message.ts │ │ │ └── v1-transaction-config.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── transactions/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── compile-transaction-test.ts │ │ │ │ ├── lifetime-test.ts │ │ │ │ ├── signatures-test.ts │ │ │ │ ├── transaction-message-size-test.ts │ │ │ │ ├── transaction-size-test.ts │ │ │ │ └── wire-transaction-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── compile-transaction-typetest.ts │ │ │ │ ├── lifetime-typetest.ts │ │ │ │ ├── sendable-transaction-typetest.ts │ │ │ │ ├── signatures-typetest.ts │ │ │ │ ├── transaction-message-size-typetest.ts │ │ │ │ ├── transaction-size-typetest.ts │ │ │ │ ├── transaction-typetest.ts │ │ │ │ ├── transaction-typetests.ts │ │ │ │ └── wire-transaction-typetests.ts │ │ │ ├── codecs/ │ │ │ │ ├── __tests__/ │ │ │ │ │ ├── signatures-encoder-test.ts │ │ │ │ │ └── transaction-codec-test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── signatures-encoder.ts │ │ │ │ └── transaction-codec.ts │ │ │ ├── compile-transaction.ts │ │ │ ├── index.ts │ │ │ ├── lifetime.ts │ │ │ ├── sendable-transaction.ts │ │ │ ├── signatures.ts │ │ │ ├── transaction-message-size.ts │ │ │ ├── transaction-size-limits.ts │ │ │ ├── transaction-size.ts │ │ │ ├── transaction.ts │ │ │ └── wire-transaction.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── tsconfig/ │ │ ├── .npmrc │ │ ├── README.md │ │ ├── base.json │ │ └── package.json │ ├── wallet-account-signer/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── wallet-account-message-signer-test.ts │ │ │ │ ├── wallet-account-signer-test.ts │ │ │ │ ├── wallet-account-transaction-sending-signer-test.ts │ │ │ │ └── wallet-account-transaction-signer-test.ts │ │ │ ├── __typetests__/ │ │ │ │ ├── wallet-account-message-signer-typetest.ts │ │ │ │ ├── wallet-account-transaction-sending-signer-typetest.ts │ │ │ │ └── wallet-account-transaction-signer-typetest.ts │ │ │ ├── index.ts │ │ │ ├── wallet-account-message-signer.ts │ │ │ ├── wallet-account-signer.ts │ │ │ ├── wallet-account-transaction-sending-signer.ts │ │ │ └── wallet-account-transaction-signer.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ ├── webcrypto-ed25519-polyfill/ │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── .prettierignore │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── .npmignore │ │ │ ├── __tests__/ │ │ │ │ ├── install-test.ts │ │ │ │ └── secrets-test.ts │ │ │ ├── index.ts │ │ │ ├── install.ts │ │ │ └── secrets.ts │ │ ├── tsconfig.declarations.json │ │ ├── tsconfig.json │ │ └── typedoc.json │ └── ws-impl/ │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── LICENSE │ ├── package.json │ ├── src/ │ │ ├── index.browser.ts │ │ └── index.node.ts │ ├── tsconfig.declarations.json │ ├── tsconfig.json │ └── tsup.config.ts ├── patches/ │ └── jest-runner-prettier@1.0.0.patch ├── pnpm-workspace.yaml ├── scripts/ │ ├── fixtures/ │ │ ├── 4nTLDQiSTRHbngKZWPMfYnZdWTbKiNeuuPcX7yFUpSAc.json │ │ ├── GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G.json │ │ ├── address-lookup-table-account.json │ │ ├── bpf-upgradeable-loader-program-account.json │ │ ├── config-stake-account.json │ │ ├── config-validator-account.json │ │ ├── example-deserialize-transaction-address-lookup-table.json │ │ ├── example-transfer-lamports-source-account.json │ │ ├── gpa1.json │ │ ├── gpa2-1.json │ │ ├── gpa2-2.json │ │ ├── nonce-account.json │ │ ├── send-transaction-fee-payer-insufficient-funds.json │ │ ├── send-transaction-fee-payer.json │ │ ├── spl-token-22-account-mega-token.json │ │ ├── spl-token-22-mint-account.json │ │ ├── spl-token-22-mint-mega-token-member.json │ │ ├── spl-token-22-mint-mega-token.json │ │ ├── spl-token-22-mint-with-token-group-account.json │ │ ├── spl-token-22-mint-with-token-group-member-account.json │ │ ├── spl-token-mint-account-with-delegated.json │ │ ├── spl-token-mint-account-with-owner.json │ │ ├── spl-token-mint-account.json │ │ ├── spl-token-mint-no-token-accounts.json │ │ ├── spl-token-multisig-account.json │ │ ├── spl-token-token-account-delegated.json │ │ ├── spl-token-token-account-owner.json │ │ ├── spl-token-token-account.json │ │ ├── stake-account.json │ │ └── vote-account.json │ ├── get-latest-validator-release-version.sh │ ├── setup-test-validator.sh │ └── start-shared-test-validator.sh ├── skills-lock.json ├── tsconfig.json ├── turbo.json ├── typedoc.json └── typedoc.plugin.mjs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .agents/skills/changesets/INJECT.md ================================================ --- heading: Changesets --- - Any PR that should trigger a package release MUST include a changeset. - Identify affected packages by mapping changed files to their nearest `package.json`. - Choose the right bump: `patch` for fixes, `minor` for features, `major` for breaking changes. - While a project is pre-1.0, `minor` bumps may be treated as breaking. - ALWAYS use `npx changeset add --empty` to generate a new changeset file with a random name. NEVER create changeset files manually. - No changeset needed for: docs-only changes, CI config, dev dependency updates, test-only changes. ================================================ FILE: .agents/skills/changesets/SKILL.md ================================================ --- name: changesets description: Generate a changeset file with a changelog entry for package releases. Use when a PR introduces user-facing changes that should trigger a version bump. argument-hint: "[patch|minor|major]" allowed-tools: Bash(npx changeset add --empty) --- # Generate Changeset Create a new changeset file for the current project using the [changesets CLI](https://github.com/changesets/changesets), then fill it with a proper changelog entry based on recent changes. ## Key Rules - Any PR that should trigger a package release MUST include a changeset. - Identify affected packages by mapping changed files to their nearest `package.json`. - Choose the right bump: `patch` for fixes, `minor` for features, `major` for breaking changes. - While a project is pre-1.0, `minor` bumps may be treated as breaking. - ALWAYS use `npx changeset add --empty` to generate a new changeset file with a random name. NEVER create changeset files manually. - No changeset needed for: docs-only changes, CI config, dev dependency updates, test-only changes. - Do NOT modify any existing changeset files. - Only create one new changeset file per invocation. ## Guidelines ### Changeset Format ``` --- '': patch '': patch --- ``` ### Changelog Entries - Write a concise but informative changelog entry (1-3 sentences). - Start with a verb — e.g. "Add", "Remove", "Replace", "Fix", "Update". - Focus on what changed from the user's perspective, not implementation details. - Mention renamed or removed APIs explicitly. ### Breaking Changes Add a `**BREAKING CHANGES**` section (bold paragraph, not a header) after the changelog entry when: - The bump level is `major`, OR - The bump level is `minor` AND the current major version is `0` (i.e. pre-1.0) ...and the changes actually introduce breaking changes. The breaking changes section should list each breaking change with a bold title summarizing the change, followed by a brief explanation and a `diff` code block showing the before/after migration when applicable. For example: ``` Replace the `useGranularImports` boolean option with a new `importStrategy` option. **BREAKING CHANGES** **`useGranularImports` replaced with `importStrategy`.** The boolean option has been replaced with a string union for finer control. \`\`\`diff - renderVisitor('./output', { useGranularImports: true }); + renderVisitor('./output', { importStrategy: 'granular' }); \`\`\` **Default import behavior changed.** The default strategy is now `'preferRoot'`, which falls back to granular packages for symbols not on the root entrypoint. \`\`\`diff - renderVisitor('./output'); // equivalent to useGranularImports: false + renderVisitor('./output'); // equivalent to importStrategy: 'preferRoot' \`\`\` ``` Not every breaking change needs a diff — use them when there's a clear before/after code change to show. For type removals or behavioral changes, a text explanation is sufficient. ## Command Process When invoked as a command, follow these steps: ### Arguments - `[patch|minor|major]` (optional): Override the bump level for all affected packages. ### Steps 1. **Determine the change source.** Check `git status` for uncommitted changes. If there are meaningful working tree changes, use those. Otherwise, use the latest commit(s) on the current branch (compared to the base branch from `.changeset/config.json`). 2. **Identify which packages changed.** Look at which files were modified and map them to their nearest `package.json` to determine affected packages. Ignore changes that are not relevant to a changeset — e.g. typo fixes in comments, dependency version bumps in `package.json`, changes to CI config, docs-only changes in non-doc packages, etc. Use your judgement to filter out noise. 3. **Run `npx changeset add --empty`** to generate a new changeset file with a random name. 4. **Identify the newly created file** — it will be the most recently created `.md` file in `.changeset/` (excluding `README.md`). 5. **Write the changeset file** with the proper frontmatter listing each affected package and the bump level, followed by the changelog entry. ================================================ FILE: .agents/skills/shipping-git/INJECT.md ================================================ --- heading: Shipping (Git) --- - NEVER commit, push, create branches, or create PRs without explicit user approval. - Before any git operation that creates or modifies a commit, present a review block containing: changeset entry (if applicable), commit title, and commit/PR description. ALWAYS wait for approval. - Use standard `git add` and `git commit` workflows. Concise title on the first line, blank line, then description body. - Use `gh pr create` for pull requests. - Write commit and PR descriptions as natural flowing prose. Do NOT insert hard line breaks mid-paragraph. ================================================ FILE: .agents/skills/shipping-git/SKILL.md ================================================ --- name: shipping-git description: Shipping workflow using standard Git and GitHub CLI. Provides guidance for committing, branching, and creating PRs. --- # Ship Code Using Git Guidance for shipping code using standard Git and the GitHub CLI (`gh`). This skill teaches the agent how to create commits, branches, and pull requests following conventional workflows. ## Key Rules - NEVER commit, push, create branches, or create PRs without explicit user approval. - Before any git operation that creates or modifies a commit, present a review block containing: changeset content (if applicable), commit title, and commit/PR description. ALWAYS wait for approval. - Show the proposed changeset entry for review before writing the changeset file. - Use standard `git add` and `git commit` workflows. Concise title on the first line, blank line, then description body. - Use `gh pr create` for pull requests. - Write commit and PR descriptions as natural flowing prose. Do NOT insert hard line breaks mid-paragraph. ## Guidelines ### Creating Commits Follow conventional commit message format: ```sh git add -A git commit -m "Commit title" -m "Description body explaining what changed and why." ``` - The first `-m` sets the commit title (first line). Keep it concise and descriptive. - The second `-m` sets the commit body. Explain what changed and why. - Use present tense, imperative mood (e.g. "Add feature" not "Added feature"). ### Creating Branches Create a descriptive branch name before committing: ```sh git checkout -b feature/short-description ``` Common prefixes: `feature/`, `fix/`, `refactor/`, `docs/`, `chore/`. ### Creating Pull Requests Use the GitHub CLI to create PRs: ```sh gh pr create --title "PR title" --body "Description explaining what changed and why." ``` - If a `.github/PULL_REQUEST_TEMPLATE.md` exists, use it as a guide for structuring the PR description. Fill in sections that are relevant and omit sections that don't apply (e.g. don't add "Fixes #" if there's no related issue). ### Pushing Changes Push the branch and set the upstream: ```sh git push -u origin HEAD ``` ### Review Block Format Before any git operation, present this review block and wait for approval: 1. **Changeset entry** (if applicable) — the proposed changelog entry and bump type. 2. **Commit title** — a concise title for the commit. 3. **Commit/PR description** — a short description that explains what changed and why. ================================================ FILE: .agents/skills/shipping-graphite/INJECT.md ================================================ --- heading: Shipping (Graphite) --- - Check if [Graphite](https://graphite.dev/) is installed (`which gt`). Prefer Graphite when available; fall back to the Git shipping workflow otherwise. - NEVER commit, push, create branches, or create PRs without explicit user approval. - Before any git operation that creates or modifies a commit, present a review block containing: changeset entry (if applicable), commit title, and commit/PR description. ALWAYS wait for approval. - Use `gt create -am "Title" -m "Description body"` for new PRs. The first `-m` sets the commit title; the second sets the PR description. - Use `gt modify -a` to amend the current branch with follow-up changes (NEVER create additional commits on the same branch). - ALWAYS escape backticks in commit messages with backslashes for shell compatibility (e.g. `"Update \`my-package\` config"`). - Do NOT run `gt submit` after committing. Only run it when the user explicitly asks to submit or push. - Write commit and PR descriptions as natural flowing prose. Do NOT insert hard line breaks mid-paragraph. ================================================ FILE: .agents/skills/shipping-graphite/SKILL.md ================================================ --- name: shipping-graphite description: Shipping workflow using Graphite CLI. Provides guidance for committing, branching, and creating PRs with Graphite's single-commit-per-branch model. --- # Ship Code Using Graphite Guidance for shipping code using the [Graphite](https://graphite.dev/) CLI (`gt`). This skill teaches the agent how to create commits, branches, and PRs using Graphite's single-commit-per-branch model. ## Key Rules - NEVER commit, push, create branches, or create PRs without explicit user approval. - Before any git operation that creates or modifies a commit, present a review block containing: changeset content (if applicable), commit title, and commit/PR description. ALWAYS wait for approval. - Show the proposed changeset entry for review before writing the changeset file. - Use `gt create -am "Title" -m "Description body"` for new PRs. The first `-m` sets the commit title; the second sets the PR description. - Use `gt modify -a` to amend the current branch with follow-up changes (NEVER create additional commits on the same branch). - ALWAYS escape backticks in commit messages with backslashes for shell compatibility (e.g. `"Update \`my-package\` config"`). - Write commit and PR descriptions as natural flowing prose. Do NOT insert hard line breaks mid-paragraph. ## Guidelines ### Creating a New PR Use `gt create` to create a new branch with a single commit: ```sh gt create -am "Commit title" -m "Description body explaining what changed and why." ``` - The first `-m` flag sets the commit title (first line of the commit message). - The second `-m` flag sets the commit body, which Graphite uses as the PR description when the stack is submitted. - Write the body as a concise flowing summary. Avoid excessive blank lines. - If a `.github/PULL_REQUEST_TEMPLATE.md` exists, use it as a guide for structuring the PR description. Fill in sections that are relevant and omit sections that don't apply (e.g. don't add "Fixes #" if there's no related issue). ### Amending an Existing PR When making follow-up changes to the current branch, ALWAYS amend rather than creating new commits: ```sh gt modify -a ``` This amends the single commit on the current branch. Since Graphite uses a one-commit-per-branch model, ALWAYS use `gt modify -a` for follow-up changes on the same PR. ### Submitting PRs Do NOT run `gt submit` (or `gt ss`) after creating or modifying branches. The `gt create` and `gt modify` steps are the end of the workflow unless the user explicitly asks to submit or push. When the user asks to submit, first run `gt sync` to ensure the local stack is up-to-date. This will restack branches from the main branch and prompt to delete any stale (merged) branches. If `gt sync` encounters any issues or conflicts, stop immediately and report the problem to the user before proceeding — await their instructions on how to resolve it. Only after a clean sync, run `gt submit` to create or update PRs for all branches in the current stack. ### Shell Escaping When commit titles or descriptions contain backticks (e.g. for package names or code references), escape them with a backslash so the shell passes them through literally: ```sh gt create -am "Align \`kit-plugins\` infrastructure" -m "This PR updates the shared config." ``` ### Review Block Format Before any git operation, present this review block and wait for approval: 1. **Changeset entry** (if applicable) — the proposed changelog entry and bump type. 2. **Commit title** — a concise title for the commit. 3. **Commit/PR description** — a short description that explains what changed and why. ================================================ FILE: .agents/skills/ts-docblocks/INJECT.md ================================================ --- heading: TypeScript Docblocks --- - All exported functions, types, interfaces, and constants MUST have JSDoc docblocks. - Start with `/**`, use `*` prefix for each line, end with `*/` — each on its own line. - Begin with a clear one-to-two line summary. Add a blank line before tags. - Include `@param`, `@typeParam`, `@return`, `@throws`, and at least one `@example` when helpful. - Use `{@link ...}` to reference related items. Add `@see` tags at the end for related APIs. ================================================ FILE: .agents/skills/ts-docblocks/SKILL.md ================================================ --- name: ts-docblocks description: Add missing JSDoc docblocks to exported symbols in TypeScript projects. Use when writing new exports or when code is missing documentation. argument-hint: "[path] [--all]" --- # Add Missing Docblocks Scan the specified path (or entire repository if no path given) and add missing docblocks to all exported functions, classes, interfaces, types, and constants. ## Key Rules - All exported functions, types, interfaces, and constants MUST have JSDoc docblocks. - Start with `/**`, use `*` prefix for each line, end with `*/` — each on its own line. - Begin with a clear one-to-two line summary. Add a blank line before tags. - Include `@param`, `@typeParam`, `@return`, `@throws`, and at least one `@example` when helpful. - Use `{@link ...}` to reference related items. Add `@see` tags at the end for related APIs. - Do NOT modify real code outside of docblocks. Do NOT modify existing docblocks. ## Guidelines ### Style Use JSDoc format with the following conventions: - Start with `/**` on its own line. - Use `*` prefix for each line. - End with `*/` on its own line. - Keep descriptions concise but complete. - Start your sentences with a capital letter and end with a period. - Limit your usage of em dashes but, when you do use them, use spaces on both sides. - Begin with a clear one or two line summary (no `@summary` tag needed). - Add a blank line after the summary if adding more details. - Include `@param` tags for all parameters. - Include `@typeParam` tags for all type parameters. Use `@typeParam`, not `@template`. - Include `@return` tag briefly describing the return value. - Add `@throws` for functions that may throw errors and list these errors. - Include at least one `@example` section whenever usage examples would be helpful. If the file is a TypeScript file, use TypeScript syntax in examples. Try to make the examples realistic but concise and pleasant to read. They must illustrate the concepts clearly at first glance. When more than one example is preferred, use multiple `@example` tags and keep the first one as simple as possible to illustrate the basic usage. Never use `any` type in examples. Display the `import` statements required for the example to work when imports from multiple libraries are required. It is acceptable to use placeholder variable names like `myUser` or even `/* ... */` for parts that are not relevant to the example. When multiple example sections are provided, add a brief description before each code block to quickly explain what the example illustrates. - In the rare case where more advanced documentation is also needed for the item, use the `@remarks` tag to add this extra information after any example sections. These remarks can include longer explanations and even additional code blocks if necessary. - When an item is deprecated, include a `@deprecated` tag with a brief explanation and, if applicable, suggest an alternative. - Use `{@link ...}` tags to reference other items in the codebase when relevant. - Add `@see` tags at the very end when applicable to point to other related items or documentation. Use `@see {@link ...}` format when linking to other code items. ### Examples ````ts /** * Creates a retry wrapper around an async function. * * Retries the given function up to `maxRetries` times with exponential * backoff between attempts. * * @param fn - The async function to retry. * @param maxRetries - Maximum number of retry attempts. * @param baseDelay - Base delay in milliseconds between retries. * @return A wrapped version of `fn` that retries on failure. * @throws Throws the last error if all retry attempts are exhausted. * * @example * ```ts * const fetchWithRetry = withRetry(fetchData, 3, 1000); * const data = await fetchWithRetry('/api/users'); * ``` * * @example * Custom retry configuration for flaky network calls. * ```ts * const resilientFetch = withRetry( * () => fetch('https://api.example.com/data'), * 5, * 500, * ); * ``` */ export function withRetry( fn: (...args: unknown[]) => Promise, maxRetries: number, baseDelay: number, ): (...args: unknown[]) => Promise; ```` ````ts /** * Fixes a `Uint8Array` to the specified length. * * If the array is longer than the specified length, it is truncated. * If the array is shorter than the specified length, it is padded with zeroes. * * @param bytes - The byte array to truncate or pad. * @param length - The desired length of the byte array. * @return The byte array truncated or padded to the desired length. * * @example * Truncates the byte array to the desired length. * ```ts * const bytes = new Uint8Array([0x01, 0x02, 0x03, 0x04]); * const fixedBytes = fixBytes(bytes, 2); * // ^ [0x01, 0x02] * ``` * * @example * Adds zeroes to the end of the byte array to reach the desired length. * ```ts * const bytes = new Uint8Array([0x01, 0x02]); * const fixedBytes = fixBytes(bytes, 4); * // ^ [0x01, 0x02, 0x00, 0x00] * ``` */ export const fixBytes = ( bytes: ReadonlyUint8Array | Uint8Array, length: number, ): ReadonlyUint8Array | Uint8Array; ```` ````ts /** * A tree structure representing a set of instructions with execution constraints. * * Supports parallel execution, sequential execution, and combinations of both * through recursive composition of plan nodes. * * @example * ```ts * const plan: InstructionPlan = parallelPlan([ * sequentialPlan([instructionA, instructionB]), * instructionC, * instructionD, * ]); * ``` * * @see {@link SingleInstructionPlan} * @see {@link ParallelInstructionPlan} * @see {@link SequentialInstructionPlan} */ export type InstructionPlan = | ParallelInstructionPlan | SequentialInstructionPlan | SingleInstructionPlan; ```` ## Command Process When invoked as a command, follow these steps: ### Arguments - `[path]` (optional): Narrow the scan to a specific path (e.g. `src/utils` or `packages/my-lib/src`). - `[--all]` (optional): Also scan non-exported items. ### Steps 1. If a path argument is provided, scan only that path; otherwise scan the entire repository. 2. Look for TypeScript/JavaScript files (`.ts`, `.tsx`, `.js`, `.jsx`). 3. Identify exported items without docblocks: - `export function` - `export class` - `export interface` - `export type` - `export const` (for constants and arrow functions) 4. If `--all` is passed, also identify non-exported items. 5. For each item missing a docblock: - Analyze the code to understand its purpose (this may span multiple files). - Examine parameters, return types, and behavior. - Generate an appropriate docblock following the style guide. 6. Present all changes clearly, grouped by file. Apply all changes without requiring further approval. ================================================ FILE: .agents/skills/ts-readme/INJECT.md ================================================ --- heading: TypeScript READMEs --- - When adding a new public API, add or update the package's README. - Structure: brief intro, installation, usage (with code snippet), deep-dive sections. - Code snippets must be realistic, concise, and use TypeScript syntax. - Focus on the quickest path to success. Developers should feel excited, not overwhelmed. ================================================ FILE: .agents/skills/ts-readme/SKILL.md ================================================ --- name: ts-readme description: Guidelines for writing developer-friendly READMEs for TypeScript libraries. Use when creating a new package, changing a public API, or updating documentation. argument-hint: "[path]" --- # Create or Update README Create a new README or update an existing one for a TypeScript package, library, or project. ## Key Rules - When adding a new public API, add or update the package's README. - Structure: brief intro, installation, usage (with code snippet), deep-dive sections. - Code snippets must be realistic, concise, and use TypeScript syntax. - Focus on the quickest path to success. Developers should feel excited, not overwhelmed. - Do NOT update any real code outside of the README file itself. If you identify errors in the codebase, warn the user but do not fix them. ## Guidelines A deep understanding of the project is necessary to create an effective README. Analyze the codebase, key features, and typical usage patterns to inform the content. If the project relies on other libraries or frameworks, consider how those influence usage and installation. ### Structure The layout of a README will vary from project to project, but they should generally follow these guidelines. #### 1. Intro Section - Package/library name as the main heading. - Badges for tests, package version, downloads, etc. (npm, GitHub, etc.). - **Very brief summary (ELI5)** — A single sentence or short paragraph that anyone can understand at first glance. - Only update the intro if you can meaningfully improve the summary; otherwise leave it intact. #### 2. Installation Section - Keep it extremely concise (1-2 lines of explanation max if necessary). - Show the install command in a code block. #### 3. Usage Section (Most Critical) - This is where readers' attention lands first after the intro. - Show the **quickest path to success** with this library. - **Must include a code snippet** illustrating basic usage. - Balance between brevity and clarity to create an "aha moment." - Optional: Brief text before/after the snippet if it adds clarity. - The code snippet should be realistic, concise, and immediately understandable. #### 4. Deep Dive Sections - Structure varies case-by-case based on the project. - Document key features, concepts, or use cases. - Each section should include **at least one code snippet**. Similar guidelines as the Usage section, that is, realistic and concise (shortest path to "aha moment"). - Organize logically (e.g. by feature, by use case, by complexity). - Common patterns: - **Features**: Individual feature documentation with examples. - **API Reference**: Core APIs or functions. - **Advanced Usage**: More complex scenarios. - **Configuration**: Setup and customization options. - **Examples**: Real-world use cases. - No need for a "Requirements" section for peer dependencies. Just mention them in the "Installation" section if necessary to get started. ### Code Snippets - Use TypeScript syntax for TypeScript projects. - Show realistic but concise examples. - Include necessary imports when relevant (e.g. when importing from multiple modules). - Use descriptive variable names (not `foo`, `bar`). - Keep examples focused on one concept at a time when possible. - Format for readability (proper indentation, spacing). ### Tone - Friendly and approachable. - Clear and direct. - Avoid marketing speak. - Focus on practical value. - Use active voice. - Assume the reader is a developer who wants to get started quickly. ### What to Avoid - Overly long introductions or preambles. - Walls of text without code examples. - Complex examples in the Usage section. - Unexplained jargon or acronyms. - Marketing-heavy language. - Duplicating information unnecessarily. ### Monorepo Considerations When working in a monorepo: - **Package READMEs**: Focus on the specific package — its API, installation, usage, and features. Include an installation note pointing to an umbrella package as an alternative if one exists. - **Root README**: Provides an overview of the entire monorepo, links to individual package READMEs for details. Focus on the quick-start experience rather than deep-diving into each package. - **Consistency**: When updating a package README, check other package READMEs for structural consistency (heading levels, section order, code example style). ## Command Process When invoked as a command, follow these steps: ### Arguments - `[path]` (optional): Path to the README file or its parent directory. Defaults to `./README.md`. ### Steps 1. Determine the target README path: - If a path argument is provided and ends with `.md`, use it directly. - If the argument is a directory path, use `/README.md`. - If no argument is provided, use `./README.md`. 2. Check if the README exists: - If it exists, read it to understand the current structure. - If it doesn't exist, prepare to create one from scratch. 3. Analyze the project context: - Examine `package.json` (if exists) for package name, description, dependencies. - Look at the source code to understand what the library does. - Identify key exports, main functions, and typical usage patterns. - Check for existing tests or examples that show usage. - Research any related libraries or frameworks that influence usage. - If in a monorepo, examine other package READMEs to ensure consistency in style and structure. 4. Create or update the README: - **For existing READMEs**: Identify missing sections or areas needing improvement. - **For new READMEs**: Build the full structure following the guidelines. - Preserve the intro unless improvements are clear. - Ensure the Usage section has a strong, clear example. - Add or improve deep dive sections as needed. - Include realistic code snippets throughout. 5. Present the complete README for review before applying changes. ================================================ FILE: .bundlemonrc.json ================================================ { "baseDir": "./packages/", "files": [ { "friendlyName": "@solana/kit production bundle", "path": "kit/dist/index.production.min.js" }, { "path": "**/dist/index.*.mjs" } ], "includeCommitMessage": true, "reportOutput": ["github"] } ================================================ FILE: .changeset/bold-drinks-strive.md ================================================ --- '@solana/rpc-spec': minor '@solana/kit': minor --- Add a `reactiveStore()` method to `PendingRpcRequest`. It fires the request on construction and synchronously returns a `ReactiveActionStore` that holds the request's `idle`/`running`/`success`/`error` lifecycle state. Compatible with `useSyncExternalStore`, Svelte stores, and other reactive primitives. Call `dispatch()` to re-fire the request (e.g. after an error), or `reset()` to abort the in-flight call and return to idle. ```ts const store = rpc.getAccountInfo(address).reactiveStore(); const state = useSyncExternalStore(store.subscribe, store.getState); if (state.status === 'error') return ; if (state.status === 'running' && !state.data) return ; return ; ``` ================================================ FILE: .changeset/brown-candles-relax.md ================================================ --- '@solana/rpc-subscriptions-spec': minor '@solana/kit': minor --- Add a `reactiveStore()` method to `PendingRpcSubscriptionsRequest`. Unlike `reactive()`, this variant returns a `ReactiveStore` synchronously and supports `retry()` to reconnect after an error. `reactive()` is now `@deprecated` in favour of `reactiveStore()`. ```ts const store = rpc.accountNotifications(address).reactiveStore({ abortSignal }); const state = useSyncExternalStore(store.subscribe, store.getUnifiedState); if (state.status === 'error') return ; ``` ================================================ FILE: .changeset/clever-spies-shout.md ================================================ --- '@solana/subscribable': minor '@solana/errors': minor '@solana/kit': minor --- Add `retry()` and `getUnifiedState()` to `ReactiveStore`. The new `getUnifiedState()` returns a discriminated `{ data, error, status }` snapshot with stable identity, so stores can be passed directly to `useSyncExternalStore` without an intermediate wrapper. `getState()` and `getError()` remain on the type but are now `@deprecated` in favour of the unified snapshot. A new `createReactiveStoreFromDataPublisherFactory` function is also introduced. It accepts a `createDataPublisher: () => Promise` factory rather than a ready-made publisher, which lets the store reconnect via `retry()` after an error. The existing `createReactiveStoreFromDataPublisher` is now `@deprecated`; calling `retry()` on a store it produced throws a new `SolanaError` with code `SOLANA_ERROR__SUBSCRIBABLE__RETRY_NOT_SUPPORTED`. `createReactiveStoreWithInitialValueAndSlotTracking` (from `@solana/kit`) now supports `retry()`, which re-sends the RPC request and re-subscribes to the subscription with a fresh abort signal while preserving the last known slot and value. ================================================ FILE: .changeset/config.json ================================================ { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", "access": "public", "baseBranch": "main", "bumpVersionsWithWorkspaceProtocolOnly": true, "changelog": [ "@changesets/changelog-github", { "repo": "anza-xyz/kit" } ], "fixed": [["@solana/!({example-*,*-impl,build-scripts,eslint-config,test-*,tsconfig,web3.js})"]], "linked": [], "snapshot": { "useCalculatedVersion": true }, "privatePackages": { "version": false, "tag": false }, "updateInternalDependencies": "patch" } ================================================ FILE: .changeset/some-views-pick.md ================================================ --- '@solana/subscribable': minor '@solana/rpc-subscriptions-spec': minor '@solana/kit': minor '@solana/errors': minor --- Rename `ReactiveStore` to `ReactiveStreamStore`. The old name remains exported as a deprecated alias and will be removed in a future major release. ================================================ FILE: .changeset/thin-cats-drop.md ================================================ --- '@solana/subscribable': minor --- Added `createReactiveActionStore` — a framework-agnostic state machine that wraps an async function and exposes a `{ dispatch, dispatchAsync, getState, subscribe, reset }` contract compatible with `useSyncExternalStore`, Svelte stores, Vue's `shallowRef`, and similar reactive primitives. `dispatch` is synchronous and fire-and-forget (safe from UI event handlers); `dispatchAsync` returns a promise that resolves to the wrapped function's result and rejects on failure or supersede — use `isAbortError` from `@solana/promises` to filter aborts. Each call creates a fresh `AbortController` and aborts the previous one, so rapid successive dispatches only produce one final state transition — the outcome of the most recent call. ================================================ FILE: .github/ISSUE_TEMPLATE/0_bug.md ================================================ --- name: Report a Bug about: Help us reproduce a bug you've found so that we can fix it title: '' labels: ['bug'] assignees: '' --- ## Overview ## Steps to reproduce ## Description of bug ================================================ FILE: .github/ISSUE_TEMPLATE/1_feature.md ================================================ --- name: Suggest a Feature about: Propose an idea for how to make this library better. title: '' labels: ['enhancement'] assignees: '' --- ## Motivation ## Example use case ## Details ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - about: 'Have a question about using this software, or about Solana in general? Post it on the Solana Stack Exchange.' name: Ask a Question url: 'https://solana.stackexchange.com/questions/ask' - about: 'Start or join a discussion on the Solana Tech Discord.' name: Start a Discussion url: 'https://solana.com/discord' ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ #### Problem #### Summary of Changes Fixes # ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: 'npm' directory: '/' schedule: interval: daily time: '01:00' timezone: America/Los_Angeles ignore: - dependency-name: '@radix-ui/react-dropdown-menu' groups: eslint: patterns: - '@eslint/*' - '@typescript-eslint/*' - 'typescript' solana-program-clients: patterns: - '@solana-program/*' react: patterns: - 'react' - 'react-dom' undici: patterns: - 'undici' - 'undici-types' - package-ecosystem: 'npm' directory: '/docs' schedule: interval: daily time: '01:00' timezone: America/Los_Angeles groups: eslint: patterns: - 'eslint*' - 'typescript' solana: patterns: - '@solana/*' solana-program-clients: patterns: - '@solana-program/*' react: patterns: - 'react' - 'react-dom' undici: patterns: - 'undici' - 'undici-types' ================================================ FILE: .github/label-actions.yml ================================================ question: issues: # Post a comment, `{issue-author}` is an optional placeholder comment: > Hi @{issue-author}, Thanks for your question! We want to make sure to keep signal strong in the GitHub issue tracker – to make sure that it remains the best place to track issues that affect the development of the Solana JavaScript SDK itself. Questions like yours deserve a purpose-built Q&A forum. Unless there exists evidence that this is a bug with the Solana JavaScript SDK itself, please post your question to the Solana Stack Exchange using this link: https://solana.stackexchange.com/questions/ask --- _This [automated message](https://github.com/anza-xyz/kit/blob/main/.github/label-actions.yml) is a result of having added the ‘question’ tag_. # Close the issue close: true ================================================ FILE: .github/workflows/actions/install-dependencies/action.yml ================================================ name: Install Dependencies description: Sets up Node and its package manager, then installs all dependencies inputs: version: default: 'lts/*' type: string runs: using: composite steps: - name: Install package manager uses: pnpm/action-setup@v4 with: version: 10.24.0 - name: Setup Node uses: actions/setup-node@v4 with: cache: 'pnpm' node-version: ${{ inputs.version }} registry-url: 'https://registry.npmjs.org' - name: Install dependencies shell: bash run: pnpm install ================================================ FILE: .github/workflows/actions/setup-validator/action.yml ================================================ name: Install Solana Test Validator description: Downloads and caches an install of the latest Solana Test Validator outputs: pid: description: The process id of the running test validator value: ${{ steps.start-validator.outputs.pid }} runs: using: composite steps: - name: Get Test Validator Latest Release id: get-test-validator-version shell: bash run: echo "version=$(./scripts/get-latest-validator-release-version.sh)" >> $GITHUB_OUTPUT - name: Cache Test Validator id: cache-test-validator uses: actions/cache@v4 with: path: .agave key: ${{ runner.os }}-test-validator-${{ steps.get-test-validator-version.outputs.version }} - name: Install Test Validator if: steps.cache-test-validator.outputs.cache-hit != 'true' shell: bash run: ./scripts/setup-test-validator.sh - name: Start Test Validator id: start-validator shell: bash run: | ./scripts/start-shared-test-validator.sh & echo "pid=$!" >> $GITHUB_OUTPUT ================================================ FILE: .github/workflows/autolock-inactive-threads.yml ================================================ name: 'Lock inactive threads' on: # Chosen to be just before London wakes up and way past San Francisco's bedtime. schedule: - cron: '0 8 * * 1-5' # This is in UTC. workflow_dispatch: permissions: issues: write pull-requests: write concurrency: group: lock jobs: autolock-inactive-threads: runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 with: github-token: ${{ github.token }} issue-inactive-days: '7' issue-lock-reason: 'resolved' issue-comment: > Because there has been no activity on this issue for 7 days since it was closed, it has been automatically locked. Please open a new issue if it requires a follow up. pr-inactive-days: '14' pr-lock-reason: 'resolved' pr-comment: > Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up. process-only: 'issues, prs' ================================================ FILE: .github/workflows/backport.yml ================================================ name: Backport on: pull_request_target: types: - closed - labeled permissions: contents: write pull-requests: write jobs: backport: name: Backport runs-on: ubuntu-latest # Only react to merged PRs for security reasons. # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. if: > github.event.pull_request.merged && ( github.event.action == 'closed' || ( github.event.action == 'labeled' && contains(github.event.label.name, 'backport') ) ) steps: - uses: tibdex/backport@9565281eda0731b1d20c4025c43339fb0a23812e with: github_token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/bundlesize.yml ================================================ name: Compare bundle size on: push: branches: - main pull_request: types: [synchronize, opened, reopened] permissions: contents: read env: # Among other things, opts out of Turborepo telemetry # See https://consoledonottrack.com/ DO_NOT_TRACK: '1' # Some tasks slow down considerably on GitHub Actions runners when concurrency is high TURBO_CONCURRENCY: 1 # Enables Turborepo Remote Caching. TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} jobs: measure-bundlesize: permissions: pull-requests: write runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Install Dependencies uses: ./.github/workflows/actions/install-dependencies with: version: current - name: Build run: pnpm turbo compile:js --concurrency=${TURBO_CONCURRENCY:-1} - name: BundleMon uses: lironer/bundlemon-action@cadbdd58f86faf1900725ef69d455444124b3748 env: # Always compare to the main branch; prevents stacked PRs from being # compared to the wrong mergebase (ie. the PR beneath the current one) CI_TARGET_BRANCH: main ================================================ FILE: .github/workflows/codeql.yml ================================================ # For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL Advanced" on: push: branches: - "main" schedule: - cron: "30 13 * * 0" jobs: analyze: name: Analyze (${{ matrix.language }}) # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false matrix: include: - language: actions build-mode: none - language: javascript-typescript build-mode: none # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository uses: actions/checkout@v4 # Add any setup steps before running the `github/codeql-action/init` action. # This includes steps like installing compilers or runtimes (`actions/setup-node` # or others). This is typically only required for manual builds. # - name: Setup runtime (example) # uses: actions/setup-example@v1 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # If the analyze step fails for one of the languages you are analyzing with # "We were unable to automatically build your code", modify the matrix above # to set the build mode to "manual" for that language. Then modify this step # to build your code. # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - name: Run manual build steps if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ 'languages you are analyzing, replace this with the commands to build' \ 'your code, for example:' echo ' make bootstrap' echo ' make release' exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" ================================================ FILE: .github/workflows/deploy-docs.yml ================================================ name: Deploy Documentation on: workflow_dispatch: branches: - main push: branches: - main permissions: contents: read pull-requests: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true env: # Among other things, opts out of Turborepo telemetry # See https://consoledonottrack.com/ DO_NOT_TRACK: '1' NEXT_TELEMETRY_DISABLED: '1' VERCEL_TELEMETRY_DISABLED: '1' # Enables Turborepo Remote Caching. TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} jobs: deploy-docs: runs-on: ubuntu-latest name: Deploy Documentation steps: - name: Checkout uses: actions/checkout@v4 - name: Install Dependencies uses: ./.github/workflows/actions/install-dependencies - name: Install Isolated Docs Dependencies working-directory: ./docs/ shell: bash run: pnpm install --ignore-workspace - name: Install Vercel CLI run: pnpm install -g vercel - name: Deploy to Vercel shell: bash env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_DOCS_PROJECT_ID }} VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: | vercel pull --token="$VERCEL_TOKEN" --yes --environment=production vercel build --token="$VERCEL_TOKEN" --prod vercel deploy --token="$VERCEL_TOKEN" --archive=tgz --prebuilt --prod - name: Synchronize InKeep Search Index uses: inkeep/pr-commenter-action@84ccc7c74b72f628ec7e2b572e0cb7afd5898594 # v10 env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} with: apiKey: ${{secrets.INKEEP_SEARCH_MANAGEMENT_API_KEY}} sourceId: '${{vars.INKEEP_SEARCH_SOURCE_ID}}' ================================================ FILE: .github/workflows/dismiss-stale-pr-reviews.yml ================================================ name: Dismiss Stale PR Reviews on: pull_request_target: types: [opened, synchronize, reopened] permissions: actions: read contents: read pull-requests: write jobs: dismiss-stale-approvals: runs-on: ubuntu-latest steps: - name: Dismiss Stale PR Reviews if: ${{ !contains('OWNER,MEMBER,COLLABORATOR', github.event.pull_request.author_association) }} uses: withgraphite/dismiss-stale-approvals@15571275d9bf7410e51244e259cd8f9f554879c8 with: github-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/label-actions.yml ================================================ name: 'Issue Label Actions' on: issues: types: [labeled, unlabeled] permissions: contents: read issues: write jobs: label: runs-on: ubuntu-latest steps: - uses: dessant/label-actions@102faf474a544be75fbaf4df54e73d3c515a0e65 ================================================ FILE: .github/workflows/manage-stale-threads.yml ================================================ name: 'Manage stale issues and PRs' on: # Chosen to be just before London wakes up and way past San Francisco's bedtime. schedule: - cron: '0 8 * * 1-5' # This is in UTC. # Do a dry-run (debug-only: true) whenever this workflow itself is changed. pull_request: paths: - .github/workflows/manage-stale-threads.yml types: - opened - synchronize permissions: issues: write pull-requests: write jobs: manage-stale-threads: runs-on: ubuntu-latest steps: - uses: actions/stale@v6 with: ascending: true # Spend API operations budget on older, more-likely-to-get-closed issues first close-issue-message: '' # Leave no comment when closing close-pr-message: '' # Leave no comment when closing days-before-issue-stale: 365 days-before-pr-stale: 14 days-before-close: 7 debug-only: ${{ github.event_name == 'pull_request' }} # Dry-run when true. exempt-all-milestones: true # Milestones can sometimes last a month, so exempt issues attached to a milestone. exempt-issue-labels: blocked,do-not-close,feature-gate,security exempt-pr-labels: blocked,do-not-close,feature-gate,security # No actual changes get made in debug-only mode, so we can raise the operations ceiling. operations-per-run: ${{ github.event_name == 'pull_request' && 1000 || 900}} stale-issue-label: stale stale-issue-message: '' # Leave no comment when marking as stale stale-pr-label: stale stale-pr-message: '' # Leave no comment when marking as stale ================================================ FILE: .github/workflows/preview-docs.yml ================================================ name: Preview Documentation on: pull_request: merge_group: permissions: contents: read env: # Among other things, opts out of Turborepo telemetry # See https://consoledonottrack.com/ DO_NOT_TRACK: '1' NEXT_TELEMETRY_DISABLED: '1' VERCEL_TELEMETRY_DISABLED: '1' # Some tasks slow down considerably on GitHub Actions runners when concurrency is high TURBO_CONCURRENCY: 1 # Enables Turborepo Remote Caching. TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} jobs: preview-docs: permissions: pull-requests: write runs-on: ubuntu-latest name: Build Documentation Preview steps: - name: Checkout uses: actions/checkout@v4 - name: Install Dependencies uses: ./.github/workflows/actions/install-dependencies - name: Install Isolated Docs Dependencies working-directory: ./docs/ shell: bash run: pnpm install --ignore-workspace - name: Install Vercel CLI run: pnpm install -g vercel - name: Deploy to Vercel if: github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' shell: bash id: vercel_deploy env: BRANCH_NAME: ${{ github.head_ref }} PR_NUMBER: ${{ github.event.pull_request.number }} VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_DOCS_PROJECT_ID }} VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} run: | vercel pull --token="$VERCEL_TOKEN" --yes --environment=preview vercel build --token="$VERCEL_TOKEN" --target=preview DEPLOY_OUTPUT=$(vercel deploy --token="$VERCEL_TOKEN" --archive=tgz --env GITHUB_PR_NUMBER="$PR_NUMBER" --env GITHUB_PR_BRANCH="$BRANCH_NAME" --prebuilt --target=preview 2>&1) DEPLOY_EXIT_CODE=$? if [ $DEPLOY_EXIT_CODE -ne 0 ]; then echo "Vercel deploy failed:" echo "$DEPLOY_OUTPUT" exit $DEPLOY_EXIT_CODE fi DEPLOY_URL=$(echo "$DEPLOY_OUTPUT" | grep -o 'https://[a-zA-Z0-9.-]*\.vercel\.app' | tail -1) echo "preview_url=$DEPLOY_URL" >> $GITHUB_OUTPUT - name: Comment on PR with Preview URL if: steps.vercel_deploy.outcome == 'success' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} PREVIEW_URL: '${{ steps.vercel_deploy.outputs.preview_url }}' run: | gh pr comment $PR_NUMBER --body "Documentation Preview: $PREVIEW_URL" --create-if-none --edit-last ================================================ FILE: .github/workflows/publish-packages.yml ================================================ name: Publish Packages (or Tag Release Manually upon manual dispatch) on: workflow_dispatch: branches: - main - backport/** inputs: version: description: 'Version' required: true type: string tag: description: 'Tag name' required: true type: string push: branches: - main - backport/** permissions: contents: read env: # Among other things, opts out of Turborepo telemetry # See https://consoledonottrack.com/ DO_NOT_TRACK: '1' # Enables Turborepo Remote Caching. TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} jobs: build-and-publish-to-npm: permissions: contents: write id-token: write pull-requests: write runs-on: ubuntu-latest if: github.event_name == 'push' steps: - name: Checkout uses: actions/checkout@v4 - name: Install Dependencies uses: ./.github/workflows/actions/install-dependencies - name: Setup Solana Test Validator id: start-test-validator uses: ./.github/workflows/actions/setup-validator - name: Build And Test (bypass cache) run: pnpm build --force - name: Create Changesets Pull Request or Publish to NPM id: changesets uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3 with: publish: pnpm changeset:publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PUBLISH_TAG: latest # Some tasks slow down considerably on GitHub Actions runners when concurrency is high TURBO_CONCURRENCY: 1 - name: Create GitHub Release if: steps.changesets.outputs.published == 'true' run: | cd ./packages/build-scripts/ ./create-github-release.ts env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Stop Test Validator if: always() && steps.start-test-validator.outcome == 'success' run: kill ${{ steps.start-test-validator.outputs.pid }} build-and-publish-snapshots-to-npm: permissions: id-token: write pull-requests: write runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/main' steps: - name: Checkout uses: actions/checkout@v4 - name: Install Dependencies uses: ./.github/workflows/actions/install-dependencies - name: Setup Solana Test Validator id: start-test-validator uses: ./.github/workflows/actions/setup-validator - name: Run Build Step run: pnpm build - name: Publish Canary Releases run: | if [ -n "$(find .changeset -name '*.md')" ]; then pnpm changeset version --snapshot canary pnpm changeset:publish else echo "No changesets found, skipping canary publish" fi env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} PUBLISH_TAG: canary - name: Stop Test Validator if: always() && steps.start-test-validator.outcome == 'success' run: kill ${{ steps.start-test-validator.outputs.pid }} validate-version-exists: permissions: contents: read runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' steps: - name: Checkout uses: actions/checkout@v4 - name: Install Dependencies uses: ./.github/workflows/actions/install-dependencies - name: Validate that a package at the given version has already been published run: | echo '${{ github.event.inputs.version }}' | grep -E "^([0-9]+\.){0,2}[0-9]+$" > /dev/null || echo 'Version must be of the form X.Y.Z' pnpm view '@solana/kit@${{ github.event.inputs.version }}' tag-release-manually: permissions: contents: write id-token: write needs: validate-version-exists runs-on: ubuntu-latest if: github.event_name == 'workflow_dispatch' steps: - name: Checkout uses: actions/checkout@v4 - name: Install Dependencies uses: ./.github/workflows/actions/install-dependencies - name: Tag Release Manually run: ./packages/build-scripts/tag-release-manually.ts --version '${{ github.event.inputs.version }}' --tag '${{ github.event.inputs.tag }}' ================================================ FILE: .github/workflows/pull-requests.yml ================================================ name: Pull requests on: pull_request: merge_group: permissions: contents: read env: # Among other things, opts out of Turborepo telemetry # See https://consoledonottrack.com/ DO_NOT_TRACK: '1' # Some tasks slow down considerably on GitHub Actions runners when concurrency is high TURBO_CONCURRENCY: 1 # Enables Turborepo Remote Caching. TURBO_REMOTE_CACHE_SIGNATURE_KEY: ${{ secrets.TURBO_REMOTE_CACHE_SIGNATURE_KEY }} TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} TURBO_TEAM: ${{ vars.TURBO_TEAM }} jobs: # Needed for grouping check-web3 strategies into one check all-web3-checks: runs-on: ubuntu-latest needs: build-and-test steps: - run: echo "Done" build-and-test: runs-on: ubuntu-latest strategy: matrix: node: - 'current' - 'lts/*' name: Build & Test on Node ${{ matrix.node }} steps: - name: Checkout uses: actions/checkout@v4 - name: Install Dependencies uses: ./.github/workflows/actions/install-dependencies with: version: ${{ matrix.node }} - name: Setup Solana Test Validator id: start-test-validator uses: ./.github/workflows/actions/setup-validator - name: Build & Test run: pnpm build # Don't add --concurrency here; it's already baked in - name: Stop Test Validator if: always() && steps.start-test-validator.outcome == 'success' run: kill ${{ steps.start-test-validator.outputs.pid }} dependabot: runs-on: ubuntu-latest if: github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' && github.repository == 'anza-xyz/kit' needs: [build-and-test] permissions: contents: write pull-requests: write steps: - name: Auto-approve the PR run: gh pr review --approve "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Enable auto-merge run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/update-docs-lockfile.yml ================================================ name: Update Docs Lockfile on: pull_request_target: paths: - 'docs/package.json' permissions: contents: write jobs: update-lockfile: runs-on: ubuntu-latest permissions: id-token: write if: github.event.pull_request.user.login == 'dependabot[bot]' steps: - uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.PRIVATE_KEY }} - name: Set git config run: | git config --global user.email "github-actions@github.com" git config --global user.name "github-actions" git config --global url."https://x-access-token:${GITHUB_TOKEN}@github.com/".insteadOf https://github.com/ env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} - name: Checkout PR branch uses: actions/checkout@v6 with: token: ${{ steps.app-token.outputs.token }} ref: ${{ github.event.pull_request.head.ref }} persist-credentials: false - name: Install Dependencies uses: ./.github/workflows/actions/install-dependencies - name: Update Docs Lockfile working-directory: ./docs/ run: pnpm install --ignore-workspace --no-frozen-lockfile --ignore-scripts - name: Commit and Push run: | git add docs/pnpm-lock.yaml if git diff --cached --quiet; then echo "No lockfile changes to commit" else git commit -m "Update docs lockfile" git push fi ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies node_modules .pnp .pnp.js # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # turbo .turbo # Sapling SCM .sl # `solana-test-validator` .agave/ test-ledger/ # ignore JetBrains IDE .idea ================================================ FILE: .npmrc ================================================ auto-install-peers=true engine-strict=true workspaces-update=false ================================================ FILE: .prettierignore ================================================ .changeset/ .github/**.md # These are autogenerated, so leave them alone **/CHANGELOG.md declarations/ dist/ doc/ lib/ !docs/src/lib pnpm-lock.yaml pnpm-workspace.yaml ================================================ FILE: .skills-inject.json ================================================ { "targets": [ "CLAUDE.md" ], "skillsDirs": [ ".agents/skills", ".claude/skills" ] } ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "esbenp.prettier-vscode" ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.defaultFormatter": "esbenp.prettier-vscode" } ================================================ FILE: CLAUDE.md ================================================ # Agent Instructions ## Project Overview Kit (`@solana/kit`) is the official JavaScript SDK for building Solana applications, maintained by Anza. The official documentation lives at https://solanakit.com (docs at `/docs`, API reference at `/api`). The GitHub repo is `anza-xyz/kit`. This is a pnpm monorepo with ~57 packages under `packages/`, orchestrated by Turborepo. The main `@solana/kit` package is a facade that re-exports ~20 workspace packages (accounts, addresses, codecs, errors, keys, rpc, signers, transactions, etc.) and adds its own convenience helpers like `sendAndConfirmTransaction`, `airdrop`, and `fetchLookupTables`. The documentation website supports an `.mdx` suffix on any page (e.g. `https://solanakit.com/docs/concepts/transactions.mdx`) which returns a cleaner markdown representation suitable for LLM consumption. Use this when you need to look up Kit APIs or concepts. There is also a companion [`anza-xyz/kit-plugins`](https://github.com/anza-xyz/kit-plugins) monorepo that builds an abstraction layer on top of Kit by offering composable plugins and ready-to-use clients (which are essentially curated sets of plugins). The plugin system relies on the `@solana/plugin-core` and `@solana/plugin-interfaces` packages defined in this repo. Additionally, `@solana/program-client-core` (re-exported from `@solana/kit/program-client-core`) provides the helpers used by the [Codama JS renderer](https://github.com/codama-idl/renderers-js) to generate Kit-compatible program clients. ## Getting Started ```shell pnpm install # Install dependencies pnpm test:setup # Install the Agave test validator (first time only) ./scripts/start-shared-test-validator.sh # Start shared test validator (needed for some tests) pnpm build # Build + test all packages ``` For single-package development: ```shell cd packages/ pnpm turbo compile:js compile:typedefs # Compile the package and its dependencies pnpm dev # Run tests in watch mode ``` ## Architecture The packages form a layered dependency graph. `@solana/errors` is the foundational leaf package with no internal dependencies — nearly every other package depends on it. Core encoding lives in `@solana/codecs-core`, `@solana/codecs-numbers`, `@solana/codecs-strings`, and `@solana/codecs-data-structures`, unified by the `@solana/codecs` facade. `@solana/keys` and `@solana/addresses` build on these codecs and `@solana/nominal-types`. Higher-level packages like `@solana/transactions`, `@solana/rpc`, and `@solana/signers` compose these primitives. `@solana/kit` sits at the top as the consumer-facing umbrella. Four private "impl" packages (`@solana/crypto-impl`, `@solana/text-encoding-impl`, `@solana/ws-impl`, `@solana/event-target-impl`) provide platform-specific implementations and are **inlined at build time** by tsup — they are never published to npm. ## Build System - **JS compilation**: tsup (esbuild) via configs in `packages/build-scripts/`. - **Type declarations**: tsc with per-package `tsconfig.declarations.json`. - **Build artifacts per package**: Node CJS + ESM, Browser CJS + ESM, React Native ESM. `@solana/kit` additionally produces IIFE bundles (minified production + development with sourcemaps). - **Platform constants** (set at build time): `__BROWSER__`, `__NODEJS__`, `__REACTNATIVE__`, `__VERSION__`. These are mutually exclusive booleans enabling platform-specific code paths to be tree-shaken. - **`__DEV__`**: In IIFE builds, statically `true`/`false`. In library builds, replaced with `process.env.NODE_ENV !== 'production'` by the DevFlagPlugin, deferring the decision to the consumer's bundler. ## Testing - **Framework**: Jest v30 with `@swc/jest` for TypeScript transformation. - **Shared config**: `packages/test-config/` (`@solana/test-config`). - **File naming**: `*-test.ts` (runs in both environments), `*-test.node.ts` (Node only), `*-test.browser.ts` (browser/jsdom only). Tests live in `src/__tests__/`. - **Type tests**: `src/__typetests__/` directories contain compile-time type tests using `satisfies` and `@ts-expect-error`. - **Lint/prettier**: Also run through Jest runners (`jest-runner-eslint`, `jest-runner-prettier`). - **Commands**: `pnpm test` runs all unit tests. `pnpm lint` runs lint checks. `pnpm style:fix` auto-fixes formatting. - **`expect.assertions`**: Only use `expect.assertions(n)` in **async** tests (where you need to guarantee the expected number of assertions ran). Synchronous tests do not need it. - **Flushing async state**: When a test needs to wait for queued microtasks or promise chains to settle, prefer `jest.useFakeTimers()` + `await jest.runAllTimersAsync()` over hand-rolled `flushMicrotasks` helpers that `await Promise.resolve()` in a loop. The loop count is fragile and breaks as soon as an extra `.then` is introduced. When enabling fake timers in a scoped `beforeEach` (i.e. not at the top of the file), pair it with an `afterEach(() => { jest.useRealTimers(); })` so subsequent describes don't inherit fake timers. - **Placeholder mocks**: When a test mock must satisfy an interface but a particular method shouldn't be called in that test, make the stub throw/reject rather than using a bare `jest.fn()` that silently returns `undefined`. For sync methods use `jest.fn().mockImplementation(() => { throw new Error('not implemented'); })`; for async methods use `jest.fn().mockRejectedValue(new Error('not implemented'))`. An accidental call then fails the test loudly instead of producing `undefined` and a confusing downstream assertion error. ## Error System All errors use the `SolanaError` class from `@solana/errors`. Key rules: - Error codes are numeric constants in `packages/errors/src/codes.ts`, named `SOLANA_ERROR____` (double-underscore separated). - Each domain reserves a dedicated numeric range. Never reuse, remove, or reorder error codes. - Human-readable messages live in `packages/errors/src/messages.ts` with `$key` context interpolation. - In production builds, messages are stripped. Use `npx @solana/errors decode -- ` to decode them. - To add a new error: add the constant to `codes.ts`, add it to the `SolanaErrorCode` union, optionally define context in `context.ts`, and add the message to `messages.ts`. ## Coding Conventions - **TypeScript strict mode**, ESM-first. - **Branded/nominal types**: `@solana/nominal-types` provides `Brand` for types like `Address`, `Signature`, `Lamports` — these are branded primitives, not classes. - **Functional composition**: Use `pipe()` from `@solana/functional` to compose operations on transaction messages and other data structures. - **Platform-specific code**: Use `__BROWSER__`, `__NODEJS__`, `__REACTNATIVE__` guards. The build system will tree-shake unused branches. - **Dev-only code**: Guard with `__DEV__` (e.g. verbose error messages, debug assertions). - **Formatting**: ESLint via `@solana/eslint-config-solana`, Prettier via `@solana/prettier-config-solana`. Run `pnpm style:fix` to auto-fix. - **All publishable packages share a fixed version** (currently in lockstep). - **Deferred promises**: Use `Promise.withResolvers()` instead of hand-rolling a `new Promise((resolve, reject) => ...)` with captured externals. Do not reintroduce a `deferred()` helper — `Promise.withResolvers` already returns `{ promise, resolve, reject }`. ## Changesets & Releases - Use `npx changeset add --empty` to generate changeset files — never create them manually. - `patch` for fixes, `minor` for features, `major` for breaking changes (while pre-1.0, `minor` may be treated as breaking). - No changeset needed for docs-only changes, CI config, dev dependency updates, or test-only changes. - All publishable packages are version-locked via the `fixed` config in `.changeset/config.json`. ## CI - **PRs**: Build + test on Node `current` and `lts/*`. - **Bundle size**: Monitored via BundleMon on every PR. - **Publishing**: On merge to `main`, changesets action creates a "Version Packages" PR or publishes to npm. Canary snapshots are published on every push to `main`. ## Skill Instructions The following instructions come from installed skills (autogenerated, do not edit manually) and should always be followed. ### Changesets - Any PR that should trigger a package release MUST include a changeset. - Identify affected packages by mapping changed files to their nearest `package.json`. - Choose the right bump: `patch` for fixes, `minor` for features, `major` for breaking changes. - While a project is pre-1.0, `minor` bumps may be treated as breaking. - ALWAYS use `npx changeset add --empty` to generate a new changeset file with a random name. NEVER create changeset files manually. - No changeset needed for: docs-only changes, CI config, dev dependency updates, test-only changes. ### Shipping (Git) - NEVER commit, push, create branches, or create PRs without explicit user approval. - Before any git operation that creates or modifies a commit, present a review block containing: changeset entry (if applicable), commit title, and commit/PR description. ALWAYS wait for approval. - Use standard `git add` and `git commit` workflows. Concise title on the first line, blank line, then description body. - Use `gh pr create` for pull requests. - Write commit and PR descriptions as natural flowing prose. Do NOT insert hard line breaks mid-paragraph. ### Shipping (Graphite) - Check if [Graphite](https://graphite.dev/) is installed (`which gt`). Prefer Graphite when available; fall back to the Git shipping workflow otherwise. - NEVER commit, push, create branches, or create PRs without explicit user approval. - Before any git operation that creates or modifies a commit, present a review block containing: changeset entry (if applicable), commit title, and commit/PR description. ALWAYS wait for approval. - Use `gt create -am "Title" -m "Description body"` for new PRs. The first `-m` sets the commit title; the second sets the PR description. - Use `gt modify -a` to amend the current branch with follow-up changes (NEVER create additional commits on the same branch). - ALWAYS escape backticks in commit messages with backslashes for shell compatibility (e.g. `"Update \`my-package\` config"`). - Do NOT run `gt submit` after committing. Only run it when the user explicitly asks to submit or push. - Write commit and PR descriptions as natural flowing prose. Do NOT insert hard line breaks mid-paragraph. ### TypeScript Docblocks - All exported functions, types, interfaces, and constants MUST have JSDoc docblocks. - Start with `/**`, use `*` prefix for each line, end with `*/` — each on its own line. - Begin with a clear one-to-two line summary. Add a blank line before tags. - Include `@param`, `@typeParam`, `@return`, `@throws`, and at least one `@example` when helpful. - Use `{@link ...}` to reference related items. Add `@see` tags at the end for related APIs. ### TypeScript READMEs - When adding a new public API, add or update the package's README. - Structure: brief intro, installation, usage (with code snippet), deep-dive sections. - Code snippets must be realistic, concise, and use TypeScript syntax. - Focus on the quickest path to success. Developers should feel excited, not overwhelmed. ================================================ FILE: CONTRIBUTING.md ================================================ Kit is developed in public and we encourage and appreciate contributions. ## Getting Started 1. Install dependencies: `pnpm install` 2. The first time you build Kit, you will need to install the Agave test validator, which is used for some tests. `pnpm test:setup` 3. Start a test validator before running tests. `./scripts/start-shared-test-validator.sh` 4. Build + test all packages: `pnpm build` ## Development Environment Kit is developed as a monorepo using [pnpm](https://pnpm.io/) and [turborepo](https://turborepo.com/). Often your changes will only apply to a single package. You can run tests for a single package and watch for changes: ```shell cd packages/accounts pnpm turbo compile:js compile:typedefs pnpm dev ``` ## A Note on AI-Generated Code We are supportive of using AI tools to _assist_ your development process (e.g., for boilerplate, optimization suggestions, or debugging). However, we do not accept "vibe-coded" or purely AI-generated contributions. You must be able to explain, test, and take full ownership of every line of code you submit. A good rule of thumb is not to use AI to write the PR description. This tends to be less clear and harder to review. **Pull requests containing code that the author clearly does not understand will be rejected.** You are the developer, not the prompt engineer. All code must be intentional and understood. ================================================ FILE: LICENSE ================================================ Copyright (c) 2023 Solana Labs, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ [![npm][npm-image]][npm-url] [![npm-downloads][npm-downloads-image]][npm-url]
[![code-style-prettier][code-style-prettier-image]][code-style-prettier-url] [code-style-prettier-image]: https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square [code-style-prettier-url]: https://github.com/prettier/prettier [npm-downloads-image]: https://img.shields.io/npm/dm/@solana/kit?style=flat [npm-image]: https://img.shields.io/npm/v/@solana/kit?style=flat [npm-url]: https://www.npmjs.com/package/@solana/kit # Kit This is the JavaScript SDK for building Solana apps for Node, web, and React Native. > [!NOTE] > Did you expect to find `@solana/web3.js` here? You're in the right place! We have renamed the 2.x line of `@solana/web3.js` to `@solana/kit`. > > The code for the 1.x line of `@solana/web3.js` can be found [here](https://github.com/solana-labs/solana-web3.js/tree/maintenance/v1.x) and the documentation [here](https://solana-foundation.github.io/solana-web3.js/). # Installation For use in a Node.js or web application: ```shell npm install --save @solana/kit ``` For use in a browser, without a build system: ```html ``` # Quick Start To get a feel for the API, run and modify the live examples in the `examples/` directory. There, you will find a series of single-purpose Node scripts that demonstrate a specific feature or use case. You will also find a React application that you can run in a browser, that demonstrates being able to create, sign, and send transactions using browser wallets. For a fully baked intro, see: [Getting started with Solana kit](https://www.solanakit.com/docs/getting-started) # What's New in Kit Kit is a response to many of the pain points you have communicated to us when developing Solana applications with web3.js. ## Tree-Shakability The object-oriented design of the web3.js (1.x) API prevents optimizing compilers from being able to ‘tree-shake’ unused code from your production builds. No matter how much of the web3.js API you use in your application, you have until now been forced to package all of it. Read more about tree-shaking here: - [Mozilla Developer Docs: Tree Shaking](https://developer.mozilla.org/en-US/docs/Glossary/Tree_shaking) - [WebPack Docs: Tree Shaking](https://webpack.js.org/guides/tree-shaking/) - [Web.Dev Blog Article: Reduce JavaScript Payloads with Tree Shaking](https://web.dev/articles/reduce-javascript-payloads-with-tree-shaking) One example of an API that can’t be tree-shaken is the `Connection` class. It has dozens of methods, but because it’s a _class_ you have no choice but to include every method in your application’s final bundle, no matter how many you _actually_ use. Needlessly large JavaScript bundles can cause issues with deployments to cloud compute providers like Cloudflare or AWS Lambda. They also impact webapp startup performance because of longer download and JavaScript parse times. Kit is fully tree-shakable and will remain so, enforced by build-time checks. Optimizing compilers can now eliminate those parts of the library that your application does not use. Kit is comprised of several smaller, modular packages under the `@solana` organization, including: - `@solana/accounts`: For fetching and decoding accounts - `@solana/codecs`: For composing data (de)serializers from a set of primitives or building custom ones - `@solana/errors`: For identifying and refining coded errors thrown in the `@solana` namespace - `@solana/rpc`: For sending RPC requests - `@solana/rpc-subscriptions`: For subscribing to RPC notifications - `@solana/signers`: For building message and/or transaction signer objects - `@solana/sysvars`: For fetching and decoding sysvar accounts - `@solana/transaction-messages`: For building and transforming Solana transaction message objects - `@solana/transactions`: For compiling and signing transactions for submission to the network - And many more! Some of these packages are themselves composed of smaller packages. For instance, `@solana/rpc` is composed of `@solana/rpc-spec` (for core JSON RPC specification types), `@solana/rpc-api` (for the Solana-specific RPC methods), `@solana/rpc-transport-http` (for the default HTTP transport) and so on. Developers can use the default configurations within the main library (`@solana/kit`) or import any of its subpackages where customization-through-composition is desired. ## Composable Internals Depending on your use case and your tolerance for certain application behaviours, you may wish to configure your application to make a different set of tradeoffs than another developer. The web3.js (1.x) API imposed a rigid set of common-case defaults on _all_ developers, some of which were impossible to change. The inability to customize web3.js up until now has been a source of frustration: - The Mango team wanted to customize the transaction confirmation strategy, but all of that functionality is hidden away behind `confirmTransaction` – a static method of `Connection`. [Here’s the code for `confirmTransaction` on GitHub](https://github.com/solana-labs/solana-web3.js/blob/69a8ad25ef09f9e6d5bff1ffa8428d9be0bd32ac/packages/library-legacy/src/connection.ts#L3734). - Solana developer ‘mPaella’ [wanted us to add a feature in the RPC](https://github.com/solana-labs/solana-web3.js/issues/1143#issuecomment-1435927152) that would failover to a set of backup URLs in case the primary one failed. - Solana developer ‘epicfaace’ wanted first-class support for automatic time-windowed batching in the RPC transport. [Here’s their pull request](https://github.com/solana-labs/solana/pull/23628). - Multiple folks have expressed the need for custom retry logic for failed requests or transactions. [Here’s a pull request from ‘dafyddd’](https://github.com/solana-labs/solana/pull/11811) and [another from ‘abrkn’](https://github.com/solana-labs/solana-web3.js/issues/1041) attempting to modify retry logic to suit their individual use cases. Kit exposes far more of its internals, particularly where communication with an RPC is concerned, and allows willing developers the ability to compose new implementations from the default ones that manifest a nearly limitless array of customizations. The individual modules that make up Kit are assembled in a **default** configuration reminiscent of the legacy library as part of the npm package `@solana/kit`, but those who wish to assemble them in different configurations may do so. Generic types are offered in numerous places, allowing you to specify new functionality, to make extensions to each API via composition and supertypes, and to encourage you to create higher-level opinionated abstractions of your own. In fact, we expect you to do so, and to open source some of those for use by others with similar needs. ## Modern JavaScript; Zero-Dependency The advance of modern JavaScript features presents an opportunity to developers of crypto applications, such as the ability to use native Ed25519 keys and to express large values as native `bigint`. The Web Incubator Community Group has advocated for the addition of Ed25519 support to the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API), and support has already landed in _most_ modern JavaScript runtimes. Engine support for `bigint` values has also become commonplace. The older `number` primitive in JavaScript has a maximum value of 2^53 - 1, whereas Rust’s `u64` can represent values up to 2^64. Kit eliminates userspace implementations of Ed25519 cryptography, large number polyfills, and more, in favour of custom implementations or the use of native JavaScript features, reducing the size of the library. It has no third-party dependencies. ## Functional Architecture The object oriented, class-based architecture of web3.js (1.x) causes unnecessary bundle bloat. Your application has no choice but to bundle _all_ of the functionality and dependencies of a class no matter how many methods you actually use at runtime. Class-based architecture also presents unique risks to developers who trigger the dual-package hazard. This describes a situation you can find yourself in if you build for both CommonJS and ES modules. It arises when two copies of the same class are present in the dependency tree, causing checks like `instanceof` to fail. This introduces aggravating and difficult to debug problems. Read more about dual-package hazard: - [NodeJS: Dual Package Hazard](https://nodejs.org/api/packages.html#dual-package-hazard) Kit implements no classes (with the notable exception of the `SolanaError` class) and implements the thinnest possible interfaces at function boundaries. ## Statistics Consider these statistical comparisons between Kit and the legacy web3.js 1.x. | | 1.x (Legacy) | Kit | +/- % | | ------------------------------------------------------------------------------------------------------ | ------------ | ---------- | ----- | | Total minified size of library | 81 KB | 57.5 KB | -29% | | Total minified size of library (when runtime supports Ed25519) | 81 KB | 53 KB | -33% | | Bundled size of a web application that executes a transfer of lamports | 111 KB | 23.9 KB | -78% | | Bundled size of a web application that executes a transfer of lamports (when runtime supports Ed25519) | 111 KB | 18.2 KB | -83% | | Performance of key generation, signing, and verifying signatures (Brave with Experimental API flag) | 700 ops/s | 7000 ops/s | +900% | | First-load size for Solana Explorer | 311 KB | 228 KB | -26% | The re-engineered library achieves these speedups and reductions in bundle size in large part through use of modern JavaScript APIs. To validate our work, we replaced the legacy 1.x library with Kit on the homepage of the Solana Explorer. Total first-load bundle size dropped by 26% without removing a single feature. [Here’s an X thread](https://twitter.com/callum_codes/status/1679124485218226176) by Callum McIntyre if you would like to dig deeper. # A Tour of the Kit API Here’s an overview of how to use the new library to interact with the RPC, configure network transports, work with Ed25519 keys, and to serialize data. ## RPC Kit ships with an implementation of the [JSON RPC specification](https://www.jsonrpc.org/specification) and a type spec for the [Solana JSON RPC](https://solana.com/docs/rpc). The main package responsible for managing communication with an RPC is `@solana/rpc`. However, this package makes use of more granular packages to break down the RPC logic into smaller pieces. Namely, these packages are: - `@solana/rpc`: Contains all logic related to sending Solana RPC calls. - `@solana/rpc-api`: Describes all Solana RPC methods using types. - `@solana/rpc-transport-http`: Provides a concrete implementation of an RPC transport using HTTP requests. - `@solana/rpc-spec`: Defines the JSON RPC spec for sending RPC requests. - `@solana/rpc-spec-types`: Shared JSON RPC specifications types and helpers that are used by both `@solana/rpc` and `@solana/rpc-subscriptions` (described in the next section). - `@solana/rpc-types`: Shared Solana RPC types and helpers that are used by both `@solana/rpc` and `@solana/rpc-subscriptions`. The main `@solana/kit` package re-exports the `@solana/rpc` package so, going forward, we will import RPC types and functions from the library directly. ### RPC Calls You can use the `createSolanaRpc` function by providing the URL of a Solana JSON RPC server. This will create a default client for interacting with the Solana JSON RPC API. ```ts import { createSolanaRpc } from '@solana/kit'; // Create an RPC client. const rpc = createSolanaRpc('http://127.0.0.1:8899'); // ^? Rpc // Send a request. const slot = await rpc.getSlot().send(); ``` ### Custom RPC Transports The `createSolanaRpc` function communicates with the RPC server using a default HTTP transport that should satisfy most use cases. You can provide your own transport or wrap an existing one to communicate with RPC servers in any way you see fit. In the example below, we explicitly create a transport and use it to create a new RPC client via the `createSolanaRpcFromTransport` function. ```ts import { createSolanaRpcFromTransport, createDefaultRpcTransport } from '@solana/kit'; // Create an HTTP transport or any custom transport of your choice. const transport = createDefaultRpcTransport({ url: 'https://api.devnet.solana.com' }); // Create an RPC client using that transport. const rpc = createSolanaRpcFromTransport(transport); // ^? Rpc // Send a request. const slot = await rpc.getSlot().send(); ``` A custom transport can implement specialized functionality such as coordinating multiple transports, implementing retries, and more. Let's take a look at some concrete examples. #### Round Robin A ‘round robin’ transport is one that distributes requests to a list of endpoints in sequence. ```ts import { createDefaultRpcTransport, createSolanaRpcFromTransport, type RpcTransport } from '@solana/kit'; // Create an HTTP transport for each RPC server. const transports = [ createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-1.com' }), createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-2.com' }), createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-3.com' }), ]; // Set up the round-robin transport. let nextTransport = 0; async function roundRobinTransport(...args: Parameters): Promise { const transport = transports[nextTransport]; nextTransport = (nextTransport + 1) % transports.length; return await transport(...args); } // Create an RPC client using the round-robin transport. const rpc = createSolanaRpcFromTransport(roundRobinTransport); ``` #### Sharding A sharding transport is a kind of distributing transport that sends requests to a particular server based on something about the request itself. Here’s an example that sends requests to different servers depending on the name of the method: ```ts import { createDefaultRpcTransport, createSolanaRpcFromTransport, type RpcTransport } from '@solana/kit'; // Create multiple transports. const transportA = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-1.com' }); const transportB = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-2.com' }); const transportC = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-3.com' }); const transportD = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-4.com' }); // Function to determine which shard to use based on the request method. function selectShard(method: string): RpcTransport { switch (method) { case 'getAccountInfo': case 'getBalance': return transportA; case 'getLatestBlockhash': case 'getTransaction': return transportB; case 'sendTransaction': return transportC; default: return transportD; } } // Create a transport that selects the correct transport given the request method name. async function shardingTransport(...args: Parameters): Promise { const payload = args[0].payload as { method: string }; const selectedTransport = selectShard(payload.method); return (await selectedTransport(...args)) as TResponse; } // Create an RPC client using the sharding transport. const rpc = createSolanaRpcFromTransport(shardingTransport); ``` #### Retry A custom transport is a good place to implement global retry logic for every request: ```ts import { createDefaultRpcTransport, createSolanaRpcFromTransport, type RpcTransport } from '@solana/kit'; // Set the maximum number of attempts to retry a request. const MAX_ATTEMPTS = 4; // Create the default transport. const defaultTransport = createDefaultRpcTransport({ url: 'https://mainnet-beta.my-server-1.com' }); // Sleep function to wait for a given number of milliseconds. function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } // Calculate the delay for a given attempt. function calculateRetryDelay(attempt: number): number { // Exponential backoff with a maximum of 1.5 seconds. return Math.min(100 * Math.pow(2, attempt), 1500); } // A retrying transport that will retry up to MAX_ATTEMPTS times before failing. async function retryingTransport(...args: Parameters): Promise { let requestError; for (let attempts = 0; attempts < MAX_ATTEMPTS; attempts++) { try { return await defaultTransport(...args); } catch (err) { requestError = err; // Only sleep if we have more attempts remaining. if (attempts < MAX_ATTEMPTS - 1) { const retryDelay = calculateRetryDelay(attempts); await sleep(retryDelay); } } } throw requestError; } // Create the RPC client using the retrying transport. const rpc = createSolanaRpcFromTransport(retryingTransport); ``` #### Failover Support for handling network failures can be implemented in the transport itself. Here’s an example of some failover logic integrated into a transport: ```ts import { createDefaultRpcTransport, createSolanaRpcFromTransport, type RpcTransport } from '@solana/kit'; // List of RPC endpoints for failover. const rpcEndpoints = [ 'https://mainnet-beta.my-server-1.com', 'https://mainnet-beta.my-server-2.com', 'https://mainnet-beta.my-server-3.com', 'https://mainnet-beta.my-server-3.com', ]; // Create an array of transports from the endpoints. const transports = rpcEndpoints.map(url => createDefaultRpcTransport({ url })); // A failover transport that switches to the next transport on failure. async function failoverTransport(...args: Parameters): Promise { let lastError; for (const transport of transports) { try { return await transport(...args); } catch (err) { lastError = err; console.warn(`Transport failed: ${err}. Trying next transport...`); } } // If all transports fail, throw the last error. throw lastError; } // Create the RPC client using the failover transport. const rpc = createSolanaRpcFromTransport(failoverTransport); ``` ### Augmenting/Constraining the RPC API Using the `createSolanaRpc` or `createSolanaRpcFromTransport` methods, we always get the same API that includes the Solana RPC API methods. Since the RPC API is described using types only, it is possible to augment those types to add your own methods. When constraining the API scope, keep in mind that types don’t affect bundle size. You may still like to constrain the type-spec for a variety of reasons, including reducing TypeScript noise. #### Constraining by Cluster If you're using a specific cluster, you may wrap your RPC URL inside a helper function like `mainnet` or `devnet` to inject that information into the RPC type system. ```ts import { createSolanaRpc, mainnet, devnet } from '@solana/kit'; const mainnetRpc = createSolanaRpc(mainnet('https://api.mainnet-beta.solana.com')); // ^? RpcMainnet const devnetRpc = createSolanaRpc(devnet('https://api.devnet.solana.com')); // ^? RpcDevnet ``` In the example above, `devnetRpc.requestAirdrop(..)` will work, but `mainnetRpc.requestAirdrop(..)` will raise a TypeScript error since `requestAirdrop` is not a valid method of the mainnet cluster. #### Cherry-Picking API Methods You can constrain the API’s type-spec even further so you are left only with the methods you need. The simplest way to do this is to cast the created RPC client to a type that only includes the required methods. ```ts import { createSolanaRpc, type Rpc, type GetAccountInfoApi, type GetMultipleAccountsApi } from '@solana/kit'; const rpc = createSolanaRpc('http://127.0.0.1:8899') as Rpc; ``` Alternatively, you can explicitly create the RPC API using the `createSolanaRpcApi` function. You will need to create your own transport and bind the two together using the `createRpc` function. ```ts import { createDefaultRpcTransport, createRpc, createSolanaRpcApi, DEFAULT_RPC_CONFIG, type GetAccountInfoApi, type GetMultipleAccountsApi, } from '@solana/kit'; const api = createSolanaRpcApi(DEFAULT_RPC_CONFIG); const transport = createDefaultRpcTransport({ url: 'http://127.0.0.1:8899' }); const rpc = createRpc({ api, transport }); ``` Note that the `createSolanaRpcApi` function is a wrapper on top of the `createJsonRpcApi` function which adds some Solana-specific transformers such as setting a default commitment on all methods or throwing an error when an integer overflow is detected. #### Creating Your Own API Methods The new library’s RPC specification supports an _infinite_ number of JSON-RPC methods with **zero increase** in bundle size. This means the library can support future additions to the official [Solana JSON RPC](https://docs.solana.com/api), or [custom RPC methods](https://docs.helius.dev/compression-and-das-api/digital-asset-standard-das-api/get-asset) defined by some RPC provider. Here’s an example of how a developer at might build a custom RPC type-spec for an RPC provider's implementation of the Metaplex Digital Asset Standard's `getAsset` method: ```ts // Define the method's response payload. type GetAssetApiResponse = Readonly<{ interface: DasApiAssetInterface; id: Address; content: Readonly<{ files?: readonly { mime?: string; uri?: string; [key: string]: unknown; }[]; json_uri: string; links?: readonly { [key: string]: unknown; }[]; metadata: DasApiMetadata; }>; /* ...etc... */ }>; // Set up a type spec for the request method. type GetAssetApi = { // Define the method's name, parameters and response type getAsset(args: { id: Address }): GetAssetApiResponse; }; // Export the type spec for downstream users. export type MetaplexDASApi = GetAssetApi; ``` Here’s how a developer might use it: ```ts import { createDefaultRpcTransport, createRpc, createJsonRpcApi } from '@solana/kit'; // Create the custom API. const api = createJsonRpcApi(); // Set up an HTTP transport to a server that supports the custom API. const transport = createDefaultRpcTransport({ url: 'https://mainnet.helius-rpc.com/?api-key=', }); // Create the RPC client. const metaplexDASRpc = createRpc({ api, transport }); // ^? Rpc ``` As long as a particular JSON RPC method adheres to the [official JSON RPC specification](https://www.jsonrpc.org/specification), it will be supported by Kit. ### Aborting RPC Requests RPC requests are now abortable with modern `AbortControllers`. When calling an RPC method such as `getSlot`, it will return a `PendingRpcRequest` proxy object that contains a `send` method to send the request to the server. ```ts const pendingRequest: PendingRpcRequest = rpc.getSlot(); const slot: Slot = await pendingRequest.send(); ``` The arguments of the `getSlot` method are reserved for the request payload, but the `send` method is where additional arguments such as an `AbortSignal` can be accepted in the context of the request. Aborting RPC requests can be useful for a variety of things such as setting a timeout on a request or cancelling a request when a user navigates away from a page. ```ts import { createSolanaRpc } from '@solana/kit'; const rpc = createSolanaRpc('http://127.0.0.1:8900'); // Create a new AbortController. const abortController = new AbortController(); // Abort the request when the user navigates away from the current page. function onUserNavigateAway() { abortController.abort(); } // The request will be aborted if and only if the user navigates away from the page. const slot = await rpc.getSlot().send({ abortSignal: abortController.signal }); ``` Read more about `AbortController` here: - [Mozilla Developer Docs: `AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) - [Mozilla Developer Docs: `AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) - [JavaScript.info: Fetch: Abort](https://javascript.info/fetch-abort) ## RPC Subscriptions Subscriptions in the legacy library do not allow custom retry logic and do not allow you to recover from potentially missed messages. The new version does away with silent retries, surfaces transport errors to your application, and gives you the opportunity to recover from gap events. The main package responsible for managing communication with RPC subscriptions is `@solana/rpc-subscriptions`. However, similarly to `@solana/rpc`, this package also makes use of more granular packages. These packages are: - `@solana/rpc-subscriptions`: Contains all logic related to subscribing to Solana RPC notifications. - `@solana/rpc-subscriptions-api`: Describes all Solana RPC subscriptions using types. - `@solana/rpc-subscriptions-channel-websocket`: Provides a concrete implementation of an RPC Subscriptions channel using WebSockets. - `@solana/rpc-subscriptions-spec`: Defines the JSON RPC spec for subscribing to RPC notifications. - `@solana/rpc-spec-types`: Shared JSON RPC specifications types and helpers that are used by both `@solana/rpc` and `@solana/rpc-subscriptions`. - `@solana/rpc-types`: Shared Solana RPC types and helpers that are used by both `@solana/rpc` and `@solana/rpc-subscriptions`. Since the main `@solana/kit` library also re-exports the `@solana/rpc-subscriptions` package we will import RPC Subscriptions types and functions directly from the main library going forward. ### Getting Started with RPC Subscriptions To get started with RPC Subscriptions, you may use the `createSolanaRpcSubscriptions` function by providing the WebSocket URL of a Solana JSON RPC server. This will create a default client for interacting with Solana RPC Subscriptions. ```ts import { createSolanaRpcSubscriptions } from '@solana/kit'; // Create an RPC Subscriptions client. const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900'); // ^? RpcSubscriptions ``` ### Subscriptions as `AsyncIterators` The new subscriptions API vends subscription notifications as an `AsyncIterator`. The `AsyncIterator` conforms to the [async iterator protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols), which allows developers to consume messages using a `for await...of` loop. Here’s an example of working with a subscription in the new library: ```ts import { address, createSolanaRpcSubscriptions, createDefaultRpcSubscriptionsTransport } from '@solana/kit'; // Create the RPC Subscriptions client. const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900'); // Set up an abort controller. const abortController = new AbortController(); // Subscribe to account notifications. const accountNotifications = await rpcSubscriptions .accountNotifications(address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3'), { commitment: 'confirmed' }) .subscribe({ abortSignal: abortController.signal }); try { // Consume messages. for await (const notification of accountNotifications) { console.log('New balance', notification.value.lamports); } } catch (e) { // The subscription went down. // Retry it and then recover from potentially having missed // a balance update, here (eg. by making a `getBalance()` call). } ``` You can read more about `AsyncIterator` at the following links: - [Mozilla Developer Docs: `AsyncIterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator) - [Luciano Mammino (Blog): JavaScript Async Iterators](https://www.nodejsdesignpatterns.com/blog/javascript-async-iterators/) ### Aborting RPC Subscriptions Similarly to RPC calls, applications can terminate active subscriptions using an `AbortController` attribute on the `subscribe` method. In fact, this parameter is _required_ for subscriptions to encourage you to clean up subscriptions that your application no longer needs. Let's take a look at some concrete examples that demonstrate how to abort subscriptions. #### Subscription Timeout Here's an example of an `AbortController` used to abort a subscription after a 5-second timeout: ```ts import { createSolanaRpcSubscriptions } from '@solana/kit'; const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900'); // Subscribe for slot notifications using an AbortSignal that times out after 5 seconds. const slotNotifications = await rpcSubscriptions .slotNotifications() .subscribe({ abortSignal: AbortSignal.timeout(5000) }); // Log slot notifications. for await (const notification of slotNotifications) { console.log('Slot notification', notification); } console.log('Done.'); ``` Read more about `AbortController` at the following links: - [Mozilla Developer Docs: `AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) - [Mozilla Developer Docs: `AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) - [JavaScript.info: Fetch: Abort](https://javascript.info/fetch-abort) #### Cancelling Subscriptions It is also possible to abort a subscription inside the `for await...of` loop. This enables us to cancel a subscription based on some condition, such as a change in the state of an account. For instance, the following example cancels a subscription when the owner of an account changes: ```ts // Subscribe to account notifications. const accountNotifications = await rpc .accountNotifications(address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3'), { commitment: 'confirmed' }) .subscribe({ abortSignal }); // Consume messages. let previousOwner = null; for await (const notification of accountNotifications) { const { value: { owner }, } = notification; // Check the owner to see if it has changed if (previousOwner && owner !== previousOwner) { // If so, abort the subscription abortController.abort(); } else { console.log(notification); } previousOwner = owner; } ``` ### Failed vs. Aborted Subscriptions It is important to note that a subscription failure behaves differently from a subscription abort. A subscription failure occurs when the subscription goes down and will throw an error that can be intercepted in a `try/catch`. However, an aborted subscription will not throw an error, but will instead exit the `for await...of` loop. ```ts try { for await (const notification of notifications) { // Consume messages. } // [ABORTED] Reaching this line means the subscription was aborted — i.e. unsubscribed. } catch (e) { // [FAILED] Reaching this line means the subscription went down. // Retry it, then recover from potential missed messages. } finally { // [ABORTED or FAILED] Whether the subscription failed or was aborted, you can run cleanup code here. } ``` ### Message Gap Recovery One of the most crucial aspects of any subscription API is managing potential missed messages. Missing messages, such as account state updates, could be catastrophic for an application. That’s why the new library provides native support for recovering missed messages using the `AsyncIterator`. When a connection fails unexpectedly, any messages you miss while disconnected can result in your UI falling behind or becoming corrupt. Because subscription failure is now made explicit in the new API, you can implement ‘catch-up’ logic after re-establishing the subscription. Here’s an example of such logic: ```ts try { for await (const notif of accountNotifications) { updateAccountBalance(notif.lamports); } } catch (e) { // The subscription failed. // First, re-establish the subscription. await setupAccountBalanceSubscription(address); // Then make a one-shot request to 'catch up' on any missed balance changes. const { value: lamports } = await rpc.getBalance(address).send(); updateAccountBalance(lamports); } ``` ### Using Custom RPC Subscriptions Transports The `createSolanaRpcSubscriptions` function communicates with the RPC server using a default `WebSocket` channel that should satisfy most use cases. However, you may here as well provide your own channel creator or decorate existing ones to communicate with RPC servers in any way you see fit. In the example below, we supply a custom `WebSocket` channel creator and use it to create a new RPC Subscriptions client via the `createSolanaRpcSubscriptionsFromTransport` function. ```ts import { createDefaultRpcSubscriptionsTransport, createSolanaRpcSubscriptionsFromTransport } from '@solana/kit'; // Create a transport with a custom channel creator of your choice. const transport = createDefaultRpcSubscriptionsTransport({ createChannel({ abortSignal }) { return createWebSocketChannel({ maxSubscriptionsPerChannel: 100, minChannels: 25, sendBufferHighWatermark: 32_768, signal: abortSignal, url: 'ws://127.0.0.1:8900', }); }, }); // Create an RPC client using that transport. const rpcSubscriptions = createSolanaRpcSubscriptionsFromTransport(transport); // ^? RpcSubscriptions ``` ### Augmenting/Constraining the RPC Subscriptions API Using the `createSolanaRpcSubscriptions` or `createSolanaRpcSubscriptionsFromTransport` functions, we always get the same RPC Subscriptions API, including all Solana RPC stable subscriptions. However, since the RPC Subscriptions API is described using types only, it is possible to constrain the API to a specific set of subscriptions or even add your own custom subscriptions. #### Constraining by Cluster If you're using a specific cluster, you may wrap your RPC URL inside a helper function like `mainnet` or `devnet` to inject that information into the RPC type system. ```ts import { createSolanaRpcSubscriptions, mainnet, devnet } from '@solana/kit'; const mainnetRpc = createSolanaRpcSubscriptions(mainnet('https://api.mainnet-beta.solana.com')); // ^? RpcSubscriptionsMainnet const devnetRpc = createSolanaRpcSubscriptions(devnet('https://api.devnet.solana.com')); // ^? RpcSubscriptionsDevnet ``` #### Including Unstable Subscriptions If your app needs access to [unstable RPC Subscriptions](https://solana.com/docs/rpc/websocket/blocksubscribe) — e.g. `BlockNotificationsApi` or `SlotsUpdatesNotificationsApi` — and your RPC server supports them, you may use the `createSolanaRpcSubscriptions_UNSTABLE` and `createSolanaRpcSubscriptionsFromTransport_UNSTABLE` functions to create an RPC Subscriptions client that includes those subscriptions. ```ts import { createDefaultSolanaRpcSubscriptionsChannelCreator, createDefaultRpcSubscriptionsTransport, createSolanaRpcSubscriptions_UNSTABLE, createSolanaRpcSubscriptionsFromTransport_UNSTABLE, } from '@solana/kit'; // Using the default WebSocket channel. const rpcSubscriptions = createSolanaRpcSubscriptions_UNSTABLE('ws://127.0.0.1:8900'); // ^? RpcSubscriptions // Using a custom transport. const transport = createDefaultRpcSubscriptionsTransport({ createChannel: createDefaultSolanaRpcSubscriptionsChannelCreator({ url: 'ws://127.0.0.1:8900', }), }); const rpcSubscriptions = createSolanaRpcSubscriptionsFromTransport_UNSTABLE(transport); // ^? RpcSubscriptions ``` #### Cherry-Picking API Methods You may constrain the scope of the Subscription API even further so you are left only with the subscriptions you need. The simplest way to do this is to cast the created RPC client to a type that only includes the methods you need. ```ts import { createSolanaRpcSubscriptions, type RpcSubscriptions, type AccountNotificationsApi, type SlotNotificationsApi, } from '@solana/kit'; const rpc = createSolanaRpcSubscriptions('ws://127.0.0.1:8900') as RpcSubscriptions< AccountNotificationsApi & SlotNotificationsApi >; ``` Alternatively, you may explicitly create the RPC Subscriptions API using the `createSolanaRpcSubscriptionsApi` function. You will then need to create your own transport explicitly and bind the two together using the `createSubscriptionRpc` function. ```ts import { createDefaultSolanaRpcSubscriptionsChannelCreator, createDefaultRpcSubscriptionsTransport, createSubscriptionRpc, createSolanaRpcSubscriptionsApi, DEFAULT_RPC_CONFIG, type AccountNotificationsApi, type SlotNotificationsApi, } from '@solana/kit'; const api = createSolanaRpcSubscriptionsApi(DEFAULT_RPC_CONFIG); const transport = createDefaultRpcSubscriptionsTransport({ createChannel: createDefaultSolanaRpcSubscriptionsChannelCreator({ url: 'ws://127.0.0.1:8900', }), }); const rpcSubscriptions = createSubscriptionRpc({ api, transport }); ``` Note that the `createSolanaRpcSubscriptionsApi` function is a wrapper on top of the `createRpcSubscriptionsApi` function which adds some Solana-specific transformers such as setting a default commitment on all methods or throwing an error when an integer overflow is detected. ## Keys The new library takes a brand-new approach to Solana key pairs and addresses, which will feel quite different from the classes `PublicKey` and `Keypair` from version 1.x. ### Web Crypto API All key operations now use the native Ed25519 implementation in JavaScript’s Web Crypto API. The API itself is designed to be a more reliably secure way to manage highly sensitive secret key information, but **developers should still use extreme caution when dealing with secret key bytes in their applications**. One thing to note is that many operations from Web Crypto – such as importing, generating, signing, and verifying are now **asynchronous**. Here’s an example of generating a `CryptoKeyPair` using the Web Crypto API and signing a message: ```ts import { generateKeyPair, signBytes, verifySignature } from '@solana/kit'; const keyPair: CryptoKeyPair = await generateKeyPair(); const message = new Uint8Array(8).fill(0); const signedMessage = await signBytes(keyPair.privateKey, message); // ^? Signature const verified = await verifySignature(keyPair.publicKey, signedMessage, message); ``` ### Web Crypto Polyfill Wherever Ed25519 is not supported, we offer a polyfill for Web Crypto’s Ed25519 API. This polyfill can be found at `@solana/webcrypto-ed25519-polyfill` and mimics the functionality of the Web Crypto API for Ed25519 key pairs using the same userspace implementation we used in web3.js 1.x. It does not polyfill other algorithms. Determine if your target runtime supports Ed25519, and install the polyfill if it does not: ```ts import { install } from '@solana/webcrypto-ed25519-polyfill'; import { generateKeyPair, signBytes, verifySignature } from '@solana/kit'; install(); const keyPair: CryptoKeyPair = await generateKeyPair(); /* Remaining logic */ ``` You can see where Ed25519 is currently supported in [this GitHub issue](https://github.com/WICG/webcrypto-secure-curves/issues/20) on the Web Crypto repository. Consider sniffing the user-agent when deciding whether or not to deliver the polyfill to browsers. Operations on `CryptoKey` objects using the Web Crypto API _or_ the polyfill are mostly handled by the `@solana/keys` package. ### String Addresses All addresses are now JavaScript strings. They are represented by the opaque type `Address`, which describes exactly what a Solana address actually is. Consequently, that means no more `PublicKey`. Here’s what they look like in development: ```ts import { Address, address, getAddressFromPublicKey, generateKeyPair } from '@solana/kit'; // Coerce a string to an `Address` const myOtherAddress = address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3'); // Typecast it instead const myAddress = 'AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3' as Address<'AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3'>; // From CryptoKey const keyPair = await generateKeyPair(); const myPublicKeyAsAddress = await getAddressFromPublicKey(keyPair.publicKey); ``` Some tooling for working with base58-encoded addresses can be found in the `@solana/addresses` package. ## Transactions ### Creating Transaction Messages Like many other familiar aspects of the 1.0 library, transactions have received a makeover. For starters, all transaction messages are now version-aware, so there’s no longer a need to juggle two different types (eg. `Transaction` vs. `VersionedTransaction`). Address lookups are now completely described inside transaction message instructions, so you don’t have to materialize `addressTableLookups` anymore. Here’s a simple example of creating a transaction message – notice how its type is refined at each step of the process: ```ts import { address, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, Blockhash, } from '@solana/kit'; const recentBlockhash = { blockhash: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY' as Blockhash, lastValidBlockHeight: 196055492n, }; const feePayer = address('AxZfZWeqztBCL37Mkjkd4b8Hf6J13WCcfozrBY6vZzv3'); // Create a new transaction message const transactionMessage = createTransactionMessage({ version: 0 }); // ^? V0TransactionMessage // Set the fee payer const transactionMessageWithFeePayer = setTransactionMessageFeePayer(feePayer, transactionMessage); // ^? V0TransactionMessage & TransactionMessageWithFeePayer const transactionMessageWithFeePayerAndLifetime = setTransactionMessageLifetimeUsingBlockhash( // ^? V0TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithBlockhashLifetime recentBlockhash, transactionMessageWithFeePayer, ); ``` As you can see, each time a transaction message is modified, the type reflects its new shape. If you add a fee payer, you’ll get a type representing a transaction message with a fee payer, and so on. Transaction message objects are also **frozen by these functions** to prevent them from being mutated in place. ### Signing Transaction Messages The `signTransaction(..)` function will raise a type error if your transaction message is not already equipped with a fee payer and a lifetime. This helps you catch errors at author-time instead of runtime. ```ts const feePayer = await generateKeyPair(); const feePayerAddress = await getAddressFromPublicKey(feePayer.publicKey); const transactionMessage = createTransactionMessage({ version: 'legacy' }); const transactionMessageWithFeePayer = setTransactionMessageFeePayer(feePayerAddress, transactionMessage); // Attempting to sign the transaction message without a lifetime will throw a type error const signedTransaction = await signTransaction([signer], transactionMessageWithFeePayer); // => "Property 'lifetimeConstraint' is missing in type" ``` ### Calibrating a Transaction Message's Compute Unit Budget Correctly budgeting a compute unit limit for your transaction message can increase the probability that your transaction will be accepted for processing. If you don't declare a compute unit limit on your transaction, validators will assume an upper limit of 200K compute units (CU) per instruction. Since validators have an incentive to pack as many transactions into each block as possible, they may choose to include transactions that they know will fit into the remaining compute budget for the current block over transactions that might not. For this reason, you should set a compute unit limit on each of your transaction messages, whenever possible. Use these utilities to estimate the actual compute unit cost of a given transaction message and set it on the message. ```ts import { createSolanaRpc, estimateComputeUnitLimitFactory, setTransactionMessageComputeUnitLimit } from '@solana/kit'; // Create an estimator function. const rpc = createSolanaRpc('http://127.0.0.1:8899'); const estimateComputeUnitLimit = estimateComputeUnitLimitFactory({ rpc }); // Create your transaction message. const transactionMessage = pipe( createTransactionMessage({ version: 'legacy' }), /* ... */ ); // Request an estimate of the actual compute units this message will consume. const computeUnitsEstimate = await estimateComputeUnitLimit(transactionMessage); // Set the transaction message's compute unit budget. const transactionMessageWithComputeUnitLimit = setTransactionMessageComputeUnitLimit( computeUnitsEstimate, transactionMessage, ); ``` > [!NOTE] > For legacy and v0 transactions, if the transaction message does not already have a `SetComputeUnitLimit` instruction, the estimator will add one before simulation. This ensures that the compute unit consumption of the instruction itself is included in the estimate. Alternatively, use `estimateAndSetComputeUnitLimitFactory` to estimate and set the compute unit limit in a single step. Pair it with `fillTransactionMessageProvisoryComputeUnitLimit` during transaction construction to reserve space for the limit that will later be estimated. ```ts import { estimateAndSetComputeUnitLimitFactory, estimateComputeUnitLimitFactory, fillTransactionMessageProvisoryComputeUnitLimit, } from '@solana/kit'; // During construction, reserve space for the compute unit limit. const messageWithProvisoryLimit = fillTransactionMessageProvisoryComputeUnitLimit(transactionMessage); // Later, estimate and replace the provisory limit. const estimator = estimateComputeUnitLimitFactory({ rpc }); const estimateAndSet = estimateAndSetComputeUnitLimitFactory(estimator); const updatedMessage = await estimateAndSet(messageWithProvisoryLimit); ``` > [!WARNING] > The compute unit estimate is just that – an estimate. The compute unit consumption of the actual transaction might be higher or lower than what was observed in simulation. Unless you are confident that your particular transaction message will consume the same or fewer compute units as was estimated, you might like to augment the estimate by either a fixed number of CUs or a multiplier. > [!NOTE] > If you are preparing an _unsigned_ transaction, destined to be signed and submitted to the network by a wallet, you might like to leave it up to the wallet to determine the compute unit limit. Consider that the wallet might have a more global view of how many compute units certain types of transactions consume, and might be able to make better estimates of an appropriate compute unit budget. ### Helpers For Building Transaction Messages Building transaction messages in this manner might feel different from what you’re used to. Also, we certainly wouldn’t want you to have to bind transformed transaction messages to a new variable at each step, so we have released a functional programming library dubbed `@solana/functional` that lets you build transaction messages in **pipelines**. Here’s how it can be used: ```ts import { pipe } from '@solana/functional'; import { address, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, Blockhash, } from '@solana/kit'; // Use `pipe(..)` to create a pipeline of transaction message transformation operations const transactionMessage = pipe( createTransactionMessage({ version: 0 }), tx => setTransactionMessageFeePayer(feePayer, tx), tx => setTransactionMessageLifetimeUsingBlockhash(recentBlockhash, tx), ); ``` Note that `pipe(..)` is general-purpose, so it can be used to pipeline any functional transforms. ## Codecs We have taken steps to make it easier to write data (de)serializers, especially as they pertain to Rust datatypes and byte buffers. Solana’s codecs libraries are broken up into modular components so you only need to import the ones you need. They are: - `@solana/codecs-core`: The core codecs library for working with codecs serializers and creating custom ones - `@solana/codecs-numbers`: Used for serialization of numbers (little-endian and big-endian bytes, etc.) - `@solana/codecs-strings`: Used for serialization of strings - `@solana/codecs-data-structures`: Codecs and serializers for structs - `@solana/options`: Designed to build codecs and serializers for types that mimic Rust’s enums, which can include embedded data within their variants such as values, tuples, and structs These packages are included in the main `@solana/kit` library but you may also import them from `@solana/codecs` if you only need the codecs. Here’s an example of encoding and decoding a custom struct with some strings and numbers: ```ts import { addCodecSizePrefix } from '@solana/codecs-core'; import { getStructCodec } from '@solana/codecs-data-structures'; import { getU32Codec, getU64Codec, getU8Codec } from '@solana/codecs-numbers'; import { getUtf8Codec } from '@solana/codecs-strings'; // Equivalent in Rust: // struct { // amount: u64, // decimals: u8, // name: String, // } const structCodec = getStructCodec([ ['amount', getU64Codec()], ['decimals', getU8Codec()], ['name', addCodecSizePrefix(getUtf8Codec(), getU32Codec())], ]); const myToken = { amount: 1000000000000000n, // `bigint` or `number` is supported decimals: 2, name: 'My Token', }; const myEncodedToken: Uint8Array = structCodec.encode(myToken); const myDecodedToken = structCodec.decode(myEncodedToken); myDecodedToken satisfies { amount: bigint; decimals: number; name: string; }; ``` You may only need to encode or decode data, but not both. Importing one or the other allows your optimizing compiler to tree-shake the other implementation away: ```ts import { Codec, combineCodec, Decoder, Encoder, addDecoderSizePrefix, addEncoderSizePrefix } from '@solana/codecs-core'; import { getStructDecoder, getStructEncoder } from '@solana/codecs-data-structures'; import { getU8Decoder, getU8Encoder, getU32Decoder, getU32Encoder, getU64Decoder, getU64Encoder, } from '@solana/codecs-numbers'; import { getUtf8Decoder, getUtf8Encoder } from '@solana/codecs-strings'; export type MyToken = { amount: bigint; decimals: number; name: string; }; export type MyTokenArgs = { amount: number | bigint; decimals: number; name: string; }; export const getMyTokenEncoder = (): Encoder => getStructEncoder([ ['amount', getU64Encoder()], ['decimals', getU8Encoder()], ['name', addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ]); export const getMyTokenDecoder = (): Decoder => getStructDecoder([ ['amount', getU64Decoder()], ['decimals', getU8Decoder()], ['name', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ]); export const getMyTokenCodec = (): Codec => combineCodec(getMyTokenEncoder(), getMyTokenDecoder()); ``` You can read more about codecs in [the official Codec documentation](https://github.com/anza-xyz/kit/blob/main/packages/codecs/README.md). ## Type-Safety The new library makes use of some advanced TypeScript features, including generic types, conditional types, `Parameters<..>`, `ReturnType<..>` and more. We’ve described the RPC API in detail so that TypeScript can determine the _exact_ type of the result you will receive from the server given a particular input. Change the type of the input, and you will see the return type reflect that change. ### RPC Types The RPC methods – both HTTP and subscriptions – are built with multiple overloads and conditional types. The expected HTTP response payload or subscription message format will be reflected in the return type of the function you’re working with when you provide the inputs in your code. Here’s an example of this in action: ```ts // Provide one set of parameters, get a certain type // These parameters resolve to return type: // { // blockhash: Blockhash; // blockHeight: bigint; // blockTime: UnixTimestamp; // parentSlot: bigint; // previousBlockhash: Blockhash; // } const blockResponse = await rpc .getBlock(0n, { rewards: false, transactionDetails: 'none', }) .send(); // Switch `rewards` to `true`, get `rewards` in the return type // { // /* ... Previous response */ // rewards: Reward[]; // } const blockWithRewardsResponse = await rpc .getBlock(0n, { rewards: true, transactionDetails: 'none', }) .send(); // Switch `transactionDetails` to `full`, get `transactions` in the return type // { // /* ... Previous response */ // transactions: TransactionResponse[]; // } const blockWithRewardsAndTransactionsResponse = await rpc .getBlock(0n, { rewards: true, transactionDetails: 'full', }) .send(); ``` ### Catching Compile-Time Bugs with TypeScript As previously mentioned, the type coverage in Kit allows developers to catch common bugs at compile time, rather than runtime. In the example below, a transaction message is created and then attempted to be signed without setting the fee payer. This would result in a runtime error from the RPC, but instead you will see a type error from TypeScript as you type: ```ts const transactionMessage = pipe(createTransactionMessage({ version: 0 }), tx => setTransactionMessageLifetimeUsingBlockhash(recentBlockhash, tx), ); const signedTransaction = await signTransaction([keyPair], transactionMessage); // ERROR: Property 'feePayer' is missing in type ``` Consider another example where a developer is attempting to send a transaction that has not been fully signed. Again, the TypeScript compiler will throw a type error: ```ts const transactionMessage = pipe( createTransactionMessage({ version: 0 }), tx => setTransactionMessageFeePayer(feePayerAddress, tx), tx => setTransactionMessageLifetimeUsingBlockhash(recentBlockhash, tx), ); const signedTransaction = await signTransaction([], transactionMessage); // Asserts the transaction is a `FullySignedTransaction` // Throws an error if any signatures are missing! assertIsFullySignedTransaction(signedTransaction); await sendAndConfirmTransaction(signedTransaction); ``` Are you building a nonce transaction and forgot to make `AdvanceNonce` the first instruction? That’s a type error: ```ts const feePayer = await generateKeyPair(); const feePayerAddress = await getAddressFromPublicKey(feePayer.publicKey); const notNonceTransactionMessage = pipe(createTransactionMessage({ version: 0 }), tx => setTransactionMessageFeePayer(feePayerAddress, tx), ); notNonceTransactionMessage satisfies TransactionMessageWithDurableNonceLifetime; // => Property 'lifetimeConstraint' is missing in type const nonceConfig = { nonce: 'nonce' as Nonce, nonceAccountAddress: address('5tLU66bxQ35so2bReGcyf3GfMMAAauZdNA1N4uRnKQu4'), nonceAuthorityAddress: address('GDhj8paPg8woUzp9n8fj7eAMocN5P7Ej3A7T9F5gotTX'), }; const stillNotNonceTransactionMessage = { lifetimeConstraint: nonceConfig, ...notNonceTransactionMessage, }; stillNotNonceTransactionMessage satisfies TransactionMessageWithDurableNonceLifetime; // => 'readonly Instruction[]' is not assignable to type 'readonly [AdvanceNonceAccountInstruction, ...Instruction[]]' const validNonceTransactionMessage = pipe( createTransactionMessage({ version: 0 }), tx => setTransactionMessageFeePayer(feePayerAddress, tx), tx => setTransactionMessageLifetimeUsingDurableNonce(nonceConfig, tx), // Adds the instruction! ); validNonceTransactionMessage satisfies TransactionMessageWithDurableNonceLifetime; // OK ``` The library’s type-checking can even catch you using lamports instead of SOL for a value: ```ts const airdropAmount = 1n; // SOL const signature = rpc.requestAirdrop(myAddress, airdropAmount).send(); ``` It will force you to cast the numerical value for your airdrop (or transfer, etc.) amount using `lamports()`, which should be a good reminder! ```ts const airdropAmount = lamports(1000000000n); const signature = rpc.requestAirdrop(myAddress, airdropAmount).send(); ``` ## Compatibility Layer You will have noticed by now that Kit is a complete and total breaking change from the web3.js 1.x line. We want to provide you with a strategy for interacting with web3.js 1.x APIs while building your application using Kit. You need a tool for converting between web3.js 1.x and Kit data types. The `@solana/compat` library allows for interoperability between functions and class objects from the legacy library - such as `VersionedTransaction`, `PublicKey`, and `Keypair` - and functions and types of the new library - such as `Address`, `Transaction`, and `CryptoKeyPair`. Here’s how you can use `@solana/compat` to convert from a legacy `PublicKey` to an `Address`: ```ts import { fromLegacyPublicKey } from '@solana/compat'; const publicKey = new PublicKey('B3piXWBQLLRuk56XG5VihxR4oe2PSsDM8nTF6s1DeVF5'); const address: Address = fromLegacyPublicKey(publicKey); ``` Here’s how to convert from a legacy `Keypair` to a `CryptoKeyPair`: ```ts import { fromLegacyKeypair } from '@solana/compat'; const keypairLegacy = Keypair.generate(); const cryptoKeyPair: CryptoKeyPair = fromLegacyKeypair(keypair); ``` Here’s how to convert legacy transaction objects to the new library’s transaction types: ```ts // Note that you can only convert `VersionedTransaction` objects const modernTransaction = fromVersionedTransaction(classicTransaction); ``` To see more conversions supported by `@solana/compat`, you can check out the package’s [README on GitHub](https://github.com/anza-xyz/kit/blob/main/packages/compat/README.md). ## Program Clients Writing JavaScript clients for on-chain programs has been done manually up until now. Without an IDL for some of the native programs, this process has been necessarily manual and has resulted in clients that lag behind the actual capabilities of the programs themselves. We think that program clients should be _generated_ rather than written. Developers should be able to write Rust programs, compile the program code, and generate all of the JavaScript client-side code to interact with the program. We use [Codama](https://github.com/codama-idl/codama) to represent Solana programs and generate clients for them. This includes a JavaScript client compatible with this library. For instance, here is how you’d construct a transaction message composed of instructions from three different core programs. ```ts import { appendTransactionMessageInstructions, createTransactionMessage, pipe } from '@solana/kit'; import { getAddMemoInstruction } from '@solana-program/memo'; import { getSetComputeUnitLimitInstruction } from '@solana-program/compute-budget'; import { getTransferSolInstruction } from '@solana-program/system'; const instructions = [ getSetComputeUnitLimitInstruction({ units: 600_000 }), getTransferSolInstruction({ source, destination, amount: 1_000_000_000 }), getAddMemoInstruction({ memo: "I'm transferring some SOL!" }), ]; // Creates a V0 transaction message with 3 instructions inside. const transactionMessage = pipe(createTransactionMessage({ version: 0 }), tx => appendTransactionMessageInstructions(instructions, tx), ); ``` As you can see, each program now generates its own library allowing you to cherry-pick your dependencies. Note that asynchronous versions may be available for some instructions which allows them to resolve more inputs on your behalf — such as PDA derivation. For instance, the `CreateLookupTable` instruction offers an asynchronous builder that derives the `address` account and the `bump` argument for us. ```ts const rpc = createSolanaRpc('http://127.0.0.1:8899'); const [authority, recentSlot] = await Promise.all([ generateKeyPairSigner(), rpc.getSlot({ commitment: 'finalized' }).send(), ]); const instruction = await getCreateLookupTableInstructionAsync({ authority, recentSlot, }); ``` Alternatively, you may use the synchronous builder if you already have all the required inputs at hand. ```ts const [address, bump] = await findAddressLookupTablePda({ authority: authority.address, recentSlot, }); const instruction = getCreateLookupTableInstruction({ address, authority, bump, recentSlot, }); ``` On top of instruction builders, these clients offer a variety of utilities such as: - Instruction codecs — e.g. `getTransferSolInstructionDataCodec`. - Account types — e.g. `AddressLookupTable`. - Account codecs — e.g. `getAddressLookupTableAccountDataCodec`. - Account helpers — e.g. `fetchAddressLookupTable`. - PDA helpers — e.g. `findAddressLookupTablePda`, `fetchAddressLookupTableFromSeeds`. - Defined types and their codecs — e.g. `NonceState`, `getNonceStateCodec`. - Program helpers — e.g. `SYSTEM_PROGRAM_ADDRESS`, `SystemAccount` enum, `identifySystemInstruction`. - And much more! Here’s another example that fetches an `AddressLookupTable` PDA from its seeds. ```ts const account = await fetchAddressLookupTableFromSeeds(rpc, { authority: authority.address, recentSlot, }); account.address; // Address account.lamports; // Lamports account.data.addresses; // Address[] account.data.authority; // Some
account.data.deactivationSlot; // Slot account.data.lastExtendedSlot; // Slot account.data.lastExtendedSlotStartIndex; // number ``` ### How Does This Work? All of this code is 100% auto-generated by Codama from a tree of standardized nodes that represent our programs. It contains obvious nodes such as `AccountNode` but also more specified nodes such as `ConditionalValueNode` that allows us to resolve account or argument default values conditionally. Codama allows us to hydrate our tree of nodes from IDLs which are typically generated by program frameworks such as [Anchor](https://github.com/coral-xyz/anchor) or [Shank](https://github.com/metaplex-foundation/shank). Additionally, visitors can be used on our nodes to expand the knowledge of our programs since the IDL itself doesn’t yet contain that level of information. Finally, special visitors called ‘renderers’ visit our tree to generate clients such as this JavaScript client. Currently, there is one other renderer that generates Rust clients but this is only the beginning. In the future, you can expect renderers for auto-generated Python clients, documentation, CLIs, etc. ## Create Solana Program We believe the whole ecosystem could benefit from generated program clients. That’s why we introduced a new NPM binary that allows you to create your Solana program — and generate clients for it — in no time. Simply run the following and follow the prompts to get started. ```sh pnpm create solana-program ``` This [`create-solana-program`](https://github.com/solana-program/create-solana-program) installer will create a new repository including: - An example program using the framework of your choice (Anchor coming soon). - Generated clients for any of the selected clients. - A set of scripts that allows you to: - Start a local validator including all programs and accounts you depend on. - Build, lint and test your programs. - Generate IDLs from your programs. - Generate clients from the generated IDLs. - Build and test each of your clients. - GitHub Actions pipelines to test your program, test your clients, and even manually publish new packages or crates for your clients. (Coming soon). When selecting the JavaScript client, you will get a fully generated library compatible with Kit much like the `@solana-program` packages showcased above. ## GraphQL Though not directly related to web3.js, we wanted to hijack your attention to show you something else that we’re working on, of particular interest to frontend developers. It’s a new API for interacting with the RPC: a GraphQL API. The `@solana/rpc-graphql` package can be used to make GraphQL queries to Solana RPC endpoints, using the same transports described above (including any customizations). Here’s an example of retrieving account data with GraphQL: ```ts const source = ` query myQuery($address: String!) { account(address: $address) { dataBase58: data(encoding: BASE_58) dataBase64: data(encoding: BASE_64) lamports } } `; const variableValues = { address: 'AyGCwnwxQMCqaU4ixReHt8h5W4dwmxU7eM3BEQBdWVca', }; const result = await rpcGraphQL.query(source, variableValues); expect(result).toMatchObject({ data: { account: { dataBase58: '2Uw1bpnsXxu3e', dataBase64: 'dGVzdCBkYXRh', lamports: 10290815n, }, }, }); ``` Using GraphQL allows developers to only specify which fields they _actually_ need, and do away with the rest of the response. However, GraphQL is also extremely powerful for **nesting queries**, which can be particularly useful if you want to, say, get the **sum** of every lamports balance of every **owner of the owner** of each token account, while discarding any mint accounts. ```ts const source = ` query getLamportsOfOwnersOfOwnersOfTokenAccounts { programAccounts(programAddress: "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") { ... on TokenAccount { owner { ownerProgram { lamports } } } } } `; const result = await rpcGraphQL.query(source); const sumOfAllLamportsOfOwnersOfOwnersOfTokenAccounts = result .map(o => o.account.owner.ownerProgram.lamports) .reduce((acc, lamports) => acc + lamports, 0); ``` The new GraphQL package supports this same style of nested querying on transactions and blocks. ```ts const source = ` query myQuery($signature: String!, $commitment: Commitment) { transaction(signature: $signature, commitment: $commitment) { message { instructions { ... on CreateAccountInstruction { lamports programId space } } } } } `; const variableValues = { signature: '63zkpxATgAwXRGFQZPDESTw2m4uZQ99sX338ibgKtTcgG6v34E3MSS3zckCwJHrimS71cvei6h1Bn1K1De53BNWC', commitment: 'confirmed', }; const result = await rpcGraphQL.query(source, variableValues); expect(result).toMatchObject({ data: { transaction: { message: { instructions: expect.arrayContaining([ { lamports: expect.any(BigInt), programId: '11111111111111111111111111111111', space: expect.any(BigInt), }, ]), }, }, }, }); ``` See more in the package’s [README on GitHub](https://github.com/anza-xyz/kit/tree/main/packages/rpc-graphql). ## Development You can see all development of this library and associated GraphQL tooling in the Kit repository on GitHub. - https://github.com/anza-xyz/kit You can follow along with program client generator development in the `@solana-program` org and the `@codama-idl/codama` repository. - https://github.com/solana-program/ - https://github.com/codama-idl/codama Solana Labs develops these tools in public, as open source. We encourage any and all developers who would like to work on these tools to contribute to the codebase. ## Thank you We’re grateful that you have read this far. If you are interested in migrating an existing application to Kit to take advantage of some of the benefits we’ve demonstrated, we want to give you some direct support. Reach out to [@steveluscher](https://t.me/steveluscher/) on Telegram to start a conversation. ================================================ FILE: SECURITY.md ================================================ # Security Policy 1. [Reporting security problems](#reporting) 2. [Incident Response Process](#process) ## Reporting security problems in the Solana JavaScript SDK **DO NOT CREATE A GITHUB ISSUE** to report a security problem. Instead please use this [Report a Vulnerability](https://github.com/anza-xyz/kit/security/advisories/new) link. Provide a helpful title, detailed description of the vulnerability and an exploit proof-of-concept. Speculative submissions without proof-of-concept will be closed with no further consideration. If you haven't done so already, please **enable two-factor auth** in your GitHub account. -- If you do not receive a response in the advisory, send an email to security@anza.xyz with the full URL of the advisory you have created. DO NOT include attachments or provide detail sufficient for exploitation regarding the security issue in this email. **Only provide such details in the advisory**. If you do not receive a response from security@anza.xyz please follow up with the team directly. You can do this in the `#js` channel of the [Solana Tech discord server](https://solana.com/discord), by pinging the `Anza` role in the channel and referencing the fact that you submitted a security problem. ## Incident Response Process In case an incident is discovered or reported, the following process will be followed to contain, respond and remediate: ### 1. Accept the new report In response to a newly reported security problem, a member of the `anza-xyz/core-contributors` group will accept the report to turn it into a draft advisory. The `anza-xyz/security-incident-response` group should be added to the draft security advisory, and create a private fork of the repository (grey button towards the bottom of the page) if necessary. If the advisory is the result of an audit finding, follow the same process as above but add the auditor's github user(s) and begin the title with "[Audit]". If the report is out of scope, a member of the `anza-xyz/core-contributors` group will comment as such and then close the report. ### 2. Triage Within the draft security advisory, discuss and determine the severity of the issue. If necessary, members of the `anza-xyz/security-incident-response` group may add other github users to the advisory to assist. If it is determined that this is not a critical issue then the advisory should be closed and if more follow-up is required a normal Solana public github issue should be created. ### 3. Prepare Fixes Prepare a fix for the issue and push them to `main` in the private repository associated with the draft security advisory. There is no CI available in the private repository so you must build from source and manually verify fixes. Code review from the reporter is ideal, as well as from multiple members of the core development team. ### 4. Ship the patch Once the fix is accepted, a member of the anza-xyz/security-incident-response group should prepare a patch using [`pnpm patch`](https://pnpm.io/cli/patch), [`yarn patch`](https://yarnpkg.com/cli/patch), and [`patch-package`](https://www.npmjs.com/package/patch-package) for developers still using `npm`. Post the patch to an unlisted [GitHub Gist](https://gist.github.com) and disseminate patch instructions privately to as many vulnerable applications as possible. ### 5. Public Disclosure and Release Once the fix has been deployed to as large an affected application set as practical, the patches from the security advisory may be merged into the main source repository. A new official release should be shipped, and old affected releases deprecated on NPM using the `npm deprecate` command. ================================================ FILE: docs/.eslintrc.json ================================================ { "extends": ["next/core-web-vitals", "next/typescript"] } ================================================ FILE: docs/.gitignore ================================================ # deps /node_modules # generated content .contentlayer .content-collections .source # test & build /coverage /.next/ /out/ /build *.tsbuildinfo # misc .DS_Store *.pem /.pnp .pnp.js npm-debug.log* yarn-debug.log* yarn-error.log* # others .env*.local .vercel next-env.d.ts ================================================ FILE: docs/.npmrc ================================================ enable-pre-post-scripts = true ================================================ FILE: docs/.prettierignore ================================================ pnpm-lock.yaml content/api/ ================================================ FILE: docs/README.md ================================================ # Kit documentation Documentation website for Kit, built with [Fumadocs](https://github.com/fuma-nama/fumadocs). ## Install dependencies Install using the `--ignore-workspace` flag to ensure the dependencies of the documentation website are separate from the library dependencies. ```bash pnpm install --ignore-workspace ``` ## Download necessary environment variables (employees only) ```bash pnpm vercel link -p kit-docs -S anza-tech --yes pnpm vercel env pull --environment=development ``` ## Run development server ```bash pnpm dev ``` Open http://localhost:3000 with your browser to see the result. ## Learn more To learn more about Next.js and Fumadocs, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs ================================================ FILE: docs/build-api-docs.sh ================================================ #!/usr/bin/env bash set -euo pipefail cd .. pnpm turbo compile:docs --output-logs=hash-only # Move generated docs. echo "➡️ Moving generated TypeDoc files to docs/content/api..." API_DIR="$(pwd)/docs/content/api" find $API_DIR -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} + mkdir -p "$API_DIR" for DOC_PATH in packages/*/.docs; do (cd "$DOC_PATH" && find . -type f ! -name "index.mdx" | while read -r file; do mkdir -p "$API_DIR/$(dirname "$file")" cp "$file" "$API_DIR/$file" done) done # Generate API index. echo "📝 Generating API index page from TypeDoc JSON..." node docs/build-api-index.js # Regenerate .source to pick up the new index.mdx echo "♻️ Regenerating .source files..." cd docs pnpm fumadocs-mdx ================================================ FILE: docs/build-api-index.js ================================================ #!/usr/bin/env node /** * Generate API index page using TypeDoc JSON output * This script uses TypeDoc's JSON output to get detailed reflection data */ const fs = require('node:fs/promises'); const path = require('node:path'); const { execFile } = require('node:child_process'); const { promisify } = require('node:util'); const execFileAsync = promisify(execFile); async function exists(path) { try { await fs.access(path); return true; } catch { return false; } } const ROOT_DIR = path.join(__dirname, '..'); const PACKAGES_DIR = path.join(ROOT_DIR, 'packages'); const OUTPUT_FILE = path.join(ROOT_DIR, 'docs', 'content', 'api', 'index.mdx'); // TypeDoc reflection kinds (from TypeDoc source) const ReflectionKind = { Project: 1, Module: 2, Namespace: 4, Enum: 8, EnumMember: 16, Variable: 32, Function: 64, Class: 128, Interface: 256, Constructor: 512, Property: 1024, Method: 2048, CallSignature: 4096, IndexSignature: 8192, ConstructorSignature: 16384, Parameter: 32768, TypeLiteral: 65536, TypeParameter: 131072, Accessor: 262144, GetSignature: 524288, SetSignature: 1048576, ObjectLiteral: 2097152, TypeAlias: 4194304, Reference: 8388608, }; const API_GROUP_ORDER = ['Classes', 'Enums', 'Types', 'Functions', 'Variables']; const REFLECTION_KIND_DATA = { [ReflectionKind.Class]: { group: 'Classes', path: 'classes' }, [ReflectionKind.Interface]: { group: 'Types', path: 'interfaces' }, [ReflectionKind.ObjectLiteral]: { group: 'Types', path: 'type-aliases' }, [ReflectionKind.TypeAlias]: { group: 'Types', path: 'type-aliases' }, [ReflectionKind.Function]: { group: 'Functions', path: 'functions' }, [ReflectionKind.Enum]: { group: 'Enums', path: 'enumerations' }, [ReflectionKind.Variable]: { group: 'Variables', path: 'variables' }, }; async function findPackageInfos() { const packages = []; const packageDirs = await fs.readdir(PACKAGES_DIR); for (const packageName of packageDirs) { const packagePath = path.join(PACKAGES_DIR, packageName); const packageJsonPath = path.join(packagePath, 'package.json'); const srcPath = path.join(packagePath, 'src'); const indexPath = path.join(srcPath, 'index.ts'); const typedocConfigPath = path.join(packagePath, 'typedoc.json'); // Skip packages without source, package.json, or typedoc config. const artifactsExistence = await Promise.all([ exists(packageJsonPath), exists(srcPath), exists(indexPath), exists(typedocConfigPath), ]); if (artifactsExistence.includes(false)) { continue; } const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')); packages.push({ name: pkg.name, path: packagePath }); } return packages.sort((a, b) => a.name.localeCompare(b.name)); } async function generateTypeDocJSON(packageInfo) { const { path: packagePath, name } = packageInfo; const jsonOutputPath = path.join(packagePath, 'typedoc-output.json'); try { // Generate JSON output only (suppress regular output). await execFileAsync( './node_modules/typedoc/bin/typedoc', [ '--json', jsonOutputPath, '--tsconfig', path.join(packagePath, 'tsconfig.json'), '--out', '/dev/null', '--excludePrivate', '--excludeProtected', '--excludeInternal', path.join(packagePath, 'src', 'index.ts'), ], { cwd: ROOT_DIR }, ); if (!(await exists(jsonOutputPath))) { console.warn(`⚠️ No JSON output generated for ${name}`); return null; } const typeDocJSON = JSON.parse(await fs.readFile(jsonOutputPath, 'utf8')); // Clean up the temp JSON file await fs.unlink(jsonOutputPath); return typeDocJSON; } catch (error) { console.error(`❌ Failed to generate JSON for ${name}:`, error.message); return null; } } function processTypeDocJSON(packageInfo, typeDocJSON) { const exports = []; const dependencies = new Set([]); if (!typeDocJSON || !typeDocJSON.children) { return { ...packageInfo, dependencies, exports }; } function processChildren(children) { for (const child of children) { // Only include relevant kinds in the exports. if (REFLECTION_KIND_DATA[child.kind]) { const fromPackage = typeDocJSON.symbolIdMap[child.id]?.packageName || typeDocJSON.packageName; const isDependency = fromPackage !== typeDocJSON.packageName; if (isDependency) { dependencies.add(fromPackage); } else { exports.push({ name: child.name, kind: child.kind }); } } // Recursively process nested children. if (child.children) { processChildren(child.children); } } } processChildren(typeDocJSON.children); return { ...packageInfo, dependencies, exports }; } function generateIndexContent(packages) { const totalPackages = packages.length; let content = `--- title: API Reference description: Explore packages, functions, types, and more --- Welcome to the Solana Kit API Reference! It covers a total of **${totalPackages} packages** most of which are available via the main \`@solana/kit\` package. ## Need Help? - Check out our [Getting Started guide](/docs/getting-started/). - Learn about key concepts in our [Advanced Guides](/docs/advanced-guides/). ## All Packages `; // Sort packages by name but with `@solana/kit` first. const sortedPackages = packages.sort((a, b) => { if (a.name === '@solana/kit') return -1; if (b.name === '@solana/kit') return 1; return a.name.localeCompare(b.name); }); // Generate entries for each package alphabetically. for (const package of sortedPackages) { content += `### \`${package.name}\`\n\n`; // List exported dependencies. if (package.dependencies.size > 0) { const dependencies = [...package.dependencies].sort((a, b) => a.localeCompare(b)); const title = `Packages (${dependencies.length})`; content += `${wrapApiGroupTitle(title)}\n\n`; const links = dependencies.map(dependency => `[${dependency}](#${dependency.replace(/(@|\/)/g, '')})`); content += `${wrapLinks(links, dependencies)}\n\n`; } // Sort exports into type groups. const apiGroups = Object.fromEntries(API_GROUP_ORDER.map(group => [group, { title: group, items: [] }])); for (const item of package.exports) { if (item.kind && REFLECTION_KIND_DATA[item.kind]) { const data = REFLECTION_KIND_DATA[item.kind]; if (apiGroups[data.group]) { apiGroups[data.group].items.push({ ...item, path: data.path }); } } } // Generate sections for each type. for (const group of Object.values(apiGroups)) { if (group.items.length > 0) { const title = `${group.title} (${group.items.length})`; content += `${wrapApiGroupTitle(title)}\n\n`; // List items with links. const links = group.items .sort((a, b) => a.name.localeCompare(b.name)) .map(item => `[${item.name}](/api/${item.path}/${item.name})`); // Wrap links in columns when possible. const names = group.items.map(item => item.name); content += `${wrapLinks(links, names)}\n\n`; } } } content += ` This documentation is automatically generated from the source code using TypeDoc. `; return content; } function wrapApiGroupTitle(content) { return `

${content}

`; } function wrapLinks(links, names) { const maxNameLength = Math.max(...names.map(name => name.length)); let linkWrapper = content => content; if (maxNameLength <= 30) { linkWrapper = content => `
\n\n${content}\n\n
`; } else if (maxNameLength <= 60) { linkWrapper = content => `
\n\n${content}\n\n
`; } return linkWrapper(links.join(' \\\n')); } // Main execution async function main() { const packageInfos = await findPackageInfos(); const packages = []; await Promise.all( packageInfos.map(async packageInfo => { const typeDocJSON = await generateTypeDocJSON(packageInfo); const package = await processTypeDocJSON(packageInfo, typeDocJSON); packages.push(package); }), ); const content = generateIndexContent(packages); await fs.writeFile(OUTPUT_FILE, content, 'utf8'); console.log('✅ API index generated successfully!'); console.log(`├─ Packages: ${packages.length}`); console.log(`└─ Output: ${OUTPUT_FILE}`); } if (require.main === module) { main().catch(console.error); } ================================================ FILE: docs/cli.json ================================================ { "$schema": "node_modules/@fumadocs/cli/dist/schema/src.json", "aliases": { "uiDir": "./components/ui", "componentsDir": "./components", "blockDir": "./components", "cssDir": "./styles", "libDir": "./lib" }, "baseDir": "src", "uiLibrary": "radix-ui", "commands": {} } ================================================ FILE: docs/content/.prettierrc ================================================ { "$schema": "https://json.schemastore.org/prettierrc", "tabWidth": 4, "singleQuote": true, "printWidth": 100 } ================================================ FILE: docs/content/docs/advanced-guides/codecs.mdx ================================================ --- title: Codecs description: Encode and decode anything --- ## Introduction Kit includes a powerful serialisation system called Codecs. Whether you're working with account data, instruction arguments, or custom binary layouts, Codecs give you the tools to transform structured data into bytes — and back again. Codecs are composable, type-safe, and environment-agnostic. They are designed to provide a flexible and consistent foundation for handling binary data across the Solana stack. ## Installation Codecs are **included within the `@solana/kit` library** but you may also install them using their standalone package. ```package-install @solana/codecs ``` Note that the `@solana/codecs` package itself is composed of several smaller packages, each providing a different set of codec helpers. Here's the list of all packages containing codecs, should you need to install them individually: | Package | Description | | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | | [`@solana/kit`](https://www.npmjs.com/package/@solana/kit) | Includes `@solana/codecs`. | | [`@solana/codecs`](https://www.npmjs.com/package/@solana/codecs) | Includes all codecs packages below. | | [`@solana/codecs-core`](https://www.npmjs.com/package/@solana/codecs-core) | Core types and utilities for building codecs. | | [`@solana/codecs-numbers`](https://www.npmjs.com/package/@solana/codecs-numbers) | Codecs for numbers of various sizes and characteristics. | | [`@solana/codecs-strings`](https://www.npmjs.com/package/@solana/codecs-strings) | Codecs for strings of various encodings and size strategies. | | [`@solana/codecs-data-structures`](https://www.npmjs.com/package/@solana/codecs-data-structures) | Codecs for a variety of data structures such as objects, enums, arrays, maps, etc. | | [`@solana/options`](https://www.npmjs.com/package/@solana/options) | Codecs for Rust-like `Options` in JavaScript. | ## What is a Codec? A Codec is an object that knows how to encode a any type into a `Uint8Array` and how to decode a `Uint8Array` back into that value. No matter which serialization strategy we use, Codecs abstract away its implementation and offer a simple encode and decode interface. They are also highly composable, allowing us to build complex data structures from simple building blocks. Here's a quick example that encodes and decodes a simple `Person` type. ```ts twoslash import { Codec, addCodecSizePrefix, getUtf8Codec, getU32Codec, getStructCodec, ReadonlyUint8Array, } from '@solana/kit'; // ---cut-before--- // Use composable codecs to build complex data structures. type Person = { name: string; age: number }; const getPersonCodec = (): Codec => getStructCodec([ ['name', addCodecSizePrefix(getUtf8Codec(), getU32Codec())], ['age', getU32Codec()], ]); // Use your own codecs to encode and decode data. const personCodec = getPersonCodec(); const encodedPerson = personCodec.encode({ name: 'John', age: 42 }); const decodedPerson = personCodec.decode(encodedPerson); ``` ## Composing codecs The easiest way to create your own codecs is to compose the [various codecs](#available-codecs) at your disposal. For instance, consider the following codecs available: - `getStructCodec`: Creates a codec for objects with named fields. - `getU32Codec`: Creates a codec for unsigned 32-bit integers. - `getUtf8Codec`: Creates a codec for UTF-8 strings. - `addCodecSizePrefix`: Creates a codec that prefixes the encoded data with its length. - `getBooleanCodec`: Creates a codec for booleans using a single byte. By combining them together we can create a custom codec for the following `Person` type. ```ts twoslash import { Codec, addCodecSizePrefix, getUtf8Codec, getU32Codec, getStructCodec, getBooleanCodec, } from '@solana/kit'; // ---cut-before--- type Person = { name: string; age: number; verified: boolean; }; const getPersonCodec = (): Codec => getStructCodec([ ['name', addCodecSizePrefix(getUtf8Codec(), getU32Codec())], ['age', getU32Codec()], ['verified', getBooleanCodec()], ]); ``` This function returns a `Codec` object which contains both an `encode` and `decode` function that can be used to convert a `Person` type to and from a `Uint8Array`. ```ts twoslash import { Codec, addCodecSizePrefix, getUtf8Codec, getU32Codec, getStructCodec, getBooleanCodec, } from '@solana/kit'; type Person = { name: string; age: number; verified: boolean }; const getPersonCodec = (): Codec => getStructCodec([ ['name', addCodecSizePrefix(getUtf8Codec(), getU32Codec())], ['age', getU32Codec()], ['verified', getBooleanCodec()], ]); // ---cut-before--- const personCodec = getPersonCodec(); const bytes = personCodec.encode({ name: 'John', age: 42, verified: true }); const person = personCodec.decode(bytes); ``` There is a significant library of composable codecs at your disposal, enabling you to compose complex types. Check out the [available codecs](#available-codecs) section for more information. If you need a custom codec that cannot be composed from existing ones, you can always create your own as we will see in the ["Creating custom codecs"](#creating-custom-codecs) section below. ## Separate encoders and decoders Whilst Codecs can both encode and decode, it is possible to only focus on encoding or decoding data, enabling the unused logic to be tree-shaken. For instance, here's our previous example using Encoders only to encode a `Person` type. ```ts twoslash import { Encoder, addEncoderSizePrefix, getUtf8Encoder, getU32Encoder, getStructEncoder, getBooleanEncoder, } from '@solana/kit'; type Person = { name: string; age: number; verified: boolean }; // ---cut-before--- const getPersonEncoder = (): Encoder => getStructEncoder([ ['name', addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ['age', getU32Encoder()], ['verified', getBooleanEncoder()], ]); const bytes = getPersonEncoder().encode({ name: 'John', age: 42, verified: true }); ``` The same can be done for decoding the `Person` type by using Decoders like so. ```ts twoslash import { Decoder, addDecoderSizePrefix, getUtf8Decoder, getU32Decoder, getStructDecoder, getBooleanDecoder, } from '@solana/kit'; type Person = { name: string; age: number; verified: boolean }; const bytes = null as unknown as Uint8Array; // ---cut-before--- const getPersonDecoder = (): Decoder => getStructDecoder([ ['name', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ['age', getU32Decoder()], ['verified', getBooleanDecoder()], ]); const person = getPersonDecoder().decode(bytes); ``` ## Combining encoders and decoders Separating Codecs into Encoders and Decoders is particularly good practice for library maintainers as it allows their users to tree-shake any of the encoders and/or decoders they don't need. However, we may still want to offer a codec helper for users who need both for convenience. That's why this library offers a `combineCodec` helper that creates a `Codec` instance from a matching `Encoder` and `Decoder`. ```ts twoslash import { Codec, Encoder, Decoder, combineCodec } from '@solana/kit'; type Person = { name: string; age: number; verified: boolean }; const getPersonEncoder = null as unknown as () => Encoder; const getPersonDecoder = null as unknown as () => Decoder; // ---cut-before--- const getPersonCodec = (): Codec => combineCodec(getPersonEncoder(), getPersonDecoder()); ``` This means library maintainers can offer Encoders, Decoders and Codecs for all their types whilst staying efficient and tree-shakeable. In summary, we recommend the following pattern when creating codecs for library types. ```ts type MyType = /* ... */; const getMyTypeEncoder = (): Encoder => { /* ... */ }; const getMyTypeDecoder = (): Decoder => { /* ... */ }; const getMyTypeCodec = (): Codec => combineCodec( getMyTypeEncoder(), getMyTypeDecoder(), ); ``` ## Different `From` and `To` types When creating codecs, the encoded type is allowed to be looser than the decoded type. A good example of that is the u64 number codec: ```ts twoslash import { Codec, getU64Codec } from '@solana/kit'; // ---cut-before--- const u64Codec: Codec = getU64Codec(); ``` As you can see, the first type parameter is looser since it accepts numbers or big integers, whereas the second type parameter only accepts big integers. That's because when _encoding_ a u64 number, you may provide either a `bigint` or a `number` for convenience. However, when you decode a u64 number, you will always get a `bigint` because not all u64 values can fit in a JavaScript `number` type. ```ts twoslash import { getU64Codec } from '@solana/kit'; const u64Codec = getU64Codec(); // ---cut-before--- const bytes = u64Codec.encode(42); const value = u64Codec.decode(bytes); // BigInt(42) ``` This relationship between the type we encode “From” and decode “To” can be generalized in TypeScript as `To extends From`. Here's another example using an object with default values. You can read more about the [transformCodec](#transform-codec) helper below. ```ts twoslash import { Codec, Encoder, Decoder, transformEncoder, getStructEncoder, getStructDecoder, addEncoderSizePrefix, addDecoderSizePrefix, getUtf8Encoder, getUtf8Decoder, getU32Encoder, getU32Decoder, combineCodec, } from '@solana/kit'; // ---cut-before--- type Person = { name: string; age: number }; type PersonInput = { name: string; age?: number }; const getPersonEncoder = (): Encoder => transformEncoder( getStructEncoder([ ['name', addEncoderSizePrefix(getUtf8Encoder(), getU32Encoder())], ['age', getU32Encoder()], ]), (input) => ({ ...input, age: input.age ?? 42 }), ); const getPersonDecoder = (): Decoder => getStructDecoder([ ['name', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ['age', getU32Decoder()], ]); const getPersonCodec = (): Codec => combineCodec(getPersonEncoder(), getPersonDecoder()); ``` ## Fixed-size and variable-size codecs It is also worth noting that Codecs can either be of fixed size or variable size. `FixedSizeCodecs` have a `fixedSize` number attribute that tells us exactly how big their encoded data is in bytes. ```ts twoslash import { FixedSizeCodec, getU32Codec } from '@solana/kit'; // ---cut-before--- const myCodec = getU32Codec(); myCodec satisfies FixedSizeCodec; myCodec.fixedSize; // 4 bytes. ``` On the other hand, `VariableSizeCodecs` do not know the size of their encoded data in advance. Instead, they will grab that information either from the provided encoded data or from the value to encode. For the former, we can simply access the length of the `Uint8Array`. For the latter, it provides a `getSizeFromValue` that tells us the encoded byte size of the provided value. ```ts twoslash import { VariableSizeCodec, getUtf8Codec, getU32Codec, addCodecSizePrefix } from '@solana/kit'; // ---cut-before--- const myCodec = addCodecSizePrefix(getUtf8Codec(), getU32Codec()); myCodec satisfies VariableSizeCodec; myCodec.getSizeFromValue('hello world'); // 4 + 11 bytes. ``` Also note that, if the `VariableSizeCodec` is bounded by a maximum size, it can be provided as a `maxSize` number attribute. The following type guards are available to identify and/or assert the size of codecs: `isFixedSize`, `isVariableSize`, `assertIsFixedSize` and `assertIsVariableSize`. Finally, note that the same is true for `Encoders` and `Decoders`. - A `FixedSizeEncoder` has a `fixedSize` number attribute. - A `VariableSizeEncoder` has a `getSizeFromValue` function and an optional `maxSize` number attribute. - A `FixedSizeDecoder` has a `fixedSize` number attribute. - A `VariableSizeDecoder` has an optional `maxSize` number attribute. ## Creating custom codecs If composing codecs isn't enough for you, you may implement your own codec logic by using the `createCodec` function. This function requires an object with a `read` and a `write` function telling us how to read from and write to an existing byte array. The `read` function accepts the `bytes` to decode from and the `offset` at each we should start reading. It returns an array with two items: - The first item should be the decoded value. - The second item should be the next offset to read from. ```ts twoslash import { createCodec, Offset } from '@solana/kit'; const write = null as unknown as Parameters>[0]['write']; const fixedSize = null as unknown as 1; // ---cut-before--- createCodec({ read(bytes, offset) { const value = bytes[offset]; return [value, offset + 1]; }, write, fixedSize, }); ``` Reciprocally, the `write` function accepts the `value` to encode, the array of `bytes` to write the encoded value to and the `offset` at which it should be written. It should encode the given value, insert it in the byte array, and provide the next offset to write to as the return value. ```ts twoslash import { createCodec } from '@solana/kit'; const read = null as unknown as Parameters>[0]['read']; const fixedSize = null as unknown as 1; // ---cut-before--- createCodec({ write(value: number, bytes, offset) { bytes.set([value], offset); return offset + 1; }, read, fixedSize, }); ``` Additionally, we must specify the size of the codec. If we are defining a `FixedSizeCodec`, we must simply provide the `fixedSize` number attribute. For `VariableSizeCodecs`, we must provide the `getSizeFromValue` function as described in the previous section. ```ts twoslash import { createCodec } from '@solana/kit'; type Config = Parameters>[0]; const read = null as unknown as Config['read']; const write = null as unknown as Config['write']; // ---cut-before--- // FixedSizeCodec. createCodec({ fixedSize: 1, read, write, }); // VariableSizeCodec. createCodec({ getSizeFromValue: (value: string) => value.length, read, write, }); ``` Here's a concrete example of a custom codec that encodes any unsigned integer in a single byte. Since a single byte can only store integers from 0 to 255, if any other integer is provided it will take its modulo 256 to ensure it fits in a single byte. Because it always requires a single byte, that codec is a `FixedSizeCodec` of size `1`. ```ts twoslash import { createCodec } from '@solana/kit'; const getModuloU8Codec = () => createCodec({ fixedSize: 1, read(bytes, offset) { const value = bytes[offset]; return [value, offset + 1]; }, write(value, bytes, offset) { bytes.set([value % 256], offset); return offset + 1; }, }); ``` Note that, it is also possible to create custom encoders and decoders separately by using the `createEncoder` and `createDecoder` functions respectively and then use the `combineCodec` function on them just like we were doing with composed codecs. This approach is recommended to library maintainers as it allows their users to tree-shake any of the encoders and/or decoders they don't need. Here's our previous modulo u8 example but split into separate `Encoder`, `Decoder` and `Codec` instances. ```ts twoslash import { createEncoder, createDecoder, combineCodec } from '@solana/kit'; const getModuloU8Encoder = () => createEncoder({ fixedSize: 1, write(value, bytes, offset) { bytes.set([value % 256], offset); return offset + 1; }, }); const getModuloU8Decoder = () => createDecoder({ fixedSize: 1, read(bytes, offset) { const value = bytes[offset]; return [value, offset + 1]; }, }); const getModuloU8Codec = () => combineCodec(getModuloU8Encoder(), getModuloU8Decoder()); ``` Here's another example returning a `VariableSizeCodec`. This one transforms a simple string composed of characters from `a` to `z` to a buffer of numbers from `1` to `26` where `0` bytes are spaces. ```ts twoslash import { createEncoder, createDecoder, combineCodec } from '@solana/kit'; const alphabet = ' abcdefghijklmnopqrstuvwxyz'; const getCipherEncoder = () => createEncoder({ getSizeFromValue: (value) => value.length, write(value, bytes, offset) { const bytesToAdd = [...value].map((char) => alphabet.indexOf(char)); bytes.set(bytesToAdd, offset); return offset + bytesToAdd.length; }, }); const getCipherDecoder = () => createDecoder({ read(bytes, offset) { const value = [...bytes.slice(offset)].map((byte) => alphabet.charAt(byte)).join(''); return [value, bytes.length]; }, }); const getCipherCodec = () => combineCodec(getCipherEncoder(), getCipherDecoder()); ``` ## Available codecs ### Core utilities
[addCodecSentinel](#add-codec-sentinel) \ [addCodecSizePrefix](#add-codec-size-prefix) \ [containsBytes](#contains-bytes) \ [fixBytes](#fix-bytes) \ [fixCodecSize](#fix-codec-size) \ [mergeBytes](#merge-bytes) \ [offsetCodec](#offset-codec) \ [padBytes](#pad-bytes) \ [padLeftCodec](#pad-left-codec) \ [padRightCodec](#pad-right-codec) \ [resizeCodec](#resize-codec) \ [reverseCodec](#reverse-codec) \ [transformCodec](#transform-codec)
### Numbers
[getI8Codec](#get-i8-codec) \ [getI16Codec](#get-i16-codec) \ [getI32Codec](#get-i32-codec) \ [getI64Codec](#get-i64-codec) \ [getI128Codec](#get-i128-codec) \ [getF32Codec](#get-f32-codec) \ [getF64Codec](#get-f64-codec) \ [getShortU16Codec](#get-short-u16-codec) \ [getU8Codec](#get-u8-codec) \ [getU16Codec](#get-u16-codec) \ [getU32Codec](#get-u32-codec) \ [getU64Codec](#get-u64-codec) \ [getU128Codec](#get-u128-codec)
### Strings
[getBase10Codec](#get-base10-codec) \ [getBase16Codec](#get-base16-codec) \ [getBase58Codec](#get-base58-codec) \ [getBase64Codec](#get-base64-codec) \ [getBaseXCodec](#get-baseX-codec) \ [getBaseXResliceCodec](#get-baseX-reslice-codec) \ [getUtf8Codec](#get-utf8-codec)
### Data structures
[getArrayCodec](#get-array-codec) \ [getBitArrayCodec](#get-bit-array-codec) \ [getBooleanCodec](#get-boolean-codec) \ [getBytesCodec](#get-bytes-codec) \ [getConstantCodec](#get-constant-codec) \ [getDiscriminatedUnionCodec](#get-discriminated-union-codec) \ [getEnumCodec](#get-enum-codec) \ [getHiddenPrefixCodec](#get-hidden-prefix-codec) \ [getHiddenSuffixCodec](#get-hidden-suffix-codec) \ [getLiteralUnionCodec](#get-literal-union-codec) \ [getMapCodec](#get-map-codec) \ [getNullableCodec](#get-nullable-codec) \ [getOptionCodec](#get-option-codec) \ [getPatternMatchCodec](#get-pattern-match-codec) \ [getPredicateCodec](#get-predicate-codec) \ [getSetCodec](#get-set-codec) \ [getStructCodec](#get-struct-codec) \ [getTupleCodec](#get-tuple-codec) \ [getUnionCodec](#get-union-codec) \ [getUnitCodec](#get-unit-codec)
## Core utilities listing [!toc] ### addCodecSentinel [!toc] [#add-codec-sentinel] One way of delimiting the size of a codec is to use sentinels. The `addCodecSentinel` function allows us to add a sentinel to the end of the encoded data and to read until that sentinel is found when decoding. It accepts any codec and a `Uint8Array` sentinel responsible for delimiting the encoded data. ```ts twoslash import { addCodecSentinel, getUtf8Codec } from '@solana/kit'; // ---cut-before--- const codec = addCodecSentinel(getUtf8Codec(), new Uint8Array([255, 255])); codec.encode('hello'); // 0x68656c6c6fffff // | └-- Our sentinel. // └-- Our encoded string. ``` Note that the sentinel _must not_ be present in the encoded data and _must_ be present in the decoded data for this to work. If this is not the case, dedicated errors will be thrown. ```ts twoslash import { addCodecSentinel, getUtf8Codec } from '@solana/kit'; // ---cut-before--- const sentinel = new Uint8Array([108, 108]); // 'll' const codec = addCodecSentinel(getUtf8Codec(), sentinel); codec.encode('hello'); // Throws: sentinel is in encoded data. codec.decode(new Uint8Array([1, 2, 3])); // Throws: sentinel missing in decoded data. ``` Separate `addEncoderSentinel` and `addDecoderSentinel` functions are also available. ```ts twoslash import { addEncoderSentinel, addDecoderSentinel, getUtf8Encoder, getUtf8Decoder, } from '@solana/kit'; const sentinel = null as unknown as Uint8Array; // ---cut-before--- const bytes = addEncoderSentinel(getUtf8Encoder(), sentinel).encode('hello'); const value = addDecoderSentinel(getUtf8Decoder(), sentinel).decode(bytes); ``` ### addCodecSizePrefix [!toc] [#add-codec-size-prefix] The `addCodecSizePrefix` function allows us to store the byte size of any codec as a number prefix, enabling us to contain variable-size codecs to their actual size. When encoding, the size of the encoded data is stored before the encoded data itself. When decoding, the size is read first to know how many bytes to read next. For example, say we want to represent a variable-size base-58 string using a `u32` size prefix. Here's how we can use the `addCodecSizePrefix` function to achieve that. ```ts twoslash import { addCodecSizePrefix, getBase58Codec, getU32Codec } from '@solana/kit'; // ---cut-before--- const getU32Base58Codec = () => addCodecSizePrefix(getBase58Codec(), getU32Codec()); getU32Base58Codec().encode('hello world'); // 0x0b00000068656c6c6f20776f726c64 // | └-- Our encoded base-58 string. // └-- Our encoded u32 size prefix. ``` You may also use the `addEncoderSizePrefix` and `addDecoderSizePrefix` functions to separate your codec logic like so: ```ts twoslash import { addEncoderSizePrefix, addDecoderSizePrefix, getBase58Encoder, getBase58Decoder, getU32Encoder, getU32Decoder, combineCodec, } from '@solana/kit'; // ---cut-before--- const getU32Base58Encoder = () => addEncoderSizePrefix(getBase58Encoder(), getU32Encoder()); const getU32Base58Decoder = () => addDecoderSizePrefix(getBase58Decoder(), getU32Decoder()); const getU32Base58Codec = () => combineCodec(getU32Base58Encoder(), getU32Base58Decoder()); ``` ### containsBytes [!toc] [#contains-bytes] Checks if a `Uint8Array` contains another `Uint8Array` at a given offset. ```ts twoslash import { containsBytes } from '@solana/kit'; // ---cut-before--- containsBytes(new Uint8Array([1, 2, 3, 4]), new Uint8Array([2, 3]), 1); // true containsBytes(new Uint8Array([1, 2, 3, 4]), new Uint8Array([2, 3]), 2); // false ``` ### fixBytes [!toc] [#fix-bytes] Pads or truncates a `Uint8Array` so it has the specified length. ```ts twoslash import { fixBytes } from '@solana/kit'; // ---cut-before--- fixBytes(new Uint8Array([1, 2]), 4); // Uint8Array([1, 2, 0, 0]) fixBytes(new Uint8Array([1, 2, 3, 4]), 2); // Uint8Array([1, 2]) ``` ### fixCodecSize [!toc] [#fix-codec-size] The `fixCodecSize` function allows us to bind the size of a given codec to the given fixed size. For instance, say we wanted to represent a base-58 string that uses exactly 32 bytes when decoded. Here's how we can use the `fixCodecSize` helper to achieve that. ```ts twoslash import { fixCodecSize, getBase58Codec } from '@solana/kit'; // ---cut-before--- const get32BytesBase58Codec = () => fixCodecSize(getBase58Codec(), 32); ``` You may also use the `fixEncoderSize` and `fixDecoderSize` functions to separate your codec logic like so: ```ts twoslash import { fixEncoderSize, fixDecoderSize, getBase58Encoder, getBase58Decoder, combineCodec, } from '@solana/kit'; // ---cut-before--- const get32BytesBase58Encoder = () => fixEncoderSize(getBase58Encoder(), 32); const get32BytesBase58Decoder = () => fixDecoderSize(getBase58Decoder(), 32); const get32BytesBase58Codec = () => combineCodec(get32BytesBase58Encoder(), get32BytesBase58Decoder()); ``` ### mergeBytes [!toc] [#merge-bytes] Concatenates an array of `Uint8Arrays` into a single `Uint8Array`. ```ts twoslash import { mergeBytes } from '@solana/kit'; // ---cut-before--- const bytes1 = new Uint8Array([0x01, 0x02]); const bytes2 = new Uint8Array([]); const bytes3 = new Uint8Array([0x03, 0x04]); const bytes = mergeBytes([bytes1, bytes2, bytes3]); // ^ [0x01, 0x02, 0x03, 0x04] ``` ### offsetCodec [!toc] [#offset-codec] The `offsetCodec` function is a powerful codec primitive that allows us to move the offset of a given codec forward or backwards. It accepts one or two functions that takes the current offset and returns a new offset. To understand how this works, let's take the following `biggerU32Codec` example which encodes a `u32` number inside an 8-byte buffer by using the [resizeCodec](#resize-codec) helper. ```ts twoslash import { resizeCodec, getU32Codec } from '@solana/kit'; // ---cut-before--- const biggerU32Codec = resizeCodec(getU32Codec(), (size) => size + 4); biggerU32Codec.encode(0xffffffff); // 0xffffffff00000000 // | └-- Empty buffer space caused by the resizeCodec function. // └-- Our encoded u32 number. ``` Now, let's say we want to move the offset of that codec 2 bytes forward so that the encoded number sits in the middle of the buffer. To achieve, this we can use the `offsetCodec` helper and provide a `preOffset` function that moves the "pre-offset" of the codec 2 bytes forward. ```ts twoslash import { offsetCodec, resizeCodec, getU32Codec } from '@solana/kit'; const biggerU32Codec = resizeCodec(getU32Codec(), (size) => size + 4); // ---cut-before--- const u32InTheMiddleCodec = offsetCodec(biggerU32Codec, { preOffset: ({ preOffset }) => preOffset + 2, }); u32InTheMiddleCodec.encode(0xffffffff); // 0x0000ffffffff0000 // └-- Our encoded u32 number is now in the middle of the buffer. ``` We refer to this offset as the "pre-offset" because, once the inner codec is encoded or decoded, an additional offset will be returned which we refer to as the "post-offset". That "post-offset" is important as, unless we are reaching the end of our codec, it will be used by any further codecs to continue encoding or decoding data. By default, that "post-offset" is simply the addition of the "pre-offset" and the size of the encoded or decoded inner data. ```ts twoslash import { offsetCodec, resizeCodec, getU32Codec } from '@solana/kit'; const biggerU32Codec = resizeCodec(getU32Codec(), (size) => size + 4); // ---cut-before--- const u32InTheMiddleCodec = offsetCodec(biggerU32Codec, { preOffset: ({ preOffset }) => preOffset + 2, }); u32InTheMiddleCodec.encode(0xffffffff); // 0x0000ffffffff0000 // | | └-- Post-offset. // | └-- New pre-offset: The original pre-offset + 2. // └-- Pre-offset: The original pre-offset before we adjusted it. ``` However, you may also provide a `postOffset` function to adjust the "post-offset". For instance, let's push the "post-offset" 2 bytes forward as well such that any further codecs will start doing their job at the end of our 8-byte `u32` number. ```ts twoslash import { offsetCodec, resizeCodec, getU32Codec } from '@solana/kit'; const biggerU32Codec = resizeCodec(getU32Codec(), (size) => size + 4); // ---cut-before--- const u32InTheMiddleCodec = offsetCodec(biggerU32Codec, { preOffset: ({ preOffset }) => preOffset + 2, postOffset: ({ postOffset }) => postOffset + 2, }); u32InTheMiddleCodec.encode(0xffffffff); // 0x0000ffffffff0000 // | | | └-- New post-offset: The original post-offset + 2. // | | └-- Post-offset: The original post-offset before we adjusted it. // | └-- New pre-offset: The original pre-offset + 2. // └-- Pre-offset: The original pre-offset before we adjusted it. ``` Both the `preOffset` and `postOffset` functions offer the following attributes: - `bytes`: The entire byte array being encoded or decoded. - `preOffset`: The original and unaltered pre-offset. - `wrapBytes`: A helper function that wraps the given offset around the byte array length. E.g. `wrapBytes(-1)` will refer to the last byte of the byte array. Additionally, the post-offset function also provides the following attributes: - `newPreOffset`: The new pre-offset after the pre-offset function has been applied. - `postOffset`: The original and unaltered post-offset. Note that you may also decide to ignore these attributes to achieve absolute offsets. However, relative offsets are usually recommended as they won't break your codecs when composed with other codecs. ```ts twoslash import { offsetCodec, resizeCodec, getU32Codec } from '@solana/kit'; const biggerU32Codec = resizeCodec(getU32Codec(), (size) => size + 4); // ---cut-before--- const u32InTheMiddleCodec = offsetCodec(biggerU32Codec, { preOffset: () => 2, postOffset: () => 8, }); u32InTheMiddleCodec.encode(0xffffffff); // 0x0000ffffffff0000 ``` Also note that any negative offset or offset that exceeds the size of the byte array will throw a `SolanaError` of code `SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE`. ```ts twoslash import { offsetCodec, resizeCodec, getU32Codec } from '@solana/kit'; const biggerU32Codec = resizeCodec(getU32Codec(), (size) => size + 4); // ---cut-before--- const u32InTheEndCodec = offsetCodec(biggerU32Codec, { preOffset: () => -4 }); u32InTheEndCodec.encode(0xffffffff); // throws new SolanaError(SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE) ``` To avoid this, you may use the `wrapBytes` function to wrap the offset around the byte array length. For instance, here's how we can use the `wrapBytes` function to move the pre-offset 4 bytes from the end of the byte array. ```ts twoslash import { offsetCodec, resizeCodec, getU32Codec } from '@solana/kit'; const biggerU32Codec = resizeCodec(getU32Codec(), (size) => size + 4); // ---cut-before--- const u32InTheEndCodec = offsetCodec(biggerU32Codec, { preOffset: ({ wrapBytes }) => wrapBytes(-4), }); u32InTheEndCodec.encode(0xffffffff); // 0x00000000ffffffff ``` As you can see, the `offsetCodec` helper allows you to jump all over the place with your codecs. This non-linear approach to encoding and decoding data allows you to achieve complex serialization strategies that would otherwise be impossible. The `offsetEncoder` and `offsetDecoder` functions can also be used to split your codec logic into tree-shakeable functions. ```ts twoslash import { offsetEncoder, resizeEncoder, getU32Encoder, offsetDecoder, resizeDecoder, getU32Decoder, combineCodec, } from '@solana/kit'; const biggerU32Encoder = resizeEncoder(getU32Encoder(), (size) => size + 4); const biggerU32Decoder = resizeDecoder(getU32Decoder(), (size) => size + 4); // ---cut-before--- const getU32InTheMiddleEncoder = () => offsetEncoder(biggerU32Encoder, { preOffset: ({ preOffset }) => preOffset + 2 }); const getU32InTheMiddleDecoder = () => offsetDecoder(biggerU32Decoder, { preOffset: ({ preOffset }) => preOffset + 2 }); const getU32InTheMiddleCodec = () => combineCodec(getU32InTheMiddleEncoder(), getU32InTheMiddleDecoder()); ``` ### padBytes [!toc] [#pad-bytes] Pads a `Uint8Array` with zeroes (to the right) to the specified length. ```ts twoslash import { padBytes } from '@solana/kit'; // ---cut-before--- padBytes(new Uint8Array([1, 2]), 4); // Uint8Array([1, 2, 0, 0]) padBytes(new Uint8Array([1, 2, 3, 4]), 2); // Uint8Array([1, 2, 3, 4]) ``` ### padLeftCodec [!toc] [#pad-left-codec] The `padLeftCodec` helper can be used to add padding to the left of a given codec. It accepts an `offset` number that tells us how big the padding should be. ```ts twoslash import { padLeftCodec, getU16Codec } from '@solana/kit'; // ---cut-before--- const leftPaddedCodec = padLeftCodec(getU16Codec(), 4); leftPaddedCodec.encode(0xffff); // 0x00000000ffff // | └-- Our encoded u16 number. // └-- Our 4-byte padding. ``` Note that the `padLeftCodec` function is a simple wrapper around the `offsetCodec` and `resizeCodec` functions. For more complex padding strategies, you may want to use the [offsetCodec](#offset-codec) and [resizeCodec](#resize-codec) functions directly instead. Encoder-only and decoder-only helpers are available for these padding functions. ```ts twoslash import { padLeftEncoder, padLeftDecoder, getU16Encoder, getU16Decoder, combineCodec, } from '@solana/kit'; // ---cut-before--- const getMyPaddedEncoder = () => padLeftEncoder(getU16Encoder(), 6); const getMyPaddedDecoder = () => padLeftDecoder(getU16Decoder(), 6); const getMyPaddedCodec = () => combineCodec(getMyPaddedEncoder(), getMyPaddedDecoder()); ``` ### padRightCodec [!toc] [#pad-right-codec] The `padRightCodec` helper can be used to add padding to the right of a given codec. It accepts an `offset` number that tells us how big the padding should be. ```ts twoslash import { padRightCodec, getU16Codec } from '@solana/kit'; // ---cut-before--- const rightPaddedCodec = padRightCodec(getU16Codec(), 4); rightPaddedCodec.encode(0xffff); // 0xffff00000000 // | └-- Our 4-byte padding. // └-- Our encoded u16 number. ``` Note that the `padRightCodec` function is a simple wrapper around the `offsetCodec` and `resizeCodec` functions. For more complex padding strategies, you may want to use the [offsetCodec](#offset-codec) and [resizeCodec](#resize-codec) functions directly instead. Encoder-only and decoder-only helpers are available for these padding functions. ```ts twoslash import { padRightEncoder, padRightDecoder, getU16Encoder, getU16Decoder, combineCodec, } from '@solana/kit'; // ---cut-before--- const getMyPaddedEncoder = () => padRightEncoder(getU16Encoder(), 6); const getMyPaddedDecoder = () => padRightDecoder(getU16Decoder(), 6); const getMyPaddedCodec = () => combineCodec(getMyPaddedEncoder(), getMyPaddedDecoder()); ``` ### resizeCodec [!toc] [#resize-codec] The `resizeCodec` helper re-defines the size of a given codec by accepting a function that takes the current size of the codec and returns a new size. This works for both fixed-size and variable-size codecs. ```ts twoslash import { resizeCodec, getU32Codec, getUtf8Codec } from '@solana/kit'; // ---cut-before--- // Fixed-size codec. const getBiggerU32Codec = () => resizeCodec(getU32Codec(), (size) => size + 4); getBiggerU32Codec().encode(42); // 0x2a00000000000000 // | └-- Empty buffer space caused by the resizeCodec function. // └-- Our encoded u32 number. // Variable-size codec. const getBiggerUtf8Codec = () => resizeCodec(getUtf8Codec(), (size) => size + 4); getBiggerUtf8Codec().encode('ABC'); // 0x41424300000000 // | └-- Empty buffer space caused by the resizeCodec function. // └-- Our encoded string. ``` Note that the `resizeCodec` function doesn't change any encoded or decoded bytes, it merely tells the `encode` and `decode` functions how big the `Uint8Array` should be before delegating to their respective `write` and `read` functions. In fact, this is completely bypassed when using the `write` and `read` functions directly. For instance: ```ts twoslash import { resizeCodec, getU32Codec } from '@solana/kit'; // ---cut-before--- const getBiggerU32Codec = () => resizeCodec(getU32Codec(), (size) => size + 4); // Using the encode function. getBiggerU32Codec().encode(42); // 0x2a00000000000000 // Using the lower-level write function. const myCustomBytes = new Uint8Array(4); getBiggerU32Codec().write(42, myCustomBytes, 0); // 0x2a000000 ``` So when would it make sense to use the `resizeCodec` function? This function is particularly useful when combined with the [offsetCodec](#offset-codec) function. Whilst `offsetCodec` may help us push the offset forward — e.g. to skip some padding — it won't change the size of the encoded data which means the last bytes will be truncated by how much we pushed the offset forward. The `resizeCodec` function can be used to fix that. For instance, here's how we can use the `resizeCodec` and the `offsetCodec` functions together to create a struct codec that includes some padding. ```ts twoslash import { getStructCodec, getUtf8Codec, getU32Codec, offsetCodec, resizeCodec, fixCodecSize, } from '@solana/kit'; // ---cut-before--- const personCodec = getStructCodec([ ['name', fixCodecSize(getUtf8Codec(), 8)], // There is a 4-byte padding between name and age. [ 'age', offsetCodec( resizeCodec(getU32Codec(), (size) => size + 4), { preOffset: ({ preOffset }) => preOffset + 4 }, ), ], ]); personCodec.encode({ name: 'Alice', age: 42 }); // 0x416c696365000000000000002a000000 // | | └-- Our encoded u32 (42). // | └-- The 4-bytes of padding we are skipping. // └-- Our 8-byte encoded string ("Alice"). ``` Note that this can be achieved using the [padLeftCodec](#pad-left-codec) helper which is implemented that way. The `resizeEncoder` and `resizeDecoder` functions can also be used to split your codec logic into tree-shakeable functions. ```ts twoslash import { resizeEncoder, resizeDecoder, getU32Encoder, getU32Decoder, combineCodec, } from '@solana/kit'; // ---cut-before--- const getBiggerU32Encoder = () => resizeEncoder(getU32Encoder(), (size) => size + 4); const getBiggerU32Decoder = () => resizeDecoder(getU32Decoder(), (size) => size + 4); const getBiggerU32Codec = () => combineCodec(getBiggerU32Encoder(), getBiggerU32Decoder()); ``` ### reverseCodec [!toc] [#reverse-codec] The `reverseCodec` helper reverses the bytes of the provided `FixedSizeCodec`. ```ts twoslash import { reverseCodec, getU64Codec } from '@solana/kit'; // ---cut-before--- const getBigEndianU64Codec = () => reverseCodec(getU64Codec()); ``` Note that number codecs can already do that for you via their `endian` option. ```ts twoslash import { getU64Codec, Endian } from '@solana/kit'; // ---cut-before--- const getBigEndianU64Codec = () => getU64Codec({ endian: Endian.Big }); ``` The `reverseEncoder` and `reverseDecoder` functions can also be used to achieve that. ```ts twoslash import { reverseEncoder, reverseDecoder, getU64Encoder, getU64Decoder, combineCodec, } from '@solana/kit'; // ---cut-before--- const getBigEndianU64Encoder = () => reverseEncoder(getU64Encoder()); const getBigEndianU64Decoder = () => reverseDecoder(getU64Decoder()); const getBigEndianU64Codec = () => combineCodec(getBigEndianU64Encoder(), getBigEndianU64Decoder()); ``` ### transformCodec [!toc] [#transform-codec] It is possible to transform a `Codec` to a `Codec` by providing two mapping functions: one that goes from `T` to `U` and one that does the opposite. For instance, here's how you would map a `u32` integer into a `string` representation of that number. ```ts twoslash import { transformCodec, getU32Codec } from '@solana/kit'; // ---cut-before--- const getStringU32Codec = () => transformCodec( getU32Codec(), (integerAsString: string): number => parseInt(integerAsString), (integer: number): string => integer.toString(), ); getStringU32Codec().encode('42'); // new Uint8Array([42]) getStringU32Codec().decode(new Uint8Array([42])); // "42" ``` If a `Codec` has [different From and To types](#different-from-and-to-types), say `Codec`, and we want to map it to `Codec`, we must provide functions that map from `NewFrom` to `OldFrom` and from `OldTo` to `NewTo`. To illustrate that, let's take our previous `getStringU32Codec` example but make it use a `getU64Codec` codec instead as it returns a `Codec`. Additionally, let's make it so our `getStringU64Codec` function returns a `Codec` so that it also accepts numbers when encoding values. Here's what our mapping functions look like: ```ts twoslash import { transformCodec, getU64Codec } from '@solana/kit'; // ---cut-before--- const getStringU64Codec = () => transformCodec( getU64Codec(), (integerInput: number | string): number | bigint => typeof integerInput === 'string' ? BigInt(integerInput) : integerInput, (integer: bigint): string => integer.toString(), ); ``` Note that the second function that maps the decoded type is optional. That means, you can omit it to simply update or loosen the type to encode whilst keeping the decoded type the same. This is particularly useful to provide default values to object structures. For instance, here's how we can map a `Person` codec to give a default value to its `age` attribute. ```ts twoslash import { transformCodec, getStructCodec, Codec } from '@solana/kit'; // ---cut-before--- type Person = { name: string; age: number }; type PersonInput = { name: string; age?: number }; // ---cut-start--- const getPersonCodec = null as unknown as () => Codec; // ---cut-end--- const getPersonWithDefaultValueCodec = (): Codec => transformCodec( getPersonCodec(), (person: PersonInput): Person => ({ ...person, age: person.age ?? 42 }), ); ``` Similar helpers exist to map `Encoder` and `Decoder` instances allowing you to separate your codec logic into tree-shakeable functions. Here's our `getStringU32Codec` written that way. ```ts twoslash import { transformEncoder, transformDecoder, getU32Encoder, getU32Decoder, combineCodec, } from '@solana/kit'; // ---cut-before--- const getStringU32Encoder = () => transformEncoder(getU32Encoder(), (integerAsString: string): number => parseInt(integerAsString), ); const getStringU32Decoder = () => transformDecoder(getU32Decoder(), (integer: number): string => integer.toString()); const getStringU32Codec = () => combineCodec(getStringU32Encoder(), getStringU32Decoder()); ``` ## Numbers listing [!toc] ### getI8Codec [!toc] [#get-i8-codec] Encodes and decodes **signed 8-bit integers**. It supports values from -127 (`-2^7`) to 128 (`2^7 - 1`). Values can be provided as either `number` or `bigint`, but the decoded value is always a `number`. ```ts twoslash import { getI8Codec } from '@solana/kit'; // ---cut-before--- const codec = getI8Codec(); const bytes = codec.encode(-42); // 0xd6 const value = codec.decode(bytes); // -42 ``` `getI8Encoder` and `getI8Decoder` functions are also available. ### getI16Codec [!toc] [#get-i16-codec] Encodes and decodes **signed 16-bit integers**. It supports values from -32,768 (`-2^15`) to 32,767 (`2^15 - 1`). Values can be provided as either `number` or `bigint`, but the decoded value is always a `number`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getI16Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getI16Codec(); const bytes = codec.encode(-42); // 0xd6ff const value = codec.decode(bytes); // -42 // Big-endian. const beCodec = getI16Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(-42); // 0xffd6 const beValue = beCodec.decode(bytes); // -42 ``` `getI16Encoder` and `getI16Decoder` functions are also available. ### getI32Codec [!toc] [#get-i32-codec] Encodes and decodes **signed 32-bit integers**. It supports values from -2,147,483,648 (`-2^31`) to 2,147,483,647 (`2^31 - 1`). Values can be provided as either `number` or `bigint`, but the decoded value is always a `number`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getI32Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getI32Codec(); const bytes = codec.encode(-42); // 0xd6ffffff const value = codec.decode(bytes); // -42 // Big-endian. const beCodec = getI32Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(-42); // 0xffffffd6 const beValue = beCodec.decode(bytes); // -42 ``` `getI32Encoder` and `getI32Decoder` functions are also available. ### getI64Codec [!toc] [#get-i64-codec] Encodes and decodes **signed 64-bit integers**. It supports values from `-2^63` to `2^63 - 1`. Values can be provided as either `number` or `bigint`, but the decoded value is always a `bigint`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getI64Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getI64Codec(); const bytes = codec.encode(-42); // 0xd6ffffffffffffff const value = codec.decode(bytes); // BigInt(-42) // Big-endian. const beCodec = getI64Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(-42); // 0xffffffffffffffd6 const beValue = beCodec.decode(bytes); // BigInt(-42) ``` `getI64Encoder` and `getI64Decoder` functions are also available. ### getI128Codec [!toc] [#get-i128-codec] Encodes and decodes **signed 128-bit integers**. It supports values from `-2^127` to `2^127 - 1`. Values can be provided as either `number` or `bigint`, but the decoded value is always a `bigint`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getI128Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getI128Codec(); const bytes = codec.encode(-42); // 0xd6ffffffffffffffffffffffffffffff const value = codec.decode(bytes); // BigInt(-42) // Big-endian. const beCodec = getI128Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(-42); // 0xffffffffffffffffffffffffffffffd6 const beValue = beCodec.decode(bytes); // BigInt(-42) ``` `getI128Encoder` and `getI128Decoder` functions are also available. ### getF32Codec [!toc] [#get-f32-codec] Encodes and decodes **32-bit floating-point numbers**. Due to the [IEEE 754](https://en.wikipedia.org/wiki/Single-precision_floating-point_format) floating-point representation, some precision loss may occur. Values can be provided as either `number` or `bigint`, but the decoded value is always a `number`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getF32Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getF32Codec(); const bytes = codec.encode(-1.5); // 0x0000c0bf const value = codec.decode(bytes); // -1.5 // Big-endian. const beCodec = getF32Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(-1.5); // 0xbfc00000 const beValue = beCodec.decode(bytes); // -1.5 ``` `getF32Encoder` and `getF32Decoder` functions are also available. ### getF64Codec [!toc] [#get-f64-codec] Encodes and decodes **64-bit floating-point numbers**. Due to the [IEEE 754](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) floating-point representation, some precision loss may occur. Values can be provided as either `number` or `bigint`, but the decoded value is always a `number`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getF64Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getF64Codec(); const bytes = codec.encode(-1.5); // 0x000000000000f8bf const value = codec.decode(bytes); // -1.5 // Big-endian. const beCodec = getF64Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(-1.5); // 0xbff8000000000000 const beValue = beCodec.decode(bytes); // -1.5 ``` `getF64Encoder` and `getF64Decoder` functions are also available. ### getShortU16Codec [!toc] [#get-short-u16-codec] Encodes and decodes **unsigned integer using 1 to 3 bytes** based on the encoded value. It supports values from 0 to 4,194,303 (`2^22 - 1`). The larger the value, the more bytes it uses. - If the value is `<= 0x7f` (127), it is stored in a **single byte** and the first bit is set to `0` to indicate the end of the value. - Otherwise, the first bit is set to `1` to indicate that the value continues in the next byte, which follows the same pattern. - This process repeats until the value is fully encoded in up to 3 bytes. The third and last byte, if needed, uses all 8 bits to store the remaining value. In other words, the encoding scheme follows this structure: ```txt 0XXXXXXX <- Values 0 to 127 (1 byte) 1XXXXXXX 0XXXXXXX <- Values 128 to 16,383 (2 bytes) 1XXXXXXX 1XXXXXXX XXXXXXXX <- Values 16,384 to 4,194,303 (3 bytes) ``` Values can be provided as either `number` or `bigint`, but the decoded value is always a `number`. ```ts twoslash import { getShortU16Codec } from '@solana/kit'; // ---cut-before--- const codec = getShortU16Codec(); const bytes1 = codec.encode(42); // 0x2a const bytes2 = codec.encode(128); // 0x8001 const bytes3 = codec.encode(16384); // 0x808001 codec.decode(bytes1); // 42 codec.decode(bytes2); // 128 codec.decode(bytes3); // 16384 ``` `getShortU16Encoder` and `getShortU16Decoder` functions are also available. ### getU8Codec [!toc] [#get-u8-codec] Encodes and decodes **unsigned 8-bit integers**. It supports values from 0 to 255 (`2^8 - 1`). Values can be provided as either `number` or `bigint`, but the decoded value is always a `number`. ```ts twoslash import { getU8Codec } from '@solana/kit'; // ---cut-before--- const codec = getU8Codec(); const bytes = codec.encode(42); // 0x2a const value = codec.decode(bytes); // 42 ``` `getU8Encoder` and `getU8Decoder` functions are also available. ### getU16Codec [!toc] [#get-u16-codec] Encodes and decodes **unsigned 16-bit integers**. It supports values from 0 to 65,535 (`2^16 - 1`). Values can be provided as either `number` or `bigint`, but the decoded value is always a `number`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getU16Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getU16Codec(); const bytes = codec.encode(42); // 0x2a00 const value = codec.decode(bytes); // 42 // Big-endian. const beCodec = getU16Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(42); // 0x002a const beValue = beCodec.decode(bytes); // 42 ``` `getU16Encoder` and `getU16Decoder` functions are also available. ### getU32Codec [!toc] [#get-u32-codec] Encodes and decodes **unsigned 32-bit integers**. It supports values from 0 to 4,294,967,295 (`2^32 - 1`). Values can be provided as either `number` or `bigint`, but the decoded value is always a `number`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getU32Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getU32Codec(); const bytes = codec.encode(42); // 0x2a000000 const value = codec.decode(bytes); // 42 // Big-endian. const beCodec = getU32Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(42); // 0x0000002a const beValue = beCodec.decode(bytes); // 42 ``` `getU32Encoder` and `getU32Decoder` functions are also available. ### getU64Codec [!toc] [#get-u64-codec] Encodes and decodes **unsigned 64-bit integers**. It supports values from 0 to `2^64 - 1`. Values can be provided as either `number` or `bigint`, but the decoded value is always a `bigint`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getU64Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getU64Codec(); const bytes = codec.encode(42); // 0x2a00000000000000 const value = codec.decode(bytes); // 42 // Big-endian. const beCodec = getU64Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(42); // 0x000000000000002a const beValue = beCodec.decode(bytes); // 42 ``` `getU64Encoder` and `getU64Decoder` functions are also available. ### getU128Codec [!toc] [#get-u128-codec] Encodes and decodes **unsigned 128-bit integers**. It supports values from 0 to `2^128 - 1`. Values can be provided as either `number` or `bigint`, but the decoded value is always a `bigint`. Endianness can be specified using the `endian` option. The default is `Endian.Little`. ```ts twoslash import { getU128Codec, Endian } from '@solana/kit'; // ---cut-before--- // Little-endian. const codec = getU128Codec(); const bytes = codec.encode(42); // 0x2a000000000000000000000000000000 const value = codec.decode(bytes); // 42 // Big-endian. const beCodec = getU128Codec({ endian: Endian.Big }); const beBytes = beCodec.encode(42); // 0x0000000000000000000000000000002a const beValue = beCodec.decode(bytes); // 42 ``` `getU128Encoder` and `getU128Decoder` functions are also available. ## Strings listing [!toc] ### getBase10Codec [!toc] [#get-base10-codec] Encodes and decodes **Base 10 strings**. ```ts twoslash import { getBase10Codec } from '@solana/kit'; // ---cut-before--- const codec = getBase10Codec(); const bytes = codec.encode('1024'); // 0x0400 const value = codec.decode(bytes); // "1024" ``` This codec does not enforce a size boundary. It will encode and decode all bytes necessary to represent the string. To add size constraints to your codec, you may use utility functions such as [`fixCodecSize`](#fix-codec-size), [`addCodecSizePrefix`](#add-codec-size-prefix) or [`addCodecSentinel`](#add-codec-sentinel). ```ts twoslash import { getBase10Codec, fixCodecSize, addCodecSizePrefix, getU32Codec, addCodecSentinel, } from '@solana/kit'; // ---cut-before--- fixCodecSize(getBase10Codec(), 4).encode('1024'); // 0x04000000 (padded to 4 bytes) addCodecSizePrefix(getBase10Codec(), getU32Codec()).encode('1024'); // 0x020000000400 // | └-- The 2 bytes of content. // └-- 4-byte prefix telling us to read 2 bytes. addCodecSentinel(getBase10Codec(), new Uint8Array([0xff, 0xff])).encode('1024'); // 0x0400ffff // | └-- The sentinel signaling the end of the content. // └-- The 2 bytes of content. ``` `getBase10Encoder` and `getBase10Decoder` functions are also available. ### getBase16Codec [!toc] [#get-base16-codec] Encodes and decodes **Base 16 strings**. ```ts twoslash import { getBase16Codec } from '@solana/kit'; // ---cut-before--- const codec = getBase16Codec(); const bytes = codec.encode('deadface'); // 0xdeadface const value = codec.decode(bytes); // "deadface" ``` This codec does not enforce a size boundary. It will encode and decode all bytes necessary to represent the string. To add size constraints to your codec, you may use utility functions such as [`fixCodecSize`](#fix-codec-size), [`addCodecSizePrefix`](#add-codec-size-prefix) or [`addCodecSentinel`](#add-codec-sentinel). ```ts twoslash import { getBase16Codec, fixCodecSize, addCodecSizePrefix, getU32Codec, addCodecSentinel, } from '@solana/kit'; // ---cut-before--- fixCodecSize(getBase16Codec(), 2).encode('deadface'); // 0xdead (truncated to 2 bytes) addCodecSizePrefix(getBase16Codec(), getU32Codec()).encode('deadface'); // 0x04000000deadface // | └-- The 4 bytes of content. // └-- 4-byte prefix telling us to read 4 bytes. addCodecSentinel(getBase16Codec(), new Uint8Array([0xff, 0xff])).encode('deadface'); // 0xdeadfaceffff // | └-- The sentinel signaling the end of the content. // └-- The 4 bytes of content. ``` `getBase16Encoder` and `getBase16Decoder` functions are also available. ### getBase58Codec [!toc] [#get-base58-codec] Encodes and decodes **Base 58 strings**. ```ts twoslash import { getBase58Codec } from '@solana/kit'; // ---cut-before--- const codec = getBase58Codec(); const bytes = codec.encode('heLLo'); // 0x1b6a3070 const value = codec.decode(bytes); // "heLLo" ``` This codec does not enforce a size boundary. It will encode and decode all bytes necessary to represent the string. To add size constraints to your codec, you may use utility functions such as [`fixCodecSize`](#fix-codec-size), [`addCodecSizePrefix`](#add-codec-size-prefix) or [`addCodecSentinel`](#add-codec-sentinel). ```ts twoslash import { getBase58Codec, fixCodecSize, addCodecSizePrefix, getU32Codec, addCodecSentinel, } from '@solana/kit'; // ---cut-before--- fixCodecSize(getBase58Codec(), 2).encode('heLLo'); // 0x1b6a (truncated to 2 bytes) addCodecSizePrefix(getBase58Codec(), getU32Codec()).encode('heLLo'); // 0x040000001b6a3070 // | └-- The 4 bytes of content. // └-- 4-byte prefix telling us to read 4 bytes. addCodecSentinel(getBase58Codec(), new Uint8Array([0xff, 0xff])).encode('heLLo'); // 0x1b6a3070ffff // | └-- The sentinel signaling the end of the content. // └-- The 4 bytes of content. ``` `getBase58Encoder` and `getBase58Decoder` functions are also available. ### getBase64Codec [!toc] [#get-base64-codec] Encodes and decodes **Base 64 strings**. ```ts twoslash import { getBase64Codec } from '@solana/kit'; // ---cut-before--- const codec = getBase64Codec(); const bytes = codec.encode('hello+world'); // 0x85e965a3ec28ae57 const value = codec.decode(bytes); // "hello+world" ``` This codec does not enforce a size boundary. It will encode and decode all bytes necessary to represent the string. To add size constraints to your codec, you may use utility functions such as [`fixCodecSize`](#fix-codec-size), [`addCodecSizePrefix`](#add-codec-size-prefix) or [`addCodecSentinel`](#add-codec-sentinel). ```ts twoslash import { getBase64Codec, fixCodecSize, addCodecSizePrefix, getU32Codec, addCodecSentinel, } from '@solana/kit'; // ---cut-before--- fixCodecSize(getBase64Codec(), 4).encode('hello+world'); // 0x85e965a3 (truncated to 4 bytes) addCodecSizePrefix(getBase64Codec(), getU32Codec()).encode('hello+world'); // 0x0400000085e965a3ec28ae57 // | └-- The 8 bytes of content. // └-- 4-byte prefix telling us to read 8 bytes. addCodecSentinel(getBase64Codec(), new Uint8Array([0xff, 0xff])).encode('hello+world'); // 0x85e965a3ec28ae57ffff // | └-- The sentinel signaling the end of the content. // └-- The 8 bytes of content. ``` `getBase64Encoder` and `getBase64Decoder` functions are also available. ### getBaseXCodec [!toc] [#get-baseX-codec] The `getBaseXCodec` accepts a custom `alphabet` of `X` characters and **creates a base-X codec using that alphabet**. It does so by iteratively dividing by `X` and handling leading zeros. ```ts twoslash import { getBaseXCodec } from '@solana/kit'; // ---cut-before--- const codec = getBaseXCodec('0ehlo'); const bytes = codec.encode('hello'); // 0x05bd const value = codec.decode(bytes); // "hello" ``` This codec does not enforce a size boundary. It will encode and decode all bytes necessary to represent the string. To add size constraints to your codec, you may use utility functions such as [`fixCodecSize`](#fix-codec-size), [`addCodecSizePrefix`](#add-codec-size-prefix) or [`addCodecSentinel`](#add-codec-sentinel). ```ts twoslash import { getBaseXCodec, fixCodecSize, addCodecSizePrefix, getU32Codec, addCodecSentinel, } from '@solana/kit'; // ---cut-before--- const codec = getBaseXCodec('0ehlo'); fixCodecSize(codec, 4).encode('hello'); // 0x05bd0000 (padded to 4 bytes) addCodecSizePrefix(codec, getU32Codec()).encode('hello'); // 0x0200000005bd // | └-- The 2 bytes of content. // └-- 4-byte prefix telling us to read 2 bytes. addCodecSentinel(codec, new Uint8Array([0xff, 0xff])).encode('hello'); // 0x05bdffff // | └-- The sentinel signaling the end of the content. // └-- The 2 bytes of content. ``` `getBaseXEncoder` and `getBaseXDecoder` functions are also available. ### getBaseXResliceCodec [!toc] [#get-baseX-reslice-codec] The `getBaseXResliceCodec` accepts a custom `alphabet` of `X` characters and **creates a base-X codec using that alphabet**. It does so by re-slicing bytes into custom chunks of bits that are then mapped to the provided `alphabet`. The number of bits per chunk is also provided as the second argument and should typically be set to `log2(alphabet.length)`. This is typically used to create codecs whose alphabet's length is a power of 2 such as base-16 or base-64. ```ts twoslash import { getBaseXResliceCodec } from '@solana/kit'; // ---cut-before--- const codec = getBaseXResliceCodec('elho', 2); const bytes = codec.encode('hellolol'); // 0x4aee const value = codec.decode(bytes); // "hellolol" ``` This codec does not enforce a size boundary. It will encode and decode all bytes necessary to represent the string. To add size constraints to your codec, you may use utility functions such as [`fixCodecSize`](#fix-codec-size), [`addCodecSizePrefix`](#add-codec-size-prefix) or [`addCodecSentinel`](#add-codec-sentinel). ```ts twoslash import { getBaseXResliceCodec, fixCodecSize, addCodecSizePrefix, getU32Codec, addCodecSentinel, } from '@solana/kit'; // ---cut-before--- const codec = getBaseXResliceCodec('elho', 2); fixCodecSize(codec, 4).encode('hellolol'); // 0x4aee0000 (padded to 4 bytes) addCodecSizePrefix(codec, getU32Codec()).encode('hellolol'); // 0x020000004aee // | └-- The 2 bytes of content. // └-- 4-byte prefix telling us to read 2 bytes. addCodecSentinel(codec, new Uint8Array([0xff, 0xff])).encode('hellolol'); // 0x4aeeffff // | └-- The sentinel signaling the end of the content. // └-- The 2 bytes of content. ``` `getBaseXResliceEncoder` and `getBaseXResliceDecoder` functions are also available. ### getUtf8Codec [!toc] [#get-utf8-codec] Encodes and decodes **UTF-8 strings**. ```ts twoslash import { getUtf8Codec } from '@solana/kit'; // ---cut-before--- const codec = getUtf8Codec(); const bytes = codec.encode('hello'); // 0x68656c6c6f const value = codec.decode(bytes); // "hello" ``` This codec does not enforce a size boundary. It will encode and decode all bytes necessary to represent the string. To add size constraints to your codec, you may use utility functions such as [`fixCodecSize`](#fix-codec-size), [`addCodecSizePrefix`](#add-codec-size-prefix) or [`addCodecSentinel`](#add-codec-sentinel). ```ts twoslash import { getUtf8Codec, fixCodecSize, addCodecSizePrefix, getU32Codec, addCodecSentinel, } from '@solana/kit'; // ---cut-before--- fixCodecSize(getUtf8Codec(), 4).encode('hello'); // 0x68656c6c (truncated to 4 bytes) addCodecSizePrefix(getUtf8Codec(), getU32Codec()).encode('hello'); // 0x0500000068656c6c6f // | └-- The 5 bytes of content. // └-- 4-byte prefix telling us to read 5 bytes. addCodecSentinel(getUtf8Codec(), new Uint8Array([0xff, 0xff])).encode('hello'); // 0x68656c6c6fffff // | └-- The sentinel signaling the end of the content. // └-- The 5 bytes of content. ``` `getUtf8Encoder` and `getUtf8Decoder` functions are also available. ## Data structures listing [!toc] ### getArrayCodec [!toc] [#get-array-codec] The `getArrayCodec` function accepts any codec of type `T` and returns a codec of type `Array`. ```ts twoslash import { getArrayCodec, getU8Codec } from '@solana/kit'; // ---cut-before--- const codec = getArrayCodec(getU8Codec()); const bytes = codec.encode([1, 2, 3]); // 0x03000000010203 const array = codec.decode(bytes); // [1, 2, 3] ``` By default, the size of the array is stored as a `u32` prefix before encoding the items. ```ts twoslash import { getArrayCodec, getU8Codec } from '@solana/kit'; // ---cut-before--- getArrayCodec(getU8Codec()).encode([1, 2, 3]); // 0x03000000010203 // | └-- 3 items of 1 byte each. // └-- 4-byte prefix telling us to read 3 items. ``` However, you may use the `size` option to configure this behaviour. It can be one of the following three strategies: - `Codec`: When a number codec is provided, that codec will be used to encode and decode the size prefix. - `number`: When a number is provided, the codec will expect a fixed number of items in the array. An error will be thrown when trying to encode an array of a different length. - `"remainder"`: When the string `"remainder"` is passed as a size, the codec will use the remainder of the bytes to encode/decode its items. This means the size is not stored or known in advance but simply inferred from the rest of the buffer. For instance, if we have an array of `u16` numbers and 10 bytes remaining, we know there are 5 items in this array. ```ts twoslash import { getArrayCodec, getU8Codec, getU16Codec } from '@solana/kit'; // ---cut-before--- getArrayCodec(getU8Codec(), { size: getU16Codec() }).encode([1, 2, 3]); // 0x0300010203 // | └-- 3 items of 1 byte each. // └-- 2-byte prefix telling us to read 3 items. getArrayCodec(getU8Codec(), { size: 3 }).encode([1, 2, 3]); // 0x010203 // └-- 3 items of 1 byte each. There must always be 3 items in the array. getArrayCodec(getU8Codec(), { size: 'remainder' }).encode([1, 2, 3]); // 0x010203 // └-- 3 items of 1 byte each. The size is inferred from the remainder of the bytes. ``` `getArrayEncoder` and `getArrayDecoder` functions are also available. ### getBitArrayCodec [!toc] [#get-bit-array-codec] The `getBitArrayCodec` function returns a codec that encodes and decodes an array of booleans such that each boolean is represented by a single bit. It requires the size of the codec in bytes and an optional `backward` flag that can be used to reverse the order of the bits. ```ts twoslash import { getBitArrayCodec } from '@solana/kit'; // ---cut-before--- const booleans = [true, false, true, false, true, false, true, false]; getBitArrayCodec(1).encode(booleans); // 0xaa or 0b10101010 getBitArrayCodec(1, { backward: true }).encode(booleans); // 0x55 or 0b01010101 ``` `getBitArrayEncoder` and `getBitArrayDecoder` functions are also available. ### getBooleanCodec [!toc] [#get-boolean-codec] The `getBooleanCodec` function returns a `Codec` that stores the boolean as `0` or `1` using a `u8` number by default. ```ts twoslash import { getBooleanCodec } from '@solana/kit'; // ---cut-before--- const codec = getBooleanCodec(); const bytes = codec.encode(true); // 0x01 const value = codec.decode(bytes); // true ``` You may configure that behaviour by providing an explicit number codec as the `size` option of the `getBooleanCodec` function. That number codec will then be used to encode and decode the values `0` and `1` accordingly. ```ts twoslash import { getBooleanCodec, getU16Codec, getU32Codec } from '@solana/kit'; // ---cut-before--- getBooleanCodec({ size: getU16Codec() }).encode(false); // 0x0000 getBooleanCodec({ size: getU16Codec() }).encode(true); // 0x0100 getBooleanCodec({ size: getU32Codec() }).encode(false); // 0x00000000 getBooleanCodec({ size: getU32Codec() }).encode(true); // 0x01000000 ``` `getBooleanEncoder` and `getBooleanDecoder` functions are also available. ### getBytesCodec [!toc] [#get-bytes-codec] The `getBytesCodec` function returns a `Codec` meaning it converts `Uint8Arrays` to and from… `Uint8Arrays`! Whilst this might seem a bit useless, it can be useful when composed into other codecs. For example, you could use it in a struct codec to say that a particular field should be left unserialised. ```ts twoslash import { getBytesCodec } from '@solana/kit'; // ---cut-before--- const codec = getBytesCodec(); const bytes = codec.encode(new Uint8Array([42])); // 0x2a const value = codec.decode(bytes); // 0x2a ``` The `getBytesCodec` function will encode and decode `Uint8Arrays` using as many bytes as necessary. If you'd like to restrict the number of bytes used by this codec, you may combine it with utilities such as [`fixCodecSize`](#fix-codec-size), [`addCodecSizePrefix`](#add-codec-size-prefix) or [`addCodecSentinel`](#add-codec-sentinel). ```ts twoslash import { getBytesCodec, fixCodecSize, addCodecSizePrefix, getU32Codec, addCodecSentinel, } from '@solana/kit'; // ---cut-before--- const value = new Uint8Array([42, 43]); // 0x2a2b fixCodecSize(getBytesCodec(), 4).encode(value); // 0x2a2b0000 (padded to 4 bytes) addCodecSizePrefix(getBytesCodec(), getU32Codec()).encode(value); // 0x020000002a2b // | └-- The 2 bytes of content. // └-- 4-byte prefix telling us to read 2 bytes. addCodecSentinel(getBytesCodec(), new Uint8Array([0xff, 0xff])).encode(value); // 0x2a2bffff // | └-- The sentinel signaling the end of the content. // └-- The 2 bytes of content. ``` `getBytesEncoder` and `getBytesDecoder` functions are also available. ### getConstantCodec [!toc] [#get-constant-codec] The `getConstantCodec` function accepts any `Uint8Array` and returns a `Codec`. When encoding, it will set the provided `Uint8Array` as-is. When decoding, it will assert that the next bytes contain the provided `Uint8Array` and move the offset forward. ```ts twoslash import { getConstantCodec } from '@solana/kit'; // ---cut-before--- const codec = getConstantCodec(new Uint8Array([1, 2, 3])); codec.encode(undefined); // 0x010203 codec.decode(new Uint8Array([1, 2, 3])); // undefined codec.decode(new Uint8Array([1, 2, 4])); // Throws an error. ``` `getConstantEncoder` and `getConstantDecoder` functions are also available. ### getDiscriminatedUnionCodec [!toc] [#get-discriminated-union-codec] In Rust, enums are powerful data types whose variants can be one of the following: - An empty variant — e.g. `enum Message { Quit }`. - A tuple variant — e.g. `enum Message { Write(String) }`. - A struct variant — e.g. `enum Message { Move { x: i32, y: i32 } }`. Whilst we do not have such powerful enums in JavaScript, we can emulate them in TypeScript using a union of objects such that each object is differentiated by a specific field. **We call this a discriminated union**. We use a special field named `__kind` to distinguish between the different variants of a discriminated union. Additionally, since all variants are objects, we can use a `fields` property to wrap the array of tuple variants. Here is an example. ```ts twoslash type Message = | { __kind: 'quit' } // Empty variant. | { __kind: 'write'; fields: [string] } // Tuple variant. | { __kind: 'move'; x: number; y: number }; // Struct variant. ``` The `getDiscriminatedUnionCodec` function helps us encode and decode these discriminated unions. It requires the discriminator and codec of each variant as a first argument. Similarly to the [getStructCodec](#get-struct-codec), these are defined as an array of variant tuples where the first item is the discriminator of the variant and the second item is its codec. Since empty variants do not have data to encode, they simply use the [getUnitCodec](#get-unit-codec) which does nothing. Here is how we can create a discriminated union codec for our previous example. ```ts twoslash import { getDiscriminatedUnionCodec, getUnitCodec, getStructCodec, getTupleCodec, getUtf8Codec, getU32Codec, addCodecSizePrefix, getI32Codec, } from '@solana/kit'; // ---cut-before--- const messageCodec = getDiscriminatedUnionCodec([ // Empty variant. ['quit', getUnitCodec()], // Tuple variant. [ 'write', getStructCodec([ ['fields', getTupleCodec([addCodecSizePrefix(getUtf8Codec(), getU32Codec())])], ]), ], // Struct variant. [ 'move', getStructCodec([ ['x', getI32Codec()], ['y', getI32Codec()], ]), ], ]); ``` And here's how we can use such a codec to encode discriminated unions. Notice that by default, they use a `u8` number prefix to distinguish between the different types of variants. ```ts twoslash import { getDiscriminatedUnionCodec, getUnitCodec, getStructCodec, getTupleCodec, getUtf8Codec, getU32Codec, addCodecSizePrefix, getI32Codec, } from '@solana/kit'; const messageCodec = getDiscriminatedUnionCodec([ // Empty variant. ['quit', getUnitCodec()], // Tuple variant. [ 'write', getStructCodec([ ['fields', getTupleCodec([addCodecSizePrefix(getUtf8Codec(), getU32Codec())])], ]), ], // Struct variant. [ 'move', getStructCodec([ ['x', getI32Codec()], ['y', getI32Codec()], ]), ], ]); // ---cut-before--- messageCodec.encode({ __kind: 'quit' }); // 0x00 // └-- 1-byte discriminator (Index 0 — the "quit" variant). messageCodec.encode({ __kind: 'write', fields: ['Hi'] }); // 0x01020000004869 // | | └-- utf8 string content ("Hi"). // | └-- u32 string prefix (2 characters). // └-- 1-byte discriminator (Index 1 — the "write" variant). messageCodec.encode({ __kind: 'move', x: 5, y: 6 }); // 0x020500000006000000 // | | └-- Field y (6). // | └-- Field x (5). // └-- 1-byte discriminator (Index 2 — the "move" variant). ``` However, you may provide a number codec as the `size` option of the `getDiscriminatedUnionCodec` function to customise that behaviour. ```ts twoslash import { getDiscriminatedUnionCodec, getUnitCodec, getStructCodec, getTupleCodec, getUtf8Codec, getU32Codec, addCodecSizePrefix, getI32Codec, } from '@solana/kit'; const quitCodec = getUnitCodec(); const writeCodec = getStructCodec([ ['fields', getTupleCodec([addCodecSizePrefix(getUtf8Codec(), getU32Codec())])], ]); const moveCodec = getStructCodec([ ['x', getI32Codec()], ['y', getI32Codec()], ]); // ---cut-before--- const u32MessageCodec = getDiscriminatedUnionCodec( [ ['quit', quitCodec], ['write', writeCodec], ['move', moveCodec], ], { size: getU32Codec() }, ); u32MessageCodec.encode({ __kind: 'quit' }); // 0x00000000 // └------┘ 4-byte discriminator (Index 0). u32MessageCodec.encode({ __kind: 'write', fields: ['Hi'] }); // 0x01000000020000004869 // └------┘ 4-byte discriminator (Index 1). u32MessageCodec.encode({ __kind: 'move', x: 5, y: 6 }); // 0x020000000500000006000000 // └------┘ 4-byte discriminator (Index 2). ``` You may also customize the discriminator property — which defaults to `__kind` — by providing the desired property name as the `discriminator` option like so: ```ts twoslash import { getDiscriminatedUnionCodec, getUnitCodec, getStructCodec, getTupleCodec, getUtf8Codec, getU32Codec, addCodecSizePrefix, getI32Codec, } from '@solana/kit'; const quitCodec = getUnitCodec(); const writeCodec = getStructCodec([ ['fields', getTupleCodec([addCodecSizePrefix(getUtf8Codec(), getU32Codec())])], ]); const moveCodec = getStructCodec([ ['x', getI32Codec()], ['y', getI32Codec()], ]); // ---cut-before--- const messageCodec = getDiscriminatedUnionCodec( [ ['quit', quitCodec], ['write', writeCodec], ['move', moveCodec], ], { discriminator: 'message' }, ); messageCodec.encode({ message: 'quit' }); messageCodec.encode({ message: 'write', fields: ['Hi'] }); messageCodec.encode({ message: 'move', x: 5, y: 6 }); ``` Note that, the discriminator value of a variant may be any scalar value — such as `number`, `bigint`, `boolean`, a JavaScript `enum`, etc. For instance, the following is also valid: ```ts twoslash import { getDiscriminatedUnionCodec, getUnitCodec, getStructCodec, getTupleCodec, getUtf8Codec, getU32Codec, addCodecSizePrefix, getI32Codec, } from '@solana/kit'; const quitCodec = getUnitCodec(); const writeCodec = getStructCodec([ ['fields', getTupleCodec([addCodecSizePrefix(getUtf8Codec(), getU32Codec())])], ]); const moveCodec = getStructCodec([ ['x', getI32Codec()], ['y', getI32Codec()], ]); // ---cut-before--- enum Message { Quit, Write, Move, } const messageCodec = getDiscriminatedUnionCodec([ [Message.Quit, quitCodec], [Message.Write, writeCodec], [Message.Move, moveCodec], ]); messageCodec.encode({ __kind: Message.Quit }); messageCodec.encode({ __kind: Message.Write, fields: ['Hi'] }); messageCodec.encode({ __kind: Message.Move, x: 5, y: 6 }); ``` `getDiscriminatedUnionEncoder` and `getDiscriminatedUnionDecoder` functions are also available. ### getEnumCodec [!toc] [#get-enum-codec] The `getEnumCodec` function accepts a JavaScript enum constructor and returns a codec for encoding and decoding values of that enum. ```ts twoslash import { getEnumCodec } from '@solana/kit'; // ---cut-before--- enum Direction { Left, Right, } const codec = getEnumCodec(Direction); const bytes = codec.encode(Direction.Left); // 0x00 const direction = codec.decode(bytes); // Direction.Left ``` When encoding an enum, you may either provide the value of the enum variant — e.g. `Direction.Left` — or its key — e.g. `'Left'`. ```ts twoslash import { getEnumCodec } from '@solana/kit'; enum Direction { Left, Right, } // ---cut-before--- getEnumCodec(Direction).encode(Direction.Left); // 0x00 getEnumCodec(Direction).encode(Direction.Right); // 0x01 getEnumCodec(Direction).encode('Left'); // 0x00 getEnumCodec(Direction).encode('Right'); // 0x01 ``` By default, a `u8` number is being used to store the enum value. However, a number codec may be passed as the `size` option to configure that behaviour. ```ts twoslash import { getEnumCodec, getU32Codec } from '@solana/kit'; enum Direction { Left, Right, } // ---cut-before--- const u32DirectionCodec = getEnumCodec(Direction, { size: getU32Codec() }); u32DirectionCodec.encode(Direction.Left); // 0x00000000 u32DirectionCodec.encode(Direction.Right); // 0x01000000 ``` This function also works with lexical enums — e.g. `enum Direction { Left = '←' }` — explicit numerical enums — e.g. `enum Speed { Left = 50 }` — and hybrid enums with a mix of both. ```ts twoslash import { getEnumCodec } from '@solana/kit'; // ---cut-before--- enum Numbers { One, Five = 5, Six, Nine = 'nine', } getEnumCodec(Numbers).encode(Numbers.One); // 0x00 getEnumCodec(Numbers).encode(Numbers.Five); // 0x01 getEnumCodec(Numbers).encode(Numbers.Six); // 0x02 getEnumCodec(Numbers).encode(Numbers.Nine); // 0x03 getEnumCodec(Numbers).encode('One'); // 0x00 getEnumCodec(Numbers).encode('Five'); // 0x01 getEnumCodec(Numbers).encode('Six'); // 0x02 getEnumCodec(Numbers).encode('Nine'); // 0x03 ``` Notice how, by default, the index of the enum variant is used to encode the value of the enum. For instance, in the example above, `Numbers.Five` is encoded as `0x01` even though its value is `5`. This is also true for lexical enums. However, when dealing with numerical enums that have explicit values, you may use the `useValuesAsDiscriminators` option to encode the value of the enum variant instead of its index. ```ts twoslash import { getEnumCodec } from '@solana/kit'; // ---cut-before--- enum Numbers { One, Five = 5, Six, Nine = 9, } const codec = getEnumCodec(Numbers, { useValuesAsDiscriminators: true }); codec.encode(Numbers.One); // 0x00 codec.encode(Numbers.Five); // 0x05 codec.encode(Numbers.Six); // 0x06 codec.encode(Numbers.Nine); // 0x09 codec.encode('One'); // 0x00 codec.encode('Five'); // 0x05 codec.encode('Six'); // 0x06 codec.encode('Nine'); // 0x09 ``` Note that when using the `useValuesAsDiscriminators` option on an enum that contains a lexical value, an error will be thrown. ```ts twoslash import { getEnumCodec } from '@solana/kit'; // ---cut-before--- enum Lexical { One, Two = 'two', } getEnumCodec(Lexical, { useValuesAsDiscriminators: true }); // Throws an error. ``` `getEnumEncoder` and `getEnumDecoder` functions are also available. ### getHiddenPrefixCodec [!toc] [#get-hidden-prefix-codec] The `getHiddenPrefixCodec` function allow us to prepend a list of hidden `Codec` to a given codec. When encoding, the hidden codecs will be encoded before the main codec and the offset will be moved accordingly. When decoding, the hidden codecs will be decoded but only the result of the main codec will be returned. This is particularly helpful when creating data structures that include constant values that should not be included in the final type. ```ts twoslash import { getHiddenPrefixCodec, getConstantCodec, getU16Codec } from '@solana/kit'; // ---cut-before--- const codec = getHiddenPrefixCodec(getU16Codec(), [ getConstantCodec(new Uint8Array([1, 2, 3])), getConstantCodec(new Uint8Array([4, 5, 6])), ]); codec.encode(42); // 0x0102030405062a00 // | | └-- Our main u16 codec (value = 42). // | └-- Our second hidden prefix codec. // └-- Our first hidden prefix codec. codec.decode(new Uint8Array([1, 2, 3, 4, 5, 6, 42, 0])); // 42 ``` `getHiddenPrefixEncoder` and `getHiddenPrefixDecoder` functions are also available. ### getHiddenSuffixCodec [!toc] [#get-hidden-suffix-codec] The `getHiddenSuffixCodec` function allow us to append a list of hidden `Codec` to a given codec. When encoding, the hidden codecs will be encoded after the main codec and the offset will be moved accordingly. When decoding, the hidden codecs will be decoded but only the result of the main codec will be returned. This is particularly helpful when creating data structures that include constant values that should not be included in the final type. ```ts twoslash import { getHiddenSuffixCodec, getConstantCodec, getU16Codec } from '@solana/kit'; // ---cut-before--- const codec = getHiddenSuffixCodec(getU16Codec(), [ getConstantCodec(new Uint8Array([1, 2, 3])), getConstantCodec(new Uint8Array([4, 5, 6])), ]); codec.encode(42); // 0x2a00010203040506 // | | └-- Our second hidden suffix codec. // | └-- Our first hidden suffix codec. // └-- Our main u16 codec (value = 42). codec.decode(new Uint8Array([42, 0, 1, 2, 3, 4, 5, 6])); // 42 ``` `getHiddenSuffixEncoder` and `getHiddenSuffixDecoder` functions are also available. ### getLiteralUnionCodec [!toc] [#get-literal-union-codec] The `getLiteralUnionCodec` function accepts an array of literal values — such as `string`, `number`, `boolean`, etc. — and returns a codec that encodes and decodes such values by using their index in the array. It uses TypeScript unions to represent all the possible values. ```ts twoslash import { getLiteralUnionCodec, FixedSizeCodec } from '@solana/kit'; // ---cut-before--- const codec = getLiteralUnionCodec(['left', 'right', 'up', 'down']); codec satisfies FixedSizeCodec<'left' | 'right' | 'up' | 'down'>; const bytes = codec.encode('left'); // 0x00 const value = codec.decode(bytes); // 'left' ``` It uses a `u8` number by default to store the index of the value. However, you may provide a number codec as the `size` option of the `getLiteralUnionCodec` function to customise that behaviour. ```ts twoslash import { getLiteralUnionCodec, getU32Codec } from '@solana/kit'; // ---cut-before--- const codec = getLiteralUnionCodec(['left', 'right', 'up', 'down'], { size: getU32Codec(), }); codec.encode('left'); // 0x00000000 codec.encode('right'); // 0x01000000 codec.encode('up'); // 0x02000000 codec.encode('down'); // 0x03000000 ``` `getLiteralUnionEncoder` and `getLiteralUnionDecoder` functions are also available. ### getMapCodec [!toc] [#get-map-codec] The `getMapCodec` function accepts two codecs of type `K` and `V` and returns a codec of type `Map`. ```ts twoslash import { getMapCodec, getU8Codec, getUtf8Codec, fixCodecSize } from '@solana/kit'; // ---cut-before--- const keyCodec = fixCodecSize(getUtf8Codec(), 8); const valueCodec = getU8Codec(); const codec = getMapCodec(keyCodec, valueCodec); const bytes = codec.encode(new Map([['alice', 42]])); // 0x01000000616c6963650000002a const map = codec.decode(bytes); // new Map([["alice", 42]]) ``` Each entry (key/value pair) is encoded one after the other with the key first and the value next. By default, the size of the map is stored as a `u32` prefix before encoding the entries. ```ts twoslash import { getMapCodec, getU8Codec, getUtf8Codec, fixCodecSize } from '@solana/kit'; // ---cut-before--- const codec = getMapCodec(fixCodecSize(getUtf8Codec(), 8), getU8Codec()); const myMap = new Map(); myMap.set('alice', 42); myMap.set('bob', 5); codec.encode(myMap); // 0x02000000616c6963650000002a626f62000000000005 // | | | | └-- 2nd entry value (5). // | | | └-- 2nd entry key ("bob"). // | | └-- 1st entry value (42). // | └-- 1st entry key ("alice"). // └-- 4-byte prefix telling us to read 2 map entries. ``` However, you may use the `size` option to configure this behaviour. It can be one of the following three strategies: - `Codec`: When a number codec is provided, that codec will be used to encode and decode the size prefix. - `number`: When a number is provided, the codec will expect a fixed number of entries in the map. An error will be thrown when trying to encode a map of a different length. - `"remainder"`: When the string `"remainder"` is passed as a size, the codec will use the remainder of the bytes to encode/decode its entries. This means the size is not stored or known in advance but simply inferred from the rest of the buffer. For instance, if we have a map of `u16` numbers and 10 bytes remaining, we know there are 5 entries in this map. ```ts twoslash import { getMapCodec, getU8Codec, getU16Codec, fixCodecSize, getUtf8Codec } from '@solana/kit'; // ---cut-before--- const keyCodec = fixCodecSize(getUtf8Codec(), 8); const valueCodec = getU8Codec(); const myMap = new Map(); myMap.set('alice', 42); myMap.set('bob', 5); getMapCodec(keyCodec, valueCodec, { size: getU16Codec() }).encode(myMap); // 0x0200616c6963650000002a626f62000000000005 // | | └-- Second entry. // | └-- First entry. // └-- 2-byte prefix telling us to read 2 entries. getMapCodec(keyCodec, valueCodec, { size: 3 }).encode(myMap); // 0x616c6963650000002a626f62000000000005 // | └-- Second entry. // └-- First entry. // There must always be 2 entries in the map. getMapCodec(keyCodec, valueCodec, { size: 'remainder' }).encode(myMap); // 0x616c6963650000002a626f62000000000005 // | └-- Second entry. // └-- First entry. // The size is inferred from the remainder of the bytes. ``` `getMapEncoder` and `getMapDecoder` functions are also available. ### getNullableCodec [!toc] [#get-nullable-codec] The `getNullableCodec` function accepts a codec of type `T` and returns a codec of type `T | null`. It stores whether or not the item exists as a boolean prefix using a `u8` by default. ```ts twoslash import { getNullableCodec, getU32Codec, addCodecSizePrefix, getUtf8Codec } from '@solana/kit'; // ---cut-before--- const stringCodec = addCodecSizePrefix(getUtf8Codec(), getU32Codec()); getNullableCodec(stringCodec).encode('Hi'); // 0x01020000004869 // | | └-- utf8 string content ("Hi"). // | └-- u32 string prefix (2 characters). // └-- 1-byte prefix (true — The item exists). getNullableCodec(stringCodec).encode(null); // 0x00 // └-- 1-byte prefix (false — The item is null). ``` You may provide a number codec as the `prefix` option of the `getNullableCodec` function to configure how to store the boolean prefix. ```ts twoslash import { getNullableCodec, getU32Codec, addCodecSizePrefix, getUtf8Codec } from '@solana/kit'; const stringCodec = addCodecSizePrefix(getUtf8Codec(), getU32Codec()); // ---cut-before--- const u32NullableStringCodec = getNullableCodec(stringCodec, { prefix: getU32Codec(), }); u32NullableStringCodec.encode('Hi'); // 0x01000000020000004869 // └------┘ 4-byte prefix (true). u32NullableStringCodec.encode(null); // 0x00000000 // └------┘ 4-byte prefix (false). ``` Additionally, if the item is a `FixedSizeCodec`, you may set the `noneValue` option to `"zeroes"` to also make the returned nullable codec a `FixedSizeCodec`. To do so, it will pad `null` values with zeroes to match the length of existing values. ```ts twoslash import { getNullableCodec, fixCodecSize, getUtf8Codec } from '@solana/kit'; // ---cut-before--- const fixedNullableStringCodec = getNullableCodec( fixCodecSize(getUtf8Codec(), 8), // Only works with fixed-size items. { noneValue: 'zeroes' }, ); fixedNullableStringCodec.encode('Hi'); // 0x014869000000000000 // | └-- 8-byte utf8 string content ("Hi"). // └-- 1-byte prefix (true — The item exists). fixedNullableStringCodec.encode(null); // 0x000000000000000000 // | └-- 8-byte of padding to make a fixed-size codec. // └-- 1-byte prefix (false — The item is null). ``` The `noneValue` option can also be set to an explicit byte array to use as the padding for `null` values. Note that, in this case, the returned codec will not be a `FixedSizeCodec` as the byte array representing `null` values may be of any length. ```ts twoslash import { getNullableCodec, getUtf8Codec } from '@solana/kit'; // ---cut-before--- const codec = getNullableCodec(getUtf8Codec(), { noneValue: new Uint8Array([255]), // 0xff means null. }); codec.encode('Hi'); // 0x014869 // | └-- 2-byte utf8 string content ("Hi"). // └-- 1-byte prefix (true — The item exists). codec.encode(null); // 0x00ff // | └-- 1-byte representing null (0xff). // └-- 1-byte prefix (false — The item is null). ``` The `prefix` option of the `getNullableCodec` function can also be set to `null`, meaning no prefix will be used to determine whether the item exists. In this case, the codec will rely on the `noneValue` option to determine whether the item is `null`. ```ts twoslash import { getNullableCodec, getU16Codec } from '@solana/kit'; // ---cut-before--- const codecWithZeroNoneValue = getNullableCodec(getU16Codec(), { noneValue: 'zeroes', // 0x0000 means null. prefix: null, }); codecWithZeroNoneValue.encode(42); // 0x2a00 codecWithZeroNoneValue.encode(null); // 0x0000 const codecWithCustomNoneValue = getNullableCodec(getU16Codec(), { noneValue: new Uint8Array([255]), // 0xff means null. prefix: null, }); codecWithCustomNoneValue.encode(42); // 0x2a00 codecWithCustomNoneValue.encode(null); // 0xff ``` Note that if `prefix` is set to `null` and no `noneValue` is provided, the codec assumes that the item exists if and only if some remaining bytes are available to decode. This could be useful to describe data structures that may or may not have additional data to the end of the buffer. ```ts const codec = getNullableCodec(getU16Codec(), { prefix: null }); codec.encode(42); // 0x2a00 codec.encode(null); // Encodes nothing. codec.decode(new Uint8Array([42, 0])); // 42 codec.decode(new Uint8Array([])); // null ``` To recap, here are all the possible configurations of the `getNullableCodec` function, using a `u16` codec as an example. | `encode(42)` / `encode(null)` | No `noneValue` (default) | `noneValue: "zeroes"` | Custom `noneValue` (`0xff`) | | ----------------------------- | ------------------------ | --------------------------- | --------------------------- | | `u8` prefix (default) | `0x012a00` / `0x00` | `0x012a00` / `0x000000` | `0x012a00` / `0x00ff` | | Custom `prefix` (`u16`) | `0x01002a00` / `0x0000` | `0x01002a00` / `0x00000000` | `0x01002a00` / `0x0000ff` | | No `prefix` | `0x2a00` / `0x` | `0x2a00` / `0x0000` | `0x2a00` / `0xff` | Note that you might be interested in the Rust-like alternative version of nullable codecs, available as the [getOptionCodec](#get-option-codec) function. `getNullableEncoder` and `getNullableDecoder` functions are also available. ### getOptionCodec [!toc] [#get-option-codec] The `getOptionCodec` function accepts a codec of type `T` and returns a codec of type `Option` — as defined in the [`@solana/options`](https://github.com/anza-xyz/kit/tree/main/packages/options) package. Note that, when encoding, `T` or `null` may also be provided directly as input and will be interpreted as `Some(T)` or `None` respectively. However, when decoding, the output will always be an `Option` type. It stores whether or not the item exists as a boolean prefix using a `u8` by default. ```ts twoslash import { getOptionCodec, getU32Codec, addCodecSizePrefix, getUtf8Codec, some, none, } from '@solana/kit'; // ---cut-before--- const stringCodec = addCodecSizePrefix(getUtf8Codec(), getU32Codec()); getOptionCodec(stringCodec).encode('Hi'); getOptionCodec(stringCodec).encode(some('Hi')); // 0x01020000004869 // | | └-- utf8 string content ("Hi"). // | └-- u32 string prefix (2 characters). // └-- 1-byte prefix (Some). getOptionCodec(stringCodec).encode(null); getOptionCodec(stringCodec).encode(none()); // 0x00 // └-- 1-byte prefix (None). ``` You may provide a number codec as the `prefix` option of the `getOptionCodec` function to configure how to store the boolean prefix. ```ts twoslash import { getOptionCodec, getU32Codec, addCodecSizePrefix, getUtf8Codec, some, none, } from '@solana/kit'; const stringCodec = addCodecSizePrefix(getUtf8Codec(), getU32Codec()); // ---cut-before--- const u32OptionStringCodec = getOptionCodec(stringCodec, { prefix: getU32Codec(), }); u32OptionStringCodec.encode(some('Hi')); // 0x01000000020000004869 // └------┘ 4-byte prefix (Some). u32OptionStringCodec.encode(none()); // 0x00000000 // └------┘ 4-byte prefix (None). ``` Additionally, if the item is a `FixedSizeCodec`, you may set the `noneValue` option to `"zeroes"` to also make the returned Option codec a `FixedSizeCodec`. To do so, it will pad `None` values with zeroes to match the length of existing values. ```ts twoslash import { getOptionCodec, fixCodecSize, getUtf8Codec, some, none } from '@solana/kit'; // ---cut-before--- const codec = getOptionCodec( fixCodecSize(getUtf8Codec(), 8), // Only works with fixed-size items. { noneValue: 'zeroes' }, ); codec.encode(some('Hi')); // 0x014869000000000000 // | └-- 8-byte utf8 string content ("Hi"). // └-- 1-byte prefix (Some). codec.encode(none()); // 0x000000000000000000 // | └-- 8-byte of padding to make a fixed-size codec. // └-- 1-byte prefix (None). ``` The `noneValue` option can also be set to an explicit byte array to use as the padding for `None` values. Note that, in this case, the returned codec will not be a `FixedSizeCodec` as the byte array representing `None` values may be of any length. ```ts twoslash import { getOptionCodec, getUtf8Codec, some, none } from '@solana/kit'; // ---cut-before--- const codec = getOptionCodec(getUtf8Codec(), { noneValue: new Uint8Array([255]), // 0xff means None. }); codec.encode(some('Hi')); // 0x014869 // | └-- 2-byte utf8 string content ("Hi"). // └-- 1-byte prefix (Some). codec.encode(none()); // 0x00ff // | └-- 1-byte representing None (0xff). // └-- 1-byte prefix (None). ``` The `prefix` option of the `getOptionCodec` function can also be set to `null`, meaning no prefix will be used to determine whether the item exists. In this case, the codec will rely on the `noneValue` option to determine whether the item is `None`. ```ts twoslash import { getOptionCodec, getU16Codec, some, none } from '@solana/kit'; // ---cut-before--- const codecWithZeroNoneValue = getOptionCodec(getU16Codec(), { noneValue: 'zeroes', // 0x0000 means None. prefix: null, }); codecWithZeroNoneValue.encode(some(42)); // 0x2a00 codecWithZeroNoneValue.encode(none()); // 0x0000 const codecWithCustomNoneValue = getOptionCodec(getU16Codec(), { noneValue: new Uint8Array([255]), // 0xff means None. prefix: null, }); codecWithCustomNoneValue.encode(some(42)); // 0x2a00 codecWithCustomNoneValue.encode(none()); // 0xff ``` Note that if `prefix` is set to `null` and no `noneValue` is provided, the codec assume that the item exists if and only if some remaining bytes are available to decode. This could be useful to describe data structures that may or may not have additional data to the end of the buffer. ```ts twoslash import { getOptionCodec, getU16Codec, some, none } from '@solana/kit'; // ---cut-before--- const codec = getOptionCodec(getU16Codec(), { prefix: null }); codec.encode(some(42)); // 0x2a00 codec.encode(none()); // Encodes nothing. codec.decode(new Uint8Array([42, 0])); // some(42) codec.decode(new Uint8Array([])); // none() ``` To recap, here are all the possible configurations of the `getOptionCodec` function, using a `u16` codec as an example. | `encode(some(42))` / `encode(none())` | No `noneValue` (default) | `noneValue: "zeroes"` | Custom `noneValue` (`0xff`) | | ------------------------------------- | ------------------------ | --------------------------- | --------------------------- | | `u8` prefix (default) | `0x012a00` / `0x00` | `0x012a00` / `0x000000` | `0x012a00` / `0x00ff` | | Custom `prefix` (`u16`) | `0x01002a00` / `0x0000` | `0x01002a00` / `0x00000000` | `0x01002a00` / `0x0000ff` | | No `prefix` | `0x2a00` / `0x` | `0x2a00` / `0x0000` | `0x2a00` / `0xff` | `getOptionEncoder` and `getOptionDecoder` functions are also available. ### getPatternMatchCodec [!toc] [#get-pattern-match-codec] The `getPatternMatchCodec` function returns a codec that selects which variant codec to use based on pattern matching. It generalises [`getPredicateCodec`](#get-predicate-codec) from two variants to any number of variants. It accepts an array of `[encodePredicate, decodePredicate, codec]` triples, where: - An encode predicate, that takes the value to encode and returns a boolean - A decode predicate, that takes a `ReadonlyUint8Array` and returns a boolean - A codec, that will be used when the corresponding predicate returns `true` Predicates are tested in order and the first match is used. If no predicate matches, a `SolanaError` is thrown. ```ts twoslash import { getPatternMatchCodec, getU8Codec, getU16Codec, getU32Codec, Codec } from '@solana/kit'; // ---cut-before--- const codec: Codec = getPatternMatchCodec([ [ (value: number) => value < 256, bytes => bytes.length === 1, getU8Codec(), ], [ (value: number) => value < 2 ** 16, bytes => bytes.length === 2, getU16Codec(), ], [ (value: number) => value < 2 ** 32, bytes => bytes.length <= 4, getU32Codec(), ], ]); const u8Bytes = codec.encode(42) // 0x2a, encoded as u8 codec.decode(u8Bytes) // 42, decoded as u8 const u16Bytes = codec.encode(1000) // 0xe803, encoded as u16 codec.decode(u16Bytes) // 1000, decoded as u16 const u32Bytes = codec.encode(100_000) // 0xa0860100, encoded as u32 codec.decode(u32Bytes) // 100_000, decoded as u32 ``` `getPatternMatchEncoder` and `getPatternMatchDecoder` functions are also available. ### getPredicateCodec [!toc] [#get-predicate-codec] The `getPredicateCodec` function returns a codec that switches between two different codecs, based on a predicate. It accepts the following arguments: - An encode predicate, that takes a value of a given type and returns a boolean - A decode predicate, that takes a `ReadOnlyUint8Array` and returns a boolean - A codec, that will be used to encode if the encode predicate is true and will be used to decode if the decode predicate is true - A codec, that will be used to encode if the encode predicate is false and will be used to decode if the decode predicate is false ```ts twoslash import { getPredicateCodec, getU8Codec, getU32Codec, Codec } from '@solana/kit'; // ---cut-before--- const codec: Codec = getPredicateCodec( (value: number) => value < 256, bytes => bytes.length === 1, getU8Codec(), getU32Codec(), ); const u8Bytes = codec.encode(42) // 0x2a, used u8 codec codec.decode(u8Bytes) // 42, used u8 codec const u32Bytes = codec.encode(1000) // 0xE8030000, used u32 codec codec.decode(u32Bytes) // 1000, used u32 codec ``` `getPredicateEncoder` and `getPredicateDecoder` functions are also available. ### getSetCodec [!toc] [#get-set-codec] The `getSetCodec` function accepts any codec of type `T` and returns a codec of type `Set`. ```ts twoslash import { getSetCodec, getU8Codec } from '@solana/kit'; // ---cut-before--- const codec = getSetCodec(getU8Codec()); const bytes = codec.encode(new Set([1, 2, 3])); // 0x03000000010203 const value = codec.decode(bytes); // new Set([1, 2, 3]) ``` By default, the size of the set is stored as a `u32` prefix before encoding the items. ```ts twoslash import { getSetCodec, getU8Codec } from '@solana/kit'; // ---cut-before--- getSetCodec(getU8Codec()).encode(new Set([1, 2, 3])); // 0x03000000010203 // | └-- 3 items of 1 byte each. // └-- 4-byte prefix telling us to read 3 items. ``` However, you may use the `size` option to configure this behaviour. It can be one of the following three strategies: - `Codec`: When a number codec is provided, that codec will be used to encode and decode the size prefix. - `number`: When a number is provided, the codec will expect a fixed number of items in the set. An error will be thrown when trying to encode a set of a different length. - `"remainder"`: When the string `"remainder"` is passed as a size, the codec will use the remainder of the bytes to encode/decode its items. This means the size is not stored or known in advance but simply inferred from the rest of the buffer. For instance, if we have a set of `u16` numbers and 10 bytes remaining, we know there are 5 items in this set. ```ts twoslash import { getSetCodec, getU8Codec, getU16Codec } from '@solana/kit'; // ---cut-before--- getSetCodec(getU8Codec(), { size: getU16Codec() }).encode(new Set([1, 2, 3])); // 0x0300010203 // | └-- 3 items of 1 byte each. // └-- 2-byte prefix telling us to read 3 items. getSetCodec(getU8Codec(), { size: 3 }).encode(new Set([1, 2, 3])); // 0x010203 // └-- 3 items of 1 byte each. There must always be 3 items in the set. getSetCodec(getU8Codec(), { size: 'remainder' }).encode(new Set([1, 2, 3])); // 0x010203 // └-- 3 items of 1 byte each. The size is inferred from the remainder of the bytes. ``` `getSetEncoder` and `getSetDecoder` functions are also available. ### getStructCodec [!toc] [#get-struct-codec] The `getStructCodec` function accepts any number of field codecs and returns a codec for an object containing all these fields. Each provided field is an array such that the first item is the name of the field and the second item is the codec used to encode and decode that field type. ```ts twoslash import { getStructCodec, getU8Codec, getUtf8Codec, addCodecSizePrefix, Codec, getU32Codec, } from '@solana/kit'; // ---cut-before--- type Person = { name: string; age: number }; const personCodec: Codec = getStructCodec([ ['name', addCodecSizePrefix(getUtf8Codec(), getU32Codec())], ['age', getU8Codec()], ]); const bytes = personCodec.encode({ name: 'alice', age: 42 }); // 0x05000000616c6963652a // | └-- Age field. // └-- Name field. const person = personCodec.decode(bytes); // { name: "alice", age: 42 } ``` `getStructEncoder` and `getStructDecoder` functions are also available. ### getTupleCodec [!toc] [#get-tuple-codec] The `getTupleCodec` function accepts any number of codecs — `T`, `U`, `V`, etc. — and returns a tuple codec of type `[T, U, V, …]` such that each item is in the order of the provided codecs. ```ts twoslash import { getTupleCodec, getU8Codec, getU64Codec, addCodecSizePrefix, getUtf8Codec, getU32Codec, } from '@solana/kit'; // ---cut-before--- const tupleCodec = getTupleCodec([ addCodecSizePrefix(getUtf8Codec(), getU32Codec()), getU8Codec(), getU64Codec(), ]); const bytes = tupleCodec.encode(['alice', 42, 123]); // 0x05000000616c6963652a7b00000000000000 // | | └-- 3rd item (123). // | └-- 2nd item (42). // └-- 1st item ("alice"). const value = tupleCodec.decode(bytes); // ["alice", 42, 123] ``` `getTupleEncoder` and `getTupleDecoder` functions are also available. ### getUnionCodec [!toc] [#get-union-codec] The `getUnionCodec` is a lower-lever codec helper that can be used to encode/decode any TypeScript union. It accepts the following arguments: - An array of codecs, each defining a variant of the union. - A `getIndexFromValue` function which, given a value of the union, returns the index of the codec that should be used to encode that value. - A `getIndexFromBytes` function which, given the byte array to decode at a given offset, returns the index of the codec that should be used to decode the next bytes. ```ts twoslash import { getUnionCodec, getU16Codec, getBooleanCodec, Codec } from '@solana/kit'; // ---cut-before--- const codec: Codec = getUnionCodec( [getU16Codec(), getBooleanCodec()], (value) => (typeof value === 'number' ? 0 : 1), (bytes, offset) => (bytes.slice(offset).length > 1 ? 0 : 1), ); codec.encode(42); // 0x2a00 codec.encode(true); // 0x01 ``` `getUnionEncoder` and `getUnionDecoder` functions are also available. ### getUnitCodec [!toc] [#get-unit-codec] The `getUnitCodec` function returns a `Codec` that encodes `undefined` into an empty `Uint8Array` and returns `undefined` without consuming any bytes when decoding. This is more of a low-level codec that can be used internally by other codecs. For instance, this is how [getDiscriminatedUnionCodec](#get-discriminated-union-codec) describes the codecs of empty variants. ```ts twoslash import { getUnitCodec } from '@solana/kit'; const anyBytes = null as unknown as Uint8Array; // ---cut-before--- getUnitCodec().encode(undefined); // Empty Uint8Array getUnitCodec().decode(anyBytes); // undefined ``` `getUnitEncoder` and `getUnitDecoder` functions are also available. ================================================ FILE: docs/content/docs/advanced-guides/errors.mdx ================================================ --- title: Errors description: Recover from errors in development and production --- ## Introduction The ecosystem of packages based on Kit's error system benefit from strongly typed error data, and descriptive error messages. Its error messages get scrubbed from your production bundles, which means that the library of error messages can grow indefinitely without adding a single byte to your app. ## Installation Errors are **included within the `@solana/kit` library** but you may also install them using their standalone package. ```package-install @solana/errors ``` ## What is a `SolanaError`? A `SolanaError` is a class made of three components: - An error **code**; found in the union named [`SolanaErrorCode`](/api/type-aliases/SolanaErrorCode) - An error **message** - Optional error **context** Here's an example of some code that responds to various errors, taking advantage of the error context offered by each one: ```ts twoslash import { Transaction, TransactionWithBlockhashLifetime, sendAndConfirmTransactionFactory, } from '@solana/kit'; const sendAndConfirmTransaction = null as unknown as ReturnType< typeof sendAndConfirmTransactionFactory >; const transaction = null as unknown as Transaction & TransactionWithBlockhashLifetime; // ---cut-before--- import { assertIsSendableTransaction, isSolanaError, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, } from '@solana/kit'; try { assertIsSendableTransaction(transaction); await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' }); } catch (e) { if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) { console.error(`Missing signatures for ${e.context.addresses.join(', ')}`); } else if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT)) { console.error( `Transaction exceeds size limit of ${e.context.transactionSizeLimit} bytes. ` + `Actual size: ${e.context.transactionSize}`, ); } throw e; } ``` ## Catching errors When you encounter an error, you can determine whether it is a `SolanaError` or not using the `isSolanaError()` function: ```ts twoslash import { isSolanaError } from '@solana/kit'; try { // ... } catch (e) { if (isSolanaError(e)) { // A Solana error was thrown } else { // Something else went wrong } } ``` When you have a strategy for handling a _particular_ error, you can supply that error's code as the second argument. This will both identify that error by code and refine the type of the `SolanaError` such that the shape of its `context` property becomes known to TypeScript. ```ts twoslash import { SolanaError } from '@solana/kit'; const e = null as unknown as SolanaError; // ---cut-before--- import { isSolanaError, SolanaErrorCode, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, } from '@solana/kit'; try { // ... } catch (e) { if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) { // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ console.error( // Now TypeScript knows the shape of this error's context `Missing signatures for these addresses: [${e.context.addresses.join(', ')}]`, // ^^^^^^^^^ ); } else { // Something else went wrong } } ``` Some Solana errors have, as their root cause, another Solana error. One such example is during transaction simulation (preflight): ```ts twoslash import { Base64EncodedWireTransaction, Rpc, SendTransactionApi } from '@solana/kit'; const rpc = null as unknown as Rpc; const wireTransaction = null as unknown as Base64EncodedWireTransaction; // ---cut-before--- import { isSolanaError, SOLANA_ERROR__INSTRUCTION_ERROR__COMPUTATIONAL_BUDGET_EXCEEDED, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, } from '@solana/kit'; try { const result = await rpc.sendTransaction(wireTransaction, { encoding: 'base64', skipPreflight: false, }); } catch (e) { if (isSolanaError(e, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE)) { // Simulation failed, but why? const underlyingError = e.cause; if ( isSolanaError( underlyingError, SOLANA_ERROR__INSTRUCTION_ERROR__COMPUTATIONAL_BUDGET_EXCEEDED, ) ) { // Now we know the root cause of the simulation failure, and can advise the user } } } ``` ## Failed transaction errors When a transaction fails to send, Kit raises one of two high-level error codes that wrap whatever actually went wrong with extra context useful for debugging. ### `SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION` [#failed-to-send-transaction] This is the error you get back when sending a single transaction fails. The message itself indicates whether the failure happened during preflight or after the transaction was submitted; the context exposes the underlying error, simulation logs, the preflight result when available, and the full `TransactionPlanResult` for deeper inspection. ```ts twoslash import { SolanaError } from '@solana/kit'; const error = null as unknown as SolanaError; // ---cut-before--- import { isSolanaError, SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION } from '@solana/kit'; if (isSolanaError(error, SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION)) { console.error(error.message); // Includes "(preflight)" or the transaction signature. console.error('Cause:', error.cause); // The underlying error that triggered the failure. console.error('Logs:', error.context.logs); // Program logs, when available. console.error('Preflight:', error.context.preflightData); // Simulation result, when available. } ``` The full `TransactionPlanResult` is also attached as `error.context.transactionPlanResult`, but it is non-enumerable so it does not pollute serialized errors. Cast it to [`TransactionPlanResult`](/api/type-aliases/TransactionPlanResult) when you need to walk the tree. ### `SOLANA_ERROR__FAILED_TO_SEND_TRANSACTIONS` [#failed-to-send-transactions] The plural variant is raised when sending more than one transaction fails. Its context exposes a `failedTransactions` array — one entry per failure, with the underlying error, its position in the plan, simulation logs, and preflight data — plus the same non-enumerable `transactionPlanResult`. ```ts twoslash import { SolanaError } from '@solana/kit'; const error = null as unknown as SolanaError; // ---cut-before--- import { isSolanaError, SOLANA_ERROR__FAILED_TO_SEND_TRANSACTIONS } from '@solana/kit'; if (isSolanaError(error, SOLANA_ERROR__FAILED_TO_SEND_TRANSACTIONS)) { for (const { error: cause, index, logs } of error.context.failedTransactions) { console.error(`Transaction #${index + 1} failed:`, cause.message); if (logs) console.error('Logs:', logs); } } ``` Each individual `error` in the array has already been unwrapped from any preflight wrapper, so you can branch on its code directly the same way you would for a standalone `SolanaError`. ## Walking transaction plan results Both failed-transaction errors above expose a `transactionPlanResult` you can inspect, and any successful multi-transaction send returns one directly. A [`TransactionPlanResult`](/api/type-aliases/TransactionPlanResult) is a tree that mirrors the original plan, with one of three statuses at each leaf — `successful`, `failed`, or `canceled`. Kit ships a small set of helpers for working with these trees without writing your own recursion. [`flattenTransactionPlanResult`](/api/functions/flattenTransactionPlanResult) collapses the tree into an array of leaf results, in the order they appear: ```ts twoslash import { TransactionPlanResult } from '@solana/kit'; const result = null as unknown as TransactionPlanResult; // ---cut-before--- import { flattenTransactionPlanResult } from '@solana/kit'; for (const single of flattenTransactionPlanResult(result)) { if (single.status === 'successful') { console.log('✅', single.context.signature); } else if (single.status === 'failed') { console.error('❌', single.error.message); } else { console.warn('⏭️', 'canceled'); } } ``` [`summarizeTransactionPlanResult`](/api/functions/summarizeTransactionPlanResult) bucketizes the leaves into successful, failed, and canceled arrays, plus a single `successful` boolean for the overall outcome: ```ts twoslash import { TransactionPlanResult } from '@solana/kit'; const result = null as unknown as TransactionPlanResult; // ---cut-before--- import { summarizeTransactionPlanResult } from '@solana/kit'; const summary = summarizeTransactionPlanResult(result); console.log(`${summary.successfulTransactions.length} ok`); console.log(`${summary.failedTransactions.length} failed`); console.log(`${summary.canceledTransactions.length} canceled`); ``` [`getFirstFailedSingleTransactionPlanResult`](/api/functions/getFirstFailedSingleTransactionPlanResult) is a convenience for when you only care about the first failure in a tree — useful for surfacing a single root cause in error UI: ```ts twoslash import { TransactionPlanResult } from '@solana/kit'; const result = null as unknown as TransactionPlanResult; // ---cut-before--- import { getFirstFailedSingleTransactionPlanResult } from '@solana/kit'; const failed = getFirstFailedSingleTransactionPlanResult(result); console.error('First failure:', failed.error.message); ``` By default, executors throw [`SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN`](/api/variables/SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN) on any failure. If you would rather inspect a partial result without a `try`/`catch`, [`passthroughFailedTransactionPlanExecution`](/api/functions/passthroughFailedTransactionPlanExecution) catches that error and returns the embedded `TransactionPlanResult` instead. ```ts twoslash import { TransactionPlanResult } from '@solana/kit'; const sendTransactionsPromise = null as unknown as Promise; // ---cut-before--- import { passthroughFailedTransactionPlanExecution } from '@solana/kit'; const result = await passthroughFailedTransactionPlanExecution(sendTransactionsPromise); ``` ## Program-specific errors Codama-generated program packages export their own error helpers so you can identify and format program-specific failures. The pattern is consistent across packages: an `isXError(error, transactionMessage)` type guard, a `getXErrorMessage(code)` formatter, and an enum-shaped `XError` for individual codes (where `X` is the program name, e.g. `System` for `@solana-program/system`). ```ts twoslash import { SolanaError, TransactionMessage } from '@solana/kit'; const error = null as unknown as SolanaError; const transactionMessage = null as unknown as TransactionMessage; // ---cut-before--- import { getSystemErrorMessage, isSystemError } from '@solana-program/system'; if (isSystemError(error, transactionMessage)) { console.error(`System program error: ${getSystemErrorMessage(error.context.code)}`); console.error(`Failed instruction index: ${error.context.index}`); } ``` When sending a transaction or transaction plan, the program-specific error usually lives a few hops down the chain — typically as the `cause` of `SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION` or as one of the `failedTransactions[i].error` entries on the plural variant. Pair the helpers above with the failed-transaction errors documented earlier to surface actionable error messages to your users. ## Error messages ### In development mode When your bundler sets the constant `__DEV__` to `true`, every error message will be included in the bundle. As such, you will be able to read them in plain language wherever they appear. The size of your JavaScript bundle will increase significantly with the inclusion of every error message in development mode. Be sure to build your bundle with `__DEV__` set to `false` when you go to production. ### In production mode When your bundler sets the constant `__DEV__` to `false`, error messages will be stripped from the bundle to save space. Only the error code will appear in the message when an error is encountered. Follow the instructions in the error message to convert the error code back to the human-readable error message. For instance, to recover the error text for the error with code `7050005`: ```shell $ pnpm dlx @solana/errors decode -- 7050005 [Decoded] Solana error code #7050005 - Insufficient funds for fee ``` ```shell $ npx @solana/errors decode -- 7050005 [Decoded] Solana error code #7050005 - Insufficient funds for fee ``` ```shell $ yarn dlx @solana/errors decode -- 7050005 [Decoded] Solana error code #7050005 - Insufficient funds for fee ``` ```shell $ bunx @solana/errors decode -- 7050005 [Decoded] Solana error code #7050005 - Insufficient funds for fee ``` ================================================ FILE: docs/content/docs/advanced-guides/index.mdx ================================================ --- title: Advanced Guides description: Deep dives into Kit's fundamental building blocks --- These guides go deep on the core building blocks Kit is built from — transactions, signers, instruction plans, errors, codecs, keypairs, and more. Each one is a reference you can dip into when you want to understand exactly how something works under the hood. The [Guides](/docs/guides) section covers the practical, task-oriented workflows most applications need day to day, and [plugin-based clients](/docs/plugins) take care of much of this automatically. Reach for the Advanced Guides when you are debugging an unexpected behavior, building a custom plugin, working without a client, or simply curious about what is happening underneath. You can read these guides in any order — start with the topic that is closest to what you are working on. ================================================ FILE: docs/content/docs/advanced-guides/instruction-plans.mdx ================================================ --- title: Instruction Plans description: Compose instructions into multi-step operations --- ## Introduction Instruction plans describe operations that go beyond a single instruction and may even span multiple transactions. They define a set of instructions that must be executed following a specific order. For instance, imagine we wanted to create an instruction plan for a simple escrow transfer between Alice and Bob. First, both would need to deposit their assets into a vault. This could happen in any order. Then and only then, the vault can be activated to switch the assets. Alice and Bob can now both withdraw each other's assets (again, in any order). Here's how we could describe an instruction plan for such an operation. ```ts twoslash import { Instruction, sequentialInstructionPlan, parallelInstructionPlan } from '@solana/kit'; const depositFromAlice = {} as unknown as Instruction; const depositFromBob = {} as unknown as Instruction; const activateVault = {} as unknown as Instruction; const withdrawToAlice = {} as unknown as Instruction; const withdrawToBob = {} as unknown as Instruction; // ---cut-before--- const instructionPlan = sequentialInstructionPlan([ parallelInstructionPlan([depositFromAlice, depositFromBob]), activateVault, parallelInstructionPlan([withdrawToAlice, withdrawToBob]), ]); ``` As you can see, instruction plans don't concern themselves with: - Adding structural instructions — e.g. compute budget limits and prices. - Building transaction messages from these instructions. That is, planning how many can fit into a single instruction, adding a fee payer, a lifetime, etc. - Compiling, signing and sending transactions to the network. Instead, they solely focus on describing operations and delegate all that to two components introduced in this package: - **Transaction planner**: builds transaction messages from an instruction plan and returns an appropriate transaction plan. - **Transaction plan executor**: compiles, signs and sends transaction plans and returns a detailed result of this operation. ```ts twoslash import { TransactionPlanner, TransactionPlanExecutor, InstructionPlan } from '@solana/kit'; const instructionPlan = {} as unknown as InstructionPlan; const transactionPlanner = {} as unknown as TransactionPlanner; const transactionPlanExecutor = {} as unknown as TransactionPlanExecutor; // ---cut-before--- // Plan instructions into transactions. const transactionPlan = await transactionPlanner(instructionPlan); // Execute transactions. const transactionPlanResult = await transactionPlanExecutor(transactionPlan); ``` This separation of concerns not only improves the developer experience but also allows program maintainers to offer helper functions that go beyond a single instruction, while leaving their consumers to decide how they want these operations to materialise. ## Installation Instruction plans are **included within the `@solana/kit` library** but you may also install them using their standalone package. ```package-install @solana/instruction-plans ``` ## Creating instruction plans This package offers several helpers to help you compose your own instruction plans. Let's have a look at them. ### Single instructions The most trivial way to create an instruction plan is to use the `singleInstructionPlan` helper to create a plan with only one instruction. ```ts twoslash import { singleInstructionPlan, Instruction } from '@solana/kit'; const transferSol = {} as unknown as Instruction; // ---cut-before--- const instructionPlan = singleInstructionPlan(transferSol); ``` ### Sequential plans The `sequentialInstructionPlan` helper allows you to create plans from other plans that must be executed sequentially. Therefore, in the example below, we guarantee that Bob will receive assets from Alice before sending them to Carla. ```ts twoslash import { singleInstructionPlan, sequentialInstructionPlan, Instruction } from '@solana/kit'; const transferFromAliceToBob = {} as unknown as Instruction; const transferFromBobToCarla = {} as unknown as Instruction; // ---cut-before--- const instructionPlan = sequentialInstructionPlan([ singleInstructionPlan(transferFromAliceToBob), singleInstructionPlan(transferFromBobToCarla), ]); ``` Note that the `sequentialInstructionPlan` helper also accept `Instruction` objects directly and automatically wraps them in `singleInstructionPlans`. Therefore the following is equivalent to the previous example. ```ts twoslash import { sequentialInstructionPlan, Instruction } from '@solana/kit'; const transferFromAliceToBob = {} as unknown as Instruction; const transferFromBobToCarla = {} as unknown as Instruction; // ---cut-before--- const instructionPlan = sequentialInstructionPlan([transferFromAliceToBob, transferFromBobToCarla]); ``` A `nonDivisibleSequentialInstructionPlan` helper is also available to define sequential plans whose inner instructions should all be executed atomically. That is, either in a single transaction or in a transaction bundle when not possible. ```ts twoslash import { nonDivisibleSequentialInstructionPlan, Instruction } from '@solana/kit'; const createAccount = {} as unknown as Instruction; const initializeMint = {} as unknown as Instruction; // ---cut-before--- const instructionPlan = nonDivisibleSequentialInstructionPlan([createAccount, initializeMint]); ``` In this example, we know that both instruction will either succeed or fail together. ### Parallel plans The `parallelInstructionPlan` function can be used to create plans from other plans that can be executed in parallel. This means direct children of this plan can be executed in separate parallel transactions without consequence. For instance, in the example below, Alice can transfer assets to both Bob and Carla in any order without affecting the final outcome. ```ts twoslash import { singleInstructionPlan, parallelInstructionPlan, Instruction } from '@solana/kit'; const transferFromAliceToBob = {} as unknown as Instruction; const transferFromAliceToCarla = {} as unknown as Instruction; // ---cut-before--- const instructionPlan = parallelInstructionPlan([ singleInstructionPlan(transferFromAliceToBob), singleInstructionPlan(transferFromAliceToCarla), ]); ``` The `parallelInstructionPlan` also accepts `Instruction` object directly and therefore the previous example can be simplified as: ```ts twoslash import { parallelInstructionPlan, Instruction } from '@solana/kit'; const transferFromAliceToBob = {} as unknown as Instruction; const transferFromAliceToCarla = {} as unknown as Instruction; // ---cut-before--- const instructionPlan = parallelInstructionPlan([transferFromAliceToBob, transferFromAliceToCarla]); ``` ### Message packer plans Message packer plans are a bit special. They can dynamically pack instructions into transaction messages. This is particularly useful when packing instructions whose size will vary based on the available space left on the transaction message being packed. For instance, imagine a `write` instruction on a program that gradually write data from instructions into a buffer account. The instruction data used in this case will ideally be as long as the transaction message can fit. Using the `getLinearMessagePackerInstructionPlan` helper, we can create an instruction plan that does just that. ```ts twoslash import { getLinearMessagePackerInstructionPlan, Instruction } from '@solana/kit'; const dataToWrite = {} as unknown as Uint8Array; const getWriteInstruction = {} as unknown as (params: { offset: number; data: Uint8Array; }) => Instruction; // ---cut-before--- const instructionPlan = getLinearMessagePackerInstructionPlan({ totalLength: dataToWrite.length, getInstruction: (offset, length) => getWriteInstruction({ offset, data: dataToWrite.slice(offset, offset + length), }), }); ``` As you can see, the `getLinearMessagePackerInstructionPlan` helper accepts a `totalLength` attribute representing the total amount of bytes we eventually want to write to the buffer. The purpose of the `getInstruction` function is then to generate these `write` instructions at the provided positions. There also exists a `getReallocMessagePackerInstructionPlan` helper that works similarly but whose purpose is to pack multiple realloc instructions to help resize an account. ```ts twoslash import { getReallocMessagePackerInstructionPlan, Instruction } from '@solana/kit'; const additionalDataSize = {} as unknown as number; const getExtendInstruction = {} as unknown as (params: { length: number }) => Instruction; // ---cut-before--- const instructionPlan = getReallocMessagePackerInstructionPlan({ totalSize: additionalDataSize, getInstruction: (size) => getExtendInstruction({ length: size }), }); ``` Whilst these helpers are fairly situational, you can create any custom message packer as long as you implement the following interfaces. ```ts twoslash import { TransactionMessage, TransactionMessageWithFeePayer } from '@solana/kit'; // ---cut-before--- type MessagePackerInstructionPlan = { getMessagePacker: () => MessagePacker; kind: 'messagePacker'; }; type MessagePacker = { done: () => boolean; packMessageToCapacity: ( transactionMessage: TransactionMessage & TransactionMessageWithFeePayer, ) => TransactionMessage & TransactionMessageWithFeePayer; }; ``` Most of your custom logic will live in the `packMessageToCapacity` function whose purpose is to pack the provided message with as many instruction datas as possible or throw when it isn't achievable. The `done` function lets us know if there is any instruction data left to pack. See [`MessagePacker`](/api/type-aliases/MessagePacker) for more information. ### Combining plans It is worth noting that more complex operations can be created by combining plans together. For instance, the following plan will: - Create two accounts in parallel, one Buffer account and one Metadata account. - The Buffer account will be written to via multiple parallel `write` instructions. - The Metadata account will be created and initialized atomically. - Once both of these accounts are created, the data in the Buffer account will be used to set the data in the Metadata account before being closed. ```ts twoslash import { sequentialInstructionPlan, nonDivisibleSequentialInstructionPlan, parallelInstructionPlan, Instruction, MessagePackerInstructionPlan, } from '@solana/kit'; const createAccount1 = {} as unknown as Instruction; const createAccount2 = {} as unknown as Instruction; const initializeBuffer = {} as unknown as Instruction; const initializeMetadata = {} as unknown as Instruction; const setMetadataFromBuffer = {} as unknown as Instruction; const closeBuffer = {} as unknown as Instruction; const writeMessagePacker = {} as unknown as MessagePackerInstructionPlan; // ---cut-before--- const instructionPlan = sequentialInstructionPlan([ parallelInstructionPlan([ sequentialInstructionPlan([ createAccount1, initializeBuffer, parallelInstructionPlan([writeMessagePacker]), ]), nonDivisibleSequentialInstructionPlan([createAccount2, initializeMetadata]), ]), setMetadataFromBuffer, closeBuffer, ]); ``` ## Planning instructions Once we have an instruction plan, the first step is to build transaction messages from it in the most optimal way whilst satisfying all the constraints defined in the instruction plan. This is the role of **transaction planners**. ### Transaction planners Transaction planners are defined as simple abortable functions that transform a given instruction plan into a set of built transaction messages called a **transaction plan**. ```ts twoslash import { TransactionPlanner, InstructionPlan } from '@solana/kit'; const transactionPlanner = {} as unknown as TransactionPlanner; const instructionPlan = {} as unknown as InstructionPlan; const abortSignal = {} as unknown as AbortSignal; // ---cut-before--- const transactionPlan = await transactionPlanner(instructionPlan, { abortSignal }); ``` ### Creating a transaction planner To spin up a transaction planner, you may use the `createTransactionPlanner` helper. This helper requires a `createTransactionMessage` function that tells us how each new transaction message should be created before being packed with instructions. For instance, in the example below we create a new planner such that each planned transaction message will be using version 0 and using the `payer` signer as a fee payer. ```ts twoslash import { createTransactionPlanner, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, TransactionSigner, } from '@solana/kit'; const payer = {} as unknown as TransactionSigner; // ---cut-before--- const transactionPlanner = createTransactionPlanner({ createTransactionMessage: () => pipe( createTransactionMessage({ version: 0 }), (message) => setTransactionMessageFeePayerSigner(payer, message), // ... ), }); ``` Additionally, the `onTransactionMessageUpdated` function may be provided to update transaction messages during the planning process. This function will be called whenever a transaction message is updated — e.g. when new instructions are added. It accepts the updated transaction message and must return a transaction message back, even if no changes were made. In the example below, we check if the packed transaction contains an instruction that transfers SOL and, if so, add a guard instruction that ensures no more than 1 SOL is transferred. ```ts twoslash import { appendTransactionMessageInstruction, createTransactionPlanner, TransactionMessage, Instruction, } from '@solana/kit'; const createTransactionMessage = {} as unknown as Parameters< typeof createTransactionPlanner >[0]['createTransactionMessage']; const containsTransferSolInstruction = {} as unknown as (message: TransactionMessage) => boolean; const transferSolGuardInstruction = {} as unknown as Instruction; // ---cut-before--- const transactionPlanner = createTransactionPlanner({ createTransactionMessage, onTransactionMessageUpdated: (message) => { if (containsTransferSolInstruction(message)) { return appendTransactionMessageInstruction( transferSolGuardInstruction, message, ) as unknown as typeof message; } return message; }, }); ``` Check out the [Recipes section](#recipes) at this end of this guide for ideas of what can be achieved with this API. ### Transaction plans Since transaction planners output transaction plans, it may be useful to see what these look like. They work similarly to instruction plans but they wrap transaction messages instead of instructions and do not contain message packers. - [`singleTransactionPlan`](/api/functions/singleTransactionPlan). Wraps a built transaction message. - [`sequentialTransactionPlan`](/api/functions/sequentialTransactionPlan): Wraps other transaction plans that must be executed sequentially. - [`nonDivisibleSequentialTransactionPlan `](/api/functions/nonDivisibleSequentialTransactionPlan): Wraps other transaction plans that must be executed sequentially and atomically. Since atomicity is at the transaction level, this suggests transaction bundles should be used if possible. Otherwise, the plan should fail to execute. - [`parallelTransactionPlan`](/api/functions/parallelTransactionPlan): Wraps other transaction plans that may be executed in parallel. ```ts twoslash import { singleTransactionPlan, sequentialTransactionPlan, parallelTransactionPlan, TransactionMessage, TransactionMessageWithFeePayer, } from '@solana/kit'; const messageA = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer; const messageB = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer; const messageC = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer; // ---cut-before--- const transactionPlan = parallelTransactionPlan([ sequentialTransactionPlan([singleTransactionPlan(messageA), singleTransactionPlan(messageB)]), singleTransactionPlan(messageC), ]); ``` ### Advanced transaction planners Whilst the `createTransactionPlanner` helper is designed to suit most projects, it may not suit yours. If so, you may create your own by offering a function that satisfy the following signature. ```ts twoslash import { InstructionPlan, TransactionPlan } from '@solana/kit'; // ---cut-before--- type TransactionPlanner = ( instructionPlan: InstructionPlan, config?: { abortSignal?: AbortSignal }, ) => Promise; ``` ## Executing transactions Now that we have obtained a transaction plan from our instruction plan, the next and final step is to send these transactions using a **transaction plan executor**. ### Transaction plan executors Transaction plan executors are defined as abortable functions that transform a given transaction plan into a mirrored data structure that contains the execution status of each transaction. That data structure is called a **transaction plan result**. ```ts twoslash import { TransactionPlanExecutor, TransactionPlan } from '@solana/kit'; const transactionPlanExecutor = {} as unknown as TransactionPlanExecutor; const transactionPlan = {} as unknown as TransactionPlan; const abortSignal = {} as unknown as AbortSignal; // ---cut-before--- const transactionPlanResult = await transactionPlanExecutor(transactionPlan, { abortSignal }); ``` ### Creating a transaction plan executor To spin up a transaction plan executor, you may use the `createTransactionPlanExecutor` helper. This helper requires an `executeTransactionMessage` function that tells us how each transaction message should be executed when encountered during the execution process. The `executeTransactionMessage` callback receives the following arguments: - **`context`**: A mutable object for storing data during execution — see the [next section](#the-execution-context) for details. - **`message`**: The transaction message to execute. - **`config`**: An optional configuration object that may include an `abortSignal`. The callback must return either a `Signature` or a full `Transaction` object directly. For instance, in the example below we create a new executor such that each transaction message will be assigned the latest blockhash lifetime before being signed and sent to the network using the provided RPC client. ```ts twoslash import { sendAndConfirmTransactionFactory, createTransactionPlanExecutor, Rpc, SolanaRpcApi, RpcSubscriptions, SolanaRpcSubscriptionsApi, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, assertIsSendableTransaction, assertIsTransactionWithBlockhashLifetime, TransactionMessage, TransactionMessageWithFeePayer, } from '@solana/kit'; const rpc = {} as unknown as Rpc; const rpcSubscriptions = {} as unknown as RpcSubscriptions; // ---cut-before--- const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); const transactionPlanExecutor = createTransactionPlanExecutor({ executeTransactionMessage: async (context, message) => { const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const messageWithBlockhash = setTransactionMessageLifetimeUsingBlockhash( latestBlockhash, message, ); context.message = messageWithBlockhash; const transaction = await signTransactionMessageWithSigners(messageWithBlockhash); context.transaction = transaction; assertIsSendableTransaction(transaction); assertIsTransactionWithBlockhashLifetime(transaction); await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' }); return transaction; }, }); ``` ### The execution context The `context` object passed to the `executeTransactionMessage` callback is a mutable object that you can populate incrementally as execution progresses. This context is preserved in the resulting `SingleTransactionPlanResult` regardless of the outcome — successful, failed, or canceled. This is particularly useful for debugging failures or building recovery plans. If an error is thrown at any point in the callback, any attributes you've already saved to the context will still be available in the `FailedSingleTransactionPlanResult`. The context object supports three optional base properties that have semantic meaning: - **`message`**: The transaction message after any modifications (e.g., after setting a lifetime). - **`transaction`**: The signed transaction ready to be sent. - **`signature`**: The transaction signature. Note that this is automatically populated when you set the `transaction` property on the context and/or return a `Transaction`. You can also add any custom properties you need: ```ts twoslash import { createTransactionPlanExecutor, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, sendAndConfirmTransactionFactory, assertIsSendableTransaction, assertIsTransactionWithBlockhashLifetime, Rpc, SolanaRpcApi, RpcSubscriptions, SolanaRpcSubscriptionsApi, } from '@solana/kit'; const rpc = {} as unknown as Rpc; const rpcSubscriptions = {} as unknown as RpcSubscriptions; const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); // ---cut-before--- const transactionPlanExecutor = createTransactionPlanExecutor({ executeTransactionMessage: async (context, message) => { // Store the start time (custom context). context.startedAt = Date.now(); const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const messageWithBlockhash = setTransactionMessageLifetimeUsingBlockhash( latestBlockhash, message, ); // Store the message about to be signed. context.message = messageWithBlockhash; const transaction = await signTransactionMessageWithSigners(messageWithBlockhash); // Store the transaction about to be sent. context.transaction = transaction; assertIsSendableTransaction(transaction); assertIsTransactionWithBlockhashLifetime(transaction); await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' }); // Store the confirmation time (custom context). context.confirmedAt = Date.now(); return transaction; }, }); ``` When accessing the context from a result, you can retrieve both the base properties and your custom properties: ```ts twoslash import { SingleTransactionPlanResult, isSuccessfulSingleTransactionPlanResult, isFailedSingleTransactionPlanResult, } from '@solana/kit'; const result = {} as unknown as SingleTransactionPlanResult<{ startedAt: number }>; // ---cut-before--- if (isSuccessfulSingleTransactionPlanResult(result)) { console.log(result.context.signature); // Always available for successful results. console.log(result.context.transaction); // Available if you stored it. console.log(result.context.startedAt); // Custom context property. } if (isFailedSingleTransactionPlanResult(result)) { console.log(result.error); // The error that caused the failure. console.log(result.context.message); // Available if stored before the failure. console.log(result.context.transaction); // Available if stored before the failure. console.log(result.context.startedAt); // Custom context property. } ``` Check out the [Recipes section](#recipes) at this end of this guide for ideas of what can be achieved with this API. ### Transaction plan results When you execute a transaction plan, you get back a `TransactionPlanResult` that tells you what happened during execution. This result object has the same tree structure as your original transaction plan, but includes execution information for each transaction message. Each transaction message in your plan can have one of three execution outcomes: - **Successful** - The transaction was sent and confirmed. You get the original planned message, a context object containing the signature (and optionally the transaction), plus any custom context data. - **Failed** - The transaction encountered an error. You get the original planned message, the error that caused the failure, and a context object with any data accumulated before the failure. - **Canceled** - The transaction was skipped because an earlier transaction failed or the operation was aborted. You get the original planned message with any context data accumulated before cancellation. The result structure mirrors your transaction plan structure: - Single transaction messages become `SingleTransactionPlanResult` with the original `plannedMessage` plus execution status - Sequential plans become `SequentialTransactionPlanResult` containing child results - Parallel plans become `ParallelTransactionPlanResult` containing child results Each `SingleTransactionPlanResult` has a `status` property that is a string literal (`'successful'`, `'failed'`, or `'canceled'`), and properties like `context`, `error` live at the top level of each variant. ```ts twoslash import { parallelTransactionPlan, singleTransactionPlan, parallelTransactionPlanResult, successfulSingleTransactionPlanResultFromTransaction, failedSingleTransactionPlanResult, isSuccessfulSingleTransactionPlanResult, isFailedSingleTransactionPlanResult, SolanaError, TransactionMessage, TransactionMessageWithFeePayer, Transaction, } from '@solana/kit'; const messageA = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer; const messageB = {} as unknown as TransactionMessage & TransactionMessageWithFeePayer; const transactionA = {} as unknown as Transaction; const error = {} as unknown as SolanaError; // ---cut-before--- // If your transaction plan looked like this: const plan = parallelTransactionPlan([ singleTransactionPlan(messageA), singleTransactionPlan(messageB), ]); // Your result may look like this: const result = parallelTransactionPlanResult([ successfulSingleTransactionPlanResultFromTransaction(messageA, transactionA), failedSingleTransactionPlanResult(messageB, error), ]); // Access the signature from a successful result: if (isSuccessfulSingleTransactionPlanResult(result.plans[0])) { console.log(result.plans[0].context.signature); console.log(result.plans[0].context.transaction); // If available } // Access the error from a failed result: if (isFailedSingleTransactionPlanResult(result.plans[1])) { console.log(result.plans[1].error); console.log(result.plans[1].context.signature); // If available console.log(result.plans[1].context.transaction); // If available } ``` ### Failed transaction executions When a transaction plan executor — created via the `createTransactionPlanExecutor` helper — encounters a failed transaction, it will cancel all remaining transactions in the plan. The executor will then throw a `SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN` error code. This error contains a `transactionPlanResult` property (accessible as a non-enumerable property) that provides detailed information about which transactions succeeded, failed, or were canceled. ```ts twoslash import { TransactionPlan, TransactionPlanExecutor, SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN, isSolanaError, TransactionPlanResult, } from '@solana/kit'; const transactionPlan = {} as unknown as TransactionPlan; const transactionPlanExecutor = {} as unknown as TransactionPlanExecutor; // ---cut-before--- try { const result = await transactionPlanExecutor(transactionPlan); } catch (error) { if (isSolanaError(error, SOLANA_ERROR__INSTRUCTION_PLANS__FAILED_TO_EXECUTE_TRANSACTION_PLAN)) { // Access the failed `TransactionPlanResult` to understand what happened. const result = error.context.transactionPlanResult as TransactionPlanResult; } } ``` ### Advanced transaction plan executors Whilst the `createTransactionPlanExecutor` helper is designed to suit most projects, it may not suit yours. If so, you may create your own by offering a function that satisfies the following signature. ```ts twoslash import { TransactionPlan, TransactionPlanResult } from '@solana/kit'; // ---cut-before--- type TransactionPlanExecutor = ( transactionPlan: TransactionPlan, config?: { abortSignal?: AbortSignal }, ) => Promise; ``` This allows you to implement custom execution strategies, such as using transaction bundles for atomic execution or adding custom transaction prioritization. Bundled clients such as `solanaRpc` and `litesvm` install ready-to-use planner and executor implementations on the client. See [Available plugins](/docs/plugins/available-plugins) for the full list, and [Sending multiple transactions](/docs/guides/sending-multiple-transactions) for the consumer-facing API they expose. ## Recipes Here are some common patterns and recipes for using instruction plans effectively. ### Setting priority fees You can set priority fees by using the [`setTransactionMessageComputeUnitPrice`](/api/functions/setTransactionMessageComputeUnitPrice) helper from `@solana/kit` in your `createTransactionMessage` function. See [Configuring compute and priority fees](/docs/advanced-guides/transactions#configuring-priority-fees) for the full set of fee setters, including the v1 [`setTransactionMessagePriorityFeeLamports`](/api/functions/setTransactionMessagePriorityFeeLamports) variant. ```ts twoslash import { createTransactionPlanner, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, TransactionSigner, } from '@solana/kit'; const payer = {} as unknown as TransactionSigner; // ---cut-before--- import { setTransactionMessageComputeUnitPrice } from '@solana/kit'; // [!code ++] const transactionPlanner = createTransactionPlanner({ createTransactionMessage: () => pipe( createTransactionMessage({ version: 0 }), (message) => setTransactionMessageFeePayerSigner(payer, message), // [!code ++:2] // Set priority fees to 0.01 lamports per compute unit. (message) => setTransactionMessageComputeUnitPrice(10_000n, message), ), }); ``` ### Estimating compute units You can estimate and set compute unit limits dynamically by using a two-step process: - First, add a provisory compute unit limit (if missing) in your transaction planner. - Then, estimate and update it right before sending the transaction. The [`fillTransactionMessageProvisoryComputeUnitLimit`](/api/functions/fillTransactionMessageProvisoryComputeUnitLimit) helper from `@solana/kit` reserves the limit by setting it to a provisory value of `0`, so the planner can account for the resulting compute budget instruction when packing transaction messages. See [Estimating the compute unit limit](/docs/advanced-guides/transactions#configuring-compute-unit-limit-estimation) for the full estimator API. ```ts twoslash import { createTransactionPlanner, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, TransactionSigner, } from '@solana/kit'; const payer = {} as unknown as TransactionSigner; // ---cut-before--- import { fillTransactionMessageProvisoryComputeUnitLimit } from '@solana/kit'; // [!code ++] const transactionPlanner = createTransactionPlanner({ createTransactionMessage: () => pipe( createTransactionMessage({ version: 0 }), (message) => setTransactionMessageFeePayerSigner(payer, message), (message) => fillTransactionMessageProvisoryComputeUnitLimit(message), // [!code ++] ), }); ``` Then, pair [`estimateComputeUnitLimitFactory`](/api/functions/estimateComputeUnitLimitFactory) with [`estimateAndSetComputeUnitLimitFactory`](/api/functions/estimateAndSetComputeUnitLimitFactory) in your transaction plan executor to estimate the compute units right before sending the transaction. ```ts twoslash import { sendAndConfirmTransactionFactory, createTransactionPlanExecutor, Rpc, SolanaRpcApi, RpcSubscriptions, SolanaRpcSubscriptionsApi, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, assertIsSendableTransaction, assertIsTransactionWithBlockhashLifetime, TransactionMessage, TransactionMessageWithFeePayer, } from '@solana/kit'; const rpc = {} as unknown as Rpc; const rpcSubscriptions = {} as unknown as RpcSubscriptions; // ---cut-before--- // [!code ++:4] import { estimateAndSetComputeUnitLimitFactory, estimateComputeUnitLimitFactory, } from '@solana/kit'; const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); // [!code ++:2] const estimateCULimit = estimateComputeUnitLimitFactory({ rpc }); const estimateAndSetCULimit = estimateAndSetComputeUnitLimitFactory(estimateCULimit); const transactionPlanExecutor = createTransactionPlanExecutor({ executeTransactionMessage: async (context, message) => { const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const messageWithBlockhash = setTransactionMessageLifetimeUsingBlockhash( latestBlockhash, message, ); context.message = messageWithBlockhash; const estimatedMessage = await estimateAndSetCULimit(messageWithBlockhash); // [!code ++] context.message = estimatedMessage; // [!code ++] const transaction = await signTransactionMessageWithSigners(estimatedMessage); context.transaction = transaction; assertIsSendableTransaction(transaction); assertIsTransactionWithBlockhashLifetime(transaction); await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' }); return transaction; }, }); ``` ### Durable nonce executor You may create transaction plans that use durable nonces for offline transaction signing by using the `setTransactionMessageLifetimeUsingDurableNonce` helper in your transaction planner. ```ts twoslash import { createTransactionPlanner, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, TransactionSigner, } from '@solana/kit'; const payer = {} as unknown as TransactionSigner; const config = {} as unknown as Parameters< typeof setTransactionMessageLifetimeUsingDurableNonce >['0']; const nonce = config.nonce; const nonceAccountAddress = config.nonceAccountAddress; const nonceAuthorityAddress = config.nonceAuthorityAddress; // ---cut-before--- import { setTransactionMessageLifetimeUsingDurableNonce } from '@solana/kit'; // [!code ++] const transactionPlanner = createTransactionPlanner({ createTransactionMessage: () => { return pipe( createTransactionMessage({ version: 0 }), (message) => setTransactionMessageFeePayerSigner(payer, message), // [!code ++:5] (message) => setTransactionMessageLifetimeUsingDurableNonce( { nonce, nonceAccountAddress, nonceAuthorityAddress }, message, ), ); }, }); ``` Then, make sure to use the `sendAndConfirmDurableNonceTransactionFactory` helper in your transaction plan executor in order to use the appropriate confirmation strategy for your transactions. ```ts twoslash import { createTransactionPlanExecutor, Rpc, SolanaRpcApi, RpcSubscriptions, SolanaRpcSubscriptionsApi, signTransactionMessageWithSigners, assertIsSendableTransaction, TransactionMessage, TransactionMessageWithFeePayer, assertIsTransactionWithDurableNonceLifetime, } from '@solana/kit'; const rpc = {} as unknown as Rpc; const rpcSubscriptions = {} as unknown as RpcSubscriptions; // ---cut-before--- // [!code ++:4] import { assertIsTransactionMessageWithDurableNonceLifetime, sendAndConfirmDurableNonceTransactionFactory, } from '@solana/kit'; // [!code ++:4] const sendAndConfirmDurableNonceTransaction = sendAndConfirmDurableNonceTransactionFactory({ rpc, rpcSubscriptions, }); const transactionPlanExecutor = createTransactionPlanExecutor({ executeTransactionMessage: async (context, message) => { assertIsTransactionMessageWithDurableNonceLifetime(message); // [!code ++] context.message = message; const transaction = await signTransactionMessageWithSigners(message); context.transaction = transaction; assertIsSendableTransaction(transaction); assertIsTransactionWithDurableNonceLifetime(transaction); await sendAndConfirmDurableNonceTransaction(transaction, { commitment: 'confirmed' }); // [!code ++] return transaction; }, }); ``` ================================================ FILE: docs/content/docs/advanced-guides/keypairs.mdx ================================================ --- title: Key pairs description: Sign and verify messages and transactions using Ed25519 keys --- ## Introduction Kit uses the primitives built-in to JavaScript’s Web Crypto API to perform cryptography. This keeps your applications small, and confers to them the security and performance characteristics of the runtime's native cryptography functions. Ed25519 keys created or imported using the native APIs are compatible with all of Kit's cryptographic features. Kit also offers several helpers to generate, import, and transform key material. Keys are a low-level primitive. While it's important to know how they work, in a Solana application it's often more appropriate to deal with key material in terms of the accounts and wallets that sign a transaction or message. The [Signers](/docs/advanced-guides/signers) API offers an ergonomic way to build and sign transactions using accounts and their associated keys. ## Installation Key management functions are **included within the `@solana/kit` library** but you may also install them using their standalone package. ```package-install @solana/keys ``` When deploying your application to a JavaScript runtime that lacks support for the Ed25519 digital signature algorithm, [import our polyfill](#polyfill) before invoking any operations that create or make use of `CryptoKey` objects. ## What is a key pair? A key pair is a data type composed of 256-bits of random data (the private key) and a Cartesian point (the public key) on the curve that is used to cryptographically sign and verify Solana transactions. The interface definition of `CryptoKeyPair` is: ```ts interface CryptoKeyPair { privateKey: CryptoKey; publicKey: CryptoKey; } ``` Together, these keys represent an address on Solana and its owner. The 256-bit address is derived from the coordinates of the public key itself. ## What is a key? A key is an object native to the JavaScript runtime that supports the [`CryptoKey`](https://developer.mozilla.org/en-US/docs/Web/API/CryptoKey) interface. You can use a `CryptoKey` with the native [`SubtleCrypto` API](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) to sign messages and to verify signatures. Kit builds on these capabilities to enable the use of `CryptoKey` objects to sign and verify Solana transactions and off-chain messages. ## Managing keys ### Generating new keys You can use the native [`SubtleCrypto#generateKey`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey) API, or the [`generateKeyPair()`](/api/functions/generateKeyPair) helper to create a new random key pair. ```ts twoslash const keyPair = await crypto.subtle.generateKey( /* algorithm */ { name: 'Ed25519' }, /* extractable */ false, /* usages */ ['sign', 'verify'], ); ``` ```ts twoslash import { generateKeyPair } from '@solana/kit'; const keyPair = await generateKeyPair(); ``` This is useful in cases where you need to sign for the creation of a new account with a random address, using an ephemeral key pair that can be discarded after the account is created and assigned to a program. ### Importing a key You can create a `CryptoKeyPair` using the 64 bytes of a pre-generated key. This is possible to do with the native [`SubtleCrypto#importKey`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey) API, but it is more convenient to use the helpers in Kit. ```ts twoslash import { createKeyPairFromBytes } from '@solana/kit'; const keyPair = await createKeyPairFromBytes( new Uint8Array([ /* 32 bytes representing the private key */ /* 32 bytes representing the public key */ ]), ); ``` In cases where you have only the 32 bytes representing the private key but not its associated public key, you can use a different function that will automatically derive the public key from the private key. ```ts twoslash import { createKeyPairFromPrivateKeyBytes } from '@solana/kit'; const keyPair = await createKeyPairFromPrivateKeyBytes( new Uint8Array([ /* 32 bytes representing the private key */ ]), ); ``` ### Storing keys `CryptoKey` objects can be stored locally by runtimes that support the [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). Given an instance of an `IDBDatabase` with an object store called `'MyKeyPairStore'`: ```ts twoslash const db = await new Promise((resolve, reject) => { const request = indexedDB.open('MyDatabase', 1); request.onupgradeneeded = (e) => { const db = (e.target as IDBOpenDBRequest).result; db.createObjectStore('MyKeyPairStore'); }; request.onsuccess = (e) => { resolve((e.target as IDBOpenDBRequest).result); }; request.onerror = (e) => { reject((e.target as IDBOpenDBRequest).error); }; }); ``` You can store a key pair like this: ```ts twoslash const db = null as unknown as IDBDatabase; const keyPair = null as unknown as CryptoKeyPair; // ---cut-before--- const transaction = db.transaction('MyKeyPairStore', 'readwrite'); const store = transaction.objectStore('MyKeyPairStore'); await new Promise((resolve, reject) => { const request = store.put(keyPair, 'myStoredKey'); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error); }); ``` And then later retrieve it like this: ```ts twoslash const db = null as unknown as IDBDatabase; // ---cut-before--- const transaction = db.transaction('MyKeyPairStore', 'readonly'); const store = transaction.objectStore('MyKeyPairStore'); const loadedKeyPair = await new Promise((resolve, reject) => { const request = store.get('myStoredKey'); request.onsuccess = () => { if (request.result) { resolve(request.result); } else { reject(new Error('Key not found')); } }; request.onerror = () => reject(request.error); }); ``` Keys stored in IndexedDB are local to the host, are subject to its [storage limits and eviction criteria](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API#storage_limits_and_eviction_criteria), and can not be accessed from a domain other than the one that stored them when the host is a web browser. Keys that are evicted from storage or erased by the user can generally not be recovered. ### Exporting a key You can obtain the 32 bytes of any public key like this: ```ts twoslash const keyPair = null as unknown as CryptoKeyPair; // ---cut-before--- const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', keyPair.publicKey)); ``` Exporting private key material requires a specially constructed `CryptoKey` with its extractable property set to `true`. ```ts twoslash const keyPair = await crypto.subtle.generateKey( /* algorithm */ { name: 'Ed25519' }, /* extractable */ true, // ^^^^ /* usages */ ['sign', 'verify'], ); ``` You can then use that `CryptoKey` with the [`SubtleCrypto#exportKey`](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/exportKey) API. The last 32 bytes of a PKCS#8 export of the key are the private key bytes. ```ts twoslash const keyPair = null as unknown as CryptoKeyPair; // ---cut-before--- const exportedPrivateKey = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey); const privateKeyBytes = new Uint8Array(exportedPrivateKey, exportedPrivateKey.byteLength - 32, 32); ``` Exporting private key material in JavaScript is not recommended, and you should take extreme care when doing so. Private key bytes exported through these APIs are vulnerable to theft (eg. by code running in your JavaScript sandbox) and accidental logging (eg. to the console or to a third-party logger). ## Using private keys A private key is a `CryptoKey` whose `type` property is set to `'private'` and whose `usages` property includes `'sign'`. Private keys can be used by their owner to produce a digital signature of a specific message. You can think of a signature as representing the key owner's approval of, agreement to, or endorsement of the message. Private key owners must never share the key with anyone. ### Signing messages Any data that you can serialize as a `Uint8Array` can be signed with a `CryptoKey`. ```ts twoslash import { ReadonlyUint8Array } from '@solana/kit'; // ---cut-before--- import { getU32Encoder, getUtf8Encoder } from '@solana/kit'; const messageFromText = getUtf8Encoder().encode('🎉'); const messageFromU32 = getU32Encoder().encode(0xdeadbeef); const messageOfBytes = new Uint8Array([1, 2, 3]); ``` Supply a message and a private key to the [`signBytes()`](/api/functions/signBytes) function to obtain a 64-byte digital signature. ```ts twoslash const bobsKeyPair = null as unknown as CryptoKeyPair; // ---cut-before--- import { getUtf8Encoder, signBytes } from '@solana/kit'; const message = getUtf8Encoder().encode('The meeting is at 6:00pm'); const bobsSignature = await signBytes(bobsKeyPair.privateKey, message); console.log(bobsSignature); // @log: Uint8Array(64) [79, 43, 140, 65, 177, 35, 169, 61, 62, 228, 190, 202, 205, 143, 4, 35, 83, 228, 47, 76, 68, 62, 125, 140, 21, 102, 182, 105, 24, 238, 67, 40, 179, 255, 247, 136, 95, 119, 46, 244, 44, 224, 100, 111, 68, 110, 189, 224, 159, 144, 197, 181, 210, 132, 101, 226, 120, 200, 0, 102, 104, 65, 216, 3] ``` The same message and private key might produce a different signature every time you call this function. Some runtimes produce randomized signatures as per [draft-irtf-cfrg-det-sigs-with-noise](https://datatracker.ietf.org/doc/draft-irtf-cfrg-det-sigs-with-noise/) while others produce deterministic signatures as per [RFC 8032](https://www.rfc-editor.org/rfc/rfc8032). ### Signing transactions In Kit, private keys are used to digitally sign transactions on behalf of account owners, to approve spending, transfers, and modifications to account data on the blockchain. ```ts twoslash import { Transaction, TransactionWithLifetime } from '@solana/kit'; const transaction = null as unknown as Transaction & TransactionWithLifetime; const bobsKeyPair = null as unknown as CryptoKeyPair; // ---cut-before--- import { signTransaction } from '@solana/kit'; const signedTransaction = await signTransaction([bobsKeyPair], transaction); ``` In practice, it's rare to sign transactions like this. Typically, you create a transaction message in your application, specify which accounts are required to sign it using the [Signers API](/docs/advanced-guides/signers), and then call [`signTransactionMessageWithSigners`](/api/functions/signTransactionMessageWithSigners) to turn it into a signed `Transaction`. ## Using public keys A public key is a `CryptoKey` whose `type` property is set to `'public'` and whose `usages` property includes `'verify'`. Solana uses public keys to verify that modifications to account data and balances through transactions are approved of by those who hold the private keys required to authorize such modifications. Kit generally only uses public keys to derive the address of their associated accounts, but it can also use public keys to enable your programs to verify arbitrary messages. ### Verifying signatures The public key associated with a given private key can be used by anyone to verify that a message was signed by the holder of the associated private key, as described above. The verification function requires the original message, a digital signature, and the public key associated with the owner who is believed to have produced that signature. Successful verification implies that the contents of the message signed by the owner are identical to the contents of the message that was given to the verification function. Key owners are free to share their public key with anyone they would like to grant the ability to verify their digital signatures. If someone sends us a message and signature that they claim to be from the example above, we could use Bob's public key to verify that the signature was in fact the one produced by Bob and that the message was not modified in transit. ```ts twoslash import { SignatureBytes } from '@solana/kit'; const bobsPublicKey = null as unknown as CryptoKey; const supposedlyBobsSignature = null as unknown as SignatureBytes; // ---cut-before--- import { getUtf8Encoder, verifySignature } from '@solana/kit'; const verificationSucceeded = await verifySignature( bobsPublicKey, supposedlyBobsSignature, getUtf8Encoder().encode('The meeting is at 6:00pm'), ); if (!verificationSucceeded) { throw new Error( 'Either the message was modified, the signature was not produced by Bob, or both', ); } ``` Verification protects equally against false claims of who produced the signature as it does against false claims about what message was signed. Both of these will return `false`; {/* prettier-ignore */} ```ts twoslash import { SignatureBytes } from '@solana/kit'; const bobsPublicKey = null as unknown as CryptoKey; const bobsSignature = null as unknown as SignatureBytes; const mallorysSignature = null as unknown as SignatureBytes; const signature = null as unknown as SignatureBytes; // ---cut-before--- import { getUtf8Encoder, verifySignature } from '@solana/kit'; // False claim that Bob signed the message when it was in fact Mallory await verifySignature( bobsPublicKey, mallorysSignature, //^^^^^^^^^^^^^^^^^ getUtf8Encoder().encode('The meeting is at 6:00pm'), ); // False claim about the contents of the message await verifySignature( bobsPublicKey, bobsSignature, getUtf8Encoder().encode('The meeting is at 6:00am'), // ^^^^^^ ); ``` ### Deriving addresses The Solana address associated with any public key can be derived using the [`getAddressFromPublicKey()`](/api/functions/getAddressFromPublicKey) function. ```ts twoslash const keyPair = null as unknown as CryptoKeyPair; // ---cut-before--- import { getAddressFromPublicKey } from '@solana/kit'; const address = await getAddressFromPublicKey(keyPair.publicKey); ``` ## Signatures The [`SignatureBytes`](/api/type-aliases/SignatureBytes) type represents a 64-byte Ed25519 signature, and the [`Signature`](/api/type-aliases/Signature) type represents such a signature as a base58-encoded string. When you acquire a string that you expect to be a base58-encoded signature (eg. of a transaction) from an untrusted network API or user input you can assert it is in fact a base58-encoded byte array of sufficient length using the [`assertIsSignature()`](/api/functions/assertIsSignature) function. ```ts twoslash // @noErrors: 1308 import { GetSignatureStatusesApi, Rpc } from '@solana/kit'; const rpc = null as unknown as Rpc; const signatureInput = null as unknown as HTMLInputElement; // ---cut-before--- import { assertIsSignature } from '@solana/kit'; // Imagine a function that asserts whether a user-supplied signature is valid or not. function handleSubmit() { // We know only that what the user typed conforms to the `string` type. const signature: string = signatureInput.value; try { // If this type assertion function doesn't throw, then // Typescript will upcast `signature` to `Signature`. assertIsSignature(signature); // At this point, `signature` is a `Signature` that can be used with the RPC. const { value: [status], } = await rpc.getSignatureStatuses([signature]).send(); } catch (e) { // `signature` turned out not to be a base58-encoded signature } } ``` Similarly, you can use the [`isSignature()`](/api/functions/isSignature) type guard. It will return `true` if the input string conforms to the `Signature` type and will refine the type for use in your program from that point onward. This method does not throw in the opposite case. ```ts twoslash import { GetSignatureStatusesApi, Rpc } from '@solana/kit'; const rpc = null as unknown as Rpc; const signature = ''; function setError(s: string) {} function setSignatureStatus( s: ReturnType['value'][number], ) {} // ---cut-before--- import { isSignature } from '@solana/kit'; if (isSignature(signature)) { // At this point, `signature` has been refined to a // `Signature` that can be used with the RPC. const { value: [status], } = await rpc.getSignatureStatuses([signature]).send(); setSignatureStatus(status); } else { setError(`${signature} is not a transaction signature`); } ``` The [`signature()`](/api/functions/signature) helper combines _asserting_ that a string is an Ed25519 signature with _coercing_ it to the `Signature` type. It's best used with untrusted input. ```ts import { signature } from '@solana/kit'; const signature = signature(userSuppliedSignature); const { value: [status], } = await rpc.getSignatureStatuses([signature]).send(); ``` ## Runtime Support All major JavaScript runtimes support the Ed25519 digital signature algorithm required by Solana. | | Runtime | Min version | Since | | -------------------: | -------------------- | ------------- | -------- | | **Desktop browsers** | Chrome | v137 | May 2025 | | | Edge | v137 | May 2025 | | | Firefox | v130 | Sep 2024 | | | Safari | v17 | Sep 2023 | | **Mobile browsers** | Android browser | v137 | May 2025 | | | Firefox for Android | v139 | May 2025 | | | Mobile Safari | iOS 17 | Sep 2023 | | **Server runtimes** | Bun | v1.2.6 | Mar 2025 | | | Cloudflare `workerd` | v1.20230419.0 | Apr 2023 | | | Deno | v1.26.1 | Oct 2022 | | | Node.js | v18.4.0 | Jun 2022 | | | Vercel Edge | v2.3.0 | May 2023 | For additional up-to-date details on runtimes' implementation status, visit https://github.com/WICG/webcrypto-secure-curves/issues/20. ### Ed25519 Polyfill [#polyfill] To use keys in a runtime without Ed25519 digital signature algorithm support, install the following polyfill. ```package-install @solana/webcrypto-ed25519-polyfill ``` Then import and install it before invoking any operations that create or make use of `CryptoKey` objects. ```ts twoslash import { install } from '@solana/webcrypto-ed25519-polyfill'; // Calling this will shim methods on `SubtleCrypto`, adding Ed25519 support. install(); // Now you can do this, in environments that do not otherwise support Ed25519. const keyPair = await crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign']); ``` Wherever you call `install()`, make sure the call is made only once, and before any key operation requiring Ed25519 support is performed. Because the polyfill's implementation of Ed25519 key generation exists in userspace, it can't guarantee that the keys you generate with it are non-exportable. Untrusted code running in your JavaScript context may still be able to gain access to and/or exfiltrate secret key material. Native `CryptoKeys` can be stored in IndexedDB, but the keys created by this polyfill can not. This is because, unlike native `CryptoKeys`, our polyfilled key objects can not implement the [structured clone algorithm](https://www.w3.org/TR/WebCryptoAPI/#cryptokey-interface-clone). ================================================ FILE: docs/content/docs/advanced-guides/kit-without-a-client.mdx ================================================ --- title: Kit without a client description: Use Kit's full power without the client abstraction --- This guide shows how to achieve the same capabilities a [Kit client](/docs/plugins) gives you, but using Kit's primitives directly. The page is organized as a reference: each section takes one piece of what a fully composed client offers and shows the manual equivalent, so you can pick and choose the exact bits you need. ## When to skip the client Going client-less is the right choice when you want the smallest possible bundles, full control over every step of the transaction lifecycle, or when you are writing library code that should not assume a Kit client. The cost is a little more boilerplate and a less prescriptive structure. The client model, on the other hand, gives you consistent ergonomics and a curated set of plugin interfaces other plugins can build on. If you need ergonomics on top of a curated set of primitives, consider [creating a custom plugin bundle](/docs/plugins/creating-custom-plugins) instead. A bundle plugin is just a regular plugin that composes several smaller ones, so you keep the client model while owning exactly which primitives are wired up. ## Set up RPC Kit ships two factories for talking to a Solana RPC node: `createSolanaRpc` for HTTP requests and `createSolanaRpcSubscriptions` for the WebSocket channel. Both return typed proxies with the full Solana JSON-RPC API surface available on them. ```ts twoslash import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com'); ``` The objects returned by these factories are exactly what `client.rpc` and `client.rpcSubscriptions` expose under the hood, so any code that takes an `Rpc` or `RpcSubscriptions` works in either setup. See [RPC requests](/docs/guides/rpc) and [RPC subscriptions](/docs/guides/rpc-subscriptions) for the user-facing API. ## Create a signer The most common way to create a signer is to generate a fresh in-memory keypair with `generateKeyPairSigner`. The returned object is a `KeyPairSigner` that satisfies both the `TransactionSigner` and `MessageSigner` interfaces. ```ts twoslash import { generateKeyPairSigner } from '@solana/kit'; const signer = await generateKeyPairSigner(); ``` If you already have a keypair stored as a JSON byte array — for example, a Solana CLI keypair file — you can rebuild a signer from it with `createKeyPairSignerFromBytes`. See [Setting up signers](/docs/guides/setting-up-signers) for the broader signer setup story and [Advanced guides — Signers](/docs/advanced-guides/signers) for the underlying signer interfaces. ```ts twoslash import { createKeyPairSignerFromBytes } from '@solana/kit'; import { readFileSync } from 'node:fs'; const bytes = JSON.parse(readFileSync('keypair.json', 'utf-8')) as number[]; const signer = await createKeyPairSignerFromBytes(new Uint8Array(bytes)); ``` ## Build a transaction message Kit transaction messages are immutable: each helper returns a new object with a narrower type, so TypeScript can enforce that required fields are set before the message is signed or sent. The `pipe` helper threads a value through a sequence of transformations and lets the type narrow at each step. ```ts twoslash import { appendTransactionMessageInstructions, createTransactionMessage, Instruction, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, } from '@solana/kit'; // ---cut-start--- import { createSolanaRpc, generateKeyPairSigner } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); const signer = await generateKeyPairSigner(); const instructionA = {} as Instruction; const instructionB = {} as Instruction; // ---cut-end--- const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(signer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([instructionA, instructionB], tx), ); ``` This is the same composition the bundled clients use internally. See [Advanced guides — Transactions](/docs/advanced-guides/transactions) for the full transaction message API, including durable-nonce lifetimes and other configuration helpers. ## Estimate compute units Setting an explicit compute unit limit close to what your transaction actually consumes increases the chance of inclusion, packs more transactions per block, and reduces priority fees. Kit ships everything you need for this without leaving `@solana/kit`: `estimateComputeUnitLimitFactory` simulates the message to estimate the cost, and `estimateAndSetComputeUnitLimitFactory` wraps that estimator to write the result back onto the message in one step. ```ts twoslash import { estimateAndSetComputeUnitLimitFactory, estimateComputeUnitLimitFactory, TransactionMessage, TransactionMessageWithFeePayer, } from '@solana/kit'; // ---cut-start--- import { createSolanaRpc } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); const transactionMessage = {} as TransactionMessage & TransactionMessageWithFeePayer; // ---cut-end--- const estimateComputeUnitLimit = estimateComputeUnitLimitFactory({ rpc }); const estimateAndSetComputeUnitLimit = estimateAndSetComputeUnitLimitFactory(estimateComputeUnitLimit); const transactionMessageWithLimit = await estimateAndSetComputeUnitLimit(transactionMessage); ``` Bundled clients such as `solanaRpc` and `litesvm` already do this for you when sending transactions. ## Sign the transaction Once a transaction message is fully configured, `signTransactionMessageWithSigners` extracts every signer attached to it (the fee payer plus any signer-typed instruction inputs) and produces a signed `Transaction`. ```ts twoslash import { signTransactionMessageWithSigners, TransactionMessage, TransactionMessageWithFeePayer, } from '@solana/kit'; // ---cut-start--- const transactionMessage = {} as TransactionMessage & TransactionMessageWithFeePayer; // ---cut-end--- const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); ``` Two assertions are typically useful at this point: `assertIsSendableTransaction` verifies the transaction has every required signature and fits within the size limit, and `assertIsTransactionWithBlockhashLifetime` narrows the lifetime so it can be passed to `sendAndConfirmTransactionFactory` below. The transaction identifier on Solana is the fee payer's signature, which is fully determined as soon as the fee payer signs — `getSignatureFromTransaction(signedTransaction)` returns that identifier directly, without needing to send the transaction first. ## Send and confirm a transaction `sendAndConfirmTransactionFactory` builds a function that sends a signed transaction and waits for the configured commitment. It needs both an `rpc` and an `rpcSubscriptions` because confirmation listens to slot and signature notifications. ```ts twoslash import { SendableTransaction, sendAndConfirmTransactionFactory, Transaction, TransactionWithBlockhashLifetime, } from '@solana/kit'; // ---cut-start--- import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com'); const signedTransaction = {} as Transaction & SendableTransaction & TransactionWithBlockhashLifetime; // ---cut-end--- const sendAndConfirmTransaction = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); await sendAndConfirmTransaction(signedTransaction, { commitment: 'confirmed' }); ``` For durable-nonce transactions, use `sendAndConfirmDurableNonceTransactionFactory` instead, which knows how to confirm against a nonce account instead of a recent blockhash. If you don't need confirmation at all, `sendTransactionWithoutConfirmingFactory` returns a fire-and-forget sender. All three return `Promise`; pair them with `getSignatureFromTransaction` to obtain the signature for logging or display. ## Fetch and decode accounts To read onchain state, `fetchEncodedAccount` wraps `getAccountInfo` and returns a `MaybeEncodedAccount` whose shape is consistent regardless of whether the account exists. `assertAccountExists` narrows the result so the rest of your code can rely on a fully populated account. ```ts twoslash import { address, assertAccountExists, fetchEncodedAccount } from '@solana/kit'; // ---cut-start--- import { createSolanaRpc } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); // ---cut-end--- const account = await fetchEncodedAccount( rpc, address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'), ); assertAccountExists(account); account.data satisfies Uint8Array; ``` For typed reads, the `@solana-program/*` packages export `fetchX` and `decodeX` standalone helpers that take any `Rpc` and return decoded `Account` values without needing a Kit client. See [Fetching accounts](/docs/guides/fetching-accounts) for the full helper line-up and [Codecs](/docs/advanced-guides/codecs) for decoding raw bytes when no helper is available. ## Next steps - [Plugins](/docs/plugins) — opt back in to the client model when you want it. - [Plugins — Creating custom plugins](/docs/plugins/creating-custom-plugins) — wrap a curated set of primitives into a reusable bundle. - [Advanced guides — Transactions](/docs/advanced-guides/transactions) — the full transaction message API. - [Advanced guides — Signers](/docs/advanced-guides/signers) — the signer interfaces in depth. ================================================ FILE: docs/content/docs/advanced-guides/meta.json ================================================ { "title": "Advanced Guides", "defaultOpen": true, "pages": ["kit-without-a-client", "..."] } ================================================ FILE: docs/content/docs/advanced-guides/offchain-messages.mdx ================================================ --- title: Offchain messages description: Build, compile, sign, and verify messages offchain --- ## Introduction Any time you want one or more parties to approve of something that is not governed by an onchain program, you can prepare a message for them to sign offchain. Such messages can contain arbitrary contents, like the text of a plain-language contract, or some data encoded as text. Each offchain message contains a list of one or more signers that must provide a signature over that encoded message. Only when all of the required signers provide an authentic signature over the message are its contents considered to be ratified. You can use Kit to create, sign, verify, encode, and decode offchain messages. ## Installation Offchain message utilities are **included within the `@solana/kit` library** but you may also install them using their standalone packages. To install the offchain message utilities: ```package-install @solana/offchain-messages ``` ## What is an offchain message? An offchain message consists of some UTF-8 message text, and a list of one or more required signers. The message is considered ratified when all of the signers provide a signature over the encoded message. Here is an example of a contract being proposed by one person as an offchain message, and ratified by another. ```ts twoslash const ursulaKeypair = null as unknown as CryptoKeyPair; // ---cut-before--- import { Address, createSignerFromKeyPair, getOffchainMessageEnvelopeEncoder, OffchainMessage, partiallySignOffchainMessageWithSigners, } from '@solana/kit'; // Ursula creates the contract as an offchain message. const ursulaSigner = await createSignerFromKeyPair(ursulaKeypair); const offchainMessage: OffchainMessage = { content: "Ursula grants Ariel three days as a human, in exchange for Ariel's voice, on the " + 'condition that Ariel secures a kiss of true love from Prince Eric before the third ' + 'sunset to remain human permanently. If Ariel fails, she reverts to a mermaid and ' + "becomes Ursula's property.", requiredSignatories: [ ursulaSigner, { address: 'ARiEL3q7uXvN9yZK8s2a5GfpHmQdR7cBv' as Address }, ], version: 1, }; // Ursula partially signs the message, producing an offchain message envelope. const offchainMessageEnvelope = await partiallySignOffchainMessageWithSigners(offchainMessage); // Ursula encodes the offchain message envelope to share with Ariel. const offchainMessageEnvelopeBytes = getOffchainMessageEnvelopeEncoder().encode(offchainMessageEnvelope); ``` ```ts twoslash import { ReadonlyUint8Array } from '@solana/kit'; const arielKeypair = null as unknown as CryptoKeyPair; const offchainMessageEnvelopeBytes = null as unknown as ReadonlyUint8Array; // ---cut-before--- import { getOffchainMessageEnvelopeCodec, signOffchainMessageEnvelope } from '@solana/kit'; // Ariel decodes the offchain message envelope received from Ursula. const offchainMessageEnvelope = getOffchainMessageEnvelopeCodec().decode(offchainMessageEnvelopeBytes); // To ratify the contract, she signs the message. const fullySignedOffchainMessageEnvelope = await signOffchainMessageEnvelope([arielKeypair], offchainMessageEnvelope); // Ariel encodes the fully signed offchain message envelope to share with Ursula. const fullySignedOffchainMessageEnvelopeBytes = getOffchainMessageEnvelopeCodec().encode(fullySignedOffchainMessageEnvelope); ``` ```ts twoslash import { ReadonlyUint8Array, OffchainMessageEnvelope } from '@solana/kit'; const offchainMessageEnvelope = null as unknown as OffchainMessageEnvelope; const receivedOffchainMessageEnvelopeBytes = null as unknown as ReadonlyUint8Array; // ---cut-before--- import { bytesEqual, getOffchainMessageEnvelopeDecoder, isSolanaError, verifyOffchainMessageEnvelope, SOLANA_ERROR__OFFCHAIN_MESSAGE__SIGNATURE_VERIFICATION_FAILURE, } from '@solana/kit'; // Upon receipt of the signed message envelope bytes from Ariel, Ursula decodes them. const receivedOffchainMessageEnvelope = getOffchainMessageEnvelopeDecoder().decode(receivedOffchainMessageEnvelopeBytes); // For good measure, Ursula verifies that the message bytes are the exact ones she sent. if (!bytesEqual(receivedOffchainMessageEnvelope.content, offchainMessageEnvelope.content)) { throw new Error('I do not accept this modified contract'); } // Ursula then verifies the signatures on the envelope. try { await verifyOffchainMessageEnvelope(receivedOffchainMessageEnvelope); console.log('We have a deal!'); } catch(e) { if (isSolanaError(e, SOLANA_ERROR__OFFCHAIN_MESSAGE__SIGNATURE_VERIFICATION_FAILURE)) { console.error('We do not have a deal!'); } else { throw e; } } ``` ## Building offchain messages Use the [`OffchainMessage`](/api/type-aliases/OffchainMessage) type to help you create an offchain message. ### Specifying the version Specify the `version` property to select the schema and capabilities of the message. The latest version is version `1` which corresponds to the specification found in [sRFC 3](https://github.com/solana-foundation/SRFCs/discussions/3). ```ts twoslash // @noErrors: 2322 import { address, OffchainMessage } from '@solana/kit'; const offchainMessage: OffchainMessage = { // [!code ++:1] version: 1, /* ... */ }; ``` The format and construction of v0 messages is beyond the scope of this guide. You can read the v0 specification [here](https://docs.solanalabs.com/proposals/off-chain-message-signing). ### Required signatories Each message must specify a list of addresses belonging to accounts that must sign the message in order for it to be considered valid. #### Using a signer [!toc] You can declare one or more required signatories using a [`MessageSigner`](/api/interfaces/MessageSigner). ```ts twoslash const myKeyPair = null as unknown as CryptoKeyPair; // ---cut-before--- // @noErrors: 2322 import { createSignerFromKeyPair, OffchainMessage } from '@solana/kit'; const messageSigner = await createSignerFromKeyPair(myKeyPair); const offchainMessage: OffchainMessage = { version: 1, // [!code ++:1] requiredSignatories: [messageSigner], /* ... */ }; ``` When you do so, the message will have the capability to self-sign using the [`signOffchainMessageWithSigners`](/api/functions/signOffchainMessageWithSigners) method. Follow the [instructions for signing offchain message envelopes with `CryptoKeyPairs`](#signing-offchain-message-envelopes) to sign it. #### Using an address [!toc] You can declare one or more required signatories for whom you don't control the private key using the addresses of their accounts. ```ts twoslash const myKeyPair = null as unknown as CryptoKeyPair; // ---cut-before--- // @noErrors: 2322 import { address, OffchainMessage } from '@solana/kit'; const offchainMessage: OffchainMessage = { version: 1, // [!code ++:1] requiredSignatories: [{ address: address('EkMpZ4tPqt7LgNCWjbNQ4WhuzFuitdwp8JPxSbCWXy9x') }], /* ... */ }; ``` ### Defining the message content Each message must contain some non-empty UTF-8 text the signatories must agree upon. #### Using text [!toc] The content can be a string of UTF-8 text. ```ts twoslash const myKeyPair = null as unknown as CryptoKeyPair; // ---cut-before--- import { address, createSignerFromKeyPair, OffchainMessage } from '@solana/kit'; const messageSigner = await createSignerFromKeyPair(myKeyPair); const offchainMessage: OffchainMessage = { version: 1, requiredSignatories: [ messageSigner, { address: address('r5FsobNdd53imrHH4rrdAt1MNkkJUjNPBTNqvKe9igR') }, ], // [!code ++:1] content: '🥓 Crispy bacon is better than floppy bacon.', }; ``` #### Using data [!toc] You can also encode arbitrary data as text using an encoding such as base-64. ```ts twoslash import { ReadonlyUint8Array } from '@solana/kit'; const bytes = null as unknown as ReadonlyUint8Array; const myKeyPair = null as unknown as CryptoKeyPair; // ---cut-before--- import { address, createSignerFromKeyPair, getBase64Decoder, OffchainMessage } from '@solana/kit'; const messageSigner = await createSignerFromKeyPair(myKeyPair); const offchainMessage: OffchainMessage = { version: 1, requiredSignatories: [ messageSigner, { address: address('r5FsobNdd53imrHH4rrdAt1MNkkJUjNPBTNqvKe9igR') }, ], // [!code ++:1] content: getBase64Decoder().decode(bytes), }; ``` ## Signing offchain messages In order to be considered ratified an offchain message must be signed by all of the private keys belonging to accounts that are required signatories of the message. - [`FullySignedOffchainMessageEnvelope`](/api/type-aliases/FullySignedOffchainMessageEnvelope): An offchain message that is signed by all of its required signatories. - [`OffchainMessageEnvelope`](/api/interfaces/OffchainMessageEnvelope): A compiled offchain message encoded as bytes, paired with a map between its required signatory addresses and their provided signatures, if any. Offchain messages whose signers are specified using `MessageSigner` objects have the ability to self-sign. This is because signers encapsulate both the address of the signing account as well as an implementation of the signing algorithm for the private key associated with that account. The [`signOffchainMessageWithSigners`](/api/functions/signOffchainMessageWithSigners) method will return a new signed offchain message envelope of type `FullySignedOffchainMessageEnvelope`. ```ts twoslash import { OffchainMessage } from '@solana/kit'; const offchainMessage = null as unknown as OffchainMessage; // ---cut-before--- import { SOLANA_ERROR__OFFCHAIN_MESSAGE__SIGNATURES_MISSING, isSolanaError, signOffchainMessageWithSigners, } from '@solana/kit'; try { const fullySignedOffchainMessageEnvelope = await signOffchainMessageWithSigners(offchainMessage); } catch (e) { if (isSolanaError(e, SOLANA_ERROR__OFFCHAIN_MESSAGE__SIGNATURES_MISSING)) { console.error('Missing signers for the following addresses:', e.context.addresses); } else { throw e; } } ``` This function will throw if the offchain message does not carry a `MessageSigner` implementation for every required signer. To partially sign a message that you know to carry a strict subset of the required `MessageSigners`, use the [`partiallySignOffchainMessageWithSigners`](/api/functions/partiallySignOffchainMessageWithSigners) method. Building offchain messages using `MessageSigners` is the recommended way to create self-signable offchain messages. To sign with a `CryptoKey` directly, you first have to compile the offchain message. ```ts twoslash import { OffchainMessage } from '@solana/kit'; const offchainMessage = null as unknown as OffchainMessage; // ---cut-before--- import { compileOffchainMessageEnvelope } from '@solana/kit'; const offchainMessageEnvelope = compileOffchainMessageEnvelope(offchainMessage); ``` This produces an unsigned offchain message envelope. Follow the [instructions for signing offchain message envelopes with `CryptoKeyPairs`](#signing-offchain-message-envelopes) to sign it. If the version of the offchain message is known, use the compile function specific to that version, such as [`compileOffchainMessageV1Envelope`](/api/functions/compileOffchainMessageV1Envelope). This will prevent you from bundling compilers you don't need, saving space in your JavaScript bundle. ## Signing offchain message envelopes Wherever you have a `OffchainMessageEnvelope` instead of an `OffchainMessage` you can add or replace a signature using the `signOffchainMessageEnvelope` method and one or more `CryptoKeyPairs`. ```ts twoslash import { OffchainMessageEnvelope } from '@solana/kit'; const keyPair = null as unknown as CryptoKeyPair; const offchainMessageEnvelope = null as unknown as OffchainMessageEnvelope; // ---cut-before--- import { SOLANA_ERROR__OFFCHAIN_MESSAGE__SIGNATURES_MISSING, isSolanaError, signOffchainMessageEnvelope, } from '@solana/kit'; try { const fullySignedOffchainMessageEnvelope = await signOffchainMessageEnvelope( [keyPair], offchainMessageEnvelope, ); } catch (e) { if (isSolanaError(e, SOLANA_ERROR__OFFCHAIN_MESSAGE__SIGNATURES_MISSING)) { console.error('Missing signers for the following addresses:', e.context.addresses); } else { throw e; } } ``` This function will throw if the resultant offchain message envelope is missing a signature for one of the offchain message's required signers. To partially sign an offchain message envelope, use the [`partiallySignOffchainMessageEnvelope`](/api/functions/partiallySignOffchainMessageEnvelope) method. ## Verifying an offchain message Given an offchain message envelope, you can verify that it has been signed by all of its required signatories using the [`verifyOffchainMessageEnvelope`](/api/functions/verifyOffchainMessageEnvelope) method. ```ts twoslash import { OffchainMessageEnvelope } from '@solana/kit'; const receivedOffchainMessageEnvelope = null as unknown as OffchainMessageEnvelope; // ---cut-before--- import { isSolanaError, verifyOffchainMessageEnvelope, SOLANA_ERROR__OFFCHAIN_MESSAGE__SIGNATURE_VERIFICATION_FAILURE, } from '@solana/kit'; try { await verifyOffchainMessageEnvelope(receivedOffchainMessageEnvelope); } catch (e) { if (isSolanaError(e, SOLANA_ERROR__OFFCHAIN_MESSAGE__SIGNATURE_VERIFICATION_FAILURE)) { if (e.context.signatoriesWithInvalidSignatures.length) { console.error( 'The signatures for the following addresses are invalid', e.context.signatoriesWithInvalidSignatures, ); } if (e.context.signatoriesWithMissingSignatures.length) { console.error( 'The following required signatories have not signed this message', e.context.signatoriesWithMissingSignatures, ); } } else { throw e; } } ``` Verifying an offchain message will tell you if its content has been signed by all required signatories, but it will _not_ ensure that the content of the message nor its list of required signatories is what you expect it to be. Take special care to inspect the content of the message before accepting it. If you have the original bytes of the message, you can compare them to the `content` of the envelope you are verifying. Otherwise, see [deserializing offchain messages](#deserializing-offchain-messages) for instructions on how to decode the envelope's `content` for inspection. ## Serializing offchain messages If you would like to share an offchain message envelope with someone, you can serialize it to bytes using an encoder. ```ts twoslash import { OffchainMessageEnvelope } from '@solana/kit'; const offchainMessageEnvelope = null as unknown as OffchainMessageEnvelope; // ---cut-before--- import { getOffchainMessageEnvelopeEncoder } from '@solana/kit'; const offchainMessageEnvelopeBytes = getOffchainMessageEnvelopeEncoder().encode(offchainMessageEnvelope); ``` ## Deserializing offchain messages Decoding the bytes of an encoded offchain message envelope yields an `OffchainMessageEnvelope` object. This takes the form of a compiled offchain message encoded as [`OffchainMessageBytes`](/api/type-aliases/OffchainMessageBytes), paired with a map between its required signatory addresses and their provided signatures, if any. ```ts twoslash import { ReadonlyUint8Array } from '@solana/kit'; const offchainMessageEnvelopeBytes = null as unknown as ReadonlyUint8Array; // ---cut-before--- import { getOffchainMessageEnvelopeDecoder } from '@solana/kit'; const offchainMessageEnvelope = getOffchainMessageEnvelopeDecoder().decode( offchainMessageEnvelopeBytes, ); ``` Decoding the bytes of the offchain message envelope will yield an object containing an offchain message in its **compiled** form – a message in a form suitable for signing and transmitting over a network. Decompiling a compiled message will yield an `OffchainMessage` object. This is the most common form of offchain message that you will encounter when using Kit to build an application. ```ts twoslash import { OffchainMessageEnvelope } from '@solana/kit'; const offchainMessageEnvelope = null as unknown as OffchainMessageEnvelope; // ---cut-before--- import { getOffchainMessageDecoder } from '@solana/kit'; const offchainMessage = getOffchainMessageDecoder().decode(offchainMessageEnvelope.content); ``` ================================================ FILE: docs/content/docs/advanced-guides/signers.mdx ================================================ --- title: Signers description: Accounts with signing capabilities --- ## Introduction Signers are an abstraction that combines an account with an implementation that computes signatures on behalf of that account. No matter what actually computes the signature – a wallet app, a network API, the `SubtleCrypto` API, or a userspace implementation – so long as it implements the correct signer interface for your intended purpose, you can use it with all of Kit's signer-aware functions. Signers are designed to make it easier to build, sign, and send transactions by taking the guesswork out of which accounts' key pairs need to sign a transaction. Signer-aware APIs allow you to associate signer objects with each transaction instruction that requires them, enabling Kit's transaction planning, signing, and sending functions to automatically collect and invoke them. ## Installation Signers are **included within the `@solana/kit` library** but you may also install them using their standalone package. ```package-install @solana/signers ``` ## What is a signer? All signers are wrappers around an [`Address`](/api/type-aliases/Address). This means that most APIs that require an `Address` can be made to accept a signer. Each specific type of signer adds one or more capabilities, such as the ability to sign a message or a transaction on behalf of the account with that address. Some even add the ability to sign _and_ send a transaction, which is common for wallets that you use in your browser or on your phone. ```ts twoslash // @noErrors: 2769 import { createSignableMessage, createTransactionMessage, generateKeyPairSigner, pipe, setTransactionMessageFeePayerSigner, signTransactionMessageWithSigners, } from '@solana/kit'; // Generate a key pair signer. const mySigner = await generateKeyPairSigner(); mySigner.address; // The address of the account // Sign one or multiple messages. const myMessage = createSignableMessage('Hello world!'); const [messageSignatures] = await mySigner.signMessages([myMessage]); // ^^^^^^^^ // Sign to pay fees for a transaction message const myTransactionMessage = pipe( createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayerSigner(mySigner, m), // ^^^^^^^^ // Add instructions, lifetime, etc. ); const signedTransaction = await signTransactionMessageWithSigners(myTransactionMessage); ``` As you can see, this provides a consistent API regardless of how things are being signed behind the scenes. If tomorrow we need to use a browser wallet instead, we'd simply need to swap the `generateKeyPairSigner()` function with the signer factory of our choice. ## Types of signers This package offers a total of five different types of signers that may be used in combination when applicable. Three of them allow us to sign transactions whereas the other two are used for regular message signing. They are separated into three categories: - **Partial signers**: Given a message or transaction, provide one or more signatures for it. These signers are not able to modify the given data which allows us to run many of them in parallel. - **Modifying signers**: Can choose to modify a message or transaction before signing it with zero or more private keys. Because modifying a message or transaction invalidates any pre-existing signatures over it, modifying signers must do their work before any other signer. - **Sending signers**: Given a transaction, signs it and sends it immediately to the blockchain. When applicable, the signer may also decide to modify the provided transaction before signing it. This interface accommodates wallets that simply cannot sign a transaction without sending it at the same time. This category of signers does not apply to regular messages. Thus, we end up with the following interfaces. | | Partial signers | Modifying signers | Sending signers | | ---------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | [`TransactionSigner`](/api/type-aliases/TransactionSigner) | [`TransactionPartialSigner`](/api/type-aliases/TransactionPartialSigner) | [`TransactionModifyingSigner`](/api/type-aliases/TransactionModifyingSigner) | [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner) | | [`MessageSigner`](/api/type-aliases/MessageSigner) | [`MessagePartialSigner`](/api/type-aliases/MessagePartialSigner) | [`MessageModifyingSigner`](/api/type-aliases/MessageModifyingSigner) | N/A | We will go through each of these five signer interfaces and their respective characteristics in the documentation below. ## Signing transactions ### Partial signers [#transaction-partial-signers] [`TransactionPartialSigner`](/api/type-aliases/TransactionPartialSigner) is an interface that signs an array of [`Transactions`](/api/type-aliases/Transaction) without modifying their content. It defines a `signTransactions` function that returns a [`SignatureDictionary`](/api/type-aliases/SignatureDictionary) for each provided transaction. Such signature dictionaries are expected to be merged with the existing ones if any. ```ts twoslash // @noErrors: 2355 import { address, SignatureDictionary, Transaction, TransactionPartialSigner } from '@solana/kit'; // ---cut-before--- const myTransactionPartialSigner: TransactionPartialSigner<'1234..5678'> = { address: address('1234..5678'), signTransactions: async (transactions): Promise => { // My custom signing logic. }, }; ``` **Characteristics**: - **Parallel**. It returns a signature dictionary for each provided transaction without modifying them, making it possible for multiple partial signers to sign the same transaction in parallel. - **Flexible order**. The order in which we use these signers for a given transaction doesn’t matter. ### Modifying signers [`TransactionModifyingSigner`](/api/type-aliases/TransactionModifyingSigner) is an interface that potentially modifies the provided [`Transactions`](/api/type-aliases/Transaction) before signing them. E.g. this enables wallets to inject additional instructions into the transaction before signing them. For each transaction, instead of returning a [`SignatureDictionary`](/api/type-aliases/SignatureDictionary), its `modifyAndSignTransactions` function returns an updated [`Transaction`](/api/type-aliases/Transaction) with a potentially modified set of instructions and signature dictionary. ```ts twoslash // @noErrors: 2355 import { address, Transaction, TransactionModifyingSigner, TransactionWithinSizeLimit, TransactionWithLifetime, } from '@solana/kit'; // ---cut-before--- const myTransactionModifyingSigner: TransactionModifyingSigner<'1234..5678'> = { address: address('1234..5678'), modifyAndSignTransactions: async ( transactions: readonly Transaction[], ): Promise => { // My custom signing logic. }, }; ``` **Characteristics**: - **Sequential**. Contrary to partial signers, these cannot be executed in parallel as each call can modify the provided transactions. - **First signers**. For a given transaction, a modifying signer must always be used before a partial signer as the former will likely modify the transaction and thus impact the outcome of the latter. - **Potential conflicts**. If more than one modifying signer is provided, the second signer may invalidate the signature of the first one. However, modifying signers may decide not to modify a transaction based on the existence of signatures for that transaction. ### Sending signers [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner) is an interface that signs one or multiple transactions before sending them immediately to the blockchain. It defines a `signAndSendTransactions` function that returns the transaction signature (i.e. its identifier) for each provided [`Transaction`](/api/type-aliases/Transaction). This interface is required for PDA wallets and other types of wallets that don't provide an interface for signing transactions without sending them. Note that it is also possible for such signers to modify the provided transactions before signing and sending them. This enables use cases where the modified transactions cannot be shared with the app and thus must be sent directly. ```ts twoslash // @noErrors: 2355 import { address, SignatureBytes, Transaction, TransactionSendingSigner } from '@solana/kit'; // ---cut-before--- const myTransactionSendingSigner: TransactionSendingSigner<'1234..5678'> = { address: address('1234..5678'), signAndSendTransactions: async (transactions: Transaction[]): Promise => { // My custom signing logic. }, }; ``` **Characteristics**: - **Single signer**. Since this signer also sends the provided transactions, we can only use a single [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner) for a given set of transactions. - **Last signer**. Trivially, that signer must also be the last one used. - **Potential conflicts**. Since signers may decide to modify the given transactions before sending them, they may invalidate previous signatures. However, signers may decide not to modify a transaction based on the existence of signatures for that transaction. - **Potential confirmation**. Whilst this is not required by this interface, it is also worth noting that most wallets will also wait for the transaction to be confirmed (typically with a `confirmed` commitment) before notifying the app that they are done. ## Signing messages ### Signable messages [`SignableMessage`](/api/type-aliases/SignableMessage) defines a message with any of the signatures that might have already been provided by other signers. This interface allows modifying signers to decide on whether or not they should modify the provided message depending on whether or not signatures already exist for such message. It also helps create a more consistent API by providing a structure analogous to transactions which also keep track of their signature dictionary. ```ts twoslash import { SignatureDictionary } from '@solana/kit'; // ---cut-before--- type SignableMessage = { content: Uint8Array; signatures: SignatureDictionary; // Record }; ``` You can use the [`createSignableMessage`](/api/functions/createSignableMessage) function to create a [`SignableMessage`](/api/type-aliases/SignableMessage) from a `Uint8Array` or UTF-8 string. It optionally accepts a signature dictionary if the message already contains signatures. ```ts twoslash import { address, createSignableMessage, SignatureBytes } from '@solana/kit'; // ---cut-before-- const myMessage = createSignableMessage(new Uint8Array([1, 2, 3])); const myMessageFromText = createSignableMessage('Hello world!'); const myMessageWithSignatures = createSignableMessage('Hello world!', { [address('1234..5678')]: new Uint8Array([1, 2, 3]) as SignatureBytes, }); ``` ### Partial signers [`MessagePartialSigner`](/api/type-aliases/MessagePartialSigner) is an interface that signs an array of [`SignableMessages`](/api/type-aliases/SignableMessage) without modifying their content. It defines a `signMessages` function that returns a [`SignatureDictionary`](/api/type-aliases/SignatureDictionary) for each provided message. Such signature dictionaries are expected to be merged with the existing ones if any. ```ts twoslash // @noErrors: 2355 import { address, MessagePartialSigner, SignableMessage, SignatureDictionary } from '@solana/kit'; // ---cut-before--- const myMessagePartialSigner: MessagePartialSigner<'1234..5678'> = { address: address('1234..5678'), signMessages: async (messages: SignableMessage[]): Promise => { // My custom signing logic. }, }; ``` **Characteristics**: - **Parallel**. When multiple signers sign the same message, we can perform this operation in parallel to obtain all their signatures. - **Flexible order**. The order in which we use these signers for a given message doesn’t matter. ### Modifying signers [`MessageModifyingSigner`](/api/type-aliases/MessageModifyingSigner) is an interface that potentially modifies the content of the provided [`SignableMessages`](/api/type-aliases/SignableMessage) before signing them. E.g. this enables wallets to prefix or suffix nonces to the messages they sign. For each message, instead of returning a [`SignatureDictionary`](/api/type-aliases/SignatureDictionary), its `modifyAndSignMessages` function returns its updated [`SignableMessage`](/api/type-aliases/SignableMessage) with a potentially modified content and signature dictionary. ```ts twoslash // @noErrors: 2355 import { address, MessageModifyingSigner, SignableMessage } from '@solana/kit'; // ---cut-before--- const myMessageModifyingSigner: MessageModifyingSigner<'1234..5678'> = { address: address('1234..5678'), modifyAndSignMessages: async (messages: SignableMessage[]): Promise => { // My custom signing logic. }, }; ``` **Characteristics**: - **Sequential**. Contrary to partial signers, these cannot be executed in parallel as each call can modify the content of the message. - **First signers**. For a given message, a modifying signer must always be used before a partial signer as the former will likely modify the message and thus impact the outcome of the latter. - **Potential conflicts**. If more than one modifying signer is provided, the second signer may invalidate the signature of the first one. However, modifying signers may decide not to modify a message based on the existence of signatures for that message. ## Available signers ### No-op signers For a given address, a no-op signer can be created to offer an implementation of both the `MessagePartialSigner` and `TransactionPartialSigner` interfaces such that they do not sign anything. Namely, signing a transaction or a message with a `NoopSigner` will return an empty `SignatureDictionary`. This signer may be useful: - For testing purposes. - For indicating that a given account is a signer and taking the responsibility to provide the signature for that account ourselves. For instance, if we need to send the transaction to a server that will sign it and send it for us. ```ts twoslash import { address, SignableMessage, Transaction, TransactionWithinSizeLimit, TransactionWithLifetime, } from '@solana/kit'; const myMessage = null as unknown as SignableMessage; const myTransaction = null as unknown as Transaction & TransactionWithinSizeLimit & TransactionWithLifetime; // ---cut-before--- import { createNoopSigner } from '@solana/kit'; const myNoopSigner = createNoopSigner(address('1234..5678')); const [myMessageSignatures] = await myNoopSigner.signMessages([myMessage]); // <- Empty signature dictionary. const [myTransactionSignatures] = await myNoopSigner.signTransactions([myTransaction]); // <- Empty signature dictionary. ``` ### Key pair signers A key pair signer uses a `CryptoKeyPair` to sign messages and transactions. It implements both the `MessagePartialSigner` and `TransactionPartialSigner` interfaces and keeps track of the `CryptoKeyPair` instance used to sign messages and transactions.
[createSignerFromKeyPair](#create-signer-from-key-pair) [generateKeyPairSigner](#generate-key-pair-signer) [createKeyPairSignerFromBytes](#create-key-pair-signer-from-bytes) [createKeyPairSignerFromPrivateKeyBytes](#create-key-pair-signer-from-private-key-bytes)
#### createSignerFromKeyPair [!toc] [#create-signer-from-key-pair] Creates a `KeyPairSigner` from a provided Crypto KeyPair. The `signMessages` and `signTransactions` functions of the returned signer will use the private key of the provided key pair to sign messages and transactions. Note that both the `signMessages` and `signTransactions` implementations are parallelized, meaning that they will sign all provided messages and transactions in parallel. ```ts twoslash import { createSignerFromKeyPair, generateKeyPair, KeyPairSigner } from '@solana/kit'; const myKeyPair: CryptoKeyPair = await generateKeyPair(); const myKeyPairSigner: KeyPairSigner = await createSignerFromKeyPair(myKeyPair); ``` #### generateKeyPairSigner [!toc] [#generate-key-pair-signer] A convenience function that generates a new Crypto KeyPair and immediately creates a `KeyPairSigner` from it. ```ts twoslash import { generateKeyPairSigner } from '@solana/kit'; const myKeyPairSigner = await generateKeyPairSigner(); ``` #### createKeyPairSignerFromBytes [!toc] [#create-key-pair-signer-from-bytes] A convenience function that creates a new KeyPair from a 64-bytes `Uint8Array` secret key and immediately creates a `KeyPairSigner` from it. ```ts twoslash import fs from 'fs'; import { createKeyPairSignerFromBytes } from '@solana/kit'; // Get bytes from local keypair file. const keypairFile = fs.readFileSync('~/.config/solana/id.json'); const keypairBytes = new Uint8Array(JSON.parse(keypairFile.toString())); // Create a KeyPairSigner from the bytes. const signer = await createKeyPairSignerFromBytes(keypairBytes); ``` #### createKeyPairSignerFromPrivateKeyBytes [!toc] [#create-key-pair-signer-from-private-key-bytes] A convenience function that creates a new KeyPair from a 32-bytes `Uint8Array` private key and immediately creates a `KeyPairSigner` from it. ```ts twoslash import { createKeyPairSignerFromPrivateKeyBytes, getUtf8Encoder } from '@solana/kit'; const message = getUtf8Encoder().encode('Hello, World!'); const seed = new Uint8Array(await crypto.subtle.digest('SHA-256', message)); const derivedSigner = await createKeyPairSignerFromPrivateKeyBytes(seed); ``` ### Wallet account signers Wallet account signers bridge between [Wallet Standard](https://github.com/wallet-standard/wallet-standard) accounts and Kit's signer interfaces. They live in the [`@solana/wallet-account-signer`](https://www.npmjs.com/package/@solana/wallet-account-signer) package and let any wallet that exposes the standard Solana features participate in signing transactions and messages. ```package-install @solana/wallet-account-signer ```
[createSignerFromWalletAccount](#create-signer-from-wallet-account) [createTransactionSignerFromWalletAccount](#create-transaction-signer-from-wallet-account) [createTransactionSendingSignerFromWalletAccount](#create-transaction-sending-signer-from-wallet-account) [createMessageSignerFromWalletAccount](#create-message-signer-from-wallet-account)
#### createSignerFromWalletAccount [!toc] [#create-signer-from-wallet-account] Inspects the wallet account's available features and returns a single signer object combining whichever capabilities the wallet supports — `modifyAndSignTransactions`, `signAndSendTransactions`, and/or `modifyAndSignMessages`. Use this when you do not know in advance which features a wallet exposes. ```ts twoslash import { UiWalletAccount } from '@wallet-standard/ui'; const walletAccount = null as unknown as UiWalletAccount; // ---cut-before--- import { createSignerFromWalletAccount } from '@solana/wallet-account-signer'; const signer = createSignerFromWalletAccount(walletAccount, 'solana:devnet'); ``` #### createTransactionSignerFromWalletAccount [!toc] [#create-transaction-signer-from-wallet-account] Returns a [`TransactionModifyingSigner`](/api/type-aliases/TransactionModifyingSigner) that signs transactions with the wallet via the `solana:signTransaction` feature. The signer is allowed to modify the transaction before signing, which lets the wallet inject guard instructions or priority fees if it wants to. ```ts twoslash import { UiWalletAccount } from '@wallet-standard/ui'; const walletAccount = null as unknown as UiWalletAccount; // ---cut-before--- import { createTransactionSignerFromWalletAccount } from '@solana/wallet-account-signer'; const signer = createTransactionSignerFromWalletAccount(walletAccount, 'solana:devnet'); ``` #### createTransactionSendingSignerFromWalletAccount [!toc] [#create-transaction-sending-signer-from-wallet-account] Returns a [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner) that uses the `solana:signAndSendTransaction` feature to sign and immediately submit the transaction to the network. This is the right shape for wallets that do not let the dapp inspect or relay the signed transaction itself. ```ts twoslash import { UiWalletAccount } from '@wallet-standard/ui'; const walletAccount = null as unknown as UiWalletAccount; // ---cut-before--- import { createTransactionSendingSignerFromWalletAccount } from '@solana/wallet-account-signer'; const signer = createTransactionSendingSignerFromWalletAccount(walletAccount, 'solana:devnet'); ``` #### createMessageSignerFromWalletAccount [!toc] [#create-message-signer-from-wallet-account] Returns a [`MessageModifyingSigner`](/api/type-aliases/MessageModifyingSigner) that signs arbitrary messages via the `solana:signMessage` feature. This is the wallet-backed counterpart to a key pair signer's message-signing capability. ```ts twoslash import { UiWalletAccount } from '@wallet-standard/ui'; const walletAccount = null as unknown as UiWalletAccount; // ---cut-before--- import { createMessageSignerFromWalletAccount } from '@solana/wallet-account-signer'; const signer = createMessageSignerFromWalletAccount(walletAccount); ``` ## Signing with signers Kit provides helper functions that use [`TransactionSigner`](/api/type-aliases/TransactionSigner) objects to sign and optionally send transactions. There are two families of signing functions: - **Transaction message functions** extract signers from the transaction message — either from the fee payer or from account metas — and handle compilation and signing automatically. - **Transaction functions** accept an explicit array of signers alongside a compiled [`Transaction`](/api/type-aliases/Transaction), giving you full control over which signers are used. ### Signing transaction messages The following functions extract [`TransactionSigners`](/api/type-aliases/TransactionSigner) from a transaction message's account metas, compile the message into a [`Transaction`](/api/type-aliases/Transaction), and use those signers to sign it. #### partiallySignTransactionMessageWithSigners [!toc] [#partially-sign-transaction-message-with-signers] Signs a transaction message without requiring all signatures to be present. This is useful when you know the message only carries a subset of the required signers and you plan to add more signatures later. ```ts twoslash import { TransactionMessage, TransactionMessageWithFeePayer, TransactionMessageWithSigners, } from '@solana/kit'; const transactionMessage = null as unknown as TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners; // ---cut-before--- import { partiallySignTransactionMessageWithSigners } from '@solana/kit'; const signedTransaction = await partiallySignTransactionMessageWithSigners(transactionMessage); ``` This function ignores any [`TransactionSendingSigners`](/api/type-aliases/TransactionSendingSigner) in the message since it does not send the transaction. Use [`signAndSendTransactionMessageWithSigners`](#sign-and-send-transaction-message-with-signers) if you need to sign and send in a single step. #### signTransactionMessageWithSigners [!toc] [#sign-transaction-message-with-signers] Signs a transaction message and asserts that all required signatures are present. The returned transaction satisfies the [`FullySignedTransaction`](/api/type-aliases/FullySignedTransaction) type. ```ts twoslash import { TransactionMessage, TransactionMessageWithFeePayer, TransactionMessageWithSigners, } from '@solana/kit'; const transactionMessage = null as unknown as TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners; // ---cut-before--- import { signTransactionMessageWithSigners } from '@solana/kit'; const fullySignedTransaction = await signTransactionMessageWithSigners(transactionMessage); ``` This function will throw if the transaction message does not carry a [`TransactionSigner`](/api/type-aliases/TransactionSigner) implementation for every required signer. To partially sign a message that you know to carry a strict subset of the required signers, use [`partiallySignTransactionMessageWithSigners`](#partially-sign-transaction-message-with-signers). #### signAndSendTransactionMessageWithSigners [!toc] [#sign-and-send-transaction-message-with-signers] Signs a transaction message and sends it to the network via the [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner) found in the message's account metas. Returns the transaction signature as [`SignatureBytes`](/api/type-aliases/SignatureBytes). ```ts twoslash import { TransactionMessage, TransactionMessageWithFeePayer, TransactionMessageWithSigners, TransactionMessageWithSingleSendingSigner, } from '@solana/kit'; const transactionMessage = null as unknown as TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners & TransactionMessageWithSingleSendingSigner; // ---cut-before--- import { signAndSendTransactionMessageWithSigners } from '@solana/kit'; const signature = await signAndSendTransactionMessageWithSigners(transactionMessage); ``` The message must contain exactly one resolvable [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner). You can check this ahead of time using [`isTransactionMessageWithSingleSendingSigner`](/api/functions/isTransactionMessageWithSingleSendingSigner) to provide a fallback strategy: ```ts twoslash // @noErrors: 2345 import { Rpc, RpcSubscriptions, SolanaRpcApi, SolanaRpcSubscriptionsApi, TransactionMessage, TransactionMessageWithBlockhashLifetime, TransactionMessageWithFeePayer, TransactionMessageWithSigners, } from '@solana/kit'; const transactionMessage = null as unknown as TransactionMessage & TransactionMessageWithBlockhashLifetime & TransactionMessageWithFeePayer & TransactionMessageWithSigners; const rpc = null as unknown as Rpc; const rpcSubscriptions = null as unknown as RpcSubscriptions; // ---cut-before--- import { isTransactionMessageWithSingleSendingSigner, sendAndConfirmTransactionFactory, signAndSendTransactionMessageWithSigners, signTransactionMessageWithSigners, } from '@solana/kit'; if (isTransactionMessageWithSingleSendingSigner(transactionMessage)) { // A sending signer is available — sign and send in one step. const signature = await signAndSendTransactionMessageWithSigners(transactionMessage); } else { // No sending signer — sign locally and use sendAndConfirmTransaction. const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); await sendAndConfirm(signedTransaction, { commitment: 'confirmed' }); } ``` ### Signing compiled transactions When you already have a compiled [`Transaction`](/api/type-aliases/Transaction) and an explicit set of signers — for example, when signers are not embedded in the transaction message or when working with an externally provided transaction — you can use the following lower-level functions. #### partiallySignTransactionWithSigners [!toc] [#partially-sign-transaction-with-signers] Signs a compiled transaction using the provided [`TransactionModifyingSigners`](/api/type-aliases/TransactionModifyingSigner) and [`TransactionPartialSigners`](/api/type-aliases/TransactionPartialSigner) without requiring all signatures to be present. ```ts twoslash import { Transaction, TransactionWithLifetime, TransactionPartialSigner, TransactionModifyingSigner, } from '@solana/kit'; const compiledTransaction = null as unknown as Transaction & TransactionWithLifetime; const signerA = null as unknown as TransactionPartialSigner; const signerB = null as unknown as TransactionPartialSigner; // ---cut-before--- import { partiallySignTransactionWithSigners } from '@solana/kit'; const signedTransaction = await partiallySignTransactionWithSigners( [signerA, signerB], compiledTransaction, ); ``` This function ignores any [`TransactionSendingSigners`](/api/type-aliases/TransactionSendingSigner) in the provided array. Use [`signAndSendTransactionWithSigners`](#sign-and-send-transaction-with-signers) if you need to sign and send. #### signTransactionWithSigners [!toc] [#sign-transaction-with-signers] Signs a compiled transaction using the provided signers and asserts that all required signatures are present. The returned transaction satisfies the [`FullySignedTransaction`](/api/type-aliases/FullySignedTransaction) type. ```ts twoslash import { Transaction, TransactionWithLifetime, TransactionPartialSigner } from '@solana/kit'; const compiledTransaction = null as unknown as Transaction & TransactionWithLifetime; const signerA = null as unknown as TransactionPartialSigner; const signerB = null as unknown as TransactionPartialSigner; // ---cut-before--- import { signTransactionWithSigners } from '@solana/kit'; const fullySignedTransaction = await signTransactionWithSigners( [signerA, signerB], compiledTransaction, ); ``` This function will throw if the resultant transaction is missing a signature for one of its required signers. To partially sign, use [`partiallySignTransactionWithSigners`](#partially-sign-transaction-with-signers). #### signAndSendTransactionWithSigners [!toc] [#sign-and-send-transaction-with-signers] Signs a compiled transaction using the provided signers and sends it to the network via the [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner) found in the array. Returns the transaction signature as [`SignatureBytes`](/api/type-aliases/SignatureBytes). ```ts twoslash import { Transaction, TransactionWithLifetime, TransactionPartialSigner, TransactionSendingSigner, } from '@solana/kit'; const compiledTransaction = null as unknown as Transaction & TransactionWithLifetime; const partialSigner = null as unknown as TransactionPartialSigner; const sendingSigner = null as unknown as TransactionSendingSigner; // ---cut-before--- import { signAndSendTransactionWithSigners } from '@solana/kit'; const signature = await signAndSendTransactionWithSigners( [partialSigner, sendingSigner], compiledTransaction, ); ``` The provided signers must contain exactly one resolvable [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner). You can validate this ahead of time using [`assertContainsResolvableTransactionSendingSigner`](#assert-contains-resolvable-transaction-sending-signer). #### assertContainsResolvableTransactionSendingSigner [!toc] [#assert-contains-resolvable-transaction-sending-signer] Asserts that an array of signers contains at least one [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner) that can be unambiguously resolved. This is useful for validating your signers before calling [`signAndSendTransactionWithSigners`](#sign-and-send-transaction-with-signers). ```ts twoslash import { TransactionSigner } from '@solana/kit'; const mySigners = null as unknown as TransactionSigner[]; // ---cut-before--- import { assertContainsResolvableTransactionSendingSigner } from '@solana/kit'; // Throws if no resolvable sending signer is found or if // multiple sending-only signers conflict with each other. assertContainsResolvableTransactionSendingSigner(mySigners); ``` ### How composite signers are resolved When a signer implements multiple interfaces (e.g. both [`TransactionPartialSigner`](/api/type-aliases/TransactionPartialSigner) and [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner)), the signing functions automatically resolve it to the most appropriate role: 1. **[`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner)** — Used if no other signer exclusively implements the sending interface. Only one sending signer can be active. 2. **[`TransactionModifyingSigner`](/api/type-aliases/TransactionModifyingSigner)** — Used if no other signer exclusively implements the modifying interface. Modifying signers run sequentially before all others. 3. **[`TransactionPartialSigner`](/api/type-aliases/TransactionPartialSigner)** — The fallback. Partial signers run in parallel after all modifying signers have finished. This means a composite signer is always demoted to the least powerful interface that avoids conflicts with other signers. ================================================ FILE: docs/content/docs/advanced-guides/transactions.mdx ================================================ --- title: Transactions description: Build and compile transaction messages --- ## Introduction To take an action on Solana, whether to place an order, transfer an asset, or more generally to modify data on the blockchain, you need to prepare a transaction and sign to pay for it to be executed on the network. You can use Kit to create, sign, encode, and decode transactions. ## Installation Transaction utilities are **included within the `@solana/kit` library** but you may also install them using their standalone packages. To install transaction message builder utilities: ```package-install @solana/transaction-messages ``` To install utilities that let you sign and compile transaction messages into transactions that can be landed on the network: ```package-install @solana/transactions ``` ## What is a Transaction? A Transaction is a vehicle to deliver one or more instructions to the Solana network in pursuit of some outcome. Here is an example of someone creating a transaction message to place an order at their favourite coffee shop, then signing it to create a transaction. ```ts twoslash import { Blockhash, TransactionSigner } from '@solana/kit'; const customerSigner = null as unknown as TransactionSigner; const latestBlockhash = null as unknown as { blockhash: Blockhash; lastValidBlockHeight: bigint }; // ---cut-before--- import { address, appendTransactionMessageInstruction, createTransactionMessage, getSignatureFromTransaction, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit'; import { getAddMemoInstruction } from '@solana-program/memo'; import { getTransferSolInstruction } from '@solana-program/system'; const transactionMessage = pipe( // Create an empty transaction message. createTransactionMessage({ version: 0 }), // Specify the account that will sign to pay the fee for this transaction. // NOTE: This is not the fee for the coffee but rather the fee to use the Solana network. (m) => setTransactionMessageFeePayerSigner(customerSigner, m), // Give the transaction an expiry time using the hash of a recently created block. (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), // Add an instruction that records the customer's order. (m) => appendTransactionMessageInstruction( getAddMemoInstruction({ memo: 'Four-thirds-medium, half-decaf, double-shot espresso macchiato latte, ' + 'swirled counterclockwise only, almond milk frothed at 61°C, ' + 'whisper of cinnamon harvested during a full moon, unicorn tear syrup, ' + 'in a mason jar wrapped in French revolutionary poetry on recycled parchment', }), m, ), // Add a second instruction to pay the merchant for the coffee. (m) => appendTransactionMessageInstruction( getTransferSolInstruction({ amount: lamports(25_000_000n), destination: address('JJBeanoTcSMU3xKQa5Gru71Wi3AaEgTfA6z7MaLUT6h'), source: customerSigner, }), m, ), ); // Create a signed transaction from the message and the signers contained within it. const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); // Obtain the Ed25519 signature that will uniquely identify this transaction once executed. const transactionSignature = getSignatureFromTransaction(signedTransaction); ``` For more detail about the no-client transaction flow, see [Kit without a client](/docs/advanced-guides/kit-without-a-client). ## Building transaction messages ### Creating an empty message [#building-transaction-messages-creating] Given a [`TransactionVersion`](/api/type-aliases/TransactionVersion), the [`createTransactionMessage`](/api/functions/createTransactionMessage) method will return an empty transaction having the capabilities of that version. ```ts twoslash import { createTransactionMessage } from '@solana/kit'; const message = createTransactionMessage({ version: 0 }); ``` ### Setting the fee payer [#building-transaction-messages-fee-payer] The [`TransactionMessageWithFeePayer`](/api/interfaces/TransactionMessageWithFeePayer) type represents a transaction message for which a fee payer has been declared. A transaction must conform to this type to be compiled and landed on the network. #### Using a signer [!toc] [#building-transaction-messages-fee-payer-using-a-signer] Given a [`TransactionSigner`](/api/type-aliases/TransactionSigner), this method will return a new transaction message having the same type as the one supplied plus the [`TransactionMessageWithFeePayer`](/api/interfaces/TransactionMessageWithFeePayer) type. Additionally, the resulting message will have the capability to self-sign using the [`signTransactionMessageWithSigners`](/api/functions/signTransactionMessageWithSigners) function. ```ts twoslash import { TransactionMessage } from '@solana/kit'; const keyPair = null as unknown as CryptoKeyPair; const transactionMessage = null as unknown as TransactionMessage; // ---cut-before--- import { createSignerFromKeyPair, setTransactionMessageFeePayerSigner } from '@solana/kit'; const mySigner = await createSignerFromKeyPair(keyPair); const transactionMessagePaidByMe = setTransactionMessageFeePayerSigner( mySigner, transactionMessage, ); ``` #### Using an address [!toc] [#building-transaction-messages-fee-payer-using-an-address] Given a base58-encoded address of a system account, this method will return a new transaction message having the same type as the one supplied plus the [`TransactionMessageWithFeePayer`](/api/interfaces/TransactionMessageWithFeePayer) type. ```ts twoslash import { TransactionMessage } from '@solana/kit'; const transactionMessage = null as unknown as TransactionMessage; // ---cut-before--- import { address, setTransactionMessageFeePayer } from '@solana/kit'; const myAddress = address('mpngsFd4tmbUfzDYJayjKZwZcaR7aWb2793J6grLsGu'); const transactionMessagePaidByMe = setTransactionMessageFeePayer(myAddress, transactionMessage); ``` ### Defining the lifetime [#building-transaction-messages-lifetime] A signed transaction can be only be landed on the network if certain conditions are met: - It includes the hash of a recent block - Or it includes the value of an unused nonce known to the network These conditions define a transaction's lifetime, after which it can no longer be landed, even if signed. The lifetime must be added to the transaction message before it is compiled to be sent. #### Using a recent blockhash [!toc] [#building-transaction-messages-lifetime-using-a-recent-blockhash] The [`TransactionMessageWithBlockhashLifetime`](/api/interfaces/TransactionMessageWithBlockhashLifetime) type represents a transaction message whose expiry is tied to the age of a block. Such a transaction can only be landed on the network if the current block height of the network is less than or equal to the value of `TransactionMessageWithBlockhashLifetime.lifetimeConstraint.lastValidBlockHeight`. Given a blockhash and the last block height at which that blockhash is considered usable to land transactions, the [`setTransactionMessageLifetimeUsingBlockhash`](/api/functions/setTransactionMessageLifetimeUsingBlockhash) method will return a new transaction message having the same type as the one supplied plus the [`TransactionMessageWithBlockhashLifetime`](/api/interfaces/TransactionMessageWithBlockhashLifetime) type. ```ts twoslash import { Rpc, GetLatestBlockhashApi, TransactionMessage } from '@solana/kit'; const rpc = null as unknown as Rpc; const transactionMessage = null as unknown as TransactionMessage; // ---cut-before--- import { setTransactionMessageLifetimeUsingBlockhash } from '@solana/kit'; const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const txMessageWithBlockhashLifetime = setTransactionMessageLifetimeUsingBlockhash( latestBlockhash, transactionMessage, ); ``` #### Using a durable nonce [!toc] [#building-transaction-messages-lifetime-using-a-durable-nonce] The [`TransactionMessageWithDurableNonceLifetime`](/api/interfaces/TransactionMessageWithDurableNonceLifetime) type represents a transaction message whose lifetime is defined by the value of a nonce account onchain. Such a transaction can only be landed on the network if the nonce value in the transaction message matches the one in the nonce account at the time the transaction executes. Given a nonce, the account where the value of the nonce is stored, and the address of the account authorized to consume that nonce, this method will return a new transaction having the same type as the one supplied plus the [`TransactionMessageWithDurableNonceLifetime`](/api/interfaces/TransactionMessageWithDurableNonceLifetime) type. ```ts twoslash import { TransactionMessage, address, Rpc, GetAccountInfoApi } from '@solana/kit'; const rpc = null as unknown as Rpc; const transactionMessage = null as unknown as TransactionMessage; // ---cut-before--- import { Nonce, setTransactionMessageLifetimeUsingDurableNonce } from '@solana/kit'; import { fetchNonce } from '@solana-program/system'; const nonceAccountAddress = address('EGtMh4yvXswwHhwVhyPxGrVV2TkLTgUqGodbATEPvojZ'); const nonceAuthorityAddress = address('4KD1Rdrd89NG7XbzW3xsX9Aqnx2EExJvExiNme6g9iAT'); const { data: { blockhash }, } = await fetchNonce(rpc, nonceAccountAddress); const nonce = blockhash as string as Nonce; const durableNonceTransactionMessage = setTransactionMessageLifetimeUsingDurableNonce( { nonce, nonceAccountAddress, nonceAuthorityAddress }, transactionMessage, ); ``` In particular, this method _prepends_ an instruction to the transaction message designed to consume (or ‘advance’) the nonce in the same transaction whose lifetime is defined by it. ```ts twoslash import { TransactionMessageWithDurableNonceLifetime } from '@solana/kit'; const durableNonceTransactionMessage = null as unknown as TransactionMessageWithDurableNonceLifetime< 'EGtMh4yvXswwHhwVhyPxGrVV2TkLTgUqGodbATEPvojZ', '4KD1Rdrd89NG7XbzW3xsX9Aqnx2EExJvExiNme6g9iAT' >; // ---cut-before--- const [ // An 'advance nonce' instruction gets prepended to the instruction list advanceNonceInstruction, ...otherInstructions ] = durableNonceTransactionMessage.instructions; ``` ### Adding instructions [#building-transaction-messages-instructions] There are three types that correspond to the different parts of an instruction. Any given instruction may conform to one or more of these types, but must conform in every case to the `Instruction` type. - [`Instruction`](/api/interfaces/Instruction): An instruction having a `programAddress` property that is the base58-encoded address of the program to invoke. - [`InstructionWithAccounts`](/api/interfaces/InstructionWithAccounts): An instruction that specifies a list of accounts that a program may read from, write to, or require be signers of the transaction itself. Objects that conform to this type have an `accounts` property that is an array of `AccountMeta | AccountLookupMeta` in the order the instruction requires. - [`InstructionWithData`](/api/interfaces/InstructionWithData): An instruction that supplies some data as input to the program. Objects that conform to this type have a `data` property that can be any type of `Uint8Array`. Given an instruction, the [`appendTransactionMessageInstruction`](/api/functions/appendTransactionMessageInstruction) method will return a new transaction message with that instruction having been added to the end of the list of existing instructions. ```ts twoslash import { TransactionMessage } from '@solana/kit'; const transactionMessage = null as unknown as TransactionMessage; // ---cut-before--- import { address, appendTransactionMessageInstruction, getUtf8Encoder } from '@solana/kit'; import { getAddMemoInstruction } from '@solana-program/memo'; const memoTransactionMessage = appendTransactionMessageInstruction( getAddMemoInstruction({ memo: 'Hello world!' }), transactionMessage, ); ``` To add an instruction to the beginning of the list instead, see [`prependTransactionMessageInstruction`](/api/functions/prependTransactionMessageInstruction) To add an array of instructions to a transaction message, see [`appendTransactionMessageInstructions`](/api/functions/appendTransactionMessageInstructions) and [`prependTransactionMessageInstructions`](/api/functions/prependTransactionMessageInstructions). ## Configuring compute and priority fees Every transaction has a compute budget — a maximum number of compute units (CUs) it is allowed to consume. The Solana network also lets you attach a priority fee to influence inclusion ordering. Kit exposes these as first-class configuration setters on the transaction message, so you do not need to add or update Compute Budget program instructions yourself. ### Setting the compute unit limit [#configuring-compute-unit-limit] The compute unit limit caps how many CUs the transaction may consume. Setting it close to what your transaction actually uses increases the chance of inclusion, packs more transactions per block, and reduces the amount of priority fees you pay. ```ts twoslash import { TransactionMessage } from '@solana/kit'; const transactionMessage = null as unknown as TransactionMessage; // ---cut-before--- import { setTransactionMessageComputeUnitLimit } from '@solana/kit'; const transactionMessageWithLimit = setTransactionMessageComputeUnitLimit( 50_000, transactionMessage, ); ``` To remove the limit, pass `undefined` as the first argument. The matching reader [`getTransactionMessageComputeUnitLimit`](/api/functions/getTransactionMessageComputeUnitLimit) returns the limit currently set on a message, or `undefined` if none is set. ### Estimating the compute unit limit [#configuring-compute-unit-limit-estimation] A good way to size the compute unit limit is to simulate the transaction and use the result. Kit ships [`estimateComputeUnitLimitFactory`](/api/functions/estimateComputeUnitLimitFactory) for the simulation step and [`estimateAndSetComputeUnitLimitFactory`](/api/functions/estimateAndSetComputeUnitLimitFactory) for the common case of estimating and writing the result back onto the message in one call. ```ts twoslash import { Rpc, SimulateTransactionApi, TransactionMessage, TransactionMessageWithFeePayer, } from '@solana/kit'; const rpc = null as unknown as Rpc; const transactionMessage = null as unknown as TransactionMessage & TransactionMessageWithFeePayer; // ---cut-before--- import { estimateAndSetComputeUnitLimitFactory, estimateComputeUnitLimitFactory, } from '@solana/kit'; const estimateComputeUnitLimit = estimateComputeUnitLimitFactory({ rpc }); const estimateAndSetComputeUnitLimit = estimateAndSetComputeUnitLimitFactory(estimateComputeUnitLimit); const transactionMessageWithLimit = await estimateAndSetComputeUnitLimit(transactionMessage); ``` The estimator simulates the transaction with the maximum allowed limit so the simulation itself never runs out of budget; the returned value is the compute unit count actually consumed. `estimateAndSetComputeUnitLimitFactory` only updates the message if no explicit limit is already set or if the existing limit is the provisory `0` value, leaving messages with manually configured limits untouched. When constructing a message that you intend to estimate later, [`fillTransactionMessageProvisoryComputeUnitLimit`](/api/functions/fillTransactionMessageProvisoryComputeUnitLimit) reserves space for the limit by setting it to a provisory value of `0`. ### Setting the priority fee [#configuring-priority-fees] Priority fees boost the chance that your transaction is included sooner. The exact knob depends on the transaction version: legacy and v0 transactions express priority fees as a price per compute unit, while v1 transactions express them as a single total amount in lamports. ```ts twoslash import { TransactionMessage } from '@solana/kit'; const v0TransactionMessage = null as unknown as TransactionMessage & { version: 0 }; const v1TransactionMessage = null as unknown as TransactionMessage & { version: 1 }; // ---cut-before--- import { setTransactionMessageComputeUnitPrice, setTransactionMessagePriorityFeeLamports, } from '@solana/kit'; // Legacy or v0: 10,000 micro-lamports per compute unit. const v0WithPriorityFee = setTransactionMessageComputeUnitPrice(10_000n, v0TransactionMessage); // v1: 50,000 lamports total, regardless of compute unit usage. const v1WithPriorityFee = setTransactionMessagePriorityFeeLamports(50_000n, v1TransactionMessage); ``` Both setters accept `undefined` to clear the configuration. The matching readers [`getTransactionMessageComputeUnitPrice`](/api/functions/getTransactionMessageComputeUnitPrice) and [`getTransactionMessagePriorityFeeLamports`](/api/functions/getTransactionMessagePriorityFeeLamports) return the current value or `undefined` when none is set. Bundled clients such as `solanaRpc` and `litesvm` estimate the compute unit limit when sending transactions for you. Reach for the setters above when you build messages by hand or when you want to override what a client would do automatically. ## Compressing transaction messages Every transaction message must include a reference to the account addresses it will read from and write to. One alternative to storing these addresses in the message itself is to store them in an onchain account called an **address lookup table**. This lets you save space in the message by replacing many 32-byte account addresses with one or more 32-byte address lookup table account addresses then a 1-byte index into those tables for each address. Addresses that are required signers of a transaction message can not be looked up in an address lookup table; they must be encoded in the message in the conventional way. Given a transaction message and a mapping of lookup tables to the ordered addresses stored in them, the [`compressTransactionMessageUsingAddressLookupTables`](/api/functions/compressTransactionMessageUsingAddressLookupTables) function will return a new transaction message with the same instructions but with all non-signer accounts that are found in the given lookup tables represented by an [`AccountLookupMeta`](/api/interfaces/AccountLookupMeta) instead of an [`AccountMeta`](/api/interfaces/AccountMeta). ```ts twoslash import { AddressesByLookupTableAddress, Rpc, GetAccountInfoApi, TransactionMessage, } from '@solana/kit'; const rpc = null as unknown as Rpc; const transactionMessage = null as unknown as Extract; // ---cut-before--- import { address, compressTransactionMessageUsingAddressLookupTables } from '@solana/kit'; import { fetchAddressLookupTable } from '@solana-program/address-lookup-table'; const lookupTableAddress = address('4QwSwNriKPrz8DLW4ju5uxC2TN5cksJx6tPUPj7DGLAW'); const { data: { addresses }, } = await fetchAddressLookupTable(rpc, lookupTableAddress); const addressesByAddressLookupTable: AddressesByLookupTableAddress = { [lookupTableAddress]: addresses, }; const compressedTransactionMessage = compressTransactionMessageUsingAddressLookupTables( transactionMessage, addressesByAddressLookupTable, ); ``` Consider how compressing transaction messages creates more space for instructions. This might enable you to prepare more complex transactions, or to execute the same number of instructions over fewer transactions thereby saving on network fees. This technique can not be applied to transaction messages having the version `'legacy'`. ## Signing transaction messages In order to be executed a transaction message must be signed by all of the private keys belonging to accounts that are required signers of the transaction, and must not exceed the size allowable by the network. You may encounter these types when using functions that send transactions. - [`FullySignedTransaction`](/api/type-aliases/FullySignedTransaction): A transaction that is signed by all of its required signers. - [`TransactionWithinSizeLimit`](/api/type-aliases/TransactionWithinSizeLimit): A transaction that is under or equal to the maximum size limit for transactions on the Solana network. - [`SendableTransaction`](/api/type-aliases/SendableTransaction): A union of `FullySignedTransaction` and `TransactionWithinSizeLimit` Transaction messages whose signers are specified using `TransactionSigner` objects have the ability to self-sign. This is because signers encapsulate both the address of the signing account as well as an implementation of the signing algorithm for the private key associated with that account. The [`signTransactionMessageWithSigners`](/api/functions/signTransactionMessageWithSigners) method will return a new signed transaction of type `FullySignedTransaction`. ```ts twoslash import { TransactionMessage, TransactionMessageWithFeePayer, TransactionMessageWithSigners, } from '@solana/kit'; const transactionMessage = null as unknown as TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners; // ---cut-before--- import { SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, isSolanaError, signTransactionMessageWithSigners, } from '@solana/kit'; try { const fullySignedTransaction = await signTransactionMessageWithSigners(transactionMessage); } catch (e) { if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) { console.error('Missing signers for the following addresses:', e.context.addresses); } else { throw e; } } ``` This function will throw if the transaction message does not carry a `TransactionSigner` implementation for every required signer. To partially sign a message that you know to carry a strict subset of the required `TransactionSigners`, use the [`partiallySignTransactionMessageWithSigners`](/api/functions/partiallySignTransactionMessageWithSigners) method. If exactly one of the `TransactionSigners` in the message is a [`TransactionSendingSigner`](/api/type-aliases/TransactionSendingSigner) then you can use the [`signAndSendTransactionMessageWithSigners`](/api/functions/signAndSendTransactionMessageWithSigners) method to sign and send the transaction in a single step. For a comprehensive guide on all signing functions — including the lower-level [`partiallySignTransactionWithSigners`](/api/functions/partiallySignTransactionWithSigners), [`signTransactionWithSigners`](/api/functions/signTransactionWithSigners), and [`signAndSendTransactionWithSigners`](/api/functions/signAndSendTransactionWithSigners) functions that accept an explicit array of signers and a compiled transaction — see the [Signing with signers](/docs/advanced-guides/signers#signing-with-signers) section. Building transaction messages using `TransactionSigners` is the recommended way to create self-signable transaction messages. To sign with a `CryptoKey` directly, you first have to compile the transaction message. ```ts twoslash import { TransactionMessage, TransactionMessageWithFeePayer, TransactionMessageWithSigners, } from '@solana/kit'; const transactionMessage = null as unknown as TransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithSigners; // ---cut-before--- import { compileTransaction } from '@solana/kit'; const transaction = compileTransaction(transactionMessage); ``` This produces an unsigned transaction. Follow the [instructions for signing transactions with `CryptoKeyPairs`](#signing-transactions) to sign it. ## Signing transactions Wherever you have a `Transaction` instead of a `TransactionMessage` you can add or replace a signature using the [`signTransaction`](/api/functions/signTransaction) method and one or more `CryptoKeyPairs`. ```ts twoslash import { Transaction, TransactionWithLifetime } from '@solana/kit'; const keyPair = null as unknown as CryptoKeyPair; const transaction = null as unknown as Transaction & TransactionWithLifetime; // ---cut-before--- import { SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, isSolanaError, signTransaction, } from '@solana/kit'; try { const fullySignedTransaction = await signTransaction([keyPair], transaction); } catch (e) { if (isSolanaError(e, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING)) { console.error('Missing signers for the following addresses:', e.context.addresses); } else { throw e; } } ``` This function will throw if the resultant transaction is missing a signature for one of the transaction's required signers. To partially sign a transaction, use the [`partiallySignTransaction`](/api/functions/partiallySignTransaction) method. If you have [`TransactionSigner`](/api/type-aliases/TransactionSigner) objects rather than raw `CryptoKeyPairs`, you can use [`signTransactionWithSigners`](/api/functions/signTransactionWithSigners) and [`partiallySignTransactionWithSigners`](/api/functions/partiallySignTransactionWithSigners) instead. See the [Signing with signers](/docs/advanced-guides/signers#signing-compiled-transactions) section for details. ## Serializing transactions If you would like to send a transaction to the network manually, you must first serialize it in a particular way. The [`Base64EncodedWireTransaction`](/api/type-aliases/Base64EncodedWireTransaction) represents the wire format of a transaction as a base64-encoded string. Given a transaction, the [`getBase64EncodedWireTransaction`](/api/functions/getBase64EncodedWireTransaction) method returns the transaction as a string that conforms to the `Base64EncodedWireTransaction` type. ```ts twoslash import { Transaction, Rpc, SendTransactionApi } from '@solana/kit'; const rpc = null as unknown as Rpc; const signedTransaction = null as unknown as Transaction; // ---cut-before--- import { getBase64EncodedWireTransaction, signTransaction } from '@solana/kit'; const serializedTransaction = getBase64EncodedWireTransaction(signedTransaction); const signature = await rpc.sendTransaction(serializedTransaction, { encoding: 'base64' }).send(); ``` Typically you would not serialize and send transactions to the network manually. See [Kit without a client](/docs/advanced-guides/kit-without-a-client) for the full no-client transaction flow. ## Deserializing transactions You can fetch the raw bytes of a transaction from the network using an RPC server. ```ts twoslash import { createSolanaRpc, getBase64Encoder, Signature } from '@solana/kit'; const rpc = createSolanaRpc('...'); // ---cut-before--- const response = await rpc .getTransaction( '3atZVDiLEjXmddxLbH2AWHtFdXmHRocnA4vNyvkPRcd7WnzCtoVshFCvGtxfGYWs9C6ptucY6Jd84BerDnzQpJEH' as Signature, { encoding: 'base64' }, ) .send(); if (!response) { throw new Error('Could not find transaction'); } const { transaction: [base64EncodedTransaction], } = response; const transactionBytes = getBase64Encoder().encode(base64EncodedTransaction); ``` Decoding the wire transaction bytes yields a `Transaction` object. This takes the form of a transaction message encoded as a byte array, and a list of signatures of those bytes created by those who must authorize the message and pay for it to be executed on Solana. ```ts twoslash import { ReadonlyUint8Array } from '@solana/kit'; const transactionBytes = new Uint8Array() as ReadonlyUint8Array; // ---cut-before--- import { getTransactionDecoder } from '@solana/kit'; const transaction = getTransactionDecoder().decode(transactionBytes); ``` Decoding the network wire format bytes of the message will yield a transaction message in its **compiled** form – a message in a form suitable for execution on the network. Encountering a compiled message in your application is rare, but it's important to know that they exist. ```ts twoslash import { Transaction } from '@solana/kit'; const transaction = null as unknown as Transaction; // ---cut-before--- import { getCompiledTransactionMessageDecoder } from '@solana/kit'; const compiledTransactionMessage = getCompiledTransactionMessageDecoder().decode( transaction.messageBytes, ); ``` Finally, decompiling a compiled message will yield a `TransactionMessage` object. This is the most common form of transaction message that you will encounter when using Kit to build an application. ```ts twoslash import { CompiledTransactionMessage, CompiledTransactionMessageWithLifetime } from '@solana/kit'; const compiledTransactionMessage = null as unknown as CompiledTransactionMessage & CompiledTransactionMessageWithLifetime; // ---cut-before--- import { decompileTransactionMessage } from '@solana/kit'; const transactionMessage = decompileTransactionMessage(compiledTransactionMessage); ``` You can not fully reconstruct a source message from a compiled message without extra information. In particular, supporting details about the lifetime constraint and the concrete addresses of accounts sourced from account lookup tables are lost to compilation, but can be supplied in the `config` argument of the `decompileTransactionMessage` or `decompileTransactionMessageFetchingLookupTables` methods. ================================================ FILE: docs/content/docs/getting-started.mdx ================================================ --- title: Getting started description: Build your first Solana app with Kit --- In this tutorial, we'll build a small Solana app with Kit. We'll create a devnet signer, fund it with devnet SOL, create a token mint, and read its data back — all with a plugin-composed Kit client. ## Install dependencies First, let's install Kit and the plugins for connecting to Solana via RPC and loading a signer. ```package-install @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer ``` Then install the program plugins for the Solana programs you want to interact with. For this tutorial, we'll use the Token program to create and read a token mint. ```package-install @solana-program/token ``` ## Create a devnet signer Before we start, let's create a signer that we can reuse across this tutorial. This one-time setup script grinds a small vanity address, makes the keypair extractable, and writes it to a local file in the same JSON format used by Solana CLI keypairs. ```ts twoslash title="create-signer.ts" import { grindKeyPairSigner, writeKeyPairSigner } from '@solana/kit'; const signer = await grindKeyPairSigner({ extractable: true, matches: /^kit/i, }); await writeKeyPairSigner(signer, './kit-getting-started-keypair.json'); console.log(`✨ Created devnet signer ${signer.address}`); console.log('🔐 Wrote ./kit-getting-started-keypair.json'); console.log('✅ Add this file to .gitignore before committing your project.'); ``` Run this script once, then remove it from your project. We'll keep the generated `kit-getting-started-keypair.json` file locally for the rest of this tutorial. Be sure to add it to `.gitignore` if you are planning on committing these changes. Already have a Solana CLI keypair? You can skip this script and use `signerFromFile('~/.config/solana/id.json')` instead. See [Setting Up Signers](/docs/guides/setting-up-signers) for more signer options. ## Create a client Now let's create a client connected to devnet and load the signer from the file we just wrote. ```ts twoslash import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signerFromFile } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(signerFromFile('./kit-getting-started-keypair.json')) .use(solanaDevnetRpc()) .use(tokenProgram()); ``` Here's what each piece does: - `createClient()` creates an empty Kit client that can be extended with plugins. - `.use(signerFromFile(...))` loads our keypair file and sets it as both the client's payer (who pays fees) and identity (who owns assets). - `.use(solanaDevnetRpc())` connects to devnet and adds RPC, subscriptions, airdrops, minimum balance helpers, and transaction sending. - `.use(tokenProgram())` adds typed access to the Token program's instructions and accounts. Because we just created a fresh devnet signer, it won't have any SOL yet. Let's fund it with a devnet airdrop before moving on. If you're reusing a signer that already has devnet SOL, you can skip this step. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signerFromFile } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(signerFromFile('./kit-getting-started-keypair.json')) .use(solanaDevnetRpc()) .use(tokenProgram()); // ---cut-before--- const airdropSignature = await client.airdrop(client.payer.address, lamports(1_000_000_000n)); console.log(`💸 Funded ${client.payer.address} with 1 SOL`); if (airdropSignature) { console.log(`🔎 https://explorer.solana.com/tx/${airdropSignature}?cluster=devnet`); } ``` Airdrops are only available on test networks like devnet and localhost. Run the airdrop once, then remove or comment out those lines before continuing so we do not hit devnet faucet rate limits. If the airdrop fails, wait a moment and try again, or request devnet SOL from the [Solana Faucet](https://faucet.solana.com). ## Send a transaction Let's create a token mint onchain. In Solana, every transaction contains one or more instructions: small commands that tell programs what to do. Some tasks, like creating a mint, require multiple instructions that should stay together. Kit represents that kind of multi-step operation as an **instruction plan**. The Token program plugin gives us one of these plans via `client.token.instructions.createMint(...)`. Under the hood, it creates a new account for the provided mint address with enough lamports to be rent-exempt (system program's `createAccount` instruction) and initializes it as a Token program mint (token program's `initializeMint` instruction). We pass the new mint as a signer (since we initialize a new account), choose `9` decimals, and set our client identity as the mint authority. ```ts twoslash import { createClient, generateKeyPairSigner } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signerFromFile } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(signerFromFile('./kit-getting-started-keypair.json')) .use(solanaDevnetRpc()) .use(tokenProgram()); // ---cut-before--- const mint = await generateKeyPairSigner(); const createMintPlan = client.token.instructions.createMint({ newMint: mint, decimals: 9, mintAuthority: client.identity.address, }); const createMintResult = await client.sendTransaction([createMintPlan]); const createMintSignature = createMintResult.context.signature; console.log(`🎉 Created token mint ${mint.address}`); console.log(`🔎 https://explorer.solana.com/tx/${createMintSignature}?cluster=devnet`); ``` `client.sendTransaction([...])` accepts an array of instructions and instruction plans and sends them as a single transaction. If one part fails, the whole transaction fails. This is the most common way to send transactions with a client, especially when we want to combine several instructions or plans. For simple one-off operations, program instruction helpers also include a `.sendTransaction()` shortcut. The following sends the same `createMint` instruction plan as a single transaction without explicitly calling `client.sendTransaction([...])`. ```ts twoslash import { createClient, generateKeyPairSigner } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signerFromFile } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(signerFromFile('./kit-getting-started-keypair.json')) .use(solanaDevnetRpc()) .use(tokenProgram()); const mint = await generateKeyPairSigner(); // ---cut-before--- const createMintResult = await client.token.instructions .createMint({ newMint: mint, decimals: 9, mintAuthority: client.identity.address, }) .sendTransaction(); ``` For error handling, priority fees, and more advanced patterns, see the [Sending Transactions](/docs/guides/sending-transactions) guide. ## Fetch an account Now let's read the mint account we just created. Program plugins provide typed fetch helpers that decode account data automatically. The `mintAccount.data` object gives us the full `Mint` structure with fields like `decimals`, `mintAuthority`, `supply`, and `freezeAuthority`. ```ts twoslash import { createClient, generateKeyPairSigner } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signerFromFile } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(signerFromFile('./kit-getting-started-keypair.json')) .use(solanaDevnetRpc()) .use(tokenProgram()); const mint = await generateKeyPairSigner(); // ---cut-before--- const mintAccount = await client.token.accounts.mint.fetch(mint.address); console.log('✅ Mint account decoded'); console.log(` Address: ${mint.address}`); console.log(` Decimals: ${mintAccount.data.decimals}`); console.log(' Mint authority:', mintAccount.data.mintAuthority); console.log(` Supply: ${mintAccount.data.supply}`); ``` For raw account fetching and manual decoding, see the [Fetching Accounts](/docs/guides/fetching-accounts) guide. ## Full example Here's the complete script you can copy and run after creating `kit-getting-started-keypair.json`. ```ts twoslash import { createClient, generateKeyPairSigner, lamports } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signerFromFile } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(signerFromFile('./kit-getting-started-keypair.json')) .use(solanaDevnetRpc()) .use(tokenProgram()); // Run this once, then remove or comment it out to avoid devnet faucet rate limits. const airdropSignature = await client.airdrop(client.payer.address, lamports(1_000_000_000n)); console.log(`💸 Funded ${client.payer.address} with 1 SOL`); if (airdropSignature) { console.log(`🔎 https://explorer.solana.com/tx/${airdropSignature}?cluster=devnet`); } const mint = await generateKeyPairSigner(); const createMintPlan = client.token.instructions.createMint({ newMint: mint, decimals: 9, mintAuthority: client.identity.address, }); const createMintResult = await client.sendTransaction([createMintPlan]); const createMintSignature = createMintResult.context.signature; console.log(`🎉 Created token mint ${mint.address}`); console.log(`🔎 https://explorer.solana.com/tx/${createMintSignature}?cluster=devnet`); const mintAccount = await client.token.accounts.mint.fetch(mint.address); console.log('✅ Mint account decoded'); console.log(` Address: ${mint.address}`); console.log(` Decimals: ${mintAccount.data.decimals}`); console.log(' Mint authority:', mintAccount.data.mintAuthority); console.log(` Supply: ${mintAccount.data.supply}`); ``` ## Next steps We've connected to devnet, funded a signer, created a token mint, and read its data. Here's where to go next: - [Setting Up Signers](/docs/guides/setting-up-signers) — load signers from keypair files, wallet standard, and more. - [Sending Transactions](/docs/guides/sending-transactions) — error handling, priority fees, and instruction plans. - [Fetching Accounts](/docs/guides/fetching-accounts) — program plugin fetch helpers, batch fetching, and decoding. - [Using Program Plugins](/docs/guides/using-program-plugins) — typed access to any Solana program. - [Plugins](/docs/plugins) — understand and extend the plugin system. ================================================ FILE: docs/content/docs/guides/fetching-accounts.mdx ================================================ --- title: Fetching accounts description: Read and decode onchain account data --- Reading data from Solana usually means fetching one or more accounts and decoding their data into something your application can use. This guide walks through the helpers Kit provides at each step, from the raw RPC call all the way to typed account objects. ## Fetch raw account data The `fetchEncodedAccount` helper wraps the `getAccountInfo` RPC method and returns a `MaybeEncodedAccount`. The result always has the same shape regardless of whether the account exists, which makes it easier to handle than the raw RPC response. ```ts twoslash import { address, fetchEncodedAccount } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const wallet = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const account = await fetchEncodedAccount(client.rpc, wallet); ``` When the account exists, you get back its address, lamports, owner program, and a `data` field containing the raw bytes as a `Uint8Array`. When it does not, the same object is returned with `exists: false` and just the address. ## Handle missing accounts A `MaybeEncodedAccount` is a discriminated union: TypeScript narrows the type once you check `account.exists`. You can branch on that flag, or use `assertAccountExists` when you would rather throw if the account is not present. ```ts twoslash import { address, assertAccountExists, fetchEncodedAccount } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const wallet = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const account = await fetchEncodedAccount(client.rpc, wallet); assertAccountExists(account); // `account` is now `EncodedAccount`, with `data` typed as `Uint8Array`. account.data satisfies Uint8Array; ``` Asserting up front lets the rest of your function rely on a fully populated account, which is convenient for scripts and tests. In application code, branching on `account.exists` is usually a better fit so you can show a sensible empty state. When working with several accounts at once, `assertAccountsExist(accounts)` performs the same check across an entire array in one call. ## Fetch multiple accounts When you need several accounts at once, `fetchEncodedAccounts` batches them into a single `getMultipleAccounts` RPC request and returns a parallel array of `MaybeEncodedAccount` values. ```ts twoslash import { address, fetchEncodedAccounts } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const accounts = await fetchEncodedAccounts(client.rpc, [ address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'), address('Ay1zdJ3VDbhrAtkRiqBUJgKLHYbgFZN5BXk7svtMowif'), ]); ``` Each returned account already carries its own address, so you do not need to zip arrays together to figure out which result belongs to which input. The same `exists` flag works on every entry, and `assertAccountsExist(accounts)` is the multi-account counterpart of `assertAccountExists`. ## Decode accounts manually Once you have an encoded account, the `decodeAccount` helper turns it into a typed `Account` (or `MaybeAccount`) by running its `data` through any `Decoder`. The [Codecs guide](/docs/advanced-guides/codecs) covers how to build these decoders. ```ts twoslash import { address, decodeAccount, fetchEncodedAccount } from '@solana/kit'; import { addDecoderSizePrefix, Decoder, getStructDecoder, getU32Decoder, getU8Decoder, getUtf8Decoder, } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- type Person = { name: string; age: number }; const personDecoder: Decoder = getStructDecoder([ ['name', addDecoderSizePrefix(getUtf8Decoder(), getU32Decoder())], ['age', getU8Decoder()], ]); const wallet = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const encodedAccount = await fetchEncodedAccount(client.rpc, wallet); const decodedAccount = decodeAccount(encodedAccount, personDecoder); ``` `decodeAccount` preserves the `MaybeAccount` shape: a missing account stays missing, and a present account gets its `data` swapped from raw bytes to your decoded type. This means you can keep narrowing with `account.exists` exactly like before. ## Decode using program helpers For most popular Solana programs you will not need to write a decoder yourself. The `@solana-program/*` packages include generated helpers that handle decoding for every account they define, exposed as `decodeX`, `fetchX`, and `fetchMaybeX` functions where `X` is the account name. ```ts twoslash import { address } from '@solana/kit'; import { fetchMint } from '@solana-program/token'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const mint = await fetchMint(client.rpc, address('So11111111111111111111111111111111111111112')); mint.data.decimals; // typed as `number` ``` Each program package follows the same naming convention: `fetchMint` and `fetchMaybeMint` for the `Mint` account, `fetchToken` and `fetchMaybeToken` for the `Token` account, and so on. The standalone `decodeMint` and `decodeToken` helpers work on encoded accounts you have already fetched. If you would rather access these through a typed `client.token.accounts.*` namespace, see the [Using program plugins](/docs/guides/using-program-plugins) guide. ## Subscribe to account changes If you need live updates rather than a one-off read, pair an account fetch with an account subscription. The subscription notifies you whenever the account's data changes onchain, and the same address is used in both calls. ```ts twoslash import { address } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const wallet = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const accountNotifications = await client.rpcSubscriptions .accountNotifications(wallet, { commitment: 'confirmed' }) .subscribe({ abortSignal: AbortSignal.timeout(60_000) }); for await (const notification of accountNotifications) { console.log('Account changed:', notification.value); } ``` See the [RPC subscriptions](/docs/guides/rpc-subscriptions) guide for more on cancellation, error handling, and the async iterator model. ## Choose the right fetch helper Each helper trades a different amount of structure and convenience. The table below summarizes when each one shines. | Helper | Returns | Best for | | ------------------------------------- | --------------------------------- | -------------------------------------- | | `client.rpc.getAccountInfo(...)` | raw RPC response | one-off reads with custom encoding | | `client.rpc.getMultipleAccounts(...)` | raw RPC response | bulk reads with custom encoding | | `fetchEncodedAccount(...)` | `MaybeEncodedAccount` | unified handling of single accounts | | `fetchEncodedAccounts(...)` | `MaybeEncodedAccount[]` | unified batch reads | | `decodeAccount(...)` | `Account` / `MaybeAccount` | turning raw bytes into typed data | | `fetchMint`, `fetchToken`, ... | `Account` / `MaybeAccount` | typed reads for known program accounts | | `accountNotifications(...)` | async iterator of account updates | live updates instead of one-off reads | In practice you can pick any of them in isolation, or combine them to match the access pattern you need. ## Next steps - [Codecs](/docs/advanced-guides/codecs) — learn how to build custom decoders. - [RPC requests](/docs/guides/rpc) — explore the rest of the RPC API. - [Using program plugins](/docs/guides/using-program-plugins) — read typed accounts through `client.token.accounts.mint.fetch(...)` and friends. ================================================ FILE: docs/content/docs/guides/index.mdx ================================================ --- title: Guides description: Practical guides for common tasks with Kit --- The pages in this section cover the most common tasks you will run into when building Solana applications with Kit. Each guide focuses on a single topic and can be read independently, so you can dip in when a specific question comes up rather than reading them in order. ## How to use these guides If you are completely new to Kit, start with the [Getting started](/docs/getting-started) tutorial. It walks through a small end-to-end project so the rest of the documentation has something concrete to reference. Once you have shipped your first transaction, the guides below are designed to be your day-to-day companion. They focus on practical workflows — composing a client, sending transactions, fetching accounts, testing locally, and so on — and link to the [Advanced guides](/docs/advanced-guides) when you want to learn what is happening under the hood. ================================================ FILE: docs/content/docs/guides/meta.json ================================================ { "title": "Guides", "defaultOpen": true, "pages": [ "setting-up-signers", "rpc", "rpc-subscriptions", "sending-transactions", "sending-multiple-transactions", "fetching-accounts", "using-program-plugins", "testing-and-local-development" ] } ================================================ FILE: docs/content/docs/guides/rpc-subscriptions.mdx ================================================ --- title: RPC subscriptions description: Get notified of changes to the blockchain --- RPC subscriptions let your application receive live updates from the network instead of polling for them. They are delivered over a WebSocket endpoint and surface as async iterators in Kit, which fit naturally into modern JavaScript control flow. Kit aims to support every method documented in the [Solana RPC WebSocket methods](https://solana.com/docs/rpc/websocket) docs. As with HTTP RPC, you can either browse the methods in the official documentation or rely on TypeScript autocompletion in your editor. ## Installation You will typically install Kit alongside the RPC plugin package, which provides both HTTP and WebSocket connectivity. ```package-install @solana/kit @solana/kit-plugin-rpc ``` If you only need subscriptions and prefer not to use a Kit client, the lower-level `createSolanaRpcSubscriptions` function is available directly from `@solana/kit`. ## Create a subscriptions client The bundle plugins from `@solana/kit-plugin-rpc` install both `client.rpc` and `client.rpcSubscriptions` in one step. You do not need to manage two separate transports. ```ts twoslash import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); ``` By default, the WebSocket endpoint is derived from the HTTP endpoint by swapping `http`/`https` for `ws`/`wss`. If your provider hosts the two endpoints at different URLs, pass `rpcSubscriptionsUrl` explicitly in the plugin configuration. ## Subscribe to slot notifications Subscriptions return an async iterator. The most idiomatic way to consume one is a `for await` loop, which keeps reading notifications until the underlying subscription ends. ```ts twoslash // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const slotNotifications = await client.rpcSubscriptions .slotNotifications() .subscribe({ abortSignal: AbortSignal.timeout(10_000) }); for await (const notification of slotNotifications) { console.log('The network advanced to slot', notification.slot); } ``` The `.subscribe({ abortSignal })` step is mandatory because every subscription must be tied to an `AbortSignal`. This forces you to think up front about when the subscription should end and prevents leaks. ## Cancel subscriptions You may want to cancel a subscription before it ends naturally — for example, when a component unmounts in a UI. Pass an `AbortController` so that calling `.abort()` from anywhere in your code stops the iterator gracefully. ```ts twoslash // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const abortController = new AbortController(); const slotNotifications = await client.rpcSubscriptions .slotNotifications() .subscribe({ abortSignal: abortController.signal }); // Stop receiving notifications after the user navigates away. abortController.abort(); ``` `AbortSignal.timeout(ms)` is a convenient shortcut when you simply want a fixed duration. For one-off subscriptions, that single line is often all you need. ## Subscribe to account changes Account subscriptions notify you whenever an account's data changes onchain. The shape of the notification mirrors the response of `getAccountInfo`, so the same encoding and commitment options apply. ```ts twoslash import { address } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const wallet = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const accountNotifications = await client.rpcSubscriptions .accountNotifications(wallet, { commitment: 'confirmed' }) .subscribe({ abortSignal: AbortSignal.timeout(60_000) }); for await (const notification of accountNotifications) { console.log('Account changed:', notification.value); } ``` If you only need a one-off read of an account, [`fetchEncodedAccount`](/docs/guides/fetching-accounts) is usually a better fit. Use subscriptions for live, ongoing updates. ## Handle disconnects and errors Long-lived subscriptions can disconnect for many reasons — flaky networks, server restarts, or connection limits — and errors can be raised at any point during iteration. Wrap the iteration in a `try`/`catch` so your application can react gracefully. ```ts twoslash import { address } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const wallet = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const accountNotifications = await client.rpcSubscriptions .accountNotifications(wallet, { commitment: 'confirmed' }) .subscribe({ abortSignal: AbortSignal.timeout(60_000) }); try { for await (const notification of accountNotifications) { console.log('Account changed:', notification.value); } } catch (error) { // Reconnect, log, surface a UI error, etc. } ``` Depending on your application, you may want to recreate the subscription with a fresh `AbortSignal` after a disconnect, possibly using a small backoff to avoid thrashing the server. ## Use subscriptions without a client If you do not need a Kit client, you can build an `RpcSubscriptions` object directly with `createSolanaRpcSubscriptions`. This is the lower-level building block the plugins use under the hood. ```ts twoslash import { createSolanaRpcSubscriptions, mainnet } from '@solana/kit'; const rpcSubscriptions = createSolanaRpcSubscriptions(mainnet('wss://api.mainnet-beta.solana.com')); const slotNotifications = await rpcSubscriptions .slotNotifications() .subscribe({ abortSignal: AbortSignal.timeout(10_000) }); ``` This is useful when you want maximum tree-shaking, when you are writing a library that should not assume a client, or when you only need a tiny subset of the subscriptions API. ## Choose a WebSocket endpoint For light development or personal use, the public WebSocket endpoints maintained by the Solana Foundation work the same way as their HTTP counterparts. - `wss://api.mainnet-beta.solana.com` - `wss://api.testnet.solana.com` - `wss://api.devnet.solana.com` In production, your HTTP and WebSocket endpoints should match — both clusters and providers — to avoid subtle mismatches between the data each transport returns. Most RPC providers offer paired endpoints; consult their documentation for the exact URLs they expect you to use. ## Next steps - [RPC requests](/docs/guides/rpc) — read state and submit one-off requests over HTTP. - [Fetching accounts](/docs/guides/fetching-accounts) — pair subscriptions with a baseline read. - [Sending transactions](/docs/guides/sending-transactions) — submit work to the network. ================================================ FILE: docs/content/docs/guides/rpc.mdx ================================================ --- title: RPC requests description: Read and write data to the blockchain --- Solana applications interact with the network through a JSON-RPC server. RPC requests are how your code reads onchain state, simulates transactions, and submits transactions to be processed. Kit ships a fully typed RPC client and exposes it on Kit clients via plugins, so the same RPC API is available whether you compose a client or use the lower-level building blocks directly. Kit aims to support every method documented in the [Solana RPC HTTP methods](https://solana.com/docs/rpc/http) docs. You can either browse the methods in the official documentation or rely on TypeScript autocompletion in your editor. ## Installation You will typically install Kit alongside the RPC plugin package. ```package-install @solana/kit @solana/kit-plugin-rpc ``` The `@solana/kit-plugin-rpc` package contains the plugins that install RPC connectivity on a client. The `@solana/kit` package contains the lower-level RPC primitives those plugins are built on top of, so you can also use it standalone if you prefer not to use a client. ## Create an RPC client The most convenient way to make RPC requests is to compose a Kit client with one of the bundle plugins, which install both `client.rpc` and `client.rpcSubscriptions` in one call. ```ts twoslash import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); ``` The bundle plugins also wire up minimum balance computation, transaction planning, and transaction sending, which you will use in the other guides. If you only need to make read-only RPC requests, you can drop the signer plugin and use `solanaRpcConnection(...)` instead, which only installs `client.rpc` and `client.rpcSubscriptions`. ## Send your first RPC request Once you have an RPC client, you build a request by calling the corresponding method on `client.rpc` and finalize it with `.send()`. Most RPC responses come back wrapped in a `{ context, value }` object, so destructuring `value` is a common pattern. ```ts twoslash import { address } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const wallet = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const { value: balance } = await client.rpc.getBalance(wallet).send(); ``` Although `client.rpc` looks like a regular object, it is actually a [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) that constructs RPC requests on demand. TypeScript provides type safety for every method based on the API installed by your plugin, which makes the available methods discoverable from your editor while keeping the bundle small. ## Use cluster-specific RPC bundles The `@solana/kit-plugin-rpc` package ships several bundle plugins, each tailored to a specific use case. The cluster-typed variants type the RPC API to match what is available on that cluster, which prevents accidents like calling `airdrop` against mainnet at compile time. | Plugin | Default endpoint | Adds to the client | | ----------------------- | ------------------------------- | ----------------------------------------------------------- | | `solanaRpc(config)` | provided via `rpcUrl` | RPC, subscriptions, planning, sending | | `solanaMainnetRpc(...)` | provided via `rpcUrl` | mainnet-typed RPC, subscriptions, planning, sending | | `solanaDevnetRpc(...)` | `https://api.devnet.solana.com` | devnet-typed RPC, subscriptions, planning, sending, airdrop | | `solanaLocalRpc(...)` | `http://127.0.0.1:8899` | localnet RPC, subscriptions, planning, sending, airdrop | If you need to talk to a custom RPC provider, pass its URL through `solanaRpc({ rpcUrl: '...' })` or `solanaMainnetRpc({ rpcUrl: '...' })`. The other settings on `SolanaRpcConfig` cover advanced options such as transport configuration and transaction planner overrides. ## Pass request options Most RPC methods accept a configuration object as their final argument. This is where you control things like the requested commitment level, encoding, or pagination, depending on the method. ```ts twoslash import { address } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); // ---cut-end--- const wallet = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const { value: balance } = await client.rpc.getBalance(wallet, { commitment: 'confirmed' }).send(); ``` The `.send(...)` call itself accepts an optional `abortSignal` that lets you cancel the request in flight. ## Use RPC without a client If you do not need a Kit client, you can build an `Rpc` object directly with `createSolanaRpc`. This is useful when you want maximum tree-shaking, when you are writing a library that should not assume a client, or when you simply want to keep your dependency surface small. ```ts twoslash import { address, createSolanaRpc } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); const wallet = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const { value: balance } = await rpc.getBalance(wallet).send(); ``` The standalone `Rpc` object exposes the exact same API as `client.rpc`, so any code that takes an `Rpc` will work in either setup. See the [Kit without a client](/docs/advanced-guides/kit-without-a-client) guide for more on this approach. ## Choose an RPC endpoint For light development or personal use, the public RPC endpoints maintained by the Solana Foundation are a fine starting point. - `https://api.mainnet-beta.solana.com` - `https://api.testnet.solana.com` - `https://api.devnet.solana.com` These endpoints are heavily rate-limited and should not be used for anything close to production traffic. When you ship to production, lease an RPC node from a provider or [run your own](https://docs.anza.xyz/operations/setup-an-rpc-node). Some RPC providers also expose extra methods on top of the standard JSON-RPC API; many of them ship a Kit-compatible SDK that you can layer on top of `createSolanaRpc(...)` to merge their additional methods into the typed RPC object. ## Next steps - [RPC subscriptions](/docs/guides/rpc-subscriptions) — receive live updates from a WebSocket endpoint. - [Fetching accounts](/docs/guides/fetching-accounts) — go from raw account bytes to typed data. - [Sending transactions](/docs/guides/sending-transactions) — submit transactions through your client. ================================================ FILE: docs/content/docs/guides/sending-multiple-transactions.mdx ================================================ --- title: Sending multiple transactions description: Plan and execute work across several transactions --- When an operation cannot fit in a single transaction — too many instructions, too much data, or work that should run in parallel — you need to split it across several transactions. Kit clients support this through the same `client.sendTransactions(...)` and `client.planTransactions(...)` methods you used in [Sending transactions](/docs/guides/sending-transactions), but plural. ## Prerequisites This guide assumes a transaction-ready client, like the one set up in [Sending transactions](/docs/guides/sending-transactions). The same bundle plugins from `@solana/kit-plugin-rpc` and `@solana/kit-plugin-litesvm` install everything `client.sendTransactions(...)` needs out of the box. ## Instruction plans An instruction plan describes an operation made of several instructions together with constraints on how they can run — some steps must happen sequentially, others may run in parallel, and some must stay atomic. Kit represents this as a tree, so simple operations stay simple while more complex flows can nest sequential and parallel branches inside each other. The plan is then handed to a [transaction planner](/docs/advanced-guides/instruction-plans) that decides how to pack it into transaction messages, and then to a [transaction plan executor](/docs/advanced-guides/instruction-plans) that sends those transactions. Kit's planners can divide work, batch it into transactions, and even pack variable-sized data into the available space. For most cases you only need to know about three building blocks: `singleInstructionPlan`, `sequentialInstructionPlan`, and `parallelInstructionPlan`. The [Instruction plans](/docs/advanced-guides/instruction-plans) advanced guide goes much deeper. ## Run work sequentially Use `sequentialInstructionPlan(...)` when each step depends on the previous one — for example, creating an account before initializing it. ```ts twoslash import { Instruction, sequentialInstructionPlan } from '@solana/kit'; const createAccount = {} as Instruction; const initializeAccount = {} as Instruction; // ---cut-before--- const instructionPlan = sequentialInstructionPlan([createAccount, initializeAccount]); ``` Sequential plans accept either raw instructions or other instruction plans, so you can compose larger flows from smaller ones. A `nonDivisibleSequentialInstructionPlan(...)` variant also exists for steps that must run atomically inside a single transaction (or transaction bundle when not possible). ## Run work in parallel Use `parallelInstructionPlan(...)` when independent steps can run in any order. The planner is then free to pack them into separate transactions and send them concurrently. ```ts twoslash import { Instruction, parallelInstructionPlan } from '@solana/kit'; const transferToBob = {} as Instruction; const transferToCarla = {} as Instruction; // ---cut-before--- const instructionPlan = parallelInstructionPlan([transferToBob, transferToCarla]); ``` The two helpers compose freely: a sequential plan can contain parallel children and vice versa. This is how you describe operations like "set up these two accounts in parallel, then run the operation that depends on both of them". ## Plan multiple transactions When you only want to inspect or persist the planned transactions, `client.planTransactions(plan)` runs the planner without executing anything and returns the resulting `TransactionPlan` tree. ```ts twoslash import { lamports, parallelInstructionPlan } from '@solana/kit'; import { getTransferSolInstruction } from '@solana-program/system'; // ---cut-start--- import { address } from '@solana/kit'; import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(10_000_000_000n))); const bob = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const carla = address('Ay1zdJ3VDbhrAtkRiqBUJgKLHYbgFZN5BXk7svtMowif'); // ---cut-end--- const instructionPlan = parallelInstructionPlan([ getTransferSolInstruction({ source: client.payer, destination: bob, amount: lamports(500_000n), }), getTransferSolInstruction({ source: client.payer, destination: carla, amount: lamports(500_000n), }), ]); const transactionPlan = await client.planTransactions(instructionPlan); ``` The returned `TransactionPlan` keeps the same sequential and parallel structure as the original instruction plan, but its leaves are the concrete transaction messages the planner chose to pack the work into. This is useful for dry runs, debugging, or feeding a custom executor. ## Send multiple transactions To plan and send in one call, use `client.sendTransactions(plan)`. The result is a `TransactionPlanResult` tree that mirrors the original plan, with one leaf per transaction. ```ts twoslash import { lamports, parallelInstructionPlan } from '@solana/kit'; import { getTransferSolInstruction } from '@solana-program/system'; // ---cut-start--- import { address } from '@solana/kit'; import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(10_000_000_000n))); const bob = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const carla = address('Ay1zdJ3VDbhrAtkRiqBUJgKLHYbgFZN5BXk7svtMowif'); // ---cut-end--- const instructionPlan = parallelInstructionPlan([ getTransferSolInstruction({ source: client.payer, destination: bob, amount: lamports(500_000n), }), getTransferSolInstruction({ source: client.payer, destination: carla, amount: lamports(500_000n), }), ]); const result = await client.sendTransactions(instructionPlan); ``` `client.sendTransactions(...)` is the multi-transaction counterpart of `client.sendTransaction(...)`. The single-transaction version asserts that the plan resolves to exactly one transaction and unwraps its result — a convenience for the common case. The plural version preserves the full tree, which you can walk to see what happened. ## Understand transaction plan results Every leaf in the result tree is a `SingleTransactionPlanResult` with one of three statuses: - `successful` — the transaction was sent and confirmed; `context.signature` is always present. - `failed` — the transaction was attempted but failed during preflight, simulation, or execution. `error` carries the underlying error. - `canceled` — the transaction was never attempted, typically because a prior transaction in a sequential branch failed first. ```ts twoslash import { TransactionPlanResult } from '@solana/kit'; const result = {} as TransactionPlanResult; // ---cut-before--- if (result.kind === 'single') { if (result.status === 'successful') { console.log('Signature:', result.context.signature); } else if (result.status === 'failed') { console.error('Failed:', result.error); } else { console.warn('Canceled before being sent'); } } ``` Branch nodes use `kind: 'parallel'` or `kind: 'sequential'`, with their child results in `plans`. You usually do not need to walk the tree by hand — the helpers in the next sections cover the common cases. ## Summarize results `summarizeTransactionPlanResult(...)` flattens the tree and bucketizes the results so you can quickly check whether everything succeeded. ```ts twoslash import { summarizeTransactionPlanResult, TransactionPlanResult } from '@solana/kit'; const result = {} as TransactionPlanResult; // ---cut-before--- const summary = summarizeTransactionPlanResult(result); if (summary.successful) { console.log(`✅ ${summary.successfulTransactions.length} transactions confirmed`); } else { console.warn( `${summary.successfulTransactions.length} ok, ` + `${summary.failedTransactions.length} failed, ` + `${summary.canceledTransactions.length} canceled`, ); } ``` This is usually the first thing to reach for after `client.sendTransactions(...)` — it gives you a quick health check without writing any tree-walking code. ## Continue after failures By default, `client.sendTransactions(...)` throws as soon as any transaction in the plan fails. That behaviour is convenient for small operations, but counterproductive when you are running a large batch and want to inspect partial results. The `passthroughFailedTransactionPlanExecution(...)` helper turns that thrown error back into a `TransactionPlanResult`. ```ts twoslash import { InstructionPlan, passthroughFailedTransactionPlanExecution } from '@solana/kit'; const instructionPlan = {} as InstructionPlan; // ---cut-start--- import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(10_000_000_000n))); // ---cut-end--- const result = await passthroughFailedTransactionPlanExecution( client.sendTransactions(instructionPlan), ); ``` After this call, `result` is the same tree you would have received on full success — but with `failed` and `canceled` leaves where the issues occurred. You can then summarize it, walk it, or react however you need. ## Handle partial success When you want to react to each transaction individually, `flattenTransactionPlanResult(...)` collapses the tree into an array of leaf results in the order they appear. ```ts twoslash import { flattenTransactionPlanResult, TransactionPlanResult } from '@solana/kit'; const result = {} as TransactionPlanResult; // ---cut-before--- for (const single of flattenTransactionPlanResult(result)) { if (single.status === 'successful') { console.log('✅', single.context.signature); } else if (single.status === 'failed') { console.error('❌', single.error.message); } else { console.warn('⏭️', 'canceled'); } } ``` Combined with `passthroughFailedTransactionPlanExecution(...)`, this is enough to drive a UI that shows per-transaction status, retry buttons, or detailed error messages for a long-running multi-transaction operation. ## Next steps - [Sending transactions](/docs/guides/sending-transactions) — single-transaction basics. - [Advanced guides — Instruction plans](/docs/advanced-guides/instruction-plans) — the full instruction plan API. - [Advanced guides — Errors](/docs/advanced-guides/errors) — recover from `SolanaError` failures. ================================================ FILE: docs/content/docs/guides/sending-transactions.mdx ================================================ --- title: Sending transactions description: Send single transactions with a Kit client --- A Kit client can take a list of instructions and turn it into a fully signed, sent, and confirmed transaction in a single call. This guide covers the basics of sending one transaction at a time. When an operation needs to span multiple transactions, see [Sending multiple transactions](/docs/guides/sending-multiple-transactions). ## Create a transaction-ready client Sending transactions requires a client that exposes a `sendTransaction` method. At the lowest level, this means installing the [`planAndSendTransactions`](/docs/plugins/available-plugins) plugin from `@solana/kit-plugin-instruction-plan` on top of a custom `transactionPlanner` and `transactionPlanExecutor`. In practice, you almost always pick a higher-level bundle that does this for you. ```ts twoslash import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaLocalRpc()); ``` The bundle plugins from `@solana/kit-plugin-rpc` (`solanaRpc`, `solanaMainnetRpc`, `solanaDevnetRpc`, `solanaLocalRpc`) and the `litesvm` plugin from `@solana/kit-plugin-litesvm` all install a working planner and executor out of the box. Community plugins that target other backends should follow the same pattern, so the rest of this guide works the same way regardless of where transactions are actually sent. The signer plugin must be installed before the bundle, because the bundle's transaction planner needs a `payer` on the client to plan transactions. See [Setting up signers](/docs/guides/setting-up-signers) for the full set of signer options. ## Send instructions Once your client is ready, `client.sendTransaction([...])` accepts an array of instructions and turns them into a single transaction. The client takes care of fetching a recent blockhash, estimating compute units when applicable, setting the fee payer, signing with all attached signers, sending the transaction, and waiting for confirmation. ```ts twoslash import { address, lamports } from '@solana/kit'; import { getTransferSolInstruction } from '@solana-program/system'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))); // ---cut-end--- const recipient = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const result = await client.sendTransaction([ getTransferSolInstruction({ source: client.payer, destination: recipient, amount: lamports(500_000n), }), getTransferSolInstruction({ source: client.payer, destination: recipient, amount: lamports(500_000n), }), ]); ``` A single `client.sendTransaction([...])` call always produces exactly one transaction onchain, which means every instruction succeeds together or fails together. If you have so many instructions that they cannot fit in one transaction, use [`client.sendTransactions(...)`](/docs/guides/sending-multiple-transactions) instead. ## Plan before sending Sometimes you do not want to send the transaction immediately. You may want to inspect the planned message before signing, log it for debugging, or take over the rest of the lifecycle yourself. The `client.planTransaction([...])` method returns the planned transaction message and stops there, leaving signing and sending to you. ```ts twoslash import { address, lamports } from '@solana/kit'; import { getTransferSolInstruction } from '@solana-program/system'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))); // ---cut-end--- const recipient = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const transactionMessage = await client.planTransaction([ getTransferSolInstruction({ source: client.payer, destination: recipient, amount: lamports(500_000n), }), ]); ``` This gives you full control over what happens next: you can sign the message with [`signTransactionMessageWithSigners`](/docs/advanced-guides/signers), encode it for transmission to another service, or pass it to a custom executor. Note that the executor used by `client.sendTransaction([...])` may further mutate the planned message before sending — for example by attaching a fresh blockhash or refreshing the compute unit limit — so the message you inspect here is not guaranteed to be byte-identical to what would land onchain. The [Kit without a client](/docs/advanced-guides/kit-without-a-client) guide goes deeper into building and sending transactions step by step. ## Handle transaction errors When a transaction fails, `client.sendTransaction(...)` throws a [`SolanaError`](/docs/advanced-guides/errors) with the `SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION` code. The error message identifies whether the failure happened in preflight or onchain, and the `cause` carries the underlying error you can branch on. ```ts twoslash import { address, isSolanaError, lamports, SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION, } from '@solana/kit'; import { getTransferSolInstruction } from '@solana-program/system'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))); const recipient = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); // ---cut-end--- try { await client.sendTransaction([ getTransferSolInstruction({ source: client.payer, destination: recipient, amount: lamports(500_000n), }), ]); } catch (error) { if (isSolanaError(error, SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION)) { console.error(error.message); console.error('Logs:', error.context.logs); } else { throw error; } } ``` The same `error.context` object also exposes the underlying `transactionPlanResult`, which is particularly useful when you want to inspect the failed transaction message or its signature. Program-specific errors (such as System program or SPL Token program errors) live deeper inside `error.cause`. The [Errors](/docs/advanced-guides/errors) guide covers how to identify and handle them. ## Inspect transaction signatures A successful `client.sendTransaction(...)` resolves with a result object whose `context` carries the transaction `signature` and the original transaction message. You can use the signature to log a link to a block explorer or store it for later reference. ```ts twoslash import { address, lamports } from '@solana/kit'; import { getTransferSolInstruction } from '@solana-program/system'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))); const recipient = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); // ---cut-end--- const result = await client.sendTransaction([ getTransferSolInstruction({ source: client.payer, destination: recipient, amount: lamports(500_000n), }), ]); console.log(`✅ ${result.context.signature}`); ``` Transaction signatures are deterministic, so the signature is known as soon as the fee payer signs the transaction — you do not need to wait for confirmation to learn what it will be. If you build and sign your own transactions, the [`getSignatureFromTransaction`](/docs/advanced-guides/transactions) helper exposes the same value. ## Next steps - [Sending multiple transactions](/docs/guides/sending-multiple-transactions) — plan and execute work that does not fit in a single transaction. - [Using program plugins](/docs/guides/using-program-plugins) — discover the `.sendTransaction()` shortcut on typed program helpers. - [Advanced guides — Transactions](/docs/advanced-guides/transactions) — learn the full transaction lifecycle. - [Advanced guides — Errors](/docs/advanced-guides/errors) — handle failures with `SolanaError`. ================================================ FILE: docs/content/docs/guides/setting-up-signers.mdx ================================================ --- title: Setting up signers description: Authorize transactions, pay fees, and own onchain assets --- Most Kit applications need at least one signer. Signers represent the accounts that authorize transactions, pay fees, and own onchain assets such as tokens or program authorities. This guide covers the different ways you can attach signers to a Kit client and the situations each one is best suited for. ## Installation You will need Kit and the signer plugin package. ```package-install @solana/kit @solana/kit-plugin-signer ``` Some examples below also use `@solana/kit-plugin-rpc` or `@solana/kit-plugin-litesvm` to fund signers in local environments. ## What is a signer? A signer is an object that carries an `Address` and knows how to produce a signature for that address. Where the underlying private key actually lives — in memory, in a keypair file, in a browser wallet, on a hardware device, or behind a remote signing service — is an implementation detail. As long as the signer implements one of Kit's signer interfaces, it can be plugged into the same APIs. This means you can swap a generated keypair for a wallet-backed signer without changing the rest of your code. The [Advanced guides — Signers](/docs/advanced-guides/signers) page goes into the different signer interfaces and when each one applies. ## Payer and identity Kit clients hold two distinct signer roles: - **`payer`** — the signer that pays transaction fees and storage costs (the rent reserved when creating new accounts). - **`identity`** — the signer that represents the wallet your application acts on behalf of, typically the authority over accounts, tokens, or other onchain assets. In most apps these roles are filled by the same signer, but they can be separated when you want a sponsored fee payer, a custodial backend, or any setup where authority and fee payment do not belong to the same key. ## Install signers on your client The most common way to install a signer is to use the `signer(...)` plugin, which sets the same signer as both the `payer` and the `identity` of the client. ```ts twoslash import { createClient, generateKeyPairSigner } from '@solana/kit'; import { signer } from '@solana/kit-plugin-signer'; const mySigner = await generateKeyPairSigner(); const client = createClient().use(signer(mySigner)); ``` The `signer(...)` plugin accepts any object that implements the `TransactionSigner` interface, so a signer obtained from somewhere else in your code — a library, a hook, a custom factory — can be passed in directly without any wrapping. If your application needs a sponsored fee payer or otherwise wants to set the two roles independently, you may use the `payer(...)` and `identity(...)` plugins instead. They have the exact same shape as `signer(...)` but install only one role each. ```ts twoslash import { createClient, generateKeyPairSigner } from '@solana/kit'; import { identity, payer } from '@solana/kit-plugin-signer'; const feePayer = await generateKeyPairSigner(); const owner = await generateKeyPairSigner(); const client = createClient().use(payer(feePayer)).use(identity(owner)); ``` ## Load a signer from a file Most Solana CLI keypairs are stored as a JSON byte array on disk. The `signerFromFile(...)` plugin reads such a file and installs the resulting signer on both the `payer` and `identity` roles. ```ts twoslash import { createClient } from '@solana/kit'; import { signerFromFile } from '@solana/kit-plugin-signer'; const client = await createClient().use(signerFromFile('~/.config/solana/id.json')); ``` This is convenient for command-line tools, scripts, and backend services that already use a Solana CLI keypair. As with the in-memory plugins above, `payerFromFile(path)` and `identityFromFile(path)` are available if you only want to install a single role. The file-based signer plugins read from the local filesystem and therefore only work in Node.js environments. They throw an error in browsers and React Native. Make sure your keypair files are excluded from version control and from any production build artifact. ## Use a browser wallet signer A dedicated wallet signer plugin is on its way and will be documented here once it lands. It will use the [Wallet Standard](https://github.com/wallet-standard/wallet-standard) under the hood and expose a framework-agnostic reactive layer, so it can be used from any UI framework — not just React. Until then, the [`@solana/react`](/api) hooks can produce a `TransactionSigner` from a connected wallet account, and you can install that signer on a Kit client with the `signer(...)`, `payer(...)`, or `identity(...)` plugins shown above. ## Generate signers for tests When writing tests, generating a fresh signer per run keeps each scenario isolated and avoids leaking state between tests. The `generatedSigner()` plugin creates a brand new keypair signer and installs it on both roles. ```ts twoslash import { createClient } from '@solana/kit'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()); ``` `generatedPayer()` and `generatedIdentity()` follow the same pattern when you only want to generate one of the two roles. Generated signers are great for tests but should not be used as long-lived production identities, since the keypair only exists for the lifetime of the client. ## Fund signers in local environments Generated signers start with no SOL, so they can not pay fees or rent until they have been funded. The typical local pattern is to install a generated signer first, then install a local environment that ships an `airdrop` capability — either `solanaLocalRpc()` or `litesvm()` — and finally airdrop SOL to the signer with the `airdropSigner(...)` plugin. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))); ``` The order matters: the signer must exist before the RPC bundle is added (so the bundle can wire the signer into transaction planning), and the airdrop function must be available before `airdropSigner(...)` runs (which `solanaLocalRpc()` and `litesvm()` both provide). `airdropPayer(...)` and `airdropIdentity(...)` are available if you split roles between two different signers. Airdrops only work on test clusters such as devnet, testnet, and local validators. Mainnet does not have a faucet, and `solanaMainnetRpc(...)` will refuse to type-check an `airdrop` call. ## Next steps - [Sending transactions](/docs/guides/sending-transactions) — use your client to send and confirm transactions. - [Testing and local development](/docs/guides/testing-and-local-development) — set up local validators or LiteSVM for testing. - [Advanced guides — Signers](/docs/advanced-guides/signers) — learn the signer interfaces in depth. ================================================ FILE: docs/content/docs/guides/testing-and-local-development.mdx ================================================ --- title: Testing & local development description: Test your Solana applications locally --- Local development and testing benefit hugely from Kit's plugin system: the same application code can run against devnet, a local validator, or an in-process [LiteSVM](https://github.com/litesvm/litesvm) instance, depending on which bundle plugin you install. This guide walks through the three options and the small patterns that make tests easy to write and maintain. ## Choose a local environment Each environment shines in a different situation. The table below summarizes when to reach for each. | Environment | Best for | Tradeoffs | | --------------- | ---------------------------------------------------- | --------------------------------------------------------- | | Devnet | shared/manual testing | faucet limits, network latency, shared state across tests | | Local validator | integration testing, full RPC API | requires a separate process, shared state across tests | | LiteSVM | fast unit tests, manipulating account state per test | Node.js only, RPC subset, in-memory only state | You do not have to commit to one environment for the whole project. It is common to use LiteSVM for fast unit tests, a local validator for integration tests, and devnet for manual exploration. ## Use devnet for shared testing Devnet is the easiest way to share a Solana environment with your team. The `solanaDevnetRpc()` plugin defaults to the public devnet endpoint and bundles an `airdrop` capability for funding accounts. ```ts twoslash import { address, createClient, lamports } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signerFromFile } from '@solana/kit-plugin-signer'; const client = await createClient() .use(signerFromFile('~/.config/solana/id.json')) .use(solanaDevnetRpc()); await client.airdrop(client.payer.address, lamports(1_000_000_000n)); ``` Devnet is great for one-off scripts and manual exploration, but the public faucet has tight rate limits. For automated tests, prefer one of the local options. ## Use a local validator A local validator runs the same software as mainnet but on your own machine. You can install the `solana-test-validator` binary by following the [Anza installation guide](https://docs.anza.xyz/cli/install) and start it with a single command. ```shell solana-test-validator ``` Once the validator is running, the `solanaLocalRpc()` plugin connects to its default endpoints (`http://127.0.0.1:8899` and `ws://127.0.0.1:8900`) and ships the same `airdrop` capability as devnet — without any rate limits. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))); ``` Use this setup for integration tests that need the full Solana RPC API or to load deployed programs the way a real validator would. Make sure the validator is running before your tests start; otherwise, the first RPC call will fail with a connection error. ## Use LiteSVM for fast tests LiteSVM runs the Solana virtual machine in-process, with no validator and no network. It is significantly faster than a local validator and is ideal for unit tests. The `litesvm()` plugin from `@solana/kit-plugin-litesvm` installs a LiteSVM-backed client that exposes the same `client.rpc`, `client.airdrop`, `client.sendTransaction`, and `client.sendTransactions` APIs as the RPC bundles. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { litesvm } from '@solana/kit-plugin-litesvm'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(litesvm()) .use(airdropSigner(lamports(1_000_000_000n))); ``` Each LiteSVM-backed client owns its own in-memory SVM instance, so every test can have a completely isolated state. This is particularly useful for unit tests where one test should never affect the state observed by another. LiteSVM runs natively in Node.js. It will throw if you try to use it in a browser or in React Native. The exposed RPC also covers a subset of the full API — enough for most tests, but worth double-checking against the [`@solana/kit-plugin-litesvm` README](https://github.com/anza-xyz/kit-plugins/tree/main/packages/kit-plugin-litesvm#readme) for the methods you rely on. ## Create a funded test client For most tests you want a fresh, funded signer for every run. The pattern is the same regardless of the chosen environment: install a generated signer, install the environment, and airdrop SOL. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { litesvm } from '@solana/kit-plugin-litesvm'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient() .use(generatedSigner()) .use(litesvm()) .use(airdropSigner(lamports(10_000_000_000n))); ``` Swapping `litesvm()` for `solanaLocalRpc()` is a one-line change and the rest of your test code stays the same. This is one of the main benefits of the plugin model: the surface your tests interact with is a `client`, not a particular environment. ## Reuse setup across tests Wrap the client creation in a small factory so every test starts from a clean state. Adding any program plugins or extra setup to the factory keeps tests focused on what they are actually testing. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { litesvm } from '@solana/kit-plugin-litesvm'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; import { tokenProgram } from '@solana-program/token'; async function createTestClient() { return await createClient() .use(generatedSigner()) .use(litesvm()) .use(airdropSigner(lamports(10_000_000_000n))) .use(systemProgram()) .use(tokenProgram()); } ``` Because Kit clients are immutable, you do not need to worry about leaking state between tests as long as you do not share a single client instance across them. ## Seed accounts and programs LiteSVM exposes the underlying SVM through `client.svm`, which lets you preload accounts and programs before your test code runs. This is the quickest way to set up a specific scenario without chaining a series of transactions. ```ts twoslash import { address, EncodedAccount, lamports } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { litesvm } from '@solana/kit-plugin-litesvm'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(litesvm()); // ---cut-end--- const myAccount: EncodedAccount = { address: address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'), data: new Uint8Array([1, 2, 3]), executable: false, lamports: lamports(1_000_000_000n), programAddress: address('11111111111111111111111111111111'), space: 3n, }; client.svm.setAccount(myAccount); client.svm.addProgramFromFile( address('MyProgram111111111111111111111111111111111'), './my_program.so', ); ``` A local validator supports similar workflows through its CLI flags (`--account`, `--bpf-program`, etc.), which you can pass when starting `solana-test-validator`. See [`@solana/kit-plugin-litesvm`](/docs/plugins/available-plugins) and the [Anza CLI documentation](https://docs.anza.xyz/cli/) for the exact APIs. ## Troubleshooting A few issues come up often when wiring up local environments: - **Faucet rate limits on devnet** — switch to `solanaLocalRpc()` or `litesvm()` for repeated runs, or top up your devnet signer manually from the [Solana Faucet](https://faucet.solana.com). - **Validator not running** — `solana-test-validator` must be running before tests start; check the process and the default port (`8899`). - **WebSocket endpoint mismatch** — when overriding `rpcUrl`, also set `rpcSubscriptionsUrl` if your provider uses a different URL for WebSocket traffic. - **`litesvm()` errors in browser tests** — LiteSVM is Node.js only; run those tests in a Node.js environment or pick a different bundle. ## Next steps - [Sending transactions](/docs/guides/sending-transactions) — exercise your client end-to-end. - [Using program plugins](/docs/guides/using-program-plugins) — add typed program access to your tests. - [Available plugins](/docs/plugins/available-plugins) — explore the LiteSVM and RPC plugin packages. ================================================ FILE: docs/content/docs/guides/using-program-plugins.mdx ================================================ --- title: Using program plugins description: Add typed program APIs to your client --- Program plugins are how Kit clients gain typed access to a specific Solana program. They install a typed namespace under the client — `client.system`, `client.token`, and so on — and expose the program's accounts and instructions through it. This guide walks through adding program plugins to a client and using them, alongside the standalone helpers that the same packages also expose. ## Installation Most popular Solana programs ship a Codama-generated package that doubles as a Kit program plugin. Install one for each program you want to interact with. ```package-install @solana-program/system @solana-program/token ``` The [Available plugins](/docs/plugins/available-plugins) page lists the program plugins available today. Once installed, apply each plugin with `.use(...)` after your signer and bundle plugins, and the client gains a new typed namespace named after the program. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(10_000_000_000n))) .use(systemProgram()) .use(tokenProgram()); ``` `client.system` and `client.token` now expose `accounts` and `instructions` namespaces typed against the System and Token programs. Multiple program plugins compose freely — installing more of them simply adds more namespaces to the client. ## What is a program plugin? A program plugin is a function from a Codama-generated package that adds a typed namespace for one program's accounts and instructions to a Kit client. The same package also exports standalone helpers — `getXInstruction`, `fetchX`, `decodeX`, and so on, where `X` is the instruction or account name — for cases where you do not want a client. ```ts twoslash import { systemProgram } from '@solana-program/system'; import { tokenProgram } from '@solana-program/token'; ``` You can think of a program plugin as a typed adapter on top of these standalone helpers. The plugin handles the wiring — passing the client's RPC and payer where needed — so your code can focus on what the program does. This is great for developer experience and program discoverability, but the tradeoff is tree-shakability: pulling an entire program namespace into your client tends to produce a larger bundle than importing only the standalone helpers you actually use. ## Fetch typed accounts Each `accounts` namespace exposes one entry per account type defined by the program, with `fetch`, `fetchMaybe`, `fetchAll`, and `fetchAllMaybe` methods that already know the codec to use. ```ts twoslash import { address } from '@solana/kit'; // ---cut-start--- import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(10_000_000_000n))) .use(tokenProgram()); // ---cut-end--- const mint = await client.token.accounts.mint.fetch( address('So11111111111111111111111111111111111111112'), ); mint.data.decimals; // typed as `number` ``` Because the account namespace is generated from the program's IDL, every field on `mint.data` is fully typed — autocompletion and refactors flow through naturally. See [Fetching accounts](/docs/guides/fetching-accounts) for raw account fetching that doesn't depend on a program plugin. ## Create typed instructions The matching `instructions` namespace exposes one builder per instruction the program defines. The builders accept a typed input and return an instruction ready to be sent. ```ts twoslash import { address, lamports } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(10_000_000_000n))) .use(systemProgram()); const recipient = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); // ---cut-end--- const transferSol = client.system.instructions.transferSol({ source: client.payer, destination: recipient, amount: lamports(500_000n), }); ``` ## Use the `.sendTransaction()` shortcut Each typed instruction returned by a program plugin also exposes a `.sendTransaction()` method that wraps `client.sendTransaction([instruction])`. This is the most concise way to send a one-off transaction. ```ts twoslash import { address, lamports } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(10_000_000_000n))) .use(systemProgram()); const recipient = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); // ---cut-end--- const result = await client.system.instructions .transferSol({ source: client.payer, destination: recipient, amount: lamports(500_000n), }) .sendTransaction(); ``` The same returned object also exposes `.sendTransactions()`, `.planTransaction()`, and `.planTransactions()` shortcuts that wrap the matching client methods. The shortcut shines for single-instruction operations where the rest of the transaction would just be ceremony. When you want to combine several instructions into one transaction or compose larger operations, [`client.sendTransaction([...])`](/docs/guides/sending-transactions) is the more flexible primitive. ## Use instruction plans from program plugins Some operations naturally span more than one instruction — creating and initializing a token mint is a classic example. Program plugins expose these as instruction plans rather than raw instructions, which means they can also be combined into larger plans. ```ts twoslash import { generateKeyPairSigner } from '@solana/kit'; // ---cut-start--- import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(10_000_000_000n))) .use(tokenProgram()); // ---cut-end--- const newMint = await generateKeyPairSigner(); const createMintPlan = client.token.instructions.createMint({ newMint, decimals: 9, mintAuthority: client.identity.address, }); ``` The returned plan can be sent on its own with `.sendTransaction()` or `.sendTransactions()`, fed into [`client.sendTransaction([...])`](/docs/guides/sending-transactions) alongside other work, or combined with [`sequentialInstructionPlan(...)`](/docs/guides/sending-multiple-transactions) and [`parallelInstructionPlan(...)`](/docs/guides/sending-multiple-transactions) helpers when building larger operations. ## Use program libraries without a client Every program package also exports standalone helpers that work on a plain `Rpc` object or with raw `Instruction`s. This is useful for libraries that should not assume a Kit client, or when you want maximum tree-shaking. ```ts twoslash import { address, createSolanaRpc, generateKeyPairSigner, lamports } from '@solana/kit'; import { fetchMint } from '@solana-program/token'; import { getTransferSolInstruction } from '@solana-program/system'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); const mint = await fetchMint(rpc, address('So11111111111111111111111111111111111111112')); const source = await generateKeyPairSigner(); const transferSol = getTransferSolInstruction({ source, destination: address('Ay1zdJ3VDbhrAtkRiqBUJgKLHYbgFZN5BXk7svtMowif'), amount: lamports(500_000n), }); ``` Because the standalone helpers and the plugin-based namespaces are generated from the same IDL, the input shapes match exactly. You can move from one style to the other without rewriting the data your application passes around. ## Generate plugins for your own programs Any program with a Codama IDL can be turned into a Kit-compatible package, with both standalone helpers and a program plugin, using the [Codama JS renderer](https://github.com/codama-idl/renderers-js). The renderer reads the IDL, emits TypeScript bindings, and wires them up through the same `@solana/program-client-core` primitives the `@solana-program/*` packages use. If your program is built with Anchor, you can convert its Anchor IDL to a Codama IDL first, which means program plugin generation works for Anchor programs as well. See [Generating program plugins](/docs/plugins/generating-program-plugins) for a step-by-step walkthrough. ## Next steps - [Available plugins](/docs/plugins/available-plugins) — browse the program plugins available today. - [Generating program plugins](/docs/plugins/generating-program-plugins) — generate a plugin for your own program. - [Fetching accounts](/docs/guides/fetching-accounts) — combine typed accounts with raw RPC reads. - [Sending transactions](/docs/guides/sending-transactions) — use typed instructions with `client.sendTransaction([...])`. ================================================ FILE: docs/content/docs/index.mdx ================================================ --- title: Installation description: Get started with Kit --- Kit is a JavaScript SDK for building Solana apps across environments like Node, the web, and React Native. It provides a comprehensive set of data types and helper functions, forming the foundation for interacting with Solana in JavaScript. Check out our [Upgrade guide](/docs/upgrade-guide) to see how Kit compares to Web3.js. ## Quick start Choose the client setup that fits your environment: ### Install packages Install Kit and the plugins for RPC connectivity and signer management. ```package-install @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer ``` ### Install program plugins Install plugins for any Solana program you want to interact with. You can find a full list on the [Available Plugins](/docs/plugins/available-plugins) page. ```package-install @solana-program/system @solana-program/token ``` ### Create your client ```ts twoslash import { createClient } from '@solana/kit'; import { solanaMainnetRpc } from '@solana/kit-plugin-rpc'; import { signerFromFile } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(signerFromFile('~/.config/solana/id.json')) .use(solanaMainnetRpc({ rpcUrl: 'https://api.mainnet-beta.solana.com' })) .use(systemProgram()) .use(tokenProgram()); // Here are some of the things you can do with your client: // client.rpc.getBalance(address).send(); // client.system.instructions.transferSol({...}).sendTransaction(); // client.token.accounts.mint.fetch(address); // client.sendTransaction(instructions); // And more... ``` This example loads the default Solana CLI keypair. See [Setting Up Signers](/docs/guides/setting-up-signers) for other production signer options such as using a wallet. ### Install packages Install Kit and the plugins for local RPC connectivity and signer management. ```package-install @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer ``` ### Install program plugins Install plugins for any Solana program you want to interact with. You can find a full list on the [Available Plugins](/docs/plugins/available-plugins) page. ```package-install @solana-program/system @solana-program/token ``` ### Start a test validator Make sure you have a local Solana validator running before using your client. ```shell solana-test-validator ``` ### Create your client ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(100_000_000_000n))) .use(systemProgram()) .use(tokenProgram()); // Signer is auto-generated and funded with SOL. // Here are some of the things you can do with your client: // client.rpc.getBalance(address).send(); // client.system.instructions.transferSol({...}).sendTransaction(); // client.token.accounts.mint.fetch(address); // client.sendTransaction(instructions); // And more... ``` ### Install packages Install Kit and the plugins for LiteSVM and signer management. ```package-install @solana/kit @solana/kit-plugin-litesvm @solana/kit-plugin-signer ``` ### Install program plugins Install plugins for any Solana program you want to interact with. You can find a full list on the [Available Plugins](/docs/plugins/available-plugins) page. ```package-install @solana-program/system @solana-program/token ``` ### Create your client ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { litesvm } from '@solana/kit-plugin-litesvm'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(litesvm()) .use(airdropSigner(lamports(100_000_000_000n))) .use(systemProgram()) .use(tokenProgram()); // Here are some of the things you can do with your client: // client.rpc.getBalance(address).send(); // client.system.instructions.transferSol({...}).sendTransaction(); // client.token.accounts.mint.fetch(address); // client.sendTransaction(instructions); // And more... ``` ### Install Kit ```package-install @solana/kit ``` ### Install program libraries Install Kit-compatible libraries for any program you want to interact with. For example, to interact with the System program, install the `@solana-program/system` package. You can find a [list of available program plugins here](/docs/plugins/available-plugins). ```package-install @solana-program/system @solana-program/token ``` ### Set up your project ```ts twoslash import { address, createSolanaRpc } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); const { value: balance } = await rpc .getBalance(address('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb')) .send(); console.log(`Balance: ${balance} lamports`); ``` See [Kit Without a Client](/docs/advanced-guides/kit-without-a-client) for the full guide on sending transactions, signing, and more without a client. Ready to build? Check out the [Getting Started](/docs/getting-started) tutorial to build your first app, or explore the [Guides](/docs/guides) for specific topics. ================================================ FILE: docs/content/docs/meta.json ================================================ { "title": "Documentation", "defaultOpen": true, "root": true, "pages": [ "index", "getting-started", "upgrade-guide", "...", "guides", "plugins", "advanced-guides" ] } ================================================ FILE: docs/content/docs/plugins/available-plugins.mdx ================================================ --- title: Available plugins description: Directory of plugins for Kit clients --- This page lists the Kit plugins published today. Anyone can author and publish a plugin; if you have one you'd like added here, open a PR against [`anza-xyz/kit`](https://github.com/anza-xyz/kit) to be listed alongside the others. The tables below group plugins by category. Within each category, generic plugins from Anza are listed first because they target the broadest range of use cases, followed by community plugins in alphabetical order. When a single package contains plugins with materially different purposes, those plugins are split across multiple rows so each description stays targeted. ## Signer plugins Signer plugins install a `TransactionSigner` on the client as the `payer`, the `identity`, or both. See [Setting up signers](/docs/guides/setting-up-signers) for an overview of when each variant fits. | Maintainer | Package | Plugins | Description | | ----------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------- | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-signer`](https://www.npmjs.com/package/@solana/kit-plugin-signer) | `signer`, `payer`, `identity` | Install an existing `TransactionSigner` on the client. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-signer`](https://www.npmjs.com/package/@solana/kit-plugin-signer) | `signerFromFile`, `payerFromFile`, `identityFromFile` | Load a Solana CLI keypair from a JSON file (Node.js only). | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-signer`](https://www.npmjs.com/package/@solana/kit-plugin-signer) | `generatedSigner`, `generatedPayer`, `generatedIdentity` | Generate a fresh in-memory keypair, useful for tests. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-signer`](https://www.npmjs.com/package/@solana/kit-plugin-signer) | `generatedSignerWithSol`, `generatedPayerWithSol`, `generatedIdentityWithSol` | Generate a signer and airdrop SOL to it in one step. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-signer`](https://www.npmjs.com/package/@solana/kit-plugin-signer) | `airdropSigner`, `airdropPayer`, `airdropIdentity` | Airdrop SOL to a signer that is already installed on the client. | ## RPC plugins RPC plugins install Solana RPC connectivity on the client. LiteSVM is included here because it implements a subset of the same RPC API in-process, with no network involved. | Maintainer | Package | Plugins | Description | | ---------------------------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-rpc`](https://www.npmjs.com/package/@solana/kit-plugin-rpc) | `solanaRpc`, `solanaMainnetRpc`, `solanaDevnetRpc`, `solanaLocalRpc` | Bundle plugins that wire up RPC, subscriptions, planning, and sending in one step. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-rpc`](https://www.npmjs.com/package/@solana/kit-plugin-rpc) | `solanaRpcConnection` | Install `client.rpc` and `client.rpcSubscriptions` from a cluster URL. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-rpc`](https://www.npmjs.com/package/@solana/kit-plugin-rpc) | `rpcAirdrop` | Add `client.airdrop` for devnet, testnet, and local validators. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-rpc`](https://www.npmjs.com/package/@solana/kit-plugin-rpc) | `rpcGetMinimumBalance` | Add `client.getMinimumBalance` using the matching RPC method. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-litesvm`](https://www.npmjs.com/package/@solana/kit-plugin-litesvm) | `litesvm` | Bundle plugin that runs an in-process Solana VM with full transaction sending support. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-litesvm`](https://www.npmjs.com/package/@solana/kit-plugin-litesvm) | `litesvmConnection` | Install `client.svm` and a subset of `client.rpc` backed by an in-process VM. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-litesvm`](https://www.npmjs.com/package/@solana/kit-plugin-litesvm) | `litesvmAirdrop` | Add `client.airdrop` against the in-process VM. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-litesvm`](https://www.npmjs.com/package/@solana/kit-plugin-litesvm) | `litesvmGetMinimumBalance` | Add `client.getMinimumBalance` against the in-process VM. | | [@amilz](https://github.com/amilz) | [`local-validator`](https://github.com/amilz/kit-helpers/tree/main/plugins/local-validator) (unpublished) | `localValidatorPlugin` | Manage the lifecycle of a local `solana-test-validator` (start, stop, restart). | ## Instruction plan plugins Instruction plan plugins install transaction planning and sending capabilities on the client. The RPC and LiteSVM packages are listed here too because they ship default planner and executor implementations on top of their respective backends — including the bundle plugins, which install everything needed to call `client.planTransaction(s)` and `client.sendTransaction(s)` in one step. | Maintainer | Package | Plugins | Description | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-instruction-plan`](https://www.npmjs.com/package/@solana/kit-plugin-instruction-plan) | `transactionPlanner` | Install a custom transaction planner on the client. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-instruction-plan`](https://www.npmjs.com/package/@solana/kit-plugin-instruction-plan) | `transactionPlanExecutor` | Install a custom transaction plan executor on the client. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-instruction-plan`](https://www.npmjs.com/package/@solana/kit-plugin-instruction-plan) | `planAndSendTransactions` | Add `client.planTransaction(s)` and `client.sendTransaction(s)` on top of an existing planner and executor. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-rpc`](https://www.npmjs.com/package/@solana/kit-plugin-rpc) | `solanaRpc`, `solanaMainnetRpc`, `solanaDevnetRpc`, `solanaLocalRpc` | Bundle plugins that wire up RPC, subscriptions, planning, and sending in one step. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-rpc`](https://www.npmjs.com/package/@solana/kit-plugin-rpc) | `rpcTransactionPlanner`, `rpcTransactionPlanExecutor` | Default transaction planner and executor backed by RPC. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-litesvm`](https://www.npmjs.com/package/@solana/kit-plugin-litesvm) | `litesvm` | Bundle plugin that runs an in-process Solana VM with full transaction sending support. | | [Anza](https://www.anza.xyz/) | [`@solana/kit-plugin-litesvm`](https://www.npmjs.com/package/@solana/kit-plugin-litesvm) | `litesvmTransactionPlanner`, `litesvmTransactionPlanExecutor` | Default transaction planner and executor backed by LiteSVM. | | [@amilz](https://github.com/amilz) | [`transaction-builder`](https://github.com/amilz/kit-helpers/tree/main/plugins/transaction-builder) (unpublished) | `transactionBuilderPlugin` | Helpers for constructing, signing, and sending transactions. | ## Program plugins Program plugins are generated from program IDLs and install a typed `client.` namespace with `accounts` and `instructions` helpers. See [Using program plugins](/docs/guides/using-program-plugins) for usage and [Generating program plugins](/docs/plugins/generating-program-plugins) for authoring new ones. | Maintainer | Package | Plugins | Description | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------- | ----------------------------------------------------------------------------- | | [Anza](https://www.anza.xyz/) | [`@solana-program/address-lookup-table`](https://www.npmjs.com/package/@solana-program/address-lookup-table) | _Plugin coming soon_ | Address Lookup Table program. | | [Anza](https://www.anza.xyz/) | [`@solana-program/compute-budget`](https://www.npmjs.com/package/@solana-program/compute-budget) | _Plugin coming soon_ | Compute Budget program: priority fees and compute unit limits. | | [Anza](https://www.anza.xyz/) | [`@solana-program/memo`](https://www.npmjs.com/package/@solana-program/memo) | _Plugin coming soon_ | Memo program. | | [Anza](https://www.anza.xyz/) | [`@solana-program/stake`](https://www.npmjs.com/package/@solana-program/stake) | _Plugin coming soon_ | Stake program. | | [Anza](https://www.anza.xyz/) | [`@solana-program/system`](https://www.npmjs.com/package/@solana-program/system) | `systemProgram` | Native System program: account creation, transfers, nonce accounts, and more. | | [Anza](https://www.anza.xyz/) | [`@solana-program/token`](https://www.npmjs.com/package/@solana-program/token) | `tokenProgram` | SPL Token program: mints, token accounts, and ATAs. | | [Anza](https://www.anza.xyz/) | [`@solana-program/token-2022`](https://www.npmjs.com/package/@solana-program/token-2022) | _Plugin coming soon_ | SPL Token Extension program (Token-2022). | | [@amilz](https://github.com/amilz) | [`airdrop-token`](https://github.com/amilz/kit-helpers/tree/main/plugins/airdrop-token) (unpublished) | `airdropToken`, `testTokenPlugin` | Helpers for creating mints, ATAs, and minting tokens for tests. | ## Other plugins Plugins that don't fit into the categories above land here. Nothing has been listed yet — open a PR against [`anza-xyz/kit`](https://github.com/anza-xyz/kit) to add yours. ## Next steps - [Creating custom plugins](/docs/plugins/creating-custom-plugins) — write your own plugin and publish it. - [Generating program plugins](/docs/plugins/generating-program-plugins) — generate a typed plugin for a Solana program. - [Using program plugins](/docs/guides/using-program-plugins) — consume program plugins from a Kit client. ================================================ FILE: docs/content/docs/plugins/creating-custom-plugins.mdx ================================================ --- title: Creating custom plugins description: Write your own plugins to extend Kit clients --- Plugins are small, focused functions that extend a Kit client with new capabilities. This page walks through writing a plugin from scratch, declaring its prerequisites, handling asynchronous setup, registering cleanup logic, and composing several plugins into a higher-level bundle. ## The `ClientPlugin` shape A plugin is a function that takes a client and returns a new client object. The recommended way to add properties is the `extendClient(client, additions)` helper from `@solana/kit`, which preserves property descriptors (getters, symbol-keyed properties) that a plain object spread would flatten. ```ts twoslash import { createClient, extendClient } from '@solana/kit'; function apple() { return (client: T) => extendClient(client, { fruit: 'apple' as const }); } const client = createClient().use(apple()); client.fruit; // 'apple' ``` Wrapping the plugin in a small factory function (`apple()` rather than `apple`) is the convention across Kit plugins. It gives you somewhere to accept configuration without changing the shape of the function returned to `.use(...)`. ## Add prerequisites with TypeScript generics Plugins can require capabilities from the input client by constraining the input type. If a consumer applies your plugin in the wrong order, TypeScript reports a compile-time error rather than letting it fail at runtime. ```ts twoslash // @errors: 2345 import { createClient, extendClient } from '@solana/kit'; function appleTart() { return (client: T) => extendClient(client, { dessert: 'appleTart' as const }); } createClient().use(appleTart()); // ❌ Missing `fruit: 'apple'`. ``` The same mechanism is what makes the official plugins compose so cleanly: `airdropPayer()` requires `ClientWithPayer & ClientWithAirdrop`, `solanaRpc(...)` requires `ClientWithPayer`, and so on. Adopting the same pattern in your own plugins keeps users honest about the order of operations. ## Use the standard interfaces Whenever possible, declare your prerequisites and additions using the standard interfaces from `@solana/plugin-interfaces` (re-exported from `@solana/kit`). This lets your plugin compose with any other plugin that targets the same shape, regardless of its package. The most common interfaces are: - `ClientWithPayer` and `ClientWithIdentity` for signer roles. - `ClientWithRpc` and `ClientWithRpcSubscriptions` for transports. - `ClientWithAirdrop` and `ClientWithGetMinimumBalance` for funding helpers. - `ClientWithTransactionPlanning` and `ClientWithTransactionSending` for the transaction lifecycle. If your plugin's capability does not match any existing interface, define your own type and export it. If you think the capability is generally useful and there is a gap in the standard interfaces, consider opening a PR against [`anza-xyz/kit`](https://github.com/anza-xyz/kit) so other plugins can adopt it too. Treating capabilities as named interfaces makes them addressable from other code without naming the specific plugin that installed them. ## Asynchronous plugins Plugins can return a `Promise` instead of a `Client`. The `.use(...)` chain awaits async plugins automatically, so you only need a single `await` at the end of the chain. ```ts twoslash import { createClient, createKeyPairSignerFromBytes, extendClient, KeyPairSigner, } from '@solana/kit'; import { readFile } from 'node:fs/promises'; function signerFromBytesFile(path: string) { return async (client: T) => { const bytes = JSON.parse(await readFile(path, 'utf-8')) as number[]; const signer: KeyPairSigner = await createKeyPairSignerFromBytes(new Uint8Array(bytes)); return extendClient(client, { payer: signer, identity: signer }); }; } const client = await createClient().use(signerFromBytesFile('./keypair.json')); ``` Async and sync plugins can be mixed freely in the same chain. Once any async plugin is involved, the `.use(...)` chain returns an `AsyncClient` that you await once at the end. ## Release resources on disposal Plugins that hold disposable resources — WebSocket connections, intervals, file handles — can register a cleanup function with `withCleanup(...)`. The returned client implements `Disposable`, so a `using` declaration runs the cleanup when the client goes out of scope. ```ts twoslash import { extendClient, withCleanup } from '@solana/kit'; function liveFeed(url: string) { return (client: T) => { const socket = new WebSocket(url); return withCleanup(extendClient(client, { socket }), () => socket.close()); }; } ``` `withCleanup` chains existing dispose logic, so you can call it more than once across plugins without overriding earlier cleanup. Reach for it whenever your plugin owns a resource the user would otherwise have to remember to release manually. ## Compose smaller plugins into a bundle A bundle plugin is just a regular plugin that applies several smaller plugins one after another. The `pipe(...)` helper from `@solana/kit` makes the composition concise and the resulting type predictable. ```ts twoslash // @noErrors import { ClientWithPayer, pipe } from '@solana/kit'; import { rpcAirdrop, rpcGetMinimumBalance, solanaRpcConnection, SolanaRpcConnectionConfig, } from '@solana/kit-plugin-rpc'; function myDevnetBundle(config: SolanaRpcConnectionConfig) { return (client: T) => pipe(client, solanaRpcConnection(config), rpcGetMinimumBalance(), rpcAirdrop()); } ``` The bundle accepts an existing client rather than calling `createClient()` itself. That keeps your bundle composable: callers stay in control of their own client and can apply additional plugins before or after yours. It is also what lets the official `solanaRpc`, `solanaDevnetRpc`, `solanaLocalRpc`, and `litesvm` bundles drop into any client the same way a single plugin would. ## Publish your plugin A few small conventions go a long way once a plugin lands on npm: - Name the factory function after the capability it installs (e.g. `signer`, `rpcAirdrop`, `tokenProgram`) rather than the package, and make sure it reads well at the call site — `use(tokenProgram())` should sound like the sentence you would write to describe what it does. - Document the standard interfaces your plugin requires and provides; this is what other plugin authors will read first. - Keep the public surface minimal — one factory function per concept — and prefer composition over bigger plugins that try to do everything. Once your plugin is published, open a PR against [`anza-xyz/kit`](https://github.com/anza-xyz/kit) to be listed on the [Available plugins](/docs/plugins/available-plugins) page so other developers can find it. ## Next steps - [Plugins](/docs/plugins) — revisit the client model and standard interfaces. - [Available plugins](/docs/plugins/available-plugins) — see how published plugins describe themselves. - [Generating program plugins](/docs/plugins/generating-program-plugins) — let Codama write a program plugin for you. ================================================ FILE: docs/content/docs/plugins/generating-program-plugins.mdx ================================================ --- title: Generating program plugins description: Generate typed plugins for your Solana programs using Codama --- Program plugins are generated from program IDLs by [Codama](https://github.com/codama-idl/codama). Codama's CLI is the quickest path to a working plugin: two commands set up a configuration file in your repo and emit a Kit-compatible package containing both standalone helpers and a typed program plugin. This page focuses on how the generated output fits into the Kit ecosystem. Codama itself has comprehensive docs that go much deeper into IDLs, visitors, and renderer configuration. ## What Codama generates For every program, the JavaScript renderer emits two things side by side: standalone helpers for use without a client, and a program plugin that installs a typed namespace on a Kit client. ```ts // Standalone helpers, one per instruction and account defined in the IDL. import { decodeMyAccount, fetchMyAccount, getMyInstruction } from '@your-scope/your-program'; // Program plugin — installs `client.yourProgram.accounts` and `client.yourProgram.instructions`. import { yourProgram } from '@your-scope/your-program'; ``` Both forms are generated automatically from your IDL. The standalone helpers tree-shake well and keeps JS bundles small, while the program plugin is the most ergonomic way to use the program from a Kit client. ## Set up Codama with `codama init` Install Codama as a dev dependency and run `codama init`. The CLI prompts you for the path to your IDL and writes a `codama.json` configuration file at the root of your project. Make sure to select the JS client option to generate a Kit-compatible program library. ```sh pnpm install codama codama init ``` The CLI auto-detects Anchor IDLs and configures the conversion step for you, so this same flow works for both native programs (with a Codama IDL) and Anchor programs (with an Anchor IDL). ## Generate the plugin with `codama run js` Once the configuration file exists, generating the JavaScript client is a single command. Codama installs the renderer the first time it runs and writes the generated files to the path declared in the configuration. ```sh codama run js ``` The output includes the standalone helpers, the program plugin, and the supporting types. Importing it from a Kit client looks the same as any other program plugin: ```ts twoslash // @noErrors import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; import { yourProgram } from '@your-scope/your-program'; const client = await createClient() .use(generatedSigner()) .use(solanaDevnetRpc()) .use(yourProgram()); ``` ## Configure the renderer The `codama.json` file generated by `codama init` controls where the output lands and which visitors run before the renderer. You can edit it to rename accounts, add custom transformations, or change the output directory. See the [Codama JS renderer docs](https://github.com/codama-idl/renderers-js) for the full list of configuration options. If you need more than one client (for example, both JavaScript and Rust), additional renderers can be added to the same configuration file and run with `codama run --all`. ## Publish your generated plugin Once your plugin is generating cleanly, publishing it follows the same pattern as the official `@solana-program/*` packages. A few conventions worth keeping in mind: - Use a `@/` package layout, with a single entry point that re-exports both the standalone helpers and the `programName()` plugin. - Once published, open a PR against [`anza-xyz/kit`](https://github.com/anza-xyz/kit) to list your plugin on the [Available plugins](/docs/plugins/available-plugins) page so other developers can find it. ## Next steps - [Using program plugins](/docs/guides/using-program-plugins) — consume program plugins from a Kit client. - [Available plugins](/docs/plugins/available-plugins) — browse program plugins published today. - [Creating custom plugins](/docs/plugins/creating-custom-plugins) — write a plugin that is not generated from an IDL. ================================================ FILE: docs/content/docs/plugins/index.mdx ================================================ --- title: Plugins description: Understand Kit's plugin ecosystem --- Plugins are how Kit clients gain capabilities. Everything from RPC connectivity, wallet signers, transaction sending, and even typed access to Solana programs is delivered as a plugin you opt into. This page explains the underlying client model and the conventions plugins follow. ## What is a Kit client? A Kit client is a frozen, typed JavaScript object created with `createClient()` from `@solana/kit` and extended by chaining `.use(plugin())` calls. Each call applies a plugin, and the returned client is a brand new immutable object — the previous one is left untouched. ```ts twoslash import { createClient } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { generatedSigner } from '@solana/kit-plugin-signer'; const client = await createClient().use(generatedSigner()).use(solanaDevnetRpc()); ``` Because the client is immutable, you cannot accidentally mutate it from somewhere else in your application: `client` always reflects the exact set of plugins you applied at creation time. ## What is a plugin? A plugin is a function that takes a client object and returns either a new client object or a promise resolving to one. The shape is captured by the `ClientPlugin` type. ```ts twoslash import { ClientPlugin, extendClient } from '@solana/kit'; const apple = (): ClientPlugin => (client) => extendClient(client, { fruit: 'apple' as const }); ``` Most plugins follow the same convention: they are exported as small factory functions (so you can pass configuration to them) that return the actual `ClientPlugin` function. Asynchronous plugins are supported too; the `.use(...)` chain awaits them automatically, so you only need a single `await` at the end. ## Plugin requirements and ordering Plugins can require capabilities from the input client by constraining the input type. This means installing them in the wrong order produces a TypeScript error rather than a runtime surprise. ```ts twoslash // @errors: 2345 import { ClientWithPayer, createClient, extendClient } from '@solana/kit'; function airdropPayer() { return (client: T) => extendClient(client, { airdropped: true }); } // Requires a `payer` to be installed first; this fails at compile time. const client = createClient().use(airdropPayer()); ``` Adding `payer(...)` (or any other plugin that satisfies `ClientWithPayer`) before `airdropPayer()` makes the chain type-check. The TypeScript compiler walks the whole chain and verifies that each plugin's input requirements are met by the time it is applied. ## Standard plugin interfaces Kit ships a small set of standard interfaces in `@solana/plugin-interfaces` (re-exported from `@solana/kit`) so plugins from different packages can interoperate. A plugin that installs `client.rpc` declares it provides `ClientWithRpc<...>`; a plugin that needs a payer constrains its input with `ClientWithPayer`. The most common interfaces are: - `ClientWithPayer` — installs `client.payer`. - `ClientWithIdentity` — installs `client.identity`. - `ClientWithRpc` — installs `client.rpc`. - `ClientWithRpcSubscriptions` — installs `client.rpcSubscriptions`. - `ClientWithAirdrop` — installs `client.airdrop`. - `ClientWithGetMinimumBalance` — installs `client.getMinimumBalance`. - `ClientWithTransactionPlanning` — installs `client.planTransaction(s)`. - `ClientWithTransactionSending` — installs `client.sendTransaction(s)`. Sticking to these interfaces lets plugins from any source compose freely with each other — and lets your application code take a `ClientWithRpc` rather than caring which specific plugin produced the RPC capability. ## Next steps - [Available plugins](/docs/plugins/available-plugins) — browse the plugins published today. - [Creating custom plugins](/docs/plugins/creating-custom-plugins) — write your own plugin. - [Generating program plugins](/docs/plugins/generating-program-plugins) — generate a typed plugin for a Solana program. ================================================ FILE: docs/content/docs/plugins/meta.json ================================================ { "title": "Plugins", "defaultOpen": true, "pages": ["available-plugins", "creating-custom-plugins", "generating-program-plugins"] } ================================================ FILE: docs/content/docs/tree-shaking.tsx ================================================ import { ImageZoom } from 'fumadocs-ui/components/image-zoom'; export default function TreeShaking() { return (
Tree-shaking illustration The left side shows a bunch of imports from Web3.js all being bundled despite only a couple being used. The right side shows an equivalent set of imports for Kit but only the used functions are bundled.
); } ================================================ FILE: docs/content/docs/upgrade-guide.mdx ================================================ --- title: Upgrade guide description: Upgrade your Web3.js project to Kit --- import TreeShaking from './tree-shaking'; ## Why upgrade? Kit is a complete rewrite of [the Web3.js library](https://github.com/solana-labs/solana-web3.js/). It is designed to be more composable, customizable, and efficient than its predecessor. Its functional design enables the entire library to be tree-shaken, drastically reducing your bundle size. It also takes advantage of modern JavaScript features — such as native Ed25519 key support and `bigint` for large values — resulting in an even smaller bundle, better performance and most importantly, a reduced attack surface for your application. Unlike Web3.js, Kit doesn't rely on JavaScript classes or other non-tree-shakeable features. This brings us to our first key difference. ## Where's my `Connection` class? In Web3.js, the `Connection` class serves as a central entry point, making the library's API easier to discover via a single object. However, this comes at a cost: using the `Connection` class forces you to bundle every method it provides, even if you only use a few. As a result, your users must download the entire library, even when most of it goes unused. To avoid this, **Kit does not include a single entry-point class like `Connection`**. Instead, it offers a set of functions that you can import and use as needed. Two key functions replace most of the `Connection` class's functionality: `createSolanaRpc` and `createSolanaRpcSubscriptions`. The former, `createSolanaRpc`, returns an `Rpc` object for making RPC requests to a specified endpoint. [Read more about RPCs here](/docs/guides/rpc).
```ts title="Web3.js" import { Connection, PublicKey } from '@solana/web3.js'; // Create a `Connection` object. const connection = new Connection('https://api.devnet.solana.com', { commitment: 'confirmed', }); // Send RPC requests. const wallet = new PublicKey('1234..5678'); const balance = await connection.getBalance(wallet); ``` ```ts twoslash title="Kit" import { address, createSolanaRpc } from '@solana/kit'; // Create an RPC proxy object. const rpc = createSolanaRpc('https://api.devnet.solana.com'); // Send RPC requests. const wallet = address('1234..5678'); const { value: balance } = await rpc.getBalance(wallet).send(); ```
The latter, `createSolanaRpcSubscriptions`, returns an `RpcSubscriptions` object, which lets you to subscribe to events on the Solana network. [Read more about RPC Subscriptions here](/docs/guides/rpc-subscriptions).
```ts title="Web3.js" import { Connection, PublicKey } from '@solana/web3.js'; // Create a `Connection` object with a WebSocket endpoint. const connection = new Connection('https://api.devnet.solana.com', { wsEndpoint: 'wss://api.devnet.solana.com', commitment: 'confirmed', }); // Subscribe to RPC events and listen to notifications. const wallet = new PublicKey('1234..5678'); connection.onAccountChange(wallet, (accountInfo) => { console.log(accountInfo); }); ``` ```ts twoslash title="Kit" import { address, createSolanaRpcSubscriptions } from '@solana/kit'; // Create an RPC subscriptions proxy object. const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com'); // Use an `AbortController` to cancel the subscriptions. const abortController = new AbortController(); // Subscribe to RPC events. const wallet = address('1234..5678'); const accountNotifications = await rpcSubscriptions .accountNotifications(wallet, { commitment: 'confirmed' }) .subscribe({ abortSignal: abortController.signal }); try { // Listen to event notifications. for await (const accountInfo of accountNotifications) { console.log(accountInfo); } } catch (e) { // Gracefully handle subscription disconnects. } ```
Note that, although `Rpc` and `RpcSubscriptions` look like classes, they are actually [`Proxy` objects](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). This means they dynamically construct RPC requests or subscriptions based on method names and parameters. TypeScript is then used to provide type safety for the RPC API, making it easy to discover available RPC methods while keeping the library lightweight. Regardless of whether your RPC supports 1 method or 100, the bundle size remains unchanged. ## Introducing Kit clients The functional approach above gives you maximum treeshakability, but it can feel verbose compared to Web3.js's `Connection`. Kit addresses this with **plugin-based clients** — a single object that bundles the functionality you need while still letting you choose what goes in. You cherry-pick plugins, so you typically only bundle what you actually use.
```ts title="Web3.js" import { Connection, PublicKey } from '@solana/web3.js'; // Create a `Connection` object. const connection = new Connection('https://api.devnet.solana.com'); // Use the connection. const wallet = new PublicKey('1234..5678'); const balance = await connection.getBalance(wallet); ``` ```ts twoslash title="Kit" import { address, createClient, generateKeyPairSigner } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signer } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; // Create a client with the plugins you need. const payer = await generateKeyPairSigner(); const client = createClient().use(signer(payer)).use(solanaDevnetRpc()).use(systemProgram()); // Use the client. const wallet = address('1234..5678'); const { value: balance } = await client.rpc.getBalance(wallet).send(); ```
A Kit client is an immutable JavaScript object built by chaining `.use()` calls. Each plugin adds capabilities to the client — like signer roles, RPC access, transaction sending, or typed program instructions. Start with `createClient()` from `@solana/kit`, add signer plugins such as `signer(payer)`, then apply RPC bundles like `solanaDevnetRpc()` to install RPC, subscriptions, transaction planning, sending, and devnet airdrops. You may then extend it with program plugins like `systemProgram()` and `tokenProgram()` to add typed access to the accounts and instructions of those programs. The rest of this guide uses a client for the Kit examples. To learn more about the plugin system, see [Plugins](/docs/plugins). ## Fetching and decoding accounts Fetching onchain accounts is as simple as calling the appropriate RPC method on the client. Kit also provides helper functions like `fetchEncodedAccount`, which returns a `MaybeAccount` object with an `exists` boolean to indicate whether the account is present onchain:
```ts title="Web3.js" import { PublicKey } from '@solana/web3.js'; const wallet = new PublicKey('1234..5678'); const account = await connection.getAccountInfo(wallet); ``` ```ts twoslash title="Kit" import { address, assertAccountExists, fetchEncodedAccount } from '@solana/kit'; // ---cut-start--- import { createClient, generateKeyPairSigner } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signer } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const payer = await generateKeyPairSigner(); const client = createClient().use(signer(payer)).use(solanaDevnetRpc()).use(systemProgram()); // ---cut-end--- const wallet = address('1234..5678'); const account = await fetchEncodedAccount(client.rpc, wallet); assertAccountExists(account); account.data satisfies Uint8Array; ```
To decode raw account data, Kit provides a composable serialization library called **codecs**. You can define a codec for any account type by combining primitives: ```ts twoslash title="Kit" import { address, Address, addCodecSizePrefix, getAddressCodec, getStructCodec, getU32Codec, getU8Codec, getUtf8Codec, } from '@solana/kit'; type Person = { age: number; discriminator: number; name: string; wallet: Address; }; const personCodec = getStructCodec([ ['discriminator', getU8Codec()], // A single-byte account discriminator. ['wallet', getAddressCodec()], // A 32-byte account address. ['age', getU8Codec()], // An 8-bit unsigned integer. ['name', addCodecSizePrefix(getUtf8Codec(), getU32Codec())], // A UTF-8 string with a 32-bit length prefix. ]); const bytes = personCodec.encode({ age: 42, discriminator: 0, name: 'Alice', wallet: address('1234..5678'), }); ``` [You can read more about codecs here](/docs/advanced-guides/codecs). Once you have a codec, you can use `decodeAccount` to transform an encoded account into a decoded one: ```ts twoslash title="Kit" import { address, decodeAccount, fetchEncodedAccount } from '@solana/kit'; // ---cut-start--- import { Address, createSolanaRpc, addCodecSizePrefix, getAddressCodec, getStructCodec, getU32Codec, getU8Codec, getUtf8Codec, } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); type Person = { age: number; discriminator: number; name: string; wallet: Address; }; const personCodec = getStructCodec([ ['discriminator', getU8Codec()], ['wallet', getAddressCodec()], ['age', getU8Codec()], ['name', addCodecSizePrefix(getUtf8Codec(), getU32Codec())], ]); // ---cut-end--- const wallet = address('1234..5678'); const account = await fetchEncodedAccount(rpc, wallet); const decodedAccount = decodeAccount(account, personCodec); if (decodedAccount.exists) { decodedAccount.data satisfies Person; } ``` ## Using program libraries Fortunately, you don't need to create a custom codec for every account. Kit provides client libraries for many popular Solana programs, [generated via Codama](https://github.com/codama-idl/codama), ensuring a consistent structure across them. These libraries can be used as standalone imports or as client plugins. As client plugins, they add typed `.accounts` and `.instructions` namespaces to your client. For example, here's how to fetch and decode a `Nonce` account using the System program:
```ts title="Web3.js" import { NonceAccount, PublicKey } from '@solana/web3.js'; const wallet = new PublicKey('1234..5678'); const accountInfo = await connection.getAccountInfo(wallet); const nonce = NonceAccount.fromAccountData(accountInfo.data); ``` ```ts twoslash title="Kit" import { address } from '@solana/kit'; // ---cut-start--- import { createClient, generateKeyPairSigner } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signer } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const payer = await generateKeyPairSigner(); const client = createClient().use(signer(payer)).use(solanaDevnetRpc()).use(systemProgram()); // ---cut-end--- const nonce = await client.system.accounts.nonce.fetch(address('1234..5678')); ```
They can also be used as standalone functions without a client: ```ts twoslash title="Kit" import { address } from '@solana/kit'; import { fetchNonce } from '@solana-program/system'; // ---cut-start--- import { createSolanaRpc } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); // ---cut-end--- const account = await fetchNonce(rpc, address('1234..5678')); ``` [Check out the "Available plugins" page](/docs/plugins/available-plugins) for a list of available program libraries compatible with Kit. ## Creating instructions Program libraries also provide functions for creating instructions. With a client, you can create instructions through the program's typed namespace:
```ts title="Web3.js" import { PublicKey, SystemProgram } from '@solana/web3.js'; const transferSol = SystemProgram.transfer({ fromPubkey: new PublicKey('2222..2222'), toPubkey: new PublicKey('3333..3333'), lamports: 1_000_000_000, // 1 SOL. }); ``` ```ts twoslash title="Kit" import { address } from '@solana/kit'; // ---cut-start--- import { createClient, generateKeyPairSigner } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signer } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const payer = await generateKeyPairSigner(); const client = createClient().use(signer(payer)).use(solanaDevnetRpc()).use(systemProgram()); // ---cut-end--- const transferSol = client.system.instructions.transferSol({ source: client.payer, destination: address('3333..3333'), amount: 1_000_000_000, // 1 SOL. }); ```
Without a client, you can use the standalone instruction helpers directly. In this case, Kit uses `TransactionSigner` objects for accounts that need to sign. This allows signers to be extracted from built transactions later, enabling automatic transaction signing without manually tracking which keys need to sign. ```ts twoslash title="Kit" import { address, generateKeyPairSigner } from '@solana/kit'; import { getTransferSolInstruction } from '@solana-program/system'; const source = await generateKeyPairSigner(); const transferSol = getTransferSolInstruction({ source, destination: address('3333..3333'), amount: 1_000_000_000, // 1 SOL. }); ``` ## Sending transactions In Web3.js, sending a transaction involves creating a `Transaction` object, adding instructions, signing it, and sending it. Kit clients reduce this to a single call:
```ts title="Web3.js" import { Keypair, PublicKey, sendAndConfirmTransaction, SystemProgram, Transaction, } from '@solana/web3.js'; const payer = Keypair.generate(); const transaction = new Transaction().add( SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: new PublicKey('3333..3333'), lamports: 1_000_000_000, }), ); const signature = await sendAndConfirmTransaction(connection, transaction, [payer]); ``` ```ts twoslash title="Kit" import { address } from '@solana/kit'; // ---cut-start--- import { createClient, generateKeyPairSigner } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signer } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const payer = await generateKeyPairSigner(); const client = createClient().use(signer(payer)).use(solanaDevnetRpc()).use(systemProgram()); // ---cut-end--- const result = await client.system.instructions .transferSol({ source: client.payer, destination: address('3333..3333'), amount: 1_000_000_000, }) .sendTransaction(); const signature = result.context.signature; ```
With a client, building the transaction message, setting the fee payer, fetching a recent blockhash, signing, sending, and confirming are all handled for you. You can also send multiple instructions as a single atomic transaction using `client.sendTransaction([...])`: ```ts twoslash title="Kit" import { address } from '@solana/kit'; // ---cut-start--- import { createClient, generateKeyPairSigner } from '@solana/kit'; import { solanaDevnetRpc } from '@solana/kit-plugin-rpc'; import { signer } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const payer = await generateKeyPairSigner(); const client = createClient().use(signer(payer)).use(solanaDevnetRpc()).use(systemProgram()); // ---cut-end--- await client.sendTransaction([ client.system.instructions.transferSol({ source: client.payer, destination: address('3333..3333'), amount: 500_000_000, }), client.system.instructions.transferSol({ source: client.payer, destination: address('4444..4444'), amount: 500_000_000, }), ]); ``` For full control over each of these steps, you can build transactions manually. Kit uses an immutable transaction model where each helper returns a new transaction object with an updated type. The `pipe` function lets you chain these helpers together: ```ts twoslash title="Kit" import { appendTransactionMessageInstructions, assertIsTransactionWithBlockhashLifetime, createTransactionMessage, getSignatureFromTransaction, pipe, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from '@solana/kit'; import { getCreateAccountInstruction } from '@solana-program/system'; import { getInitializeMintInstruction, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token'; // ---cut-start--- import { address, createSolanaRpc, createSolanaRpcSubscriptions, generateKeyPairSigner, } from '@solana/kit'; const rpc = createSolanaRpc('https://api.devnet.solana.com'); const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com'); const space = null as unknown as Parameters[0]['space']; const lamports = null as unknown as Parameters[0]['lamports']; // ---cut-end--- // Create signers — Kit uses the native CryptoKeyPair API for secure key management. const [payer, mint] = await Promise.all([generateKeyPairSigner(), generateKeyPairSigner()]); // Create instructions — signers are attached directly to instructions. const createAccount = getCreateAccountInstruction({ payer, newAccount: mint, space, lamports, programAddress: TOKEN_PROGRAM_ADDRESS, }); const initializeMint = getInitializeMintInstruction({ mint: mint.address, mintAuthority: address('1234..5678'), decimals: 2, }); // Build the transaction message. const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(payer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([createAccount, initializeMint], tx), ); // Sign — signers are extracted automatically from the transaction message. const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); // Send and confirm. assertIsTransactionWithBlockhashLifetime(signedTransaction); const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); const signature = getSignatureFromTransaction(signedTransaction); await sendAndConfirm(signedTransaction, { commitment: 'confirmed' }); ``` A few things to note about the manual approach: - **Signers**: Kit uses `TransactionSigner` objects (highlighted above) instead of raw keypairs. Since signers are attached to instructions and the fee payer, `signTransactionMessageWithSigners` can sign the transaction automatically without needing to pass signers separately. [Read more about signers here](/docs/advanced-guides/signers). - **Immutability**: Each transaction helper returns a new object with a narrower type, ensuring that required fields (fee payer, lifetime, etc.) are set before the transaction can be signed or sent. The `pipe` function chains these transformations together. - **Signatures**: Transaction signatures are deterministically known before sending. The `getSignatureFromTransaction` helper extracts the signature from the signed transaction, and `sendAndConfirm` does not return it. ## Closing words We've now explored the key differences between Web3.js and Kit, giving you a solid foundation for upgrading your project. Since Kit is a complete rewrite, there's no simple step-by-step migration guide, and the process may take some time. To ease the transition, Kit provides a `@solana/compat` package, which allows for incremental migration. This package includes functions that convert core types — such as public keys and transactions — between Web3.js and Kit. This means you can start integrating Kit without having to refactor your entire project at once. ```ts twoslash title="Kit" import { Keypair, PublicKey } from '@solana/web3.js'; import { createSignerFromKeyPair } from '@solana/kit'; import { fromLegacyKeypair, fromLegacyPublicKey, fromLegacyTransactionInstruction, fromVersionedTransaction, } from '@solana/compat'; // ---cut-start--- const transactionInstruction = null as unknown as Parameters< typeof fromLegacyTransactionInstruction >[0]; const versionedTransaction = null as unknown as Parameters[0]; // ---cut-end--- // Convert `PublicKeys`. const address = fromLegacyPublicKey(new PublicKey('1234..5678')); // Convert `Keypairs`. const cryptoKeypair = await fromLegacyKeypair(Keypair.generate()); const signer = await createSignerFromKeyPair(cryptoKeypair); // Convert `TransactionInstruction`. const instruction = fromLegacyTransactionInstruction(transactionInstruction); // Convert `VersionedTransaction`. const transaction = fromVersionedTransaction(versionedTransaction); ``` If you're using a Kit client, the upgrade is even more straightforward — the client-based API is conceptually closer to Web3.js's `Connection` model while still giving you the benefits of Kit's modular architecture. Check out the [Getting Started](/docs/getting-started) tutorial to see the client-based approach in action or explore [Plugins](/docs/plugins) to learn more about the plugin system. ================================================ FILE: docs/content/home/example-codecs.mdx ================================================ --- title: Codecs Example --- {/* Please keep visible code between 4 to 8 lines of code. */} ```ts const personCodec = getStructCodec([ ['age', getU8Codec()], ['name', addCodecSizePrefix(getUtf8Codec(), getU16Codec())], ]); const bytes = personCodec.encode({ age: 30, name: 'Alice' }); const person = personCodec.decode(bytes); ``` ================================================ FILE: docs/content/home/example-rpc-subscriptions.mdx ================================================ --- title: RPC Subscriptions Example --- {/* Please keep visible code between 4 to 8 lines of code. */} ```ts const rpcSubscriptions = createSolanaRpcSubscriptions('ws://api.mainnet-beta.solana.com'); const accountNotifications = await rpcSubscriptions .accountNotifications(myAccount, { commitment: 'confirmed' }) .subscribe({ abortSignal: abortController.signal }); for await (const accountNotification of accountNotifications) { console.log(accountNotification); } ``` ================================================ FILE: docs/content/home/example-rpc.mdx ================================================ --- title: RPC Example --- {/* Please keep visible code between 4 to 8 lines of code. */} ```ts const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com'); const { value: balance } = await rpc.getBalance(myWallet).send(); const { value: accountInfo } = await rpc.getAccountInfo(myAccount).send(); const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); ``` ================================================ FILE: docs/content/home/example-signers.mdx ================================================ --- title: Signers Example --- {/* Please keep visible code between 4 to 8 lines of code. */} ```ts const keypairSigner = await generateKeyPairSigner(); const walletSigner = useWalletAccountTransactionSigner(wallet, 'solana:mainnet-beta'); const noopSigner = createNoopSigner(myAddress); ``` ================================================ FILE: docs/content/home/example-solana-programs.mdx ================================================ --- title: Solana Programs Example --- {/* Please keep visible code between 4 to 8 lines of code. */} ```ts import { getTransferSolInstruction } from '@solana-program/system'; const transferSol = getTransferSolInstruction({ source, destination, amount }); import { fetchMint } from '@solana-program/token'; const mintAccount = await fetchMint(rpc, mintAddress); ``` ================================================ FILE: docs/content/home/example-transactions.mdx ================================================ --- title: Transactions Example --- {/* Please keep visible code between 4 to 8 lines of code. */} ```ts const transactionMessage = pipe( createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageFeePayerSigner(payer, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([createAccountIx, initializeMintIx], tx), ); ``` ================================================ FILE: docs/content/recipes/airdropping-tokens.mdx ================================================ --- title: Airdropping tokens description: Airdrop tokens to many wallets using parallel transaction plans --- This recipe airdrops a freshly minted SPL token to many recipient wallets at once. It demonstrates how to compose an [instruction plan](/docs/guides/sending-multiple-transactions) that creates the mint sequentially first and then mints to every recipient in parallel — letting Kit pack the work into as few transactions as possible and send the parallel batches concurrently. ## Set up a client Install Kit and the plugins you need. ```package-install @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token ``` Compose a client with a generated signer, a local RPC connection, an airdrop to fund the signer, and the Token program plugin. Make sure a local validator is running (`solana-test-validator`) before this code executes. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(100_000_000_000n))) .use(tokenProgram()); ``` The signer is funded with 100 SOL because creating ATAs costs a small amount of rent for each recipient. ## Generate the recipients For the demo, generate 100 random destination addresses up front. In a real airdrop, this list would come from a database, a CSV, or some other source. ```ts twoslash import { generateKeyPairSigner } from '@solana/kit'; const recipients = await Promise.all( Array.from({ length: 100 }, async () => (await generateKeyPairSigner()).address), ); ``` ## Build the airdrop plan Compose the work as a `sequentialInstructionPlan` whose first child creates the mint and whose second child is a `parallelInstructionPlan` containing one mint-to-ATA plan per recipient. ```ts twoslash import { Address, generateKeyPairSigner, parallelInstructionPlan, sequentialInstructionPlan, } from '@solana/kit'; const recipients = null as unknown as Address[]; // ---cut-start--- import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(100_000_000_000n))) .use(tokenProgram()); // ---cut-end--- const mint = await generateKeyPairSigner(); const instructionPlan = sequentialInstructionPlan([ client.token.instructions.createMint({ newMint: mint, decimals: 6, mintAuthority: client.identity.address, }), parallelInstructionPlan( await Promise.all( recipients.map((owner) => client.token.instructions.mintToATA({ mint: mint.address, owner, mintAuthority: client.identity, amount: 1_000n * 10n ** 6n, // 1,000 tokens decimals: 6, }), ), ), ), ]); ``` The mint must exist before any recipient can receive tokens, which is why the outer plan is sequential. Each `mintToATA` is independent of the others, so wrapping them in a `parallelInstructionPlan` lets Kit run them concurrently. The Token plugin's `mintToATA` is asynchronous because it derives each recipient's ATA address up front, so wrap the whole list in a single `Promise.all`. ## Send and inspect the results `client.sendTransactions(plan)` plans, signs, sends, and confirms every transaction in the tree. Wrap the call in [`passthroughFailedTransactionPlanExecution`](/docs/advanced-guides/errors#walking-transaction-plan-results) so a failure on one transaction does not throw — you keep the full result tree and can decide what to do per recipient. ```ts twoslash import { flattenTransactionPlanResult, InstructionPlan, passthroughFailedTransactionPlanExecution, } from '@solana/kit'; const instructionPlan = null as unknown as InstructionPlan; // ---cut-start--- import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(100_000_000_000n))) .use(tokenProgram()); // ---cut-end--- const result = await passthroughFailedTransactionPlanExecution( client.sendTransactions(instructionPlan), ); for (const [index, single] of flattenTransactionPlanResult(result).entries()) { if (single.status === 'successful') { console.log(`#${index} ✅ ${single.context.signature}`); } else if (single.status === 'failed') { console.error(`#${index} ❌ ${single.error.message}`); } else { console.warn(`#${index} ⏭️ canceled`); } } ``` `flattenTransactionPlanResult` collapses the tree into one entry per transaction in the order they were planned, so iterating the array is enough to log each outcome individually. For a single bottom-line view across the whole airdrop, [`summarizeTransactionPlanResult`](/docs/advanced-guides/errors#walking-transaction-plan-results) returns success/failure/cancellation counts and a top-level `successful` boolean. ## Next steps - [Sending multiple transactions](/docs/guides/sending-multiple-transactions) — the full guide on multi-transaction operations and instruction plan composition. - [Advanced guides — Errors](/docs/advanced-guides/errors) — deep dive on `TransactionPlanResult`, walking failed trees, and program-specific errors. - [Creating a token](/recipes/creating-a-token) — the simpler single-recipient version of this flow. ================================================ FILE: docs/content/recipes/creating-a-token.mdx ================================================ --- title: Creating a token description: Create a new SPL token mint, mint tokens, and transfer them --- This recipe walks through the full lifecycle of an SPL token: creating a mint, minting tokens to your own associated token account (ATA), and transferring some to another wallet. It runs against a local validator so you can experiment without spending mainnet SOL. ## Set up a client Install Kit and the plugins you need. ```package-install @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token ``` Compose a client with a generated signer, a local RPC connection, an airdrop to fund the signer, and the Token program plugin. Make sure a local validator is running (`solana-test-validator`) before this code executes. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))) .use(tokenProgram()); ``` ## Create the mint Generate a fresh signer for the mint account and ask the Token program plugin to build a `createMint` instruction plan. The plan creates the mint account, funds it for rent exemption, and initializes it as a Token mint — all in one atomic operation. ```ts twoslash import { generateKeyPairSigner } from '@solana/kit'; // ---cut-start--- import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))) .use(tokenProgram()); // ---cut-end--- const mint = await generateKeyPairSigner(); await client.token.instructions .createMint({ newMint: mint, decimals: 9, mintAuthority: client.identity.address, }) .sendTransaction(); console.log(`🎉 Mint created: ${mint.address}`); ``` The mint authority is the signer allowed to mint new tokens later. Setting it to `client.identity.address` makes the current client's identity the authority, which is what you want for a recipe like this one. ## Mint tokens to your ATA Tokens live in associated token accounts (ATAs) — one ATA per `(owner, mint)` pair. The plugin's `mintToATA` helper derives the ATA address, creates it if it does not exist yet, and mints tokens to it in a single instruction plan. ```ts twoslash import { Address } from '@solana/kit'; const mintAddress = null as unknown as Address; // ---cut-start--- import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))) .use(tokenProgram()); // ---cut-end--- await client.token.instructions .mintToATA({ mint: mintAddress, owner: client.identity.address, mintAuthority: client.identity, amount: 1_000n * 10n ** 9n, // 1,000 tokens with 9 decimals decimals: 9, }) .sendTransaction(); ``` `mintToATA` is asynchronous because it derives the ATA address before building the plan, but the `.sendTransaction()` shortcut is exposed on the returned promise itself — a single `await` on the call is enough. Pass the same `decimals` you used when creating the mint; the plugin uses it as a safety check to make sure you are minting against the mint you think you are. ## Transfer tokens to a recipient Sending tokens to another wallet follows the same pattern as minting: the plugin derives both the source and destination ATAs, creates the destination if it doesn't exist, and issues the transfer. ```ts twoslash import { address, Address } from '@solana/kit'; const mintAddress = null as unknown as Address; // ---cut-start--- import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { tokenProgram } from '@solana-program/token'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))) .use(tokenProgram()); // ---cut-end--- const recipient = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); await client.token.instructions .transferToATA({ mint: mintAddress, authority: client.identity, recipient, amount: 100n * 10n ** 9n, // 100 tokens decimals: 9, }) .sendTransaction(); ``` The `authority` is the owner of the source tokens, which here is the same identity that minted them. The plugin pulls tokens out of `authority`'s ATA for the given mint, creates the recipient's ATA if needed, and deposits the tokens. ## Next steps - [Using program plugins](/docs/guides/using-program-plugins) — explore everything `client.token` and other program namespaces expose. - [Fetching accounts](/docs/guides/fetching-accounts) — read the mint and token account balances back from the chain. - [Airdropping tokens](/recipes/airdropping-tokens) — extend this recipe to many recipients in parallel. ================================================ FILE: docs/content/recipes/index.mdx ================================================ --- title: Recipes description: Quick how-to guides for common Solana tasks --- Recipes are short, copy-pasteable walkthroughs for specific tasks. Each one starts with a working setup, then walks through the steps you need to ship a feature — sending SOL, creating a token, airdropping tokens to many wallets, and so on. If you are new to Kit, start with the [Getting started](/docs/getting-started) tutorial first. If you are looking for deeper, reference-style coverage, the [Guides](/docs/guides) section is the next step. Recipes sit between the two: focused, end-to-end snippets you can drop into a project today. ================================================ FILE: docs/content/recipes/meta.json ================================================ { "title": "Recipes", "root": true, "pages": ["...", "transferring-sol", "creating-a-token", "airdropping-tokens"] } ================================================ FILE: docs/content/recipes/transferring-sol.mdx ================================================ --- title: Transferring SOL description: Send SOL from one account to another with error handling --- This recipe walks through sending SOL from your client's payer to another address. It uses a local validator so you can run it end-to-end without touching real funds, and shows how to surface a useful error when a transfer fails. ## Set up a client Install Kit and the plugins you need. ```package-install @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/system ``` Compose a client with a generated signer, a local RPC connection, an airdrop to fund the signer, and the System program plugin. Make sure a local validator is running (`solana-test-validator`) before this code executes. ```ts twoslash import { createClient, lamports } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))) .use(systemProgram()); ``` ## Send the transfer Use `client.system.instructions.transferSol` to build the transfer instruction, then call `.sendTransaction()` to plan, sign, send, and confirm it in one step. ```ts twoslash import { address, lamports } from '@solana/kit'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))) .use(systemProgram()); // ---cut-end--- const recipient = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); const result = await client.system.instructions .transferSol({ source: client.payer, destination: recipient, amount: lamports(10_000_000n), // 0.01 SOL }) .sendTransaction(); console.log(`✅ ${result.context.signature}`); ``` The shortcut is equivalent to passing the instruction to `client.sendTransaction([...])`. Reach for the array form when you want to combine several instructions into a single atomic transaction. ## Handle errors When a transfer fails, `client.sendTransaction(...)` throws a [`SolanaError`](/docs/advanced-guides/errors) with the `SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION` code. Wrapping the call lets you surface a useful message and inspect the underlying program error. ```ts twoslash import { address, isSolanaError, lamports, SingleTransactionPlanResult, SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION, } from '@solana/kit'; import { getSystemErrorMessage, isSystemError } from '@solana-program/system'; // ---cut-start--- import { createClient } from '@solana/kit'; import { solanaLocalRpc } from '@solana/kit-plugin-rpc'; import { airdropSigner, generatedSigner } from '@solana/kit-plugin-signer'; import { systemProgram } from '@solana-program/system'; const client = await createClient() .use(generatedSigner()) .use(solanaLocalRpc()) .use(airdropSigner(lamports(1_000_000_000n))) .use(systemProgram()); const recipient = address('GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ'); // ---cut-end--- try { await client.system.instructions .transferSol({ source: client.payer, destination: recipient, amount: lamports(10_000_000n), }) .sendTransaction(); } catch (e) { if (!isSolanaError(e, SOLANA_ERROR__FAILED_TO_SEND_TRANSACTION)) throw e; const result = e.context.transactionPlanResult as SingleTransactionPlanResult; const transactionMessage = result.context.message ?? result.plannedMessage; const cause = e.cause as Error; if (isSystemError(cause, transactionMessage)) { console.error(`System program error: ${getSystemErrorMessage(cause.context.code)}`); } else { console.error('Transfer failed:', e.message); } } ``` `error.cause` carries the underlying error, and `error.context.transactionPlanResult` carries the planned transaction message. Pairing them with `isSystemError` and `getSystemErrorMessage` from `@solana-program/system` turns an opaque program error code into a human-readable message — invaluable when something like an underfunded source account causes the transfer to fail. ## Next steps - [Sending transactions](/docs/guides/sending-transactions) — the full guide on single transactions, including planning and configuration. - [Advanced guides — Errors](/docs/advanced-guides/errors) — deep dive on `SolanaError` codes and program-specific error parsing. ================================================ FILE: docs/next.config.mjs ================================================ import { createMDX } from 'fumadocs-mdx/next'; const withMDX = createMDX(); const docsRouteRedirects = [ ['compatible-clients', 'plugins/available-plugins'], ['concepts', 'advanced-guides'], ['concepts/codecs', 'advanced-guides/codecs'], ['concepts/errors', 'advanced-guides/errors'], ['concepts/instruction-plans', 'advanced-guides/instruction-plans'], ['concepts/keypairs', 'advanced-guides/keypairs'], ['concepts/offchain-messages', 'advanced-guides/offchain-messages'], ['concepts/rpc', 'guides/rpc'], ['concepts/rpc-subscriptions', 'guides/rpc-subscriptions'], ['concepts/signers', 'advanced-guides/signers'], ['concepts/transactions', 'advanced-guides/transactions'], ['getting-started/setup', 'advanced-guides/kit-without-a-client'], ['getting-started/signers', 'advanced-guides/kit-without-a-client'], ['getting-started/instructions', 'advanced-guides/kit-without-a-client'], ['getting-started/build-transaction', 'advanced-guides/kit-without-a-client'], ['getting-started/send-transaction', 'advanced-guides/kit-without-a-client'], ['getting-started/fetch-account', 'advanced-guides/kit-without-a-client'], ]; const createDocsRedirects = () => docsRouteRedirects.flatMap(([sourcePath, destinationPath]) => [ { source: `/docs/${sourcePath}`, destination: `/docs/${destinationPath}`, permanent: true, }, { source: `/docs/${sourcePath}.mdx`, destination: `/docs/${destinationPath}.mdx`, permanent: true, }, ]); /** @type {import('next').NextConfig} */ const config = { redirects: async () => createDocsRedirects(), reactStrictMode: true, serverExternalPackages: ['twoslash', 'typescript'], rewrites: async () => { return [ { source: '/api/:path*.mdx', destination: '/llms.mdx/api/:path*', }, { source: '/docs/:path*.mdx', destination: '/llms.mdx/docs/:path*', }, { source: '/recipes/:path*.mdx', destination: '/llms.mdx/recipes/:path*', }, ]; }, }; export default withMDX(config); ================================================ FILE: docs/package.json ================================================ { "name": "@solana/kit-docs", "version": "0.0.0", "private": true, "scripts": { "build": "NODE_OPTIONS='--max-old-space-size=8192' next build", "dev": "NODE_OPTIONS=\"--no-experimental-webstorage\" next dev --turbo", "start": "next start", "postinstall": "fumadocs-mdx", "prebuild": "./build-api-docs.sh", "predev": "./build-api-docs.sh" }, "dependencies": { "@inkeep/cxkit-react": "^0.5.116", "@radix-ui/react-popover": "^1.1.15", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.2.10", "fumadocs-docgen": "^2.1.0", "fumadocs-mdx": "12.0.3", "fumadocs-twoslash": "^3.1.1", "fumadocs-ui": "15.2.10", "lucide-react": "^0.563.0", "next": "^16.1.7", "react": "^19.2.4", "react-dom": "^19.2.4", "tailwind-merge": "^3.4.0", "twoslash": "^0.3.6" }, "devDependencies": { "@solana-program/address-lookup-table": "^0.11.0", "@solana-program/compute-budget": "^0.15.0", "@solana-program/memo": "^0.11.0", "@solana-program/system": "^0.12.0", "@solana-program/token": "^0.12.0", "@solana/compat": "6.8.0", "@solana/kit": "6.8.0", "@solana/kit-plugin-litesvm": "^0.10.0", "@solana/kit-plugin-rpc": "^0.11.0", "@solana/kit-plugin-signer": "^0.10.0", "@solana/react": "6.8.0", "@solana/wallet-account-signer": "6.8.0", "@solana/web3.js": "^1.98.4", "@solana/webcrypto-ed25519-polyfill": "6.8.0", "@wallet-standard/ui": "^1.0.2", "@tailwindcss/postcss": "^4.2.2", "@types/mdx": "^2.0.13", "@types/node": "25.4.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "eslint": "^10", "eslint-config-next": "^16.1.6", "postcss": "^8.5.14", "tailwindcss": "^4.2.1", "typescript": "^5.9.3", "vercel": "^50.22.1" }, "pnpm": { "overrides": { "js-yaml@<3.14.2": "^3.14.2", "@vercel/node>path-to-regexp": "^6.3.0", "@vercel/remix-builder>path-to-regexp": "^6.3.0" } }, "packageManager": "pnpm@10.4.1" } ================================================ FILE: docs/postcss.config.mjs ================================================ export default { plugins: { '@tailwindcss/postcss': {}, }, }; ================================================ FILE: docs/source.config.ts ================================================ import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins'; import { remarkInstall } from 'fumadocs-docgen'; import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; import { transformerTwoslash } from 'fumadocs-twoslash'; export const api = defineDocs({ dir: 'content/api', docs: { postprocess: { includeProcessedMarkdown: true, }, }, }); export const docs = defineDocs({ dir: 'content/docs', docs: { postprocess: { includeProcessedMarkdown: true, }, }, }); export const recipes = defineDocs({ dir: 'content/recipes', docs: { postprocess: { includeProcessedMarkdown: true, }, }, }); export const home = defineDocs({ dir: 'content/home', }); export default defineConfig({ mdxOptions: { rehypeCodeOptions: { langs: [ // FIXME(#403): If a popup itself contains a code fence in any language other than // `ts`, the Shiki highlighter will throw an error that it hasn't loaded that // language. Until we figure this out, preemptively load the `js` language. 'js', ], themes: { dark: 'github-dark', light: 'github-light', }, transformers: [...(rehypeCodeDefaultOptions.transformers ?? []), transformerTwoslash()], }, remarkPlugins: [() => remarkInstall({ persist: { id: 'package-install' } })], }, }); ================================================ FILE: docs/src/app/(home)/layout.tsx ================================================ import type { ReactNode } from 'react'; import { HomeLayout, HomeLayoutProps } from 'fumadocs-ui/layouts/home'; import { baseOptions } from '@/app/layout.config'; export default function Layout({ children }: { children: ReactNode }) { const homeProps: HomeLayoutProps = { ...baseOptions, }; return {children}; } ================================================ FILE: docs/src/app/(home)/page/code.tsx ================================================ 'use client'; import React, { useContext } from 'react'; import { ExampleContext } from './example-context'; import { Play } from 'lucide-react'; type Props = Readonly<{ examples: Record; }>; export default function CodeSection({ examples }: Props) { const { example, progress, paused, resume } = useContext(ExampleContext); return (
{paused && ( )} {examples[example]}
); } ================================================ FILE: docs/src/app/(home)/page/cta.tsx ================================================ import { ThemedImage } from '@/lib/ThemedImage'; import DarkMascotInquisitiveImage from '@/public/mascots/dark-mascot-inquisitive.svg'; import LightMascotInquisitiveImage from '@/public/mascots/light-mascot-inquisitive.svg'; import { Link } from 'fumadocs-core/framework'; export default function CtaSection() { return (

Would you like a tour?

If you'd like to learn more about Kit but don't know where to start, worry not! A guided tour is available to help you get acquainted with the library and its main features. Through this tour, you'll end up setting your environment, creating a token account and fetching its data.

Get started
); } ================================================ FILE: docs/src/app/(home)/page/example-context.tsx ================================================ 'use client'; import { createContext, useEffect, useMemo, useRef, useState } from 'react'; const EXAMPLES = ['rpc', 'codecs', 'signers', 'transactions', 'rpc-subscriptions', 'solana-programs'] as const; const TIMER_INTERVAL = 8000; // 8 seconds export type ExampleKey = (typeof EXAMPLES)[number]; export const ExampleContext = createContext<{ example: ExampleKey; setExample: (example: ExampleKey) => void; progress: number; resume: () => void; paused: boolean; }>({ example: EXAMPLES[0], setExample: () => {}, progress: 0, resume: () => {}, paused: false, }); export function ExampleProvider({ children, defaultExample, }: { children: React.ReactNode; defaultExample?: ExampleKey; }) { const [example, setExample] = useState(defaultExample ?? EXAMPLES[0]); const [progress, setProgress] = useState(0); const [paused, setPaused] = useState(false); const exampleIndex = useMemo(() => EXAMPLES.findIndex(e => e === example), [example]); const startTimeRef = useRef(performance.now()); const rafRef = useRef(null); useEffect(() => { if (!paused) { function tick() { const now = performance.now(); const elapsed = now - startTimeRef.current; const newProgress = Math.min(elapsed / TIMER_INTERVAL, 1); setProgress(newProgress); if (newProgress >= 1) { const nextIndex = (exampleIndex + 1) % EXAMPLES.length; setExample(EXAMPLES[nextIndex]); startTimeRef.current = now; setProgress(0); } rafRef.current = requestAnimationFrame(tick); } rafRef.current = requestAnimationFrame(tick); } return () => { if (rafRef.current !== null) { cancelAnimationFrame(rafRef.current); rafRef.current = null; } }; }, [exampleIndex, paused]); const setExampleAndPause = (newExample: ExampleKey) => { setExample(newExample); setProgress(0); setPaused(true); startTimeRef.current = performance.now(); }; const resume = () => { startTimeRef.current = performance.now(); setPaused(false); }; return ( {children} ); } ================================================ FILE: docs/src/app/(home)/page/features.tsx ================================================ 'use client'; import { ThemedImage } from '@/lib/ThemedImage'; import DarkCodecsImage from '@/public/features/dark-codecs.png'; import DarkRpcSubscriptionsImage from '@/public/features/dark-rpc-subscriptions.png'; import DarkRpcImage from '@/public/features/dark-rpc.png'; import DarkSignersImage from '@/public/features/dark-signers.png'; import DarkSolanaProgramsImage from '@/public/features/dark-solana-programs.png'; import DarkTransactionsImage from '@/public/features/dark-transactions.png'; import LightCodecsImage from '@/public/features/light-codecs.png'; import LightRpcSubscriptionsImage from '@/public/features/light-rpc-subscriptions.png'; import LightRpcImage from '@/public/features/light-rpc.png'; import LightSignersImage from '@/public/features/light-signers.png'; import LightSolanaProgramsImage from '@/public/features/light-solana-programs.png'; import LightTransactionsImage from '@/public/features/light-transactions.png'; import { Link } from 'fumadocs-core/framework'; import { cn } from 'fumadocs-ui/utils/cn'; import { useContext } from 'react'; import { ExampleContext, ExampleKey } from './example-context'; import { StaticImageData } from 'next/image'; type Feature = { key: ExampleKey; label: string; description: string; href: string; image: Readonly<{ dark: StaticImageData; light: StaticImageData; }>; }; const features: Feature[] = [ { key: 'rpc', label: 'RPC', description: 'Configure your Solana RPC and send requests to send transactions or fetch data.', href: '/docs/guides/rpc', image: { dark: DarkRpcImage, light: LightRpcImage, }, }, { key: 'codecs', label: 'Codecs', description: 'Compose codecs together to encode and decode any value to and from a byte array.', href: '/docs/advanced-guides/codecs', image: { dark: DarkCodecsImage, light: LightCodecsImage, }, }, { key: 'signers', label: 'Signers', description: 'Create signer objects to sign transactions and/or messages with your wallets.', href: '/docs/advanced-guides/signers', image: { dark: DarkSignersImage, light: LightSignersImage, }, }, { key: 'transactions', label: 'Transactions', description: 'Gradually build transaction messages before compiling and sending them to the network.', href: '/docs/advanced-guides/transactions', image: { dark: DarkTransactionsImage, light: LightTransactionsImage, }, }, { key: 'rpc-subscriptions', label: 'RPC Subscriptions', description: 'Subscribe to RPC notifications such as account changes, slot updates, and more.', href: '/docs/guides/rpc-subscriptions', image: { dark: DarkRpcSubscriptionsImage, light: LightRpcSubscriptionsImage, }, }, { key: 'solana-programs', label: 'Solana Programs', description: 'Interact with various Solana programs using their Codama-generated SDKs.', href: '/docs/plugins/available-plugins', image: { dark: DarkSolanaProgramsImage, light: LightSolanaProgramsImage, }, }, ]; export default function FeaturesSection() { const { example, setExample } = useContext(ExampleContext); const set = (key: ExampleKey) => () => setExample(key); return (
{features.map(feature => (

{feature.label}

{feature.description}

Learn more
))}
And many more...
); } ================================================ FILE: docs/src/app/(home)/page/footer.tsx ================================================ import { ThemedImage } from '@/lib/ThemedImage'; import DarkFooter from '@/public/footer/dark-footer.svg'; import LightFooter from '@/public/footer/light-footer.svg'; import { Link } from 'fumadocs-core/framework'; export default function FooterSection() { const linkClassName = 'text-linen-600 hover:text-linen-800 dark:text-mauve-300 dark:hover:text-mauve-100'; return (
Documentation API Reference Maintained by Anza
); } ================================================ FILE: docs/src/app/(home)/page/hero.tsx ================================================ import { ThemedImage } from '@/lib/ThemedImage'; import DarkHeader from '@/public/header/dark-header.jpg'; import LightHeader from '@/public/header/light-header.jpg'; import Link from 'fumadocs-core/link'; export default function HeroSection() { return (

Your JavaScript
SDK for Solana

Get started with Kit
); } ================================================ FILE: docs/src/app/(home)/page.tsx ================================================ import { home } from '@/.source'; import React from 'react'; import { mdxComponents } from '../layout.config'; import HeroSection from './page/hero'; import CodeSection from './page/code'; import FeaturesSection from './page/features'; import CtaSection from './page/cta'; import FooterSection from './page/footer'; import { ExampleProvider } from './page/example-context'; function getExamples(): Record { const examples: Record = {}; home.docs.forEach(doc => { const match = doc.info.path.match(/^example-(.*)\.mdx$/); if (match) { const MDXContent = doc.body; examples[match[1]] = ; } }); return examples; } export default function HomePage() { const examples = getExamples(); return (
); } ================================================ FILE: docs/src/app/api/[[...slug]]/page.tsx ================================================ import { mdxComponents } from '@/app/layout.config'; import { LLMCopyButton, ViewOptions } from '@/components/page-actions'; import { apiSource } from '@/lib/source'; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'; import { notFound } from 'next/navigation'; export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { const params = await props.params; const page = apiSource.getPage(params.slug); if (!page) notFound(); const MDX = page.data.body; return ( {page.data.title} {page.data.description}
); } export async function generateStaticParams() { return apiSource.generateParams(); } export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) { const params = await props.params; const page = apiSource.getPage(params.slug); if (!page) notFound(); return { title: page.data.title, description: page.data.description, }; } ================================================ FILE: docs/src/app/api/layout.tsx ================================================ import { DocsLayout, DocsLayoutProps } from 'fumadocs-ui/layouts/docs'; import type { ReactNode } from 'react'; import { baseOptions } from '@/app/layout.config'; import { apiSource } from '@/lib/source'; import { Sidebar } from '../docs/sidebar'; import { Navbar } from '../docs/navbar'; export default function Layout({ children }: { children: ReactNode }) { const baseProps: DocsLayoutProps = { ...baseOptions, tree: apiSource.pageTree, }; const apiProps: DocsLayoutProps = { ...baseProps, sidebar: { ...baseProps.sidebar, component: Sidebar(baseProps) }, nav: { ...baseProps.nav, component: Navbar(baseProps) }, }; return {children}; } ================================================ FILE: docs/src/app/docs/[[...slug]]/page.tsx ================================================ import { mdxComponents } from '@/app/layout.config'; import { LLMCopyButton, ViewOptions } from '@/components/page-actions'; import { overridenMdxComponents } from '@/lib/Overrides'; import { docsSource } from '@/lib/source'; import { getPageTreePeers } from 'fumadocs-core/server'; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'; import { notFound } from 'next/navigation'; export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { const params = await props.params; const page = docsSource.getPage(params.slug); if (!page) notFound(); const MDX = page.data.body; const hasCategory = page.file.name === 'index' && page.slugs.length > 0; return ( {page.data.title} {page.data.description}
{hasCategory && ( {getPageTreePeers(docsSource.pageTree, page.url).map(peer => ( {peer.description} ))} )}
); } export async function generateStaticParams() { return docsSource.generateParams(); } export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) { const params = await props.params; const page = docsSource.getPage(params.slug); if (!page) notFound(); return { title: page.data.title, description: page.data.description, }; } ================================================ FILE: docs/src/app/docs/layout.tsx ================================================ import { baseOptions } from '@/app/layout.config'; import { docsSource } from '@/lib/source'; import { DocsLayout, DocsLayoutProps } from 'fumadocs-ui/layouts/docs'; import type { ReactNode } from 'react'; import { Sidebar } from './sidebar'; import { Navbar } from './navbar'; export default function Layout({ children }: { children: ReactNode }) { const baseProps: DocsLayoutProps = { ...baseOptions, tree: docsSource.pageTree, }; const docsProps: DocsLayoutProps = { ...baseProps, sidebar: { ...baseProps.sidebar, component: Sidebar(baseProps) }, nav: { ...baseProps.nav, component: Navbar(baseProps) }, }; return {children}; } ================================================ FILE: docs/src/app/docs/navbar.tsx ================================================ import Link from 'fumadocs-core/link'; import { SearchToggle } from 'fumadocs-ui/components/layout/search-toggle'; import { buttonVariants } from 'fumadocs-ui/components/ui/button'; import { DocsLayoutProps } from 'fumadocs-ui/layouts/docs'; import { Navbar as BaseNavbar, NavbarSidebarTrigger } from 'fumadocs-ui/layouts/docs-client'; import { cn } from 'fumadocs-ui/utils/cn'; import { Sidebar as SidebarIcon } from 'lucide-react'; import { Logo } from '../logo'; export function Navbar({ nav = {}, sidebar: { enabled: sidebarEnabled = true } = {}, searchToggle = {}, }: DocsLayoutProps) { return ( {searchToggle.enabled !== false && (searchToggle.components?.sm ?? )} {sidebarEnabled && ( )} ); } ================================================ FILE: docs/src/app/docs/sidebar.tsx ================================================ import Link from 'fumadocs-core/link'; import { RootToggle } from 'fumadocs-ui/components/layout/root-toggle'; import { LargeSearchToggle } from 'fumadocs-ui/components/layout/search-toggle'; import { CollapsibleSidebar, SidebarCollapseTrigger, SidebarFooter, SidebarHeader, SidebarPageTree, SidebarViewport, } from 'fumadocs-ui/components/layout/sidebar'; import { ThemeToggle } from 'fumadocs-ui/components/layout/theme-toggle'; import { LanguageToggle } from 'fumadocs-ui/components/layout/language-toggle'; import { buttonVariants } from 'fumadocs-ui/components/ui/button'; import { CollapsibleControl, DocsLayoutProps } from 'fumadocs-ui/layouts/docs'; import { getSidebarTabsFromOptions, SidebarLinkItem } from 'fumadocs-ui/layouts/docs/shared'; import { BaseLinkItem, IconItemType } from 'fumadocs-ui/layouts/links'; import { getLinks } from 'fumadocs-ui/layouts/shared'; import { cn } from 'fumadocs-ui/utils/cn'; import { Languages, SidebarIcon } from 'lucide-react'; import { useMemo } from 'react'; import { LogoWithSolana } from '../logo'; export function Sidebar({ nav = {}, sidebar: { tabs: sidebarTabs, footer: sidebarFooter, banner: sidebarBanner, components: sidebarComponents, ...sidebarProps } = {}, searchToggle = {}, themeSwitch = { enabled: true }, i18n = false, ...props }: DocsLayoutProps) { const tabs = useMemo(() => getSidebarTabsFromOptions(sidebarTabs, props.tree) ?? [], [sidebarTabs, props.tree]); const links = getLinks(props.links ?? [], props.githubUrl); const iconLinks = links.filter((item): item is IconItemType => item.type === 'icon'); const header = (
{searchToggle.enabled !== false && (searchToggle.components?.lg ?? ( ))} {tabs.length > 0 && } {sidebarBanner}
); const viewport = ( {links .filter(v => v.type !== 'icon') .map((item, i, list) => ( ))} ); const footer = (
{iconLinks.map((item, i) => ( {item.icon} ))} {i18n ? ( ) : null} {themeSwitch.enabled !== false && (themeSwitch.component ?? )}
{sidebarFooter}
); return ( <>
{header} {viewport} {footer} ); } ================================================ FILE: docs/src/app/global.css ================================================ @import 'tailwindcss'; /* Fumadocs imports. */ @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; @import 'fumadocs-twoslash/twoslash.css'; @source '../../node_modules/fumadocs-ui/dist/**/*.js'; /* App imports. */ @import '../../src/app/styles/brand.css'; @import '../../src/app/styles/typography.css'; @import '../../src/app/styles/fumadocs-overrides.css'; ================================================ FILE: docs/src/app/layout.config.tsx ================================================ import { CardTab, CardTabs } from '@/components/card-tabs'; import { overridenMdxComponents } from '@/lib/Overrides'; import { Spread } from '@/lib/Spread'; import { Popup, PopupContent, PopupTrigger } from 'fumadocs-twoslash/ui'; import { ImageZoom } from 'fumadocs-ui/components/image-zoom'; import { Step, Steps } from 'fumadocs-ui/components/steps'; import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; import defaultMdxComponents from 'fumadocs-ui/mdx'; import { BookTextIcon, CookingPotIcon, LibraryBigIcon } from 'lucide-react'; import { Logo, LogoWithSolana } from './logo'; import { MDXComponents } from 'mdx/types'; /** * Shared layout configurations * * you can customise layouts individually from: * Home Layout: app/(home)/layout.tsx * Docs Layout: app/docs/layout.tsx */ export const baseOptions: BaseLayoutProps = { nav: { title: (
), }, links: [ { text: 'Documentation', url: '/docs', active: 'nested-url', icon: , }, { text: 'Recipes', url: '/recipes', active: 'nested-url', icon: , }, { text: 'API Reference', url: '/api', active: 'nested-url', icon: , }, ], }; export const mdxComponents: MDXComponents = { ...defaultMdxComponents, ...overridenMdxComponents, img: props => , CardTab, CardTabs, Popup, PopupContent, PopupTrigger, Spread, Step, Steps, }; ================================================ FILE: docs/src/app/layout.tsx ================================================ import './global.css'; import InKeepSearchDialog from '@/lib/InKeepSearchDialog'; import { RootProvider } from 'fumadocs-ui/provider'; import type { ReactNode } from 'react'; export default function Layout({ children }: { children: ReactNode }) { return ( {children} ); } ================================================ FILE: docs/src/app/llms-full.txt/route.ts ================================================ import { apiSource, docsSource, recipesSource } from '@/lib/source'; import { getLLMText } from '@/lib/get-llm-text'; // cached forever export const revalidate = false; export async function GET() { const scan = [...docsSource.getPages(), ...recipesSource.getPages(), ...apiSource.getPages()].map(getLLMText); const scanned = await Promise.all(scan); return new Response(scanned.join('\n\n')); } ================================================ FILE: docs/src/app/llms.mdx/api/[[...slug]]/route.ts ================================================ import { getLLMText } from '@/lib/get-llm-text'; import { apiSource } from '@/lib/source'; import { notFound } from 'next/navigation'; export const revalidate = false; export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/api/[[...slug]]'>) { const { slug } = await params; const page = apiSource.getPage(slug); if (!page) notFound(); return new Response(await getLLMText(page), { headers: { 'Content-Type': 'text/markdown', }, }); } export function generateStaticParams() { return apiSource.generateParams(); } ================================================ FILE: docs/src/app/llms.mdx/docs/[[...slug]]/route.ts ================================================ import { getLLMText } from '@/lib/get-llm-text'; import { docsSource } from '@/lib/source'; import { notFound } from 'next/navigation'; export const revalidate = false; export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) { const { slug } = await params; const page = docsSource.getPage(slug); if (!page) notFound(); return new Response(await getLLMText(page), { headers: { 'Content-Type': 'text/markdown', }, }); } export function generateStaticParams() { return docsSource.generateParams(); } ================================================ FILE: docs/src/app/llms.mdx/recipes/[[...slug]]/route.ts ================================================ import { getLLMText } from '@/lib/get-llm-text'; import { recipesSource } from '@/lib/source'; import { notFound } from 'next/navigation'; export const revalidate = false; export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/recipes/[[...slug]]'>) { const { slug } = await params; const page = recipesSource.getPage(slug); if (!page) notFound(); return new Response(await getLLMText(page), { headers: { 'Content-Type': 'text/markdown', }, }); } export function generateStaticParams() { return recipesSource.generateParams(); } ================================================ FILE: docs/src/app/logo.tsx ================================================ import { cn } from 'fumadocs-ui/utils/cn'; export function LogoWithSolana({ className, solanaClassName, }: React.ComponentProps<'svg'> & { solanaClassName?: string }) { return ( ); } export function Logo({ className }: React.ComponentProps<'svg'>) { return ( ); } ================================================ FILE: docs/src/app/recipes/[[...slug]]/page.tsx ================================================ import { mdxComponents } from '@/app/layout.config'; import { LLMCopyButton, ViewOptions } from '@/components/page-actions'; import { overridenMdxComponents } from '@/lib/Overrides'; import { recipesSource } from '@/lib/source'; import { getPageTreePeers } from 'fumadocs-core/server'; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page'; import { notFound } from 'next/navigation'; export default async function Page(props: { params: Promise<{ slug?: string[] }> }) { const params = await props.params; const page = recipesSource.getPage(params.slug); if (!page) notFound(); const MDX = page.data.body; const hasCategory = page.file.name === 'index' && page.slugs.length > 0; return ( {page.data.title} {page.data.description}
{hasCategory && ( {getPageTreePeers(recipesSource.pageTree, page.url).map(peer => ( {peer.description} ))} )}
); } export async function generateStaticParams() { return recipesSource.generateParams(); } export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) { const params = await props.params; const page = recipesSource.getPage(params.slug); if (!page) notFound(); return { title: page.data.title, description: page.data.description, }; } ================================================ FILE: docs/src/app/recipes/layout.tsx ================================================ import { baseOptions } from '@/app/layout.config'; import { recipesSource } from '@/lib/source'; import { DocsLayout, DocsLayoutProps } from 'fumadocs-ui/layouts/docs'; import type { ReactNode } from 'react'; import { Sidebar } from '../docs/sidebar'; import { Navbar } from '../docs/navbar'; export default function Layout({ children }: { children: ReactNode }) { const baseProps: DocsLayoutProps = { ...baseOptions, tree: recipesSource.pageTree, }; const recipesProps: DocsLayoutProps = { ...baseProps, sidebar: { ...baseProps.sidebar, component: Sidebar(baseProps) }, nav: { ...baseProps.nav, component: Navbar(baseProps) }, }; return {children}; } ================================================ FILE: docs/src/app/styles/brand.css ================================================ @theme { /* ---------*/ /* Colours. */ /* ---------*/ /* Linen scale. */ --color-linen-50: #f9f6f1; --color-linen-100: #f4eee5; /* Brand color. */ --color-linen-200: #eae1da; /* Brand color. */ --color-linen-300: #d6beac; /* Brand color. */ --color-linen-400: #b2927c; /* Brand color. */ --color-linen-500: #8a6b5b; --color-linen-600: #715346; --color-linen-700: #513a30; --color-linen-800: #2f2420; --color-linen-900: #14131d; /* Brand color. */ /* Coral scale. */ --color-coral-50: #fff4ef; --color-coral-100: #ffe3d6; --color-coral-200: #ffc1ac; --color-coral-300: #ffa184; --color-coral-400: #f5814e; /* Brand color. */ --color-coral-500: #d8582b; /* Brand color. */ --color-coral-600: #b94727; --color-coral-700: #933923; --color-coral-800: #6b2a1f; --color-coral-900: #421b1a; /* Mauve scale. */ --color-mauve-50: #fbf9fb; --color-mauve-100: #f5edf3; --color-mauve-200: #e2d8e0; --color-mauve-300: #c6b4c4; --color-mauve-400: #9b8196; /* Brand color. */ --color-mauve-450: #8b79ad; /* Brand color. */ --color-mauve-500: #83617d; /* Brand color. */ --color-mauve-600: #6b4e66; --color-mauve-700: #533b4f; --color-mauve-800: #392938; --color-mauve-900: #1f1721; /* -------*/ /* Fonts. */ /* -------*/ --font-sans: 'corbel', 'sans-serif'; --font-title: 'neulis-neue', 'sans-serif'; /* -------*/ /* Sizes. */ /* -------*/ --spacing-home-container: 1000px; } ================================================ FILE: docs/src/app/styles/fumadocs-overrides.css ================================================ /* -------- */ /* Colours. */ /* -------- */ @theme { --color-fd-background: var(--color-linen-100); --color-fd-foreground: var(--color-linen-900); --color-fd-border: var(--color-linen-300); --color-fd-muted: var(--color-linen-200); --color-fd-muted-foreground: var(--color-linen-500); --color-fd-popover: var(--color-linen-200); --color-fd-popover-foreground: var(--color-linen-900); --color-fd-card: var(--color-linen-200); --color-fd-card-foreground: var(--color-linen-800); --color-fd-primary: var(--color-coral-400); --color-fd-primary-foreground: var(--color-linen-100); --color-fd-secondary: var(--color-mauve-400); --color-fd-secondary-foreground: var(--color-linen-100); --color-fd-accent: var(--color-linen-300); --color-fd-accent-foreground: var(--color-linen-900); } .dark { --color-fd-background: var(--color-mauve-900); --color-fd-foreground: var(--color-mauve-100); --color-fd-border: var(--color-linen-700); --color-fd-muted: var(--color-linen-800); --color-fd-muted-foreground: var(--color-linen-400); --color-fd-popover: var(--color-mauve-900); --color-fd-popover-foreground: var(--color-linen-50); --color-fd-card: var(--color-linen-800); --color-fd-card-foreground: var(--color-linen-200); --color-fd-primary: var(--color-coral-400); --color-fd-primary-foreground: var(--color-linen-100); --color-fd-secondary: var(--color-linen-800); --color-fd-card-foreground: var(--color-linen-200); --color-fd-accent: var(--color-linen-500); --color-fd-accent-foreground: var(--color-linen-300); } /* ------- */ /* Navbar. */ /* ------- */ #nd-nav { --spacing-fd-container: var(--spacing-home-container); --color-fd-secondary: var(--color-fd-background); left: 0; /* overrides: left-1/2 */ max-width: 100%; /* overrides: max-w-fd-container */ translate: none; /* overrides: -translate-x-1/2 */ margin: 0; /* overrides: lg:mt-2 */ width: 100%; /* overrides: w-full lg:w-[calc(100%-1rem)] */ border: none; /* overrides: lg:border border-b border-fd-foreground/10 */ border-bottom: 1px solid var(--color-fd-border); box-shadow: none; /* overrides: lg:shadow */ border-radius: 0; /* overrides: lg:rounded-2xl */ } #nd-home-layout { @apply pt-14 md:pt-18; /* overrides: pt-14 */ } #nd-nav nav { @apply h-14 md:h-18; /* overrides: h-14 */ } #nd-nav > div:first-child { @apply max-w-fd-container mx-auto; } /* -------- */ /* Sidebar. */ /* -------- */ #nd-sidebar { background-color: var(--color-fd-background); } .dark #nd-sidebar { --color-fd-muted-foreground: var(--color-linen-400); } /* --------------- */ /* Search buttons. */ /* --------------- */ #nd-sidebar button[data-search-full], #nd-nav button[data-search-full] { @apply bg-linen-200 hover:bg-linen-50 dark:bg-mauve-800 dark:hover:bg-mauve-900; } /* ----- */ /* Tabs. */ /* ----- */ .fd-tabs { background-color: inherit; --color-fd-border: var(--color-mauve-400); --color-fd-secondary: var(--color-mauve-400); --color-fd-muted-foreground: var(--color-mauve-200); } .fd-tabs > div[role='tablist'] { --color-fd-primary: var(--color-linen-100); --color-fd-accent-foreground: var(--color-linen-100); } .dark .fd-tabs { --color-fd-border: var(--color-mauve-700); --color-fd-secondary: var(--color-mauve-700); --color-fd-muted-foreground: var(--color-mauve-200); } .dark .fd-tabs > div[role='tablist'] { --color-fd-primary: var(--color-linen-100); --color-fd-accent-foreground: var(--color-linen-100); } /* ----- */ /* Code. */ /* ----- */ .fd-codeblock { --color-fd-muted: var(--color-mauve-400); --color-fd-muted-foreground: var(--color-linen-100); --color-fd-secondary: var(--color-mauve-200); --color-fd-border: var(--color-mauve-400); } .dark .fd-codeblock { --color-fd-muted: var(--color-mauve-700); --color-fd-muted-foreground: var(--color-linen-200); --color-fd-secondary: var(--color-mauve-800); --color-fd-border: var(--color-mauve-700); } #hero-code .fd-codeblock pre { @apply p-4 md:p-6 lg:p-6; } #hero-code .fd-codeblock pre > code { @apply text-sm md:text-base lg:text-base; } /* ------ */ /* Steps. */ /* ------ */ .fd-steps { --color-fd-border: var(--color-linen-300); } .fd-step::before { font-family: var(--font-title); font-size: 0.85rem; --color-fd-secondary: var(--color-linen-400); } .dark .fd-steps { --color-fd-border: var(--color-linen-700); } .dark .fd-step::before { --color-fd-secondary: var(--color-linen-600); } /* ------ */ /* Cards. */ /* ------ */ .fd-card { @apply bg-linen-200 hover:bg-linen-300 dark:bg-linen-800 dark:hover:bg-linen-700; } /* ------------------ */ /* Prev/Next Buttons. */ /* ------------------ */ #nd-page > article > div:last-child > a { @apply bg-linen-100 hover:bg-linen-200 dark:bg-mauve-900 dark:hover:bg-linen-800; } ================================================ FILE: docs/src/app/styles/typography.css ================================================ /* ------ */ /* Fonts. */ /* ------ */ h1, h2, h3 { font-family: var(--font-title); font-weight: 700; } .prose :where(strong):not(:where([class~='not-prose'], [class~='not-prose'] *)) { font-weight: 700; } .prose :where(code):not(:where([class~='not-prose'], [class~='not-prose'] *)) { font-weight: inherit; } /* -------- */ /* Spacing. */ /* -------- */ .prose :where(h2):not(:where([class~='not-prose'], [class~='not-prose'] *, :first-child)) { margin-top: 4em; } .prose :where(h3):not(:where([class~='not-prose'], [class~='not-prose'] *)) { margin-top: 3em; } /* ------- */ /* Tables. */ /* ------- */ .prose :where(table):not(:where([class~='not-prose'], [class~='not-prose'] *)) { background-color: inherit; } ================================================ FILE: docs/src/components/card-tabs.tsx ================================================ 'use client'; import { Tabs, TabsContent, TabsList, TabsTrigger } from 'fumadocs-ui/components/ui/tabs'; import { createContext, useContext, useLayoutEffect, useMemo, useState, type ReactNode } from 'react'; import { cn } from 'fumadocs-ui/utils/cn'; import { FlaskConicalIcon, GlobeIcon, LaptopIcon, PuzzleIcon, type LucideIcon } from 'lucide-react'; const iconMap: Record = { globe: GlobeIcon, laptop: LaptopIcon, 'flask-conical': FlaskConicalIcon, puzzle: PuzzleIcon, }; interface CardDefinition { title: string; description: string; icon?: string; } interface CardTabsProps { cards: CardDefinition[]; groupId?: string; persist?: boolean; children: ReactNode; } function toValue(title: string): string { return title.toLowerCase().replace(/\s+/g, '-'); } const CardTabContext = createContext<{ values: string[]; register: () => number }>({ values: [], register: () => 0, }); /** * Renders a tab list as a grid of selectable cards. * * @param props - Card tab configuration and tab panel children. * @return A card-styled tab group. */ export function CardTabs({ cards, groupId, persist = false, children }: CardTabsProps) { const values = useMemo(() => cards.map(c => toValue(c.title)), [cards]); const [value, setValue] = useState(values[0]); useLayoutEffect(() => { if (!groupId) return; const stored = persist ? localStorage.getItem(groupId) : sessionStorage.getItem(groupId); if (stored && values.includes(stored)) { setValue(stored); } }, [groupId, persist, values]); function handleChange(v: string) { setValue(v); if (groupId) { if (persist) localStorage.setItem(groupId, v); else sessionStorage.setItem(groupId, v); } } // Collection pattern: each CardTab calls register() during render // to get its index. The counter resets on each render cycle. const collectionRef = useMemo(() => ({ current: 0 }), []); collectionRef.current = 0; const ctx = useMemo( () => ({ values, register: () => collectionRef.current++, }), [values, collectionRef], ); return ( {cards.map((card, i) => { const Icon = card.icon ? iconMap[card.icon] : undefined; return (
{Icon && ( )}
{card.title}
{card.description}
); })}
{children}
); } /** * Renders one panel within a {@link CardTabs} group. * * @param props - The tab panel children to render. * @return A tab panel associated with the next card in the parent group. * @throws Throws if there are more `CardTab` children than card definitions. */ export function CardTab({ children }: { children: ReactNode }) { const ctx = useContext(CardTabContext); const index = ctx.register(); const value = ctx.values[index]; if (!value) { throw new Error( `CardTab at index ${index} does not have a matching card definition. ` + `Make sure the number of CardTab children matches the number of cards.`, ); } return ( {children} ); } ================================================ FILE: docs/src/components/page-actions.tsx ================================================ 'use client'; import { useMemo, useState } from 'react'; import { Check, ChevronDown, Copy, ExternalLinkIcon } from 'lucide-react'; import { cn } from '../lib/cn'; import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button'; import { buttonVariants } from './ui/button'; import { Popover, PopoverContent, PopoverTrigger } from './ui/popover'; import { cva } from 'class-variance-authority'; const cache = new Map(); export function LLMCopyButton({ /** * A URL to fetch the raw Markdown/MDX content of page */ markdownUrl, }: { markdownUrl: string; }) { const [isLoading, setLoading] = useState(false); const [checked, onClick] = useCopyButton(async () => { const cached = cache.get(markdownUrl); if (cached) return navigator.clipboard.writeText(cached); setLoading(true); try { await navigator.clipboard.write([ new ClipboardItem({ 'text/plain': fetch(markdownUrl).then(async (res) => { const content = await res.text(); cache.set(markdownUrl, content); return content; }), }), ]); } finally { setLoading(false); } }); return ( ); } const optionVariants = cva( 'text-sm p-2 rounded-lg inline-flex items-center gap-2 hover:text-fd-accent-foreground hover:bg-fd-accent [&_svg]:size-4', ); export function ViewOptions({ markdownUrl, githubUrl, }: { /** * A URL to the raw Markdown/MDX content of page */ markdownUrl: string; /** * Source file URL on GitHub */ githubUrl: string; }) { const items = useMemo(() => { const fullMarkdownUrl = typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading'; const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`; return [ { title: 'Open in GitHub', href: githubUrl, icon: ( GitHub ), }, { title: 'Open in Scira AI', href: `https://scira.ai/?${new URLSearchParams({ q, })}`, icon: ( Scira AI ), }, { title: 'Open in ChatGPT', href: `https://chatgpt.com/?${new URLSearchParams({ hints: 'search', q, })}`, icon: ( OpenAI ), }, { title: 'Open in Claude', href: `https://claude.ai/new?${new URLSearchParams({ q, })}`, icon: ( Anthropic ), }, { title: 'Open in Cursor', icon: ( Cursor ), href: `https://cursor.com/link/prompt?${new URLSearchParams({ text: q, })}`, }, ]; }, [githubUrl, markdownUrl]); return ( Open {items.map((item) => ( {item.icon} {item.title} ))} ); } ================================================ FILE: docs/src/components/ui/button.tsx ================================================ import { cva, type VariantProps } from 'class-variance-authority'; const variants = { primary: 'bg-fd-primary text-fd-primary-foreground hover:bg-fd-primary/80 disabled:bg-fd-secondary disabled:text-fd-secondary-foreground', outline: 'border hover:bg-fd-accent hover:text-fd-accent-foreground', ghost: 'hover:bg-fd-accent hover:text-fd-accent-foreground', secondary: 'border bg-linen-200 text-linen-700 hover:bg-linen-300 dark:bg-fd-secondary dark:text-fd-secondary-foreground dark:hover:bg-fd-accent dark:hover:text-fd-accent-foreground', } as const; export const buttonVariants = cva( 'inline-flex items-center justify-center rounded-md p-2 text-sm font-medium transition-colors duration-100 cursor-pointer disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring', { variants: { variant: variants, // fumadocs use `color` instead of `variant` color: variants, size: { sm: 'gap-1 px-2 py-1.5 text-xs', icon: 'p-1.5 [&_svg]:size-5', 'icon-sm': 'p-1.5 [&_svg]:size-4.5', 'icon-xs': 'p-1 [&_svg]:size-4', }, }, }, ); export type ButtonProps = VariantProps; ================================================ FILE: docs/src/components/ui/popover.tsx ================================================ 'use client'; import * as PopoverPrimitive from '@radix-ui/react-popover'; import * as React from 'react'; import { cn } from '../../lib/cn'; const Popover = PopoverPrimitive.Root; const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverContent = React.forwardRef< React.ComponentRef, React.ComponentPropsWithoutRef >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( )); PopoverContent.displayName = PopoverPrimitive.Content.displayName; const PopoverClose = PopoverPrimitive.PopoverClose; export { Popover, PopoverTrigger, PopoverContent, PopoverClose }; ================================================ FILE: docs/src/lib/InKeepSearchDialog.tsx ================================================ 'use client'; import { type InkeepModalSearchAndChatProps } from '@inkeep/cxkit-react'; import type { SharedProps } from 'fumadocs-ui/components/dialog/search'; import dynamic from 'next/dynamic'; const InkeepModalSearchAndChat = dynamic( () => import('@inkeep/cxkit-react').then(({ InkeepModalSearchAndChat }) => InkeepModalSearchAndChat), { ssr: false }, ); const AI_ASSISTANT_NAME = 'Kit'; export default function InKeepSearchDialog(props: SharedProps) { const { open, onOpenChange } = props; const config: InkeepModalSearchAndChatProps = { baseSettings: { apiKey: /** * Need to use search in development? * * ```shell * pnpm vercel link -p kit-docs -S anza-tech --yes * pnpm vercel env pull --environment=development * ``` */ process.env.NEXT_PUBLIC_INKEEP_SEARCH_API_KEY, colorMode: { sync: { target: globalThis.document?.documentElement, attributes: ['class'], isDarkMode: attributes => !!attributes.class?.includes('dark'), }, }, ...(process.env.NODE_ENV === 'development' ? { env: 'development' } : null), organizationDisplayName: 'Kit', primaryBrandColor: (() => { if (typeof globalThis.getComputedStyle === 'undefined') { return '#ff0000'; // Should never appear. } const rootStyles = getComputedStyle(document.documentElement); return rootStyles.getPropertyValue('--color-coral-400').trim(); })(), privacyPreferences: { optOutAnalyticalCookies: true, }, transformSource(source) { const detectedTabs: string[] = []; if (source.url.startsWith('https://solana.stackexchange.com/')) { detectedTabs.push('Q & A'); } else if (source.contentType === 'documentation') { detectedTabs.push(source.breadcrumbs[0] === 'API' ? 'API' : 'Docs'); } else if (source.breadcrumbs.length === 0 && source.tabs?.includes('GitHub')) { // InKeep does not yet produce breadcrumbs for GitHub results, so we synthesize // them here. const match = source.url.match(/\/(issues|pull)\/(\d+)/); if (match) { const [ // eslint-disable-next-line @typescript-eslint/no-unused-vars _, model, number, ] = match; source.breadcrumbs = [model === 'pull' ? 'Pull Request' : 'Issue', number]; } } return { ...source, tabs: Array.from(new Set([...(source.tabs ?? []), ...detectedTabs])), url: // Strip absolute URLs from the search results because InKeep will not // consider doc site articles as being first-party from the perspective of // the main domain in all environments (eg. development, staging). [/^https:\/\/[\w-]+\.vercel\.app\//, /^https:\/\/solanakit\.com\//].reduce( (url, regex) => url.replace(regex, '/'), source.url, ), }; }, }, modalSettings: { isOpen: open, onOpenChange, }, searchSettings: { debounceTimeMs: 250, tabs: ['All', ['API', { isAlwaysVisible: true }], ['Docs', { isAlwaysVisible: true }], 'Q & A', 'GitHub'], }, aiChatSettings: { aiAssistantAvatar: { dark: '/mascots/dark-mascot-3-4.svg', light: '/mascots/light-mascot-3-4.svg', }, aiAssistantName: AI_ASSISTANT_NAME, conversationVisibility: 'private', disclaimerSettings: { isEnabled: true, label: 'Disclaimer', tooltip: 'These AI-generated responses may be inaccurate or incomplete.', }, exampleQuestions: [ 'I have a 64-byte secret key. Can I use it to sign messages and transactions with Kit?', 'Show me how to subscribe for updates to the data of an account', 'How can I fetch all transaction in a given block?', ], introMessage: `Hi!\n\nI'm an AI assistant trained on the docs and API reference on this site.\n\nAsk me anything about \`${AI_ASSISTANT_NAME}\`.`, placeholder: 'Ask me a question', }, }; return ; } ================================================ FILE: docs/src/lib/Overrides.tsx ================================================ import { Card as BaseCard, Cards as BaseCards } from 'fumadocs-ui/components/card'; import { Tab as BaseTab, Tabs as BaseTabs } from 'fumadocs-ui/components/tabs'; import { Callout as BaseCallout } from 'fumadocs-ui/components/callout'; export const Callout = (props => ) as typeof BaseCallout; export const Card: typeof BaseCard = props => ; export const Cards: typeof BaseCards = BaseCards; export const Tab: typeof BaseTab = BaseTab; export const Tabs: typeof BaseTabs = props => ; export const overridenMdxComponents = { Callout, Card, Cards, Tab, Tabs }; ================================================ FILE: docs/src/lib/Spread.tsx ================================================ import { ReactNode } from 'react'; export function Spread({ children }: { children: ReactNode }) { return (
{children}
); } ================================================ FILE: docs/src/lib/ThemedImage.tsx ================================================ import { cn } from 'fumadocs-ui/utils/cn'; import Image, { ImageProps, StaticImageData } from 'next/image'; export function ThemedImage({ alt, className, src, ...props }: Omit & { src: Readonly<{ dark: StaticImageData; light: StaticImageData; }>; }) { const { // eslint-disable-next-line @typescript-eslint/no-unused-vars blurWidth: _1, // eslint-disable-next-line @typescript-eslint/no-unused-vars blurHeight: _2, ...darkSrc } = src.dark; const { // eslint-disable-next-line @typescript-eslint/no-unused-vars blurWidth: _3, // eslint-disable-next-line @typescript-eslint/no-unused-vars blurHeight: _4, ...lightSrc } = src.light; return ( <> {alt} {alt} ); } ================================================ FILE: docs/src/lib/cn.ts ================================================ export { twMerge as cn } from 'tailwind-merge'; ================================================ FILE: docs/src/lib/get-llm-text.ts ================================================ import { apiSource, docsSource } from '@/lib/source'; import type { InferPageType } from 'fumadocs-core/source'; export async function getLLMText(page: InferPageType) { const processed = await page.data.getText('processed'); return `# ${page.data.title} (${page.url}) ${processed}`; } ================================================ FILE: docs/src/lib/source.ts ================================================ import { api, docs, recipes } from '@/.source'; import { loader } from 'fumadocs-core/source'; export const apiSource = loader({ baseUrl: '/api', source: api.toFumadocsSource(), }); export const docsSource = loader({ baseUrl: '/docs', source: docs.toFumadocsSource(), }); export const recipesSource = loader({ baseUrl: '/recipes', source: recipes.toFumadocsSource(), }); ================================================ FILE: docs/tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", "incremental": true, "paths": { "@/.source": ["./.source/index.ts"], "@/public/*": ["./public/*"], "@/*": ["./src/*"] }, "plugins": [ { "name": "next" } ] }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"], "exclude": ["node_modules"] } ================================================ FILE: eslint.config.mjs ================================================ export { default } from '@solana/eslint-config/eslint.config.mjs'; ================================================ FILE: examples/README.md ================================================ # Examples ## One-time setup Start by installing all dependencies. ```shell pnpm install ``` Most examples use a local test Solana validator. Install one by running the following command in the root of this monorepo. ```shell pnpm test:setup ``` ## Run an example In the directory of a given example, run its start script. ```shell pnpm start ``` ================================================ FILE: examples/deserialize-transaction/LICENSE ================================================ Copyright (c) 2023 Solana Labs, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: examples/deserialize-transaction/package.json ================================================ { "name": "@solana/example-deserialize-transaction", "private": true, "type": "module", "scripts": { "prestart": "turbo --output-logs=errors-only compile:js compile:typedefs", "run:example": "tsx src/example.ts", "start": "start-server-and-test '../../scripts/start-shared-test-validator.sh' http://127.0.0.1:8899/health run:example", "style:fix": "pnpm eslint --fix src && pnpm prettier --log-level warn --ignore-unknown --write ./*", "test:lint": "TERM_OVERRIDE=\"${TURBO_HASH:+dumb}\" TERM=${TERM_OVERRIDE:-$TERM} jest -c ../../node_modules/@solana/test-config/jest-lint.config.js --rootDir . --silent --testMatch 'src/**/*.{ts,tsx}'", "test:prettier": "TERM_OVERRIDE=\"${TURBO_HASH:+dumb}\" TERM=${TERM_OVERRIDE:-$TERM} jest -c ../../node_modules/@solana/test-config/jest-prettier.config.js --rootDir . --silent", "test:typecheck": "tsc" }, "dependencies": { "@solana-program/address-lookup-table": "^0.11.0", "@solana-program/memo": "^0.11.0", "@solana-program/system": "^0.12.0", "@solana/example-utils": "workspace:*", "@solana/kit": "workspace:*" }, "devDependencies": { "start-server-and-test": "^2.1.5", "tsx": "^4.21.0" } } ================================================ FILE: examples/deserialize-transaction/src/example.ts ================================================ /** * EXAMPLE * Deserialize and inspect a transaction with @solana/kit * * Before running any of the examples in this monorepo, make sure to set up a test validator by * running `pnpm test:setup` in the root directory. * * To run this example, execute `pnpm start` in this directory. */ import { createLogger } from '@solana/example-utils/createLogger.js'; import { Address, appendTransactionMessageInstructions, assertIsInstructionWithAccounts, assertIsInstructionWithData, compressTransactionMessageUsingAddressLookupTables, createKeyPairSignerFromBytes, createSolanaRpc, createTransactionMessage, decompileTransactionMessageFetchingLookupTables, getBase64EncodedWireTransaction, getBase64Encoder, getCompiledTransactionMessageDecoder, getPublicKeyFromAddress, getTransactionDecoder, lamports, partiallySignTransactionMessageWithSigners, pipe, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, verifySignature, } from '@solana/kit'; import { fetchAddressLookupTable } from '@solana-program/address-lookup-table'; import { getAddMemoInstruction, MEMO_PROGRAM_ADDRESS, parseAddMemoInstruction } from '@solana-program/memo'; import { getTransferSolInstruction, identifySystemInstruction, parseTransferSolInstruction, SystemInstruction, } from '@solana-program/system'; const log = createLogger('Deserialize'); /** * SETUP: LOOKUP TABLE ADDRESS * Our fixtures include a lookup table, which we're going to use for this transaction * This is the address of the lookup table account from `scripts/fixtures/example-deserialize-transaction-address-lookup-table.json` */ const LOOKUP_TABLE_ADDRESS = 'DUbh3qSh4Vvxa52LGtCBCcuvAEMh62FLNkupnsBjhrCc' as Address; log.info({ address: LOOKUP_TABLE_ADDRESS }, '[setup] Setting lookup table address'); /** * SETUP: RPC CONNECTION * While in this example we won't send the transaction, we will use the remote procedure call (RPC) server to: * - fetch the blockhash for the transaction lifetime * - fetch the address lookup table used in the transaction * * This example uses your local test validator which must be running before you run this script. */ const rpc = createSolanaRpc('http://127.0.0.1:8899'); /** * SETUP: TRANSACTION LIFETIME * We will fetch the latest blockhash, which we will use as the transaction's lifetime * See `examples/transfer-lamports` for more detail on transaction lifetime */ const { value: latestBlockhash } = await rpc.getLatestBlockhash().send(); log.info(latestBlockhash, '[setup] Got a blockhash'); /** * SETUP: CREATE A TRANSACTION MESSAGE * We create a transaction that sends lamports from a source account to a destination account, * where the destination account is in the lookup table. * We will then serialize this transaction, and then explore how to deserialize and inspect it. * See the `transfer-lamports` example for more detailed documentation on creating, signing and sending a transaction. */ const SOURCE_ACCOUNT_SIGNER = await createKeyPairSignerFromBytes( /** * These are the bytes that we saved at the time this account's key pair was originally * generated. Here, they are inlined into the source code, but you can also imagine them being * loaded from disk or, better yet, read from an environment variable. */ new Uint8Array( // prettier-ignore [2, 194, 94, 194, 31, 15, 34, 248, 159, 9, 59, 156, 194, 152, 79, 148, 81, 17, 63, 53, 245, 175, 37, 0, 134, 90, 111, 236, 245, 160, 3, 50, 196, 59, 123, 60, 59, 151, 65, 255, 27, 247, 241, 230, 52, 54, 143, 136, 108, 160, 7, 128, 4, 14, 232, 119, 234, 61, 47, 158, 9, 241, 48, 140], ), // Address: ED1WqT2hWJLSZtj4TtTdoovmpMrr7zpkUdbfxmcJR1Fq ); log.info({ address: SOURCE_ACCOUNT_SIGNER.address }, '[setup] Loaded key pair for source account'); // This is the first address in the lookup table const DESTINATION_ADDRESS = 'F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn' as Address; log.info({ address: DESTINATION_ADDRESS }, '[setup] Setting destination address'); /** * This is an arbitrary address that we set as the fee payer * In this example we won't send the transaction so it doesn't matter if this account is funded */ const FEE_PAYER_ADDRESS = '9xaf9RQvmr47tcKZ2y8KdpcSn6KyymGU6PZAFC9AKjPd' as Address; log.info({ address: FEE_PAYER_ADDRESS }, '[setup] Setting fee payer address'); const transactionMessage = pipe( createTransactionMessage({ version: 0 }), tx => setTransactionMessageFeePayer(FEE_PAYER_ADDRESS, tx), tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), tx => appendTransactionMessageInstructions( [ getTransferSolInstruction({ amount: lamports(12345678n), destination: DESTINATION_ADDRESS, source: SOURCE_ACCOUNT_SIGNER, }), getAddMemoInstruction({ memo: 'hello from @solana/kit', }), ], tx, ), ); log.info('[setup] Created the transaction message'); /** * SETUP: COMPRESS WITH LOOKUP TABLE * As the destination of our transfer SOL instruction is in our lookup table, we can use the * lookup table to compress the transaction. * * This will modify the accounts referred to in the transaction, so that where possible they * refer to an index of an address lookup table instead of an address directly. This makes the * transaction smaller when it is compiled, particularly if many addresses can use a lookup table. */ // We fetch the JSON parsed representation of the lookup table from the RPC const lookupTableAccount = await fetchAddressLookupTable(rpc, LOOKUP_TABLE_ADDRESS); const transactionMessageWithLookupTables = compressTransactionMessageUsingAddressLookupTables(transactionMessage, { [LOOKUP_TABLE_ADDRESS]: lookupTableAccount.data.addresses, }); log.info(`[setup] Compressed the transaction message using lookup table ${LOOKUP_TABLE_ADDRESS}`); /** * SETUP: PARTIALLY SIGN THE TRANSACTION * A Solana transaction can have multiple signers that must sign before it is sent to the network. * In our case the fee payer and the source account would both need to sign the transaction * We used a signer for the source account, so it will sign it when we call `signTransactionMessageWithSigners` * But we only used an address for the fee payer, and we don't have its private key to sign the transaction * So we will use `partiallySignTransactionMessageWithSigners` to get a transaction that has been signed by * the source account, but not by the fee payer. */ const signedTransaction = await partiallySignTransactionMessageWithSigners(transactionMessageWithLookupTables); log.info(`[setup] Partially signed the transaction with a signature from ${SOURCE_ACCOUNT_SIGNER.address}`); /** * SETUP: ENCODE THE TRANSACTION AS BASE64 * This is a common format to send a transaction between different systems * If you wanted to convert it to a byte array instead, you could do this: * * ```ts * const transactionEncoder = getTransactionEncoder(); * const transactionBytes = transactionEncoder.encode(signedTransaction); * ``` */ const base64EncodedTransaction = getBase64EncodedWireTransaction(signedTransaction); log.info(`[setup] Encoded the transaction as base64: ${base64EncodedTransaction.slice(0, 64)}...`); /** * SETUP COMPLETE * At this point, imagine we've received `base64EncodedTransaction` as input and don't know anything about it * Let's use web3js to decode and inspect it */ /** * STEP 1: DECODE TO TRANSACTION * @solana/kit has encoders/decoders for many Solana data structures and common data formats, * including both base64 strings and our `Transaction` data structure * To convert between these, we first encode to a byte array, and then decode to the * desired data structure. * In our case we first encode the `base64EncodedTransaction` to a byte array, and then * decode to a `Transaction`. * If we had instead received a byte array `transactionBytes` then we would just skip * the base64 encode step */ const base64Encoder = getBase64Encoder(); const transactionBytes = base64Encoder.encode(base64EncodedTransaction); const transactionDecoder = getTransactionDecoder(); const decodedTransaction = transactionDecoder.decode(transactionBytes); log.info('[step 1] Decoded the transaction'); /** * First let's inspect the signatures on our decoded transaction * `signatures` is a map from `Address` to `SignatureBytes | null` * If the address has signed the transaction then we can access their signature * If not then `null` will be stored */ for (const [address, maybeSignature] of Object.entries(decodedTransaction.signatures)) { if (maybeSignature) { log.info(`[step 1] ${address} has signed the transaction`); } else { log.info(`[step 1] ${address} is required to sign the transaction but hasn't yet`); } } // Now we know that we have a signature for `ED1WqT2hWJLSZtj4TtTdoovmpMrr7zpkUdbfxmcJR1Fq` // Let's verify that it's correct const signedByAddress = 'ED1WqT2hWJLSZtj4TtTdoovmpMrr7zpkUdbfxmcJR1Fq' as Address; const signedBySignature = decodedTransaction.signatures[signedByAddress]!; // We create a public Ed25519 key with that address (a `CryptoKey` object with the role `verify`) const signedByPublicKey = await getPublicKeyFromAddress(signedByAddress); // Now we can verify the signature using that key const verifiedSignature = await verifySignature(signedByPublicKey, signedBySignature, decodedTransaction.messageBytes); log.info( `[step 1] The signature for ${signedByAddress} is ${verifiedSignature ? 'valid' : 'not valid'} for the transaction`, ); /** * STEP 2: DECODE TO COMPILED TRANSACTION MESSAGE * We verified the signature is valid for the `messageBytes` field of the Transaction. This is a byte array * representing the compiled version of the `TransactionMessage`. If we decode this we can see some information * about the transaction. Let's do that next. */ // Again we have a decoder to convert from bytes to a Solana data structure, in this case the `CompiledTransactionMessage` const compiledTransactionMessageDecoder = getCompiledTransactionMessageDecoder(); const compiledTransactionMessage = compiledTransactionMessageDecoder.decode(decodedTransaction.messageBytes); // This gives us the data structure `CompiledTransactionMessage`. This is the format that transactions are // compiled before the entire transaction is encoded to base64 to be sent to the Solana network. // Let's inspect some fields of `compiledTransactionMessage` // We can see its version: log.info(`[step 2] The transaction is version ${compiledTransactionMessage.version}`); // We can see the lifetime token, though we don't have enough context yet to know if it's a blockhash or a durable nonce log.info( `[step 2] We can see the transaction lifetime token, but we don't know if it's a blockhash or durable nonce: ${compiledTransactionMessage.lifetimeToken}`, ); // We can see the static accounts: log.info(compiledTransactionMessage.staticAccounts, '[step 2] Static accounts of the transaction'); // Note that the destination address (F1Vc6AGoxXLwGB7QV8f4So3C5d8SXEk3KKGHxKGEJ8qn) is not here, as it comes from the lookup table // The `addressLookupTables` field is only included for v0 transactions // Here we tell Typescript to narrow the type to exclude legacy transactions if (compiledTransactionMessage.version !== 0) { throw new Error('We compiled with version: 0'); } // Now we can view address lookup tables: log.info(compiledTransactionMessage.addressTableLookups, '[step 2] Address lookup tables for the transaction'); // We see that the transaction uses our address lookup table. // Let's look at a compiled instruction log.info( { ...compiledTransactionMessage.instructions[0], data: '(removed for brevity)' }, '[step 2] The first instruction of the compiled transaction message', ); /** * This is the SOL transfer instruction * The program account index is 2, which matches the system program (1111...) in our static accounts * The first account index is 1, which matches the signer we used as the source address * But the second account index is 4, which is outside our array of static accounts. * We need to resolve the lookup table in order to know what address this actually is */ /** * STEP 3: DECOMPILING THE TRANSACTION MESSAGE * Let's decompile the transaction message into a structure that is easier to inspect and parse * To decompile this transaction message, we need to know the addresses in this lookup table * If we already have the addresses in this lookup table fetched, then we can pass them to * `decompileTransactionMessage` without fetching them again. Like so: * * ```ts * const decompiledTransactionMessage = decompileTransactionMessage(compiledTransactionMessage, { * addressesByLookupTableAddress: { * [LOOKUP_TABLE_ADDRESS]: lookupTableAccount.data.addresses, * }, * }); * ``` * * But let's pretend that we don't already have that data fetched, we've just received the * transaction and need to decompile it. We will use `decompileTransactionMessageFetchingLookupTables` * instead * * This will use the RPC to fetch the address lookup table data for us, and then use it to * decompile the transaction message. */ const decompiledTransactionMessage = await decompileTransactionMessageFetchingLookupTables( compiledTransactionMessage, rpc, ); // This is our `TransactionMessage` structure, which is much easier to understand and parse // This is the same data structure that was created before we first signed the transaction // We can see the fee payer: log.info(`[step 3] The transaction fee payer is ${decompiledTransactionMessage.feePayer.address}`); // And the lifetime constraint: log.info(decompiledTransactionMessage.lifetimeConstraint, '[step 3] The transaction lifetime constraint'); /** * Here we can see that the lifetime constraint is actually a blockhash * The `decompileTransactionMessage` call inspects the transaction and can distinguish * between blockhash and nonce for us * We can also narrow this type with typescript */ if ('blockhash' in decompiledTransactionMessage.lifetimeConstraint) { log.info(`[step 3] The transaction blockhash is ${decompiledTransactionMessage.lifetimeConstraint.blockhash}`); } /** * Note that the `lastValidBlockHeight` won't necessarily match that used when the transaction * was first created. This is not encoded into the transaction, so we can't decode it back out * By default `decompileTransactionMessageFetchingLookupTables` and `decompileTransactionMessage` * will set it to U64 MAX. But if you know the correct value, * you can pass it like so: * * ```ts * const decompiledTransactionMessage = await decompileTransactionMessageFetchingLookupTables(compiledTransactionMessage, rpc, { * lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, * }) * ``` */ // We can also view the decompiled instructions here log.info( { ...decompiledTransactionMessage.instructions[0], data: '(removed for brevity)' }, '[step 3] The first decoded instruction', ); /** * We can see the programAddress is `11111111111111111111111111111111` * We no longer need to look this up in a list of accounts from an index * * We also see the address of each account * For the second account, we have its address, as well as the lookup table it comes from * Decompiling the transaction message unpacks all the lookup accounts for us */ // We removed the data array for brevity when we logged the instructions, but let's take a look at it now log.info(decompiledTransactionMessage.instructions[0].data, '[step 3] The data bytes of the first instruction'); // This is opaque, it's just a byte array, but it encodes exactly what the instruction is actually doing // Let's look next at how we can make sense of it /** * STEP 4: PARSING THE INSTRUCTIONS * To understand what is actually happening in each instruction, we need to decode the data field * We will do this by using the generated `@solana-program/system` client, which can decode data * from the @solana/kit instruction data structure for the System program * We know from the program address (11111111111111111111111111111111) that the first instruction * is to the system program * You can generate such a client for any Solana program using Codama * See https://github.com/codama-idl/codama for more information on Codama */ const firstInstruction = decompiledTransactionMessage.instructions[0]; // Narrow the type, the `identifySystemInstruction` requires data to identify an instruction assertIsInstructionWithData(firstInstruction); const identifiedInstruction = identifySystemInstruction(firstInstruction); // We can compare the `identifiedInstruction` to the enum values, like this if (identifiedInstruction === SystemInstruction.TransferSol) { log.info('[step 4] The first instruction calls the TransferSol instruction of the system program'); // Narrow the type again, the instruction must have accounts to be parsed as a transfer SOL instruction assertIsInstructionWithAccounts(firstInstruction); // TODO: This can just be `parseTransferSolInstruction(firstInstruction)` when the client is updated // with the `@solana/kit` version that changes the instruction data type to `ReadonlyUint8Array` const parsedFirstInstruction = parseTransferSolInstruction({ ...firstInstruction, data: firstInstruction.data as unknown as Uint8Array, }); log.info(parsedFirstInstruction, '[step 4] The parsed Transfer SOL instruction'); // This gives us an understanding of what exactly is happening in the instruction // We can see the source address, the destination address, and the amount of lamports const { accounts, data } = parsedFirstInstruction; log.info( `[step 4] In the first instruction, ${accounts.source.address} transfers ${data.amount.toLocaleString()} lamports to ${accounts.destination.address}`, ); } // Now let's do the same with the second instruction const secondInstruction = decompiledTransactionMessage.instructions[1]; log.info(`[step 4] The second instruction calls the ${secondInstruction.programAddress} program`); // We know that the second instruction is to the memo program, but we can also programmatically check this // Each generated client exposes its program address as a constant if (secondInstruction.programAddress === MEMO_PROGRAM_ADDRESS) { log.info(`[step 4] The second instruction calls the memo program`); } // The memo program only has one instruction, so there is no `identify` function // We know it's always an addMemo instruction assertIsInstructionWithData(secondInstruction); // TODO: This can just be `parseAddMemoInstruction(secondInstruction)` when the client is updated // with the `@solana/kit` version that changes the instruction data type to `ReadonlyUint8Array` const parsedSecondInstruction = parseAddMemoInstruction({ ...secondInstruction, data: secondInstruction.data as unknown as Uint8Array, }); log.info(parsedSecondInstruction, '[step 4] The parsed Add Memo instruction'); log.info(`[step 4] The second instruction adds a memo with message "${parsedSecondInstruction.data.memo}"`); // We've now parsed both instructions, and we know exactly what the transaction does ================================================ FILE: examples/deserialize-transaction/tsconfig.json ================================================ { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", "noEmit": true, "target": "ESNext" }, "display": "@solana/example-deserialize-transaction", "extends": "../../packages/tsconfig/base.json", "include": ["src"] } ================================================ FILE: examples/react-app/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: examples/react-app/LICENSE ================================================ Copyright (c) 2023 Solana Labs, Inc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: examples/react-app/README.md ================================================ # @solana/example-react-app This is an example of how to use `@solana/kit` and `@solana/react` to build a React web application. ## Features - Connects to browser wallets that support the Wallet Standard; one or more at a time - Fetches and subscribes to the balance of the selected wallet - Allows you to sign an arbitrary message using a wallet account - Allows you to make a transfer from the selected wallet to any other connected wallet ## Developing Start a server in development mode. ```shell pnpm install pnpm turbo compile:js compile:typedefs pnpm dev ``` Press o + Enter to open the app in a browser. Edits to the source code will automatically reload the app. ## Building for deployment Build a static bundle and HTML for deployment to a webserver. ```shell pnpm install pnpm turbo build ``` The contents of the `dist/` directory can now be uploaded to a webserver. ## Enabling Mainnet-Beta Access to this cluster is typically blocked by [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) rules, so it is disabled in the example app by default. To enable it, start the server or compile the application with the `REACT_EXAMPLE_APP_ENABLE_MAINNET` environment variable set to `"true"`. ```shell REACT_EXAMPLE_APP_ENABLE_MAINNET=true pnpm dev REACT_EXAMPLE_APP_ENABLE_MAINNET=true pnpm build ``` ================================================ FILE: examples/react-app/eslint.config.mjs ================================================ import solanaReactConfig from '@solana/eslint-config/eslint.config.react.mjs'; import reactRefreshPlugin from 'eslint-plugin-react-refresh'; import globals from 'globals'; export default [ { ignores: ['**/dist', '**/*.css'], }, ...solanaReactConfig, { languageOptions: { globals: { ...globals.browser, ...globals.es2020, }, }, plugins: { 'react-refresh': reactRefreshPlugin, }, rules: { '@typescript-eslint/no-misused-promises': 'off', '@typescript-eslint/no-unsafe-argument': 'off', '@typescript-eslint/no-unsafe-assignment': 'off', '@typescript-eslint/restrict-template-expressions': 'error', 'react-refresh/only-export-components': [ 'warn', { allowConstantExport: true, }, ], }, }, ]; ================================================ FILE: examples/react-app/index.html ================================================ Solana React Example App
================================================ FILE: examples/react-app/package.json ================================================ { "name": "@solana/example-react-app", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "compile:js": "vite build", "preview": "vite preview", "style:fix": "pnpm eslint --fix src && pnpm prettier --log-level warn --ignore-unknown --write ./*", "test:lint": "TERM_OVERRIDE=\"${TURBO_HASH:+dumb}\" TERM=${TERM_OVERRIDE:-$TERM} jest -c ../../node_modules/@solana/test-config/jest-lint.config.js --rootDir . --silent --testMatch 'src/**/*.{ts,tsx}'", "test:prettier": "TERM_OVERRIDE=\"${TURBO_HASH:+dumb}\" TERM=${TERM_OVERRIDE:-$TERM} jest -c ../../node_modules/@solana/test-config/jest-prettier.config.js --rootDir . --silent", "test:typecheck": "tsc" }, "dependencies": { "@radix-ui/react-dropdown-menu": "2.1.6", "@radix-ui/react-icons": "1.3.2", "@radix-ui/themes": "3.3.0", "@solana-program/system": "^0.12.0", "@solana/kit": "workspace:*", "@solana/react": "workspace:*", "@wallet-standard/core": "^1.1.1", "@wallet-standard/react": "^1.0.1", "react": "^19.2.6", "react-dom": "^19.2.6", "react-error-boundary": "^5.0.0", "swr": "^2.4.1" }, "devDependencies": { "@solana/eslint-config": "workspace:*", "@solana/wallet-standard-features": "^1.3.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react-swc": "^4.2.3", "eslint": "^9.39.2", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.6.0", "jest": "^30.0.0-alpha.6", "vite": "^8.0.10" } } ================================================ FILE: examples/react-app/src/components/AirdropButton.tsx ================================================ import { Blockquote, Button, Dialog, Flex, Link, Text } from '@radix-ui/themes'; import { Address, airdropFactory, lamports, Rpc, Signature, SolanaRpcApi } from '@solana/kit'; import { useCallback, useContext, useMemo, useRef, useState } from 'react'; import { ChainContext } from '../context/ChainContext'; import { RpcContext } from '../context/RpcContext'; import { ErrorDialog } from './ErrorDialog'; export function AirdropButton({ address }: { address: Address }) { const { current: NO_ERROR } = useRef(Symbol()); const { chain } = useContext(ChainContext); const { rpc, rpcSubscriptions } = useContext(RpcContext); const [error, setError] = useState(NO_ERROR); const [lastSignature, setLastSignature] = useState(); const isMainnet = chain === 'solana:mainnet'; // Cast RPC for airdrop, this is safe because we disable the airdrop button on mainnet const airdrop = useMemo( () => airdropFactory({ rpc: rpc as Rpc, rpcSubscriptions }), [rpc, rpcSubscriptions], ); const [loading, setLoading] = useState(false); const handleAirdrop = useCallback(async () => { try { if (isMainnet) throw new Error('Airdrops are not available on mainnet'); setLoading(true); const signature = await airdrop({ commitment: 'confirmed', lamports: lamports(1_000_000_000n), recipientAddress: address, }); setLastSignature(signature); setError(NO_ERROR); } catch (e) { setError(e); } finally { setLoading(false); } }, [airdrop, address, setLoading, NO_ERROR, isMainnet]); return ( <> { if (!open) { setLastSignature(undefined); } }} > {lastSignature ? ( { e.stopPropagation(); }} > Airdrop successful! Signature:
{lastSignature}
View this transaction {' '} on Explorer
) : null}
{error !== NO_ERROR ? ( setError(NO_ERROR)} title="Airdrop failed" /> ) : null} ); } ================================================ FILE: examples/react-app/src/components/Balance.tsx ================================================ import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { Text, Tooltip } from '@radix-ui/themes'; import { address } from '@solana/kit'; import type { UiWalletAccount } from '@wallet-standard/react'; import { useContext, useMemo } from 'react'; import useSWRSubscription from 'swr/subscription'; import { ChainContext } from '../context/ChainContext'; import { RpcContext } from '../context/RpcContext'; import { getErrorMessage } from '../errors'; import { balanceSubscribe } from '../functions/balance'; import { ErrorDialog } from './ErrorDialog'; type Props = Readonly<{ account: UiWalletAccount; }>; const seenErrors = new WeakSet(); export function Balance({ account }: Props) { const { chain } = useContext(ChainContext); const { rpc, rpcSubscriptions } = useContext(RpcContext); const subscribe = useMemo(() => balanceSubscribe.bind(null, rpc, rpcSubscriptions), [rpc, rpcSubscriptions]); const { data: lamports, error } = useSWRSubscription({ address: address(account.address), chain }, subscribe); if (error && !seenErrors.has(error)) { return ( <> { seenErrors.add(error); }} title="Failed to fetch account balance" /> Could not fetch balance: {getErrorMessage(error, 'Unknown reason')}}> ); } else if (lamports == null) { return ; } else { const formattedSolValue = new Intl.NumberFormat(undefined, { maximumFractionDigits: 5 }).format( // @ts-expect-error This format string is 100% allowed now. `${lamports}E-9`, ); return {`${formattedSolValue} \u25CE`}; } } ================================================ FILE: examples/react-app/src/components/BaseSignMessageFeaturePanel.tsx ================================================ import { Pencil1Icon } from '@radix-ui/react-icons'; import { Blockquote, Box, Button, Code, DataList, Dialog, Flex, TextField } from '@radix-ui/themes'; import { getBase64Decoder } from '@solana/kit'; import type { ReadonlyUint8Array } from '@wallet-standard/core'; import type { SyntheticEvent } from 'react'; import { useRef, useState } from 'react'; import { ErrorDialog } from '../components/ErrorDialog'; type Props = Readonly<{ signMessage(message: ReadonlyUint8Array): Promise; }>; export function BaseSignMessageFeaturePanel({ signMessage }: Props) { const { current: NO_ERROR } = useRef(Symbol()); const [isSigningMessage, setIsSigningMessage] = useState(false); const [error, setError] = useState(NO_ERROR); const [lastSignature, setLastSignature] = useState(); const [text, setText] = useState(); return (
{ e.preventDefault(); setError(NO_ERROR); setIsSigningMessage(true); try { const signature = await signMessage(new TextEncoder().encode(text)); setLastSignature(signature); } catch (e) { setLastSignature(undefined); setError(e); } finally { setIsSigningMessage(false); } }} > ) => setText(e.currentTarget.value)} value={text} > { if (!open) { setLastSignature(undefined); } }} > {lastSignature ? ( { e.stopPropagation(); }} > You Signed a Message! Message
{text}
Signature {getBase64Decoder().decode(lastSignature)}
) : null}
{error !== NO_ERROR ? ( setError(NO_ERROR)} title="Failed to sign message" /> ) : null}
); } ================================================ FILE: examples/react-app/src/components/ConnectWalletMenu.tsx ================================================ import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { Button, Callout, DropdownMenu } from '@radix-ui/themes'; import { useSelectedWalletAccount } from '@solana/react'; import { StandardConnect, StandardDisconnect } from '@wallet-standard/core'; import type { UiWallet } from '@wallet-standard/react'; import { uiWalletAccountBelongsToUiWallet } from '@wallet-standard/react'; import { useRef, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { ConnectWalletMenuItem } from './ConnectWalletMenuItem'; import { ErrorDialog } from './ErrorDialog'; import { UnconnectableWalletMenuItem } from './UnconnectableWalletMenuItem'; import { WalletAccountIcon } from './WalletAccountIcon'; type Props = Readonly<{ children: React.ReactNode; }>; export function ConnectWalletMenu({ children }: Props) { const { current: NO_ERROR } = useRef(Symbol()); const [selectedWalletAccount, setSelectedWalletAccount, wallets] = useSelectedWalletAccount(); const [error, setError] = useState(NO_ERROR); const [forceClose, setForceClose] = useState(false); function renderItem(wallet: UiWallet) { return ( } key={`wallet:${wallet.name}`} > { setSelectedWalletAccount(account); setForceClose(true); }} onDisconnect={wallet => { if (selectedWalletAccount && uiWalletAccountBelongsToUiWallet(selectedWalletAccount, wallet)) { setSelectedWalletAccount(undefined); } }} onError={setError} wallet={wallet} /> ); } const walletsThatSupportStandardConnect = []; const unconnectableWallets = []; for (const wallet of wallets) { if (wallet.features.includes(StandardConnect) && wallet.features.includes(StandardDisconnect)) { walletsThatSupportStandardConnect.push(wallet); } else { unconnectableWallets.push(wallet); } } return ( <> {wallets.length === 0 ? ( This browser has no wallets installed. ) : ( <> {walletsThatSupportStandardConnect.map(renderItem)} {unconnectableWallets.length ? ( <> {unconnectableWallets.map(renderItem)} ) : null} )} {error !== NO_ERROR ? setError(NO_ERROR)} /> : null} ); } ================================================ FILE: examples/react-app/src/components/ConnectWalletMenuItem.tsx ================================================ import { DropdownMenu } from '@radix-ui/themes'; import { useSelectedWalletAccount } from '@solana/react'; import type { UiWallet, UiWalletAccount } from '@wallet-standard/react'; import { uiWalletAccountsAreSame, useConnect, useDisconnect } from '@wallet-standard/react'; import { useCallback } from 'react'; import { WalletMenuItemContent } from './WalletMenuItemContent'; type Props = Readonly<{ onAccountSelect(account: UiWalletAccount | undefined): void; onDisconnect(wallet: UiWallet): void; onError(err: unknown): void; wallet: UiWallet; }>; export function ConnectWalletMenuItem({ onAccountSelect, onDisconnect, onError, wallet }: Props) { const [isConnecting, connect] = useConnect(wallet); const [isDisconnecting, disconnect] = useDisconnect(wallet); const isPending = isConnecting || isDisconnecting; const isConnected = wallet.accounts.length > 0; const [selectedWalletAccount] = useSelectedWalletAccount(); const handleConnectClick = useCallback(async () => { try { const existingAccounts = [...wallet.accounts]; const nextAccounts = await connect(); // Try to choose the first never-before-seen account. for (const nextAccount of nextAccounts) { if (!existingAccounts.some(existingAccount => uiWalletAccountsAreSame(nextAccount, existingAccount))) { onAccountSelect(nextAccount); return; } } // Failing that, choose the first account in the list. if (nextAccounts[0]) { onAccountSelect(nextAccounts[0]); } } catch (e) { onError(e); } }, [connect, onAccountSelect, onError, wallet.accounts]); return ( Accounts {wallet.accounts.map(account => ( { onAccountSelect(account); }} > {account.address.slice(0, 8)}… ))} { e.preventDefault(); await handleConnectClick(); }} > Connect More { e.preventDefault(); try { await disconnect(); onDisconnect(wallet); } catch (e) { onError(e); } }} > Disconnect ); } ================================================ FILE: examples/react-app/src/components/DisconnectButton.tsx ================================================ import { ExclamationTriangleIcon, ExitIcon } from '@radix-ui/react-icons'; import { Button, Tooltip } from '@radix-ui/themes'; import type { UiWallet } from '@wallet-standard/react'; import { useDisconnect } from '@wallet-standard/react'; import { useState } from 'react'; import { NO_ERROR } from '../errors'; type Props = Readonly<{ wallet: UiWallet; }>; export function DisconnectButton({ wallet, ...buttonProps }: Omit, 'color' | 'loading' | 'onClick'> & Props) { const [isDisconnecting, disconnect] = useDisconnect(wallet); const [lastError, setLastError] = useState(NO_ERROR); return ( Error:{' '} {lastError && typeof lastError === 'object' && 'message' in lastError ? lastError.message : String(lastError)} } open={lastError !== NO_ERROR} side="left" > ); } ================================================ FILE: examples/react-app/src/components/ErrorDialog.tsx ================================================ import { AlertDialog, Blockquote, Button, Flex } from '@radix-ui/themes'; import { useState } from 'react'; import { getErrorMessage } from '../errors'; type Props = Readonly<{ error: unknown; onClose?(): false | void; title?: string; }>; export function ErrorDialog({ error, onClose, title }: Props) { const [isOpen, setIsOpen] = useState(true); return ( { if (!open) { if (!onClose || onClose() !== false) { setIsOpen(false); } } }} > {title ?? 'We encountered the following error'}
{getErrorMessage(error, 'Unknown')}
); } ================================================ FILE: examples/react-app/src/components/FeatureNotSupportedCallout.tsx ================================================ import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { Callout } from '@radix-ui/themes'; import React from 'react'; import type { FallbackProps } from 'react-error-boundary'; import { getErrorMessage } from '../errors'; interface Props extends Callout.RootProps, FallbackProps {} export function FeatureNotSupportedCallout({ error, resetErrorBoundary: _, ...rootProps }: Props): React.ReactElement { return ( {getErrorMessage(error, 'This account does not support this feature')} ); } ================================================ FILE: examples/react-app/src/components/FeaturePanel.tsx ================================================ import { DataList } from '@radix-ui/themes'; import React from 'react'; type Props = Readonly<{ children: React.ReactNode; label: React.ReactNode; }>; export function FeaturePanel({ children, label }: Props) { return ( {label} {children} ); } ================================================ FILE: examples/react-app/src/components/Nav.tsx ================================================ import { Badge, Box, DropdownMenu, Flex, Heading } from '@radix-ui/themes'; import { useContext } from 'react'; import { ChainContext } from '../context/ChainContext'; import { ConnectWalletMenu } from './ConnectWalletMenu'; import { SignInMenu } from './SignInMenu'; export function Nav() { const { displayName: currentChainName, chain, setChain } = useContext(ChainContext); const currentChainBadge = ( {currentChainName} ); return ( Solana React App{' '} {setChain ? ( {currentChainBadge} { setChain(value as 'solana:${string}'); }} value={chain} > {process.env.REACT_EXAMPLE_APP_ENABLE_MAINNET === 'true' ? ( Mainnet Beta ) : null} Devnet Testnet ) : ( currentChainBadge )} Connect Wallet Sign In ); } ================================================ FILE: examples/react-app/src/components/SignInMenu.tsx ================================================ import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { Button, Callout, DropdownMenu } from '@radix-ui/themes'; import { useSelectedWalletAccount } from '@solana/react'; import { SolanaSignIn } from '@solana/wallet-standard-features'; import type { UiWallet } from '@wallet-standard/react'; import { useRef, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { ErrorDialog } from './ErrorDialog'; import { SignInMenuItem } from './SignInMenuItem'; import { UnconnectableWalletMenuItem } from './UnconnectableWalletMenuItem'; type Props = Readonly<{ children: React.ReactNode; }>; export function SignInMenu({ children }: Props) { const { current: NO_ERROR } = useRef(Symbol()); const [, setSelectedWalletAccount, wallets] = useSelectedWalletAccount(); const [error, setError] = useState(NO_ERROR); const [forceClose, setForceClose] = useState(false); function renderItem(wallet: UiWallet) { return ( } key={`wallet:${wallet.name}`} > { setSelectedWalletAccount(account); setForceClose(true); }} onError={setError} wallet={wallet} /> ); } const walletsThatSupportSignInWithSolana = []; for (const wallet of wallets) { if (wallet.features.includes(SolanaSignIn)) { walletsThatSupportSignInWithSolana.push(wallet); } } return ( <> {walletsThatSupportSignInWithSolana.length === 0 ? ( This browser has no wallets installed that support{' '} Sign In With Solana . ) : ( walletsThatSupportSignInWithSolana.map(renderItem) )} {error !== NO_ERROR ? setError(NO_ERROR)} /> : null} ); } ================================================ FILE: examples/react-app/src/components/SignInMenuItem.tsx ================================================ import { DropdownMenu } from '@radix-ui/themes'; import { useSignIn } from '@solana/react'; import type { UiWallet, UiWalletAccount } from '@wallet-standard/react'; import React, { useCallback, useState } from 'react'; import { WalletMenuItemContent } from './WalletMenuItemContent'; type Props = Readonly<{ onError(err: unknown): void; onSignIn(account: UiWalletAccount | undefined): void; wallet: UiWallet; }>; export function SignInMenuItem({ onSignIn, onError, wallet }: Props) { const signIn = useSignIn(wallet); const [isSigningIn, setIsSigningIn] = useState(false); const handleSignInClick = useCallback( async (e: React.MouseEvent) => { e.preventDefault(); try { setIsSigningIn(true); try { const { account } = await signIn({ statement: 'You will enjoy being signed in.', }); onSignIn(account); } finally { setIsSigningIn(false); } } catch (e) { onError(e); } }, [signIn, onSignIn, onError], ); return ( ); } ================================================ FILE: examples/react-app/src/components/SlotIndicator.tsx ================================================ import { Link, Text } from '@radix-ui/themes'; import { useContext, useEffect, useState, useSyncExternalStore } from 'react'; import { ChainContext } from '../context/ChainContext'; import { RpcContext } from '../context/RpcContext'; type SlotNotification = Readonly<{ parent: bigint; root: bigint; slot: bigint; }>; type ReactiveStore = { getError(): unknown; getState(): T | undefined; subscribe(callback: () => void): () => void; }; function createNoopStore(error?: unknown): ReactiveStore { return { getError: () => error, getState: () => undefined, subscribe: () => () => { }, }; } const slotFormatter = new Intl.NumberFormat(); export function SlotIndicator() { const { rpcSubscriptions } = useContext(RpcContext); const { chain, solanaExplorerClusterName } = useContext(ChainContext); const [store, setStore] = useState>(createNoopStore); useEffect(() => { const abortController = new AbortController(); rpcSubscriptions .slotNotifications() .reactive({ abortSignal: abortController.signal }) .then(setStore) .catch(e => setStore(createNoopStore(e))); return () => abortController.abort(); }, [rpcSubscriptions, chain]); const slot = useSyncExternalStore(store.subscribe, () => { if (store.getError()) throw store.getError(); return store.getState(); }); if (!slot) { return {'\u2013'}; } return ( {slotFormatter.format(slot.slot)} ); } ================================================ FILE: examples/react-app/src/components/SolanaPartialSignTransactionFeaturePanel.tsx ================================================ import { Blockquote, Box, Button, Dialog, Flex, Link, Select, Text, TextField } from '@radix-ui/themes'; import { Address, address, appendTransactionMessageInstruction, assertIsSendableTransaction, assertIsTransactionWithBlockhashLifetime, createKeyPairFromBytes, createTransactionMessage, getBase58Encoder, getSignatureFromTransaction, getTransactionDecoder, getTransactionEncoder, lamports, pipe, SendableTransaction, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, Signature, SignatureBytes, signTransaction, signTransactionMessageWithSigners, Transaction, TransactionPartialSigner, TransactionWithBlockhashLifetime, } from '@solana/kit'; import { useWalletAccountTransactionSigner } from '@solana/react'; import { getTransferSolInstruction } from '@solana-program/system'; import { ReadonlyUint8Array } from '@wallet-standard/core'; import { getUiWalletAccountStorageKey, type UiWalletAccount, useWallets } from '@wallet-standard/react'; import type { SyntheticEvent } from 'react'; import { useContext, useId, useMemo, useRef, useState } from 'react'; import { useSWRConfig } from 'swr'; import { ChainContext } from '../context/ChainContext'; import { RpcContext } from '../context/RpcContext'; import signerBytes from '../signerBytes.json' with { type: 'json' }; import { AirdropButton } from './AirdropButton'; import { ErrorDialog } from './ErrorDialog'; import { WalletMenuItemContent } from './WalletMenuItemContent'; type Props = Readonly<{ account: UiWalletAccount; }>; function solStringToLamports(solQuantityString: string) { if (Number.isNaN(parseFloat(solQuantityString))) { throw new Error('Could not parse token quantity: ' + String(solQuantityString)); } const formatter = new Intl.NumberFormat('en-US', { useGrouping: false }); const bigIntLamports = BigInt( // @ts-expect-error - scientific notation is supported by `Intl.NumberFormat` but the types are wrong formatter.format(`${solQuantityString}E9`).split('.')[0], ); return lamports(bigIntLamports); } type SignTransactionState = | { kind: 'creating-transaction'; } | { kind: 'inputs-form-active'; } | { kind: 'ready-to-send'; recipientAddress: Address; transaction: SendableTransaction & Transaction & TransactionWithBlockhashLifetime; } | { kind: 'sending-transaction'; }; async function mockApiRequest(serializedTransaction: ReadonlyUint8Array): Promise { const keypair = await createKeyPairFromBytes(new Uint8Array(signerBytes)); const transaction = getTransactionDecoder().decode(serializedTransaction); const signedTransaction = await signTransaction([keypair], transaction); return getBase58Encoder().encode(getSignatureFromTransaction(signedTransaction)) as SignatureBytes; } export function SolanaPartialSignTransactionFeaturePanel({ account }: Props) { const { mutate } = useSWRConfig(); const { current: NO_ERROR } = useRef(Symbol()); const { rpc, rpcSubscriptions } = useContext(RpcContext); const sendAndConfirmTransaction = useMemo( () => sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }), [rpc, rpcSubscriptions], ); const wallets = useWallets(); const [error, setError] = useState(NO_ERROR); const [lastSignature, setLastSignature] = useState(); const [solQuantityString, setSolQuantityString] = useState(''); const [recipientAccountStorageKey, setRecipientAccountStorageKey] = useState(); const recipientAccount = useMemo(() => { if (recipientAccountStorageKey) { for (const wallet of wallets) { for (const account of wallet.accounts) { if (getUiWalletAccountStorageKey(account) === recipientAccountStorageKey) { return account; } } } } }, [recipientAccountStorageKey, wallets]); const { chain: currentChain, solanaExplorerClusterName } = useContext(ChainContext); const transactionSigner = useWalletAccountTransactionSigner(account, currentChain); const lamportsInputId = useId(); const recipientSelectId = useId(); const [signTransactionState, setSignTransactionState] = useState({ kind: 'inputs-form-active', }); const formDisabled = signTransactionState.kind !== 'inputs-form-active'; const formLoading = signTransactionState.kind === 'creating-transaction' || signTransactionState.kind === 'sending-transaction'; const feePayerAddress = address('HWJowarVUwY7ewUMeFCBqwkin9RPmkmfsVPYrUszNHDV'); const transactionEncoder = getTransactionEncoder(); const feePayerSigner: TransactionPartialSigner = { address: feePayerAddress, async signTransactions(transactions) { return await Promise.all( transactions.map(async transaction => { const serializedTransaction = transactionEncoder.encode(transaction); const signatureBytes = await mockApiRequest(serializedTransaction); return { [feePayerAddress]: signatureBytes }; }), ); }, }; async function handleCreateTransaction(event: React.FormEvent) { event.preventDefault(); setError(NO_ERROR); setSignTransactionState({ kind: 'creating-transaction' }); try { const amount = solStringToLamports(solQuantityString); if (!recipientAccount) { throw new Error('The address of the recipient could not be found'); } const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'confirmed' }).send(); const message = pipe( createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayerSigner(feePayerSigner, m), m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), m => appendTransactionMessageInstruction( getTransferSolInstruction({ amount, destination: address(recipientAccount.address), source: transactionSigner, }), m, ), ); const transaction = await signTransactionMessageWithSigners(message); assertIsSendableTransaction(transaction); assertIsTransactionWithBlockhashLifetime(transaction); setSignTransactionState({ kind: 'ready-to-send', recipientAddress: recipientAccount.address as Address, transaction, }); } catch (e) { setLastSignature(undefined); setError(e); setSignTransactionState({ kind: 'inputs-form-active' }); } } async function handleSendTransaction( { recipientAddress, transaction, }: { recipientAddress: Address; transaction: SendableTransaction & Transaction & TransactionWithBlockhashLifetime; }, event: React.FormEvent, ) { event.preventDefault(); setError(NO_ERROR); setSignTransactionState({ kind: 'sending-transaction' }); try { const signature = getSignatureFromTransaction(transaction); await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' }); void mutate({ address: transactionSigner.address, chain: currentChain }); void mutate({ address: recipientAddress, chain: currentChain }); setLastSignature(signature); setSolQuantityString(''); setSignTransactionState({ kind: 'inputs-form-active' }); } catch (e) { setLastSignature(undefined); setError(e); setSignTransactionState({ kind: 'inputs-form-active' }); } } return (
) => setSolQuantityString(e.currentTarget.value) } style={{ width: 'auto' }} type="number" value={solQuantityString} > {'\u25ce'} To Account {wallets.flatMap(wallet => wallet.accounts .filter(({ chains }) => chains.includes(currentChain)) .map(account => { const key = getUiWalletAccountStorageKey(account); return ( {account.address} ); }), )} { if (!open) { setLastSignature(undefined); } }} > {lastSignature ? ( { e.stopPropagation(); }} > You transferred tokens! Signature:
{lastSignature}
View this transaction {' '} on Explorer
) : null}
{error !== NO_ERROR ? ( setError(NO_ERROR)} title="Transfer failed" /> ) : null}
); } ================================================ FILE: examples/react-app/src/components/SolanaSignAndSendTransactionFeaturePanel.tsx ================================================ import { Blockquote, Box, Button, Dialog, Flex, Link, Select, Text, TextField } from '@radix-ui/themes'; import { address, appendTransactionMessageInstruction, assertIsTransactionMessageWithSingleSendingSigner, createTransactionMessage, getBase58Decoder, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signAndSendTransactionMessageWithSigners, } from '@solana/kit'; import { useWalletAccountTransactionSendingSigner } from '@solana/react'; import { getTransferSolInstruction } from '@solana-program/system'; import { getUiWalletAccountStorageKey, type UiWalletAccount, useWallets } from '@wallet-standard/react'; import type { SyntheticEvent } from 'react'; import { useContext, useId, useMemo, useRef, useState } from 'react'; import { useSWRConfig } from 'swr'; import { ChainContext } from '../context/ChainContext'; import { RpcContext } from '../context/RpcContext'; import { ErrorDialog } from './ErrorDialog'; import { WalletMenuItemContent } from './WalletMenuItemContent'; type Props = Readonly<{ account: UiWalletAccount; }>; function solStringToLamports(solQuantityString: string) { if (Number.isNaN(parseFloat(solQuantityString))) { throw new Error('Could not parse token quantity: ' + String(solQuantityString)); } const formatter = new Intl.NumberFormat('en-US', { useGrouping: false }); const bigIntLamports = BigInt( // @ts-expect-error - scientific notation is supported by `Intl.NumberFormat` but the types are wrong formatter.format(`${solQuantityString}E9`).split('.')[0], ); return lamports(bigIntLamports); } export function SolanaSignAndSendTransactionFeaturePanel({ account }: Props) { const { mutate } = useSWRConfig(); const { current: NO_ERROR } = useRef(Symbol()); const { rpc } = useContext(RpcContext); const wallets = useWallets(); const [isSendingTransaction, setIsSendingTransaction] = useState(false); const [error, setError] = useState(NO_ERROR); const [lastSignature, setLastSignature] = useState(); const [solQuantityString, setSolQuantityString] = useState(''); const [recipientAccountStorageKey, setRecipientAccountStorageKey] = useState(); const recipientAccount = useMemo(() => { if (recipientAccountStorageKey) { for (const wallet of wallets) { for (const account of wallet.accounts) { if (getUiWalletAccountStorageKey(account) === recipientAccountStorageKey) { return account; } } } } }, [recipientAccountStorageKey, wallets]); const { chain: currentChain, solanaExplorerClusterName } = useContext(ChainContext); const transactionSendingSigner = useWalletAccountTransactionSendingSigner(account, currentChain); const lamportsInputId = useId(); const recipientSelectId = useId(); return (
{ e.preventDefault(); setError(NO_ERROR); setIsSendingTransaction(true); try { const amount = solStringToLamports(solQuantityString); if (!recipientAccount) { throw new Error('The address of the recipient could not be found'); } const { value: latestBlockhash } = await rpc .getLatestBlockhash({ commitment: 'confirmed' }) .send(); const message = pipe( createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayerSigner(transactionSendingSigner, m), m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), m => appendTransactionMessageInstruction( getTransferSolInstruction({ amount, destination: address(recipientAccount.address), source: transactionSendingSigner, }), m, ), ); assertIsTransactionMessageWithSingleSendingSigner(message); const signature = await signAndSendTransactionMessageWithSigners(message); void mutate({ address: transactionSendingSigner.address, chain: currentChain }); void mutate({ address: recipientAccount.address, chain: currentChain }); setLastSignature(signature); setSolQuantityString(''); } catch (e) { setLastSignature(undefined); setError(e); } finally { setIsSendingTransaction(false); } }} > ) => setSolQuantityString(e.currentTarget.value) } style={{ width: 'auto' }} type="number" value={solQuantityString} > {'\u25ce'} To Account {wallets.flatMap(wallet => wallet.accounts .filter(({ chains }) => chains.includes(currentChain)) .map(account => { const key = getUiWalletAccountStorageKey(account); return ( {account.address} ); }), )} { if (!open) { setLastSignature(undefined); } }} > {lastSignature ? ( { e.stopPropagation(); }} > You transferred tokens! Signature:
{getBase58Decoder().decode(lastSignature)}
View this transaction {' '} on Explorer
) : null}
{error !== NO_ERROR ? ( setError(NO_ERROR)} title="Transfer failed" /> ) : null}
); } ================================================ FILE: examples/react-app/src/components/SolanaSignMessageFeaturePanel.tsx ================================================ import type { Address } from '@solana/kit'; import { useWalletAccountMessageSigner } from '@solana/react'; import type { ReadonlyUint8Array } from '@wallet-standard/core'; import type { UiWalletAccount } from '@wallet-standard/react'; import { useCallback } from 'react'; import { BaseSignMessageFeaturePanel } from './BaseSignMessageFeaturePanel'; type Props = Readonly<{ account: UiWalletAccount; }>; export function SolanaSignMessageFeaturePanel({ account }: Props) { const messageSigner = useWalletAccountMessageSigner(account); const signMessage = useCallback( async (message: ReadonlyUint8Array) => { const [result] = await messageSigner.modifyAndSignMessages([ { content: message as Uint8Array, signatures: {}, }, ]); const signature = result?.signatures[account.address as Address]; if (!signature) { throw new Error(); } return signature as ReadonlyUint8Array; }, [account.address, messageSigner], ); return ; } ================================================ FILE: examples/react-app/src/components/SolanaSignTransactionFeaturePanel.tsx ================================================ import { Blockquote, Box, Button, Dialog, Flex, Link, Select, Text, TextField } from '@radix-ui/themes'; import { Address, address, appendTransactionMessageInstruction, assertIsSendableTransaction, assertIsTransactionWithBlockhashLifetime, createTransactionMessage, getSignatureFromTransaction, lamports, pipe, SendableTransaction, sendAndConfirmTransactionFactory, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, Signature, signTransactionMessageWithSigners, Transaction, TransactionWithBlockhashLifetime, } from '@solana/kit'; import { useWalletAccountTransactionSigner } from '@solana/react'; import { getTransferSolInstruction } from '@solana-program/system'; import { getUiWalletAccountStorageKey, type UiWalletAccount, useWallets } from '@wallet-standard/react'; import type { SyntheticEvent } from 'react'; import { useContext, useId, useMemo, useRef, useState } from 'react'; import { useSWRConfig } from 'swr'; import { ChainContext } from '../context/ChainContext'; import { RpcContext } from '../context/RpcContext'; import { ErrorDialog } from './ErrorDialog'; import { WalletMenuItemContent } from './WalletMenuItemContent'; type Props = Readonly<{ account: UiWalletAccount; }>; function solStringToLamports(solQuantityString: string) { if (Number.isNaN(parseFloat(solQuantityString))) { throw new Error('Could not parse token quantity: ' + String(solQuantityString)); } const formatter = new Intl.NumberFormat('en-US', { useGrouping: false }); const bigIntLamports = BigInt( // @ts-expect-error - scientific notation is supported by `Intl.NumberFormat` but the types are wrong formatter.format(`${solQuantityString}E9`).split('.')[0], ); return lamports(bigIntLamports); } type SignTransactionState = | { kind: 'creating-transaction'; } | { kind: 'inputs-form-active'; } | { kind: 'ready-to-send'; recipientAddress: Address; transaction: SendableTransaction & Transaction & TransactionWithBlockhashLifetime; } | { kind: 'sending-transaction'; }; export function SolanaSignTransactionFeaturePanel({ account }: Props) { const { mutate } = useSWRConfig(); const { current: NO_ERROR } = useRef(Symbol()); const { rpc, rpcSubscriptions } = useContext(RpcContext); const sendAndConfirmTransaction = useMemo( () => sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }), [rpc, rpcSubscriptions], ); const wallets = useWallets(); const [error, setError] = useState(NO_ERROR); const [lastSignature, setLastSignature] = useState(); const [solQuantityString, setSolQuantityString] = useState(''); const [recipientAccountStorageKey, setRecipientAccountStorageKey] = useState(); const recipientAccount = useMemo(() => { if (recipientAccountStorageKey) { for (const wallet of wallets) { for (const account of wallet.accounts) { if (getUiWalletAccountStorageKey(account) === recipientAccountStorageKey) { return account; } } } } }, [recipientAccountStorageKey, wallets]); const { chain: currentChain, solanaExplorerClusterName } = useContext(ChainContext); const transactionSigner = useWalletAccountTransactionSigner(account, currentChain); const lamportsInputId = useId(); const recipientSelectId = useId(); const [signTransactionState, setSignTransactionState] = useState({ kind: 'inputs-form-active', }); const formDisabled = signTransactionState.kind !== 'inputs-form-active'; const formLoading = signTransactionState.kind === 'creating-transaction' || signTransactionState.kind === 'sending-transaction'; async function handleCreateTransaction(event: React.FormEvent) { event.preventDefault(); setError(NO_ERROR); setSignTransactionState({ kind: 'creating-transaction' }); try { const amount = solStringToLamports(solQuantityString); if (!recipientAccount) { throw new Error('The address of the recipient could not be found'); } const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'confirmed' }).send(); const message = pipe( createTransactionMessage({ version: 0 }), m => setTransactionMessageFeePayerSigner(transactionSigner, m), m => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), m => appendTransactionMessageInstruction( getTransferSolInstruction({ amount, destination: address(recipientAccount.address), source: transactionSigner, }), m, ), ); const transaction = await signTransactionMessageWithSigners(message); assertIsSendableTransaction(transaction); assertIsTransactionWithBlockhashLifetime(transaction); setSignTransactionState({ kind: 'ready-to-send', recipientAddress: recipientAccount.address as Address, transaction, }); } catch (e) { setLastSignature(undefined); setError(e); setSignTransactionState({ kind: 'inputs-form-active' }); } } async function handleSendTransaction( { recipientAddress, transaction, }: { recipientAddress: Address; transaction: SendableTransaction & Transaction & TransactionWithBlockhashLifetime; }, event: React.FormEvent, ) { event.preventDefault(); setError(NO_ERROR); setSignTransactionState({ kind: 'sending-transaction' }); try { const signature = getSignatureFromTransaction(transaction); await sendAndConfirmTransaction(transaction, { commitment: 'confirmed' }); void mutate({ address: transactionSigner.address, chain: currentChain }); void mutate({ address: recipientAddress, chain: currentChain }); setLastSignature(signature); setSolQuantityString(''); setSignTransactionState({ kind: 'inputs-form-active' }); } catch (e) { setLastSignature(undefined); setError(e); setSignTransactionState({ kind: 'inputs-form-active' }); } } return (
) => setSolQuantityString(e.currentTarget.value) } style={{ width: 'auto' }} type="number" value={solQuantityString} > {'\u25ce'} To Account {wallets.flatMap(wallet => wallet.accounts .filter(({ chains }) => chains.includes(currentChain)) .map(account => { const key = getUiWalletAccountStorageKey(account); return ( {account.address} ); }), )} { if (!open) { setLastSignature(undefined); } }} > {lastSignature ? ( { e.stopPropagation(); }} > You transferred tokens! Signature:
{lastSignature}
View this transaction {' '} on Explorer
) : null}
{error !== NO_ERROR ? ( setError(NO_ERROR)} title="Transfer failed" /> ) : null}
); } ================================================ FILE: examples/react-app/src/components/UnconnectableWalletMenuItem.tsx ================================================ import { ExclamationTriangleIcon } from '@radix-ui/react-icons'; import { Box, DropdownMenu, Text } from '@radix-ui/themes'; import type { UiWallet } from '@wallet-standard/react'; import { useState } from 'react'; import { ErrorDialog } from './ErrorDialog'; import { WalletMenuItemContent } from './WalletMenuItemContent'; type Props = Readonly<{ error: unknown; wallet: UiWallet; }>; export function UnconnectableWalletMenuItem({ error, wallet }: Props) { const [dialogIsOpen, setDialogIsOpen] = useState(false); return ( <> setDialogIsOpen(true)}> {wallet.name} {dialogIsOpen ? ( setDialogIsOpen(false)} title="Unconnectable wallet" /> ) : null} ); } ================================================ FILE: examples/react-app/src/components/WalletAccountIcon.tsx ================================================ import type { UiWalletAccount } from '@wallet-standard/react'; import { uiWalletAccountBelongsToUiWallet, useWallets } from '@wallet-standard/react'; import React from 'react'; type Props = React.ComponentProps<'img'> & Readonly<{ account: UiWalletAccount; }>; export function WalletAccountIcon({ account, ...imgProps }: Props) { const wallets = useWallets(); let icon; if (account.icon) { icon = account.icon; } else { for (const wallet of wallets) { if (uiWalletAccountBelongsToUiWallet(account, wallet)) { icon = wallet.icon; break; } } } return icon ? : null; } ================================================ FILE: examples/react-app/src/components/WalletMenuItemContent.tsx ================================================ import { Avatar, Flex, Spinner, Text } from '@radix-ui/themes'; import type { UiWallet } from '@wallet-standard/react'; import React from 'react'; type Props = Readonly<{ children?: React.ReactNode; loading?: boolean; wallet: UiWallet; }>; export function WalletMenuItemContent({ children, loading, wallet }: Props) { return ( {wallet.name.slice(0, 1)}} radius="none" src={wallet.icon} style={{ height: 18, width: 18 }} /> {children ?? wallet.name} ); } ================================================ FILE: examples/react-app/src/context/ChainContext.tsx ================================================ import type { ClusterUrl } from '@solana/kit'; import { devnet } from '@solana/kit'; import { createContext } from 'react'; export type ChainContext = Readonly<{ chain: `solana:${string}`; displayName: string; setChain?(chain: `solana:${string}`): void; solanaExplorerClusterName: 'devnet' | 'mainnet-beta' | 'testnet'; solanaRpcSubscriptionsUrl: ClusterUrl; solanaRpcUrl: ClusterUrl; }>; export const DEFAULT_CHAIN_CONFIG = Object.freeze({ chain: 'solana:devnet', displayName: 'Devnet', solanaExplorerClusterName: 'devnet', solanaRpcSubscriptionsUrl: devnet('wss://api.devnet.solana.com'), solanaRpcUrl: devnet('https://api.devnet.solana.com'), }); export const ChainContext = createContext(DEFAULT_CHAIN_CONFIG); ================================================ FILE: examples/react-app/src/context/ChainContextProvider.tsx ================================================ import { mainnet, testnet } from '@solana/kit'; import { useMemo, useState } from 'react'; import { ChainContext, DEFAULT_CHAIN_CONFIG } from './ChainContext'; const STORAGE_KEY = 'solana-example-react-app:selected-chain'; export function ChainContextProvider({ children }: { children: React.ReactNode }) { const [chain, setChain] = useState(() => localStorage.getItem(STORAGE_KEY) ?? 'solana:devnet'); const contextValue = useMemo(() => { switch (chain) { // @ts-expect-error Intentional fall through case 'solana:mainnet': if (process.env.REACT_EXAMPLE_APP_ENABLE_MAINNET === 'true') { return { chain: 'solana:mainnet', displayName: 'Mainnet Beta', solanaExplorerClusterName: 'mainnet-beta', solanaRpcSubscriptionsUrl: mainnet('wss://api.mainnet-beta.solana.com'), solanaRpcUrl: mainnet('https://api.mainnet-beta.solana.com'), }; } // falls through case 'solana:testnet': return { chain: 'solana:testnet', displayName: 'Testnet', solanaExplorerClusterName: 'testnet', solanaRpcSubscriptionsUrl: testnet('wss://api.testnet.solana.com'), solanaRpcUrl: testnet('https://api.testnet.solana.com'), }; case 'solana:devnet': default: if (chain !== 'solana:devnet') { localStorage.removeItem(STORAGE_KEY); console.error(`Unrecognized chain \`${chain}\``); } return DEFAULT_CHAIN_CONFIG; } }, [chain]); return ( ({ ...contextValue, setChain(chain) { localStorage.setItem(STORAGE_KEY, chain); setChain(chain); }, }), [contextValue], )} > {children} ); } ================================================ FILE: examples/react-app/src/context/RpcContext.tsx ================================================ import type { Rpc, RpcSubscriptions, SolanaRpcApiMainnet, SolanaRpcSubscriptionsApi } from '@solana/kit'; import { createSolanaRpc, createSolanaRpcSubscriptions, devnet } from '@solana/kit'; import { createContext } from 'react'; export const RpcContext = createContext<{ rpc: Rpc; // Limit the API to only those methods found on Mainnet (ie. not `requestAirdrop`) rpcSubscriptions: RpcSubscriptions; }>({ rpc: createSolanaRpc(devnet('https://api.devnet.solana.com')), rpcSubscriptions: createSolanaRpcSubscriptions(devnet('wss://api.devnet.solana.com')), }); ================================================ FILE: examples/react-app/src/context/RpcContextProvider.tsx ================================================ import { createSolanaRpc, createSolanaRpcSubscriptions } from '@solana/kit'; import { ReactNode, useContext, useMemo } from 'react'; import { ChainContext } from './ChainContext'; import { RpcContext } from './RpcContext'; type Props = Readonly<{ children: ReactNode; }>; export function RpcContextProvider({ children }: Props) { const { solanaRpcSubscriptionsUrl, solanaRpcUrl } = useContext(ChainContext); return ( ({ rpc: createSolanaRpc(solanaRpcUrl), rpcSubscriptions: createSolanaRpcSubscriptions(solanaRpcSubscriptionsUrl), }), [solanaRpcSubscriptionsUrl, solanaRpcUrl], )} > {children} ); } ================================================ FILE: examples/react-app/src/errors.tsx ================================================ import { Code, Flex, Text } from '@radix-ui/themes'; import { isSolanaError, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE } from '@solana/kit'; import { isWalletStandardError, WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED, WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED, WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED, } from '@wallet-standard/core'; import React from 'react'; export const NO_ERROR = Symbol(); export function getErrorMessage(err: unknown, fallbackMessage: React.ReactNode): React.ReactNode { if (isWalletStandardError(err, WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_FEATURE_UNIMPLEMENTED)) { return ( <> This account does not support the {err.context.featureName} feature ); } else if (isWalletStandardError(err, WALLET_STANDARD_ERROR__FEATURES__WALLET_FEATURE_UNIMPLEMENTED)) { return ( The wallet '{err.context.walletName}' ( {err.context.supportedChains.sort().map((chain, ii, { length }) => ( {chain} {ii === length - 1 ? null : ', '} ))} ) does not support the {err.context.featureName} feature. Features supported:
    {err.context.supportedFeatures.sort().map(featureName => (
  • {featureName}
  • ))}
); } else if (isWalletStandardError(err, WALLET_STANDARD_ERROR__FEATURES__WALLET_ACCOUNT_CHAIN_UNSUPPORTED)) { return ( This account does not support the chain {err.context.chain}. Chains supported:
    {err.context.supportedChains.sort().map(chain => (
  • {chain}
  • ))}
); } else if (isSolanaError(err, SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE)) { return ( {err.message} Transaction logs:
    {Object.entries(err.context.logs ?? []).map(([key, value]) => (
  1. {String(value)}
  2. ))}
); } else if (err && typeof err === 'object' && 'message' in err) { return String(err.message); } return fallbackMessage; } ================================================ FILE: examples/react-app/src/functions/balance.ts ================================================ import { AccountNotificationsApi, Address, createReactiveStoreWithInitialValueAndSlotTracking, GetBalanceApi, Lamports, Rpc, RpcSubscriptions, } from '@solana/kit'; import { SWRSubscription } from 'swr/subscription'; /** * This is an example of a strategy to fetch some account data and to keep it up to date over time. * It's implemented as an SWR subscription function (https://swr.vercel.app/docs/subscription) but * the approach is generalizable. * * It uses {@link createReactiveStoreWithInitialValueAndSlotTracking} to combine an initial RPC fetch with an * ongoing subscription, using slot-based comparison to ensure only the latest value is published. */ export function balanceSubscribe( rpc: Rpc, rpcSubscriptions: RpcSubscriptions, ...subscriptionArgs: Parameters> ) { const [{ address }, { next }] = subscriptionArgs; const abortController = new AbortController(); const store = createReactiveStoreWithInitialValueAndSlotTracking({ abortSignal: abortController.signal, rpcRequest: rpc.getBalance(address, { commitment: 'confirmed' }), rpcSubscriptionRequest: rpcSubscriptions.accountNotifications(address), rpcSubscriptionValueMapper: ({ lamports }) => lamports, rpcValueMapper: lamports => lamports, }); store.subscribe(() => { const error = store.getError(); if (error) { next(error as Error); } else { next(null, store.getState()?.value); } }); return () => { abortController.abort(); }; } ================================================ FILE: examples/react-app/src/hooks/useStable.ts ================================================ /* eslint-disable react-hooks/refs */ import { useRef } from 'react'; const UNRESOLVED = Symbol(); export function useStable(getValue: () => T): T { const ref = useRef(UNRESOLVED); if (ref.current === UNRESOLVED) { ref.current = getValue(); } return ref.current; } ================================================ FILE: examples/react-app/src/index.css ================================================ @import './reset.css'; ================================================ FILE: examples/react-app/src/main.tsx ================================================ import './index.css'; import '@radix-ui/themes/styles.css'; import { Flex, Section, Theme } from '@radix-ui/themes'; import { SelectedWalletAccountContextProvider } from '@solana/react'; import type { UiWallet } from '@wallet-standard/react'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { Nav } from './components/Nav.tsx'; import { ChainContextProvider } from './context/ChainContextProvider.tsx'; import { RpcContextProvider } from './context/RpcContextProvider.tsx'; import Root from './routes/root.tsx'; const STORAGE_KEY = 'solana-wallet-standard-example-react:selected-wallet-and-address'; const stateSync = { deleteSelectedWallet: () => localStorage.removeItem(STORAGE_KEY), getSelectedWallet: () => localStorage.getItem(STORAGE_KEY), storeSelectedWallet: (accountKey: string) => localStorage.setItem(STORAGE_KEY, accountKey), }; const rootNode = document.getElementById('root')!; const root = createRoot(rootNode); root.render( true} stateSync={stateSync}>