Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ test-ledger

# local
txtx.yml

# npm
**/node_modules/
bun.lockb

# ts-rs generated bindings
crates/types/bindings/
crates/core/bindings/
33 changes: 33 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ serde = { version = "1.0.226", default-features = false }
serde_derive = { version = "1.0.226", default-features = false } # must match the serde version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251
serde_json = { version = "1.0.135", default-features = false }
serde_with = { version = "3.14.0", default-features = false }
ts-rs = { version = "12.0.1", default-features = false, features = ["serde-compat"] }
strum = { version = "0.26", default-features = false, features = ["derive"] }
solana-account = { version = "3.2", default-features = false }
solana-account-decoder = { version = "3.1", default-features = false }
Expand Down
2 changes: 2 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ txtx-addon-kit = { workspace = true }
txtx-addon-network-svm-types = { workspace = true }
txtx-addon-network-svm = { workspace = true }

ts-rs = { workspace = true, optional = true }

[dev-dependencies]
test-case = { workspace = true }
Expand All @@ -115,3 +116,4 @@ postgres = ["surfpool-db/postgres"]
ignore_tests_ci = []
geyser_plugin = [] # Disabled: solana-geyser-plugin-manager conflicts with litesvm 0.9.1
register-tracing = ["litesvm/register-tracing"]
ts = ["ts-rs"]
2 changes: 2 additions & 0 deletions crates/core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,8 @@ impl GeyserAccountUpdate {
}
}

#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts", ts(export))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum TimeTravelConfig {
Expand Down
95 changes: 95 additions & 0 deletions crates/sdk-kit/DEVELOPING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Developing `@surfpool/kit`

Typed `@solana/kit` plugin for Surfnet cheatcode RPC methods. Types are auto-generated from Rust source via `ts-rs`.

## Architecture

```
src/
├── generated/ ← Auto-generated from Rust (DO NOT EDIT)
│ ├── AccountUpdate.ts
│ ├── TimeTravelConfig.ts
│ └── ...
├── index.ts ← surfpool() plugin + createSurfpoolClient()
├── surfnet-rpc.ts ← createSurfnetCheatcodesRpc() with request/response transformers
└── types.ts ← Re-exports generated types + hand-maintained API types
```

### What's auto-generated

All param/response types in `src/generated/` come from Rust structs in:

- `crates/types/src/types.rs` — most types (`AccountUpdate`, `TokenAccountUpdate`, etc.)
- `crates/core/src/types.rs` — `TimeTravelConfig`

These are annotated with `#[cfg_attr(feature = "ts", derive(ts_rs::TS))]` and generated by running `cargo test --features ts`.

### What's hand-maintained

**`src/types.ts`** contains:

1. **Re-exports** of all generated types (add a new line when a new type is generated)
2. **`SurfnetCheatcodesApi`** — the intersection type mapping clean method names to param/response types. When a new RPC method is added to `crates/core/src/rpc/surfnet_cheatcodes.rs`, add its API type here.
3. **RPC-only types** not in Rust source:
- `ClockState` — returned by `timeTravel`, `pauseClock`, `resumeClock` (maps to Solana's `EpochInfo`)
- `AnchorIdl` — for `registerIdl` / `getActiveIdl`
- `Scenario`, `ScenarioOverride` — for `registerScenario`
- `LocalSignatureEntry` — for `getLocalSignatures`
- `ComputeUnitsProfile`, `TransactionProfileResult` — for profiling methods

## Regenerating types

```bash
bun run generate
```

This runs:
1. `cargo test --features ts -p surfpool-types -p surfpool-core export_bindings` with `TS_RS_LARGE_INT=number`
2. Copies `.ts` files from `crates/types/bindings/` and `crates/core/bindings/` into `src/generated/`
3. Runs `scripts/generate.ts` to patch missing imports (ts-rs doesn't add imports for types referenced in `ts(type = "...")` overrides)

## Adding a new RPC method

1. **Add the Rust types** to `crates/types/src/types.rs` (or `crates/core/src/types.rs`):
```rust
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts", ts(export, optional_fields))] // optional_fields if struct has Option<T> fields
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MyNewParam { ... }
```
2. **Regenerate**: `bun run generate`
3. **Update `src/types.ts`**:
- Add re-export: `export type { MyNewParam } from './generated/MyNewParam.ts';`
- Add import (if used in API types below)
- Add API type: `export type SurfnetMyNewMethodApi = { myNewMethod(param: MyNewParam): MyResponse; };`
- Add to `SurfnetCheatcodesApi` intersection
4. **Add tests** in `tests/`

## ts-rs annotation guide

| Scenario | Annotation |
|---|---|
| Struct with all-optional fields (RPC params) | `ts(export, optional_fields)` |
| Simple enum | `ts(export)` |
| Field with external type (e.g. `Pubkey`) | `ts(type = "string")` |
| `Option<T>` field with external inner type | `ts(as = "Option<String>")` (works with `optional_fields`) |
| Enum with `UiAccount` or other non-TS external types | Manual `TS` impl (see `UiAccountChange` / `UiAccountProfileState`) |
| `IndexMap<Pubkey, T>` with custom serde | `ts(type = "Record<string, T>")` |

`TS_RS_LARGE_INT=number` maps Rust `u64` → TypeScript `number` (not `bigint`), which is correct for JSON-RPC wire format.

## Running tests

Tests require a running Surfpool instance:

```bash
surfpool # in another terminal
bun test # 29 pass, 1 skip (writeProgram — known surfpool bug)
```

## Typechecking

```bash
bun run typecheck # runs tsc --noEmit
```
3 changes: 3 additions & 0 deletions crates/sdk-kit/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[test]
preload = ["./tests/setup.ts"]
timeout = 30000
26 changes: 26 additions & 0 deletions crates/sdk-kit/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@surfpool/kit",
"version": "0.1.0",
"description": "Typed @solana/kit plugin for Surfnet cheatcode RPC methods",
"type": "module",
"main": "src/index.ts",
"types": "src/index.ts",
"files": [
"src/"
],
"scripts": {
"typecheck": "bunx tsc --noEmit",
"test": "bun test",
"generate": "cd ../.. && TS_RS_LARGE_INT=number cargo test --features ts -p surfpool-types -p surfpool-core export_bindings && cp crates/types/bindings/*.ts crates/sdk-kit/src/generated/ && cp crates/core/bindings/*.ts crates/sdk-kit/src/generated/ && bun run crates/sdk-kit/scripts/generate.ts"
},
"dependencies": {
"@solana/kit": "^6.5.0",
"@solana/kit-plugin-instruction-plan": "^0.9.0",
"@solana/kit-plugin-payer": "^0.9.0",
"@solana/kit-plugin-rpc": "^0.9.0"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5"
}
}
64 changes: 64 additions & 0 deletions crates/sdk-kit/scripts/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Post-generation fixup for ts-rs generated bindings.
*
* ts-rs doesn't add imports for types referenced in `ts(type = "...")`
* overrides. This script patches the generated files to add missing imports.
*/
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';

const GENERATED_DIR = join(import.meta.dirname, '..', 'src', 'generated');

// Map of type names to their file (without extension)
const typeFiles = new Map<string, string>();
for (const file of readdirSync(GENERATED_DIR)) {
if (file.endsWith('.ts')) {
typeFiles.set(file.replace('.ts', ''), file);
}
}

// For each generated file, find type references that need imports
for (const file of readdirSync(GENERATED_DIR)) {
if (!file.endsWith('.ts')) continue;

const filePath = join(GENERATED_DIR, file);
let content = readFileSync(filePath, 'utf-8');
const typeName = file.replace('.ts', '');

// Find all type names referenced in the file body (after imports)
const existingImports = new Set<string>();
for (const match of content.matchAll(/import type \{ (\w+) \}/g)) {
existingImports.add(match[1]!);
}

// Check for type references that aren't imported and aren't the file's own type
const missingImports: string[] = [];
for (const [name, _sourceFile] of typeFiles) {
if (name === typeName) continue;
if (existingImports.has(name)) continue;

// Check if this type name appears in the body (not in import lines)
const bodyLines = content.split('\n').filter(l => !l.startsWith('import '));
const body = bodyLines.join('\n');
// Match whole word only (not part of another identifier)
const regex = new RegExp(`\\b${name}\\b`);
if (regex.test(body)) {
missingImports.push(name);
}
}

if (missingImports.length > 0) {
const importLines = missingImports
.map(name => `import type { ${name} } from "./${name}";`)
.join('\n');

// Insert after the ts-rs header comment
const headerEnd = content.indexOf('\n\n');
if (headerEnd !== -1) {
content = content.slice(0, headerEnd + 1) + importLines + '\n' + content.slice(headerEnd + 1);
}

writeFileSync(filePath, content);
console.log(`Patched ${file}: added imports for ${missingImports.join(', ')}`);
}
}
11 changes: 11 additions & 0 deletions crates/sdk-kit/src/generated/AccountSnapshot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type AccountSnapshot = { lamports: number, owner: string, executable: boolean, rentEpoch: number,
/**
* Base64 encoded data
*/
data: string,
/**
* Parsed account data if available
*/
parsedData: unknown, };
23 changes: 23 additions & 0 deletions crates/sdk-kit/src/generated/AccountUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type AccountUpdate = {
/**
* providing this value sets the lamports in the account
*/
lamports?: number,
/**
* providing this value sets the data held in this account
*/
data?: string,
/**
* providing this value sets the program that owns this account. If executable, the program that loads this account.
*/
owner?: string,
/**
* providing this value sets whether this account's data contains a loaded program (and is now read-only)
*/
executable?: boolean,
/**
* providing this value sets the epoch at which this account will next owe rent
*/
rentEpoch?: number, };
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/CheatcodeControlConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type CheatcodeControlConfig = { lockout?: boolean, };
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/CheatcodeFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type CheatcodeFilter = string | Array<string>;
6 changes: 6 additions & 0 deletions crates/sdk-kit/src/generated/ComputeUnitsEstimationResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Result structure for compute units estimation.
*/
export type ComputeUnitsEstimationResult = { success: boolean, computeUnitsConsumed: number, logMessages?: Array<string>, errorMessage?: string, };
5 changes: 5 additions & 0 deletions crates/sdk-kit/src/generated/ExportSnapshotConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ExportSnapshotFilter } from "./ExportSnapshotFilter";
import type { ExportSnapshotScope } from "./ExportSnapshotScope";

export type ExportSnapshotConfig = { includeParsedAccounts?: boolean, filter?: ExportSnapshotFilter, scope: ExportSnapshotScope, };
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/ExportSnapshotFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type ExportSnapshotFilter = { includeProgramAccounts?: boolean, includeAccounts?: Array<string>, excludeAccounts?: Array<string>, };
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/ExportSnapshotScope.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type ExportSnapshotScope = "network" | { "preTransaction": string };
4 changes: 4 additions & 0 deletions crates/sdk-kit/src/generated/GetStreamedAccountsResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { StreamedAccountInfo } from "./StreamedAccountInfo";

export type GetStreamedAccountsResponse = { accounts: Array<StreamedAccountInfo>, };
4 changes: 4 additions & 0 deletions crates/sdk-kit/src/generated/GetSurfnetInfoResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { RunbookExecutionStatusReport } from "./RunbookExecutionStatusReport";

export type GetSurfnetInfoResponse = { runbookExecutions: Array<RunbookExecutionStatusReport>, };
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/OfflineAccountConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type OfflineAccountConfig = { includeOwnedAccounts?: boolean, };
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/ResetAccountConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type ResetAccountConfig = { includeOwnedAccounts?: boolean, };
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/RpcProfileDepth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type RpcProfileDepth = "transaction" | "instruction";
4 changes: 4 additions & 0 deletions crates/sdk-kit/src/generated/RpcProfileResultConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { RpcProfileDepth } from "./RpcProfileDepth";

export type RpcProfileResultConfig = { encoding?: string, depth?: RpcProfileDepth, };
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/RunbookExecutionStatusReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type RunbookExecutionStatusReport = { startedAt: number, completedAt: number | null, runbookId: string, errors: Array<string> | null, };
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/SetSomeAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type SetSomeAccount = string | null;
3 changes: 3 additions & 0 deletions crates/sdk-kit/src/generated/StreamAccountConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type StreamAccountConfig = { includeOwnedAccounts?: boolean, };
Loading
Loading