Skip to content

Add writeKeyPair and writeKeyPairSigner helpers#1537

Merged
lorisleiva merged 1 commit into
mainfrom
04-09-add_writekeypair_and_writekeypairsigner_helpers
Apr 9, 2026
Merged

Add writeKeyPair and writeKeyPairSigner helpers#1537
lorisleiva merged 1 commit into
mainfrom
04-09-add_writekeypair_and_writekeypairsigner_helpers

Conversation

@lorisleiva
Copy link
Copy Markdown
Member

This PR adds writeKeyPair to @solana/keys and writeKeyPairSigner to @solana/signers, giving callers a one-line way to save an extractable key pair as a 64-byte JSON array that's byte-for-byte compatible with solana-keygen new. Missing parent directories are created automatically, and the resulting file is written with mode 0600 so only the owner can read or write it. Both helpers refuse to overwrite an existing file by default and require the caller to explicitly set unsafelyOverwriteExistingKeyPair: true to replace one — the name is deliberately alarming because overwriting a key pair file permanently destroys the previous key and any assets it controls. Calling either helper in a browser or React Native environment throws SOLANA_ERROR__KEYS__WRITE_KEY_PAIR_UNSUPPORTED_ENVIRONMENT up front.

To keep browser and React Native bundles free of any node:fs/promises or node:path references, filesystem access is routed through a new private @solana/fs-impl package that follows the same pattern as @solana/crypto-impl and friends. Its Node entry re-exports mkdir, writeFile, and dirname; its browser entry provides structurally-identical stubs that throw a new generic SOLANA_ERROR__FS__UNSUPPORTED_ENVIRONMENT error as defense-in-depth beneath the helper-specific guard. The package is added to noExternal in getBaseConfig.ts so it gets inlined at build time.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: aec6308

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 46 packages
Name Type
@solana/keys Minor
@solana/signers Minor
@solana/compat Minor
@solana/instruction-plans Minor
@solana/kit Minor
@solana/offchain-messages Minor
@solana/plugin-interfaces Minor
@solana/react Minor
@solana/rpc-api Minor
@solana/rpc-graphql Minor
@solana/rpc-subscriptions-api Minor
@solana/transaction-confirmation Minor
@solana/transactions Minor
@solana/wallet-account-signer Minor
@solana/program-client-core Minor
@solana/rpc Minor
@solana/sysvars Minor
@solana/rpc-subscriptions Minor
@solana/accounts Minor
@solana/addresses Minor
@solana/assertions Minor
@solana/codecs-core Minor
@solana/codecs-data-structures Minor
@solana/codecs-numbers Minor
@solana/codecs-strings Minor
@solana/codecs Minor
@solana/errors Minor
@solana/fast-stable-stringify Minor
@solana/functional Minor
@solana/instructions Minor
@solana/nominal-types Minor
@solana/options Minor
@solana/plugin-core Minor
@solana/programs Minor
@solana/promises Minor
@solana/rpc-parsed-types Minor
@solana/rpc-spec-types Minor
@solana/rpc-spec Minor
@solana/rpc-subscriptions-channel-websocket Minor
@solana/rpc-subscriptions-spec Minor
@solana/rpc-transformers Minor
@solana/rpc-transport-http Minor
@solana/rpc-types Minor
@solana/subscribable Minor
@solana/transaction-messages Minor
@solana/webcrypto-ed25519-polyfill Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Member Author

lorisleiva commented Apr 9, 2026

@bundlemon
Copy link
Copy Markdown

bundlemon Bot commented Apr 9, 2026

BundleMon

Files added (2)
Status Path Size Limits
fs-impl/dist/index.browser.mjs
+245B -
fs-impl/dist/index.node.mjs
+120B -
Files updated (13)
Status Path Size Limits
keys/dist/index.node.mjs
3.06KB (+283B +9.93%) -
errors/dist/index.browser.mjs
19.59KB (+122B +0.61%) -
errors/dist/index.native.mjs
19.59KB (+121B +0.61%) -
errors/dist/index.node.mjs
19.61KB (+120B +0.6%) -
wallet-account-signer/dist/index.node.mjs
16.62KB (+90B +0.53%) -
wallet-account-signer/dist/index.browser.mjs
16.6KB (+89B +0.53%) -
wallet-account-signer/dist/index.native.mjs
16.6KB (+89B +0.53%) -
@solana/kit production bundle
kit/dist/index.production.min.js
47.43KB (+87B +0.18%) -
keys/dist/index.browser.mjs
2.85KB (+71B +2.49%) -
keys/dist/index.native.mjs
2.85KB (+70B +2.46%) -
signers/dist/index.browser.mjs
3.26KB (+34B +1.03%) -
signers/dist/index.native.mjs
3.26KB (+33B +1%) -
signers/dist/index.node.mjs
3.26KB (+33B +1%) -
Unchanged files (129)
Status Path Size Limits
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.81KB -
rpc-graphql/dist/index.node.mjs
18.81KB -
transaction-messages/dist/index.browser.mjs
11.32KB -
transaction-messages/dist/index.native.mjs
11.32KB -
transaction-messages/dist/index.node.mjs
11.32KB -
instruction-plans/dist/index.browser.mjs
6.58KB -
instruction-plans/dist/index.native.mjs
6.58KB -
instruction-plans/dist/index.node.mjs
6.58KB -
codecs-data-structures/dist/index.browser.mjs
5.04KB -
codecs-data-structures/dist/index.native.mjs
5.03KB -
codecs-data-structures/dist/index.node.mjs
5.03KB -
offchain-messages/dist/index.browser.mjs
4.89KB -
offchain-messages/dist/index.native.mjs
4.89KB -
offchain-messages/dist/index.node.mjs
4.89KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
kit/dist/index.browser.mjs
3.72KB -
kit/dist/index.native.mjs
3.72KB -
kit/dist/index.node.mjs
3.72KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
codecs-core/dist/index.node.mjs
3.62KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
rpc-transformers/dist/index.browser.mjs
3.16KB -
rpc-transformers/dist/index.native.mjs
3.16KB -
rpc-transformers/dist/index.node.mjs
3.16KB -
react/dist/index.browser.mjs
3.09KB -
react/dist/index.native.mjs
3.09KB -
react/dist/index.node.mjs
3.09KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
codecs-strings/dist/index.native.mjs
2.47KB -
transaction-confirmation/dist/index.node.mjs
2.41KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
transaction-confirmation/dist/index.native.mj
s
2.36KB -
transaction-confirmation/dist/index.browser.m
js
2.35KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.21KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.17KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.16KB -
subscribable/dist/index.node.mjs
1.97KB -
rpc/dist/index.node.mjs
1.95KB -
codecs-numbers/dist/index.browser.mjs
1.95KB -
codecs-numbers/dist/index.native.mjs
1.95KB -
codecs-numbers/dist/index.node.mjs
1.94KB -
subscribable/dist/index.native.mjs
1.92KB -
subscribable/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.native.mjs
1.9KB -
rpc/dist/index.native.mjs
1.81KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.72KB -
rpc-types/dist/index.browser.mjs
1.53KB -
rpc-types/dist/index.native.mjs
1.53KB -
rpc-types/dist/index.node.mjs
1.53KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
program-client-core/dist/index.browser.mjs
1.21KB -
program-client-core/dist/index.native.mjs
1.21KB -
program-client-core/dist/index.node.mjs
1.21KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.17KB -
accounts/dist/index.native.mjs
1.17KB -
accounts/dist/index.node.mjs
1.16KB -
rpc-api/dist/index.browser.mjs
976B -
rpc-api/dist/index.native.mjs
975B -
rpc-api/dist/index.node.mjs
973B -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec-types/dist/index.browser.mjs
962B -
rpc-spec-types/dist/index.native.mjs
961B -
rpc-spec-types/dist/index.node.mjs
959B -
rpc-subscriptions-api/dist/index.native.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
869B -
rpc-subscriptions-api/dist/index.browser.mjs
868B -
rpc-spec/dist/index.browser.mjs
852B -
rpc-spec/dist/index.native.mjs
851B -
rpc-spec/dist/index.node.mjs
850B -
promises/dist/index.browser.mjs
799B -
promises/dist/index.native.mjs
798B -
promises/dist/index.node.mjs
797B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
plugin-core/dist/index.browser.mjs
749B -
plugin-core/dist/index.native.mjs
749B -
plugin-core/dist/index.node.mjs
747B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
137B -
codecs/dist/index.native.mjs
136B -
codecs/dist/index.node.mjs
134B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change +1.57KB +0.32%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

Documentation Preview: https://kit-docs-ot4ygze9f-anza-tech.vercel.app

@lorisleiva lorisleiva marked this pull request as ready for review April 9, 2026 13:25
@lorisleiva
Copy link
Copy Markdown
Member Author

@trevor-cortex

Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

This PR adds writeKeyPair (@solana/keys) and writeKeyPairSigner (@solana/signers) — one-line helpers to persist an extractable Ed25519 key pair to disk as a 64-byte JSON array, byte-for-byte compatible with solana-keygen new. A new private @solana/fs-impl package isolates Node filesystem access (following the crypto-impl / event-target-impl pattern), with browser stubs that throw a structured error. The implementation is clean, well-documented, and thoroughly tested.

Key observations

@solana/fs-impl export conditions: edge-light / workerd should use browser stubs

The edge-light and workerd export conditions in packages/fs-impl/package.json currently point to the Node entry (index.node.mjs / index.node.cjs). Edge runtimes like Vercel Edge Functions and Cloudflare Workers don't have node:fs/promises or node:path, so these should resolve to the browser stubs instead — the same way @solana/crypto-impl maps edge-light / workerd to the browser entry for types but still points to Node for runtime (since those runtimes do have crypto.subtle). For fs-impl, however, there's no filesystem in those runtimes, so both the runtime and types should use the browser entry.

This is the only issue I'd flag as needing a fix — the rest of the PR looks great.

Everything else looks solid

  • PKCS#8 slice-at-16 approach — correctly documented and matches the existing createPrivateKeyFromBytes implementation that prepends the same fixed 16-byte header.
  • wx flag for default no-overwrite is the right primitive — lets the OS enforce atomicity.
  • unsafelyOverwriteExistingKeyPair naming is appropriately scary.
  • Error codes are properly registered with context, messages, and the union type.
  • Tests cover the important cases: round-trip, file mode, recursive dir creation, non-extractable keys, overwrite protection, and browser environments.
  • writeKeyPairSigner is a clean thin wrapper that delegates to writeKeyPair.
  • Docs (@see cross-references between createKeyPairFromByteswriteKeyPairwriteKeyPairSigner) are helpful.

Notes for subsequent reviewers

  • Verify that the @solana/fs-impl package is wired into the monorepo's CI (turbo pipeline, lint, typecheck, treeshakability).
  • The PKCS#8 header length (16 bytes) is hard-coded — this is correct for Ed25519 but worth noting in case the codebase ever adds other curve types.
  • @solana/fs-impl is a devDependency of @solana/keys (since it gets inlined via noExternal). Confirm that the published @solana/keys bundle doesn't carry a runtime dependency reference to @solana/fs-impl.

Comment thread packages/fs-impl/package.json Outdated
Comment thread packages/keys/src/write-keypair.ts
@lorisleiva lorisleiva force-pushed the 04-09-add_writekeypair_and_writekeypairsigner_helpers branch from 62090da to d408bf4 Compare April 9, 2026 13:46
@lorisleiva lorisleiva force-pushed the 04-08-add_grindkeypair_and_grindkeypairsigner_for_mining_vanity_addresses branch from ef02578 to 206f03e Compare April 9, 2026 13:46
@lorisleiva lorisleiva force-pushed the 04-09-add_writekeypair_and_writekeypairsigner_helpers branch from d408bf4 to 684df34 Compare April 9, 2026 14:17
@lorisleiva lorisleiva requested a review from trevor-cortex April 9, 2026 14:18
Copy link
Copy Markdown

@trevor-cortex trevor-cortex left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review

All feedback from the previous pass has been addressed. LGTM — submitting as COMMENT since I can't approve without explicit repo access.

Changes since last review

  1. @solana/fs-impl export conditions fixed ✅ — edge-light and workerd now resolve to the browser stubs (index.browser.mjs / .cjs) for both import and require, matching the browser condition. Edge runtimes will no longer pull in node:fs/promises / node:path.

  2. New PKCS#8 length guard in writeKeyPair — after slicing the 16-byte PKCS#8 header, the code now asserts privateKeyBytes.byteLength === 32 and throws SOLANA_ERROR__KEYS__INVALID_PRIVATE_KEY_BYTE_LENGTH otherwise. Good forward-compat defense if WebCrypto ever changes the Ed25519 envelope shape. The inline comment explaining the 16-byte prefix is helpful context for future readers.

  3. README documentation added for both writeKeyPair (in @solana/keys) and writeKeyPairSigner (in @solana/signers). Both snippets correctly show generateKeyPair(true) / generateKeyPairSigner(true) to highlight the extractable requirement, and both call out the overwrite-destroys-funds footgun.

Minor observations (non-blocking)

  • The new PKCS#8 length branch is effectively unreachable today (Ed25519 PKCS#8 envelopes are fixed at 48 bytes → 32 after the slice), so it's correctly omitted from tests — just flagging it for coverage-report readers who might wonder.
  • WriteKeyPairConfig is defined in @solana/keys and re-exported from @solana/signers via export type { WriteKeyPairConfig }. Clean — avoids type drift between the two helpers.

Notes for subsequent reviewers

  • Still worth verifying @solana/fs-impl is wired into CI (turbo pipeline, lint, typecheck, treeshakability scripts are all declared in its package.json, but confirm the root turbo config picks it up).
  • The writeKeyPair round-trip test asserts the written bytes are compatible with createKeyPairFromBytes, which is the key correctness guarantee — good coverage.

@lorisleiva lorisleiva requested a review from mcintyre94 April 9, 2026 14:20
Copy link
Copy Markdown
Member

@mcintyre94 mcintyre94 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, LGTM!

Comment thread packages/errors/src/messages.ts Outdated
@lorisleiva lorisleiva force-pushed the 04-09-add_writekeypair_and_writekeypairsigner_helpers branch from 684df34 to 523a7df Compare April 9, 2026 16:59
Copy link
Copy Markdown
Member Author

lorisleiva commented Apr 9, 2026

Merge activity

  • Apr 9, 9:41 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Apr 9, 9:42 PM UTC: Graphite rebased this pull request as part of a merge.
  • Apr 9, 9:51 PM UTC: @lorisleiva merged this pull request with Graphite.

@lorisleiva lorisleiva changed the base branch from 04-08-add_grindkeypair_and_grindkeypairsigner_for_mining_vanity_addresses to graphite-base/1537 April 9, 2026 21:41
@lorisleiva lorisleiva changed the base branch from graphite-base/1537 to main April 9, 2026 21:41
This PR adds `writeKeyPair` to `@solana/keys` and `writeKeyPairSigner` to `@solana/signers`, giving callers a one-line way to save an extractable key pair as a 64-byte JSON array that's byte-for-byte compatible with `solana-keygen new`. Missing parent directories are created automatically, and the resulting file is written with mode `0600` so only the owner can read or write it. Both helpers refuse to overwrite an existing file by default and require the caller to explicitly set `unsafelyOverwriteExistingKeyPair: true` to replace one — the name is deliberately alarming because overwriting a key pair file permanently destroys the previous key and any assets it controls. Calling either helper in a browser or React Native environment throws `SOLANA_ERROR__KEYS__WRITE_KEY_PAIR_UNSUPPORTED_ENVIRONMENT` up front.

To keep browser and React Native bundles free of any `node:fs/promises` or `node:path` references, filesystem access is routed through a new private `@solana/fs-impl` package that follows the same pattern as `@solana/crypto-impl` and friends. Its Node entry re-exports `mkdir`, `writeFile`, and `dirname`; its browser entry provides structurally-identical stubs that throw a new generic `SOLANA_ERROR__FS__UNSUPPORTED_ENVIRONMENT` error as defense-in-depth beneath the helper-specific guard. The package is added to `noExternal` in `getBaseConfig.ts` so it gets inlined at build time.
@lorisleiva lorisleiva force-pushed the 04-09-add_writekeypair_and_writekeypairsigner_helpers branch from 523a7df to aec6308 Compare April 9, 2026 21:42
@lorisleiva lorisleiva merged commit fdfcb6c into main Apr 9, 2026
14 checks passed
@lorisleiva lorisleiva deleted the 04-09-add_writekeypair_and_writekeypairsigner_helpers branch April 9, 2026 21:51
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

🔎💬 Inkeep AI search and chat service is syncing content for source 'Solana Kit Docs'

@github-actions
Copy link
Copy Markdown
Contributor

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.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Apr 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants