Engine
The engine is the single implementation of all tachyon verbs. It is a flat struct that wires together the compiler, tester, simulator, deployer, wallet gate, chain manager, and...
Source files: internal/engine/engine.go, internal/engine/workdir.go
The engine is the single implementation of all tachyon verbs. It is a flat struct that wires together the compiler, tester, simulator, deployer, wallet gate, chain manager, and registry. Every verb (compile, test, simulate, deploy, call, chain_list, chain_register, chain_use, artifact_get, registry_lookup) is a method on Engine that returns a typed Envelope[T].
Design decisions
Flat verb dispatch, not a plugin system
The engine does not use reflection or a plugin registry to route verbs. Each verb is a concrete method with a concrete request and response type. This makes the API contract explicit, enables compile-time checking, and keeps the JSON-RPC / REST / MCP dispatch layers thin.
type Engine struct {
Cfg config.Config
Reg *registry.Registry
Chains *chains.Manager
Compiler *compiler.Compiler
Tester *tester.Tester
Simulator *simulate.Simulator
Deployer *deployer.Deployer
Wallet *wallet.Gate
}
Envelope wrapping
Every verb returns types.Envelope[T] — a uniform JSON shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "retry": false } }
The envelope is constructed at the engine boundary, not in the transport layer. This means REST, JSON-RPC, and MCP all see the same shape. types.OK(data) and types.Fail(err) are the only constructors.
Partial results on failure
Some verbs return partial data even when the overall operation fails:
- Test: If tests run but assertions fail, the envelope is
ok: falsebutdatacontains the suite results with pass/fail counts. - Simulate: If
eth_callreverts, the envelope isok: falsebutdatacontains the gas estimate and revert reason.
This lets agents inspect failure details without a second round-trip.
Self-contained source uploads
When CompileRequest.Sources or TestRequest.Sources is non-empty, the engine materializes the source set into an ephemeral Foundry project:
- Creates a temp directory
- Writes uploaded files (with path sanitization via
safeRel) - Symlinks the baked dependency tree (
lib/→ box lib,.oz/→ box contracts) - Generates
foundry.tomlandremappings.txtif not provided - Derives a deterministic
ProjectIDfrom the source set (SHA-256 of sorted paths+contents) - Runs forge in the temp dir
- Cleans up via deferred
cleanup()
The deterministic ProjectID means a compile and a later deploy/call resolve the same registry entries without the caller threading a path-derived id.
Conservative EVM version default
defaultEVMVersion = "shanghai" is used for uploaded-source compiles when the caller does not pin one. This is deliberate: Paxeer mainnet (chain 125) and many other EVM chains are pre-Cancun. solc 0.8.27's default "cancun" emits the MCOPY opcode (0x5E), which causes "invalid opcode: opcode 0x5e not defined" reverts on pre-Cancun chains. "shanghai" still emits PUSH0 (which Paxeer supports) but no Cancun-only opcodes.
Callers can override per-chain via CompileRequest.EVMVersion (e.g., "cancun" for a chain that supports it, or "paris" for a PUSH0-less node).
Verb implementations
Compile
func (e *Engine) Compile(ctx context.Context, req types.CompileRequest) types.Envelope[types.CompileResponse]
- If
req.Sourcesis non-empty: materializes ephemeral workdir, derivesProjectID, runs forge there - Otherwise: uses
req.ProjectRoot(or engine default), runs forge in place - Collects artifacts from
out/, normalizes ABI/bytecode/compiler metadata - Indexes artifacts in registry
- Returns
ProjectID+ artifact list
Test
func (e *Engine) Test(ctx context.Context, req types.TestRequest) types.Envelope[types.TestResponse]
- Same source-upload logic as Compile
- Runs
forge test --json - Parses NDJSON or single JSON output into
TestResponsewith suite/case granularity - Returns partial results even when tests fail
Simulate
func (e *Engine) Simulate(ctx context.Context, req types.SimulateRequest) types.Envelope[types.SimulateResponse]
- Resolves chain profile (from
chain_id,rpc_url, or active registry entry) - Dials RPC, runs
eth_callwith 30s timeout - If
req.Trace: runsdebug_traceCallwhen supported - Returns result hex, gas estimate, revert reason, optional trace
Deploy
func (e *Engine) Deploy(ctx context.Context, req types.DeployRequest) types.Envelope[types.DeployResponse]
- Checks registry for existing deployment by
idempotency_key+chain_id - If found and confirmed on-chain: returns existing address (
Existing: true) - Resolves artifact from registry by
contract+ProjectID - Packs constructor args via
abienc.PackConstructorArgs - Builds
TxIntent(plain creation or CREATE2 factory call) - Authorizes via wallet policy gate
- Signs and broadcasts, waits for receipt
- Records deployment in registry
Call
func (e *Engine) Call(ctx context.Context, req types.CallRequest) types.Envelope[types.CallResponse]
- Resolves calldata: ABI-encoded from
Method+Args(with ABI from inline or registry), or pre-encoded hexData - If
SimulateOnly: runseth_callvia simulator, returns result/revert - Otherwise: builds tx, authorizes policy, signs, broadcasts raw tx, returns tx hash
Chain management
func (e *Engine) ChainList() types.Envelope[types.ChainListResponse]
func (e *Engine) ChainRegister(req types.ChainRegisterRequest) types.Envelope[types.ChainProfile]
func (e *Engine) ChainUse(req types.ChainUseRequest) types.Envelope[types.ChainListResponse]
ChainList: returns all presets + custom profiles, marks activeChainRegister: adds custom chain to in-memory map (not persisted)ChainUse: sets active chain in registry, returns updated list
Registry lookups
func (e *Engine) ArtifactGet(req types.ArtifactGetRequest) types.Envelope[types.ArtifactGetResponse]
func (e *Engine) RegistryLookup(req types.RegistryLookupRequest) types.Envelope[types.RegistryLookupResponse]
ArtifactGet: fetches cached ABI/bytecode by contract name + project IDRegistryLookup: resolves prior deployment by idempotency key + chain
Workdir helpers
workdir.go contains the ephemeral source workdir logic:
func (e *Engine) prepareSourceWorkdir(sources map[string]string, evmVersion string) (string, func(), *types.Error)
func sourcesProjectID(sources map[string]string) string
func foundryTomlFor(evmVersion string) string
func safeRel(p string) (string, bool)
safeRelrejects absolute paths, backslashes, and parent-directory escapes (../)maxSourceBytes = 8 << 20(8 MiB) bounds individual file size- The dependency tree is linked via symlinks, not copied, so ephemeral workdirs are cheap
Modifying the engine
| What to change | Where |
|---|---|
| Add a new verb | internal/engine/engine.go — add method + type in pkg/types/ |
| Change envelope shape | pkg/types/types.go — Envelope[T] and Error |
| Add error code | pkg/types/errors.go — add constant + update docs |
| Change EVM default | internal/engine/workdir.go — defaultEVMVersion |
| Add source upload limit | internal/engine/workdir.go — maxSourceBytes |
| Change dependency linking | internal/engine/workdir.go — prepareSourceWorkdir symlink logic |
