husdindex

package
v1.49.21 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package husdindex is Step 3 of the chain-backed credit ledger: it makes the on-chain HUSD balance the source of truth and the commerce DB a cache.

It has two decomplected halves:

  • client.go: a minimal JSON-RPC READ client (net/http, encoding/json) for the Hanzo EVM — block height, ERC-20 balanceOf, and Transfer logs. No geth, no cgo: reads only, so the indexer builds and the live read-proof runs CGO-free.
  • index.go: the Sync projector — scan Transfer events INTO org addresses and project each, idempotently, into the ledger tagged by its off-chain issuance bucket. Pure logic over small interfaces, unit-tested with fakes.

The invariant it upholds: an org's indexed balance == its on-chain balanceOf(address), reconciled to the cent (Σ projected credits − debits).

Index

Constants

View Source
const TransferTopic0 = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"

TransferTopic0 is keccak256("Transfer(address,address,uint256)") — the ERC-20 Transfer event signature topic. Verified against luxfi/crypto.Keccak256 in the tests so a typo can never silently mis-filter logs.

Variables

This section is empty.

Functions

func OnChainBalanceCents

func OnChainBalanceCents(ctx context.Context, br BalanceReader, addr string, decimals int) (int64, error)

OnChainBalanceCents returns the org's on-chain HUSD balance in whole cents.

Types

type AddressBook

type AddressBook interface {
	// Addresses returns the set of org HUSD addresses to watch.
	Addresses(ctx context.Context) ([]string, error)
	// OrgFor returns the orgID owning a (lowercased) address, ok=false if unknown.
	OrgFor(addr string) (string, bool)
}

AddressBook maps derived org addresses (lowercased) → orgID, and lists the addresses to filter Transfer logs by. Production derives it from the org list + master seed (treasury.AddressForOrg); tests supply it directly.

type BalanceReader

type BalanceReader interface {
	BalanceOf(ctx context.Context, addr string) (*big.Int, error)
}

Reconcile fetches an org's on-chain balanceOf and returns it in cents together with any sub-cent remainder — the exact check the migration + monitoring assert against the indexed ledger balance. balanceReader is *Client (or a fake). Kept separate from Sync so reconciliation reads chain truth directly.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client reads HUSD state over JSON-RPC. It is safe for concurrent use.

func NewClient

func NewClient(rpcURL, tokenAddr string) *Client

NewClient builds a read client for the given RPC endpoint + HUSD token.

func (*Client) BalanceOf

func (c *Client) BalanceOf(ctx context.Context, addr string) (*big.Int, error)

BalanceOf returns the token balance of addr in base units (wei).

func (*Client) BlockNumber

func (c *Client) BlockNumber(ctx context.Context) (uint64, error)

BlockNumber returns the current chain head.

func (*Client) TransfersInTx

func (c *Client) TransfersInTx(ctx context.Context, txHash string, addrs []string) ([]Transfer, error)

TransfersInTx returns the HUSD Transfer events emitted by ONE mined tx whose recipient is one of addrs. It reads eth_getTransactionReceipt (not eth_getLogs) so a just-submitted mint can be projected the instant it is mined, without waiting for a block-range scan. addrs match case-insensitively; only logs from the HUSD token contract are considered. Returns nil if the tx is not yet mined (no receipt) — the caller falls back to the background Sync.

func (*Client) TransfersTo

func (c *Client) TransfersTo(ctx context.Context, addrs []string, fromBlock, toBlock uint64) ([]Transfer, error)

TransfersTo returns HUSD Transfer events whose recipient (topic2) is one of addrs, between fromBlock and toBlock (inclusive). addrs are matched case-insensitively.

type Config

type Config struct {
	Decimals      int
	Confirmations uint64 // blocks below head to treat as final (reorg safety)
	StartBlock    uint64 // first block to scan when the cursor is empty
	MaxRange      uint64 // 0 → default 2000
}

Config parameterizes an Indexer.

type Credit

type Credit struct {
	OrgID string
	// Subject is the ledger DestinationId to credit (the mint's subject; for an
	// external payin with no issuance it is the org slug). This is where the
	// balance read looks, so the projection MUST write to it, not to OrgID.
	Subject     string
	AmountCents int64
	Tag         string // billing/bucket ledger tag (credit:husd | husd)
	Test        bool   // ledger Test partition (matches the mint / balance read)
	TxHash      string
	LogIndex    uint64
	DedupKey    string // txHash:logIndex — idempotency key for the projection
}

Credit is one projected on-chain HUSD credit for an org.

type Cursor

type Cursor interface {
	Last(ctx context.Context) (uint64, error)
	Save(ctx context.Context, block uint64) error
}

Cursor persists the last fully-scanned block so a restart resumes, not restarts.

type Indexer

type Indexer struct {
	// contains filtered or unexported fields
}

Indexer projects on-chain HUSD Transfers into the ledger.

func NewIndexer

func NewIndexer(reader Reader, ledger Ledger, lookup IssuanceLookup, cursor Cursor, book AddressBook, cfg Config) *Indexer

NewIndexer builds an Indexer.

func (*Indexer) ProjectTx

func (ix *Indexer) ProjectTx(ctx context.Context, txHash string) (int, error)

ProjectTx synchronously projects the HUSD Transfers in ONE mined tx (a just-submitted treasury mint) into the ledger, so the balance reflects the mint immediately rather than after the next Sync tick. It funnels through the SAME idempotent project() the background Sync uses (dedup on txHash:logIndex), so a later Sync over the same block never double-credits. Returns the number of transfers projected (0 if the tx touched none of our addresses — e.g. it is not yet mined; the background Sync will pick it up once final).

It does NOT advance the Sync cursor: the background Sync remains the authoritative, gap-free scanner + reconciler; ProjectTx is a latency optimization layered on top, never a replacement.

func (*Indexer) Sync

func (ix *Indexer) Sync(ctx context.Context) (int, error)

Sync scans new Transfer events up to (head − confirmations) and projects each into the ledger. It is idempotent: the ledger dedups on DedupKey, so a re-scan (overlap after a restart, or a re-run) never double-credits. Returns the number of transfers projected.

type IssuanceLookup

type IssuanceLookup interface {
	ByTxHash(ctx context.Context, txHash string) (*treasury.Issuance, error)
}

IssuanceLookup resolves a mint tx to its off-chain issuance so the projected credit is tagged into the right bucket (a fungible Transfer carries no tag). Returns nil for a transfer not originated by a treasury mint (external payin).

type Ledger

type Ledger interface {
	Credit(ctx context.Context, c Credit) error
}

Ledger is where the indexer PROJECTS on-chain HUSD credits. In production this writes an idempotent tagged deposit into the commerce transaction ledger (so the existing GET /v1/billing/*/balance reads chain-sourced balance unchanged); in tests it is a fake. Credit MUST be idempotent on Credit.DedupKey — the indexer re-scans overlapping ranges after restarts and must never double-credit.

type Reader

type Reader interface {
	BlockNumber(ctx context.Context) (uint64, error)
	TransfersTo(ctx context.Context, addrs []string, fromBlock, toBlock uint64) ([]Transfer, error)
}

Reader is the subset of the RPC client the projector needs. *Client satisfies it; unit tests inject a fake so Sync is proven without a chain.

type Transfer

type Transfer struct {
	From     string // 0x, lowercased
	To       string // 0x, lowercased
	ValueWei *big.Int
	TxHash   string
	LogIndex uint64
	Block    uint64
}

Transfer is a decoded ERC-20 Transfer event.

func (Transfer) DedupKey

func (t Transfer) DedupKey() string

DedupKey is the idempotency key for projecting this transfer: unique per log.

type TxReader

type TxReader interface {
	TransfersInTx(ctx context.Context, txHash string, addrs []string) ([]Transfer, error)
}

TxReader reads the HUSD Transfer logs contained in a single already-mined tx. *Client satisfies it. It is a NARROW extension of Reader (kept separate so the Sync fakes need not implement it) used by ProjectTx for immediate, synchronous projection of a just-submitted mint.

Jump to

Keyboard shortcuts

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