tbc-tx-parser
v0.1.10
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 freeze / unfreeze.
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.
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 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 | state | 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 \
npm run pool-nft:preflight
TBC_POOL_NFT_JOURNAL=/absolute/path/to/pool-nft-broadcast-journal.json \
npm run 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 \
npm run 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 \
npm run 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 \
npm run 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/. Pool reserve state belongs under chain.outputs[].script.data.poolNft; only observable transfers, mint/burn, source-linked returns, and visible fees belong in assetFlows.
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.
