diff --git a/go.mod b/go.mod index a7899f8..e593114 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/dynamodb v1.59.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 github.com/code-payments/code-vm-indexer v1.2.0 - github.com/code-payments/ocp-protobuf-api v1.16.1-0.20260918154336-e3d25a85b1e1 + github.com/code-payments/ocp-protobuf-api v1.17.1-0.20261011002549-00ea80d00cf1 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.3.3 github.com/golang/protobuf v1.5.4 diff --git a/go.sum b/go.sum index 0093131..decaa8d 100644 --- a/go.sum +++ b/go.sum @@ -80,8 +80,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/code-payments/code-vm-indexer v1.2.0 h1:rSHpBMiT9BKgmKcXg/VIoi/h0t7jNxGx07Qz59m+6Q0= github.com/code-payments/code-vm-indexer v1.2.0/go.mod h1:vn91YN2qNqb+gGJeZe2+l+TNxVmEEiRHXXnIn2Y40h8= -github.com/code-payments/ocp-protobuf-api v1.16.1-0.20260918154336-e3d25a85b1e1 h1:/cE319d70UiXoor2x9b2faiFTUFlzdgs10D4Fvq7dN4= -github.com/code-payments/ocp-protobuf-api v1.16.1-0.20260918154336-e3d25a85b1e1/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= +github.com/code-payments/ocp-protobuf-api v1.17.1-0.20261011002549-00ea80d00cf1 h1:G/9uGik00/xSGQ2Xsbs33WWq7k+MjFaBgvN739P8QmU= +github.com/code-payments/ocp-protobuf-api v1.17.1-0.20261011002549-00ea80d00cf1/go.mod h1:tw6BooY5a8l6CtSZnKOruyKII0W04n89pcM4BizrgG8= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/ocp/rpc/balance/server.go b/ocp/rpc/balance/server.go index 242319a..238e1e4 100644 --- a/ocp/rpc/balance/server.go +++ b/ocp/rpc/balance/server.go @@ -90,7 +90,7 @@ func (s *server) GetBalances(ctx context.Context, req *balancepb.GetBalancesRequ balancesByOwner := make(map[string]*balancepb.OwnerBalance, len(owners)) for _, owner := range owners { - ownerBalance, err := s.valueOwnerBalance(ctx, owner, balanceByOwnerAndTokenAccount[owner.PublicKey().ToBase58()], reserveStateCache, exchangeRates) + ownerBalance, err := s.valueOwnerBalance(ctx, owner, balanceByOwnerAndTokenAccount[owner.PublicKey().ToBase58()], reserveStateCache, exchangeRates, req.ExcludePerMintBalances) if err != nil { log.With(zap.Error(err), zap.String("owner_account", owner.PublicKey().ToBase58())).Warn("failure valuing owner balance") return nil, status.Error(codes.Internal, "") @@ -107,8 +107,9 @@ func (s *server) GetBalances(ctx context.Context, req *balancepb.GetBalancesRequ // valueOwnerBalance values an owner's cached holdings. Owner metadata checks // are intentionally skipped as an optimization, so an owner with no ledger -// records has an empty balance. -func (s *server) valueOwnerBalance(ctx context.Context, owner *common.Account, balanceByTokenAccount map[string]*balance.Balance, reserveStateCache reserveStateCache, exchangeRates exchangeRates) (*balancepb.OwnerBalance, error) { +// records has an empty balance. Per-mint balances are still calculated when +// excluded, since they make up the owner's total. +func (s *server) valueOwnerBalance(ctx context.Context, owner *common.Account, balanceByTokenAccount map[string]*balance.Balance, reserveStateCache reserveStateCache, exchangeRates exchangeRates, excludePerMintBalances bool) (*balancepb.OwnerBalance, error) { balancesByMint, err := s.calculateCoreMintValueByMint(ctx, balanceByTokenAccount, reserveStateCache) if err != nil { return nil, err @@ -117,7 +118,13 @@ func (s *server) valueOwnerBalance(ctx context.Context, owner *common.Account, b var totalCoreMintValue uint64 for _, mintBalance := range balancesByMint { totalCoreMintValue += mintBalance.CoreMintValue - mintBalance.FiatValuesByCurrency = exchangeRates.toFiatValues(mintBalance.CoreMintValue) + if !excludePerMintBalances { + mintBalance.FiatValuesByCurrency = exchangeRates.toFiatValues(mintBalance.CoreMintValue) + } + } + + if excludePerMintBalances { + balancesByMint = nil } return &balancepb.OwnerBalance{ @@ -221,7 +228,8 @@ func (s *server) getLiveReserveState(ctx context.Context, mint *common.Account, } // calculateCoreMintValueByMint values cached holdings in each mint with a -// non-zero balance. Each balance carries the mint it holds. +// non-zero balance. Each balance carries the mint it holds and the quarks of +// that mint. func (s *server) calculateCoreMintValueByMint(ctx context.Context, balanceByTokenAccount map[string]*balance.Balance, reserveStateCache reserveStateCache) (map[string]*balancepb.MintBalance, error) { quarksByMint := make(map[string]uint64) for _, cached := range balanceByTokenAccount { @@ -262,6 +270,7 @@ func (s *server) calculateCoreMintValueByMint(ctx context.Context, balanceByToke balancesByMint[mint] = &balancepb.MintBalance{ Mint: mintAccount.ToProto(), + Quarks: quarks, CoreMintValue: coreMintValue, } } diff --git a/ocp/rpc/balance/server_test.go b/ocp/rpc/balance/server_test.go index 1806d79..e22f1bd 100644 --- a/ocp/rpc/balance/server_test.go +++ b/ocp/rpc/balance/server_test.go @@ -158,19 +158,19 @@ func TestGetBalances_HappyPath(t *testing.T) { ownerBalance1 := assertOwnerBalance(t, resp, ownerAccount1, common.ToCoreMintQuarks(42)+expectedLaunchpadMintValue, nil) require.Len(t, ownerBalance1.BalancesByMint, 2) - assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), nil) - assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, expectedLaunchpadMintValue, nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), common.ToCoreMintQuarks(42), nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, currencycreator.ToQuarks(100), expectedLaunchpadMintValue, nil) ownerBalance2 := assertOwnerBalance(t, resp, ownerAccount2, common.ToCoreMintQuarks(7), nil) require.Len(t, ownerBalance2.BalancesByMint, 1) - assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), nil) + assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), common.ToCoreMintQuarks(7), nil) unknownOwnerBalance := assertOwnerBalance(t, resp, unknownOwnerAccount, 0, nil) assert.Empty(t, unknownOwnerBalance.BalancesByMint) giftCardOwnerBalance := assertOwnerBalance(t, resp, giftCardOwnerAccount, common.ToCoreMintQuarks(1), nil) require.Len(t, giftCardOwnerBalance.BalancesByMint, 1) - assertMintBalance(t, giftCardOwnerBalance.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(1), nil) + assertMintBalance(t, giftCardOwnerBalance.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(1), common.ToCoreMintQuarks(1), nil) // Every requested currency with a live rate is additionally populated on // both the owner total and the per-mint breakdown, using the same rate @@ -185,12 +185,12 @@ func TestGetBalances_HappyPath(t *testing.T) { ownerBalance1 = assertOwnerBalance(t, resp, ownerAccount1, common.ToCoreMintQuarks(42)+expectedLaunchpadMintValue, expectedCurrencyCodes) require.Len(t, ownerBalance1.BalancesByMint, 2) - assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), expectedCurrencyCodes) - assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, expectedLaunchpadMintValue, expectedCurrencyCodes) + assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), common.ToCoreMintQuarks(42), expectedCurrencyCodes) + assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, currencycreator.ToQuarks(100), expectedLaunchpadMintValue, expectedCurrencyCodes) ownerBalance2 = assertOwnerBalance(t, resp, ownerAccount2, common.ToCoreMintQuarks(7), expectedCurrencyCodes) require.Len(t, ownerBalance2.BalancesByMint, 1) - assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), expectedCurrencyCodes) + assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), common.ToCoreMintQuarks(7), expectedCurrencyCodes) // An owner with no balance still reports a zero fiat value in each // available currency @@ -199,7 +199,7 @@ func TestGetBalances_HappyPath(t *testing.T) { giftCardOwnerBalance = assertOwnerBalance(t, resp, giftCardOwnerAccount, common.ToCoreMintQuarks(1), expectedCurrencyCodes) require.Len(t, giftCardOwnerBalance.BalancesByMint, 1) - assertMintBalance(t, giftCardOwnerBalance.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(1), expectedCurrencyCodes) + assertMintBalance(t, giftCardOwnerBalance.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(1), common.ToCoreMintQuarks(1), expectedCurrencyCodes) // Requesting only currencies without a live rate results in no fiat values req.CurrencyCodes = []string{"jpy", "gbp"} @@ -211,19 +211,19 @@ func TestGetBalances_HappyPath(t *testing.T) { ownerBalance1 = assertOwnerBalance(t, resp, ownerAccount1, common.ToCoreMintQuarks(42)+expectedLaunchpadMintValue, nil) require.Len(t, ownerBalance1.BalancesByMint, 2) - assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), nil) - assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, expectedLaunchpadMintValue, nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), common.ToCoreMintQuarks(42), nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, currencycreator.ToQuarks(100), expectedLaunchpadMintValue, nil) ownerBalance2 = assertOwnerBalance(t, resp, ownerAccount2, common.ToCoreMintQuarks(7), nil) require.Len(t, ownerBalance2.BalancesByMint, 1) - assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), nil) + assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), common.ToCoreMintQuarks(7), nil) unknownOwnerBalance = assertOwnerBalance(t, resp, unknownOwnerAccount, 0, nil) assert.Empty(t, unknownOwnerBalance.BalancesByMint) giftCardOwnerBalance = assertOwnerBalance(t, resp, giftCardOwnerAccount, common.ToCoreMintQuarks(1), nil) require.Len(t, giftCardOwnerBalance.BalancesByMint, 1) - assertMintBalance(t, giftCardOwnerBalance.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(1), nil) + assertMintBalance(t, giftCardOwnerBalance.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(1), common.ToCoreMintQuarks(1), nil) } func TestGetBalances_MintFilter(t *testing.T) { @@ -262,11 +262,11 @@ func TestGetBalances_MintFilter(t *testing.T) { ownerBalance1 := assertOwnerBalance(t, resp, ownerAccount1, common.ToCoreMintQuarks(42), nil) require.Len(t, ownerBalance1.BalancesByMint, 1) - assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), common.ToCoreMintQuarks(42), nil) ownerBalance2 := assertOwnerBalance(t, resp, ownerAccount2, common.ToCoreMintQuarks(7), nil) require.Len(t, ownerBalance2.BalancesByMint, 1) - assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), nil) + assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), common.ToCoreMintQuarks(7), nil) // The filter applies to every owner. An owner holding nothing in the // filtered mints is still present with an empty balance. @@ -280,7 +280,7 @@ func TestGetBalances_MintFilter(t *testing.T) { ownerBalance1 = assertOwnerBalance(t, resp, ownerAccount1, expectedLaunchpadMintValue, nil) require.Len(t, ownerBalance1.BalancesByMint, 1) - assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, expectedLaunchpadMintValue, nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, currencycreator.ToQuarks(100), expectedLaunchpadMintValue, nil) ownerBalance2 = assertOwnerBalance(t, resp, ownerAccount2, 0, nil) assert.Empty(t, ownerBalance2.BalancesByMint) @@ -296,12 +296,12 @@ func TestGetBalances_MintFilter(t *testing.T) { ownerBalance1 = assertOwnerBalance(t, resp, ownerAccount1, common.ToCoreMintQuarks(42)+expectedLaunchpadMintValue, nil) require.Len(t, ownerBalance1.BalancesByMint, 2) - assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), nil) - assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, expectedLaunchpadMintValue, nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), common.ToCoreMintQuarks(42), nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, launchpadMint, currencycreator.ToQuarks(100), expectedLaunchpadMintValue, nil) ownerBalance2 = assertOwnerBalance(t, resp, ownerAccount2, common.ToCoreMintQuarks(7), nil) require.Len(t, ownerBalance2.BalancesByMint, 1) - assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), nil) + assertMintBalance(t, ownerBalance2.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(7), common.ToCoreMintQuarks(7), nil) // A mint no owner holds results in an OK response with no balances resp, err = env.client.GetBalances(env.ctx, &balancepb.GetBalancesRequest{ @@ -319,6 +319,65 @@ func TestGetBalances_MintFilter(t *testing.T) { assert.Empty(t, ownerBalance2.BalancesByMint) } +func TestGetBalances_ExcludePerMintBalances(t *testing.T) { + env, cleanup := setup(t) + defer cleanup() + + coreVmConfig := testutil.NewRandomVmConfig(t, true) + launchpadMint := testutil.SetupLaunchpadCurrency(t, env.data, env.reserveStore, env.holderStore) + launchpadVmConfig, err := common.GetVmConfigForMint(env.ctx, env.data, launchpadMint) + require.NoError(t, err) + + ownerAccount1 := testutil.NewRandomAccount(t) + ownerAccount2 := testutil.NewRandomAccount(t) + + owner1CoreMintAccountRecords := setupAccountRecords(t, env, ownerAccount1, ownerAccount1, coreVmConfig, 0, commonpb.AccountType_PRIMARY) + owner1LaunchpadMintAccountRecords := setupAccountRecords(t, env, ownerAccount1, ownerAccount1, launchpadVmConfig, 0, commonpb.AccountType_PRIMARY) + owner2CoreMintAccountRecords := setupAccountRecords(t, env, ownerAccount2, ownerAccount2, coreVmConfig, 0, commonpb.AccountType_PRIMARY) + + setupCachedBalance(t, env, owner1CoreMintAccountRecords, common.ToCoreMintQuarks(42)) + setupCachedBalance(t, env, owner1LaunchpadMintAccountRecords, currencycreator.ToQuarks(100)) + setupCachedBalance(t, env, owner2CoreMintAccountRecords, common.ToCoreMintQuarks(7)) + + expectedLaunchpadMintValue := estimateLaunchpadSellValue(t, currencycreator.ToQuarks(100)) + + owners := []*commonpb.SolanaAccountId{ownerAccount1.ToProto(), ownerAccount2.ToProto()} + expectedCurrencyCodes := []string{"usd", "cad"} + + // Owner totals still account for every mint, but the per-mint breakdown + // is omitted + resp, err := env.client.GetBalances(env.ctx, &balancepb.GetBalancesRequest{ + Owners: owners, + CurrencyCodes: expectedCurrencyCodes, + ExcludePerMintBalances: true, + }) + require.NoError(t, err) + assert.Equal(t, balancepb.GetBalancesResponse_OK, resp.Result) + require.Len(t, resp.BalancesByOwner, 2) + + ownerBalance1 := assertOwnerBalance(t, resp, ownerAccount1, common.ToCoreMintQuarks(42)+expectedLaunchpadMintValue, expectedCurrencyCodes) + assert.Empty(t, ownerBalance1.BalancesByMint) + + ownerBalance2 := assertOwnerBalance(t, resp, ownerAccount2, common.ToCoreMintQuarks(7), expectedCurrencyCodes) + assert.Empty(t, ownerBalance2.BalancesByMint) + + // The mint filter still applies to owner totals + resp, err = env.client.GetBalances(env.ctx, &balancepb.GetBalancesRequest{ + Owners: owners, + Mints: []*commonpb.SolanaAccountId{launchpadMint.ToProto()}, + ExcludePerMintBalances: true, + }) + require.NoError(t, err) + assert.Equal(t, balancepb.GetBalancesResponse_OK, resp.Result) + require.Len(t, resp.BalancesByOwner, 2) + + ownerBalance1 = assertOwnerBalance(t, resp, ownerAccount1, expectedLaunchpadMintValue, nil) + assert.Empty(t, ownerBalance1.BalancesByMint) + + ownerBalance2 = assertOwnerBalance(t, resp, ownerAccount2, 0, nil) + assert.Empty(t, ownerBalance2.BalancesByMint) +} + func TestGetBalances_UnmanagedAccountsExcluded(t *testing.T) { env, cleanup := setup(t) defer cleanup() @@ -362,7 +421,7 @@ func TestGetBalances_UnmanagedAccountsExcluded(t *testing.T) { // Only the account still managed by Code contributes to owner 1's balance ownerBalance1 := assertOwnerBalance(t, resp, ownerAccount1, common.ToCoreMintQuarks(42), nil) require.Len(t, ownerBalance1.BalancesByMint, 1) - assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), common.ToCoreMintQuarks(42), nil) // An owner whose only account is unmanaged has an empty balance ownerBalance2 := assertOwnerBalance(t, resp, ownerAccount2, 0, nil) @@ -413,14 +472,14 @@ func TestGetBalances_DustHidden(t *testing.T) { // Dust is excluded from both the total and the per-mint breakdown ownerBalance1 := assertOwnerBalance(t, resp, ownerAccount1, common.ToCoreMintQuarks(42), nil) require.Len(t, ownerBalance1.BalancesByMint, 1) - assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), nil) + assertMintBalance(t, ownerBalance1.BalancesByMint, common.CoreMintAccount, common.ToCoreMintQuarks(42), common.ToCoreMintQuarks(42), nil) ownerBalance2 := assertOwnerBalance(t, resp, ownerAccount2, 0, nil) assert.Empty(t, ownerBalance2.BalancesByMint) ownerBalance3 := assertOwnerBalance(t, resp, ownerAccount3, dustValue, nil) require.Len(t, ownerBalance3.BalancesByMint, 1) - assertMintBalance(t, ownerBalance3.BalancesByMint, common.CoreMintAccount, dustValue, nil) + assertMintBalance(t, ownerBalance3.BalancesByMint, common.CoreMintAccount, dustValue, dustValue, nil) } func TestGetBalances_UnknownOwnerAccount(t *testing.T) { @@ -567,10 +626,11 @@ func assertOwnerBalance(t *testing.T, resp *balancepb.GetBalancesResponse, owner return ownerBalance } -func assertMintBalance(t *testing.T, balancesByMint map[string]*balancepb.MintBalance, mint *common.Account, expectedCoreMintValue uint64, expectedCurrencyCodes []string) { +func assertMintBalance(t *testing.T, balancesByMint map[string]*balancepb.MintBalance, mint *common.Account, expectedQuarks, expectedCoreMintValue uint64, expectedCurrencyCodes []string) { mintBalance, ok := balancesByMint[mint.PublicKey().ToBase58()] require.True(t, ok) assert.Equal(t, mint.PublicKey().ToBytes(), mintBalance.Mint.Value) + assert.EqualValues(t, expectedQuarks, mintBalance.Quarks) assert.EqualValues(t, expectedCoreMintValue, mintBalance.CoreMintValue) assertFiatValues(t, mintBalance.FiatValuesByCurrency, expectedCoreMintValue, expectedCurrencyCodes) }