Build
Build on HELIX
@helixfi/sdk mirrors the program in TypeScript: PDAs, account decoders, an instruction builder for every instruction a client sends (all but the hook-only write_hook_data), quotes that match the program to the lamport, and the events and errors it logs. It is built on @solana/kit, and it is what this site uses.
// an app inside the HELIX monorepo
{
"dependencies": {
"@helixfi/sdk": "workspace:*",
"@solana/kit": "^8.4.0"
}
}Once it is published, it installs with:
pnpm add @helixfi/sdk @solana/kit| Module | Exports |
|---|---|
| constants | HELIX_PROGRAM_ID, EXAMPLE_HOOK_PROGRAM_ID, SYSTEM_PROGRAM_ID, LOADER_UPGRADEABLE_ID, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, ATA_PROGRAM_ID, METAPLEX_PROGRAM_ID, ZERO_ADDRESS, IX, KIND, SIZE, HOOK_FLAGS, HOOK_DISC, RULES, KIT_RULES, MINT_FLAGS, CONTEXT, MODE, LAUNCH_STATE, SIDE, PAUSE, AUTHORITY, BOUNDS, CEILINGS, DEFAULT_CONFIG, BPS, SCALE, LAMPORTS_PER_SOL, MIN_LIQUIDITY, LAUNCH_DECIMALS, HOOK_DATA_LEN |
| pda | configAddress, holdingAddress, poolAddress, launchAddress, positionAddress, hookSignerAddress, hookSelfAddress, extraAccountsAddress, programDataAddress, wrapperAddress, wrappedMintAddress, mirrorAddress, mirrorMintAddress, ataAddress, metaplexMetadataAddress |
| layouts | decodeConfig, decodeMint, decodeHolding, decodePool, decodeLaunch, decodePosition, decodeWrapper, decodeMirror, decodeTokenAccount, decodeKitHookData, OFFSETS, and their types |
ix | createMint, createHolding, transfer, mintTo, burn, approve, revoke, setFrozen, closeHolding, setAuthority, setHook, updateMetadata, swap, createPool, addLiquidity, removeLiquidity, collectProtocolFees, createLaunch, claimCreatorFees, claimRewards, shareWithHolders, registerWrapper, wrap, unwrap, createMirror, bridgeOut, bridgeBack, createAtaIdempotent, initConfig, updateConfig, acceptAdmin, exampleHookInit. NO_RULES and the LaunchRules type come from the same module. |
| math | quoteBuy, quoteSell, curveBuy, curveSell, ammOut, sniperBps, paysHolders, spotPrice, marketCapLamports, curveProgress, sideFeeBps, claimableRewards, bpsOf |
| hooks | hookKind, hookAccounts, swapHookAccounts, decodeExtraAccounts, exampleHookExtras, exampleHookConfigAddress |
| events | parseEvents, decodeEvent, EVENT, HelixEvent |
| errors | explainFailure, errorName, ERRORS |
| presets | PRESETS |
| compute | setComputeUnitLimit, setComputeUnitPrice, COMPUTE_BUDGET_PROGRAM_ID |
Everything is exported from the package root; the builders sit under the ix namespace. Amounts are bigint throughout: lamports, and token base units (6 decimals on launches).
Derive the addresses, decode the pool, quote the buy, and send create_holding + swap. The holding instruction is idempotent, so it can go in front of every buy.
import {
address,
appendTransactionMessageInstructions,
assertIsTransactionWithBlockhashLifetime,
createSolanaRpc,
createSolanaRpcSubscriptions,
createTransactionMessage,
getBase64Encoder,
getSignatureFromTransaction,
pipe,
sendAndConfirmTransactionFactory,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
type Address,
type Instruction,
type KeyPairSigner,
} from '@solana/kit';
import {
configAddress,
decodeLaunch,
decodeMint,
decodePool,
holdingAddress,
ix,
launchAddress,
poolAddress,
quoteBuy,
setComputeUnitLimit,
swapHookAccounts,
} from '@helixfi/sdk';
const rpc = createSolanaRpc('https://api.devnet.solana.com');
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.devnet.solana.com');
/** An account's data, or an error if it doesn't exist. */
async function read(a: Address): Promise<Uint8Array> {
const { value } = await rpc.getAccountInfo(a, { encoding: 'base64' }).send();
if (!value) throw new Error('no account at ' + a);
return new Uint8Array(getBase64Encoder().encode(value.data[0]));
}
/** Signs with every signer the instructions name, sends, waits for confirmation. */
async function send(payer: KeyPairSigner, instructions: Instruction[]) {
const { value: blockhash } = await rpc.getLatestBlockhash().send();
const message = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayerSigner(payer, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
(m) => appendTransactionMessageInstructions(instructions, m),
);
const tx = await signTransactionMessageWithSigners(message);
assertIsTransactionWithBlockhashLifetime(tx);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(tx, { commitment: 'confirmed' });
return getSignatureFromTransaction(tx);
}
export async function buy(trader: KeyPairSigner, mintAddress: string, lamports: bigint) {
const mint = address(mintAddress);
// 1. Addresses: every HELIX account but the mint is a PDA.
const pool = await poolAddress(mint);
const launch = await launchAddress(mint);
const poolHolding = await holdingAddress(mint, pool);
const traderHolding = await holdingAddress(mint, trader.address);
// 2. State, decoded at fixed offsets.
const m = decodeMint(await read(mint));
const p = decodePool(await read(pool));
const l = p.launch ? decodeLaunch(await read(launch)) : null;
// 3. A quote that matches the program to the lamport (before a custom hook's cut).
const now = BigInt(Math.floor(Date.now() / 1000));
const q = quoteBuy(p, l, lamports, now, trader.address);
const minOut = (q.tokensToTrader * 99n) / 100n; // accept 1% slippage
// 4. Create the holding if needed (idempotent), then swap.
return send(trader, [
setComputeUnitLimit(300_000), // a safe ceiling for a trade; simulate to measure what it really uses
ix.createHolding({ payer: trader, holding: traderHolding, mint, owner: trader.address }),
ix.swap(
// A token with a custom hook also needs its extras: swapHookAccounts(m, extras).
{ trader, config: await configAddress(), mint, pool, poolHolding, traderHolding, launch, hook: await swapHookAccounts(m) },
{ side: 'buy', amountIn: lamports, minOut },
),
]);
}quoteBuyis what the program will do, before a custom hook's cut.q.graduatessays whether this buy sells the curve out; thenq.solInis less than offered.- A sell is the same call with
side: 'sell',amountInin tokens, andquoteSell(pool, launch, tokens).solOutfor the minimum. 300_000compute units is a safe ceiling for a trade, and600_000for a token-to-token route (a sell and a buy in one transaction). They are ceilings, not costs: simulate the transaction to measure what it uses; an external hook adds its own.- In a browser wallet, build the same instructions with a signer for the wallet's address and let the wallet sign; see
apps/web/src/lib/actions.ts.
import { createSolanaRpc, getBase64Encoder, type Address, type Base58EncodedBytes, type Signature } from '@solana/kit';
import { HELIX_PROGRAM_ID, OFFSETS, SIZE, decodeHolding, explainFailure, parseEvents } from '@helixfi/sdk';
const rpc = createSolanaRpc('https://api.devnet.solana.com');
// What a confirmed transaction did, from its logs.
export async function eventsOf(signature: Signature) {
const tx = await rpc.getTransaction(signature, { encoding: 'json', maxSupportedTransactionVersion: 0, commitment: 'confirmed' }).send();
const logs = tx?.meta?.logMessages ?? [];
if (tx?.meta?.err) throw new Error(explainFailure(logs) ?? 'failed');
return parseEvents(logs); // [{ kind: 'transfer', … }, { kind: 'swap', sol, tokens, creatorFee, … }]
}
// Every HELIX holding of one wallet: filter by size and by the owner's offset.
export async function holdingsOf(owner: Address) {
const accounts = await rpc
.getProgramAccounts(HELIX_PROGRAM_ID, {
encoding: 'base64',
filters: [
{ dataSize: BigInt(SIZE.holding) },
{ memcmp: { offset: BigInt(OFFSETS.holding.owner), bytes: owner as unknown as Base58EncodedBytes, encoding: 'base58' } },
],
})
.send();
return accounts.map((a) => decodeHolding(new Uint8Array(getBase64Encoder().encode(a.account.data[0]))));
}OFFSETS gives the byte offsets the decoders use for getProgramAccounts filters: a holding's mint and owner, a pool's mint, a launch's mint and creator, a position's pool and owner. The program id is HLX49qz3CSKQrHrdzrrWygZBaKHWWSG8JwUMFHcZ47Fr.
The protocol behind them is on Hooks and Launches; bridging from code is on Bridge. The SDK's tests, packages/sdk/test/*.ts, run every flow on LiteSVM and are the most complete examples.
Paste this into your coding agent's context before asking it to build on HELIX. Every name in it is a real export of the SDK.
You are writing code against HELIX: a Solana token standard, DEX, launchpad and bridge in ONE program.
Program id: HLX49qz3CSKQrHrdzrrWygZBaKHWWSG8JwUMFHcZ47Fr. SDK: @helixfi/sdk (TypeScript, built on @solana/kit 8).
HELIX tokens are NOT SPL tokens: to hold, move or trade them, never use the Token or Token-2022 programs or associated
token accounts. The one exception is the bridge (below): its instructions move SPL / Token-2022 tokens in and out of HELIX.
Amounts are bigint: lamports (1 SOL = 1_000_000_000n) and token base units (launches use 6 decimals).
ACCOUNTS (owned by the HELIX program; all PDAs except a keypair mint; decode with the SDK)
- Mint: decodeMint. A keypair account when made by createMint or createLaunch (it must sign); a PDA when bridged in,
wrappedMintAddress(splMint). hookProgram: null = no hook, HELIX_PROGRAM_ID = built-in kit, else external.
- Holding: holdingAddress(mint, owner) = ["holding", mint, owner]; decodeHolding. Exactly one per (mint, owner).
- Pool: poolAddress(mint) = ["pool", mint]; decodePool. Quoted in native SOL. mode: MODE.CURVE or MODE.AMM.
- Launch: launchAddress(mint); decodeLaunch. Position: positionAddress(pool, owner). Config: configAddress(); decodeConfig.
INSTRUCTIONS: the `ix` namespace returns @solana/kit Instructions; signer fields take TransactionSigners.
- ix.createHolding({ payer, holding, mint, owner }): idempotent; prepend it before a buy or an incoming transfer.
- ix.swap({ trader, config, mint, pool, poolHolding, traderHolding, launch, hook }, { side: 'buy' | 'sell', amountIn, minOut })
buy: amountIn = most lamports to spend, minOut = fewest tokens to receive. sell: amountIn = tokens, minOut = fewest lamports.
poolHolding = holdingAddress(mint, pool); launch = launchAddress(mint) (any address if the pool has no launch).
- ix.transfer({ authority, source, destination, mint, hook }, amount); ix.mintTo; ix.burn; ix.approve; ix.revoke; ix.closeHolding.
- ix.createLaunch({ creator, config, mint, pool, poolHolding, launch, feeCollector, hook? },
{ name, symbol, uri, rules, customHook?, customHookFlags? })
mint = await generateKeyPairSigner() (it must sign); feeCollector = decodeConfig(configData).feeCollector.
- ix.claimCreatorFees({ creator, launch }); ix.claimRewards({ owner, mint, launch, holding }); ix.shareWithHolders({ payer, launch }, lamports).
- ix.createPool, ix.addLiquidity, ix.removeLiquidity, ix.collectProtocolFees.
HOOK ACCOUNTS (the `hook` field)
- swaps: await swapHookAccounts(decodedMint, extras). Empty for no hook and for the kit.
- transfers, mints, burns, liquidity: await hookAccounts(mint, decodedMint, extras). [launch] for the kit.
- an external hook's extras: decodeExtraAccounts(data at extraAccountsAddress(hookProgram, mint)), if the hook publishes them.
QUOTES, STATE, EVENTS
- quoteBuy(pool, launch | null, lamportsIn, nowUnixSecs, trader?) -> { solIn, fees, net, tokensToTrader, burned, graduates, impactBps }
- quoteSell(pool, launch | null, tokensIn) -> { gross, fees, burned, solOut, impactBps }. Neither includes an external hook's cut.
- spotPrice, marketCapLamports, curveProgress, sideFeeBps, sniperBps, paysHolders, claimableRewards(launch, holding, now).
- parseEvents(logs) -> HelixEvent[] (kind: 'swap', 'transfer', 'launchCreated', 'graduated', ...). explainFailure(logs) names errors (6000+).
LAUNCH RULES (LaunchRules, integers; start from NO_RULES or PRESETS[i].rules; bounds are BOUNDS)
- creatorFeeBps <= 200; holderFeeBuyBps, holderFeeSellBps <= 300; burnBuyBps, burnSellBps <= 200
- creator + holder + burn <= 500 on each side; maxWalletBps 0 or 50..500; creatorLockSecs <= 31536000
- earlyWindowSecs <= 300 and earlyLockSecs <= 2592000, both set or both 0
- a customHook can't be combined with holder fees, maxWalletBps, creatorLockSecs or the early lock.
BRIDGE (one for one, no bridge fee; the SPL side uses TOKEN_PROGRAM_ID or TOKEN_2022_PROGRAM_ID and ataAddress)
- In, SPL / Token-2022 -> HELIX: wrapper = wrapperAddress(splMint), helixMint = wrappedMintAddress(splMint),
vault = ataAddress(wrapper, splMint, tokenProgram), ownerToken = ataAddress(owner, splMint, tokenProgram),
ownerHolding = holdingAddress(helixMint, owner).
ix.registerWrapper({ payer, splMint, tokenProgram, wrapper, helixMint, vault, metaplexMetadata }) once per SPL mint
(metaplexMetadata = await metaplexMetadataAddress(splMint)); the name and symbol are read from the token's metadata.
ix.wrap({ owner, splMint, tokenProgram, wrapper, helixMint, vault, ownerToken, ownerHolding }, amount) after ix.createHolding;
ix.unwrap(same accounts, amount) after ix.createAtaIdempotent({ payer, ata: ownerToken, owner, mint: splMint, tokenProgram }).
- Out, HELIX -> SPL mirror (a Token-2022 mint, a PDA of HELIX): mirror = mirrorAddress(mint), mirrorMint = mirrorMintAddress(mint),
vault = holdingAddress(mint, mirror), ownerToken = ataAddress(owner, mirrorMint, TOKEN_2022_PROGRAM_ID).
ix.createMirror({ payer, helixMint, mirror, mirrorMint, vault, launch }) once per token; a launch only after it graduated
(launch = launchAddress(mint), any address if the token wasn't launched).
ix.bridgeOut({ owner, helixMint, mirror, mirrorMint, vault, ownerHolding, ownerToken, hook }, amount) after ix.createAtaIdempotent;
ix.bridgeBack(same accounts, amount) after ix.createHolding. hook = await hookAccounts(mint, decodedMint, extras);
the token's hook sees these transfers with context Bridge.
- decodeWrapper, decodeMirror, decodeTokenAccount. A wrapped token can't be mirrored: unwrap it instead.
TRANSACTIONS: prepend setComputeUnitLimit: 300_000 for a trade, 600_000 for a token -> token route (a sell and a buy in one
transaction), 400_000 for a launch with a first buy, a wrap or a bridge out. These are safe ceilings, not costs: simulate the
transaction to measure what it uses (an external hook adds its own). Sign with signTransactionMessageWithSigners so a new
mint's keypair signs too.
HOOKS IN RUST: depend on the helix-hook crate (a path dependency for now). Dispatch on helix_hook::disc. Check that account 0
is a signer equal to PDA(["hook-authority", your program id]) derived from the HELIX program id. TokenHookArgs::decode the
data after the 8-byte discriminator; read args.context (Plain, Buy, Sell, Liquidity, or Bridge: into or out of the
bridge's vault). Answer a before_* callback with HookReturn::encode + set_return_data: at most 3 deltas, each to a writable
HELIX holding of the same mint at account index >= 5.
Never hand-encode HELIX instructions or account layouts: use the SDK.