nodeapi

package
v0.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package nodeapi talks to a Teranode node over HTTP: JSON-RPC for mining and direct submission, the asset API for reading what got mined.

It is the funding and settlement side of a producer, and nothing a reader depends on: a reader verifies against a header source, never against a node it would have to trust.

Every response body is bounded before it is parsed and every declared count in a binary proof is checked against the bytes present before the SDK is allowed to allocate for it. The node is trusted to be honest about the chain, which is not the same as trusting it to be well behaved about response size (the same rule the headers package states).

The JSON shapes decoded here are the ones a Teranode node's RPC and asset API answer. The tests' response bodies are written to those shapes, not captured from a live node.

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrBodyTooLarge = errors.New("nodeapi: response body exceeds the bound")

ErrBodyTooLarge refuses a response above the bound.

View Source
var ErrDoubleSpent = errors.New("an input is spent by another transaction")

ErrDoubleSpent is a transaction one of whose inputs the node shows spent by another transaction. It never mines. Every error this package and the packages built on it return for that case wraps it, so errors.Is finds it whatever the broadcaster said.

View Source
var ErrNoID = errors.New("nodeapi: rpc: no request id")

ErrNoID refuses a call on an RPC with no request id. The library has no id of its own to fall back on, and any default would put one application's name on another's requests.

View Source
var ErrNotMined = errors.New("nodeapi: not mined")

ErrNotMined reports a transaction the node has not placed in a main-chain block yet, or a proof it cannot build yet. It is a state, not a failure: callers poll again after the next block.

View Source
var ErrSpendUnknown = errors.New("the node does not say whether the output is spent")

ErrSpendUnknown is the node's UTXO view saying neither that an output is unspent nor who spent it. Every error Spender returns wraps it, so a caller that must not mistake "no answer" for "unspent" tests one error. An output the answer leaves out, a status other than OK or SPENT, a SPENT with no spender, an answer that does not decode, a transaction the node does not serve (a 404, which a node also answers for a fully spent transaction it has pruned) and a failed read are all this. The status NOT_FOUND is one of them: a node answers it for an output it holds no record of, which it has pruned or could not read, as well as for one it never stored.

Functions

func IsHTTP

func IsHTTP(err error, status int) bool

IsHTTP reports whether err is an HTTPError with the given status.

func ProofFor

func ProofFor(raw []byte, txid string) (*transaction.MerklePath, error)

ProofFor parses a BUMP someone else supplied as the proof for txid, and refuses one that does not name txid at its leaf level. Every source of a proof goes through here: the bytes come from a service, and a path for some other transaction would build a BEEF that no host accepts. The proof is held to the same bound as every answer this package reads.

Example

ProofFor is where every proof a service supplies enters: the guard walks it before the SDK parses it, and a proof that does not name the transaction it was asked for is refused, because it would verify perfectly and prove nothing.

package main

import (
	"crypto/sha256"
	"fmt"

	"github.com/bsv-blockchain/go-sdk/chainhash"
	"github.com/bsv-blockchain/go-sdk/transaction"

	"github.com/lightwebinc/bcommon/nodeapi"
)

func main() {
	txid := chainhash.Hash(sha256.Sum256([]byte("a transaction")))
	sibling := chainhash.Hash(sha256.Sum256([]byte("its neighbour")))
	isTxid := true
	raw := transaction.NewMerklePath(90, [][]*transaction.PathElement{{
		{Offset: 0, Hash: &sibling},
		{Offset: 1, Hash: &txid, Txid: &isTxid},
	}}).Bytes()

	mp, err := nodeapi.ProofFor(raw, txid.String())
	fmt.Println("asked for:", mp != nil, err)

	unrelated := chainhash.Hash(sha256.Sum256([]byte("some other transaction")))
	_, err = nodeapi.ProofFor(raw, unrelated.String())
	fmt.Println("asked for another:", err)

	_, err = nodeapi.ProofFor(raw[:len(raw)-1], txid.String())
	fmt.Println("truncated:", err)
}
Output:
asked for: true <nil>
asked for another: proof does not contain the txid
truncated: bump ends mid-structure

func WaitMined

func WaitMined(ctx context.Context, asset *Asset, txid string, poll time.Duration) (*transaction.MerklePath, uint32, error)

WaitMined polls until txid is placed in a main-chain block and its proof is served, or ctx ends. It returns the proof and the block height.

func WaitSettled added in v0.5.4

func WaitSettled(ctx context.Context, asset *Asset, tx *transaction.Transaction, poll time.Duration) (*transaction.MerklePath, uint32, error)

WaitSettled is WaitMined for a transaction the caller holds: while tx has not mined, each poll also asks the node whether one of its inputs is spent by another transaction, and returns that *SpentError (errors.Is ErrDoubleSpent) at once rather than waiting until ctx ends.

Types

type Asset

type Asset struct {
	// Base is the API root with no path suffix, e.g. http://node.example.com:20090.
	Base string
	// Client is optional; the default has a 30s timeout.
	Client *http.Client
}

Asset is a Teranode asset-HTTP-API client.

func (*Asset) BestHeader

func (a *Asset) BestHeader(ctx context.Context) (*Header, error)

BestHeader reads the tip.

func (*Asset) Block

func (a *Asset) Block(ctx context.Context, hash string) (*Block, error)

Block reads one block by hash.

func (*Asset) HashAtHeight

func (a *Asset) HashAtHeight(ctx context.Context, height uint32) (string, error)

HashAtHeight resolves a main-chain block hash by height through the paged block list, where offset counts back from the tip. The tip can move between the two reads, so the answer is checked against the height asked for and the read is retried when it does.

func (*Asset) Header

func (a *Asset) Header(ctx context.Context, hash string) (*Header, error)

Header reads one block header by hash.

func (*Asset) MerkleProof

func (a *Asset) MerkleProof(ctx context.Context, txid string) (*transaction.MerklePath, error)

MerkleProof returns the BRC-74 BUMP for a mined transaction, parsed.

The node answers 404 for a transaction it has not placed in a main-chain block and 500 for a proof with an empty path (a single-transaction block); both are ErrNotMined to a caller that will ask again after the next block.

The bytes are walked by the guard package before the SDK parses them, and the parse runs under a recover, so a malformed proof is an error and never a crash. The parsed path must also contain txid at its leaf level: a proof the node served for the wrong transaction verifies perfectly and proves nothing.

func (*Asset) Proof

func (a *Asset) Proof(ctx context.Context, txid string) (*transaction.MerklePath, uint32, error)

Proof asks once whether txid is mined and returns its proof and height if so, or ErrNotMined. It is WaitMined without the waiting, for a caller that collects proofs later rather than blocking on them.

func (*Asset) Spender added in v0.5.4

func (a *Asset) Spender(ctx context.Context, txid string, vout uint32) (string, error)

Spender reads the node's UTXO view of txid (/api/v1/utxos/{txid}/json) and names the transaction that spent output vout, or "" with a nil error when the node shows the output unspent (status OK). Only those two answers are evidence. Anything else is an error wrapping ErrSpendUnknown, never "": see ErrSpendUnknown for what the node answers that is not evidence. A transaction the node does not serve is also an *HTTPError with status 404 (IsHTTP).

A caller that acts on "unspent", such as returning a coin to a pool, acts only on a nil error and "", and treats an error as undecided: it changes nothing and asks again later.

func (*Asset) SpentElsewhere added in v0.5.4

func (a *Asset) SpentElsewhere(ctx context.Context, tx *transaction.Transaction) error

SpentElsewhere returns a *SpentError for the first input of tx the node shows spent by another transaction, or nil when there is none: then tx can still mine as far as its inputs go. An input the node cannot answer for (Spender's ErrSpendUnknown: a parent it does not serve, a status that is neither OK nor SPENT, a read that failed) is passed over, so this refuses only on the node's positive word. nil is therefore not evidence that tx's inputs are unspent; a caller that needs that asks Spender per input.

func (*Asset) TxMeta

func (a *Asset) TxMeta(ctx context.Context, txid string) (*TxMeta, error)

TxMeta reads a transaction's placement. A 404 is ErrNotMined: for a submission in flight, "never seen" and "not placed yet" are one state.

func (*Asset) TxRaw

func (a *Asset) TxRaw(ctx context.Context, txid string) ([]byte, error)

TxRaw returns a transaction's raw serialisation. A node may answer in the raw form or in Extended Format (BRC-30), the raw form with each input's previous output added, as a Teranode asset API does; either is walked by guard.RawTransaction and the raw form returned, so a caller always gets bytes guard.ParseTransaction reads. The previous outputs an Extended Format answer carries are dropped.

type Block

type Block struct {
	Hash       string `json:"hash"`
	Height     uint32 `json:"height"`
	CoinbaseTx struct {
		TxID    string `json:"txid"`
		Outputs []struct {
			Satoshis      uint64 `json:"satoshis"`
			LockingScript string `json:"lockingScript"`
		} `json:"outputs"`
	} `json:"coinbase_tx"`
}

Block is the subset of /api/v1/block/{hash}/json read here: the coinbase outputs are what fund the pool.

type HTTPError

type HTTPError struct {
	Method string
	Path   string
	Status int
	Body   string
}

HTTPError is a non-2xx answer, kept so callers can distinguish a 404 from a transport failure.

func (*HTTPError) Error

func (e *HTTPError) Error() string
type Header struct {
	Hash       string `json:"hash"`
	Prev       string `json:"previousblockhash"`
	MerkleRoot string `json:"merkleroot"`
	Height     uint32 `json:"height"`
}

Header is the subset of a block header answer a caller needs.

type Info

type Info struct {
	Blocks      int64 `json:"blocks"`
	Connections int64 `json:"connections"`
	Version     int64 `json:"version"`
}

Info is the subset of getinfo used here. Teranode registers getblockcount but answers "Command unimplemented", so the height comes from here.

type RPC

type RPC struct {
	// URL is the RPC endpoint, e.g. http://node.example.com:21292/.
	URL  string
	User string
	Pass string
	// ID is the JSON-RPC request id, sent verbatim on every call. It is the
	// application's to choose and it is required: Call refuses an empty one
	// before any request is sent rather than choosing one itself.
	ID string
	// Client is optional; the default has a 60s timeout, which is long
	// because generatetoaddress mines inline. The lever for a slow node is
	// the batch size, not this.
	Client *http.Client
}

RPC is a Teranode JSON-RPC client.

func (*RPC) Call

func (r *RPC) Call(ctx context.Context, method string, params []any, out any) error

Call issues one JSON-RPC call and unmarshals the result into out, which may be nil. An RPC-level error is returned as the node phrased it. An RPC with no ID is refused with ErrNoID before anything is sent.

func (*RPC) GenerateToAddress

func (r *RPC) GenerateToAddress(ctx context.Context, n int, addr string) ([]string, error)

GenerateToAddress mines n blocks paying the coinbase to addr and returns the block hashes. Keep n small: a large call can outlast the RPC timeout, which is why FundFromCoinbase mines in batches.

func (*RPC) GetInfo

func (r *RPC) GetInfo(ctx context.Context) (*Info, error)

GetInfo reads the node's view of the tip.

func (*RPC) SendRawTransaction

func (r *RPC) SendRawTransaction(ctx context.Context, rawHex string) (string, error)

SendRawTransaction submits standard-format hex straight to the node and returns the txid it acknowledged. This is the settlement leg with an ack; the TCP ingress has none.

type SpentError added in v0.5.4

type SpentError struct {
	Txid string
	// Input is the index of the input in Txid; Outpoint is what it spends,
	// txid.vout.
	Input    int
	Outpoint string
	By       string
}

SpentError names the input of Txid that the node shows spent by By.

func (*SpentError) Error added in v0.5.4

func (e *SpentError) Error() string

func (*SpentError) Unwrap added in v0.5.4

func (e *SpentError) Unwrap() error

Unwrap makes a *SpentError errors.Is ErrDoubleSpent.

type TxMeta

type TxMeta struct {
	BlockHashes    []string `json:"blockHashes"`
	BlockHeights   []uint32 `json:"blockHeights"`
	SubtreeIdxs    []int    `json:"subtreeIdxs"`
	MainChainIndex int      `json:"mainChainIndex"`
	IsCoinbase     bool     `json:"isCoinbase"`
}

TxMeta is the placement subset of /api/v1/txmeta/{txid}/json. The slices are parallel, one entry per block the transaction landed in, and MainChainIndex selects the main-chain one.

func (*TxMeta) Placement

func (m *TxMeta) Placement() (hash string, height uint32, err error)

Placement returns the main-chain block hash and height, or ErrNotMined when the node knows the transaction but has not placed it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL