tbc-tx-parser
v0.3.0
Published
Decode TBC txraw hex into structured transaction facts.
Maintainers
Readme
tbc-tx-parser
An SDK for decoding raw TBC transactions (txraw) into on-chain facts and asset flows for wallets, block explorers, and debugging tools.
Installation
npm install tbc-tx-parserpnpm add tbc-tx-parserSupported Features
Currently supported:
- Native TBC transfers, coinbase rewards, source-linked returns, fees, and common locking-script outputs.
- FT / StableCoin mint, transfer, merge, and mixed FT/TBC transactions.
- NFT collection mint, ordinary NFT mint/transfer, and hash-only Hold transfer destinations.
- OrderBook placement, cancellation, update, and matching semantics.
- PoolNFT create, init, increase liquidity, consume liquidity, TBC-to-FT swaps, FT-to-TBC swaps, merge FT in pool, standalone LP merge, and LP burn.
- PiggyBank / TBCTimelock freeze and unfreeze, with block-height and Unix-timestamp lock domains.
- HTLC native/token deploy, preimage withdrawal, and timeout refund.
- StableCoin freeze/unfreeze and standalone LP transfers.
The parser never accesses the network or fetches prevouts by txid. The caller must provide inputUtxos for every transaction input.
Coinbase reward transactions have no spendable input prevouts and are the only exception that may omit inputUtxos.
0.3.0: tbc-contract 1.7.3 with historical compatibility
This release pins [email protected] and [email protected]. The runtime alias
tbc-contract-172 pins the original [email protected] AMM codecs so existing
143-byte pools remain decodable alongside 1.7.3 168-byte pools. Renamed
implementations remain in their existing business families:
| Internally recognized implementation | Public business family |
| --- | --- |
| TBC20Standard | ft |
| TBC20Stablecoin | stablecoin |
| TBC20LP | ftlp asset, poolNft actions |
| TBC721Standard | nft |
| TBCAMM | poolNft |
| TBCLOP | orderBook |
| TBCTimelock | piggyBank |
| TBCHTLC | htlc |
Contract implementations and generations are internal parsing details. Public results normalize them into classification.protocol, classification.action, and asset flows; scripts and pool metadata do not expose contract-version fields. The top-level result.version is the transaction wire-format version, not a contract version. Historical contract decoding remains available. This package is a decoder and adds no stablecoin-pool or deprecated PoolNFT v1 transaction-building API.
New contract generations use the existing public asset schema. Immutable Code identity is internal; contractId remains optional: supply a verified indexed ID or trace the origin. The parser never substitutes an arbitrary transfer txid for a missing asset ID. Code recognition checks the complete pinned template; a terminal marker alone is insufficient. Associated Tape data must belong to the same parent and the expected adjacent output.
Modern action checks include token conservation and input amount slots, StableCoin certificate supply changes, AMM reserve/LP/fee transitions, order pairing/remainders/settlement, and HTLC preimage/timelock conditions. Unsupported or incomplete evidence is reported through classification.issues. These checks are not signature execution or a proof that a node accepted the transaction. Caller-provided prevouts are trusted as supplied chain evidence; explicit outpoint mismatches are rejected, but callers must authenticate raw parents independently.
All contract generations use the same asset-flow conventions. assetFlows describes transfers, locks, releases, minting, burning and source-linked returns. Contract carriers and full pool reserves stay in chain; they are not additional payments. Repeated payments remain separate, and unknown positive outputs remain unrecognized TBC transfers. An address endpoint is { address }; transaction output locations stay in chain.outputs. Malformed evidence is reported through classification.issues and unrecognized flow context.
AMM 1.7.3 retains the public poolNft family and adds optional pool metadata:
lpFeeRate, taxFeeRate, serviceProviderFeeRate, protocolFeeRate (basis points),
feeAddress (the single P2PKH recipient for non-LP fee shares), and
serviceProviderTagHex (the exact immutable tag bytes, including empty/non-UTF-8
labels). serviceFeeRate is the sum of the four rates; lpPlan is absent for
1.7.3 pools. Existing 1.7.2 and legacy PoolNFT metadata keeps its original shape.
The new fields appear in decoded Pool Tape and pool-creation asset metadata.
The parser authenticates each generation's full Code template, checks Code/Tape
version and authorization consistency, and verifies the fee recipient against the
Code's committed script hash before accepting a standard pool transition.
Plan 6 decoding distinguishes the historical 130/80-bp total/LP schedule from the current 330/200-bp schedule using the on-chain total fee rate. Legacy locked LP tapes and native pool payouts below 500 satoshis are also handled.
Basic Usage
import { decodeTxRaw } from 'tbc-tx-parser';
const result = await decodeTxRaw(txraw, {
inputUtxos,
});Classification TBC display unit
Native TBC values in classification.description use satoshis by default. Callers may request TBC display values without changing any structured amount fields:
const result = await decodeTxRaw(txraw, {
inputUtxos,
display: {
tbcUnit: 'tbc', // "satoshi" (default) or "tbc"
},
});For example, the same description can be rendered as either:
Sends 100,000,000 satoshis to address ...; network fee: 80 satoshis.
Sends 100 TBC to address ...; network fee: 0.000080 TBC.This option affects only native amounts embedded in classification.description. Fields such as chain.outputs[].satoshis, chain.fee.satoshis, and assetFlows[].amount.satoshis always remain exact satoshi values. Normal TBC amounts use up to six decimal places with trailing zeroes removed; network fees use exactly six decimal places.
To switch units after a transaction has already been decoded, render the description directly from the existing result:
import { renderTransactionDescription } from 'tbc-tx-parser';
const description = renderTransactionDescription(result, {
tbcUnit: 'tbc',
});renderTransactionDescription is synchronous and does not parse txraw, fetch input UTXOs, mutate the result, or repeat protocol classification. This makes it suitable for a UI unit selector while keeping structured amounts unchanged.
Input UTXOs
Asset-flow decoding requires the prevout for every input. Passing only txraw throws because the parser cannot reliably determine where assets originated:
const result = await decodeTxRaw(txraw, {
inputUtxos: {
'prevTxid:0': {
satoshis: 1000000,
lockingScript: '76a914...88ac',
},
},
});The key format is:
`${prevTxid}:${vout}`An array ordered by input may also be used:
const result = await decodeTxRaw(txraw, {
inputUtxos: [
{
satoshis: 1000000,
lockingScript: '76a914...88ac',
},
],
});Contract-asset inputs also require their paired unspendable tape prevouts:
- FT / StableCoin Code input:
associatedTapeisvout + 1in the same transaction and must be a zero-satoshiFTape. - StableCoin transfer/merge Code inputs: also provide the canonical StableCoin ID as
contractId. The complete ID is not directly encoded in the current StableCoin script, so obtain it from an authoritative indexer or by tracing the StableCoin UTXO lineage; never substitute the current transaction ID, an OrderBook ID, or a Pool LP ID. - NFT Code input:
associatedTapeisvout + 2in the same transaction and must be a zero-satoshiNTape; the same spend must also contain the corresponding NFT Hold input.associatedOriginshould provide the origin UTXO referenced by the Code script. A Mint NHold origin proves collection membership, while an ordinary P2PKH origin proves a standalone NFT that does not use a collection UTXO. - PoolNFT state input: when the state prevout does not directly carry the PoolNFT tape, provide the adjacent PoolNFT OP_RETURN tape at
vout + 1asassociatedTapeso the parser can compare the previous and current pool states.
If a paired tape is missing, scripts do not match, or an input prevout is not a valid script for the asset, the result does not emit strongly verified protocol asset flows. Related positive-value locking outputs are displayed as TBC flowing to an address, a known contract, or an unknown locking script.
Return Value
const result = await decodeTxRaw(txraw, { inputUtxos });
result.txid;
result.version;
result.locktime;
result.size;
result.chain.inputs;
result.chain.outputs;
result.chain.fee;
result.assetFlows;
result.unspendableAssetFlows;
result.classification;Common fields:
chain.inputs: transaction inputs and decoded prevout facts.chain.outputs: transaction outputs, addresses, script classifications, and protocol tape state.chain.fee: transaction fee.assetFlows: primary asset-flow data for application displays.unspendableAssetFlows: optional FT declarations excluded from normal asset flows after a verified token-amount conservation violation.classification: deterministic transaction type, protocol conformance, and an English result description.
The SDK exposes transaction classification separately from asset flows so applications can show a concise conclusion without treating weak protocol evidence as definitive:
result.classification.protocol; // e.g. "orderBook"
result.classification.action; // e.g. "match"
result.classification.conformance; // standard | nonstandard | indeterminate
result.classification.description; // fact-based English conclusion for this transaction
result.classification.issues; // optional stable issue codesstandard means the transaction matches a supported protocol action and every invariant currently implemented by this SDK for that action. It is not a consensus-validity or spendability guarantee. nonstandard means the protocol family or action is recognizable but an implemented rule is violated. indeterminate means the available chain evidence is insufficient to run the relevant checks; missing associated prevouts are not reported as protocol violations.
For legacy non-mint FT transactions, classification includes token-amount conservation. If declared FT outputs exceed fully verified FT inputs, the result is nonstandard with issue code FT_OUTPUT_AMOUNT_EXCEEDS_VERIFIED_INPUT. Those FT declarations are excluded from assetFlows and returned in unspendableAssetFlows; their decoded scripts also remain available in chain.outputs. The field name describes the SDK's conservative application view and does not independently prove consensus-level unspendability. If the relevant FT input amounts are incomplete, conformance is indeterminate instead of reporting a violation.
protocol and action form the structured transaction type. Their TypeScript definitions are correlated, so unsupported combinations are rejected at compile time. Classification confidence remains an internal parsing concern rather than a public result field.
description is generated from the classified action together with the transaction's verified assetFlows, endpoint types, and network fee. It summarizes the primary business action instead of replacing the full chain or asset-flow data, for example: Sends 1,000 PFT to address 1Pz2eAg3JjXQSqs5nGQj7XGhPzvBEbaiP9; network fee: 445 satoshis. A single verified destination includes its full address, multisignature address, pool ID, or hash; callers decide whether to shorten it in the UI. Multiple destinations are deduplicated and summarized using one total amount and a destination count, never per-destination amounts. Source-linked self flows remain available in assetFlows, but are omitted from descriptions that already contain an external transfer. Transactions containing only self flows use neutral Internal transfer ...; no funds were sent ... wording without describing UTXO split/merge mechanics or unknown attached data. Protocol carrier values are omitted from material secondary transfers unless they also match a standalone payment output. When evidence is incomplete, the wording becomes explicitly cautious and does not invent amounts or actions. The final network fee clause is the chain transaction fee only; protocol-specific trading or liquidity fees are not folded into it.
The first classification release is intentionally fixture-driven: it covers native TBC coinbase, transfer, and MultiSig transactions; StableCoin bootstrap and token actions; and the FT, NFT, PiggyBank, OrderBook, and PoolNFT actions already represented by the SDK test corpus. Unknown contracts remain indeterminate instead of receiving a guessed definitive type.
The current version also keeps internal semantic, protocolDetails, and unsupported data private. These remain internal protocol-classification and diagnostic capabilities and can later be exposed through an optional diagnostic result if an explorer or protocol-details view needs them.
Core assetFlows structure:
result.assetFlows[0]?.purpose; // transfer | self | fee | lock | release | mint | burn | unknown
result.assetFlows[0]?.direction; // in | out | self | unknown
result.assetFlows[0]?.asset; // { type: 'tbc' | 'ft' | 'stablecoin' | 'nft', ... }
result.assetFlows[0]?.amount; // Present for divisible assets such as TBC/FT; absent from NFT flows
result.assetFlows[0]?.from; // address / addresses / hash / poolId / txid+vout / outpoints
result.assetFlows[0]?.to; // address / addresses / hash / poolId / script / txid+vout
result.assetFlows[0]?.context; // optional { protocol, recognized, action?, markers? }Canonical flow combinations and endpoint rules:
| Purpose | Direction | Endpoints |
| --- | --- | --- |
| self | self | to matches a verified source endpoint; this covers address returns and consolidations without asserting wallet intent |
| transfer | out | both from and to are present |
| mint | in | no from; to is present only when a recipient is decoded |
| burn | out | from is present; no to |
| lock | out | both from and to are present |
| release | in | both from and to are present |
The parser never emits public purpose: "transfer" with direction: "self"; a verified source-linked return is normalized to self/self. A self flow retains both endpoints even when they are equal, but its presence alone does not make the whole transaction a self-transfer. When a consumed asset's source address or hash is unavailable, from uses the verified input { txid, vout }, or outpoints for multiple source UTXOs, instead of guessing an address. Explicitly partial unknown flows may keep optional endpoints.
A mint may omit to when asset creation is verified but no recipient identity is decoded. PoolNFT create is the canonical example: its Pool Code output is a state anchor, so the contract outpoint remains in chain.outputs and is not presented as an asset-flow destination.
Protocol-specific flows retain a generic purpose and may include context. For example, PiggyBank freeze and OrderBook placement both use purpose: "lock", while context.protocol and context.action distinguish piggyBank/freeze from orderBook/placeBuy or orderBook/placeSell. Recognized OrderBook context is attached only to placement locks. Match settlement and match fee-recipient payments are plain transfers, cancellation refunds are releases, and recreated partial-fill or update order state is read from chain.outputs[].script.data.orderBook rather than exposed as an asset flow. OrderBook fee-recipient payments use purpose: "transfer". PoolNFT reserve snapshots are likewise read from chain.outputs[].script.data.poolNft; assetFlows contains only the pool's observable transfers, LP mint/burn, source-linked returns, and visible fee flows. Unknown contract carriers remain purpose: "transfer" and use protocol: "unknown" with recognized: false; marker evidence may be included without guessing a protocol name. When no source address or hash can be extracted from a locking script, the destination is represented by to.script.
Display guidance:
- Summarize what a transaction does from
assetFlows, script facts, and application context. - Use
assetFlowsas the primary source for showing how assets move. - Treat
selfas a source-linked structural flow. Do not present it as change or returned funds; omit it from the primary description when externaltransferflows are also present. - Read
chain.inputs/chain.outputsscript.datawhen displaying contract state or protocol tape fields.
Amount Units
TBC amounts use satoshis:
result.chain.outputs[0].satoshis;
result.chain.fee?.satoshis;FT / StableCoin / PoolNFT LP amounts use raw smallest-unit strings:
result.assetFlows[0]?.amount;NFTs are indivisible, so ordinary NFT flows do not contain amount. supply is collection-creation metadata and does not represent a quantity for an individual NFT. An NFT minted from a collection Mint NHold may include collectionId; a standalone NFT originating from ordinary P2PKH retains only its own contractId and has no collection fields.
PoolNFT Notes
PoolNFT is a composite protocol. Decoding considers all of the following rather than relying only on the PoolNFT tape:
- Previous pool state input.
- Current pool state output.
- PoolNFT OP_RETURN tape.
- FT A / FTLP movements.
- TBC reserve delta, payout, and fee output.
- LP token identity, partial hash, and burn receiver.
The active PoolNFT fixture corpus contains exactly 18 real testnet broadcasts: the nine operations above for one unlocked and one public-key-locked PoolNFT v2 pool. Both modes use independent ordinary FT contracts and independent pools. The locked cases have withLock: true; the unlocked cases have withLock: false; all 18 cases use withLockTime: false.
Current PoolNFT coverage includes the primary paths and several false-positive protections:
- Reserve delta, FT A amount, and fee relationships for both swap directions.
- FT A, LP, and TBC relationships for increase / consume liquidity.
- Returned LP amounts, no-visible-fee cases, and small TBC rounding boundaries during consume.
- Current FT A reserve validation for merge FT in pool.
- Standalone FTLP merges remain FT self flows without recreating PoolNFT state.
- Standalone FTLP burns remain FT burn flows without recreating PoolNFT state.
- Multiple PoolNFT tapes or multiple previous PoolNFT state inputs are currently treated as unsupported and do not produce PoolNFT movements.
If the caller omits the previous pool state's associated tape, PoolNFT may still be recognized as heuristic, but a complete state delta cannot be calculated.
Regenerating PoolNFT broadcast fixtures
The runner is testnet-only and resumable. Its key file, public transaction journal, and generated candidates remain ignored under scripts/. Preserve the journal until fixture generation finishes: it records both the 18 target transactions and the preparation transactions needed to resume safely. Preparation transactions are never promoted as fixtures.
Run the read-only checks first:
TBC_KEYS_FILE=/absolute/path/to/tbc-test-addresses.json \
TBC_POOL_NFT_JOURNAL=/absolute/path/to/pool-nft-broadcast-journal.json \
pnpm pool-nft:preflight
TBC_POOL_NFT_JOURNAL=/absolute/path/to/pool-nft-broadcast-journal.json \
pnpm pool-nft:statusBroadcast each lifecycle only when testnet spending is intended. TBC_BROADCAST=1 is an explicit mutation guard and these commands spend testnet UTXOs:
TBC_BROADCAST=1 TBC_KEYS_FILE=/absolute/path/to/tbc-test-addresses.json \
TBC_POOL_NFT_JOURNAL=/absolute/path/to/pool-nft-broadcast-journal.json \
pnpm pool-nft:run unlocked
TBC_BROADCAST=1 TBC_KEYS_FILE=/absolute/path/to/tbc-test-addresses.json \
TBC_POOL_NFT_JOURNAL=/absolute/path/to/pool-nft-broadcast-journal.json \
pnpm pool-nft:run lockedAfter status reports all 18 targets complete, generate reviewed candidates without broadcasting:
TBC_POOL_NFT_JOURNAL=/absolute/path/to/pool-nft-broadcast-journal.json \
pnpm pool-nft:fixturesThe generator re-fetches every target raw transaction from testnet, verifies its txid, decodes its prevouts, validates lock flags, rejects public purpose: "state" flows, and writes candidates under scripts/pool-nft-generated/. For these legacy PoolNFT fixtures, pool reserve state belongs under chain.outputs[].script.data.poolNft; only observable transfers, mint/burn, source-linked returns, and visible fees belong in assetFlows.
Tests and broadcast evidence
Repository development uses [email protected] and pnpm-lock.yaml. Install dependencies with pnpm install --frozen-lockfile; do not commit an npm lockfile. Consumers can install the published package with either npm or pnpm.
On 2026-09-30, 568 transactions were freshly broadcast to testnet and fetched back after its reset:
- 513 transactions covering the supported contract actions: FT, NFT, StableCoin, AMM/LP, native/token HTLC, timelock, native/token-pair orders, legacy StableCoin, and funding transactions.
- 55 additional legacy contract transactions: FT, NFT, MultiSig, ordinary/locked PoolNFT v2, and OrderBook.
On 2026-10-12, another 72 previously broadcast/refetched 1.7.3 AMM lifecycle transactions were captured: 24 each from JS, Rust and Go, covering public and controller pools, plain/timelocked LP, liquidity, both swaps and LP transfers. No new signing or broadcasting was needed for this parser update.
The checked-in tests/fixtures/contract-172-live.json, contract-legacy-live.json
and contract-173-amm-live.json contain raw transactions, complete raw parents,
and input context. The compatibility runner checked native builders against the pinned JS reference and script interpreters before broadcasting. Parser regressions check txids, parent bindings, fees, business classification, and malformed evidence. This is the covered broadcast matrix, not a claim to test every possible contract combination.
pnpm build
pnpm typecheck
pnpm test # deterministic offline regressions, including fresh captured transactions
pnpm test:live # read-only exact-raw refetch of all 640 captured txids; no private key
pnpm test:all # offline regressions plus the current live refetchStart with the decode example guide. It links one real transaction per business scenario, and index.json lists all 640 results with complete txids and source corpora:
tests/decode/new-contracts/<family>/: 387 transactions for the modern implementations.tests/decode/legacy-contracts/<family>/: 68 newly broadcast legacy compatibility transactions.tests/decode/native/: 185 ordinary TBC transactions, including funding and multisig; these do not count as new-contract action coverage.
Each case-*.json is the actual public return value. Case metadata, generation grouping and broadcast evidence belong to the index, not the parser's result. Filenames use business actions and txids, without transaction-builder language categories. Default integration tests compare every result against a fresh offline decode. Public compatibility tests also verify endpoints, pool deltas, LP redemption and order settlement independently of the snapshots.
pnpm generate:decode-cases # rebuild the accepted transaction results offline
node tests/tools/generate-decode-cases.mjs --check # verify without writing; run after buildtests/historical-decode/ preserves the older examples and generators, including historical broadcasts, synthetic cases and malformed-evidence cases. They are not evidence for the 568 broadcasts after the reset. pnpm generate:historical-decode-cases runs their original generators and may require historical transactions that no longer exist. Mutation tests are labeled separately and never count as accepted broadcasts.
tests/historical-live preserves the previous network-dependent cases. pnpm test:historical-live is optional and may fail after a testnet reset because those historical txids no longer exist. Default regressions use captured raw parents and remain runnable after future resets. Capture tools under tests/tools consume successful broadcast evidence and never require private keys.
Notes
- The SDK does not access the network.
- The SDK does not accept a
networkparameter. - The SDK does not query transactions by
txid. - The SDK does not fetch input prevouts; callers must provide
inputUtxos. - The SDK exposes top-level
classification; internalsemantic,protocolDetails, andunsupportedfields remain private. - TBC addresses use canonical TBC address encoding.
tbc-lib-jsandtbc-contractare runtime dependencies installed with the npm package.docs/,scripts/, andtests/are not published to npm; the package contains onlydist,README.md, andpackage.json.
