deposits

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package deposits is the kit's deposit-scanning core (contracts/ go-client-api.md §deposits, R6): tolerant block parsing and watch-set classification into the ChainTransferLedger (parse.go), FR-023 credit-condition evaluation and the transactional block/credit write paths (state.go), and the checkpointed ascending scanner engine (scanner.go).

Index

Constants

View Source
const (
	TypeURLMsgSend      = "/cosmos.bank.v1beta1.MsgSend"
	TypeURLMsgMultiSend = "/cosmos.bank.v1beta1.MsgMultiSend"
)

Only these two message shapes are ever unpacked (R6): the SDK TxConfig decoder's strict unknown-type rejection fails on this chain's custom-module traffic, so TxRaw → TxBody + AuthInfo are decoded with gogoproto and every other Any is kept packed.

View Source
const DefaultConfirmations = 2

DefaultConfirmations is the published recommended launch value (FR-028): CometBFT is single-block-final (1 committed block = protocol finality), so the recommended launch default of 2 is a node-operations sanity buffer for node comparison, monitoring, and incident detection — not a reorg defence. The supported range is 1..12 and the final threshold is an exchange risk decision.

View Source
const DefaultPollInterval = 2 * time.Second

DefaultPollInterval is the block poll cadence. The kit's client transports are unary-only, so a short poll interval is the accelerator-equivalent of the optional WebSocket NewBlock subscription (documented deviation — FR-027 polling remains the primary mechanism either way).

View Source
const OutboxTopicDepositCredited = "deposit.credited"

OutboxTopicDepositCredited is the transactional-outbox topic emitted exactly once per credited deposit (data model §3b invariant).

View Source
const ReviewOpIndexBase uint32 = 1 << 16

ReviewOpIndexBase offsets event-derived UNATTRIBUTED_REVIEW ledger rows above every body-decoded op index (bounded by 2 × the message's coin count), so tx-scoped review entries never collide with decoded transfers under UNIQUE(chain_id, tx_hash, message_index, op_index).

Variables

View Source
var ErrCreditGateClosed = errors.New("deposits: credit gate closed inside credit transaction")

ErrCreditGateClosed aborts an in-flight credit: the gate re-check inside the crediting transaction found crediting paused, scan-without-credit engaged, or a chain-review condition open. The CREDITABLE→CREDITED flip and outbox enqueue never happen, so the record stays CREDITABLE for a later pass. It is a benign TOCTOU guard (a batch's gate load precedes each credit tx), never a failure — callers must stop the batch on it, not surface it as an error.

Functions

func ConfirmationCount

func ConfirmationCount(latestHeight, blockHeight uint64) uint64

ConfirmationCount is latest − block_height + 1 — recomputed, never stored authority (data model §3b).

func CreditDeposit

func CreditDeposit(ctx context.Context, store storage.Store, d storage.DepositRecord, now time.Time) error

CreditDeposit flips CREDITABLE → CREDITED and enqueues the credited event in one transaction (transactional outbox — the §3b exactly-once invariant). Callers must have evaluated DecisionCredit first. The credit gate is re-validated transactionally with the flip: a pause / chain-review opened after the caller's gate load must not let a batch keep crediting (ErrCreditGateClosed aborts, leaving the record CREDITABLE).

func DepositDedupKey

func DepositDedupKey(d storage.DepositRecord) string

DepositDedupKey is the outbox dedup key making the credited event exactly-once per FR-024 unique deposit key.

func TxHashHex

func TxHashHex(txBytes []byte) string

TxHashHex is the CometBFT transaction hash: uppercase hex SHA-256 of the raw tx bytes.

Types

type BlockEventEntry

type BlockEventEntry struct {
	// EventIndex is 2 × the finalize_block_events index (+1 for OUT rows)
	// so one event touching two watched addresses yields distinct rows
	// under UNIQUE(chain_id, block_height, event_index).
	EventIndex      uint32
	Direction       storage.LedgerDirection
	Address         string
	CounterpartySet []string
	AmountBaseUnits sdkmath.Int
	Reason          string
}

BlockEventEntry is a block-scoped unattributed-activity record derived from finalize_block_events, which carry no transaction association — never a tx-level candidate (R6 event scoping).

func (BlockEventEntry) LedgerEntry

func (b BlockEventEntry) LedgerEntry(chainID string, blockHeight uint64, now time.Time) storage.LedgerEntry

LedgerEntry converts the record to its BLOCK_EVENT ledger row.

type BlockParse

type BlockParse struct {
	ChainID       string
	Height        uint64
	BlockHash     string
	LastBlockHash string
	Time          time.Time

	Transfers        []TransferCandidate
	FeeDeductions    []FeeDeduction
	ReviewCandidates []ReviewCandidate
	BlockEvents      []BlockEventEntry
}

BlockParse is the full parse output for one block.

func ParseBlockTransfers

func ParseBlockTransfers(block *client.Block, results *client.BlockResults, watch WatchSet) (*BlockParse, error)

ParseBlockTransfers performs the tolerant raw decode and watch-set classification for one block (contracts/go-client-api.md §deposits). Individual undecodable txs are never an error — they route through the event secondary-detection channel instead.

type CreditDecision

type CreditDecision string

CreditDecision is the outcome of evaluating the FR-023 condition list.

const (
	// DecisionCredit — every FR-023 condition holds; credit now.
	DecisionCredit CreditDecision = "CREDIT"
	// DecisionHold — a condition does not hold yet (or a pause/chain-review
	// gate is closed); re-evaluate later, no state change.
	DecisionHold CreditDecision = "HOLD"
	// DecisionSuspend — scan-without-credit is engaged; park as SUSPENDED.
	DecisionSuspend CreditDecision = "SUSPEND"
	// DecisionNever — a permanent condition fails (failed tx, wrong denom,
	// non-positive amount, already credited); the deposit must not credit.
	DecisionNever CreditDecision = "NEVER"
)

func EvaluateCreditConditions

func EvaluateCreditConditions(d storage.DepositRecord, latestHeight, confirmations uint64, gate CreditGate) (CreditDecision, string)

EvaluateCreditConditions applies the full FR-023 list to one deposit. Structural conditions (committed block, recognized transfer shape, exchange-controlled recipient, external classification) are established at derivation time — a DepositRecord only exists when they held; this function re-checks the record-level and runtime conditions.

type CreditGate

type CreditGate struct {
	CreditPaused      bool // FR-051 deposit-crediting pause
	ScanWithoutCredit bool // FR-051 scan-without-credit — parks as SUSPENDED
	ChainReviewOpen   bool // FR-044 open condition closes the crediting gate
}

CreditGate carries the runtime suspension state consulted on every credit decision (FR-023 tail conditions).

func LoadCreditGate

func LoadCreditGate(ctx context.Context, store storage.Store, chainID string) (CreditGate, error)

LoadCreditGate reads the gate from controls + open chain-review conditions.

type FeeDeduction

type FeeDeduction struct {
	TxHash         string
	PayerAddress   string
	FeeBaseUnits   sdkmath.Int
	TxCode         uint32
	FeeGranterUsed bool
	GranterAddress string
}

FeeDeduction is one FEE_DEDUCTION capture (data model §8a). Recorded iff the fee-deduction ante event was present and the resolved payer is watched.

type RecordOutcome

type RecordOutcome struct {
	LedgerAppends     int
	FeeOutflows       int
	DepositsInserted  int
	DepositsRevived   int
	Duplicates        int
	ReviewItemsOpened int
}

RecordOutcome reports what one block's transactional write did.

func RecordBlock

func RecordBlock(ctx context.Context, store storage.Store, bp *BlockParse, pol RecordPolicy, now time.Time) (RecordOutcome, error)

RecordBlock persists one parsed block atomically: ledger appends, fee outflows, deposit derivation (exclusively from EXTERNAL_DEPOSIT entries), review-queue items, and the checkpoint advance — all in one Store.WithTx transaction (FR-026). Replays are idempotent: every unique-key hit is tolerated and counted, never re-credited (FR-024).

type RecordPolicy

type RecordPolicy struct {
	ChainID string
	// MinimumDepositUsovr parks smaller validated deposits as BELOW_MINIMUM;
	// nil or zero disables parking (threshold is exchange configuration,
	// never a code constant — FR-040).
	MinimumDepositUsovr sdkmath.Int
}

RecordPolicy configures the block write path.

type ReviewCandidate

type ReviewCandidate struct {
	TxHash       string
	MessageIndex uint32
	EventIndex   uint32
	// OpIndex is ReviewOpIndexBase + 2×EventIndex (+1 for OUT rows).
	OpIndex         uint32
	Direction       storage.LedgerDirection
	Address         string
	CounterpartySet []string
	AmountBaseUnits sdkmath.Int
	TxCode          uint32
	Reason          string
}

ReviewCandidate is a tx-correlated secondary-detection hit (FR-030): a watched address appears in txs_results[i].events of a tx the body decode could not fully attribute (authz-wrapped sends, wasm transfers, module payouts). Never a crediting source.

func (ReviewCandidate) LedgerEntry

func (c ReviewCandidate) LedgerEntry(chainID string, blockHeight uint64, now time.Time) storage.LedgerEntry

LedgerEntry converts the review candidate to its tx-scoped UNATTRIBUTED_REVIEW ledger row.

type Scanner

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

Scanner is the checkpointed ascending block walker (FR-026): it advances the durable checkpoint only after a block is fully persisted (same transaction), verifies the block-hash chain (R6), and evaluates the FR-023 crediting conditions for pending deposits each cycle.

func NewScanner

func NewScanner(c client.Client, store storage.Store, cfg ScannerConfig) (*Scanner, error)

NewScanner builds a Scanner; cfg zero-values take documented defaults.

func (*Scanner) Cycle

func (s *Scanner) Cycle(ctx context.Context) error

Cycle performs one scan pass: walk new blocks, then evaluate pending deposits. Exported for tests and drills; Run calls it on every tick.

func (*Scanner) RescanFrom

func (s *Scanner) RescanFrom(ctx context.Context, height uint64, actor, reason string) error

RescanFrom records an explicit rescan base in the operational controls (admin POST /v1/scanner/resume-from writes the same field); the scanner picks it up on its next cycle. Replay is idempotent — unique keys make re-processing safe (FR-024).

func (*Scanner) Run

func (s *Scanner) Run(ctx context.Context) error

Run polls until ctx is done. Cycle errors (node outage, storage outage) are logged and retried on the next tick — restart-safety comes from the durable checkpoint, not from in-memory state.

type ScannerConfig

type ScannerConfig struct {
	ChainID string
	// Confirmations defaults to DefaultConfirmations when zero.
	Confirmations uint64
	// PollInterval defaults to DefaultPollInterval when zero.
	PollInterval time.Duration
	// StartHeight, when non-zero, is the explicit rescan base applied on the
	// first cycle; zero resumes from the durable checkpoint (FR-026).
	StartHeight uint64
	// MinimumDepositUsovr parks smaller deposits BELOW_MINIMUM; nil/zero
	// disables parking.
	MinimumDepositUsovr sdkmath.Int
	// DisableHashChainVerification turns off the R6 last_block_id.hash chain
	// check (on by default; disable only in tests).
	DisableHashChainVerification bool
	// MemoRecognizer recognizes omnibus memos (FR-016); nil ⇒ any non-empty
	// memo is recognized.
	MemoRecognizer func(memo string) bool
	// PendingBatchLimit bounds each per-status pending sweep; default 500.
	PendingBatchLimit int

	Logger  *slog.Logger
	Metrics *metrics.Set
	// Now is the clock; nil ⇒ time.Now (tests inject).
	Now func() time.Time
}

ScannerConfig configures one Scanner (contract: go-client-api.md §deposits). Every economic value comes from configuration (FR-040).

type TransferCandidate

type TransferCandidate struct {
	TxHash       string
	MessageIndex uint32
	// CoinIndex is the deposit-identity output-coin index (FR-024):
	// outputs' coins flattened in wire order, non-usovr coins included in
	// the numbering so indexes are stable regardless of filtering.
	CoinIndex uint32
	// OpIndex is the ledger row index: IN rows reuse CoinIndex; sender-side
	// OUT rows continue the numbering after all output coins.
	OpIndex   uint32
	Direction storage.LedgerDirection
	// Address is the watched address this row is about.
	Address string
	// CounterpartySet is the full opposite-side address set — MsgMultiSend
	// inputs have no deterministic input→output attribution.
	CounterpartySet []string
	// SenderAddress is the single unambiguous input for IN rows; nil when
	// the input set is ambiguous.
	SenderAddress   *string
	Denom           string
	AmountBaseUnits sdkmath.Int
	Memo            string
	TxCode          uint32
	TxLog           string
	Classification  storage.Classification
	// ReviewReason is non-empty when the movement must route to review
	// (omnibus memo policy, mixed watched/external input set).
	ReviewReason string
}

TransferCandidate is one classified usovr movement decoded from a tx body — exactly one ChainTransferLedger row (data model §3). Deposit records derive only from Direction IN + Classification EXTERNAL_DEPOSIT rows.

func (TransferCandidate) LedgerEntry

func (c TransferCandidate) LedgerEntry(chainID string, blockHeight uint64, now time.Time) storage.LedgerEntry

LedgerEntry converts the candidate to its ChainTransferLedger row.

type WatchSet

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

WatchSet is the parse-time view of the exchange-controlled address set (data model §9). Only active entries participate in classification.

func NewWatchSet

func NewWatchSet(addrs []storage.WatchedAddress) WatchSet

NewWatchSet builds a WatchSet from watched-address rows; inactive entries are excluded.

func (WatchSet) Contains

func (w WatchSet) Contains(addr string) bool

Contains reports whether addr is exchange-controlled.

func (WatchSet) Get

func (w WatchSet) Get(addr string) (storage.WatchedAddress, bool)

Get returns the watch entry for addr.

func (WatchSet) Len

func (w WatchSet) Len() int

Len returns the number of active watched addresses.

func (WatchSet) WithMemoRecognizer

func (w WatchSet) WithMemoRecognizer(fn func(memo string) bool) WatchSet

WithMemoRecognizer returns a copy using fn to recognize omnibus memos.

Jump to

Keyboard shortcuts

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