Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3bd0fe7
Implement new balance table
jeffyanta Aug 26, 2026
28e37d5
Update balance calculators
jeffyanta Aug 26, 2026
823bcf4
Updates to balance store implementations
jeffyanta Aug 26, 2026
8231ed6
Gate new balance table reads on a config
jeffyanta Aug 26, 2026
9d26aac
Add more balance utilities
jeffyanta Aug 27, 2026
ff19335
Integrate new balance table into all call sites
jeffyanta Aug 27, 2026
6144714
Bump Postgres test docker image to 14.24
jeffyanta Aug 27, 2026
83773c8
Update mint index
jeffyanta Aug 28, 2026
a59a305
Update holder count worker to use new balance table
jeffyanta Aug 28, 2026
f4ad70c
GetTokenAccountInfos now fully utilizes new balance table
jeffyanta Aug 28, 2026
175d9c9
Remove balance locks
jeffyanta Aug 28, 2026
da32ee2
Tighten delta debit check against open status
jeffyanta Aug 28, 2026
7cd3d45
Default balance ledger read and writes to true
jeffyanta Aug 28, 2026
51f5de7
Balance rows now have lock state
jeffyanta Aug 28, 2026
eafdbc0
Merge branch 'refactor-balance' into update-balance-callsites
jeffyanta Aug 28, 2026
2bf2071
Remove intentBalanceLock
jeffyanta Aug 28, 2026
095a290
Fix comment
jeffyanta Aug 28, 2026
ae28581
Update testGetAllLockedByMint
jeffyanta Aug 28, 2026
928a0c2
Unlocked gift card accounts are now cleaned up vs auto-returned
jeffyanta Aug 31, 2026
56dd251
VoidGiftCard now handles unlocked gift card accounts
jeffyanta Aug 31, 2026
d095032
Allow credits to unlocked accounts and introduce a cost basis adjustm…
jeffyanta Aug 31, 2026
d8e669d
Merge branch 'refactor-balance' into update-balance-callsites
jeffyanta Aug 31, 2026
ff2cb2c
Remove legacy balance calculations and ledger configs
jeffyanta Aug 31, 2026
e742c19
Remove backfill
jeffyanta Aug 31, 2026
0f93361
Quarks can now be uint64
jeffyanta Aug 31, 2026
a87b69a
Remove unused USD cost basis helpers
jeffyanta Aug 31, 2026
3fa809a
Further simplify balance calculators
jeffyanta Aug 31, 2026
90396bd
Add BatchCalculateFromCacheByOwner utility and use it in the Balance …
jeffyanta Aug 31, 2026
9d6d2e0
Merge branch 'main' into balance-cleanup
jeffyanta Sep 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
535 changes: 61 additions & 474 deletions ocp/balance/calculator.go

Large diffs are not rendered by default.

809 changes: 110 additions & 699 deletions ocp/balance/calculator_test.go

Large diffs are not rendered by default.

25 changes: 0 additions & 25 deletions ocp/balance/config.go

This file was deleted.

53 changes: 13 additions & 40 deletions ocp/balance/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,10 @@ import (
// ledger doesn't track.
var ErrUntrackedAccount = errors.New("account is not tracked by the balance ledger")

// LedgerReadsEnabled reports whether backfilled ledger records are the
// authoritative source for balance reads.
func LedgerReadsEnabled(ctx context.Context) bool {
return enableLedgerReads.Get(ctx)
}

// LedgerWritesEnabled reports whether the ledger is being written to.
// Callers use it to skip building deltas entirely when writes are disabled,
// since builders reject flows the ledger doesn't support.
func LedgerWritesEnabled(ctx context.Context) bool {
return enableLedgerWrites.Get(ctx)
}

// ApplyDeltasInTx applies balance deltas to the ledger. It must be called
// within the DB transaction that commits the records the deltas are derived
// from, so the ledger can never disagree with them.
//
// It is a no-op while ledger writes are disabled.
//
// The ledger only tracks timelock accounts. Credits to any other account,
// like an external wallet or the fee collector, are dropped, since delta
// builders don't know which destinations OCP manages. Outgoing deltas from
Expand All @@ -44,15 +29,14 @@ func LedgerWritesEnabled(ctx context.Context) bool {
// Credits are still applied, since an unlocked record is excluded from every
// read and turning one away only blocks the flow recording it.
//
// Any timelock account in the delta set that has no ledger record yet lazily
// gets one that is not backfilled, so accounts that predate the ledger start
// accumulating deltas on first touch regardless of direction.
// A timelock account with no ledger record at all is balance.ErrRecordNotFound
// in either direction, since every one gets a record when it's opened.
//
// Store predicate failures (balance.ErrInsufficientBalance,
// balance.ErrBalanceChanged, balance.ErrAccountClosed,
// balance.ErrAccountUnlocked) are returned as is for the caller to map.
func ApplyDeltasInTx(ctx context.Context, data ocp_data.Provider, deltas ...*balance.Delta) error {
if !enableLedgerWrites.Get(ctx) || len(deltas) == 0 {
if len(deltas) == 0 {
return nil
}

Expand Down Expand Up @@ -84,13 +68,12 @@ func ApplyDeltasInTx(ctx context.Context, data ocp_data.Provider, deltas ...*bal

// CreateRecordInTx creates the ledger record for a newly opened account. It
// must be called within the DB transaction that creates the account info
// record. A new account has no history, so its record is created backfilled
// at zero and predicates are enforced from the start.
// record, so every timelock account has a record from the moment it exists.
//
// It is a no-op while ledger writes are disabled, and for accounts that
// aren't timelock accounts, which the ledger doesn't track.
// It is a no-op for accounts that aren't timelock accounts, which the ledger
// doesn't track.
func CreateRecordInTx(ctx context.Context, data ocp_data.Provider, accountInfoRecord *account.Record) error {
if !enableLedgerWrites.Get(ctx) || !accountInfoRecord.IsTimelock() {
if !accountInfoRecord.IsTimelock() {
return nil
}

Expand All @@ -100,17 +83,18 @@ func CreateRecordInTx(ctx context.Context, data ocp_data.Provider, accountInfoRe
MintAccount: accountInfoRecord.MintAccount,
IsOpen: true,
IsLocked: true,
IsBackfilled: true,
})
if errors.Is(err, balance.ErrRecordExists) {
return nil
}
return err
}

// resolveRecords reports which accounts in the delta set the ledger tracks,
// creating a non-backfilled record for every timelock account that doesn't
// have one yet.
// resolveRecords reports which accounts in the delta set the ledger tracks. A
// timelock account without a record is a broken invariant rather than an
// untracked account, since CreateRecordInTx gives every one a record when it's
// opened, so it fails with balance.ErrRecordNotFound instead of being seeded
// with a balance that has no relationship to the account's history.
func resolveRecords(ctx context.Context, data ocp_data.Provider, deltas []*balance.Delta) (map[string]bool, error) {
tracked := make(map[string]bool)
var tokenAccounts []string
Expand Down Expand Up @@ -143,18 +127,7 @@ func resolveRecords(ctx context.Context, data ocp_data.Provider, deltas []*balan
continue
}

err = data.CreateBalance(ctx, &balance.Record{
TokenAccount: accountInfoRecord.TokenAccount,
OwnerAccount: accountInfoRecord.OwnerAccount,
MintAccount: accountInfoRecord.MintAccount,
IsOpen: true,
IsLocked: true,
IsBackfilled: false,
})
if err != nil && !errors.Is(err, balance.ErrRecordExists) {
return nil, err
}
tracked[tokenAccount] = true
return nil, fmt.Errorf("%w: %s", balance.ErrRecordNotFound, tokenAccount)
}
return tracked, nil
}
105 changes: 30 additions & 75 deletions ocp/balance/ledger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,62 +9,42 @@ import (

commonpb "github.com/code-payments/ocp-protobuf-api/generated/go/common/v1"

"github.com/code-payments/ocp-server/config/memory"
"github.com/code-payments/ocp-server/config/wrapper"
ocp_data "github.com/code-payments/ocp-server/ocp/data"
"github.com/code-payments/ocp-server/ocp/data/account"
"github.com/code-payments/ocp-server/ocp/data/balance"
"github.com/code-payments/ocp-server/testutil"
)

func TestApplyDeltasInTx_WritesDisabled(t *testing.T) {
func TestApplyDeltasInTx_TrackedAccounts(t *testing.T) {
ctx := context.Background()
data := ocp_data.NewTestDataProvider()
disableLedgerWritesForTest(t)

source := newLedgerTestAccount(t, ctx, data, commonpb.AccountType_PRIMARY)

require.NoError(t, ApplyDeltasInTx(ctx, data, &balance.Delta{
TokenAccount: source,
Kind: balance.DeltaDebit,
Quarks: 100,
}))

_, err := data.GetBalance(ctx, source)
assert.Equal(t, balance.ErrRecordNotFound, err)
}

func TestApplyDeltasInTx_SeedsTimelockAccounts(t *testing.T) {
ctx := context.Background()
data := ocp_data.NewTestDataProvider()
enableLedgerWritesForTest(t)

source := newLedgerTestAccount(t, ctx, data, commonpb.AccountType_PRIMARY)
destination := newLedgerTestAccount(t, ctx, data, commonpb.AccountType_REMOTE_SEND_GIFT_CARD)
source := newLedgerTestAccountInfo(t, ctx, data, commonpb.AccountType_PRIMARY)
destination := newLedgerTestAccountInfo(t, ctx, data, commonpb.AccountType_REMOTE_SEND_GIFT_CARD)
swap := newLedgerTestAccount(t, ctx, data, commonpb.AccountType_SWAP)
external := testutil.NewRandomAccount(t).PublicKey().ToBase58()

require.NoError(t, CreateRecordInTx(ctx, data, source))
require.NoError(t, CreateRecordInTx(ctx, data, destination))
require.NoError(t, ApplyDeltasInTx(ctx, data, &balance.Delta{TokenAccount: source.TokenAccount, Kind: balance.DeltaCredit, Quarks: 100, UsdCostBasis: 1_000_000}))

require.NoError(t, ApplyDeltasInTx(ctx, data,
&balance.Delta{TokenAccount: source, Kind: balance.DeltaDebit, Quarks: 100, UsdCostBasis: 1_000_000},
&balance.Delta{TokenAccount: destination, Kind: balance.DeltaCredit, Quarks: 60, UsdCostBasis: 600_000},
&balance.Delta{TokenAccount: source.TokenAccount, Kind: balance.DeltaDebit, Quarks: 100, UsdCostBasis: 1_000_000},
&balance.Delta{TokenAccount: destination.TokenAccount, Kind: balance.DeltaCredit, Quarks: 60, UsdCostBasis: 600_000},
&balance.Delta{TokenAccount: swap, Kind: balance.DeltaCredit, Quarks: 20, UsdCostBasis: 200_000},
&balance.Delta{TokenAccount: external, Kind: balance.DeltaCredit, Quarks: 20, UsdCostBasis: 200_000},
))

// Existing accounts get a non-backfilled row that accumulates freely,
// including a negative balance for a source that predates the ledger
record, err := data.GetBalance(ctx, source)
record, err := data.GetBalance(ctx, source.TokenAccount)
require.NoError(t, err)
assert.EqualValues(t, -100, record.Quarks)
assert.EqualValues(t, -1_000_000, record.UsdCostBasis)
assert.False(t, record.IsBackfilled)
assert.EqualValues(t, 0, record.Quarks)
assert.EqualValues(t, 0, record.UsdCostBasis)
assert.True(t, record.IsOpen)

record, err = data.GetBalance(ctx, destination)
record, err = data.GetBalance(ctx, destination.TokenAccount)
require.NoError(t, err)
assert.EqualValues(t, 60, record.Quarks)
assert.EqualValues(t, 600_000, record.UsdCostBasis)
assert.False(t, record.IsBackfilled)

// Credits to accounts OCP doesn't hold a timelock for are dropped, and
// those accounts never get a row
Expand All @@ -73,24 +53,29 @@ func TestApplyDeltasInTx_SeedsTimelockAccounts(t *testing.T) {
_, err = data.GetBalance(ctx, external)
assert.Equal(t, balance.ErrRecordNotFound, err)

// Once backfilled, predicates are enforced
require.NoError(t, data.BackfillBalance(ctx, source, func(context.Context) (*balance.BackfillResult, error) {
return &balance.BackfillResult{Quarks: 400, UsdCostBasis: 4_000_000, IsOpen: true, IsLocked: true}, nil
}))
err = ApplyDeltasInTx(ctx, data, &balance.Delta{TokenAccount: source, Kind: balance.DeltaDebit, Quarks: 401})
// Predicates are enforced against every record
err = ApplyDeltasInTx(ctx, data, &balance.Delta{TokenAccount: destination.TokenAccount, Kind: balance.DeltaDebit, Quarks: 61})
assert.Equal(t, balance.ErrInsufficientBalance, err)
require.NoError(t, ApplyDeltasInTx(ctx, data, &balance.Delta{TokenAccount: source, Kind: balance.DeltaDebit, Quarks: 400, UsdCostBasis: 4_000_000}))
}

record, err = data.GetBalance(ctx, source)
require.NoError(t, err)
assert.EqualValues(t, 0, record.Quarks)
assert.True(t, record.IsBackfilled)
func TestApplyDeltasInTx_MissingRecord(t *testing.T) {
ctx := context.Background()
data := ocp_data.NewTestDataProvider()

// A timelock account with no record is a broken invariant in either
// direction, not an account the ledger doesn't track
tokenAccount := newLedgerTestAccount(t, ctx, data, commonpb.AccountType_PRIMARY)
for _, delta := range []*balance.Delta{
{TokenAccount: tokenAccount, Kind: balance.DeltaCredit, Quarks: 1},
{TokenAccount: tokenAccount, Kind: balance.DeltaDebit, Quarks: 1},
} {
assert.ErrorIs(t, ApplyDeltasInTx(ctx, data, delta), balance.ErrRecordNotFound)
}
}

func TestApplyDeltasInTx_UnknownSource(t *testing.T) {
ctx := context.Background()
data := ocp_data.NewTestDataProvider()
enableLedgerWritesForTest(t)

external := testutil.NewRandomAccount(t).PublicKey().ToBase58()
swap := newLedgerTestAccount(t, ctx, data, commonpb.AccountType_SWAP)
Expand All @@ -103,7 +88,6 @@ func TestApplyDeltasInTx_UnknownSource(t *testing.T) {
func TestApplyDeltasInTx_OnlyUntrackedCredits(t *testing.T) {
ctx := context.Background()
data := ocp_data.NewTestDataProvider()
enableLedgerWritesForTest(t)

external := testutil.NewRandomAccount(t).PublicKey().ToBase58()
require.NoError(t, ApplyDeltasInTx(ctx, data, &balance.Delta{TokenAccount: external, Kind: balance.DeltaCredit, Quarks: 1}))
Expand All @@ -114,7 +98,6 @@ func TestApplyDeltasInTx_OnlyUntrackedCredits(t *testing.T) {
func TestApplyDeltasInTx_UnlockedAccount(t *testing.T) {
ctx := context.Background()
data := ocp_data.NewTestDataProvider()
enableLedgerWritesForTest(t)

unlocked := newLedgerTestAccountInfo(t, ctx, data, commonpb.AccountType_PRIMARY)
require.NoError(t, CreateRecordInTx(ctx, data, unlocked))
Expand All @@ -139,7 +122,6 @@ func TestApplyDeltasInTx_UnlockedAccount(t *testing.T) {
func TestApplyDeltasInTx_InvalidDelta(t *testing.T) {
ctx := context.Background()
data := ocp_data.NewTestDataProvider()
enableLedgerWritesForTest(t)

source := newLedgerTestAccount(t, ctx, data, commonpb.AccountType_PRIMARY)
assert.Error(t, ApplyDeltasInTx(ctx, data, &balance.Delta{TokenAccount: source, Kind: balance.DeltaDebit}))
Expand All @@ -152,19 +134,9 @@ func TestCreateRecordInTx(t *testing.T) {
ctx := context.Background()
data := ocp_data.NewTestDataProvider()

// Disabled writes are a no-op
primary := newLedgerTestAccountInfo(t, ctx, data, commonpb.AccountType_PRIMARY)
func() {
disableLedgerWritesForTest(t)
require.NoError(t, CreateRecordInTx(ctx, data, primary))
_, err := data.GetBalance(ctx, primary.TokenAccount)
assert.Equal(t, balance.ErrRecordNotFound, err)
}()

enableLedgerWritesForTest(t)

// A new timelock account starts backfilled at zero, so predicates are
// enforced immediately
// A new timelock account starts at zero, with predicates enforced
require.NoError(t, CreateRecordInTx(ctx, data, primary))
record, err := data.GetBalance(ctx, primary.TokenAccount)
require.NoError(t, err)
Expand All @@ -174,7 +146,6 @@ func TestCreateRecordInTx(t *testing.T) {
assert.EqualValues(t, 0, record.Quarks)
assert.EqualValues(t, 0, record.UsdCostBasis)
assert.True(t, record.IsOpen)
assert.True(t, record.IsBackfilled)

err = ApplyDeltasInTx(ctx, data, &balance.Delta{TokenAccount: primary.TokenAccount, Kind: balance.DeltaDebit, Quarks: 1})
assert.Equal(t, balance.ErrInsufficientBalance, err)
Expand Down Expand Up @@ -213,19 +184,3 @@ func newLedgerTestAccountInfo(t *testing.T, ctx context.Context, data ocp_data.P
require.NoError(t, data.CreateAccountInfo(ctx, record))
return record
}

func disableLedgerWritesForTest(t *testing.T) {
previous := enableLedgerWrites
enableLedgerWrites = wrapper.NewBoolConfig(memory.NewConfig(false), defaultEnableLedgerWrites)
t.Cleanup(func() {
enableLedgerWrites = previous
})
}

func enableLedgerWritesForTest(t *testing.T) {
previous := enableLedgerWrites
enableLedgerWrites = wrapper.NewBoolConfig(memory.NewConfig(true), defaultEnableLedgerWrites)
t.Cleanup(func() {
enableLedgerWrites = previous
})
}
Loading
Loading