solana-web3-to-kit
A Codemod workflow that migrates
@solana/web3.jsv1 codebases to@solana/kit— the new functional, tree-shakeable Solana SDK.
Built with jssg (deterministic AST rewrites) plus an AI step for structural edge cases.
Why
@solana/web3.js v1 is a class-based, monolithic SDK. @solana/kit (formerly @solana/web3.js v2) is a functional, tree-shakeable rewrite with bigint numerics, async signers, and a pipe-based transaction builder. The migration changes hundreds of call-sites in any non-trivial dApp. This codemod automates the deterministic 80–95% so that engineers only review the structural edges.
Coverage on a real repo
Validated end-to-end on solana-developers/helpers — a 2,000-LOC Solana TypeScript utility library actively used in tutorials.
| Pattern | Remaining |
|---|---|
new PublicKey(x) | 1 (JSDoc comment — correctly skipped) |
Keypair.generate() | 0 |
Keypair.fromSecretKey(x) | 0 |
SystemProgram.transfer({...}) | 0 |
SystemProgram.programId | 0 |
clusterApiUrl(x) | 0 |
PublicKey.findProgramAddressSync(...) | 0 |
new Connection(...) | 0 |
100% coverage on every deterministic pattern. Zero false positives. 12 files changed, 223 insertions, 273 deletions.
What it rewrites (deterministically)
Calls and constructors
| v1 pattern | kit pattern |
|---|---|
new PublicKey(x) | address(x) |
Keypair.generate() | await generateKeyPairSigner() |
Keypair.fromSecretKey(x) | await createKeyPairSignerFromBytes(x) |
const [pda, bump] = PublicKey.findProgramAddressSync(seeds, prog) | const { address: pda, bump } = await getProgramDerivedAddress({ programAddress: prog, seeds }) |
clusterApiUrl('devnet') | 'https://api.devnet.solana.com' (and testnet, mainnet-beta) |
SystemProgram.transfer({ fromPubkey, toPubkey, lamports }) | getTransferSolInstruction({ source, destination, amount }) (all shorthand variants) |
SystemProgram.programId | SYSTEM_PROGRAM_ADDRESS |
new Connection(url, commitment?) | createSolanaRpc(url) (commitment dropped — kit applies per-call) |
RPC method propagation — <rpcVar>.<method>(...) → <rpcVar>.<method>(...).send() for ~40 whitelisted RPC methods (getBalance, getLatestBlockhash, requestAirdrop, sendTransaction, etc.), where <rpcVar> is discovered via:
- variable declarations:
const conn = new Connection(...)/const conn: Connection - function parameters:
(connection: Connection, ...) - class properties:
constructor(public conn: Connection)(resolvesthis.conn.<method>(...)too) - never via name-baseline guesses (avoids the wallet-adapter false-positive class of bug)
Signer property renames — <signerVar>.publicKey → <signerVar>.address, <signerVar>.secretKey → <signerVar>.privateKey, where <signerVar> is discovered from:
- direct declarations:
const kp = Keypair.generate()/Keypair.fromSecretKey(x) - destructured arrays:
const [a, b] = [Keypair.generate(), Keypair.generate()] - typed declarations and parameters:
: Keypair - class properties via
this.kp.publicKeyresolution - never on arbitrary
.publicKeyaccess (so wallet-adapterwallet.publicKeyis safely preserved)
Type annotations — Keypair → KeyPairSigner, PublicKey → Address, Connection → Rpc<SolanaRpcApi>. Matches type_identifier AST nodes only, never the value-position usages.
Imports — every rewrite adds the new specifiers to @solana/kit / @solana-program/system and merges idempotently with any pre-existing import from the same module. The original @solana/web3.js import is left intact for the AI step / human to clean up unused specifiers.
Safety guarantees
- Gated on
@solana/web3.jsimport. The codemod first scans forimport {...} from '@solana/web3.js'and only rewrites identifiers actually imported from there. A local class namedSystemProgramorPublicKeyis left untouched. (Seetests/11-no-solana-imports/.) - No-op on unrelated files. Files that don't import from
@solana/web3.jsproduce zero edits. (Seetests/08-no-op/.) - AST-precise patterns. Matches use tree-sitter, not regex — patterns inside comments, strings, or template literals are correctly skipped. (See
idl.ts:53JSDoc case in the real-repo validation.) - Deterministic and idempotent. Running twice produces the same result.
What the AI step covers (edge cases)
The deterministic step leaves these to the AI step (workflow.yaml → ai):
{value}unwrap on RPC results that the deterministic.send()propagation cannot infer ((await rpc.getBalance(addr).send()).value)new Transaction().add(ix)+sendAndConfirmTransaction→ fullpipe(createTransactionMessage, ...)rewrite + factory hoistingBNarithmetic → nativebigintoperatorsconnection.onAccountChange(...)callback →accountNotifications().subscribe()AsyncIterable +AbortControllerpk.toBase58()/.equals()removal on receivers we can't statically prove are addresses/signersBuffer→Uint8Array+ encoder swapsTransactionInstructionliteral field renames (isSigner/isWritable→AccountRole)- Async propagation: marking enclosing functions
asyncwhen newly-async calls are introduced - Removing now-unused specifiers from
@solana/web3.jsimports
The AI step uses Anthropic Claude with the bash, glob, and str_replace_based_edit_tool tools, scoped to TypeScript files only.
Layout
text
Usage
bash
Development
bash
Scoring methodology
Following the hackathon evaluation formula
100 × (1 − ((FP × wFP) + (FN × wFN)) ÷ (N × (wFP + wFN))):
- N (total real patterns in
solana-developers/helpers): 60 - FP (incorrect changes): 0
- FN (missed real patterns): 0 (the 1 unmatched case is a JSDoc comment, not a real call site)
- Score: 100
License
MIT