Documentation
¶
Overview ¶
Package reconcile is the kit's reconciliation engine (T063; FR-046–FR-048, data model §8/§8a):
- the ledger-based address formula — expected balance = Σ ChainTransferLedger inflows − Σ outflows (including FEE_DEDUCTION fee outflows), computed from the immutable ledger only and therefore independent of customer-credit workflow status: below-minimum, review-parked, fee-funding and internal movements are all in the ledger and can never produce false discrepancies;
- per-transaction reconciliation (chain truth re-derived and diffed against the persisted ledger rows for that transaction);
- hot-wallet comparison including pending-signed / broadcast-unconfirmed in-flight work (business.go);
- a separate business-layer section reconciling customer-credit and sweep/withdrawal workflow totals against the ledger (business.go);
- schedule kinds — near-real-time / hourly wallet / daily full-address / manual (schedule.go);
- the FR-044 two-node disagreement monitor (disagreement.go).
Every non-zero difference produces a ReconciliationEntry carrying all FR-048 fields (address, expected, observed, difference, earliest suspected height, related tx hashes, recommended rescan height), increments sovren_reconciliation_discrepancies_total, and logs the alert payload verbatim.
Index ¶
- Constants
- type BusinessSection
- type ChainView
- type Comparer
- type Config
- type DisagreementConfig
- type HotWalletReport
- type LedgerPosition
- type Monitor
- type MonitorOption
- type Option
- type Reconciler
- func (r *Reconciler) Business(ctx context.Context) (BusinessSection, error)
- func (r *Reconciler) ExpectedBalance(ctx context.Context, address string) (LedgerPosition, error)
- func (r *Reconciler) HotWallet(ctx context.Context, address string) (HotWalletReport, error)
- func (r *Reconciler) NearRealTimePass(ctx context.Context, cursors map[string]int64) error
- func (r *Reconciler) ReconcileAddress(ctx context.Context, address string) (storage.ReconciliationEntry, error)
- func (r *Reconciler) ReconcileAddressReport(ctx context.Context, address string) (storage.ReconciliationReport, error)
- func (r *Reconciler) ReconcileTx(ctx context.Context, txHash string, kind storage.ReconciliationKind) (storage.ReconciliationReport, error)
- func (r *Reconciler) Run(ctx context.Context, kind storage.ReconciliationKind) (storage.ReconciliationReport, error)
- func (r *Reconciler) RunSchedules(ctx context.Context, sched Schedule) error
- type Schedule
Constants ¶
const ( DefaultNearRealTimeInterval = time.Minute DefaultWalletInterval = time.Hour DefaultFullAddressInterval = 24 * time.Hour )
Defaults for the Schedule zero values.
const DefaultDisagreementInterval = 30 * time.Second
DefaultDisagreementInterval matches the adapter.yaml nodes.disagreement.check_interval default.
const DefaultHeightTolerance = 5
DefaultHeightTolerance matches the adapter.yaml nodes.disagreement.height_divergence_threshold default.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BusinessSection ¶
type BusinessSection struct {
ChainID string
GeneratedAt time.Time
// LedgerExternalDepositTotal: Σ successful EXTERNAL_DEPOSIT IN rows
// across watched addresses — every real external inflow, credited or not.
LedgerExternalDepositTotal sdkmath.Int
// CreditedDepositTotal: Σ deposit records that reached CREDITED
// (including SWEEP_PENDING / SWEPT afterwards).
CreditedDepositTotal sdkmath.Int
// UncreditedExternalTotal = ledger − credited. Below-minimum, review-
// parked, awaiting-confirmation and suspended inflows live here; it is
// never negative in a healthy system.
UncreditedExternalTotal sdkmath.Int
// LedgerWithdrawalOutflowTotal: Σ successful WITHDRAWAL OUT rows.
LedgerWithdrawalOutflowTotal sdkmath.Int
// WorkflowWithdrawalTotal: Σ CONFIRMED withdrawal record amounts.
WorkflowWithdrawalTotal sdkmath.Int
// InFlightWithdrawalTotal: Σ BROADCAST/INCLUDED amounts (may already be
// in the ledger ahead of workflow confirmation, or vice versa).
InFlightWithdrawalTotal sdkmath.Int
// LedgerSweepInflowTotal: Σ successful SWEEP IN rows (counted on the
// receiving side only, so one sweep is one amount).
LedgerSweepInflowTotal sdkmath.Int
// WorkflowSweepTotal: Σ CONFIRMED sweep job amounts.
WorkflowSweepTotal sdkmath.Int
// Findings are human-readable inconsistencies. An impossible state
// (workflow total exceeding what the ledger can support) also increments
// the discrepancy counter.
Findings []string
}
BusinessSection reconciles the customer-credit and sweep/withdrawal workflow layers against the ledger's EXTERNAL_DEPOSIT / SWEEP / WITHDRAWAL entries (data model §8 — "a separate business-layer section"). It is returned and logged alongside address reports, not persisted in the entry-based report record.
type ChainView ¶
type ChainView interface {
Balance(ctx context.Context, addr, denom string) (sdkmath.Int, error)
Tx(ctx context.Context, hash string) (*client.TxInfo, error)
LatestBlock(ctx context.Context) (*client.Block, error)
}
ChainView is the read-only chain subset the reconciler needs. client.Client implementations and the failover wrapper satisfy it.
type Comparer ¶
type Comparer interface {
Compare(ctx context.Context, req client.CompareRequest) (*client.CompareResult, error)
}
Comparer runs the FR-044 two-node checks. *client.Failover satisfies it.
type DisagreementConfig ¶
type DisagreementConfig struct {
ChainID string
// Interval between checks; DefaultDisagreementInterval when zero.
Interval time.Duration
// HeightTolerance is the allowed latest-height gap (blocks);
// DefaultHeightTolerance when zero.
HeightTolerance int64
// SequenceAddress optionally adds an account-sequence check (hot wallet).
SequenceAddress string
// BalanceAddress optionally adds a balance check (hot wallet).
BalanceAddress string
// CompareHashAtCheckpoint adds a block-hash check at the scanner
// checkpoint height (both nodes must agree on scanned history).
CompareHashAtCheckpoint bool
}
DisagreementConfig configures a Monitor.
type HotWalletReport ¶
type HotWalletReport struct {
ChainID string
Address string
// LedgerExpected is the §8 ledger formula result.
LedgerExpected sdkmath.Int
// Observed is the live on-chain balance.
Observed sdkmath.Int
// Difference is Observed − LedgerExpected.
Difference sdkmath.Int
// PendingSignedOutflow: withdrawals SIGNED but not yet broadcast from
// this wallet, plus signed-not-broadcast sweeps sourced here
// (amount + known fee). Not yet on chain — never explains drift, listed
// for the operator's forward view.
PendingSignedOutflow sdkmath.Int
// BroadcastUnconfirmedOutflow: withdrawals BROADCAST/INCLUDED and sweeps
// BROADCAST sourced from this wallet (amount + known fee). These may
// already be on chain ahead of the scanner — negative drift up to this
// value is explained.
BroadcastUnconfirmedOutflow sdkmath.Int
// BroadcastUnconfirmedInflow: sweeps BROADCAST targeting this wallet.
// Positive drift up to this value is explained.
BroadcastUnconfirmedInflow sdkmath.Int
// ConfirmedWithdrawalOutflow / ConfirmedSweepInflow are the ledger's
// settled WITHDRAWAL (OUT) and SWEEP (IN) totals for this wallet.
ConfirmedWithdrawalOutflow sdkmath.Int
ConfirmedSweepInflow sdkmath.Int
// Explained is true when Difference is zero or within the in-flight
// window: [−BroadcastUnconfirmedOutflow, +BroadcastUnconfirmedInflow].
Explained bool
GeneratedAt time.Time
}
HotWalletReport is the hot-wallet comparison: on-chain balance vs the ledger-based expectation, with the in-flight (pending-signed / broadcast-unconfirmed) work that legitimately explains transient drift.
type LedgerPosition ¶
type LedgerPosition struct {
// Expected is Σ inflows − Σ outflows − Σ fee deductions.
Expected sdkmath.Int
// EarliestHeight is the lowest block height among the address's
// counted ledger rows (0 when the address has no ledger activity).
EarliestHeight uint64
// LatestHeight is the highest counted block height.
LatestHeight uint64
// RelatedTxHashes are the distinct tx hashes touched (newest last,
// capped at maxRelatedTxHashes).
RelatedTxHashes []string
// Rows is the number of counted ledger rows (fee outflows excluded).
Rows int
}
LedgerPosition summarizes the ledger walk backing one expected-balance computation.
type Monitor ¶
type Monitor struct {
// contains filtered or unexported fields
}
Monitor is the periodic disagreement checker.
func NewMonitor ¶
func NewMonitor(cmp Comparer, store storage.Store, cfg DisagreementConfig, opts ...MonitorOption) (*Monitor, error)
NewMonitor builds a Monitor.
type MonitorOption ¶
type MonitorOption func(*Monitor)
MonitorOption configures a Monitor.
func WithMonitorLogger ¶
func WithMonitorLogger(l *slog.Logger) MonitorOption
WithMonitorLogger replaces the default logger.
func WithMonitorMetrics ¶
func WithMonitorMetrics(s *metrics.Set) MonitorOption
WithMonitorMetrics attaches the adapter metric set.
func WithMonitorNow ¶
func WithMonitorNow(now func() time.Time) MonitorOption
WithMonitorNow injects the clock (tests).
type Option ¶
type Option func(*Reconciler)
Option configures a Reconciler.
func WithMetrics ¶
WithMetrics attaches the adapter metric set (discrepancy counter + gauges).
type Reconciler ¶
type Reconciler struct {
// contains filtered or unexported fields
}
Reconciler computes reconciliation reports from the store's ledger and the live chain.
func New ¶
New builds a Reconciler. chain may be nil only for ledger-only operations (ExpectedBalance); every comparison against live state requires it.
func (*Reconciler) Business ¶
func (r *Reconciler) Business(ctx context.Context) (BusinessSection, error)
Business computes the business-layer section.
func (*Reconciler) ExpectedBalance ¶
func (r *Reconciler) ExpectedBalance(ctx context.Context, address string) (LedgerPosition, error)
ExpectedBalance computes the ledger-based expected balance for one address (data model §8): every ledger row regardless of classification — including UNATTRIBUTED_REVIEW and block-scoped rows — counts, because the ledger records on-chain truth, not workflow opinion. Rows from failed transactions (tx_code != 0) moved no funds and are excluded; the fee those transactions still paid is captured by the FEE_DEDUCTION table and subtracted here.
func (*Reconciler) HotWallet ¶
func (r *Reconciler) HotWallet(ctx context.Context, address string) (HotWalletReport, error)
HotWallet builds the comparison for one wallet address.
func (*Reconciler) NearRealTimePass ¶
NearRealTimePass reconciles every transaction newly appended to the ledger since the previous pass (per-address AfterID cursors, in-memory — a restart simply restarts the window; the hourly/daily balance formulas cover anything a restart skipped). Exported for the admin service and tests.
func (*Reconciler) ReconcileAddress ¶
func (r *Reconciler) ReconcileAddress(ctx context.Context, address string) (storage.ReconciliationEntry, error)
ReconcileAddress compares the ledger-based expected balance against the live chain balance for one address and returns the FR-048 entry.
func (*Reconciler) ReconcileAddressReport ¶
func (r *Reconciler) ReconcileAddressReport(ctx context.Context, address string) (storage.ReconciliationReport, error)
ReconcileAddressReport reconciles one address now and persists a MANUAL report (admin POST /v1/reconcile/address).
func (*Reconciler) ReconcileTx ¶
func (r *Reconciler) ReconcileTx(ctx context.Context, txHash string, kind storage.ReconciliationKind) (storage.ReconciliationReport, error)
ReconcileTx re-derives one transaction's watched-address movements from chain truth and diffs them against the persisted ledger rows (the TX_NEAR_REAL_TIME / manual per-tx check). kind must be TX_NEAR_REAL_TIME or MANUAL. A transaction the chain does not know is reported without entries (nothing on-chain to reconcile against; the ledger cannot be queried by bare hash) and logged.
func (*Reconciler) Run ¶
func (r *Reconciler) Run(ctx context.Context, kind storage.ReconciliationKind) (storage.ReconciliationReport, error)
Run generates one reconciliation report for the given kind:
WALLET_HOURLY — operational wallets only (hot / cold / fee) ADDRESS_DAILY — every active watched address MANUAL — every active watched address, operator-triggered
(TX_NEAR_REAL_TIME reports come from ReconcileTx.) The report is persisted; every non-zero difference emits the FR-048 alert.
func (*Reconciler) RunSchedules ¶
func (r *Reconciler) RunSchedules(ctx context.Context, sched Schedule) error
RunSchedules drives the three periodic kinds until ctx is done. Pass errors are logged and retried on the next tick; nothing here is fatal — reconciliation is an observer, never a mutator of chain-facing state.
type Schedule ¶
type Schedule struct {
NearRealTimeInterval time.Duration // default 1m
WalletInterval time.Duration // default 1h (adapter.yaml reconciler.wallet_interval)
FullAddressInterval time.Duration // default 24h (reconciler.full_address_interval)
}
Schedule carries the three periodic cadences. Zero values take the adapter.yaml contract defaults.