Skip to main content
On Solana a program runs through the Kitchen on the owner’s Pot, or directly on the engine with no Pot (Kitchen). From TypeScript you compile the program for solana, resolve the accounts its manifest names, and build the cook with the Kitchen client or with the instruction builders. The library exports no RPC endpoint: every function that reads the cluster takes your RPC client or rpcUrl.
The examples take @solana/kit’s Address, RPC client and TransactionSigner; install the major version the package depends on. @eco-incorp/sauce/svm re-exports the kit functions its own functions take (address, generateKeyPairSigner, getProgramDerivedAddress, AccountRole, getAddressEncoder, blockhash). The program and Kitchen ids come from v12SvmEngineProgramId() and v12SvmKitchenProgramId() in @eco-incorp/sauce/deployments (Deployments).

Compile and keep the account manifest

routes.compileSauceProgram("solana", source) returns the bytecode and the account manifest: the accounts the program needs, in order, with the role each requires. The token methods and venue namespaces add their accounts to main, and derivedAccounts says how each is derived.
sol-compile.mts
entryParams holds main’s values; the account slots are not among them. Encode the values with routes.encodeEntryArguments and append them to the bytecode (Entry arguments). The program names pot, so its accounts derive from the Pot (pot, pot:USDC) and it runs through the Kitchen only; potMention gives the line and column of that first mention. A program that never names pot derives them from authority (authority:USDC), the account that signs the engine call: the Pot through the Kitchen, the owner on a direct cook, so it runs either way. The two programs below never name pot.

Venue accounts and data

A venue namespace call adds every account its swap takes to main, and derivedAccounts names each one. This program swaps at Orca’s whirlpool of tick spacing 4 (@sauce/router on Solana):
orca-swap.sauce.ts
orca-compile.mts
A program or code generator that calls a venue’s @sauce/router function itself needs those accounts in the venue’s own order. routerSvmAccountOrder(venue, options) and routerSvmAccounts(venue, options, refs) from @eco-incorp/sauce/svm list them, with whether the instruction writes each and whether it signs; routerSvmDataTemplate(venue, options) encodes the instruction’s fields once in TypeScript; and ROUTER_SVM_VENUES gives each venue’s program and its …Data and …Cpi functions (Router swap accounts, Router swap data).

resolveSvmAccountName

Resolves one account name, in the language sauce run --account NAME=<value> takes, to the address a cook attaches.

Import

Usage

sol-accounts.mts

Signature

Names

A token is a registered symbol (USDC, TSLAx, matched without regard to case), a mint’s base58 address, or @<param>. A registered mint’s program comes from the registry; any other is read through ctx.programOf.

Returns

ResolvedSvmAccountName: value is "pot", "owner" or an address; kind is pot, owner, address, account, mint or program; account carries the mint, holder and token program of a token account.

Throws

SvmAccountNameError for a name that cannot be resolved, such as a slot without ctx.load or an unregistered mint without ctx.programOf.

resolveSvmToken

Reads a mint and resolves its token program, decimals and Token-2022 extensions.

Import

Usage

sol-token.mts

Signature

Parameters

  • load: an owned-account loader, which returns each account’s data and owner. kitBatchAccountLoader(rpc) wraps a kit RPC’s getMultipleAccounts, and coalescingOwnedAccountLoader batches the loads made in one turn into one request. The owner is the only way to tell an extensionless Token-2022 mint from an SPL Token one.
  • mint: the mint’s address.

Returns

  • tokenProgram: SPL Token or Token-2022, the program that owns the mint: a checked write’s tokenProgram account, and the program a token account derives under.
  • decimals: a checked write’s decimals argument.
  • extensions: transferFee and transferHook where set, blocking (non-transferable, frozen by default, paused) and advisories (permanent delegate, confidential transfers and other flags that do not stop a plain transfer).
  • transferRefusals: why @sauce/token’s checked transfer of this mint would be refused: a transfer hook that names a program, or a blocking extension. Empty when none.
  • svmTokenAccount(token, owner): the owner’s associated token account under the mint’s program. A legacy derivation of a Token-2022 holder’s account gives an address nothing created.
  • svmTokenReceived and svmTokenToSend: what a transfer of amount delivers at epoch, and what to send for the destination to receive exactly amount, netting the mint’s transfer fee as Token-2022 computes it.

Throws

resolveSvmToken throws when the account is missing, is owned by neither token program, or is not an initialized mint. A Token-2022 transfer fee is withheld from what the destination receives, so a program that pays one measures what arrived. In a program, @sauce/token’s transferChecked and approveChecked move either program’s tokens; the legacy transfer, transferFrom and approve are SPL Token only (Names).

The execution fee and a cook built by hand

Every cook pays the Kitchen’s execution fee in lamports, and signs a maximum. fetchExecutionFee reads the fee from the Kitchen’s ["execution_fee"] config account. The instruction builders then assemble a cook for any owner without a signature, which is how you measure or inspect one before a wallet signs it.

Usage

sol-cook-unsigned.mts

Signature

Notes

  • fetchExecutionFee throws when the config is absent, is not owned by the Kitchen, or does not decode: every cook is refused on chain then. A cook whose maxExecutionFee is below the live fee is refused before anything runs, so read the fee immediately before signing.
  • resolveAccounts keeps manifest order. An entry’s signer replaces the manifest’s requirement: the Pot is attached with signer: false because the Kitchen signs for it during the cook, and a non-payer authority is { address, signer: transactionSigner }. The reserved payer reference resolves to the fee payer. A manifest ending in remaining leaves the final tail to you.
  • Every cook transaction carries buildHeapFramePrepend() exactly once: the request for the engine’s 262,144-byte heap frame. Without it the engine fails before any instruction of the program runs, and a second ComputeBudget instruction of the same type fails the whole transaction. The client adds it.
  • buildCookInstruction names the Pot as a readonly account, and the Kitchen validates it with potSalt and the Pot’s canonical bump, which the builder derives from kitchen, owner and potSalt. Pass pot as derivePotPda(kitchen, owner, potSalt).address. The Kitchen signs for the Pot during the cook; a Pot the program writes also belongs in tail, unsigned.
  • The Kitchen’s cook instruction carries the Pot’s bump after its salt. Build every cook with this SDK’s builders.
  • measureExecuteTransaction compiles the transaction with no signature and a placeholder blockhash, for its size and accounts. A transaction holds at most 1,232 bytes.

createSauceSvmClient

Builds a client that simulates, builds, signs and sends Kitchen cooks for one payer.

Import

Usage

sol-client.mts

Signature

Parameters

Returns

SauceSvmClient: computeUnitLimit: "auto" simulates first and sets the limit from the units used, padded 20% and capped at 1,400,000. A cook’s own maxExecutionFee option overrides the client’s.

Throws

When rpcUrl is missing or via is not "kitchen" or "engine". A simulation failure is a result with ok: false, not a throw.

cookTransaction

The signed cook transaction, built as execute builds it, for a caller that sends it another way. It is a method of the client above.

Signature

Returns

A kit transaction signed by the client’s payer, with a fresh blockhash, the heap-frame request and, with computeUnitLimit: "auto", a compute limit from a simulation. Nothing is sent. To build a cook with no signature, use the instruction builders in the section above.

When the accounts do not fit

A cook that names about thirty accounts no longer fits one transaction. execute and executeStaged then create an address lookup table holding the accounts a table may hold (all but signers and called programs), wait until it is usable, and send through it. The payer pays the table’s rent and owns it; the result’s lookupTableAddress names it, and passing it as lookupTable to a later cook reuses it. lookupTable: false sends the cook as it is. To simulate such a cook or build it with cookTransaction, get the table first with cookLookupTable and pass its lookupTables.

Stage a large program

A program larger than one transaction is staged into a Kitchen code buffer, then cooked from it. The client has no staging method: build these Kitchen instructions and send them in order.

Usage

sol-stage.mts
orca-swap.sauce.ts is the program in Venue accounts and data.

Steps

  1. deriveCodeBufferPda(kitchen, owner, nonce) and deriveCodePda(kitchen, codeBuffer) give the buffer’s metadata and code accounts.
  2. buildCreateCodeBufferInstruction allocates them. A capacity above GROWTH_STEP (10,240 bytes) needs Math.ceil(capacity / GROWTH_STEP) - 1 more buildGrowCodeBufferInstruction calls, which can share one transaction.
  3. buildWriteCodeBufferInstruction writes each chunk at its offset.
  4. buildFinalizeCodeBufferInstruction seals the buffer with the program’s length and SHA-256 digest. The Kitchen checks the digest and shrinks the code account to that length.
  5. Cook from it: the Kitchen’s buildCookFromAccountInstruction, the client’s simulateStaged and executeStaged, or on a direct cook buildCookAndPayFromAccountPinnedInstruction. buildCloseCodeBufferInstruction closes the buffer and returns its rent.

What the pin checks

  • Through the Kitchen, buildCookFromAccountInstruction takes a pin, and the Kitchen refuses the cook unless the staged bytes hash to it; the Kitchen runs only a finalized buffer the signing owner holds. The client’s expectedSha256 option throws on this path: it is not that pin.
  • Direct to the engine, the client always pins a staged cook, to expectedSha256 or to the sha256 of the StagedBuffer you pass, and refuses a bare buffer address with neither. buildCookAndPayFromAccountInstruction, unpinned, runs whatever the account holds when the transaction lands, with the signer’s authority: use it only for an account no one else can change.

Two ways to cook

The fee and the compiled program are the same either way. A direct cook cannot write a Pot’s storage, and a transfer whose authority is a Pot fails for the missing signature; it leaves the program one more level of calls into other programs.

/svm/engine

The engine’s wire format and instruction builders, without the rest of /svm.

Usage

sol-engine.mts

Exports

  • The three buildCookAndPay… builders are the engine’s paid direct cooks. Each appends the fee payer, the System program and the fee config after the program’s tail; the engine takes them off before the program runs. The fee payer must be a System-owned account holding no data.
  • buildExecuteInstruction and buildExecuteFromAccountInstruction build the engine’s execute and execute_from_account, the instructions the Kitchen sends the engine during a cook. The released engine refuses them from anything but the Kitchen.
  • engineFaultName(code) names a failed engine instruction’s Custom(n) code. Custom(0) is Revert, a program’s own throw or require.

/svm/engine-artifacts

File paths to the packaged engine and Kitchen programs and the Kitchen IDL, for a local LiteSVM harness. Node only.

Usage

sol-artifacts.mts

Signature

Parameters

  • cluster: which engine build. Default "devnet".
  • path, paths: an explicit file to use instead of the packaged one.

Returns

Absolute paths. Load each program into LiteSVM or a local validator at v12SvmEngineProgramId() and v12SvmKitchenProgramId(). @eco-incorp/sauce/testing runs programs on such a local Solana from a test (Testing).

Throws

When a file is absent. svmEngineArtifacts throws if any piece is missing, rather than returning a partial set.

Next steps