Help us shape the future of Go! Take the 2026 Go Developer Survey

funds

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 8 Imported by: 0

Documentation

Overview

Package funds is the funds engine shared by the VM and the tree-walking interpreter: per-(account, scope, asset, color) balances, a FIFO queue of funding sources fed by Pull/PullUncapped, and the postings produced by Send/SendUncapped.

Balances load lazily. An entry holds only the net delta applied this run until an operation needs the absolute value (a bounded pull, a send-all, a balance() read); the Store is consulted then and the starting balance folded in. An unbounded pull (nil overdraft) makes cap available regardless of balance, so it never triggers a fetch — that is what keeps send-from-@world free of Store round-trips. Nothing here knows the name "world": callers decide which accounts are unbounded and pass a nil overdraft.

Color "" means uncolored. Pull tags queued funds with a color; Send drains only matching sources, preserving the position of the rest.

A *RunState is NOT safe for concurrent use. Use one per execution.

Amounts are *big.Int, a mutable reference type, so this package clones values it ingests from the Store and amounts it intends to mutate, only mutates big.Ints it privately owns (queued source amounts), and never hands out a live reference to internal state.

Index

Constants

This section is empty.

Variables

View Source
var ErrNegativePosting = errors.New("funds: negative forced posting")

ErrNegativePosting is returned by ForcePosting for a negative amount. A negative conversion means the caller.s own arithmetic is inconsistent (e.g. a scaling solution whose net differs from the legs it asks to post), and posting only the positive legs would move money without moving it back. Fail closed.

View Source
var ErrNoOpenMark = errors.New("funds: no open mark to end")

ErrNoOpenMark is the only error MarkEnd returns. Well-formed bytecode matches pushes to ends, so it only surfaces for hand-written IR or a hand-crafted .numb.

Functions

func AssetToScaledAsset

func AssetToScaledAsset(asset string) string

AssetToScaledAsset maps an asset to its wildcard-scale form, e.g. "EUR/2" -> "EUR/*" and "EUR" -> "EUR/*".

func BuildScaledAsset

func BuildScaledAsset(baseAsset string, scale int64) string

BuildScaledAsset composes a base asset and a scale into an asset string, e.g. ("EUR", 2) -> "EUR/2" and ("EUR", 0) -> "EUR".

func GetAssets

func GetAssets(accountBalances []AccountBalance, baseAsset string) map[int64]*big.Int

GetAssets collects, per scale, the (uncolored) amount an account holds of baseAsset. Scaling converts only uncolored balances and emits uncolored postings, so colored balances are excluded.

Postings name the asset with BuildScaledAsset, so a balance whose asset is not in that form (e.g. "EUR/0", "EUR/02") is excluded too: it can't be withdrawn under the name the conversion would post, and it would collide with the canonical asset of the same scale.

func GetBaseAndScale

func GetBaseAndScale(asset string) (string, int64)

GetBaseAndScale splits an asset into its base and scale, e.g. "EUR/2" -> ("EUR", 2) and "EUR" -> ("EUR", 0).

func ParseMonetary

func ParseMonetary(source string) (asset string, amount *big.Int, err error)

ParseMonetary parses "ASSET AMOUNT" (e.g. "USD/2 100") into its asset and amount.

func ParseNumber

func ParseNumber(s string) (*big.Int, bool)

ParseNumber parses a base-10 integer (arbitrary precision).

func ParsePortion

func ParsePortion(input string) (*big.Rat, error)

ParsePortion parses a portion given as a percentage ("12%") or a fraction ("1/3"), returning it as a reduced ratio in [0, 1]. The error message is the reason (callers may wrap it into their own error type).

func ValidateAccount

func ValidateAccount(addr string) bool

func ValidateAsset

func ValidateAsset(v string) bool

func ValidateColor

func ValidateColor(v string) bool

func ValidatePosting added in v0.1.1

func ValidatePosting(p Posting) bool

ValidatePosting reports whether a posting has a non-negative amount and well-formed accounts, scopes, asset and color.

func ValidateScope

func ValidateScope(v string) bool

Types

type AccountBalance

type AccountBalance struct {
	Asset  string
	Color  string
	Amount *big.Int
}

type AccountMetadataEntry

type AccountMetadataEntry struct {
	Account string `json:"account"`
	Scope   string `json:"scope,omitempty"`
	Key     string `json:"key"`
	Value   string `json:"value"`
}

AccountMetadataEntry is one piece of account metadata set during execution. Scope is a second dimension of the account, like the balance PairKey: a flat list rather than a nested map, since JSON object keys must be strings and folding scope into the account key would need an encoding hack.

type AccountsMetadata

type AccountsMetadata []AccountMetadataEntry

AccountsMetadata is the account metadata produced by the script (the execution result's accountsMeta).

func (AccountsMetadata) PrettyPrint

func (m AccountsMetadata) PrettyPrint() string

type ExecutionResult

type ExecutionResult struct {
	Postings         []Posting         `json:"postings"`
	Metadata         map[string]string `json:"txMeta"`
	AccountsMetadata AccountsMetadata  `json:"accountsMeta"`
}

type PairKey

type PairKey struct {
	Account string
	Scope   string
	Asset   string
	Color   string
}

PairKey identifies a balance slot. Scope is a second dimension of the account (scoped accounts hold separate balances); the VM leaves it empty.

type Posting

type Posting struct {
	Source           string   `json:"source"`
	SourceScope      string   `json:"sourceScope,omitempty"`
	Destination      string   `json:"destination"`
	DestinationScope string   `json:"destinationScope,omitempty"`
	Amount           *big.Int `json:"amount"`
	Asset            string   `json:"asset"`
	Color            string   `json:"color,omitempty"`
}

Posting is aliased as the interpreter's public Posting type, so the json tags define the public ledger serialization contract — keep field names and order stable. The VM leaves the scope fields empty; the interpreter fills them.

type RunState

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

The zero value is not usable; call New.

func New

func New(store Store) *RunState

New creates an empty RunState backed by store.

func (*RunState) AccountBalances

func (s *RunState) AccountBalances(account, scope, baseAsset string) ([]AccountBalance, error)

AccountBalances returns copies of the tracked balance entries for account in baseAsset's family (baseAsset itself and its baseAsset/n scalings), with starting balances folded in so the amounts are absolute. It only reports triples already touched this run — it does not enumerate the Store — so an account never prewarmed or touched yields an empty slice.

The family filter guards loadBase's side effect as much as the output: an entry outside the family can hold only a write delta whose base was never prewarmed, and against a zero-backed Store loadBase would stamp it loaded, masking the starting balance from every later read of that asset.

Entries are visited in (asset, color) order, not map order, so both the returned slice and the sequence of Store reads — hence which error surfaces when several reads fail — are the same on every run.

func (*RunState) ForcePosting

func (s *RunState) ForcePosting(src, srcScope, dst, dstScope, asset, color string, amount *big.Int) error

ForcePosting moves amount from src to dst bypassing the funding queue, for movements the queue does not model (e.g. asset-scaling conversions). Unlike Send it uses the explicit asset argument, which may differ from the current asset. A zero amount is a no-op and a negative one is ErrNegativePosting; no balance sufficiency check is performed.

Safe inside a region, unlike Send: it touches no queue entry, so a rewinding MarkEnd undoes it completely by reversing the posting.

func (*RunState) GetAccountBalance

func (s *RunState) GetAccountBalance(account, scope, asset, color string) (*big.Int, error)

GetAccountBalance returns a fresh copy of the balance for (account, asset, color), fetching from the Store on first access.

"" is the unset sentinel for asset, meaning "use currentAsset"; a real asset is never the empty string. For color, "" is a legitimate value: uncolored.

func (*RunState) GetPostings

func (s *RunState) GetPostings() []Posting

GetPostings returns a fresh slice, so callers cannot alter the internal length/order. Posting amounts are write-once — addPosting clones on append and never mutates an existing posting — so the *big.Int values are shared rather than deep-cloned.

func (*RunState) Has

func (s *RunState) Has(account, scope, asset, color string) bool

Has reports whether (account, asset, color) already has its starting balance loaded. An entry that only holds a delta so far reports false, so a caller still fetches and folds in the base.

func (*RunState) HasOpenMark

func (s *RunState) HasOpenMark() bool

func (*RunState) MarkEnd

func (s *RunState) MarkEnd(rewind bool) error

MarkEnd closes the innermost region, always popping the mark.

rewind == false commits: whatever was pulled stays queued and whatever was posted stays posted.

rewind == true rolls the region back in two parts. Postings emitted since the MarkPush are reversed and dropped; each posting records its own asset, so this part does not depend on currentAsset. Sources still queued above the mark are repaid to the balance they were debited from, then the queue is truncated. compactAt may have folded funds, but the fold preserves (account, scope, color), so the repay still lands correctly. The two parts are both additive balance adjustments, so their order is conventional rather than required.

INVARIANT: no Send may have run inside the region. Reversing a posting credits its source back, which is only correct because the queue entry that funded it was pulled *inside* the region and is therefore not also repaid by the second loop. A Send can consume entries queued *below* the mark, and for those the credit and the repay would both apply — inventing money. Hence Op_SendToAccount is refused while a mark is open; relaxing that requires undoing queue consumption too.

func (*RunState) MarkPush

func (s *RunState) MarkPush()

MarkPush opens a region at the current source-queue depth and posting-list length. Allocation-free once the stack has reached its high-water mark, since the backing array is retained across Reset.

func (*RunState) Prewarm

func (s *RunState) Prewarm(balances map[PairKey]*big.Int)

Prewarm seeds the balance cache from a bulk fetch, so the lazy per-key Store.GetBalance path is never hit for those keys.

Call it once, before any Pull/Send/Save/ForcePosting. Amounts are cloned. A key whose base is already loaded is left untouched (the live value wins), so a stray double-call cannot clobber computed state; a key that only holds a delta has the base folded into it here.

func (*RunState) Pull

func (s *RunState) Pull(out *big.Int, src string, scope string, cap *big.Int, overdraft *big.Int, color string) error

Pull debits up to cap from src's (currentAsset, color) balance, queues the pulled amount as a funding source tagged with color, and writes the amount made available into out:

overdraft == nil -> unbounded: available = max(0, cap)
overdraft == b   -> available = min(max(0, balance + max(0,b)), max(0, cap))
                    (pass big.NewInt(0) for the "balance only" default)

out is overwritten and may be any addressable *big.Int (e.g. a VM register). cap and overdraft are not mutated. The only allocation per call is the queued source's own copy of the amount, which must outlive out.

func (*RunState) PullUncapped

func (s *RunState) PullUncapped(out *big.Int, src string, scope string, overdraftBound *big.Int, color string) error

PullUncapped makes available max(0, balance + max(0, overdraftBound)) of src's (currentAsset, color) balance, queuing it only when positive, and writes the available amount into out. As in Pull a negative overdraftBound is clamped to 0, so it never eats into a positive balance; pass big.NewInt(0) for the "balance only" default. overdraftBound is not mutated.

func (*RunState) Reset

func (s *RunState) Reset(store Store)

Reset clears all per-execution state and rebinds the store, retaining map/slice capacity so a RunState can be reused across executions. Any mark left open by a run that failed mid-region is dropped.

GetPostings returns copies, so a result obtained before Reset stays valid. Reset(nil) releases the store between executions.

func (*RunState) Save

func (s *RunState) Save(account, scope, asset, color string, amount *big.Int) error

Save implements the numscript `save` statement: it protects funds from being pulled later by reducing the (account, asset, color) balance, floored at zero.

amount != nil -> balance = max(0, balance - amount)   (PRE: amount >= 0)
amount == nil -> "save all": a positive balance becomes 0; a negative
                 balance is left unchanged (= min(balance, 0))

func (*RunState) Send

func (s *RunState) Send(dest *string, destScope string, cap *big.Int, color *string) error

Send drains queued funding sources in FIFO order until cap is satisfied or eligible sources run out. Each emitted posting carries the *consumed source's* own color.

color == nil   -> match anything; one drain may consume and emit funds of
                  several colors at once (the interpreter's destination mode)
color != nil   -> only sources whose color == *color are consumed; others are
                  skipped and left in place (*color == "" meaning uncolored)

dest == nil is the keep/refund path: the source is credited back and no posting is emitted. A partially consumed source's remainder stays at its position.

PRE: no mark is open (see HasOpenMark). Draining consumes sources from the front, renumbering the queue and leaving an open mark at the wrong boundary.

func (*RunState) SendUncapped

func (s *RunState) SendUncapped(dest *string, destScope string, color *string) error

SendUncapped applies the same color filter as Send: color == nil drains every queued source (each posting keeping its own color); color != nil drains only matching ones, leaving others in place.

PRE: no mark is open, for the same reason as Send.

func (*RunState) SetCurrentAsset

func (s *RunState) SetCurrentAsset(asset string)

SetCurrentAsset sets the asset used when an operation omits one.

PRE: no mark is open (see HasOpenMark). A rewinding MarkEnd repays queued funds into the current asset's balance, so changing the asset mid-region would repay into the wrong one.

type ScalePair

type ScalePair struct {
	Scale  int64
	Amount *big.Int
}

ScalePair is an (amount at scale) entry: a conversion output of FindScalingSolution.

func FindScalingSolution

func FindScalingSolution(
	neededAmt *big.Int,
	neededAmtScale int64,
	scales map[int64]*big.Int,
) ([]ScalePair, *big.Int)

FindScalingSolution finds a set of conversions from the available "scales" to [ASSET/$neededAmtScale $neededAmt], so that there's no rounding error and no spare amount. neededAmt may be nil (meaning "convert everything available").

type Store

type Store interface {
	GetBalance(account, scope, asset, color string) (*big.Int, error)
}

Store supplies the starting balance for an (account, scope, asset, color) tuple. Implementations should return 0 (or nil, treated as 0) for unknown tuples, not an error. The returned *big.Int is cloned on ingest, so the Store may reuse it.

Jump to

Keyboard shortcuts

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