@titon-network/forgeton-sdk
v0.8.1
Published
TypeScript SDK for ForgeTON — shared-security staking pool for TON. Stake automatons, slash, receive AutomatonSync pushes. **TSA AUDITED — zero findings** (https://github.com/titon-network/forgeton/blob/main/tsa-analysis/AUDIT_REPORT.md): 11 symbolic-exec
Downloads
700
Maintainers
Readme
@titon-network/forgeton-sdk
Titon-internal. TypeScript client for ForgeTON. Consumed by sibling titon repos (
../kronos/,../fortuna/, future../atlas/) and the Automaton operator binary (../automaton/). Published to npm so siblings can pin via SemVer — not a public-developer package.
Audit status
TSA-audited — zero findings. Full report:
tsa-analysis/AUDIT_REPORT.md.
| Layer | Result |
|---|---|
| TSA symbolic execution (@anthropic-ai/skills/ton-smart-contract-audit, v0.5.3) | 0 findings across 11 checkers (5 single-contract + 6 inter-contract: forgeton↔kronos, forgeton→atlas, forgeton→fortuna) |
| Manual review against references/vulnerabilities.md | every receiver, every revert path |
| Contract test suite (pnpm run test) | 262 / 262 passing |
| Property fuzz (tests/Fuzz.spec.ts) | 192 random-op iterations × 10 invariants green |
| Cross-titon ABI byte-shape pin | tests/WireFormat.spec.ts green |
| Coverage caveat | TSA v0.5.3 has a known STIR-opcode coverage gap on Tolk-1.3 storage-write paths; "0 results" is partial evidence rather than exhaustive proof. Compensated by manual review + 262 concrete tests. See report for the honest evidence boundary. |
| Third-party audit | Pending. Mainnet is gated on this + a 60-day burn-in (see DEPLOY.md §"Mainnet pilot caps"). |
Live deployment: testnet. The SDK's FORGETON_TESTNET constant tracks the current testnet pool address.
@ton/core is a peer dependency (>= 0.63.0). When developing in-tree the sibling repos pull this via file:../forgeton/sdk and rely on the parent repo's node_modules (do not pnpm install inside sdk/ — see ../CLAUDE.md Debugging table).
Quickstart
npm install @titon-network/forgeton-sdk
# in-tree dev: "@titon-network/forgeton-sdk": "file:../forgeton/sdk" in the sibling's package.jsonimport { ForgeTON, FORGETON_TESTNET, summarizeTx, formatTxSummary } from '@titon-network/forgeton-sdk';
const pool = tonClient.open(ForgeTON.createFromAddress(FORGETON_TESTNET.forgeton));
const value = await pool.getRegisterValue();
const result = await pool.sendRegisterAutomaton(via, { value });
for (const tx of result.transactions) console.log(formatTxSummary(summarizeTx(tx)));That's the staker happy path. Owner-side admission, slashing, event decoding, schema verification — see §Task → call below and the examples/ directory.
Surfaces
┌─────────────────────────────────────────────────────────────┐
│ explainError ForgetonError summarizeTx formatTxSummary│ diagnostics
├─────────────────────────────────────────────────────────────┤
│ decodeEvent decodeEvents tryDecodeEvent │ events
├─────────────────────────────────────────────────────────────┤
│ ForgeTON newPool FORGETON_DEFAULTS │ contract + factory
├─────────────────────────────────────────────────────────────┤
│ OP ERR loadForgetonCode FORGETON_TESTNET │ constants + artifacts
└─────────────────────────────────────────────────────────────┘Hand-written 1:1 ABI wrapper. No façade, no fluent builder — every titon caller hits the same surface.
Where this SDK is used
| Sibling repo | Touchpoint | Notes |
|--------------|-----------|-------|
| ../kronos/ | Wraps the pool for owner-side admission scripts; consumer contract embeds the Slash / AutomatonSync wire structs verbatim into its own messages.tolk (no Tolk-level dep). | tests/AbiCompat.spec.ts in kronos guards byte-equivalence. |
| ../fortuna/ | Same pattern as Kronos. Consumer wrapper lives in fortuna/sdk/. | Slash on missed BLS fulfillment; ctx = requestId. |
| ../atlas/ | Subscribes to AutomatonSync only — never sends Slash. Atlas is fan-out-only. | No slash budget required at admission. |
| ../automaton/ | Operator binary — wraps register / unstake / event subscription. | Imports getRegisterValue, getUnstakeValue, event decoder, summarizeTx. |
| ../mcp/ | MCP server exposes pool getters as tools. | Read-only surface. |
When ForgeTON's ABI changes (any edit to contracts/messages.tolk or errors.tolk), every consumer above needs the new SDK and (if Tolk wire structs moved) a re-copy of the affected struct. Run pnpm run gen:opcodes first; coordinate the cascade with BLUEPRINT.md in mind.
Task → call (LLM quick reference)
| Task | Call |
|------|------|
| Deploy a fresh pool | newPool({ owner }) → pool.sendDeploy(via, toNano('0.5')) |
| Admit a titon consumer | pool.sendSetConsumer(ownerVia, { value, contract, isActive: true, maxSlashPerEvent, isOptInRequired }) |
| Remove / replace a consumer | Same call with isActive: false. Re-admission resets slash-budget window + opt-in map. |
| Tune one consumer's slash cap mid-flight | pool.sendSetConsumerSlashCap(ownerVia, { value, consumer, maxSlashPerEvent }) — preserves window + opt-in map (unlike re-admission). |
| Stake an Automaton (gas-scaled to consumer count) | pool.sendRegisterAutomaton(via, { value: await pool.getRegisterValue() }) |
| Top up / partial exit | sendIncreaseStake / sendUnstake({ amount, value: await pool.getUnstakeValue({ crossingThreshold: false }) }) |
| Full exit | sendRequestUnstake → wait cfg.unstakeCooldown → sendUnstake({ amount, value: await pool.getUnstakeValue() }) |
| Automaton opt-in / opt-out for a isOptInRequired consumer | sendSetConsumerOptIn / sendSetConsumerOptOut (ops 0x30 / 0x31) — sender = the automaton |
| Slash from a consumer (TS-side, mostly tests) | pool.sendSlash(consumerVia, { value, automaton, reason, ctx }) |
| Slash from a consumer (Tolk-side, production) | Build Slash { automaton, reason, ctx, amount } (opcode 0x14) and send with SEND_MODE_PAY_FEES_SEPARATELY so the pool's handleSlash gate (msgValue >= MIN_XC_GAS = 0.01 TON) sees the EXACT value attached. SEND_MODE_REGULAR would deduct forward fees from the value and silently shortfall the gate. |
| Decode external-out event body | decodeEvent(body) (throws on unknown) / tryDecodeEvent / decodeEvents |
| Diagnose any tx | summarizeTx(tx) → { success, exitCode, explanation, events, ... } ; formatTxSummary(s) for one-line print |
| Pre-validate a config update | ForgeTON.validateConfig(opts) — throws same exit-code as the contract would |
| Recover from sync drift | Owner: pool.sendForceSync({ automaton }) — bypasses round-robin cursor |
| Propose a code upgrade | sendProposeCodeUpgrade({ newCode, eta }) (absolute) or { newCode, delaySeconds } (relative). Exactly one. ≥ 24 h. |
| Verify SDK ↔ on-chain | await pool.getStorageVersion() vs FORGETON_STORAGE_VERSION — or npx forgeton verify --testnet |
Construct + connect
import { ForgeTON, FORGETON_TESTNET, newPool, loadForgetonCode } from '@titon-network/forgeton-sdk';
// Existing testnet pool (most common — siblings + Automaton hit this)
const pool = tonClient.open(ForgeTON.createFromAddress(FORGETON_TESTNET.forgeton));
// Fresh deploy (mainnet, sandbox, replacement testnet)
const pool = tonClient.open(newPool({ owner: ownerAddress }));
// ≡ ForgeTON.createFromConfig({ owner: ownerAddress }, loadForgetonCode())
// Existing pool by raw address
const pool = tonClient.open(ForgeTON.createFromAddress(addr));FORGETON_TESTNET carries { owner, forgeton, expectedCodeHash, expectedSchema } — the on-chain snapshot the SDK was built against. Mainnet equivalent lands as FORGETON_MAINNET post-deploy.
Cross-contract ABI (consumer side)
There is no shared Tolk package — every titon consumer embeds the wire structs in its own messages.tolk. The bytes must match forgeton/contracts/messages.tolk exactly:
struct (0x00000014) Slash {
automaton: address
reason: uint32
ctx: uint64
}
struct (0x0000001A) AutomatonSync {
automaton: address
isActive: bool
}If either of these changes here, every consumer repo needs a re-copy. Kronos pins this via tests/AbiCompat.spec.ts; Fortuna does the same. When editing the structs, drive both consumer test suites before merging.
examples/consumer-template.tolk is the reference scaffold — the pattern that Kronos and Fortuna's consumer wrappers followed. Use it when wiring up the next titon consumer (e.g. when Atlas grows a slash path, or for a future product).
Diagnose any tx
import { summarizeTx, formatTxSummary } from '@titon-network/forgeton-sdk';
const result = await pool.sendSlash(via, opts);
for (const tx of result.transactions) {
const s = summarizeTx(tx);
console.log(formatTxSummary(s));
// [ok] events: AutomatonSlashed
// [fail] exit 160 NotAuthorizedConsumer — Slash sender is not in ForgeTON's consumer set.
if (!s.success) throw new Error(s.explanation?.hint ?? 'unknown failure');
}summarizeTx(tx) returns { success, exitCode, actionResultCode, explanation, events, rawExternalBodies } — one call replaces hand-walking compute-phase exit codes, plucking external-out bodies, and decoding each.
Decode events (Argus indexer pattern)
import { decodeEvents } from '@titon-network/forgeton-sdk';
for (const ev of decodeEvents(externalBodies)) {
switch (ev.kind) {
case 'AutomatonRegistered':
console.log(`+ automaton ${ev.automaton} staked ${ev.stake}`);
break;
case 'AutomatonSlashed':
console.warn(`${ev.automaton} slashed -${ev.amount} by ${ev.slasher} (reason=${ev.reason})`);
break;
case 'ConsumerUpdated':
console.log(`consumer ${ev.contract} ${ev.isActive ? 'admitted' : 'removed'} (count=${ev.consumerCount})`);
break;
}
}This is the same decode path Argus uses (titon/argus/PLAN.md). Drift detection lives entirely in Argus — the contract holds no on-chain counters.
Reference
ForgeTON class
1:1 ABI wrapper. send* per inbound op, get* per get fun.
Construct: newPool({ owner, workchain? }) / ForgeTON.createFromConfig({ owner }, code, workchain?) / ForgeTON.createFromAddress(addr).
Send methods:
| Method | Sender | Purpose |
|--------|--------|---------|
| sendDeploy(via, value) | anyone (deployer) | Initial deploy. Takes value as positional arg (Blueprint convention). |
| sendRegisterAutomaton | anyone | Stake. Funded for minStake + minGasForRegister + consumerCount × syncGasCost — use getRegisterValue(). |
| sendIncreaseStake | the staked automaton | Top up. |
| sendRequestUnstake | the staked automaton | Start cooldown. |
| sendCancelUnstake | the staked automaton | Abort cooldown. |
| sendUnstake | the staked automaton | Withdraw after cooldown. Cross-zero: minGasForUnstake + consumerCount × syncGasCost — use getUnstakeValue(). |
| sendSlash | admitted consumer | Slash. reason: uint32 surfaces in AutomatonSlashed; ctx: uint64 is inbound-only. |
| sendSetConsumer | owner | Admit / remove. ≤ MAX_CONSUMERS = 16. |
| sendSetConsumerSlashCap | owner | Retune maxSlashPerEvent mid-flight (preserves budget window + opt-in map). |
| sendSetConsumerOptIn / sendSetConsumerOptOut | the automaton | Toggle eligibility for an isOptInRequired consumer. |
| sendForceSync | owner | Rebroadcast one automaton's state. Cursor-agnostic. |
| sendUpdateForgetonConfig | owner | Rewrite the 8-field config blob. Pre-validate with ForgeTON.validateConfig. |
| sendWithdrawSlashed | owner | Drain slashed funds. |
| sendPruneAutomaton | owner | Emergency-remove a stuck automaton. |
| sendPause / sendUnpause | owner | Block new staking. |
| sendProposeCodeUpgrade | owner | Pass eta (absolute) or delaySeconds (relative). 24 h floor. |
| sendExecuteCodeUpgrade | owner | After eta. |
| sendCancelCodeUpgrade | owner | Abort pending. |
Helpers (no I/O for validate*, one-or-two getter calls for get*Value):
| Method | Returns | Notes |
|--------|---------|-------|
| getRegisterValue() | Promise<bigint> | Live getConfig + getConsumerCount; applies the formula. |
| getUnstakeValue({ crossingThreshold }) | Promise<bigint> | true (default) includes fan-out; false for partials. |
| ForgeTON.validateConfig(opts) | void (throws) | Local pre-flight matching the contract's lower + upper bounds. Throws ForgetonError with the same exit code the contract would. |
Getters:
| Method | Returns |
|--------|---------|
| getStorageVersion() | FORGETON_STORAGE_VERSION — verify deployed schema matches SDK |
| getOwner() | owner address |
| getIsPaused() | pause flag |
| getConfig() | full ForgetonConfigReply (8 fields incl. maxSlashPerConsumerPerDay) |
| getAutomaton(addr) | AutomatonInfo \| null — throws SchemaDriftError on parse failure |
| getIsAutomaton(addr) | boolean |
| getActiveAutomatonCount() | active pool size |
| getAutomatonCount() | zombie-inclusive count (incl. fully-slashed leftovers) |
| getTotalStaked() | sum of all stakes |
| getConsumer(addr) | ConsumerInfo \| null (slash-budget window + index) — throws SchemaDriftError |
| getIsConsumer(addr) | boolean |
| getIsAutomatonOptedIn(consumer, automaton) | boolean — false if either consumer or membership absent |
| getConsumerAt(index) | dense lookup in [0, consumerCount) |
| getConsumerCount() | admitted consumer count |
| getSyncCursor() | round-robin starting index for next pushSync |
| getPendingUpgrade() | { codeHash, eta } — (0n, 0) when none pending |
Drift / fan-out / slash totals live in Argus (titon/argus/PLAN.md), not on-chain. Argus diffs pool-side AutomatonSync outbounds against each consumer's local syncesReceived getter; owner repairs with sendForceSync({ automaton }).
Events
import { decodeEvent, decodeEvents, tryDecodeEvent } from '@titon-network/forgeton-sdk';
decodeEvent(body) // throws on unknown opcode — use when source is trusted
tryDecodeEvent(body) // null on unknown
decodeEvents(bodies) // batch — silently drops unknownsTyped discriminated union via event.kind:
AutomatonRegistered StakeIncreased UnstakeRequested UnstakeCancelled
Unstaked AutomatonSlashed AutomatonPruned ForgetonConfigUpdated
ConsumerUpdated SlashedWithdrawn PausedChanged ForceSyncTriggered
ConsumerSlashCapUpdated AutomatonOptInChanged
UpgradeProposed UpgradeCancelled CodeUpdatedAutomatonSlashed carries slasher + reason for attribution. Does NOT carry ctx — correlate by tx hash with the inbound Slash body if you need per-incident context.
Errors
import { explainError, ForgetonError, ERR } from '@titon-network/forgeton-sdk';
const e = explainError(160);
// { code: 160, origin: 'forgeton', name: 'NotAuthorizedConsumer',
// message: 'Slash sender is not in ForgeTON\'s consumer set.',
// hint: 'Owner must SetConsumer { contract: <consumer>, isActive: true } before that contract can send Slash.' }
throw new ForgetonError(ERR.NotAuthorizedConsumer, `tx ${txHash}`);Covers every E_* from contracts/errors.tolk plus common TVM exit codes. Code ranges:
| Range | Owner | |-------|-------| | 0–99 | TVM | | 100–119 | shared admin (NotOwner, Paused, InsufficientGas, BadSchemaVersion) | | 160–179 | ForgeTON staking | | 180–199 | ForgeTON multi-consumer admission | | ≥ 200 | consumer product (Kronos: 100–146 reused per-product; pick distinct ranges) |
Diagnostics
import { summarizeTx, summarizeTxs, formatTxSummary } from '@titon-network/forgeton-sdk';
const s = summarizeTx(tx);
// { success, exitCode, actionResultCode, explanation, events, rawExternalBodies }
formatTxSummary(s);
// '[ok] events: AutomatonSlashed' | '[fail] exit 160 NotAuthorizedConsumer — ...'Accepts any @ton/core Transaction (sandbox BlockchainTransaction works too).
Constants
| Constant | Value | Purpose |
|----------|-------|---------|
| FORGETON_DEFAULTS | 8-field default config | Default values used at deploy |
| FORGETON_STORAGE_VERSION | 1 | Root-storage schema version |
| FORGETON_CONFIG_BLOB_VERSION | 1 | Config-blob schema version |
| AUTOMATON_INFO_VERSION | 1 | Map-value schema for AutomatonInfo |
| CONSUMER_INFO_VERSION | 1 | Map-value schema for ConsumerInfo |
| MAX_CONSUMERS | 16 | Hard cap on simultaneous admitted consumers |
| MIN_UPGRADE_DELAY_SECONDS | 86400 | 24 h timelock floor |
| MAX_UNSTAKE_COOLDOWN | 2592000 | Config ceiling for unstakeCooldown (30 days) |
| MIN_SYNC_GAS_COST | toNano('0.01') | Config floor for syncGasCost (= every consumer's MIN_XC_GAS; see §"PAY_FEES_SEPARATELY pushSync" below) |
| MAX_SYNC_GAS_COST | toNano('1') | Config ceiling for syncGasCost |
| MAX_MIN_STAKE | toNano('10000') | Config ceiling for minStake (pilot 10k TON) |
| MAX_MIN_STORAGE_RESERVE | toNano('100') | Config ceiling for minStorageReserve |
| MAX_MIN_GAS_PER_OP | toNano('1') | Config ceiling for minGasForRegister and minGasForUnstake |
| MAX_SLASH_PER_CONSUMER_PER_DAY | toNano('100000') | Mainnet pilot cap (effective rolling-24h ≤ 100k via token-bucket doubling) |
| MIN_XC_GAS | toNano('0.01') | Cross-contract gas floor |
| FORGETON_CODE_HASH | { hex, base64 } | Code-hash of the bundled BoC |
Live deployments
import { FORGETON_TESTNET } from '@titon-network/forgeton-sdk';
FORGETON_TESTNET.forgeton // address
FORGETON_TESTNET.expectedCodeHash // verify against on-chain
FORGETON_TESTNET.expectedSchema // hardcoded snapshotFORGETON_MAINNET lands when the mainnet deploy completes per DEPLOY.md.
Bundled compiled contract
import { loadForgetonCode, FORGETON_CODE_HASH } from '@titon-network/forgeton-sdk';
const code = loadForgetonCode();
// Verify SDK ↔ on-chain alignment:
const liveHash = (await tonClient.getContractState(addr)).codeHash?.toString('hex');
if (liveHash !== FORGETON_CODE_HASH.hex) console.warn('SDK ↔ on-chain code drift');CLI (npx forgeton)
| Command | Use |
|---------|-----|
| forgeton explain <code> | Decode an exit code |
| forgeton decode <hex> | Decode an external-out event body |
| forgeton info <addr> --testnet | Live pool snapshot |
| forgeton estimate register --pool <addr> --testnet | Compute the right value: for register |
| forgeton verify --testnet | Confirm FORGETON_TESTNET matches the live deploy |
Network commands (info, estimate, verify) need @ton/ton installed (lazy import).
Examples + skills (LLM tooling)
Sandbox-based runnable scripts under examples/ — used as reference scaffolds when wiring up new titon consumers or extending Automaton. skills/ are LLM-invocable task files for the in-tree titon agents.
| Path | What it shows |
|------|--------------|
| examples/01-deploy-pool.ts | Deploy + verify config + admit a consumer |
| examples/02-register-as-automaton.ts | Register, walk full unstake cooldown |
| examples/03-slash-from-consumer.ts | Real-counterparty slash via TestConsumer |
| examples/04-indexer.ts | Decode events from raw txs (Argus pattern) |
| examples/operator-skeleton.ts | Minimal automaton operator — superseded by the Automaton binary; keep as reference |
| examples/consumer-template.tolk | Tolk consumer scaffold — the pattern Kronos/Fortuna consumers follow |
| skills/ | LLM-invocable task files (titon-internal): integrate-consumer, deploy-pool, run-operator, debug-exit-code, monitor-events |
Versioning
v0.x — on-chain ABI still evolving; SDK majors track contract upgrades. Pin exact versions in sibling repos. When a new schema version ships, increment the matching *_VERSION constant; consumers see SchemaDriftError on getAutomaton / getConsumer until they upgrade.
License
MIT.
