From 6cf4c9fe494efe30c0c2c4a72038bc79c268f05a Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 24 Jun 2026 21:35:02 +0200 Subject: [PATCH 1/9] =?UTF-8?q?feat!:=20color=20of=20money=20=E2=80=94=20s?= =?UTF-8?q?egregated=20balances=20per=20(account,=20asset,=20color)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed from 30+ iterations. Color is a first-class dimension on every balance, posting, audit order, and read-side projection. - Posting.Color, AccountVolume keyed by (account, asset, color) - Numscript scripts default to "" color; explicit color via syntax - Audit orders carry color through transient/purged exclusion sets - check/ verifies color through excludedVolumesSet, partitionVolumes - REST/gRPC/JSON always emit color (including uncolored bucket) - analysis carries color through flow signatures, NUL-separated to prevent collisions - admission validates Posting.Color before preload extraction Will require manual rebase on top of main (post-master→main migration, metadata-immutability refactor, AppliedProposal envelopes, etc.). --- cmd/ledgerctl/accounts/aggregate.go | 5 +- cmd/ledgerctl/accounts/get.go | 24 +- cmd/ledgerctl/queries/execute.go | 6 +- cmd/ledgerctl/transactions/create.go | 29 +- cmd/ledgerctl/transactions/display_volumes.go | 27 +- cmd/ledgerctl/transactions/get.go | 10 +- cmd/ledgerctl/transactions/revert.go | 7 +- docs/ops/cli.md | 20 +- .../architecture/data-model/color-of-money.md | 97 + docs/technical/contributing/api-comparison.md | 26 +- go.mod | 6 +- go.sum | 30 +- internal/adapter/grpc/client_bucket.go | 8 +- internal/adapter/grpc/client_bucket_test.go | 5 +- .../adapter/grpc/controller_generated_test.go | 13 +- internal/adapter/grpc/server_bucket.go | 5 +- .../adapter/http/backend_generated_test.go | 13 +- .../http/handlers_aggregate_volumes.go | 9 +- .../http/handlers_aggregate_volumes_test.go | 37 + .../http/handlers_analyze_transactions.go | 6 + .../adapter/http/handlers_coverage_test.go | 5 +- internal/adapter/http/handlers_get_account.go | 10 +- .../adapter/http/handlers_get_account_test.go | 93 +- internal/application/admission/admission.go | 43 +- .../application/admission/validate_order.go | 41 + .../admission/validate_order_test.go | 79 + internal/application/check/checker.go | 35 +- internal/application/ctrl/controller.go | 16 +- .../application/ctrl/controller_default.go | 10 +- .../ctrl/controller_generated_test.go | 8 +- .../ctrl/ctrlmock/controller_generated.go | 9 +- internal/application/ctrl/store.go | 111 +- internal/application/ctrl/store_color_test.go | 101 + .../events/clickhouse_data_test.go | 60 + .../application/events/sink_data_common.go | 6 + .../indexbuilder/backfill_postings.go | 2 +- .../application/indexbuilder/backfill_test.go | 8 +- .../application/indexbuilder/process_logs.go | 22 +- .../indexbuilder/protowire_postings.go | 54 +- internal/bootstrap/controller_routed.go | 4 +- internal/domain/analysis/compact.go | 5 + internal/domain/analysis/flow_discovery.go | 20 +- .../domain/analysis/flow_discovery_test.go | 39 + internal/domain/errors.go | 94 +- internal/domain/errors_test.go | 2 + internal/domain/keys.go | 52 +- internal/domain/keys_test.go | 68 +- .../domain/processing/numscript/emulate.go | 36 +- .../processing/numscript/emulate_test.go | 22 + .../domain/processing/numscript/errors.go | 19 + .../numscript_store_adapter_test.go | 78 +- .../domain/processing/processor_posting.go | 21 +- .../processing/processor_posting_test.go | 27 +- .../processor_revert_transaction.go | 16 +- .../processor_revert_transaction_test.go | 24 +- internal/domain/processing/processor_test.go | 6 +- .../processing/processor_transaction.go | 10 +- .../processor_transaction_numscript.go | 92 +- .../processing/processor_transaction_test.go | 30 +- .../domain/processing/processor_volumes.go | 86 +- internal/domain/reason.go | 2 + internal/domain/replay/replay.go | 17 +- internal/domain/touched_volume.go | 2 +- internal/domain/validation.go | 35 + internal/domain/validation_test.go | 35 + internal/infra/receipt/receipt.go | 6 + internal/infra/receipt/receipt_test.go | 40 + internal/infra/state/sentinel.go | 4 +- internal/infra/state/write_set.go | 29 +- .../infra/state/write_set_ephemeral_purge.go | 43 +- internal/proto/commonpb/account.go | 45 + internal/proto/commonpb/account_json_test.go | 33 + internal/proto/commonpb/common.pb.go | 1975 +++++++++-------- internal/proto/commonpb/common.pb.json.go | 47 +- internal/proto/commonpb/common_dethash.pb.go | 68 +- internal/proto/commonpb/common_reader.pb.go | 219 +- internal/proto/commonpb/common_vtproto.pb.go | 1679 +++++++++----- internal/proto/commonpb/posting.go | 59 +- internal/proto/commonpb/posting_json_test.go | 35 + internal/proto/commonpb/transaction.go | 34 - internal/proto/commonpb/volumes.go | 218 +- internal/proto/servicepb/bucket.pb.go | 65 +- internal/proto/servicepb/bucket_reader.pb.go | 15 + internal/proto/servicepb/bucket_vtproto.pb.go | 121 + internal/query/aggregate.go | 164 +- internal/query/aggregate_test.go | 142 +- internal/query/compact_account.go | 61 +- misc/proto/bucket.proto | 13 + misc/proto/common.proto | 51 +- openapi.yml | 112 +- pkg/actions/actions.go | 7 +- pkg/scenario/block.go | 30 +- .../bin/cmds/main/eventually_correct/main.go | 25 +- .../eventually_cross_node_identity/main.go | 4 +- .../main/parallel_driver_stale_reads/main.go | 2 +- tests/antithesis/workload/go.mod | 4 +- tests/antithesis/workload/go.sum | 12 +- tests/antithesis/workload/internal/checks.go | 18 +- tests/e2e/business/barrier_test.go | 5 +- tests/e2e/business/color_numscript_test.go | 340 +++ tests/e2e/business/color_segregation_test.go | 174 ++ tests/e2e/business/ephemeral_purge_test.go | 12 +- tests/e2e/business/force_test.go | 32 +- tests/e2e/business/idempotency_test.go | 10 +- .../business/ledger_delete_cleanup_test.go | 8 +- tests/e2e/business/numscript_library_test.go | 4 +- tests/e2e/business/numscript_test.go | 42 +- tests/e2e/business/reversions_test.go | 28 +- tests/e2e/business/transactions_test.go | 99 +- tests/e2e/business/transient_accounts_test.go | 28 +- tests/e2e/cluster/bloom_config_test.go | 4 +- tests/e2e/cluster/bootstrap_test.go | 10 +- tests/e2e/cluster/cache_divergence_test.go | 8 +- tests/e2e/cluster/query_checkpoint_test.go | 10 +- tests/e2e/cluster/restore_test.go | 16 +- tests/e2e/cluster/rolling_config_test.go | 4 +- tests/scenarios/scenariotest/scenariotest.go | 82 +- 117 files changed, 5566 insertions(+), 2668 deletions(-) create mode 100644 docs/technical/architecture/data-model/color-of-money.md create mode 100644 internal/application/ctrl/store_color_test.go create mode 100644 internal/proto/commonpb/account.go create mode 100644 internal/proto/commonpb/account_json_test.go create mode 100644 internal/proto/commonpb/posting_json_test.go create mode 100644 tests/e2e/business/color_numscript_test.go create mode 100644 tests/e2e/business/color_segregation_test.go diff --git a/cmd/ledgerctl/accounts/aggregate.go b/cmd/ledgerctl/accounts/aggregate.go index ac8c013a31..1dbffbab2d 100644 --- a/cmd/ledgerctl/accounts/aggregate.go +++ b/cmd/ledgerctl/accounts/aggregate.go @@ -99,6 +99,7 @@ func runAggregateVolumes(cmd *cobra.Command, _ []string) error { { type jsonVolume struct { Asset string `json:"asset"` + Color string `json:"color"` Input string `json:"input"` Output string `json:"output"` Balance string `json:"balance"` @@ -112,6 +113,7 @@ func runAggregateVolumes(cmd *cobra.Command, _ []string) error { balance := new(big.Int).Sub(input, output) volumes = append(volumes, jsonVolume{ Asset: vol.GetAsset(), + Color: vol.GetColor(), Input: input.String(), Output: output.String(), Balance: balance.String(), @@ -130,7 +132,7 @@ func runAggregateVolumes(cmd *cobra.Command, _ []string) error { } tableData := pterm.TableData{ - {"ASSET", "INPUT", "OUTPUT", "BALANCE"}, + {"ASSET", "COLOR", "INPUT", "OUTPUT", "BALANCE"}, } for _, vol := range result.GetVolumes() { @@ -139,6 +141,7 @@ func runAggregateVolumes(cmd *cobra.Command, _ []string) error { balance := new(big.Int).Sub(input, output) tableData = append(tableData, []string{ vol.GetAsset(), + vol.GetColor(), input.String(), output.String(), formatBalance(balance), diff --git a/cmd/ledgerctl/accounts/get.go b/cmd/ledgerctl/accounts/get.go index fbe03b9a60..8917c1a5a9 100644 --- a/cmd/ledgerctl/accounts/get.go +++ b/cmd/ledgerctl/accounts/get.go @@ -3,7 +3,6 @@ package accounts import ( "errors" "fmt" - "sort" "github.com/pterm/pterm" "github.com/spf13/cobra" @@ -129,18 +128,13 @@ func runGet(cmd *cobra.Command, args []string) error { if len(account.GetVolumes()) > 0 { volumesTable := pterm.TableData{ - {"ASSET", "INPUT", "OUTPUT", "BALANCE"}, + {"ASSET", "COLOR", "INPUT", "OUTPUT", "BALANCE"}, } - assets := make([]string, 0, len(account.GetVolumes())) - for asset := range account.GetVolumes() { - assets = append(assets, asset) - } - - sort.Strings(assets) - - for _, asset := range assets { - vol := account.GetVolumes()[asset] + // account.GetVolumes() is already sorted by (asset, color) ascending + // server-side, so we just render in-order. + for _, entry := range account.GetVolumes() { + vol := entry.GetVolumes() balance := vol.GetBalance() balanceColor := pterm.Green @@ -148,8 +142,14 @@ func runGet(cmd *cobra.Command, args []string) error { balanceColor = pterm.Red } + displayColor := entry.GetColor() + if displayColor == "" { + displayColor = "-" + } + volumesTable = append(volumesTable, []string{ - asset, + entry.GetAsset(), + displayColor, vol.GetInput(), vol.GetOutput(), balanceColor(balance), diff --git a/cmd/ledgerctl/queries/execute.go b/cmd/ledgerctl/queries/execute.go index 00307d1c96..968278a376 100644 --- a/cmd/ledgerctl/queries/execute.go +++ b/cmd/ledgerctl/queries/execute.go @@ -232,10 +232,11 @@ func renderAggregate(cmd *cobra.Command, result *commonpb.AggregateResult) error } if len(result.GetVolumes()) > 0 { - tableData := pterm.TableData{{"ASSET", "INPUT", "OUTPUT"}} + tableData := pterm.TableData{{"ASSET", "COLOR", "INPUT", "OUTPUT"}} for _, v := range result.GetVolumes() { tableData = append(tableData, []string{ v.GetAsset(), + v.GetColor(), v.GetInput().ToBigInt().String(), v.GetOutput().ToBigInt().String(), }) @@ -248,10 +249,11 @@ func renderAggregate(cmd *cobra.Command, result *commonpb.AggregateResult) error pterm.Println() pterm.Printfln("Group: %s", g.GetPrefix()) - tableData := pterm.TableData{{"ASSET", "INPUT", "OUTPUT"}} + tableData := pterm.TableData{{"ASSET", "COLOR", "INPUT", "OUTPUT"}} for _, v := range g.GetVolumes() { tableData = append(tableData, []string{ v.GetAsset(), + v.GetColor(), v.GetInput().ToBigInt().String(), v.GetOutput().ToBigInt().String(), }) diff --git a/cmd/ledgerctl/transactions/create.go b/cmd/ledgerctl/transactions/create.go index 74039c7f58..df8ff9d122 100644 --- a/cmd/ledgerctl/transactions/create.go +++ b/cmd/ledgerctl/transactions/create.go @@ -75,7 +75,7 @@ func NewCreateCommand() *cobra.Command { Long: `Create a new transaction via gRPC. Postings can be provided via flag, or use a Numscript file. -Flag format: --posting "source,destination,amount,asset" +Flag format: --posting "source,destination,amount,asset[,color]" Examples: ledgerctl transactions create --ledger my-ledger --posting "world,bank,1000,USD" @@ -88,7 +88,7 @@ Examples: } cmd.Flags().String("ledger", "", "Name of the ledger") - cmd.Flags().StringArray("posting", nil, "Posting in format: source,destination,amount,asset (can be repeated)") + cmd.Flags().StringArray("posting", nil, "Posting in format: source,destination,amount,asset[,color] (can be repeated)") cmd.Flags().String("script", "", "Path to a Numscript file (mutually exclusive with --posting)") cmd.Flags().StringArray("var", nil, "Script variable in format: name=value (can be repeated, only with --script)") cmd.Flags().String("reference", "", "Transaction reference") @@ -460,10 +460,14 @@ func runCreate(cmd *cobra.Command, _ []string) error { pterm.Println("Postings:") postingsTable := pterm.TableData{ - {"#", "SOURCE", "", "DESTINATION", "AMOUNT", "ASSET"}, + {"#", "SOURCE", "", "DESTINATION", "AMOUNT", "ASSET", "COLOR"}, } for i, posting := range tx.GetPostings() { + color := posting.GetColor() + if color == "" { + color = "-" + } postingsTable = append(postingsTable, []string{ strconv.Itoa(i + 1), posting.GetSource(), @@ -471,6 +475,7 @@ func runCreate(cmd *cobra.Command, _ []string) error { posting.GetDestination(), posting.GetAmount().Dec(), posting.GetAsset(), + color, }) } @@ -512,20 +517,28 @@ func runCreate(cmd *cobra.Command, _ []string) error { return nil } -// parsePosting parses a posting from string format "source,destination,amount,asset". +// parsePosting parses a posting from string format +// - "source,destination,amount,asset" (uncolored, 4 fields) +// - "source,destination,amount,asset,color" (colored, 5 fields) +// +// The color field is optional. An empty fifth field is treated as no color. func parsePosting(s string) (*commonpb.Posting, error) { parts := strings.Split(s, ",") - if len(parts) != 4 { - return nil, errors.New("expected format: source,destination,amount,asset") + if len(parts) != 4 && len(parts) != 5 { + return nil, errors.New("expected format: source,destination,amount,asset[,color]") } source := strings.TrimSpace(parts[0]) destination := strings.TrimSpace(parts[1]) amountStr := strings.TrimSpace(parts[2]) asset := strings.TrimSpace(parts[3]) + color := "" + if len(parts) == 5 { + color = strings.TrimSpace(parts[4]) + } if source == "" || destination == "" || amountStr == "" || asset == "" { - return nil, errors.New("all fields are required") + return nil, errors.New("source, destination, amount and asset are required") } amount, ok := new(big.Int).SetString(amountStr, 10) @@ -533,7 +546,7 @@ func parsePosting(s string) (*commonpb.Posting, error) { return nil, fmt.Errorf("invalid amount: %s", amountStr) } - return commonpb.NewPosting(source, destination, asset, amount), nil + return commonpb.NewColoredPosting(source, destination, asset, color, amount), nil } // promptVariable prompts the user for a Numscript variable value based on its type. diff --git a/cmd/ledgerctl/transactions/display_volumes.go b/cmd/ledgerctl/transactions/display_volumes.go index dfa0e19d0e..6762e23ef1 100644 --- a/cmd/ledgerctl/transactions/display_volumes.go +++ b/cmd/ledgerctl/transactions/display_volumes.go @@ -9,6 +9,8 @@ import ( ) // renderPostCommitVolumes displays a PostCommitVolumes table in the CLI output. +// Volumes are listed per (account, asset, color). The "" color is rendered as +// "-" so the uncolored bucket stands out in the table. func renderPostCommitVolumes(pcv *commonpb.PostCommitVolumes) error { if len(pcv.GetVolumesByAccount()) == 0 { return nil @@ -18,33 +20,28 @@ func renderPostCommitVolumes(pcv *commonpb.PostCommitVolumes) error { pterm.Println("Post-Commit Volumes:") table := pterm.TableData{ - {"ACCOUNT", "ASSET", "INPUT", "OUTPUT"}, + {"ACCOUNT", "ASSET", "COLOR", "INPUT", "OUTPUT"}, } - // Sort accounts for stable output accounts := make([]string, 0, len(pcv.GetVolumesByAccount())) for account := range pcv.GetVolumesByAccount() { accounts = append(accounts, account) } - sort.Strings(accounts) for _, account := range accounts { vba := pcv.GetVolumesByAccount()[account] - - // Sort assets for stable output - assets := make([]string, 0, len(vba.GetVolumes())) - for asset := range vba.GetVolumes() { - assets = append(assets, asset) - } - - sort.Strings(assets) - - for _, asset := range assets { - v := vba.GetVolumes()[asset] + // VolumesByAssets.Volumes is sorted by (asset, color) server-side. + for _, entry := range vba.GetVolumes() { + v := entry.GetVolumes() + displayColor := entry.GetColor() + if displayColor == "" { + displayColor = "-" + } table = append(table, []string{ account, - asset, + entry.GetAsset(), + displayColor, v.GetInput(), v.GetOutput(), }) diff --git a/cmd/ledgerctl/transactions/get.go b/cmd/ledgerctl/transactions/get.go index cd2e69d913..eac0a6e864 100644 --- a/cmd/ledgerctl/transactions/get.go +++ b/cmd/ledgerctl/transactions/get.go @@ -148,7 +148,7 @@ func runGet(cmd *cobra.Command, args []string) error { pterm.Println("Postings:") postingsTable := pterm.TableData{ - {"#", "SOURCE", "", "DESTINATION", "AMOUNT", "ASSET"}, + {"#", "SOURCE", "", "DESTINATION", "AMOUNT", "ASSET", "COLOR"}, } termWidth := pterm.GetTerminalWidth() @@ -167,7 +167,7 @@ func runGet(cmd *cobra.Command, args []string) error { for line := range maxLines { src, dst := "", "" - num, arrow, amount, asset := "", "", "", "" + num, arrow, amount, asset, color := "", "", "", "", "" if line < len(srcLines) { src = srcLines[line] @@ -188,10 +188,14 @@ func runGet(cmd *cobra.Command, args []string) error { arrow = "→" amount = posting.GetAmount().Dec() asset = posting.GetAsset() + color = posting.GetColor() + if color == "" { + color = "-" + } } postingsTable = append(postingsTable, []string{ - num, src, arrow, dst, amount, asset, + num, src, arrow, dst, amount, asset, color, }) } } diff --git a/cmd/ledgerctl/transactions/revert.go b/cmd/ledgerctl/transactions/revert.go index e20c183d43..f76e2d4c8e 100644 --- a/cmd/ledgerctl/transactions/revert.go +++ b/cmd/ledgerctl/transactions/revert.go @@ -233,10 +233,14 @@ func runRevert(cmd *cobra.Command, args []string) error { pterm.Println("Revert Postings:") postingsTable := pterm.TableData{ - {"#", "SOURCE", "", "DESTINATION", "AMOUNT", "ASSET"}, + {"#", "SOURCE", "", "DESTINATION", "AMOUNT", "ASSET", "COLOR"}, } for i, posting := range revertedTx.GetRevertTransaction().GetPostings() { + color := posting.GetColor() + if color == "" { + color = "-" + } postingsTable = append(postingsTable, []string{ strconv.Itoa(i + 1), posting.GetSource(), @@ -244,6 +248,7 @@ func runRevert(cmd *cobra.Command, args []string) error { posting.GetDestination(), posting.GetAmount().Dec(), posting.GetAsset(), + color, }) } diff --git a/docs/ops/cli.md b/docs/ops/cli.md index 2e9832f5e0..871b900319 100644 --- a/docs/ops/cli.md +++ b/docs/ops/cli.md @@ -1285,7 +1285,7 @@ ledgerctl transactions create [flags] | Flag | Default | Description | |------|---------|-------------| | `--ledger` | | Name of the ledger | -| `--posting` | | Posting in format: `source,destination,amount,asset` (can be repeated) | +| `--posting` | | Posting in format: `source,destination,amount,asset[,color]` (can be repeated) | | `--script` | | Path to a Numscript file (mutually exclusive with `--posting`) | | `--var` | | Script variable in format: `name=value` (can be repeated, only with `--script`) | | `--reference` | | Transaction reference | @@ -1316,8 +1316,26 @@ ledgerctl transactions create --ledger my-ledger \ ledgerctl transactions create --ledger my-ledger \ --posting "empty-account,destination,1000,USD" \ --force + +# Colored postings (segregated balances per (account, asset, color)). +# Color must match ^[A-Z]*$. An empty/missing color is the uncolored bucket +# and is itself segregated from every colored bucket. +ledgerctl transactions create --ledger my-ledger \ + --posting "world,treasury,1000,USD/2,GRANTS" + +# Mixed colored and uncolored postings in a single transaction +ledgerctl transactions create --ledger my-ledger \ + --posting "world,treasury,500,USD/2" \ + --posting "world,treasury,500,USD/2,OPS" ``` +**Inspecting colored balances:** + +`ledgerctl accounts get` lists one row per `(asset, color)` tuple. To sum +every colored bucket of the same asset into a single uncolored entry, pass +`?collapseColors=true` on the HTTP endpoint (the CLI surfaces the raw, +segregated view by design). + **Creating transactions with Numscript:** ```bash diff --git a/docs/technical/architecture/data-model/color-of-money.md b/docs/technical/architecture/data-model/color-of-money.md new file mode 100644 index 0000000000..7fd17c5f8e --- /dev/null +++ b/docs/technical/architecture/data-model/color-of-money.md @@ -0,0 +1,97 @@ +# Color of money + +Color is a per-posting segregation key. The ledger tracks balances per +`(account, asset, color)` triple. Two postings on the same `(account, asset)` +but different colors operate on strictly isolated balances. + +## Invariants + +1. **Color is immutable.** Once funds are emitted under a color, no operation + ever changes that color. To "convert" color X to color Y the operator must + compose two transactions: send `X` to a clearing account, then mint `Y` + from `@world` back to the original holder. + +2. **Empty color is a bucket.** `Color == ""` is the *uncolored* bucket and + participates in segregation just like any colored bucket. A posting with + `Color == ""` cannot pull funds from a colored bucket, and vice versa. + +3. **Color charset is `^[A-Z]*$`.** Enforced both by the upstream + numscript interpreter and at the ledger admission boundary. The empty + string remains valid (the uncolored bucket). + +4. **Double-entry holds per bucket.** For every `(asset, color)`, the sum of + all account balances is zero. Each bucket is its own conservation universe. + +## Wire shapes + +- `commonpb.Posting.color: string` — primary carrier of the field; rides on + every read and write path. +- `commonpb.AccountVolume{asset, color, volumes}` — `Account.volumes` is a + list sorted by `(asset, color)` ascending. Deterministic serialization is + required for stable JSON / snapshot tests. +- `commonpb.VolumeEntry{asset, color, volumes}` — same shape, used inside + `PostCommitVolumes.volumes_by_account[account].volumes`. +- `commonpb.AggregatedVolume.color: string` — set on every entry returned by + `AggregateVolumes`. By default one entry per `(asset, color)` tuple; + `collapse_colors` flag sums across colors. + +## Storage layout + +`domain.VolumeKey` carries `Color string`. Canonical bytes: + +``` +[ledgerID BE 4B] [account] \x00 [color] \x00 [asset_base] [precision 1B] +``` + +The color sits between account and asset so prefix scans behave naturally: + +- `[ledgerID][account]\x00` returns every `(color, asset)` for the account. +- `[ledgerID][account]\x00[color]\x00` returns every asset for that color. +- The trailing `precision` byte can be `0x00` (e.g. `"EUR"`) without + encoding ambiguity because nothing follows it. + +## Numscript contract + +The numscript interpreter (`github.com/formancehq/numscript`, branch +`feat/color-on-posting`, see PR +[formancehq/numscript#139](https://github.com/formancehq/numscript/pull/139)) +exposes color as a first-class property of the output `Posting`. The +in-memory shapes are: + +```go +type AssetColor struct { Asset string; Color string } +type BalanceQuery map[string][]AssetColor +type ColorBalance map[string]*big.Int // color -> amount +type AccountBalance map[string]ColorBalance // asset -> ColorBalance +type Balances map[string]AccountBalance // account -> AccountBalance +``` + +The Numscript syntax `source = @alice \ "GRANTS"` produces a posting with +`Color = "GRANTS"` and only draws from the matching bucket. The previous +`USD_GRANTS` asset-suffix encoding is gone — color is never folded into the +asset string anywhere in the contract. + +## Collapse modes + +For consumers that want a per-asset summary (totals ignoring color), the +read API exposes opt-in collapse flags: + +- `GET /{ledger}/accounts/{address}?collapseColors=true` — `Account.volumes` + returns one entry per asset with `color = ""` and amounts summed. +- `GET /{ledger}/volumes?collapseColors=true` — `AggregatedVolume` entries + collapse to one per `(asset, precision)` with `color = ""`. + +Collapse is always opt-in: the default keeps the segregation visible so +clients cannot accidentally aggregate across buckets that should stay +isolated. + +## What this does NOT do + +- **No color filtering on list queries.** This iteration does not surface a + native `WHERE color = "GRANTS"` filter on `ListAccounts` / `ListTransactions`. + Filter client-side, or rely on the generic `filterexpr` engine. +- **No re-coloring primitive.** Numscript does not (yet) expose a syntax to + mutate the color of funds in-place. The composition pattern (clearing + account + mint from `@world`) is the supported path. +- **No automatic propagation from account metadata to color.** Color is + carried by the posting, not derived from the account's metadata. diff --git a/docs/technical/contributing/api-comparison.md b/docs/technical/contributing/api-comparison.md index e5b659537a..1d35526f44 100644 --- a/docs/technical/contributing/api-comparison.md +++ b/docs/technical/contributing/api-comparison.md @@ -137,12 +137,36 @@ This document compares the POC's API with the original Formance ledger API and d **Numscript Experimental Features (available, require `#![feature(...)]` opt-in):** - ✅ Account interpolation (dynamic addresses like `@escrow:$order_id`) -- ✅ Asset colors (fund origin tracking) +- ✅ Asset colors — promoted to first-class posting field. Postings carry + `color: string` and balances are strictly segregated per + `(account, asset, color)`. The empty color is the uncolored bucket and + is itself segregated from every colored bucket. Color values match + `^[A-Z]*$` and are immutable once carried by funds. See "Color of money + semantics" below. - ✅ `get_amount()` / `get_asset()` functions - ✅ Mid-script function calls (balance queries during execution) - ✅ `oneof` selector (conditional routing) - ✅ `overdraft()` function (dynamic overdraft calculation) +**Color of money semantics (new in this POC):** +- `Posting.color` is exposed on every read/write path. Direct postings + accept the new field through the `--color` CLI flag and the + `source,destination,amount,asset[,color]` `--posting` syntax on + `ledgerctl transactions create`. +- `Account.volumes` is a deterministic sorted list of + `{asset, color, volumes}` entries. The HTTP query parameter + `?collapseColors=true` on `GET /{ledger}/accounts/{address}` sums every + colored bucket of the same asset into a single entry under `color: ""`. +- `AggregatedVolume.color` is set on every entry returned by + `GET /{ledger}/volumes`. The same `?collapseColors=true` flag collapses + the result to one entry per `(asset, precision)`. +- The double-entry invariant holds per `(asset, color)` bucket: each + segregated bucket is its own conservation universe. +- Numscript `source = @acc \ "RED"` produces a `Posting` with + `Color = "RED"` and only draws from the matching bucket. Spending more + than the bucket holds returns `ErrInsufficientFunds` even when other + colored or uncolored buckets have plenty. + See [Numscript Guide](./numscript.md) for complete documentation. ### 2. Transaction Revert diff --git a/go.mod b/go.mod index db777bdd30..1f68d0bab7 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.14 github.com/aws/aws-sdk-go-v2/service/s3 v1.98.0 github.com/bytedance/sonic v1.15.0 - github.com/formancehq/numscript v0.0.24 + github.com/formancehq/numscript v0.0.25-0.20260615131322-cbc11e844233 github.com/go-chi/chi/v5 v5.2.5 github.com/go-jose/go-jose/v4 v4.1.4 github.com/golang-jwt/jwt/v5 v5.3.1 @@ -160,7 +160,7 @@ require ( github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/getkin/kin-openapi v0.134.0 // indirect - github.com/getsentry/sentry-go v0.35.1 // indirect + github.com/getsentry/sentry-go v0.43.0 // indirect github.com/go-chi/chi v4.1.2+incompatible // indirect github.com/go-chi/render v1.0.3 // indirect github.com/go-faster/city v1.0.1 // indirect @@ -302,7 +302,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/crypto v0.52.0 // indirect - golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.55.0 // indirect golang.org/x/sys v0.45.0 // indirect diff --git a/go.sum b/go.sum index ba2220d74b..13787070a7 100644 --- a/go.sum +++ b/go.sum @@ -238,24 +238,22 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/formancehq/go-libs/v5 v5.3.0 h1:1+rXYjCNGZG0tQayESllEassrejzI/vc4Gz3p568tnU= github.com/formancehq/go-libs/v5 v5.3.0/go.mod h1:ms6tCGw1yqB4qtEbAuqPOQegWo4rU48vDobNkK7Ak6U= -github.com/formancehq/numscript v0.0.24 h1:YBiDZ9zLVxTZVhtQ+taRcb6q2jArAvznWMfoWRVYGT0= -github.com/formancehq/numscript v0.0.24/go.mod h1:hC/VY5Vg04F5QkgdPPc6z/YsS/vh8V1qVJVa1VWnYMA= +github.com/formancehq/numscript v0.0.25-0.20260615131322-cbc11e844233 h1:uRt0sq1nV+emELMBtKvgHhb0aERyioLl7v9rJwINhWw= +github.com/formancehq/numscript v0.0.25-0.20260615131322-cbc11e844233/go.mod h1:DUR23FgL3NVEYvjr8KHt5bGFDpcheydwbvs+iTGohAU= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/getkin/kin-openapi v0.134.0 h1:/L5+1+kfe6dXh8Ot/wqiTgUkjOIEJiC0bbYVziHB8rU= github.com/getkin/kin-openapi v0.134.0/go.mod h1:wK6ZLG/VgoETO9pcLJ/VmAtIcl/DNlMayNTb716EUxE= -github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ= -github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= +github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4= +github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0= github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9 h1:r5GgOLGbza2wVHRzK7aAj6lWZjfbAwiu/RDCVOKjRyM= github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= -github.com/gkampitakis/ciinfo v0.3.3 h1:28PgAHtW3wG7UCAKuCK+17rBib9iqtLjajuWsVLUPQY= -github.com/gkampitakis/ciinfo v0.3.3/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= -github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= -github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= -github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= -github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/gkampitakis/ciinfo v0.3.4 h1:5eBSibVuSMbb/H6Elc0IIEFbkzCJi3lm94n0+U7Z0KY= +github.com/gkampitakis/ciinfo v0.3.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-snaps v0.5.21 h1:SvhSFeZviQXwlT+dnGyAIATVehkhqRVW6qfQZhCZH+Y= +github.com/gkampitakis/go-snaps v0.5.21/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs= github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= @@ -293,8 +291,8 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -431,8 +429,8 @@ github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8S github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= github.com/mailru/easyjson v0.9.2/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= -github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= -github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg= +github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -770,8 +768,8 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58 golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= diff --git a/internal/adapter/grpc/client_bucket.go b/internal/adapter/grpc/client_bucket.go index 590d3ec2a9..f7e52f32ca 100644 --- a/internal/adapter/grpc/client_bucket.go +++ b/internal/adapter/grpc/client_bucket.go @@ -91,10 +91,11 @@ func (g *BucketGrpcClient) ListTransactions(ctx context.Context, ledgerName stri return NewUpstreamPeekCursor(stream), nil } -func (g *BucketGrpcClient) GetAccount(ctx context.Context, ledgerName string, address string) (*commonpb.Account, error) { +func (g *BucketGrpcClient) GetAccount(ctx context.Context, ledgerName string, address string, opts ctrl.GetAccountOptions) (*commonpb.Account, error) { return g.client.GetAccount(ctx, &servicepb.GetAccountRequest{ - Ledger: ledgerName, - Address: address, + Ledger: ledgerName, + Address: address, + CollapseColors: opts.CollapseColors, }) } @@ -378,6 +379,7 @@ func (g *BucketGrpcClient) AggregateVolumes(ctx context.Context, ledgerName stri Ledger: ledgerName, Filter: filter, UseMaxPrecision: opts.UseMaxPrecision, + CollapseColors: opts.CollapseColors, GroupByPrefixes: opts.GroupByPrefixes, }) } diff --git a/internal/adapter/grpc/client_bucket_test.go b/internal/adapter/grpc/client_bucket_test.go index dbe55c0b57..dbcce175f6 100644 --- a/internal/adapter/grpc/client_bucket_test.go +++ b/internal/adapter/grpc/client_bucket_test.go @@ -14,6 +14,7 @@ import ( "github.com/formancehq/go-libs/v5/pkg/authn/oidc" "github.com/formancehq/ledger/v3/internal/adapter/auth" + appctrl "github.com/formancehq/ledger/v3/internal/application/ctrl" "github.com/formancehq/ledger/v3/internal/proto/auditpb" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/servicepb" @@ -128,7 +129,7 @@ func TestGetAccount_Success(t *testing.T) { mock.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Return(expected, nil) client := NewLedgerGrpcClient(mock) - account, err := client.GetAccount(context.Background(), "ledger1", "user:001") + account, err := client.GetAccount(context.Background(), "ledger1", "user:001", appctrl.GetAccountOptions{}) require.NoError(t, err) require.Equal(t, "user:001", account.GetAddress()) } @@ -141,7 +142,7 @@ func TestGetAccount_ReturnsError(t *testing.T) { mock.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Return(nil, errors.New("unavailable")) client := NewLedgerGrpcClient(mock) - _, err := client.GetAccount(context.Background(), "ledger1", "user:001") + _, err := client.GetAccount(context.Background(), "ledger1", "user:001", appctrl.GetAccountOptions{}) require.Error(t, err) } diff --git a/internal/adapter/grpc/controller_generated_test.go b/internal/adapter/grpc/controller_generated_test.go index 78a40355df..25937fa0a1 100644 --- a/internal/adapter/grpc/controller_generated_test.go +++ b/internal/adapter/grpc/controller_generated_test.go @@ -11,6 +11,7 @@ import ( context "context" reflect "reflect" + ctrl "github.com/formancehq/ledger/v3/internal/application/ctrl" cursor "github.com/formancehq/ledger/v3/internal/pkg/cursor" auditpb "github.com/formancehq/ledger/v3/internal/proto/auditpb" commonpb "github.com/formancehq/ledger/v3/internal/proto/commonpb" @@ -278,18 +279,18 @@ func (c *MockControllerExecutePreparedQueryCall) DoAndReturn(f func(context.Cont } // GetAccount mocks base method. -func (m *MockController) GetAccount(ctx context.Context, ledgerName, address string) (*commonpb.Account, error) { +func (m *MockController) GetAccount(ctx context.Context, ledgerName, address string, opts ctrl.GetAccountOptions) (*commonpb.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccount", ctx, ledgerName, address) + ret := m.ctrl.Call(m, "GetAccount", ctx, ledgerName, address, opts) ret0, _ := ret[0].(*commonpb.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAccount indicates an expected call of GetAccount. -func (mr *MockControllerMockRecorder) GetAccount(ctx, ledgerName, address any) *MockControllerGetAccountCall { +func (mr *MockControllerMockRecorder) GetAccount(ctx, ledgerName, address, opts any) *MockControllerGetAccountCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockController)(nil).GetAccount), ctx, ledgerName, address) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockController)(nil).GetAccount), ctx, ledgerName, address, opts) return &MockControllerGetAccountCall{Call: call} } @@ -305,13 +306,13 @@ func (c *MockControllerGetAccountCall) Return(arg0 *commonpb.Account, arg1 error } // Do rewrite *gomock.Call.Do -func (c *MockControllerGetAccountCall) Do(f func(context.Context, string, string) (*commonpb.Account, error)) *MockControllerGetAccountCall { +func (c *MockControllerGetAccountCall) Do(f func(context.Context, string, string, ctrl.GetAccountOptions) (*commonpb.Account, error)) *MockControllerGetAccountCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockControllerGetAccountCall) DoAndReturn(f func(context.Context, string, string) (*commonpb.Account, error)) *MockControllerGetAccountCall { +func (c *MockControllerGetAccountCall) DoAndReturn(f func(context.Context, string, string, ctrl.GetAccountOptions) (*commonpb.Account, error)) *MockControllerGetAccountCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/adapter/grpc/server_bucket.go b/internal/adapter/grpc/server_bucket.go index 1b093391e9..54db6fb143 100644 --- a/internal/adapter/grpc/server_bucket.go +++ b/internal/adapter/grpc/server_bucket.go @@ -618,7 +618,9 @@ func (impl *BucketServiceServerImpl) GetAccount(ctx context.Context, req *servic } defer cleanup() - return c.GetAccount(ctx, req.GetLedger(), req.GetAddress()) + return c.GetAccount(ctx, req.GetLedger(), req.GetAddress(), ctrl.GetAccountOptions{ + CollapseColors: req.GetCollapseColors(), + }) } func (impl *BucketServiceServerImpl) ListAccounts(req *servicepb.ListAccountsRequest, stream servicepb.BucketService_ListAccountsServer) error { @@ -1497,6 +1499,7 @@ func (impl *BucketServiceServerImpl) AggregateVolumes(ctx context.Context, req * result, err := c.AggregateVolumes(profileCtx, req.GetLedger(), req.GetFilter(), query.AggregateOptions{ UseMaxPrecision: req.GetUseMaxPrecision(), GroupByPrefixes: req.GetGroupByPrefixes(), + CollapseColors: req.GetCollapseColors(), }) impl.emitProfile(ctx, profile) diff --git a/internal/adapter/http/backend_generated_test.go b/internal/adapter/http/backend_generated_test.go index 9e1897f5a2..b743ebbd4c 100644 --- a/internal/adapter/http/backend_generated_test.go +++ b/internal/adapter/http/backend_generated_test.go @@ -11,6 +11,7 @@ import ( context "context" reflect "reflect" + ctrl "github.com/formancehq/ledger/v3/internal/application/ctrl" cursor "github.com/formancehq/ledger/v3/internal/pkg/cursor" auditpb "github.com/formancehq/ledger/v3/internal/proto/auditpb" clusterpb "github.com/formancehq/ledger/v3/internal/proto/clusterpb" @@ -279,18 +280,18 @@ func (c *MockBackendExecutePreparedQueryCall) DoAndReturn(f func(context.Context } // GetAccount mocks base method. -func (m *MockBackend) GetAccount(ctx context.Context, ledgerName, address string) (*commonpb.Account, error) { +func (m *MockBackend) GetAccount(ctx context.Context, ledgerName, address string, opts ctrl.GetAccountOptions) (*commonpb.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccount", ctx, ledgerName, address) + ret := m.ctrl.Call(m, "GetAccount", ctx, ledgerName, address, opts) ret0, _ := ret[0].(*commonpb.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAccount indicates an expected call of GetAccount. -func (mr *MockBackendMockRecorder) GetAccount(ctx, ledgerName, address any) *MockBackendGetAccountCall { +func (mr *MockBackendMockRecorder) GetAccount(ctx, ledgerName, address, opts any) *MockBackendGetAccountCall { mr.mock.ctrl.T.Helper() - call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockBackend)(nil).GetAccount), ctx, ledgerName, address) + call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockBackend)(nil).GetAccount), ctx, ledgerName, address, opts) return &MockBackendGetAccountCall{Call: call} } @@ -306,13 +307,13 @@ func (c *MockBackendGetAccountCall) Return(arg0 *commonpb.Account, arg1 error) * } // Do rewrite *gomock.Call.Do -func (c *MockBackendGetAccountCall) Do(f func(context.Context, string, string) (*commonpb.Account, error)) *MockBackendGetAccountCall { +func (c *MockBackendGetAccountCall) Do(f func(context.Context, string, string, ctrl.GetAccountOptions) (*commonpb.Account, error)) *MockBackendGetAccountCall { c.Call = c.Call.Do(f) return c } // DoAndReturn rewrite *gomock.Call.DoAndReturn -func (c *MockBackendGetAccountCall) DoAndReturn(f func(context.Context, string, string) (*commonpb.Account, error)) *MockBackendGetAccountCall { +func (c *MockBackendGetAccountCall) DoAndReturn(f func(context.Context, string, string, ctrl.GetAccountOptions) (*commonpb.Account, error)) *MockBackendGetAccountCall { c.Call = c.Call.DoAndReturn(f) return c } diff --git a/internal/adapter/http/handlers_aggregate_volumes.go b/internal/adapter/http/handlers_aggregate_volumes.go index 1fb019bff8..f4c4f7b613 100644 --- a/internal/adapter/http/handlers_aggregate_volumes.go +++ b/internal/adapter/http/handlers_aggregate_volumes.go @@ -16,7 +16,11 @@ type aggregateVolumesResponseJSON struct { } type aggregatedVolumeJSON struct { - Asset string `json:"asset"` + Asset string `json:"asset"` + // Color is always emitted (even when empty) so clients can distinguish + // the uncolored bucket from an older response shape that didn't carry + // the color dimension at all. + Color string `json:"color"` Input string `json:"input"` Output string `json:"output"` Balance string `json:"balance"` @@ -34,6 +38,7 @@ func toAggregatedVolumeJSON(v *commonpb.AggregatedVolume) *aggregatedVolumeJSON return &aggregatedVolumeJSON{ Asset: v.GetAsset(), + Color: v.GetColor(), Input: input.String(), Output: output.String(), Balance: balance.String(), @@ -74,6 +79,7 @@ func (s *Server) handleAggregateVolumes(w http.ResponseWriter, r *http.Request) } useMaxPrecision := queryParamBool(r, "useMaxPrecision") + collapseColors := queryParamBool(r, "collapseColors") var groupByPrefixes []string if g := r.URL.Query().Get("groupByPrefixes"); g != "" { @@ -96,6 +102,7 @@ func (s *Server) handleAggregateVolumes(w http.ResponseWriter, r *http.Request) result, err := s.backend.AggregateVolumes(ctx, ledgerName, filter, query.AggregateOptions{ UseMaxPrecision: useMaxPrecision, + CollapseColors: collapseColors, GroupByPrefixes: groupByPrefixes, }) if err != nil { diff --git a/internal/adapter/http/handlers_aggregate_volumes_test.go b/internal/adapter/http/handlers_aggregate_volumes_test.go index 5d8c419aa1..da1d568d75 100644 --- a/internal/adapter/http/handlers_aggregate_volumes_test.go +++ b/internal/adapter/http/handlers_aggregate_volumes_test.go @@ -235,3 +235,40 @@ func TestHandleAggregateVolumes_FullRouteIntegration(t *testing.T) { require.Equal(t, http.StatusOK, w.Code) } + +// TestHandleAggregateVolumes_EmitsColorAlways pins the wire shape: the +// `color` field is present on every aggregate entry, including for the +// uncolored bucket (empty string). The OpenAPI contract documents +// color as first-class; an `omitempty` tag would drop the field exactly +// when color="" and break clients that expect it on every entry. +func TestHandleAggregateVolumes_EmitsColorAlways(t *testing.T) { + t.Parallel() + + backend := NewMockBackend(gomock.NewController(t)) + backend.EXPECT().AggregateVolumes(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(_ context.Context, _ string, _ *commonpb.QueryFilter, opts query.AggregateOptions) (*commonpb.AggregateResult, error) { + require.True(t, opts.CollapseColors, "?collapseColors=true must reach the backend") + + return &commonpb.AggregateResult{ + Volumes: []*commonpb.AggregatedVolume{ + { + Asset: "USD/2", + Color: "", // uncolored / collapsed bucket + Input: commonpb.NewUint256FromUint64(100), + Output: commonpb.NewUint256FromUint64(30), + }, + }, + }, nil + }) + + handler := NewHandler(logging.Testing(), backend, internalauth.AuthConfig{}) + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/my-ledger/volumes?collapseColors=true", nil) + + handler.ServeHTTP(w, r) + + require.Equal(t, http.StatusOK, w.Code) + require.Contains(t, w.Body.String(), `"color":""`, + `empty color must surface as "color":"" not be omitted by omitempty`) +} diff --git a/internal/adapter/http/handlers_analyze_transactions.go b/internal/adapter/http/handlers_analyze_transactions.go index f8de7f7442..53404ea197 100644 --- a/internal/adapter/http/handlers_analyze_transactions.go +++ b/internal/adapter/http/handlers_analyze_transactions.go @@ -29,6 +29,11 @@ type normalizedPostingJSON struct { SourcePattern string `json:"sourcePattern"` DestinationPattern string `json:"destinationPattern"` Asset string `json:"asset"` + // Color is always emitted (even when empty) so REST clients can + // distinguish patterns that differ only by color bucket. Two flows + // with the same (source, destination, asset) but different colors + // surface here as distinct rows. + Color string `json:"color"` } type temporalStatsJSON struct { @@ -95,6 +100,7 @@ func toFlowPatternJSON(fp *servicepb.FlowPattern) *flowPatternJSON { SourcePattern: p.GetSourcePattern(), DestinationPattern: p.GetDestinationPattern(), Asset: p.GetAsset(), + Color: p.GetColor(), }) } diff --git a/internal/adapter/http/handlers_coverage_test.go b/internal/adapter/http/handlers_coverage_test.go index 33bf73ff17..86d39a095f 100644 --- a/internal/adapter/http/handlers_coverage_test.go +++ b/internal/adapter/http/handlers_coverage_test.go @@ -15,6 +15,7 @@ import ( logging "github.com/formancehq/go-libs/v5/pkg/observe/log" internalauth "github.com/formancehq/ledger/v3/internal/adapter/auth" + "github.com/formancehq/ledger/v3/internal/application/ctrl" "github.com/formancehq/ledger/v3/internal/domain" "github.com/formancehq/ledger/v3/internal/pkg/cursor" "github.com/formancehq/ledger/v3/internal/pkg/version" @@ -499,8 +500,8 @@ func TestHandleGetAccount_GetAccountError(t *testing.T) { func(_ context.Context, _ string) (*commonpb.LedgerInfo, error) { return &commonpb.LedgerInfo{Name: "ledger1"}, nil }).AnyTimes() - backend.EXPECT().GetAccount(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, _ string, _ string) (*commonpb.Account, error) { + backend.EXPECT().GetAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, _ string, _ string, _ ctrl.GetAccountOptions) (*commonpb.Account, error) { return nil, errors.New("storage error") }).AnyTimes() srv := newTestServer(t, backend) diff --git a/internal/adapter/http/handlers_get_account.go b/internal/adapter/http/handlers_get_account.go index 9c63657e24..d6e6dafdfe 100644 --- a/internal/adapter/http/handlers_get_account.go +++ b/internal/adapter/http/handlers_get_account.go @@ -5,9 +5,13 @@ import ( "net/http" "github.com/go-chi/chi/v5" + + "github.com/formancehq/ledger/v3/internal/application/ctrl" ) // handleGetAccount handles GET /{ledgerName}/accounts/{address} to retrieve an account. +// The optional ?collapseColors=true query param sums every colored bucket of +// the same asset into a single entry with color="" in the response. func (s *Server) handleGetAccount(w http.ResponseWriter, r *http.Request) { ledgerName, ok := requireLedgerName(w, r) if !ok { @@ -29,7 +33,11 @@ func (s *Server) handleGetAccount(w http.ResponseWriter, r *http.Request) { return } - account, err := s.backend.GetAccount(r.Context(), ledgerName, address) + opts := ctrl.GetAccountOptions{ + CollapseColors: r.URL.Query().Get("collapseColors") == "true", + } + + account, err := s.backend.GetAccount(r.Context(), ledgerName, address, opts) if err != nil { s.logger.WithFields(map[string]any{ "ledger": ledgerName, diff --git a/internal/adapter/http/handlers_get_account_test.go b/internal/adapter/http/handlers_get_account_test.go index 89ba99a203..35e8be1b43 100644 --- a/internal/adapter/http/handlers_get_account_test.go +++ b/internal/adapter/http/handlers_get_account_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" + "github.com/formancehq/ledger/v3/internal/application/ctrl" "github.com/formancehq/ledger/v3/internal/proto/commonpb" ) @@ -20,8 +21,8 @@ func TestHandleGetAccount_Success(t *testing.T) { func(_ context.Context, _ string) (*commonpb.LedgerInfo, error) { return &commonpb.LedgerInfo{Name: "ledger1"}, nil }).AnyTimes() - backend.EXPECT().GetAccount(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( - func(_ context.Context, _ string, addr string) (*commonpb.Account, error) { + backend.EXPECT().GetAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, _ string, addr string, _ ctrl.GetAccountOptions) (*commonpb.Account, error) { return &commonpb.Account{Address: addr}, nil }).AnyTimes() srv := newTestServer(t, backend) @@ -37,6 +38,94 @@ func TestHandleGetAccount_Success(t *testing.T) { require.Equal(t, http.StatusOK, w.Code) } +// TestHandleGetAccount_PropagatesCollapseColors pins that the HTTP handler +// forwards `?collapseColors=true` through to the backend as +// GetAccountOptions.CollapseColors=true. A regression where the handler stops +// parsing or forwarding the field would fail the matcher on EXPECT().GetAccount. +func TestHandleGetAccount_PropagatesCollapseColors(t *testing.T) { + t.Parallel() + + backend := NewMockBackend(gomock.NewController(t)) + backend.EXPECT().GetLedgerByName(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, _ string) (*commonpb.LedgerInfo, error) { + return &commonpb.LedgerInfo{Name: "ledger1"}, nil + }).AnyTimes() + // The matcher rejects the call if opts.CollapseColors is not true. + backend.EXPECT().GetAccount(gomock.Any(), gomock.Any(), gomock.Any(), ctrl.GetAccountOptions{CollapseColors: true}). + DoAndReturn(func(_ context.Context, _ string, addr string, _ ctrl.GetAccountOptions) (*commonpb.Account, error) { + return &commonpb.Account{Address: addr}, nil + }) + srv := newTestServer(t, backend) + + w := httptest.NewRecorder() + r := newRequest(t, http.MethodGet, "/ledger1/accounts/alice?collapseColors=true", nil, map[string]string{ + "ledgerName": "ledger1", + "address": "alice", + }) + + srv.handleGetAccount(w, r) + + require.Equal(t, http.StatusOK, w.Code) +} + +// TestHandleGetAccount_VolumesInJSON pins that Account.MarshalJSON emits the +// Volumes array with color:"" for the uncolored bucket. Without this +// assertion a marshaller regression that drops the Volumes field (the +// previous behaviour) would slip through, since the handler test only +// looks at the status code. +func TestHandleGetAccount_VolumesInJSON(t *testing.T) { + t.Parallel() + + backend := NewMockBackend(gomock.NewController(t)) + backend.EXPECT().GetLedgerByName(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, _ string) (*commonpb.LedgerInfo, error) { + return &commonpb.LedgerInfo{Name: "ledger1"}, nil + }).AnyTimes() + backend.EXPECT().GetAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(_ context.Context, _ string, addr string, _ ctrl.GetAccountOptions) (*commonpb.Account, error) { + return &commonpb.Account{ + Address: addr, + Volumes: []*commonpb.AccountVolume{ + { + Asset: "USD/2", + Color: "", // uncolored bucket — must appear in JSON with color:"" + Volumes: &commonpb.VolumesWithBalance{ + Input: "100", + Output: "30", + Balance: "70", + }, + }, + { + Asset: "USD/2", + Color: "GRANTS", + Volumes: &commonpb.VolumesWithBalance{ + Input: "50", + Output: "0", + Balance: "50", + }, + }, + }, + }, nil + }).AnyTimes() + srv := newTestServer(t, backend) + + w := httptest.NewRecorder() + r := newRequest(t, http.MethodGet, "/ledger1/accounts/alice", nil, map[string]string{ + "ledgerName": "ledger1", + "address": "alice", + }) + + srv.handleGetAccount(w, r) + + require.Equal(t, http.StatusOK, w.Code) + body := w.Body.String() + require.Contains(t, body, `"volumes":[`, "Account JSON must include the volumes array") + require.Contains(t, body, `"asset":"USD/2"`) + require.Contains(t, body, `"color":""`, "the uncolored bucket must surface as color:\"\" not be omitted") + require.Contains(t, body, `"color":"GRANTS"`) + require.Contains(t, body, `"balance":"70"`) +} + func TestHandleGetAccount_MissingAddress(t *testing.T) { t.Parallel() diff --git a/internal/application/admission/admission.go b/internal/application/admission/admission.go index f155d3a05b..8d094560fc 100644 --- a/internal/application/admission/admission.go +++ b/internal/application/admission/admission.go @@ -775,17 +775,22 @@ func wrapSystemScoped(order *raftcmdpb.Order, ss *raftcmdpb.SystemScopedOrder) { order.Type = &raftcmdpb.Order_SystemScoped{SystemScoped: ss} } -// addVolumeNeed adds a volume key to the preload needs. Since EN-1378 a -// declared-but-absent volume key resolves to a `Declare` plan (pure -// coverage, no FSM-side cache mutation); the FSM-side `Scope.GetVolume` -// returns `domain.ErrNotFound` and callers treat it as a fresh zero -// balance (see `processing.readVolumeOrZero`). A `*state.ErrCoverageMiss` -// (admission contract violation — need never declared) stays distinct -// and propagates loud through `ErrStorageOperation{Cause: covErr}`. -func addVolumeNeed(p *plan.Needs, ledgerName string, account, asset string) { +// addVolumeNeed adds a (account, asset, color) volume key to the preload +// needs. The empty color is the uncolored bucket; colored postings must +// request their own bucket so the FSM doesn't error. +// +// Since EN-1378 a declared-but-absent volume key resolves to a `Declare` +// plan (pure coverage, no FSM-side cache mutation); the FSM-side +// `Scope.Volumes().Get` returns `domain.ErrNotFound` and callers treat it +// as a fresh zero balance (see `processing.readVolumeOrZero`). A +// `*state.ErrCoverageMiss` (admission contract violation — need never +// declared) stays distinct and propagates loud through +// `ErrStorageOperation{Cause: covErr}`. +func addVolumeNeed(p *plan.Needs, ledgerName, account, asset, color string) { p.Volumes[domain.VolumeKey{ AccountKey: domain.AccountKey{LedgerName: ledgerName, Account: account}, Asset: asset, + Color: color, }] = struct{}{} } @@ -825,9 +830,11 @@ func extractLedgerScopedNeeds(p *plan.Needs, ls *raftcmdpb.LedgerScopedOrder) { postings = rt.GetReversePostings() } + // Mirror ingestion only handles v2 logs, which have no color + // dimension — every mirrored posting lands in the uncolored bucket. for _, posting := range postings { - addVolumeNeed(p, ledgerName, posting.GetSource(), posting.GetAsset()) - addVolumeNeed(p, ledgerName, posting.GetDestination(), posting.GetAsset()) + addVolumeNeed(p, ledgerName, posting.GetSource(), posting.GetAsset(), "") + addVolumeNeed(p, ledgerName, posting.GetDestination(), posting.GetAsset(), "") } if ct := mi.GetEntry().GetCreatedTransaction(); ct != nil { @@ -946,8 +953,8 @@ func extractLedgerScopedNeeds(p *plan.Needs, ls *raftcmdpb.LedgerScopedOrder) { if !scriptBacked { for _, posting := range applyData.CreateTransaction.GetPostings() { - addVolumeNeed(p, ledgerName, posting.GetSource(), posting.GetAsset()) - addVolumeNeed(p, ledgerName, posting.GetDestination(), posting.GetAsset()) + addVolumeNeed(p, ledgerName, posting.GetSource(), posting.GetAsset(), posting.GetColor()) + addVolumeNeed(p, ledgerName, posting.GetDestination(), posting.GetAsset(), posting.GetColor()) } } @@ -958,8 +965,8 @@ func extractLedgerScopedNeeds(p *plan.Needs, ls *raftcmdpb.LedgerScopedOrder) { }] = struct{}{} for _, posting := range applyData.RevertTransaction.GetOriginalPostings() { - addVolumeNeed(p, ledgerName, posting.GetDestination(), posting.GetAsset()) - addVolumeNeed(p, ledgerName, posting.GetSource(), posting.GetAsset()) + addVolumeNeed(p, ledgerName, posting.GetDestination(), posting.GetAsset(), posting.GetColor()) + addVolumeNeed(p, ledgerName, posting.GetSource(), posting.GetAsset(), posting.GetColor()) } case *raftcmdpb.LedgerApplyOrder_AddMetadata: @@ -1177,13 +1184,13 @@ func (a *Admission) resolveScriptsAndEnrichNeeds(ctx context.Context, orders []* if discovered != nil { for key := range discovered.SourceVolumes { - addVolumeNeed(p, key.LedgerName, key.Account, key.Asset) - addVolumeNeed(orderNeeds, key.LedgerName, key.Account, key.Asset) + addVolumeNeed(p, key.LedgerName, key.Account, key.Asset, key.Color) + addVolumeNeed(orderNeeds, key.LedgerName, key.Account, key.Asset, key.Color) } for key := range discovered.DestinationVolumes { - addVolumeNeed(p, key.LedgerName, key.Account, key.Asset) - addVolumeNeed(orderNeeds, key.LedgerName, key.Account, key.Asset) + addVolumeNeed(p, key.LedgerName, key.Account, key.Asset, key.Color) + addVolumeNeed(orderNeeds, key.LedgerName, key.Account, key.Asset, key.Color) } for key := range discovered.WrittenMetadata { diff --git a/internal/application/admission/validate_order.go b/internal/application/admission/validate_order.go index 687cbf617f..d36c5937b0 100644 --- a/internal/application/admission/validate_order.go +++ b/internal/application/admission/validate_order.go @@ -30,6 +30,47 @@ func validateOrder(order *raftcmdpb.Order) error { return &domain.BusinessError{Err: err} } + if err := validateOrderPostingColors(order); err != nil { + return &domain.BusinessError{Err: err} + } + + return nil +} + +// validateOrderPostingColors validates Color on every direct Posting attached +// to a CreateTransaction or RevertTransaction order, BEFORE admission extracts +// preload needs from those postings. +// +// Color flows directly from the request into the volume-key tuple admission +// uses for preload extraction, so a malformed value (e.g. `Color="A\x00B"`) +// would otherwise materialize a corrupted cache key before the FSM's own +// ValidateColor rejected the order. Validating here closes that window. +// +// Postings produced by Numscript still get their second-pass validation in +// the FSM (`validatePostings` in processor_transaction.go); the FSM stays the +// audit-trail enforcement layer for script-resolved postings. This admission +// check only covers the direct-postings shape that bypasses the producer. +func validateOrderPostingColors(order *raftcmdpb.Order) domain.Describable { + apply, ok := order.GetLedgerScoped().GetPayload().(*raftcmdpb.LedgerScopedOrder_Apply) + if !ok { + return nil + } + + switch d := apply.Apply.GetData().(type) { + case *raftcmdpb.LedgerApplyOrder_CreateTransaction: + for _, p := range d.CreateTransaction.GetPostings() { + if err := domain.ValidateColor(p.GetColor()); err != nil { + return err + } + } + case *raftcmdpb.LedgerApplyOrder_RevertTransaction: + for _, p := range d.RevertTransaction.GetOriginalPostings() { + if err := domain.ValidateColor(p.GetColor()); err != nil { + return err + } + } + } + return nil } diff --git a/internal/application/admission/validate_order_test.go b/internal/application/admission/validate_order_test.go index 0924281f2c..5613788002 100644 --- a/internal/application/admission/validate_order_test.go +++ b/internal/application/admission/validate_order_test.go @@ -1,6 +1,7 @@ package admission import ( + "math/big" "testing" "github.com/stretchr/testify/require" @@ -802,3 +803,81 @@ func TestValidateOrderContent(t *testing.T) { }) } } + +// TestValidateOrder_RejectsInvalidPostingColor guards the admission-side color +// validation that runs BEFORE preload extraction. A malformed Color value +// (lowercase, NUL byte, oversize) would otherwise reach addVolumeNeed and +// materialize a corrupted cache key before the FSM rejected the order. +func TestValidateOrder_RejectsInvalidPostingColor(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + color string + wantErr bool + }{ + {name: "empty color (uncolored bucket)", color: "", wantErr: false}, + {name: "valid uppercase color", color: "GRANTS", wantErr: false}, + {name: "lowercase rejected", color: "grants", wantErr: true}, + {name: "embedded NUL rejected", color: "A\x00B", wantErr: true}, + {name: "mixed-case rejected", color: "Grants", wantErr: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + order := &raftcmdpb.Order{ + Type: &raftcmdpb.Order_LedgerScoped{ + LedgerScoped: &raftcmdpb.LedgerScopedOrder{ + Ledger: "default", + Payload: &raftcmdpb.LedgerScopedOrder_Apply{ + Apply: &raftcmdpb.LedgerApplyOrder{ + Data: &raftcmdpb.LedgerApplyOrder_CreateTransaction{ + CreateTransaction: &raftcmdpb.CreateTransactionOrder{ + Postings: []*commonpb.Posting{ + commonpb.NewColoredPosting("world", "users:alice", "USD/2", tt.color, big.NewInt(100)), + }, + }, + }, + }, + }, + }, + }, + } + + err := validateOrder(order) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateOrder_RejectsInvalidColorInRevert(t *testing.T) { + t.Parallel() + + order := &raftcmdpb.Order{ + Type: &raftcmdpb.Order_LedgerScoped{ + LedgerScoped: &raftcmdpb.LedgerScopedOrder{ + Ledger: "default", + Payload: &raftcmdpb.LedgerScopedOrder_Apply{ + Apply: &raftcmdpb.LedgerApplyOrder{ + Data: &raftcmdpb.LedgerApplyOrder_RevertTransaction{ + RevertTransaction: &raftcmdpb.RevertTransactionOrder{ + TransactionId: 42, + OriginalPostings: []*commonpb.Posting{ + commonpb.NewColoredPosting("users:alice", "world", "USD/2", "lowercase", big.NewInt(100)), + }, + }, + }, + }, + }, + }, + }, + } + + require.Error(t, validateOrder(order)) +} diff --git a/internal/application/check/checker.go b/internal/application/check/checker.go index 4aa5a08ec6..c64a0fe3c4 100644 --- a/internal/application/check/checker.go +++ b/internal/application/check/checker.go @@ -193,13 +193,13 @@ func (c *Checker) Check(ctx context.Context, callback func(*servicepb.CheckStore // replay consumes — a tampered AppliedProposal.TransientVolumes or // LedgerLog.PurgedVolumes record cannot influence the integrity check. excluded := excludedVolumesSet{} - exclusionCollector := func(ledger, account, asset string) { + exclusionCollector := func(ledger, account, asset, color string) { set, exists := excluded[ledger] if !exists { set = make(map[domain.AccountAssetKey]struct{}) excluded[ledger] = set } - set[domain.AccountAssetKey{Account: account, Asset: asset}] = struct{}{} + set[domain.AccountAssetKey{Account: account, Asset: asset, Color: color}] = struct{}{} } // stored mirrors `excluded` but is built from the Pebble projections @@ -211,13 +211,13 @@ func (c *Checker) Check(ctx context.Context, callback func(*servicepb.CheckStore // indirectly relies on, so a tampered cache cannot make a corrupted // state look consistent. stored := excludedVolumesSet{} - addStored := func(ledger, account, asset string) { + addStored := func(ledger, account, asset, color string) { set, exists := stored[ledger] if !exists { set = make(map[domain.AccountAssetKey]struct{}) stored[ledger] = set } - set[domain.AccountAssetKey{Account: account, Asset: asset}] = struct{}{} + set[domain.AccountAssetKey{Account: account, Asset: asset, Color: color}] = struct{}{} } nextProposalEnd, hasProposalEnd, err := proposalBoundaries.Next() @@ -436,7 +436,7 @@ func (c *Checker) Check(ctx context.Context, callback func(*servicepb.CheckStore // AppliedProposal.TransientVolumes is added in a // single pass below. for _, v := range payload.Apply.GetLog().GetPurgedVolumes() { - addStored(ledgerName, v.GetAccount(), v.GetAsset()) + addStored(ledgerName, v.GetAccount(), v.GetAsset(), v.GetColor()) } } } @@ -553,14 +553,14 @@ func (c *Checker) Check(ctx context.Context, callback func(*servicepb.CheckStore } // collectStoredTransientVolumes walks the AppliedProposal stream and feeds -// every (ledger, account, asset) declared in TransientVolumes into the -// addStored callback. Paired with the LedgerLog.PurgedVolumes captured +// every (ledger, account, asset, color) declared in TransientVolumes into +// the addStored callback. Paired with the LedgerLog.PurgedVolumes captured // during the replay loop, this builds the "stored" projection the checker // compares against the audit-derived ground truth. func (c *Checker) collectStoredTransientVolumes( ctx context.Context, reader dal.PebbleReader, - addStored func(ledger, account, asset string), + addStored func(ledger, account, asset, color string), ) error { proposals, err := query.ReadAppliedProposals(ctx, reader, nil) if err != nil { @@ -581,7 +581,7 @@ func (c *Checker) collectStoredTransientVolumes( for ledgerName, volumeList := range entry.GetTransientVolumes() { for _, v := range volumeList.GetVolumes() { - addStored(ledgerName, v.GetAccount(), v.GetAsset()) + addStored(ledgerName, v.GetAccount(), v.GetAsset(), v.GetColor()) } } } @@ -824,7 +824,7 @@ func (c *Checker) compareIndexes( // index builder and cannot be trusted by the integrity checker. type excludedVolumesSet map[string]map[domain.AccountAssetKey]struct{} -func (e excludedVolumesSet) contains(ledgerName, account, asset string) bool { +func (e excludedVolumesSet) contains(ledgerName, account, asset, color string) bool { if e == nil { return false } @@ -834,7 +834,7 @@ func (e excludedVolumesSet) contains(ledgerName, account, asset string) bool { return false } - _, has := keys[domain.AccountAssetKey{Account: account, Asset: asset}] + _, has := keys[domain.AccountAssetKey{Account: account, Asset: asset, Color: color}] return has } @@ -1023,12 +1023,13 @@ func (c *Checker) compareVolumes(ctx context.Context, reader dal.PebbleReader, b // Check). That set is derived from the hash-chain-bound audit // trail — NOT from AppliedProposal.TransientVolumes or // LedgerLog.PurgedVolumes, which are unhashed caches and must - // stay untrusted here. The exclusion key is (account, asset) so a - // multi-asset account whose USD was purged still has its EUR - // compared. Do not "align" this code to consult those proto - // records — it would reintroduce the tampering vector this - // design deliberately removes. - if excluded.contains(vk.LedgerName, vk.Account, vk.Asset) { + // stay untrusted here. The exclusion key is + // (account, asset, color) so a multi-bucket account whose + // (USD, RED) was purged still has its (USD, BLUE) compared. Do + // not "align" this code to consult those proto records — it + // would reintroduce the tampering vector this design + // deliberately removes. + if excluded.contains(vk.LedgerName, vk.Account, vk.Asset, vk.Color) { continue } diff --git a/internal/application/ctrl/controller.go b/internal/application/ctrl/controller.go index 955e860129..c4deaf7525 100644 --- a/internal/application/ctrl/controller.go +++ b/internal/application/ctrl/controller.go @@ -10,6 +10,14 @@ import ( "github.com/formancehq/ledger/v3/internal/query" ) +// GetAccountOptions configures a GetAccount read. +type GetAccountOptions struct { + // CollapseColors sums every colored bucket of the same asset into a + // single entry with Color = "" in the returned Account.volumes list. + // When false (default), each (asset, color) tuple gets its own entry. + CollapseColors bool +} + //go:generate mockgen -write_source_comment=false -write_package_comment=false -source controller.go -destination controller_generated_test.go -package ctrl . Controller //go:generate mockgen -write_source_comment=false -write_package_comment=false -source controller.go -destination ctrlmock/controller_generated.go -package ctrlmock . Controller type Controller interface { @@ -20,15 +28,17 @@ type Controller interface { // Read operations GetTransaction(ctx context.Context, ledgerName string, transactionID uint64) (*commonpb.Transaction, error) ListTransactions(ctx context.Context, ledgerName string, pageSize uint32, afterTxID uint64, filter *commonpb.QueryFilter, reverse bool) (cursor.Cursor[*commonpb.Transaction], error) - GetAccount(ctx context.Context, ledgerName string, address string) (*commonpb.Account, error) + GetAccount(ctx context.Context, ledgerName string, address string, opts GetAccountOptions) (*commonpb.Account, error) ListAccounts(ctx context.Context, ledgerName string, pageSize uint32, afterAddress string, filter *commonpb.QueryFilter, reverse bool) (cursor.Cursor[*commonpb.Account], error) // Stats operations GetLedgerStats(ctx context.Context, ledgerName string) (*commonpb.LedgerStats, error) // Log operations - // ListLogs returns logs for a specific ledger, ordered by ledger-local log ID. - // Use a LogIdCondition in the filter for pagination. + // ListLogs returns logs for a specific ledger, ordered by ledger-local log + // ID. afterSequence is the ledger-local log ID to start after; the filter + // may add further conditions (e.g. date ranges). Use a LogIdCondition in + // the filter for pagination. ListLogs(ctx context.Context, ledgerName string, afterSequence uint64, pageSize uint32, filter *commonpb.QueryFilter) (cursor.Cursor[*commonpb.Log], error) GetLog(ctx context.Context, sequence uint64) (*commonpb.Log, error) diff --git a/internal/application/ctrl/controller_default.go b/internal/application/ctrl/controller_default.go index fb4de00393..a42c3efa9c 100644 --- a/internal/application/ctrl/controller_default.go +++ b/internal/application/ctrl/controller_default.go @@ -458,7 +458,7 @@ func (ctrl *DefaultController) ListAccounts(ctx context.Context, ledgerName stri return cursor.NewClosingCursor(cursor.NewSliceCursor(accounts), handle), nil } -func (ctrl *DefaultController) GetAccount(ctx context.Context, ledgerName string, address string) (*commonpb.Account, error) { +func (ctrl *DefaultController) GetAccount(ctx context.Context, ledgerName string, address string, opts GetAccountOptions) (*commonpb.Account, error) { _, span := tracer.Start(ctx, "ctrl.get_account", trace.WithAttributes( attribute.String("ledger", ledgerName), @@ -484,7 +484,7 @@ func (ctrl *DefaultController) GetAccount(ctx context.Context, ledgerName string defer func() { _ = handle.Close() }() - return scanAccount(handle, ctrl.attrs, ledgerInfo.GetName(), address, ctrl.logger) + return scanAccount(handle, ctrl.attrs, ledgerInfo.GetName(), address, opts.CollapseColors, ctrl.logger) } // GetLedgerStats returns aggregate statistics for a ledger. @@ -1252,7 +1252,11 @@ func (ctrl *DefaultController) ListPreparedQueries(ctx context.Context, ledger s func (ctrl *DefaultController) entityEnricher() *query.EntityEnricher { return &query.EntityEnricher{ EnrichAccount: func(reader dal.PebbleReader, ledgerName string, address string) (*commonpb.Account, error) { - return scanAccount(reader, ctrl.attrs, ledgerName, address, ctrl.logger) + // List-style enrichment paths do not surface a per-call collapse + // flag; entries are returned color-segregated and the caller can + // collapse client-side if needed. Per-account GetAccount honors + // the flag through the GetAccountOptions path. + return scanAccount(reader, ctrl.attrs, ledgerName, address, false, ctrl.logger) }, EnrichTransaction: func(ctx context.Context, reader dal.PebbleReader, ledgerName string, txID uint64) (*commonpb.Transaction, error) { return ctrl.buildTransaction(ctx, reader, ledgerName, txID) diff --git a/internal/application/ctrl/controller_generated_test.go b/internal/application/ctrl/controller_generated_test.go index 6b2177df84..fc527b79a8 100644 --- a/internal/application/ctrl/controller_generated_test.go +++ b/internal/application/ctrl/controller_generated_test.go @@ -134,18 +134,18 @@ func (mr *MockControllerMockRecorder) ExecutePreparedQuery(ctx, req any) *gomock } // GetAccount mocks base method. -func (m *MockController) GetAccount(ctx context.Context, ledgerName, address string) (*commonpb.Account, error) { +func (m *MockController) GetAccount(ctx context.Context, ledgerName, address string, opts GetAccountOptions) (*commonpb.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccount", ctx, ledgerName, address) + ret := m.ctrl.Call(m, "GetAccount", ctx, ledgerName, address, opts) ret0, _ := ret[0].(*commonpb.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAccount indicates an expected call of GetAccount. -func (mr *MockControllerMockRecorder) GetAccount(ctx, ledgerName, address any) *gomock.Call { +func (mr *MockControllerMockRecorder) GetAccount(ctx, ledgerName, address, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockController)(nil).GetAccount), ctx, ledgerName, address) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockController)(nil).GetAccount), ctx, ledgerName, address, opts) } // GetAuditEntry mocks base method. diff --git a/internal/application/ctrl/ctrlmock/controller_generated.go b/internal/application/ctrl/ctrlmock/controller_generated.go index f9accf4e8b..599d135abb 100644 --- a/internal/application/ctrl/ctrlmock/controller_generated.go +++ b/internal/application/ctrl/ctrlmock/controller_generated.go @@ -11,6 +11,7 @@ import ( context "context" reflect "reflect" + ctrl "github.com/formancehq/ledger/v3/internal/application/ctrl" cursor "github.com/formancehq/ledger/v3/internal/pkg/cursor" auditpb "github.com/formancehq/ledger/v3/internal/proto/auditpb" commonpb "github.com/formancehq/ledger/v3/internal/proto/commonpb" @@ -134,18 +135,18 @@ func (mr *MockControllerMockRecorder) ExecutePreparedQuery(ctx, req any) *gomock } // GetAccount mocks base method. -func (m *MockController) GetAccount(ctx context.Context, ledgerName, address string) (*commonpb.Account, error) { +func (m *MockController) GetAccount(ctx context.Context, ledgerName, address string, opts ctrl.GetAccountOptions) (*commonpb.Account, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAccount", ctx, ledgerName, address) + ret := m.ctrl.Call(m, "GetAccount", ctx, ledgerName, address, opts) ret0, _ := ret[0].(*commonpb.Account) ret1, _ := ret[1].(error) return ret0, ret1 } // GetAccount indicates an expected call of GetAccount. -func (mr *MockControllerMockRecorder) GetAccount(ctx, ledgerName, address any) *gomock.Call { +func (mr *MockControllerMockRecorder) GetAccount(ctx, ledgerName, address, opts any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockController)(nil).GetAccount), ctx, ledgerName, address) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAccount", reflect.TypeOf((*MockController)(nil).GetAccount), ctx, ledgerName, address, opts) } // GetAuditEntry mocks base method. diff --git a/internal/application/ctrl/store.go b/internal/application/ctrl/store.go index 3c10acfa50..88e2d68000 100644 --- a/internal/application/ctrl/store.go +++ b/internal/application/ctrl/store.go @@ -3,6 +3,7 @@ package ctrl import ( "fmt" "math/big" + "sort" logging "github.com/formancehq/go-libs/v5/pkg/observe/log" @@ -14,10 +15,13 @@ import ( ) // assembleAccount builds a commonpb.Account from flushed volume and metadata accumulator entries. +// When collapseColors is true, all colored buckets of the same asset are summed +// into a single entry with Color = "" in the returned Account.volumes list. func assembleAccount( address string, volEntries []attributes.ComputedEntry[*raftcmdpb.VolumePair], metaEntries []attributes.ComputedEntry[*commonpb.MetadataValue], + collapseColors bool, ) *commonpb.Account { account := &commonpb.Account{ Address: address, @@ -25,36 +29,7 @@ func assembleAccount( } if len(volEntries) > 0 { - volumes := make(map[string]*commonpb.VolumesWithBalance, len(volEntries)) - for _, entry := range volEntries { - var vk domain.VolumeKey - - err := vk.Unmarshal(entry.CanonicalKey) - if err != nil { - continue - } - - input := big.NewInt(0) - output := big.NewInt(0) - - if entry.Value != nil { - if entry.Value.GetInput() != nil { - input = entry.Value.GetInput().ToBigInt() - } - - if entry.Value.GetOutput() != nil { - output = entry.Value.GetOutput().ToBigInt() - } - } - - volumes[vk.Asset] = &commonpb.VolumesWithBalance{ - Input: input.String(), - Output: output.String(), - Balance: new(big.Int).Sub(input, output).String(), - } - } - - account.Volumes = volumes + account.Volumes = buildAccountVolumes(volEntries, collapseColors) } if len(metaEntries) > 0 { @@ -76,6 +51,79 @@ func assembleAccount( return account } +// buildAccountVolumes turns the flushed volume entries into the +// `repeated AccountVolume` list carried by Account.volumes. The list is +// sorted by (asset, color) ascending. If collapseColors is true, entries +// with the same asset (different colors) are summed under color = "". +func buildAccountVolumes(volEntries []attributes.ComputedEntry[*raftcmdpb.VolumePair], collapseColors bool) []*commonpb.AccountVolume { + type key struct { + asset string + color string + } + + totals := make(map[key]*commonpb.AccountVolume, len(volEntries)) + + for _, entry := range volEntries { + var vk domain.VolumeKey + if err := vk.Unmarshal(entry.CanonicalKey); err != nil { + continue + } + + input := big.NewInt(0) + output := big.NewInt(0) + if entry.Value != nil { + if entry.Value.GetInput() != nil { + input = entry.Value.GetInput().ToBigInt() + } + if entry.Value.GetOutput() != nil { + output = entry.Value.GetOutput().ToBigInt() + } + } + + bucketColor := vk.Color + if collapseColors { + bucketColor = "" + } + + k := key{asset: vk.Asset, color: bucketColor} + acc, ok := totals[k] + if !ok { + acc = &commonpb.AccountVolume{ + Asset: vk.Asset, + Color: bucketColor, + Volumes: &commonpb.VolumesWithBalance{ + Input: big.NewInt(0).String(), + Output: big.NewInt(0).String(), + }, + } + totals[k] = acc + } + + currentIn, _ := new(big.Int).SetString(acc.GetVolumes().GetInput(), 10) + currentOut, _ := new(big.Int).SetString(acc.GetVolumes().GetOutput(), 10) + acc.Volumes.Input = currentIn.Add(currentIn, input).String() + acc.Volumes.Output = currentOut.Add(currentOut, output).String() + } + + out := make([]*commonpb.AccountVolume, 0, len(totals)) + for _, v := range totals { + // Compute balance once at the end (after potential color collapse). + input, _ := new(big.Int).SetString(v.GetVolumes().GetInput(), 10) + output, _ := new(big.Int).SetString(v.GetVolumes().GetOutput(), 10) + v.Volumes.Balance = new(big.Int).Sub(input, output).String() + out = append(out, v) + } + sort.Slice(out, func(i, j int) bool { + if a, b := out[i].GetAsset(), out[j].GetAsset(); a != b { + return a < b + } + + return out[i].GetColor() < out[j].GetColor() + }) + + return out +} + // scanAccount performs two forward scans — one for Volume and one for Metadata — // and returns an assembled Account. With the type-prefixed key layout, V and M // entries are in separate Pebble ranges. @@ -87,6 +135,7 @@ func scanAccount( attrs *attributes.Attributes, ledgerName string, address string, + collapseColors bool, diagLogger ...logging.Logger, ) (*commonpb.Account, error) { var logger logging.Logger @@ -126,5 +175,5 @@ func scanAccount( }).Infof("scanAccount complete") } - return assembleAccount(address, volEntries, metaEntries), nil + return assembleAccount(address, volEntries, metaEntries, collapseColors), nil } diff --git a/internal/application/ctrl/store_color_test.go b/internal/application/ctrl/store_color_test.go new file mode 100644 index 0000000000..1ede1b0bd6 --- /dev/null +++ b/internal/application/ctrl/store_color_test.go @@ -0,0 +1,101 @@ +package ctrl + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/formancehq/ledger/v3/internal/domain" + "github.com/formancehq/ledger/v3/internal/infra/attributes" + "github.com/formancehq/ledger/v3/internal/proto/commonpb" + "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" +) + +// volEntry is a small helper to build an attributes.ComputedEntry against +// a (account, asset, color) tuple with concrete input/output amounts. +func volEntry(t *testing.T, ledgerName string, account, asset, color string, in, out int64) attributes.ComputedEntry[*raftcmdpb.VolumePair] { + t.Helper() + vk := domain.NewVolumeKey(ledgerName, account, asset, color) + + return attributes.ComputedEntry[*raftcmdpb.VolumePair]{ + CanonicalKey: vk.Bytes(), + Value: &raftcmdpb.VolumePair{ + Input: commonpb.NewUint256FromUint64(uint64(in)), + Output: commonpb.NewUint256FromUint64(uint64(out)), + }, + } +} + +// Default mode (collapseColors=false) keeps every (asset, color) bucket as +// its own entry, sorted by (asset, color) ascending. +func TestAssembleAccount_SegregatesColorsByDefault(t *testing.T) { + t.Parallel() + + entries := []attributes.ComputedEntry[*raftcmdpb.VolumePair]{ + volEntry(t, "test", "alice", "USD/2", "OPS", 25, 0), + volEntry(t, "test", "alice", "USD/2", "", 100, 0), + volEntry(t, "test", "alice", "USD/2", "GRANTS", 50, 0), + volEntry(t, "test", "alice", "EUR/2", "", 10, 0), + } + + acct := assembleAccount("alice", entries, nil, false) + + // Order: (EUR/2,""), (USD/2,""), (USD/2,"GRANTS"), (USD/2,"OPS") + got := acct.GetVolumes() + require.Len(t, got, 4) + + require.Equal(t, "EUR/2", got[0].GetAsset()) + require.Equal(t, "", got[0].GetColor()) + + require.Equal(t, "USD/2", got[1].GetAsset()) + require.Equal(t, "", got[1].GetColor()) + require.Equal(t, "100", got[1].GetVolumes().GetInput()) + require.Equal(t, "100", got[1].GetVolumes().GetBalance()) + + require.Equal(t, "USD/2", got[2].GetAsset()) + require.Equal(t, "GRANTS", got[2].GetColor()) + require.Equal(t, "50", got[2].GetVolumes().GetBalance()) + + require.Equal(t, "USD/2", got[3].GetAsset()) + require.Equal(t, "OPS", got[3].GetColor()) + require.Equal(t, "25", got[3].GetVolumes().GetBalance()) +} + +// Collapse mode sums every (asset, *) bucket into a single entry with +// color = "" and amounts summed. +func TestAssembleAccount_CollapseColors(t *testing.T) { + t.Parallel() + + entries := []attributes.ComputedEntry[*raftcmdpb.VolumePair]{ + volEntry(t, "test", "alice", "USD/2", "", 100, 0), + volEntry(t, "test", "alice", "USD/2", "GRANTS", 50, 10), + volEntry(t, "test", "alice", "USD/2", "OPS", 25, 5), + } + + acct := assembleAccount("alice", entries, nil, true) + + require.Len(t, acct.GetVolumes(), 1) + entry := acct.GetVolumes()[0] + require.Equal(t, "USD/2", entry.GetAsset()) + require.Equal(t, "", entry.GetColor(), "collapsed entries are produced under the empty color") + require.Equal(t, "175", entry.GetVolumes().GetInput()) // 100 + 50 + 25 + require.Equal(t, "15", entry.GetVolumes().GetOutput()) // 0 + 10 + 5 + require.Equal(t, "160", entry.GetVolumes().GetBalance()) // 175 - 15 +} + +// FindVolume helper round-trip: drilling into the returned Account by +// (asset, color) must return the correct entry both colored and uncolored. +func TestAssembleAccount_FindVolume(t *testing.T) { + t.Parallel() + + entries := []attributes.ComputedEntry[*raftcmdpb.VolumePair]{ + volEntry(t, "test", "alice", "USD/2", "", 100, 0), + volEntry(t, "test", "alice", "USD/2", "GRANTS", 50, 0), + } + acct := assembleAccount("alice", entries, nil, false) + + require.Equal(t, "100", acct.FindVolume("USD/2", "").GetBalance()) + require.Equal(t, "50", acct.FindVolume("USD/2", "GRANTS").GetBalance()) + require.Nil(t, acct.FindVolume("USD/2", "MISSING")) + require.Nil(t, acct.FindVolume("EUR/2", "")) +} diff --git a/internal/application/events/clickhouse_data_test.go b/internal/application/events/clickhouse_data_test.go index daa58eff4b..0462161b8a 100644 --- a/internal/application/events/clickhouse_data_test.go +++ b/internal/application/events/clickhouse_data_test.go @@ -639,6 +639,66 @@ func TestSinkConvertTransaction_Nil(t *testing.T) { require.Nil(t, result) } +// TestSinkConvertTransaction_PreservesColor pins that the color dimension +// reaches every analytical sink (ClickHouse/Databricks/Kafka/NATS share this +// converter). Without this guarantee, downstream warehouses cannot reconstruct +// the segregated balance history from emitted events. +func TestSinkConvertTransaction_PreservesColor(t *testing.T) { + t.Parallel() + + tx := &commonpb.Transaction{ + Id: 1, + Postings: []*commonpb.Posting{ + { + Source: "world", + Destination: "alice", + Asset: "USD/2", + Color: "GRANTS", + Amount: commonpb.NewUint256FromUint64(100), + }, + }, + Timestamp: &commonpb.Timestamp{Data: 1700000000}, + InsertedAt: &commonpb.Timestamp{Data: 1700000000}, + } + + result := sinkConvertTransaction(tx) + require.NotNil(t, result) + require.Len(t, result.Postings, 1) + require.Equal(t, "GRANTS", result.Postings[0].Color, + "color must flow through to the analytical-sink payload") +} + +// TestSinkPosting_AlwaysEmitsColor pins the analytical-sink JSON contract: +// the uncolored bucket must serialize as `color:""` (not be omitted), so +// downstream warehouses can distinguish a NULL color from a pre-color schema +// row. Mirrors the contract enforced by commonpb.Posting.MarshalJSON. +func TestSinkPosting_AlwaysEmitsColor(t *testing.T) { + t.Parallel() + + tx := &commonpb.Transaction{ + Id: 1, + Postings: []*commonpb.Posting{ + { + Source: "world", + Destination: "alice", + Asset: "USD/2", + Amount: commonpb.NewUint256FromUint64(100), + // Color intentionally left empty — uncolored bucket. + }, + }, + Timestamp: &commonpb.Timestamp{Data: 1700000000}, + InsertedAt: &commonpb.Timestamp{Data: 1700000000}, + } + + sink := sinkConvertTransaction(tx) + require.NotNil(t, sink) + + data, err := json.Marshal(sink.Postings[0]) + require.NoError(t, err) + require.Contains(t, string(data), `"color":""`, + "uncolored postings must surface color:\"\" in sink JSON, not be omitted") +} + func TestClickHouseCreateTableDDL(t *testing.T) { t.Parallel() diff --git a/internal/application/events/sink_data_common.go b/internal/application/events/sink_data_common.go index 5570aba0ed..b3ac4f5f84 100644 --- a/internal/application/events/sink_data_common.go +++ b/internal/application/events/sink_data_common.go @@ -70,6 +70,11 @@ type sinkPosting struct { Destination string `json:"destination"` Amount *big.Int `json:"amount"` Asset string `json:"asset"` + // Color is always emitted (no `omitempty`) so downstream analytical + // consumers can distinguish the uncolored bucket (`color:""`) from an + // older payload that predates the dimension — same contract as + // commonpb.Posting.MarshalJSON, VolumeEntry, accountVolumeJSON. + Color string `json:"color"` } // ---------- Conversion from Event protobuf ---------- @@ -178,6 +183,7 @@ func sinkConvertTransaction(tx *commonpb.Transaction) *sinkTransaction { Source: p.GetSource(), Destination: p.GetDestination(), Asset: p.GetAsset(), + Color: p.GetColor(), Amount: p.GetAmount().ToBigInt(), } } diff --git a/internal/application/indexbuilder/backfill_postings.go b/internal/application/indexbuilder/backfill_postings.go index f76188ac54..23ab098322 100644 --- a/internal/application/indexbuilder/backfill_postings.go +++ b/internal/application/indexbuilder/backfill_postings.go @@ -171,7 +171,7 @@ func (b *Builder) processBackfillPostings(ctx context.Context, stop <-chan struc for i := range parsed.Postings { p := &parsed.Postings[i] if err := b.indexPostingAddressMappings( - kb, cfg, parsed.Ledger, parsed.TxID, p.Source, p.Destination, p.Asset, + kb, cfg, parsed.Ledger, parsed.TxID, p.Source, p.Destination, p.Asset, p.Color, indexAny, indexSrc, indexDst, excludedVolumes, ); err != nil { _ = batch.Cancel() diff --git a/internal/application/indexbuilder/backfill_test.go b/internal/application/indexbuilder/backfill_test.go index 9cb5bf579f..0fabdafd16 100644 --- a/internal/application/indexbuilder/backfill_test.go +++ b/internal/application/indexbuilder/backfill_test.go @@ -293,19 +293,19 @@ func TestIndexPostingAddressMappingsSkipsExcludedAccounts(t *testing.T) { } require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 42, "transient:source", "kept:dest", "USD", + b.kb, cfg, "test", 42, "transient:source", "kept:dest", "USD", "", true, true, true, excludedVolumes, )) require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 43, "kept:source", "purged:dest", "USD", + b.kb, cfg, "test", 43, "kept:source", "purged:dest", "USD", "", true, true, true, excludedVolumes, )) require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 44, "shared:account", "kept:dest", "USD", + b.kb, cfg, "test", 44, "shared:account", "kept:dest", "USD", "", true, true, true, excludedVolumes, )) require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 45, "shared:account", "kept:dest", "EUR", + b.kb, cfg, "test", 45, "shared:account", "kept:dest", "EUR", "", true, true, true, excludedVolumes, )) require.NoError(t, b.wb.Flush()) diff --git a/internal/application/indexbuilder/process_logs.go b/internal/application/indexbuilder/process_logs.go index 266e7e10be..d2fb99f9db 100644 --- a/internal/application/indexbuilder/process_logs.go +++ b/internal/application/indexbuilder/process_logs.go @@ -489,7 +489,7 @@ func (b *Builder) indexCreatedTransaction( b.accounts[posting.GetDestination()] = struct{}{} if err := b.indexPostingAddressMappings( - kb, cfg, ledger, txn.GetId(), posting.GetSource(), posting.GetDestination(), posting.GetAsset(), + kb, cfg, ledger, txn.GetId(), posting.GetSource(), posting.GetDestination(), posting.GetAsset(), posting.GetColor(), indexAny, indexSrc, indexDst, excludedVolumes, ); err != nil { return err @@ -615,7 +615,7 @@ func (b *Builder) indexRevertedTransaction( b.accounts[posting.GetDestination()] = struct{}{} if err := b.indexPostingAddressMappings( - kb, cfg, ledger, revertTxn.GetId(), posting.GetSource(), posting.GetDestination(), posting.GetAsset(), + kb, cfg, ledger, revertTxn.GetId(), posting.GetSource(), posting.GetDestination(), posting.GetAsset(), posting.GetColor(), indexAny, indexSrc, indexDst, excludedVolumes, ); err != nil { return err @@ -681,14 +681,15 @@ func (b *Builder) indexPostingAddressMappings( source string, destination string, asset string, + color string, indexAny bool, indexSrc bool, indexDst bool, excludedVolumes map[domain.AccountAssetKey]struct{}, ) error { wb := b.wb - srcExcluded := isExcluded(excludedVolumes, source, asset) - dstExcluded := isExcluded(excludedVolumes, destination, asset) + srcExcluded := isExcluded(excludedVolumes, source, asset, color) + dstExcluded := isExcluded(excludedVolumes, destination, asset, color) // Account has-asset index: record every (account, assetBase, precision) a // posting touches, for both source and destination, skipping excluded @@ -1116,16 +1117,17 @@ func extractMetadataKeyFromReverseMap(key, nsPrefix []byte, ns string) string { return "" } -// isExcluded returns true if the (account, asset) tuple is in the excluded -// set (transient or purged ephemeral). Both dimensions matter — a multi-asset -// account may have one asset purged while another stays kept, and we must -// not over-skip mappings for the kept asset. -func isExcluded(excluded map[domain.AccountAssetKey]struct{}, account, asset string) bool { +// isExcluded returns true if the (account, asset, color) tuple is in the +// excluded set (transient or purged ephemeral). All three dimensions matter +// — a multi-bucket account may have one (asset, color) purged while another +// color of the same asset stays kept, and we must not over-skip mappings for +// the kept bucket. +func isExcluded(excluded map[domain.AccountAssetKey]struct{}, account, asset, color string) bool { if excluded == nil { return false } - _, ok := excluded[domain.AccountAssetKey{Account: account, Asset: asset}] + _, ok := excluded[domain.AccountAssetKey{Account: account, Asset: asset, Color: color}] return ok } diff --git a/internal/application/indexbuilder/protowire_postings.go b/internal/application/indexbuilder/protowire_postings.go index f93759bb8d..c02c919aa5 100644 --- a/internal/application/indexbuilder/protowire_postings.go +++ b/internal/application/indexbuilder/protowire_postings.go @@ -9,13 +9,16 @@ import ( "github.com/formancehq/ledger/v3/internal/proto/commonpb" ) -// rawPosting holds the source, destination and asset extracted from a -// Posting message. The asset is kept so per-asset exclusion lookups against -// purged/transient volume sets stay precise inside multi-asset accounts. +// rawPosting holds the source, destination, asset, and color extracted from +// a Posting message. Asset and color are both kept so per-bucket exclusion +// lookups against purged/transient volume sets stay precise: two color +// buckets of the same (account, asset) can have different purge fates and +// must not be collapsed. type rawPosting struct { Source string Destination string Asset string + Color string } // parsedLog holds the fields extracted by the protowire fast path. @@ -307,12 +310,12 @@ func parseTransaction(data []byte, postings []rawPosting) (txID uint64, result [ return 0, result, errors.New("protowire: invalid bytes for Posting") } - src, dst, asset, perr := parsePosting(b) + src, dst, asset, color, perr := parsePosting(b) if perr != nil { return 0, result, perr } - result = append(result, rawPosting{Source: src, Destination: dst, Asset: asset}) + result = append(result, rawPosting{Source: src, Destination: dst, Asset: asset, Color: color}) data = data[bn:] case num == 5 && typ == protowire.Fixed64Type: v, vn := protowire.ConsumeFixed64(data) @@ -335,8 +338,11 @@ func parseTransaction(data []byte, postings []rawPosting) (txID uint64, result [ return txID, result, nil } -// parseTouchedVolume extracts account (field 1) and asset (field 2) from a -// commonpb.TouchedVolume sub-message embedded in LedgerLog.purged_volumes. +// parseTouchedVolume extracts account (field 1), asset (field 2), and color +// (field 3) from a commonpb.TouchedVolume sub-message embedded in +// LedgerLog.purged_volumes. Color is part of the volume identity so +// indexer exclusions don't over-collapse colored buckets sharing +// (account, asset). func parseTouchedVolume(data []byte) (*commonpb.TouchedVolume, error) { out := &commonpb.TouchedVolume{} for len(data) > 0 { @@ -364,6 +370,14 @@ func parseTouchedVolume(data []byte) (*commonpb.TouchedVolume, error) { out.Asset = string(b) data = data[bn:] + case num == 3 && typ == protowire.BytesType: + b, bn := protowire.ConsumeBytes(data) + if bn < 0 { + return nil, errors.New("protowire: invalid bytes for TouchedVolume.color") + } + + out.Color = string(b) + data = data[bn:] default: n := protowire.ConsumeFieldValue(num, typ, data) if n < 0 { @@ -377,12 +391,14 @@ func parseTouchedVolume(data []byte) (*commonpb.TouchedVolume, error) { return out, nil } -// parsePosting extracts source, destination and asset from Posting bytes. -func parsePosting(data []byte) (source, destination, asset string, err error) { +// parsePosting extracts source, destination, asset, and color from Posting +// bytes. Color (field 5) is part of the volume identity so per-bucket +// exclusion lookups can distinguish colored buckets sharing (account, asset). +func parsePosting(data []byte) (source, destination, asset, color string, err error) { for len(data) > 0 { num, typ, n := protowire.ConsumeTag(data) if n < 0 { - return "", "", "", errors.New("protowire: invalid tag in Posting") + return "", "", "", "", errors.New("protowire: invalid tag in Posting") } data = data[n:] @@ -391,7 +407,7 @@ func parsePosting(data []byte) (source, destination, asset string, err error) { case num == 1 && typ == protowire.BytesType: b, bn := protowire.ConsumeBytes(data) if bn < 0 { - return "", "", "", errors.New("protowire: invalid bytes for Posting.source") + return "", "", "", "", errors.New("protowire: invalid bytes for Posting.source") } source = string(b) @@ -399,7 +415,7 @@ func parsePosting(data []byte) (source, destination, asset string, err error) { case num == 2 && typ == protowire.BytesType: b, bn := protowire.ConsumeBytes(data) if bn < 0 { - return "", "", "", errors.New("protowire: invalid bytes for Posting.destination") + return "", "", "", "", errors.New("protowire: invalid bytes for Posting.destination") } destination = string(b) @@ -407,22 +423,30 @@ func parsePosting(data []byte) (source, destination, asset string, err error) { case num == 4 && typ == protowire.BytesType: b, bn := protowire.ConsumeBytes(data) if bn < 0 { - return "", "", "", errors.New("protowire: invalid bytes for Posting.asset") + return "", "", "", "", errors.New("protowire: invalid bytes for Posting.asset") } asset = string(b) data = data[bn:] + case num == 5 && typ == protowire.BytesType: + b, bn := protowire.ConsumeBytes(data) + if bn < 0 { + return "", "", "", "", errors.New("protowire: invalid bytes for Posting.color") + } + + color = string(b) + data = data[bn:] default: n := protowire.ConsumeFieldValue(num, typ, data) if n < 0 { - return "", "", "", errors.New("protowire: invalid field in Posting") + return "", "", "", "", errors.New("protowire: invalid field in Posting") } data = data[n:] } } - return source, destination, asset, nil + return source, destination, asset, color, nil } // scanBytesField scans protobuf fields looking for a length-delimited field diff --git a/internal/bootstrap/controller_routed.go b/internal/bootstrap/controller_routed.go index 318cd6d919..0f2a149aa4 100644 --- a/internal/bootstrap/controller_routed.go +++ b/internal/bootstrap/controller_routed.go @@ -230,7 +230,7 @@ func (b *RoutedController) GetAuditEntry(ctx context.Context, sequence uint64) ( return c.GetAuditEntry(ctx, sequence) } -func (b *RoutedController) GetAccount(ctx context.Context, ledgerName string, address string) (*commonpb.Account, error) { +func (b *RoutedController) GetAccount(ctx context.Context, ledgerName string, address string, opts ctrl.GetAccountOptions) (*commonpb.Account, error) { c, barrier, err := b.readCtrl(ctx) if err != nil { return nil, err @@ -248,7 +248,7 @@ func (b *RoutedController) GetAccount(ctx context.Context, ledgerName string, ad }).Infof("read barrier for GetAccount") } - return c.GetAccount(ctx, ledgerName, address) + return c.GetAccount(ctx, ledgerName, address, opts) } func (b *RoutedController) ListAccounts(ctx context.Context, ledgerName string, pageSize uint32, afterAddress string, filter *commonpb.QueryFilter, reverse bool) (cursor.Cursor[*commonpb.Account], error) { diff --git a/internal/domain/analysis/compact.go b/internal/domain/analysis/compact.go index 6e27a9c37e..14178930f7 100644 --- a/internal/domain/analysis/compact.go +++ b/internal/domain/analysis/compact.go @@ -15,10 +15,14 @@ type CompactAccount struct { } // CompactPosting holds only the fields needed for transaction analysis. +// Color is part of the posting identity for analysis purposes: two flows +// that share (source, destination, asset) but differ in color are two +// different segregated buckets and must produce distinct signatures. type CompactPosting struct { Source string Destination string Asset string + Color string Amount *big.Int } @@ -39,6 +43,7 @@ func ExtractCompactTransaction(tx *commonpb.Transaction) CompactTransaction { Source: p.GetSource(), Destination: p.GetDestination(), Asset: p.GetAsset(), + Color: p.GetColor(), Amount: p.GetAmount().ToBigInt(), } } diff --git a/internal/domain/analysis/flow_discovery.go b/internal/domain/analysis/flow_discovery.go index e9c9affca3..ae70a7c783 100644 --- a/internal/domain/analysis/flow_discovery.go +++ b/internal/domain/analysis/flow_discovery.go @@ -351,24 +351,36 @@ func normalizePostings(postings []CompactPosting, root *trieNode, addrCache map[ SourcePattern: cachedNormalizeAddress(postings[i].Source, root, addrCache), DestinationPattern: cachedNormalizeAddress(postings[i].Destination, root, addrCache), Asset: postings[i].Asset, + Color: postings[i].Color, } } return normalized } +// flowSignaturePart formats a single posting into its signature contribution. +// Color is always included so two postings differing only by color produce +// distinct signatures (and therefore distinct flow stats). +// +// Components are joined with NUL bytes: ValidateAccountAddress / ValidateAsset +// / ValidateColor all reject NUL upstream, so the separator cannot appear +// inside any component. This keeps the signature unambiguous even if a +// component contains characters previously used as separators (`->`, `[`, +// `|`, `]`). +func flowSignaturePart(p *servicepb.NormalizedPosting) string { + return p.GetSourcePattern() + "\x00" + p.GetDestinationPattern() + "\x00" + p.GetAsset() + "\x00" + p.GetColor() +} + // computeFlowSignature returns a canonical, sorted signature string from normalized postings. func computeFlowSignature(postings []*servicepb.NormalizedPosting) string { if len(postings) == 1 { // Fast path: single posting, no sorting needed. - p := postings[0] - - return p.GetSourcePattern() + "->" + p.GetDestinationPattern() + "[" + p.GetAsset() + "]" + return flowSignaturePart(postings[0]) } parts := make([]string, len(postings)) for i, p := range postings { - parts[i] = p.GetSourcePattern() + "->" + p.GetDestinationPattern() + "[" + p.GetAsset() + "]" + parts[i] = flowSignaturePart(p) } sort.Strings(parts) diff --git a/internal/domain/analysis/flow_discovery_test.go b/internal/domain/analysis/flow_discovery_test.go index d0fe98c4e2..7ff4c0a8fd 100644 --- a/internal/domain/analysis/flow_discovery_test.go +++ b/internal/domain/analysis/flow_discovery_test.go @@ -296,3 +296,42 @@ func TestAnalyzeTransactions_GroupedBySignature(t *testing.T) { assert.Equal(t, uint64(2), resp.GetFlowPatterns()[0].GetTransactionCount()) assert.Equal(t, uint64(1), resp.GetFlowPatterns()[1].GetTransactionCount()) } + +// TestFlowSignaturePart_NoCollisionWithSeparatorsInComponents guards the +// signature format against component-vs-separator confusion. Even if the +// upstream validators ever loosened to allow `->`, `[`, `|`, or `]` inside +// account / asset / color components, the NUL-byte separator keeps the +// signature unambiguous. +func TestFlowSignaturePart_NoCollisionWithSeparatorsInComponents(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + a, b *servicepb.NormalizedPosting + }{ + { + name: "arrow-in-account vs literal arrow", + a: &servicepb.NormalizedPosting{SourcePattern: "src->x", DestinationPattern: "dst", Asset: "USD", Color: ""}, + b: &servicepb.NormalizedPosting{SourcePattern: "src", DestinationPattern: "x->dst", Asset: "USD", Color: ""}, + }, + { + name: "bracket-in-color vs literal", + a: &servicepb.NormalizedPosting{SourcePattern: "src", DestinationPattern: "dst", Asset: "USD", Color: "A]B"}, + b: &servicepb.NormalizedPosting{SourcePattern: "src", DestinationPattern: "dst", Asset: "USD]A", Color: "B"}, + }, + { + name: "pipe in components", + a: &servicepb.NormalizedPosting{SourcePattern: "src", DestinationPattern: "dst", Asset: "USD|X", Color: ""}, + b: &servicepb.NormalizedPosting{SourcePattern: "src", DestinationPattern: "dst", Asset: "USD", Color: "X"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + require.NotEqual(t, flowSignaturePart(tc.a), flowSignaturePart(tc.b), + "signatures must not collide when separators appear inside components") + }) + } +} diff --git a/internal/domain/errors.go b/internal/domain/errors.go index 077c827b4d..442d3c0ae7 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -168,6 +168,8 @@ const ( ErrReasonTransactionAlreadyReverted = "TRANSACTION_ALREADY_REVERTED" ErrReasonInsufficientFunds = "INSUFFICIENT_FUNDS" ErrReasonVolumeOverflow = "VOLUME_OVERFLOW" + ErrReasonAggregateOverflow = "AGGREGATE_OVERFLOW" + ErrReasonBalanceNotFound = "BALANCE_NOT_FOUND" ErrReasonBalanceNotPreloaded = "BALANCE_NOT_PRELOADED" ErrReasonNumscriptParseError = "NUMSCRIPT_PARSE_ERROR" ErrReasonValidation = "VALIDATION" @@ -493,23 +495,40 @@ func (e *ErrTransactionAlreadyReverted) Metadata() map[string]string { return map[string]string{"transactionId": strconv.FormatUint(e.TransactionID, 10)} } -// ErrInsufficientFunds — source account does not have enough balance. +// ErrInsufficientFunds is returned when a source account does not have enough +// balance in the requested (asset, color) bucket. The empty color is the +// uncolored bucket and is segregated from any colored bucket of the same +// (account, asset). type ErrInsufficientFunds struct { Account string Asset string + Color string Amount string // requested amount (decimal string) Balance string // available balance (decimal string) } func (e *ErrInsufficientFunds) Error() string { + if e.Color == "" { + return fmt.Sprintf( + "insufficient funds on account %q for asset %s: needed %s, available %s", + e.Account, e.Asset, e.Amount, e.Balance, + ) + } + return fmt.Sprintf( - "insufficient funds on account %q for asset %s: needed %s, available %s", - e.Account, e.Asset, e.Amount, e.Balance, + "insufficient funds on account %q for asset %s color %q: needed %s, available %s", + e.Account, e.Asset, e.Color, e.Amount, e.Balance, ) } func (*ErrInsufficientFunds) Reason() string { return ErrReasonInsufficientFunds } func (e *ErrInsufficientFunds) Metadata() map[string]string { - return map[string]string{"account": e.Account, "asset": e.Asset, "amount": e.Amount, "balance": e.Balance} + return map[string]string{ + "account": e.Account, + "asset": e.Asset, + "color": e.Color, + "amount": e.Amount, + "balance": e.Balance, + } } // ErrVolumeOverflow — a posting would push an account's volume past 2^256. @@ -521,6 +540,7 @@ func (e *ErrInsufficientFunds) Metadata() map[string]string { type ErrVolumeOverflow struct { Account string Asset string + Color string Side string // "input" or "output" Amount string // requested amount (decimal string) Current string // current volume on that side (decimal string) @@ -528,8 +548,8 @@ type ErrVolumeOverflow struct { func (e *ErrVolumeOverflow) Error() string { return fmt.Sprintf( - "%s volume overflow on account %q for asset %s: current=%s + amount=%s exceeds 2^256", - e.Side, e.Account, e.Asset, e.Current, e.Amount, + "%s volume overflow on account %q for asset %s color=%q: current=%s + amount=%s exceeds 2^256", + e.Side, e.Account, e.Asset, e.Color, e.Current, e.Amount, ) } func (*ErrVolumeOverflow) Reason() string { return ErrReasonVolumeOverflow } @@ -537,12 +557,46 @@ func (e *ErrVolumeOverflow) Metadata() map[string]string { return map[string]string{ "account": e.Account, "asset": e.Asset, + "color": e.Color, "side": e.Side, "amount": e.Amount, "current": e.Current, } } +// ErrAggregateOverflow signals that summing colored or precision-rescaled +// buckets in the read-side aggregator exceeded the 2^256 uint256 ceiling. +// The FSM already rejects per-bucket overflow on write (#321); this guards +// the aggregator with the same discipline since collapseColors and +// use_max_precision can sum many buckets together. +type ErrAggregateOverflow struct { + Stage string // "collapse-colors" or "max-precision-rescale" + Side string // "input" or "output" +} + +func (e *ErrAggregateOverflow) Error() string { + return fmt.Sprintf("aggregate volume %s overflowed 2^256 during %s", e.Side, e.Stage) +} +func (*ErrAggregateOverflow) Reason() string { return ErrReasonAggregateOverflow } +func (e *ErrAggregateOverflow) Metadata() map[string]string { + return map[string]string{"stage": e.Stage, "side": e.Side} +} + +// ErrBalanceNotFound — balance for a source account cannot be determined. +type ErrBalanceNotFound struct { + Account string + Asset string +} + +func (e *ErrBalanceNotFound) Error() string { + return fmt.Sprintf("balance not found for account %q asset %q", e.Account, e.Asset) +} +func (*ErrBalanceNotFound) Reason() string { return ErrReasonBalanceNotFound } +func (e *ErrBalanceNotFound) Metadata() map[string]string { + return map[string]string{"account": e.Account, "asset": e.Asset} +} + + // ErrSinkAlreadyExists — adding a sink that already exists. type ErrSinkAlreadyExists struct { Name string @@ -945,22 +999,27 @@ func (e *ErrDependencyDiscoveryFailed) Metadata() map[string]string { return map[string]string{"details": e.Error()} } -// ErrBalanceNotPreloaded — a balance the script reads was not preloaded into -// the cache by admission. A transient server-side gap (e.g. the boot-time -// bloom-populate window, #318), not a caller-satisfiable precondition: a retry -// re-runs preload and can succeed — hence KindUnavailable, and never frozen as -// an idempotency outcome. +// ErrBalanceNotPreloaded — a balance the script reads (account, asset, color) +// was not preloaded into the cache by admission. A transient server-side gap +// (e.g. the boot-time bloom-populate window, #318), not a caller-satisfiable +// precondition: a retry re-runs preload and can succeed — hence +// KindUnavailable, and never frozen as an idempotency outcome. type ErrBalanceNotPreloaded struct { Account string Asset string + Color string } func (e *ErrBalanceNotPreloaded) Error() string { - return fmt.Sprintf("balance not preloaded for account %q asset %q", e.Account, e.Asset) + if e.Color == "" { + return fmt.Sprintf("balance not preloaded for account %q asset %q", e.Account, e.Asset) + } + + return fmt.Sprintf("balance not preloaded for account %q asset %q color %q", e.Account, e.Asset, e.Color) } func (*ErrBalanceNotPreloaded) Reason() string { return ErrReasonBalanceNotPreloaded } func (e *ErrBalanceNotPreloaded) Metadata() map[string]string { - return map[string]string{"account": e.Account, "asset": e.Asset} + return map[string]string{"account": e.Account, "asset": e.Asset, "color": e.Color} } // ErrTransientAccountNonZero — transient account has non-zero balance at end of batch. @@ -1136,22 +1195,23 @@ func (e *ErrNumscriptRuntime) Metadata() map[string]string { return map[string]string{"detail": e.Detail} } -// ErrVolumeNotMaterialized — a posting references a (Account, Asset) pair -// whose Input/Output volumes have not been fully fetched into the FSM's +// ErrVolumeNotMaterialized — a posting references an (Account, Asset, Color) +// tuple whose Input/Output volumes have not been fully fetched into the FSM's // working set. KindInternal: indicates a preloading miss the admission // layer should have caught; reaching this branch is a server bug. type ErrVolumeNotMaterialized struct { Account string Asset string + Color string Side string // "source" or "destination" } func (e *ErrVolumeNotMaterialized) Error() string { - return fmt.Sprintf("%s volume %s/%s not fully materialized", e.Side, e.Account, e.Asset) + return fmt.Sprintf("%s volume %s/%s color=%q not fully materialized", e.Side, e.Account, e.Asset, e.Color) } func (*ErrVolumeNotMaterialized) Reason() string { return ErrReasonVolumeNotMaterialized } func (e *ErrVolumeNotMaterialized) Metadata() map[string]string { - return map[string]string{"account": e.Account, "asset": e.Asset, "side": e.Side} + return map[string]string{"account": e.Account, "asset": e.Asset, "color": e.Color, "side": e.Side} } // ErrMetadataKeyValidation wraps another Describable to add the metadata-key diff --git a/internal/domain/errors_test.go b/internal/domain/errors_test.go index 9a9245aff0..642eebd30e 100644 --- a/internal/domain/errors_test.go +++ b/internal/domain/errors_test.go @@ -273,6 +273,8 @@ func TestEveryDomainErrorImplementsDescribable(t *testing.T) { "ErrTransactionAlreadyReverted": &ErrTransactionAlreadyReverted{}, "ErrInsufficientFunds": &ErrInsufficientFunds{}, "ErrVolumeOverflow": &ErrVolumeOverflow{}, + "ErrAggregateOverflow": &ErrAggregateOverflow{}, + "ErrBalanceNotFound": &ErrBalanceNotFound{}, "ErrSinkAlreadyExists": &ErrSinkAlreadyExists{}, "ErrSinkBatchSizeTooLarge": &ErrSinkBatchSizeTooLarge{}, "ErrMetadataNotFound": &ErrMetadataNotFound{}, diff --git a/internal/domain/keys.go b/internal/domain/keys.go index 5162d91158..aa34916f0b 100644 --- a/internal/domain/keys.go +++ b/internal/domain/keys.go @@ -51,14 +51,20 @@ type AccountKey struct { } // AccountAssetKey identifies a single Pebble volume cell within a ledger by -// its (account, asset) coordinates. It is the ledger-local subset of +// its (account, asset, color) coordinates. It is the ledger-local subset of // VolumeKey (no LedgerName), used as map key in code that already scopes // data per ledger — exclusion sets in the index builder and the integrity // checker, transient/purged dedup helpers, etc. Keep this type plain (no // derived fields) so it is a value-equal map key. +// +// Color is part of the identity: two color buckets of the same (account, +// asset) are distinct cells that can have different purge/keep fates. The +// empty color is the uncolored bucket and is itself just another segregated +// cell. type AccountAssetKey struct { Account string Asset string + Color string } type VolumeKey struct { @@ -73,11 +79,16 @@ type VolumeKey struct { // or aggregating. AssetBase string AssetPrecision uint8 + + // Color segregates balances within the same (account, asset). The empty + // string is the "uncolored" bucket and is the default. Color values are + // constrained to ^[A-Z]*$ at admission time (matches numscript's rule). + Color string } // NewVolumeKey creates a VolumeKey with pre-parsed AssetBase and AssetPrecision, // avoiding re-parsing on every AppendBytes call in the hot path. -func NewVolumeKey(ledgerName, account, asset string) VolumeKey { +func NewVolumeKey(ledgerName, account, asset, color string) VolumeKey { base, precision := ParseAssetPrecision(asset) return VolumeKey{ @@ -85,11 +96,21 @@ func NewVolumeKey(ledgerName, account, asset string) VolumeKey { Asset: asset, AssetBase: base, AssetPrecision: precision, + Color: color, } } // AppendBytes appends the canonical byte representation to dst and returns the -// extended slice. Format: [ledgerName padded 64B][account][sep][asset_base][precision_byte]. +// extended slice. +// +// Format: [ledgerName padded 64B][account]\x00[color]\x00[asset_base][precision_byte]. +// +// Putting color between account and asset (rather than at the end) preserves +// prefix-scan semantics: scanning by (ledgerName, account) returns every color, +// and scanning by (ledgerName, account, color) is a cheap fixed prefix lookup. +// It also keeps precision as the trailing byte, which matters because precision +// can legitimately be 0x00 (e.g. "EUR") — putting it last means no separator +// follows it, so no encoding ambiguity arises. func (bk VolumeKey) AppendBytes(dst []byte) []byte { base := bk.AssetBase precision := bk.AssetPrecision @@ -102,6 +123,8 @@ func (bk VolumeKey) AppendBytes(dst []byte) []byte { dst = appendLedgerName(dst, bk.LedgerName) dst = append(dst, bk.Account...) dst = append(dst, dal.CanonicalKeySepVolume) + dst = append(dst, bk.Color...) + dst = append(dst, dal.CanonicalKeySepVolume) dst = append(dst, base...) dst = append(dst, precision) @@ -114,6 +137,12 @@ func (bk VolumeKey) Bytes() []byte { } // Unmarshal parses canonical bytes into the VolumeKey. +// +// Layout after the ledger-name block: [account]\x00[color]\x00[asset_base][precision_byte]. +// `account` and `color` are split on their respective \x00 separators (no +// validator allows \x00 inside either upstream); `asset_base` may itself be +// empty (e.g. legacy data) but the trailing precision byte MUST be present, +// so `assetPart` must be at least 1 byte. func (bk *VolumeKey) Unmarshal(d []byte) error { name, rest, err := readLedgerName(d) if err != nil { @@ -122,21 +151,22 @@ func (bk *VolumeKey) Unmarshal(d []byte) error { bk.LedgerName = name - // The remaining bytes are [account][sep_volume=0x00][asset_base][precision_byte]. - // CanonicalKeySepVolume is 0x00; account and asset_base are byte strings - // without embedded zero bytes (validated upstream). - before, after, ok := bytes.Cut(rest, []byte{dal.CanonicalKeySepVolume}) + accountBytes, afterAccount, ok := bytes.Cut(rest, []byte{dal.CanonicalKeySepVolume}) if !ok { - return errors.New("invalid balance key bytes: missing account/asset separator") + return errors.New("invalid balance key bytes: missing account/color separator") } - bk.Account = string(before) + colorBytes, assetPart, ok := bytes.Cut(afterAccount, []byte{dal.CanonicalKeySepVolume}) + if !ok { + return errors.New("invalid balance key bytes: missing color/asset separator") + } - assetPart := after - if len(assetPart) < 2 { + if len(assetPart) < 1 { return errors.New("invalid balance key bytes: asset part too short") } + bk.Account = string(accountBytes) + bk.Color = string(colorBytes) bk.AssetBase = string(assetPart[:len(assetPart)-1]) bk.AssetPrecision = assetPart[len(assetPart)-1] bk.Asset = FormatAsset(bk.AssetBase, bk.AssetPrecision) diff --git a/internal/domain/keys_test.go b/internal/domain/keys_test.go index 203bfd54d2..51ebc6091f 100644 --- a/internal/domain/keys_test.go +++ b/internal/domain/keys_test.go @@ -18,6 +18,10 @@ func padName(name string) []byte { } func newVolumeKey(ak AccountKey, asset string) VolumeKey { + return newColoredVolumeKey(ak, asset, "") +} + +func newColoredVolumeKey(ak AccountKey, asset, color string) VolumeKey { base, prec := ParseAssetPrecision(asset) return VolumeKey{ @@ -25,6 +29,7 @@ func newVolumeKey(ak AccountKey, asset string) VolumeKey { Asset: asset, AssetBase: base, AssetPrecision: prec, + Color: color, } } @@ -74,8 +79,8 @@ func TestVolumeKey_ByteFormat(t *testing.T) { vk := newVolumeKey(AccountKey{LedgerName: "test", Account: "a"}, "USD/4") data := vk.Bytes() - // Expected: [ledgerName padded 64B] "a" \x00 "USD" \x04 - expected := append(padName("test"), 'a', 0x00, 'U', 'S', 'D', 0x04) + // Expected: [ledgerName padded 64B] "a" \x00 [color=""] \x00 "USD" \x04 + expected := append(padName("test"), 'a', 0x00, 0x00, 'U', 'S', 'D', 0x04) require.Equal(t, expected, data) } @@ -89,10 +94,67 @@ func TestVolumeKey_StructLiteralFallback(t *testing.T) { } data := vk.Bytes() - expected := append(padName("test"), 'a', 0x00, 'E', 'U', 'R', 0x02) + expected := append(padName("test"), 'a', 0x00, 0x00, 'E', 'U', 'R', 0x02) + require.Equal(t, expected, data) +} + +func TestVolumeKey_ColorByteFormat(t *testing.T) { + t.Parallel() + + vk := newColoredVolumeKey(AccountKey{LedgerName: "test", Account: "a"}, "USD/4", "RED") + + data := vk.Bytes() + // Expected: [ledgerName padded 64B] "a" \x00 "RED" \x00 "USD" \x04 + expected := append(padName("test"), 'a', 0x00, 'R', 'E', 'D', 0x00, 'U', 'S', 'D', 0x04) require.Equal(t, expected, data) } +func TestVolumeKey_ColorRoundTrip(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + asset string + color string + }{ + {"uncolored EUR", "EUR", ""}, + {"uncolored USD/4", "USD/4", ""}, + {"colored RED USD/4", "USD/4", "RED"}, + {"colored GRANTS USD", "USD", "GRANTS"}, + {"colored long name BTC/8", "BTC/8", "TREASURY"}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + vk := newColoredVolumeKey(AccountKey{LedgerName: "test", Account: "users:alice"}, tc.asset, tc.color) + data := vk.Bytes() + + var decoded VolumeKey + require.NoError(t, decoded.Unmarshal(data)) + require.Equal(t, vk, decoded) + require.Equal(t, tc.color, decoded.Color) + }) + } +} + +// Distinct colors on the same (account, asset) must serialize to distinct +// byte sequences — the whole point of the segregation. +func TestVolumeKey_ColorSegregatesBytes(t *testing.T) { + t.Parallel() + + ak := AccountKey{LedgerName: "test", Account: "alice"} + + uncolored := newColoredVolumeKey(ak, "USD/2", "").Bytes() + red := newColoredVolumeKey(ak, "USD/2", "RED").Bytes() + blue := newColoredVolumeKey(ak, "USD/2", "BLUE").Bytes() + + require.NotEqual(t, uncolored, red) + require.NotEqual(t, uncolored, blue) + require.NotEqual(t, red, blue) +} + func TestMetadataKey_RoundTrip(t *testing.T) { t.Parallel() diff --git a/internal/domain/processing/numscript/emulate.go b/internal/domain/processing/numscript/emulate.go index b73551c0d9..d44dd4ff99 100644 --- a/internal/domain/processing/numscript/emulate.go +++ b/internal/domain/processing/numscript/emulate.go @@ -4,6 +4,7 @@ import ( "context" "maps" "math/big" + "strings" numscriptlib "github.com/formancehq/numscript" @@ -54,18 +55,29 @@ func (s *discoveryStore) GetBalances(_ context.Context, query numscriptlib.Balan s.balancesCalled = true - balances := make(numscriptlib.Balances, len(query)) - for account, assets := range query { - accountBalance := make(numscriptlib.AccountBalance, len(assets)) - - balances[account] = accountBalance - for _, asset := range assets { - s.queriedVolumes[domain.VolumeKey{ - AccountKey: domain.AccountKey{Account: account}, - Asset: asset, - }] = struct{}{} - accountBalance[asset] = new(big.Int).Set(MaxForceBalance) + balances := make(numscriptlib.Balances, 0, len(query)) + for _, item := range query { + // Catch-all asset queries (`BASE/*`) cannot be preloaded: discovery + // does not iterate the storage to enumerate every existing precision + // flavor of BASE on the account. Bail out with the same error the + // apply-time adapter returns so admission rejects the script up + // front instead of producing a phantom Need pointing at "BASE/*". + if strings.HasSuffix(item.Asset, "/*") { + return nil, ErrCatchAllAssetNotSupported } + + s.queriedVolumes[domain.VolumeKey{ + AccountKey: domain.AccountKey{Account: item.Account}, + Asset: item.Asset, + Color: item.Color, + }] = struct{}{} + + balances = append(balances, numscriptlib.BalanceRow{ + Account: item.Account, + Asset: item.Asset, + Color: item.Color, + Amount: new(big.Int).Set(MaxForceBalance), + }) } return balances, nil @@ -141,6 +153,7 @@ func DiscoverNumscriptDependencies(cache *NumscriptCache, script string, vars ma sourceVolumes[domain.VolumeKey{ AccountKey: domain.AccountKey{LedgerName: ledgerName, Account: posting.Source}, Asset: posting.Asset, + Color: posting.Color, }] = struct{}{} if destinationVolumes == nil { @@ -150,6 +163,7 @@ func DiscoverNumscriptDependencies(cache *NumscriptCache, script string, vars ma destinationVolumes[domain.VolumeKey{ AccountKey: domain.AccountKey{LedgerName: ledgerName, Account: posting.Destination}, Asset: posting.Asset, + Color: posting.Color, }] = struct{}{} } } diff --git a/internal/domain/processing/numscript/emulate_test.go b/internal/domain/processing/numscript/emulate_test.go index c652f9f12b..aa27955795 100644 --- a/internal/domain/processing/numscript/emulate_test.go +++ b/internal/domain/processing/numscript/emulate_test.go @@ -1,10 +1,13 @@ package numscript import ( + "context" "testing" "github.com/stretchr/testify/require" + numscriptlib "github.com/formancehq/numscript" + "github.com/formancehq/ledger/v3/internal/domain" ) @@ -292,3 +295,22 @@ func TestDiscoverNumscriptDependencies(t *testing.T) { require.Nil(t, result) }) } + +// TestDiscoveryStore_RejectsCatchAllAsset pins the surface-loudly contract on +// the discovery side. When the numscript runtime asks for "BASE/*" — a query +// it emits to enumerate precision flavors — the ledger has no iteration +// capability to expand it, and pretending the catch-all is a literal asset +// would produce a phantom Need pointing at "BASE/*" that the FSM apply path +// could never satisfy. +func TestDiscoveryStore_RejectsCatchAllAsset(t *testing.T) { + t.Parallel() + + store := &discoveryStore{ + queriedVolumes: make(map[domain.VolumeKey]struct{}), + } + + _, err := store.GetBalances(context.Background(), numscriptlib.BalanceQuery{ + {Account: "alice", Asset: "USD/*"}, + }) + require.ErrorIs(t, err, ErrCatchAllAssetNotSupported) +} diff --git a/internal/domain/processing/numscript/errors.go b/internal/domain/processing/numscript/errors.go index 855d2f75fc..c9fe258631 100644 --- a/internal/domain/processing/numscript/errors.go +++ b/internal/domain/processing/numscript/errors.go @@ -38,6 +38,25 @@ func (errMetaNotSupported) Metadata() map[string]string { return nil } var ErrMetaNotSupported domain.Describable = errMetaNotSupported{} +// ErrCatchAllAssetNotSupported is returned when a Numscript script triggers +// the runtime's catch-all asset query (`BASE/*`) — used internally by the +// numscript interpreter to enumerate every precision flavor of an asset +// on an account when the script references the bare base. The ledger +// adapter cannot today expand the catch-all because the in-memory store +// exposes only point lookups, not iteration; until that capability lands +// we fail explicitly rather than letting the script see a phantom +// ErrBalanceNotPreloaded for `BASE/*`. +type errCatchAllAssetNotSupported struct{} + +func (errCatchAllAssetNotSupported) Error() string { + return "asset catch-all queries (BASE/*) are not yet supported: use the explicit precision (e.g. `send [USD/2 N]` instead of `send [USD N]`)" +} +func (errCatchAllAssetNotSupported) Kind() domain.ErrorKind { return domain.KindValidation } +func (errCatchAllAssetNotSupported) Reason() string { return domain.ErrReasonValidation } +func (errCatchAllAssetNotSupported) Metadata() map[string]string { return nil } + +var ErrCatchAllAssetNotSupported domain.Describable = errCatchAllAssetNotSupported{} + // convertNumscriptError translates known numscript library errors into domain // errors so that the gRPC error mapper can return proper status codes. Library // errors that have no specific mapping are wrapped as ErrNumscriptRuntime diff --git a/internal/domain/processing/numscript_store_adapter_test.go b/internal/domain/processing/numscript_store_adapter_test.go index e87029efa0..5e68f4af36 100644 --- a/internal/domain/processing/numscript_store_adapter_test.go +++ b/internal/domain/processing/numscript_store_adapter_test.go @@ -2,6 +2,7 @@ package processing import ( "context" + "math/big" "testing" "github.com/stretchr/testify/require" @@ -10,6 +11,7 @@ import ( numscriptlib "github.com/formancehq/numscript" "github.com/formancehq/ledger/v3/internal/domain" + "github.com/formancehq/ledger/v3/internal/domain/processing/numscript" "github.com/formancehq/ledger/v3/internal/proto/commonpb" "github.com/formancehq/ledger/v3/internal/proto/raftcmdpb" ) @@ -28,17 +30,23 @@ func TestGetBalances_ForceMode(t *testing.T) { } query := numscriptlib.BalanceQuery{ - "bank": {"USD", "EUR"}, + {Account: "bank", Asset: "USD"}, + {Account: "bank", Asset: "EUR"}, } balances, err := adapter.GetBalances(context.Background(), query) require.NoError(t, err) require.NotNil(t, balances) - // In force mode, all balances should be MaxForceBalance - require.NotNil(t, balances["bank"]["USD"]) - require.NotNil(t, balances["bank"]["EUR"]) - require.Positive(t, balances["bank"]["USD"].Sign()) + // In force mode, every queried (account, asset, color) tuple is + // materialized with MaxForceBalance under the uncolored bucket. + usd, ok := findBalance(balances, "bank", "USD", "") + require.True(t, ok) + require.Positive(t, usd.Sign()) + + eur, ok := findBalance(balances, "bank", "EUR", "") + require.True(t, ok) + require.Positive(t, eur.Sign()) } func TestGetBalances_PreloadedVolumes(t *testing.T) { @@ -54,7 +62,7 @@ func TestGetBalances_PreloadedVolumes(t *testing.T) { force: false, } - volumeKey := domain.NewVolumeKey("test", "bank", "USD") + volumeKey := domain.NewVolumeKey("test", "bank", "USD", "") // Input=1000, Output=300, Balance=700 expectGetVolume(mockStore, volumeKey, (&raftcmdpb.VolumePair{ @@ -63,13 +71,15 @@ func TestGetBalances_PreloadedVolumes(t *testing.T) { }).AsReader(), nil) query := numscriptlib.BalanceQuery{ - "bank": {"USD"}, + {Account: "bank", Asset: "USD"}, } balances, err := adapter.GetBalances(context.Background(), query) require.NoError(t, err) require.NotNil(t, balances) - require.Equal(t, int64(700), balances["bank"]["USD"].Int64()) + amt, ok := findBalance(balances, "bank", "USD", "") + require.True(t, ok) + require.Equal(t, int64(700), amt.Int64()) } func TestGetBalances_NotPreloaded(t *testing.T) { @@ -85,13 +95,13 @@ func TestGetBalances_NotPreloaded(t *testing.T) { force: false, } - volumeKey := domain.NewVolumeKey("test", "bank", "USD") + volumeKey := domain.NewVolumeKey("test", "bank", "USD", "") // Volume exists but has no input values (not preloaded) expectGetVolume(mockStore, volumeKey, (&raftcmdpb.VolumePair{}).AsReader(), nil) query := numscriptlib.BalanceQuery{ - "bank": {"USD"}, + {Account: "bank", Asset: "USD"}, } _, err := adapter.GetBalances(context.Background(), query) @@ -100,7 +110,7 @@ func TestGetBalances_NotPreloaded(t *testing.T) { } // TestGetBalances_VolumeNotFound_TreatedAsZero pins the EN-1378 contract: -// a declared-but-absent volume key (Scope.GetVolume → domain.ErrNotFound) +// a declared-but-absent volume key (Scope.Volumes().Get → domain.ErrNotFound) // is treated as a fresh zero balance by the numscript balance adapter, not // as an admission failure. The coverage gate (one layer up) is what catches // "admission forgot to declare"; ErrNotFound is the legitimate signal once @@ -118,18 +128,46 @@ func TestGetBalances_VolumeNotFound_TreatedAsZero(t *testing.T) { force: false, } - volumeKey := domain.NewVolumeKey("test", "bank", "USD") + volumeKey := domain.NewVolumeKey("test", "bank", "USD", "") expectGetVolume(mockStore, volumeKey, nil, domain.ErrNotFound) query := numscriptlib.BalanceQuery{ - "bank": {"USD"}, + {Account: "bank", Asset: "USD", Color: ""}, } balances, err := adapter.GetBalances(context.Background(), query) require.NoError(t, err) - require.NotNil(t, balances["bank"]) - require.Equal(t, "0", balances["bank"]["USD"].String()) + require.Len(t, balances, 1) + require.Equal(t, "bank", balances[0].Account) + require.Equal(t, "USD", balances[0].Asset) + require.Equal(t, "", balances[0].Color) + require.Equal(t, "0", balances[0].Amount.String()) +} + +// TestGetBalances_CatchAllRejected pins the surface-loudly contract: +// the numscript runtime emits "BASE/*" when a script references a bare +// base asset (no precision). Until the in-memory store grows an +// iterator that lets us enumerate concrete precisions, the adapter must +// surface the unsupported case rather than miss on a literal "BASE/*" +// volume key with a confusing ErrBalanceNotPreloaded. +func TestGetBalances_CatchAllRejected(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + adapter := &numscriptStoreAdapter{ + store: NewMockScope(ctrl), + ledgerName: "test", + } + + query := numscriptlib.BalanceQuery{ + {Account: "alice", Asset: "USD/*"}, + } + + _, err := adapter.GetBalances(context.Background(), query) + require.ErrorIs(t, err, numscript.ErrCatchAllAssetNotSupported) } func TestGetAccountsMetadata_Basic(t *testing.T) { @@ -261,3 +299,13 @@ func TestGetAccountsMetadata_NoSchemaLedger(t *testing.T) { require.NotNil(t, result) require.Equal(t, "25", result["users:001"]["age"]) } + +func findBalance(rows numscriptlib.Balances, account, asset, color string) (*big.Int, bool) { + for _, r := range rows { + if r.Account == account && r.Asset == asset && r.Color == color { + return r.Amount, true + } + } + + return nil, false +} diff --git a/internal/domain/processing/processor_posting.go b/internal/domain/processing/processor_posting.go index 7bf45f3970..d431a2aaa9 100644 --- a/internal/domain/processing/processor_posting.go +++ b/internal/domain/processing/processor_posting.go @@ -54,9 +54,9 @@ type cachedAssetPrecision struct { // cachedVolumeKey builds a VolumeKey, using the assetCache to avoid // re-parsing the asset precision when the same asset string recurs. // If assetCache is nil, falls back to domain.NewVolumeKey. -func cachedVolumeKey(ledgerName string, account, asset string, assetCache map[string]cachedAssetPrecision) domain.VolumeKey { +func cachedVolumeKey(ledgerName, account, asset, color string, assetCache map[string]cachedAssetPrecision) domain.VolumeKey { if assetCache == nil { - return domain.NewVolumeKey(ledgerName, account, asset) + return domain.NewVolumeKey(ledgerName, account, asset, color) } cached, ok := assetCache[asset] @@ -70,6 +70,7 @@ func cachedVolumeKey(ledgerName string, account, asset string, assetCache map[st Asset: asset, AssetBase: cached.base, AssetPrecision: cached.precision, + Color: color, } } @@ -78,8 +79,13 @@ func cachedVolumeKey(ledgerName string, account, asset string, assetCache map[st // increases Output for source and Input for destination. // All volumes must be preloaded by the admission layer — nil volumes return an error. // assetCache, if non-nil, avoids redundant ParseAssetPrecision calls across postings. +// +// Color is carried into both source and destination volume keys, so balances are +// strictly segregated per (account, asset, color). The empty color is the +// uncolored bucket and is itself one of these segregated buckets. func applyPosting(s Scope, ledgerName string, posting *commonpb.Posting, skipBalanceCheck bool, assetCache map[string]cachedAssetPrecision) domain.Describable { - sourceKey := cachedVolumeKey(ledgerName, posting.GetSource(), posting.GetAsset(), assetCache) + color := posting.GetColor() + sourceKey := cachedVolumeKey(ledgerName, posting.GetSource(), posting.GetAsset(), color, assetCache) // Decode posting amount into stack variable to avoid heap allocation var amount uint256.Int @@ -94,7 +100,7 @@ func applyPosting(s Scope, ledgerName string, posting *commonpb.Posting, skipBal return &domain.ErrStorageOperation{Operation: "loading source volume", Cause: err} } if sourceReader == nil || sourceReader.GetInput() == nil || sourceReader.GetOutput() == nil { - return &domain.ErrBalanceNotPreloaded{Account: posting.GetSource(), Asset: posting.GetAsset()} + return &domain.ErrBalanceNotPreloaded{Account: posting.GetSource(), Asset: posting.GetAsset(), Color: color} } // Balance check (skip for "world" account and when skipBalanceCheck is true) @@ -113,6 +119,7 @@ func applyPosting(s Scope, ledgerName string, posting *commonpb.Posting, skipBal return &domain.ErrInsufficientFunds{ Account: posting.GetSource(), Asset: posting.GetAsset(), + Color: color, Amount: amount.Dec(), Balance: balanceBig.String(), } @@ -136,6 +143,7 @@ func applyPosting(s Scope, ledgerName string, posting *commonpb.Posting, skipBal return &domain.ErrVolumeOverflow{ Account: posting.GetSource(), Asset: posting.GetAsset(), + Color: color, Side: "output", Amount: amount.Dec(), Current: scratch.Dec(), @@ -146,14 +154,14 @@ func applyPosting(s Scope, ledgerName string, posting *commonpb.Posting, skipBal s.Volumes().Put(sourceKey, sourceVol) // Destination receives credit - increase Input - destKey := cachedVolumeKey(ledgerName, posting.GetDestination(), posting.GetAsset(), assetCache) + destKey := cachedVolumeKey(ledgerName, posting.GetDestination(), posting.GetAsset(), color, assetCache) destReader, err := readVolumeOrZero(s, destKey) if err != nil { return &domain.ErrStorageOperation{Operation: "loading destination volume", Cause: err} } if destReader == nil || destReader.GetInput() == nil || destReader.GetOutput() == nil { - return &domain.ErrBalanceNotPreloaded{Account: posting.GetDestination(), Asset: posting.GetAsset()} + return &domain.ErrBalanceNotPreloaded{Account: posting.GetDestination(), Asset: posting.GetAsset(), Color: color} } destVol := destReader.Mutate() @@ -163,6 +171,7 @@ func applyPosting(s Scope, ledgerName string, posting *commonpb.Posting, skipBal return &domain.ErrVolumeOverflow{ Account: posting.GetDestination(), Asset: posting.GetAsset(), + Color: color, Side: "input", Amount: amount.Dec(), Current: scratch.Dec(), diff --git a/internal/domain/processing/processor_posting_test.go b/internal/domain/processing/processor_posting_test.go index e32b3b7b05..b048d15b9d 100644 --- a/internal/domain/processing/processor_posting_test.go +++ b/internal/domain/processing/processor_posting_test.go @@ -19,8 +19,9 @@ func TestApplyPosting_WorldAccount_SkipsBalanceCheck(t *testing.T) { defer ctrl.Finish() mockStore := NewMockScope(ctrl) - sourceKey := domain.NewVolumeKey("test", "world", "USD") - destKey := domain.NewVolumeKey("test", "users:001", "USD") + + sourceKey := domain.NewVolumeKey("test", "world", "USD", "") + destKey := domain.NewVolumeKey("test", "users:001", "USD", "") zeroVol := &raftcmdpb.VolumePair{ Input: commonpb.NewUint256FromUint64(0), @@ -51,7 +52,8 @@ func TestApplyPosting_InsufficientFunds(t *testing.T) { defer ctrl.Finish() mockStore := NewMockScope(ctrl) - sourceKey := domain.NewVolumeKey("test", "bank", "USD") + + sourceKey := domain.NewVolumeKey("test", "bank", "USD", "") // Source has input=100, output=50, balance=50, but posting is 200 sourceVol := &raftcmdpb.VolumePair{ @@ -84,7 +86,8 @@ func TestApplyPosting_ZeroInputBalance(t *testing.T) { defer ctrl.Finish() mockStore := NewMockScope(ctrl) - sourceKey := domain.NewVolumeKey("test", "bank", "USD") + + sourceKey := domain.NewVolumeKey("test", "bank", "USD", "") // Source has zero input balance, Output=0 sourceVol := &raftcmdpb.VolumePair{ @@ -118,8 +121,9 @@ func TestApplyPosting_ForceSkipsBalanceCheck(t *testing.T) { defer ctrl.Finish() mockStore := NewMockScope(ctrl) - sourceKey := domain.NewVolumeKey("test", "bank", "USD") - destKey := domain.NewVolumeKey("test", "users:001", "USD") + + sourceKey := domain.NewVolumeKey("test", "bank", "USD", "") + destKey := domain.NewVolumeKey("test", "users:001", "USD", "") // Source has insufficient balance, but force=true skips the check sourceVol := &raftcmdpb.VolumePair{ @@ -154,7 +158,8 @@ func TestApplyPosting_NotPreloaded(t *testing.T) { defer ctrl.Finish() mockStore := NewMockScope(ctrl) - sourceKey := domain.NewVolumeKey("test", "bank", "USD") + + sourceKey := domain.NewVolumeKey("test", "bank", "USD", "") expectGetVolume(mockStore, sourceKey, nil, nil) //nolint:nilnil // test: nil volume @@ -264,8 +269,9 @@ func TestApplyPosting_DestinationInputOverflow_Rejects(t *testing.T) { defer ctrl.Finish() mockStore := NewMockScope(ctrl) - sourceKey := domain.NewVolumeKey("test", "world", "USD") - destKey := domain.NewVolumeKey("test", "users:001", "USD") + + sourceKey := domain.NewVolumeKey("test", "world", "USD", "") + destKey := domain.NewVolumeKey("test", "users:001", "USD", "") // world output is 0 — safe to add anything on the source side. worldVol := &raftcmdpb.VolumePair{ @@ -315,7 +321,8 @@ func TestApplyPosting_SourceOutputOverflow_Rejects(t *testing.T) { defer ctrl.Finish() mockStore := NewMockScope(ctrl) - sourceKey := domain.NewVolumeKey("test", "world", "USD") + + sourceKey := domain.NewVolumeKey("test", "world", "USD", "") worldVol := &raftcmdpb.VolumePair{ Input: commonpb.NewUint256FromUint64(0), diff --git a/internal/domain/processing/processor_revert_transaction.go b/internal/domain/processing/processor_revert_transaction.go index 420a374d9b..c521463d78 100644 --- a/internal/domain/processing/processor_revert_transaction.go +++ b/internal/domain/processing/processor_revert_transaction.go @@ -34,15 +34,17 @@ func processRevertTransaction(ledger string, order *raftcmdpb.RevertTransactionO } // Create reversed postings and update volumes - // For a revert: original destination becomes source, original source becomes destination + // For a revert: original destination becomes source, original source becomes destination. + // Color carries over from the original posting — the funds were segregated under + // (account, asset, color) on the way out, so they must return under the same bucket. revertPostings := make([]*commonpb.Posting, len(order.GetOriginalPostings())) for i, originalPosting := range order.GetOriginalPostings() { - // Create reversed posting revertPostings[i] = &commonpb.Posting{ - Source: originalPosting.GetDestination(), // Original destination is now source - Destination: originalPosting.GetSource(), // Original source is now destination + Source: originalPosting.GetDestination(), + Destination: originalPosting.GetSource(), Amount: originalPosting.GetAmount(), Asset: originalPosting.GetAsset(), + Color: originalPosting.GetColor(), } } @@ -108,7 +110,11 @@ func processRevertTransaction(ledger string, order *raftcmdpb.RevertTransactionO // Compute post-commit volumes if requested var postCommitVolumes *commonpb.PostCommitVolumes if order.GetExpandVolumes() { - postCommitVolumes = buildPostCommitVolumes(s, ledger, revertPostings) + var pcvErr domain.Describable + postCommitVolumes, pcvErr = buildPostCommitVolumes(s, ledger, revertPostings) + if pcvErr != nil { + return nil, pcvErr + } } return &commonpb.LedgerLogPayload{ diff --git a/internal/domain/processing/processor_revert_transaction_test.go b/internal/domain/processing/processor_revert_transaction_test.go index c32e00841c..0dde7d307e 100644 --- a/internal/domain/processing/processor_revert_transaction_test.go +++ b/internal/domain/processing/processor_revert_transaction_test.go @@ -44,10 +44,10 @@ func TestProcessRevertTransaction_Success(t *testing.T) { // Reversed posting: destination becomes source, source becomes destination // Original: bank -> users:123 for 100 USD // Revert: users:123 -> bank for 100 USD - expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD"), sourceVol.AsReader(), nil) - expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD"), nil) - expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD"), destVol.AsReader(), nil) - expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD"), nil) + expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD", ""), sourceVol.AsReader(), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD", ""), nil) + expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD", ""), destVol.AsReader(), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD", ""), nil) mockStore.EXPECT().PutReverted(txKey, true) @@ -132,10 +132,10 @@ func TestProcessRevertTransaction_AtEffectiveDate(t *testing.T) { mockStore.EXPECT().GetReverted(txKey).Return(false, nil) mockStore.EXPECT().GetDate().Return(now.AsReader()).AnyTimes() - expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD"), sourceVol.AsReader(), nil) - expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD"), nil) - expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD"), destVol.AsReader(), nil) - expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD"), nil) + expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD", ""), sourceVol.AsReader(), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD", ""), nil) + expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD", ""), destVol.AsReader(), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD", ""), nil) mockStore.EXPECT().PutReverted(txKey, true) @@ -210,10 +210,10 @@ func TestProcessRevertTransaction_AtEffectiveDate_MissingOriginalTimestamp(t *te mockStore.EXPECT().GetReverted(txKey).Return(false, nil) mockStore.EXPECT().GetDate().Return(now.AsReader()).AnyTimes() - expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD"), sourceVol.AsReader(), nil) - expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD"), nil) - expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD"), destVol.AsReader(), nil) - expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD"), nil) + expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD", ""), sourceVol.AsReader(), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "users:123", "USD", ""), nil) + expectGetVolume(mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD", ""), destVol.AsReader(), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("test-ledger", "bank", "USD", ""), nil) mockStore.EXPECT().PutReverted(txKey, true) diff --git a/internal/domain/processing/processor_test.go b/internal/domain/processing/processor_test.go index 6b35870c9b..094076bd0b 100644 --- a/internal/domain/processing/processor_test.go +++ b/internal/domain/processing/processor_test.go @@ -173,9 +173,9 @@ func TestCreateLedgerAndTransactInSameBatch(t *testing.T) { mockStore.EXPECT().GetCurrentOpenChapter().Return(nil, false) - // Volume operations: the LedgerID should be 1 (assigned by CreateLedger). - srcKey := domain.NewVolumeKey("myled", "world", "USD") - dstKey := domain.NewVolumeKey("myled", "users:bob", "USD") + // Volume operations: keyed by ledger name ("myled" — the ledger created above). + srcKey := domain.NewVolumeKey("myled", "world", "USD", "") + dstKey := domain.NewVolumeKey("myled", "users:bob", "USD", "") zeroVol := &raftcmdpb.VolumePair{ Input: commonpb.NewUint256FromUint64(0), diff --git a/internal/domain/processing/processor_transaction.go b/internal/domain/processing/processor_transaction.go index ef41a6612c..a4ef165469 100644 --- a/internal/domain/processing/processor_transaction.go +++ b/internal/domain/processing/processor_transaction.go @@ -190,7 +190,11 @@ func processCreateTransaction(ledger string, order *raftcmdpb.CreateTransactionO // Compute post-commit volumes if requested var postCommitVolumes *commonpb.PostCommitVolumes if order.GetExpandVolumes() { - postCommitVolumes = buildPostCommitVolumes(s, ledger, result.Postings) + var pcvErr domain.Describable + postCommitVolumes, pcvErr = buildPostCommitVolumes(s, ledger, result.Postings) + if pcvErr != nil { + return nil, pcvErr + } } // Get the current open chapter ID for the receipt @@ -235,6 +239,10 @@ func validatePostings(postings []*commonpb.Posting) domain.Describable { if err := domain.ValidateAsset(p.GetAsset()); err != nil { return err } + + if err := domain.ValidateColor(p.GetColor()); err != nil { + return err + } } return nil diff --git a/internal/domain/processing/processor_transaction_numscript.go b/internal/domain/processing/processor_transaction_numscript.go index 69aeb971c9..0119be889f 100644 --- a/internal/domain/processing/processor_transaction_numscript.go +++ b/internal/domain/processing/processor_transaction_numscript.go @@ -6,6 +6,7 @@ import ( "fmt" "maps" "math/big" + "strings" "github.com/holiman/uint256" @@ -82,15 +83,16 @@ func (p *numscriptPostingProducer) produce(s Scope, ledgerName string, order *ra Destination: posting.Destination, Amount: commonpb.NewUint256(&u256Amount), Asset: posting.Asset, + Color: posting.Color, } // Update source output (money going out) - sourceKey := domain.NewVolumeKey(ledgerName, posting.Source, posting.Asset) + sourceKey := domain.NewVolumeKey(ledgerName, posting.Source, posting.Asset, posting.Color) sourceReader, err := readVolumeOrZero(s, sourceKey) if err != nil { return nil, &domain.ErrStorageOperation{ - Operation: fmt.Sprintf("source volume %s/%s", posting.Source, posting.Asset), + Operation: fmt.Sprintf("source volume %s/%s color=%q", posting.Source, posting.Asset, posting.Color), Cause: err, } } @@ -98,6 +100,7 @@ func (p *numscriptPostingProducer) produce(s Scope, ledgerName string, order *ra return nil, &domain.ErrVolumeNotMaterialized{ Account: posting.Source, Asset: posting.Asset, + Color: posting.Color, Side: "source", } } @@ -111,6 +114,7 @@ func (p *numscriptPostingProducer) produce(s Scope, ledgerName string, order *ra return nil, &domain.ErrVolumeOverflow{ Account: posting.Source, Asset: posting.Asset, + Color: posting.Color, Side: "output", Amount: u256Amount.Dec(), Current: scratch.Dec(), @@ -121,12 +125,12 @@ func (p *numscriptPostingProducer) produce(s Scope, ledgerName string, order *ra s.Volumes().Put(sourceKey, sourceVol) // Update destination input (money coming in) - destKey := domain.NewVolumeKey(ledgerName, posting.Destination, posting.Asset) + destKey := domain.NewVolumeKey(ledgerName, posting.Destination, posting.Asset, posting.Color) destReader, err := readVolumeOrZero(s, destKey) if err != nil { return nil, &domain.ErrStorageOperation{ - Operation: fmt.Sprintf("destination volume %s/%s", posting.Destination, posting.Asset), + Operation: fmt.Sprintf("destination volume %s/%s color=%q", posting.Destination, posting.Asset, posting.Color), Cause: err, } } @@ -134,6 +138,7 @@ func (p *numscriptPostingProducer) produce(s Scope, ledgerName string, order *ra return nil, &domain.ErrVolumeNotMaterialized{ Account: posting.Destination, Asset: posting.Asset, + Color: posting.Color, Side: "destination", } } @@ -145,6 +150,7 @@ func (p *numscriptPostingProducer) produce(s Scope, ledgerName string, order *ra return nil, &domain.ErrVolumeOverflow{ Account: posting.Destination, Asset: posting.Asset, + Color: posting.Color, Side: "input", Amount: u256Amount.Dec(), Current: scratch.Dec(), @@ -219,43 +225,63 @@ type numscriptStoreAdapter struct { } func (s *numscriptStoreAdapter) GetBalances(_ context.Context, query numscriptlib.BalanceQuery) (numscriptlib.Balances, error) { - balances := make(numscriptlib.Balances) + balances := make(numscriptlib.Balances, 0, len(query)) var inputVal, outputVal uint256.Int // stack scratch reused across iterations - for account, assets := range query { - accountBalance := make(numscriptlib.AccountBalance) - balances[account] = accountBalance - - for _, asset := range assets { - // When force mode is enabled, return unlimited balance for all accounts - // This bypasses all balance checks in Numscript execution - if s.force { - accountBalance[asset] = new(big.Int).Set(numscript.MaxForceBalance) - - continue - } - - volumeKey := domain.NewVolumeKey(s.ledgerName, account, asset) + for _, item := range query { + // Reject the numscript runtime's catch-all asset query (`BASE/*`) + // explicitly: the in-memory store does not expose iteration, so we + // cannot expand the wildcard to the concrete precision flavors that + // live on the account. Surface the unsupported case loudly rather + // than letting readVolumeOrZero miss on a literal "BASE/*" key. + if strings.HasSuffix(item.Asset, "/*") { + return nil, numscript.ErrCatchAllAssetNotSupported + } - vol, err := readVolumeOrZero(s.store, volumeKey) - if err != nil { - return nil, err - } + // When force mode is enabled, return unlimited balance for the + // queried (account, asset, color) tuple. This bypasses balance + // checks inside numscript while still respecting the color + // dimension numscript will use to assemble postings. + if s.force { + balances = append(balances, numscriptlib.BalanceRow{ + Account: item.Account, + Asset: item.Asset, + Color: item.Color, + Amount: new(big.Int).Set(numscript.MaxForceBalance), + }) + + continue + } - if vol == nil || vol.GetInput() == nil || vol.GetOutput() == nil { - return nil, &domain.ErrBalanceNotPreloaded{Account: account, Asset: asset} - } + volumeKey := domain.NewVolumeKey(s.ledgerName, item.Account, item.Asset, item.Color) - // Calculate balance: Input - Output using uint256, then convert to *big.Int at boundary - vol.GetInput().IntoUint256(&inputVal) - vol.GetOutput().IntoUint256(&outputVal) + vol, err := readVolumeOrZero(s.store, volumeKey) + if err != nil { + return nil, err + } - // balance escapes into the map, so it must be heap-allocated - // Convert to *big.Int at the numscript boundary (numscript uses *big.Int) - balance := new(big.Int).Sub(inputVal.ToBig(), outputVal.ToBig()) - accountBalance[asset] = balance + // Mirrors the guard in applyPosting (processor_posting.go) and produce() + // above: WriteSet.GetVolume legitimately returns (nil, nil) for a key the + // admission layer never preloaded (e.g. a colored bucket touched by a + // catch-all expansion that didn't preload everything). Calling GetInput() + // on a nil interface panics in the FSM apply path and desyncs the cluster. + if vol == nil || vol.GetInput() == nil || vol.GetOutput() == nil { + return nil, &domain.ErrBalanceNotPreloaded{Account: item.Account, Asset: item.Asset, Color: item.Color} } + + // Calculate balance: Input - Output using uint256, then convert to *big.Int at boundary + vol.GetInput().IntoUint256(&inputVal) + vol.GetOutput().IntoUint256(&outputVal) + + // balance escapes into the row, so it must be heap-allocated + // Convert to *big.Int at the numscript boundary (numscript uses *big.Int) + balances = append(balances, numscriptlib.BalanceRow{ + Account: item.Account, + Asset: item.Asset, + Color: item.Color, + Amount: new(big.Int).Sub(inputVal.ToBig(), outputVal.ToBig()), + }) } return balances, nil diff --git a/internal/domain/processing/processor_transaction_test.go b/internal/domain/processing/processor_transaction_test.go index 4409a2648e..7b74ce3e2c 100644 --- a/internal/domain/processing/processor_transaction_test.go +++ b/internal/domain/processing/processor_transaction_test.go @@ -25,8 +25,8 @@ func TestProcessCreateTransaction(t *testing.T) { now := &commonpb.Timestamp{Data: 1234567890} boundaries := &raftcmdpb.LedgerBoundaries{NextTransactionId: 1, NextLogId: 1} - sourceKey := domain.NewVolumeKey("test-ledger", "bank", "USD") - destKey := domain.NewVolumeKey("test-ledger", "users:123", "USD") + sourceKey := domain.NewVolumeKey("test-ledger", "bank", "USD", "") + destKey := domain.NewVolumeKey("test-ledger", "users:123", "USD", "") // Source has 1000 input, 0 output -> balance = 1000 sourceVolume := &raftcmdpb.VolumePair{ @@ -103,7 +103,7 @@ func TestProcessCreateTransaction_InsufficientFunds(t *testing.T) { boundaries := &raftcmdpb.LedgerBoundaries{NextTransactionId: 1, NextLogId: 1} - sourceKey := domain.NewVolumeKey("test-ledger", "users:123", "USD") + sourceKey := domain.NewVolumeKey("test-ledger", "users:123", "USD", "") // Source has only 50 balance (100 input - 50 output) sourceVolume := &raftcmdpb.VolumePair{ @@ -154,8 +154,8 @@ func TestProcessCreateTransaction_WorldSource(t *testing.T) { now := &commonpb.Timestamp{Data: 1234567890} boundaries := &raftcmdpb.LedgerBoundaries{NextTransactionId: 1, NextLogId: 1} - worldKey := domain.NewVolumeKey("test-ledger", "world", "USD") - destKey := domain.NewVolumeKey("test-ledger", "users:123", "USD") + worldKey := domain.NewVolumeKey("test-ledger", "world", "USD", "") + destKey := domain.NewVolumeKey("test-ledger", "users:123", "USD", "") // World has negative balance (but "world" bypasses balance check) worldVolume := &raftcmdpb.VolumePair{ @@ -1065,8 +1065,8 @@ func TestProcessCreateTransaction_Force_InsufficientFunds(t *testing.T) { now := &commonpb.Timestamp{Data: 1234567890} boundaries := &raftcmdpb.LedgerBoundaries{NextTransactionId: 1, NextLogId: 1} - sourceKey := domain.NewVolumeKey("test-ledger", "users:123", "USD") - destKey := domain.NewVolumeKey("test-ledger", "merchant", "USD") + sourceKey := domain.NewVolumeKey("test-ledger", "users:123", "USD", "") + destKey := domain.NewVolumeKey("test-ledger", "merchant", "USD", "") // Source has only 50 balance (100 input - 50 output) - not enough for 100 sourceVolume := &raftcmdpb.VolumePair{ @@ -1144,8 +1144,8 @@ func TestProcessCreateTransaction_Force_ZeroBalance(t *testing.T) { now := &commonpb.Timestamp{Data: 1234567890} boundaries := &raftcmdpb.LedgerBoundaries{NextTransactionId: 1, NextLogId: 1} - sourceKey := domain.NewVolumeKey("test-ledger", "users:new", "USD") - destKey := domain.NewVolumeKey("test-ledger", "merchant", "USD") + sourceKey := domain.NewVolumeKey("test-ledger", "users:new", "USD", "") + destKey := domain.NewVolumeKey("test-ledger", "merchant", "USD", "") // Source has zero balance, force=true skips balance check zeroVol := &raftcmdpb.VolumePair{ @@ -1371,8 +1371,8 @@ func TestProcessCreateTransaction_ChapterIdInCreatedTransaction(t *testing.T) { now := &commonpb.Timestamp{Data: 1234567890} boundaries := &raftcmdpb.LedgerBoundaries{NextTransactionId: 1, NextLogId: 1} - sourceKey := domain.NewVolumeKey("test-ledger", "world", "USD") - destKey := domain.NewVolumeKey("test-ledger", "users:alice", "USD") + sourceKey := domain.NewVolumeKey("test-ledger", "world", "USD", "") + destKey := domain.NewVolumeKey("test-ledger", "users:alice", "USD", "") zeroVol := &raftcmdpb.VolumePair{ Input: commonpb.NewUint256FromUint64(0), @@ -1438,8 +1438,8 @@ func TestProcessCreateTransaction_ChapterIdZeroWhenNoChapter(t *testing.T) { now := &commonpb.Timestamp{Data: 1234567890} boundaries := &raftcmdpb.LedgerBoundaries{NextTransactionId: 1, NextLogId: 1} - sourceKey := domain.NewVolumeKey("test-ledger", "world", "USD") - destKey := domain.NewVolumeKey("test-ledger", "users:bob", "USD") + sourceKey := domain.NewVolumeKey("test-ledger", "world", "USD", "") + destKey := domain.NewVolumeKey("test-ledger", "users:bob", "USD", "") zeroVol := &raftcmdpb.VolumePair{ Input: commonpb.NewUint256FromUint64(0), @@ -1519,8 +1519,8 @@ func TestProcessCreateTransaction_StoresAccountMetadataVerbatim(t *testing.T) { }, } - worldKey := domain.NewVolumeKey("test-ledger", "world", "USD") - destKey := domain.NewVolumeKey("test-ledger", "users:123", "USD") + worldKey := domain.NewVolumeKey("test-ledger", "world", "USD", "") + destKey := domain.NewVolumeKey("test-ledger", "users:123", "USD", "") zero := &raftcmdpb.VolumePair{Input: commonpb.NewUint256FromUint64(0), Output: commonpb.NewUint256FromUint64(0)} metaKey := domain.MetadataKey{ diff --git a/internal/domain/processing/processor_volumes.go b/internal/domain/processing/processor_volumes.go index 26704c9ed8..ffcbdf08f1 100644 --- a/internal/domain/processing/processor_volumes.go +++ b/internal/domain/processing/processor_volumes.go @@ -9,43 +9,55 @@ import ( "github.com/formancehq/ledger/v3/internal/proto/commonpb" ) -// buildPostCommitVolumes computes the post-commit volumes for all (account, asset) -// pairs involved in the given postings. It reads the current volume state from the -// in-memory store (after postings have been applied) and converts Known values -// into concrete Input/Output values as big integer strings. -func buildPostCommitVolumes(s Scope, ledgerName string, postings []*commonpb.Posting) *commonpb.PostCommitVolumes { - // Collect unique (account, asset) pairs - type accountAsset struct { +// buildPostCommitVolumes computes the post-commit volumes for all +// (account, asset, color) tuples involved in the given postings. It reads the +// current volume state from the in-memory store (after postings have been +// applied) and converts Known values into concrete Input/Output values as big +// integer strings. +// +// The returned VolumesByAssets list is sorted by (asset, color) ascending so +// the response is deterministic across reads. +// +// A non-NotFound storage error from GetVolume is surfaced through the +// returned error (rather than silently skipping the tuple): two nodes with +// different transient store errors would otherwise emit divergent PCV +// payloads for the same applied index, breaking the determinism invariant. +func buildPostCommitVolumes(s Scope, ledgerName string, postings []*commonpb.Posting) (*commonpb.PostCommitVolumes, domain.Describable) { + type tuple struct { account string asset string + color string } - seen := make(map[accountAsset]struct{}) + seen := make(map[tuple]struct{}) - var pairs []accountAsset + var tuples []tuple - for _, p := range postings { - srcKey := accountAsset{account: p.GetSource(), asset: p.GetAsset()} - if _, ok := seen[srcKey]; !ok { - seen[srcKey] = struct{}{} - pairs = append(pairs, srcKey) + add := func(t tuple) { + if _, ok := seen[t]; ok { + return } + seen[t] = struct{}{} + tuples = append(tuples, t) + } - dstKey := accountAsset{account: p.GetDestination(), asset: p.GetAsset()} - if _, ok := seen[dstKey]; !ok { - seen[dstKey] = struct{}{} - pairs = append(pairs, dstKey) - } + for _, p := range postings { + color := p.GetColor() + add(tuple{account: p.GetSource(), asset: p.GetAsset(), color: color}) + add(tuple{account: p.GetDestination(), asset: p.GetAsset(), color: color}) } - volumesByAccount := make(map[string]*commonpb.VolumesByAssets, len(pairs)) + volumesByAccount := make(map[string]*commonpb.VolumesByAssets, len(tuples)) var scratch uint256.Int - for _, pair := range pairs { - vol, err := s.Volumes().Get(domain.NewVolumeKey(ledgerName, pair.account, pair.asset)) + for _, t := range tuples { + vol, err := s.Volumes().Get(domain.NewVolumeKey(ledgerName, t.account, t.asset, t.color)) if err != nil && !errors.Is(err, domain.ErrNotFound) { - continue + return nil, &domain.ErrStorageOperation{ + Operation: "buildPostCommitVolumes: loading volume", + Cause: err, + } } var inputStr, outputStr string @@ -60,21 +72,23 @@ func buildPostCommitVolumes(s Scope, ledgerName string, postings []*commonpb.Pos outputStr = "0" } - byAssets, ok := volumesByAccount[pair.account] + byAssets, ok := volumesByAccount[t.account] if !ok { - byAssets = &commonpb.VolumesByAssets{ - Volumes: make(map[string]*commonpb.Volumes), - } - volumesByAccount[pair.account] = byAssets - } - - byAssets.Volumes[pair.asset] = &commonpb.Volumes{ - Input: inputStr, - Output: outputStr, + byAssets = &commonpb.VolumesByAssets{} + volumesByAccount[t.account] = byAssets } + byAssets.Volumes = append(byAssets.Volumes, &commonpb.VolumeEntry{ + Asset: t.asset, + Color: t.color, + Volumes: &commonpb.Volumes{ + Input: inputStr, + Output: outputStr, + }, + }) } - return &commonpb.PostCommitVolumes{ - VolumesByAccount: volumesByAccount, - } + out := &commonpb.PostCommitVolumes{VolumesByAccount: volumesByAccount} + out.SortVolumes() + + return out, nil } diff --git a/internal/domain/reason.go b/internal/domain/reason.go index e6b06b3927..2827f1586f 100644 --- a/internal/domain/reason.go +++ b/internal/domain/reason.go @@ -91,6 +91,8 @@ func KindForReason(code commonpb.ErrorReason) ErrorKind { return KindConflict case commonpb.ErrorReason_ERROR_REASON_INSUFFICIENT_FUNDS, commonpb.ErrorReason_ERROR_REASON_VOLUME_OVERFLOW, + commonpb.ErrorReason_ERROR_REASON_AGGREGATE_OVERFLOW, + commonpb.ErrorReason_ERROR_REASON_BALANCE_NOT_FOUND, commonpb.ErrorReason_ERROR_REASON_AUDIT_DISABLED, commonpb.ErrorReason_ERROR_REASON_NO_CHAPTER_OPEN, commonpb.ErrorReason_ERROR_REASON_CHAPTER_NOT_CLOSING, diff --git a/internal/domain/replay/replay.go b/internal/domain/replay/replay.go index 1c2251b3a3..1605ed90cf 100644 --- a/internal/domain/replay/replay.go +++ b/internal/domain/replay/replay.go @@ -193,14 +193,17 @@ type pendingEphemeralPurge struct { postings []*commonpb.Posting } -// ExclusionCollector is called once per (ledger, account, asset) that the -// replay-time purge logic decides to delete from the replay store. The -// integrity checker uses it to derive its exclusion set independently of the +// ExclusionCollector is called once per (ledger, account, asset, color) that +// the replay-time purge logic decides to delete from the replay store. Color +// is part of the volume identity: two color buckets of the same (account, +// asset) can have different purge fates, and collapsing them would let +// EXCLUSION_RECORD_MISMATCH miss a real divergence. The integrity checker +// uses it to derive its exclusion set independently of the // AppliedProposal.TransientVolumes / LedgerLog.PurgedVolumes records — // neither is bound to the audit hash chain, so trusting them would let a // tampered store hide live mutations on otherwise-purged accounts. Other // replay consumers (backup rebuild) pass nil to discard. -type ExclusionCollector func(ledger, account, asset string) +type ExclusionCollector func(ledger, account, asset, color string) // EphemeralPurgeBuffer accumulates transaction postings until the caller reaches // the same proposal boundary used by the FSM's WriteSet.Merge(). @@ -311,6 +314,7 @@ func ApplyPostings( ) error { for _, posting := range postings { amount := posting.GetAmount().ToBigInt() + color := posting.GetColor() sourceKey := domain.VolumeKey{ AccountKey: domain.AccountKey{ @@ -318,6 +322,7 @@ func ApplyPostings( Account: posting.GetSource(), }, Asset: posting.GetAsset(), + Color: color, } if err := w.AddVolumeDelta(sourceKey.Bytes(), big.NewInt(0), amount); err != nil { @@ -330,6 +335,7 @@ func ApplyPostings( Account: posting.GetDestination(), }, Asset: posting.GetAsset(), + Color: color, } if err := w.AddVolumeDelta(destKey.Bytes(), amount, big.NewInt(0)); err != nil { @@ -386,6 +392,7 @@ func SimulateEphemeralPurge( vk := domain.VolumeKey{ AccountKey: domain.AccountKey{LedgerName: ledger, Account: addr}, Asset: p.GetAsset(), + Color: p.GetColor(), } pair, err := w.GetVolume(vk.Bytes()) @@ -405,7 +412,7 @@ func SimulateEphemeralPurge( return fmt.Errorf("deleting ephemeral volume: %w", err) } if collector != nil { - collector(ledger, addr, p.GetAsset()) + collector(ledger, addr, p.GetAsset(), p.GetColor()) } } } diff --git a/internal/domain/touched_volume.go b/internal/domain/touched_volume.go index 0a70c7bc07..7368bf158b 100644 --- a/internal/domain/touched_volume.go +++ b/internal/domain/touched_volume.go @@ -18,7 +18,7 @@ func TouchedVolumeSet(volumes []*commonpb.TouchedVolume) map[AccountAssetKey]str set := make(map[AccountAssetKey]struct{}, len(volumes)) for _, v := range volumes { - set[AccountAssetKey{Account: v.GetAccount(), Asset: v.GetAsset()}] = struct{}{} + set[AccountAssetKey{Account: v.GetAccount(), Asset: v.GetAsset(), Color: v.GetColor()}] = struct{}{} } return set diff --git a/internal/domain/validation.go b/internal/domain/validation.go index 30542aa112..ce4fde95c1 100644 --- a/internal/domain/validation.go +++ b/internal/domain/validation.go @@ -33,6 +33,12 @@ const maxSigningKeyIDLength = 256 // maxAccountAddressLength is the maximum allowed length for an account address. const maxAccountAddressLength = 1024 +// maxColorLength caps the optional Color tag on a Posting. Color is a small +// dimension label (e.g. "GRANTS", "OPS") not a free-form string; 32 bytes is +// large enough for any sensible tag and short enough to stay cheap to repeat +// inside every volume key. +const maxColorLength = 32 + // Storage-safety validation sentinels. All are Describable so they flow // through BusinessError with Kind=KindValidation. var ( @@ -56,6 +62,9 @@ var ( ErrAccountAddressTooLong = newValidationSentinel(fmt.Sprintf("account address exceeds maximum length of %d bytes", maxAccountAddressLength)) ErrAssetInvalid = newValidationSentinel("asset must match [A-Z][A-Z0-9]{0,16}(_[A-Z]{1,16})?(/[1-9][0-9]{0,2})? with precision in [1, 255]") + + ErrColorInvalid = newValidationSentinel("color must match ^[A-Z]*$ (uppercase letters only)") + ErrColorTooLong = newValidationSentinel(fmt.Sprintf("color exceeds maximum length of %d bytes", maxColorLength)) ) // isPrintableASCII reports whether every byte of s is in the printable ASCII @@ -259,6 +268,32 @@ func ValidateAsset(asset string) Describable { return nil } +// ValidateColor checks that a posting Color tag is safe for use in the +// canonical volume key encoding. The Color value is embedded raw between +// two 0x00 separators in `[ledgerID][account]\x00[color]\x00[asset_base][precision]` +// so any byte that aliases the separator or shifts the parser is a key-collision +// vector: two distinct (account, asset, color) tuples could fuse onto a single +// Pebble row and silently merge balances — the same class of bug as +// metadata keys (#322) and asset names (#303). +// +// The rule is ^[A-Z]*$: uppercase letters only. Empty is allowed (the +// "uncolored" bucket) but anything else must be uppercase ASCII. Length is +// capped to keep the key short. +func ValidateColor(color string) Describable { + if len(color) > maxColorLength { + return ErrColorTooLong + } + + for i := range len(color) { + c := color[i] + if c < 'A' || c > 'Z' { + return ErrColorInvalid + } + } + + return nil +} + // validateAssetPrecision enforces a canonical, uint8-safe precision suffix: // - 1 to 3 digits (max numeric value 255 fits in 3 chars). // - no leading zero (rejects "02" → 2 aliasing). diff --git a/internal/domain/validation_test.go b/internal/domain/validation_test.go index 3cdc10094b..bbbbf5c125 100644 --- a/internal/domain/validation_test.go +++ b/internal/domain/validation_test.go @@ -310,3 +310,38 @@ func TestValidateAsset_CanonicalRoundTrip(t *testing.T) { }) } } + +func TestValidateColor(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + wantErr error + }{ + {name: "empty is the uncolored bucket", input: ""}, + {name: "single letter", input: "R"}, + {name: "typical tag", input: "GRANTS"}, + {name: "max length (32)", input: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"}, + {name: "contains null byte", input: "A\x00B", wantErr: ErrColorInvalid}, + {name: "lowercase rejected", input: "red", wantErr: ErrColorInvalid}, + {name: "digit rejected", input: "RED2", wantErr: ErrColorInvalid}, + {name: "hyphen rejected", input: "R-G", wantErr: ErrColorInvalid}, + {name: "space rejected", input: "R G", wantErr: ErrColorInvalid}, + {name: "high byte rejected", input: "RÉD", wantErr: ErrColorInvalid}, + {name: "over max length (33)", input: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", wantErr: ErrColorTooLong}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + err := ValidateColor(tt.input) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/infra/receipt/receipt.go b/internal/infra/receipt/receipt.go index 65a58d438d..c92a88860a 100644 --- a/internal/infra/receipt/receipt.go +++ b/internal/infra/receipt/receipt.go @@ -22,11 +22,15 @@ func NewSigner(key []byte) *Signer { } // PostingClaim is the JSON representation of a posting inside a JWT. +// Color is part of the receipt-bound identity because balances are +// segregated per (account, asset, color); a receipt that did not bind +// the color would be verifiable against the wrong bucket on revert. type PostingClaim struct { Source string `json:"source"` Destination string `json:"destination"` Amount string `json:"amount"` Asset string `json:"asset"` + Color string `json:"color,omitempty"` } // Claims are the custom JWT claims for a transaction receipt. @@ -48,6 +52,7 @@ func (s *Signer) Sign(ledger string, txID uint64, postings []*commonpb.Posting, Destination: p.GetDestination(), Amount: p.GetAmount().Dec(), Asset: p.GetAsset(), + Color: p.GetColor(), } } @@ -117,6 +122,7 @@ func ClaimsToPostings(claims []PostingClaim) []*commonpb.Posting { Destination: c.Destination, Amount: commonpb.NewUint256(&v), Asset: c.Asset, + Color: c.Color, } } diff --git a/internal/infra/receipt/receipt_test.go b/internal/infra/receipt/receipt_test.go index 60cba92406..e5b5bb80e7 100644 --- a/internal/infra/receipt/receipt_test.go +++ b/internal/infra/receipt/receipt_test.go @@ -211,3 +211,43 @@ func TestSignWithNilTimestamp(t *testing.T) { require.NoError(t, err) require.Equal(t, uint64(0), claims.ChapterID) } + +// TestSignBindsColor pins the receipt-vs-color contract: two postings +// identical in (source, destination, asset, amount) but differing in +// Color must produce distinct signatures, and the verified claims must +// expose the Color so the revert path can target the correct bucket. +func TestSignBindsColor(t *testing.T) { + t.Parallel() + + signer := NewSigner([]byte("test-secret-key-for-color-binding!!")) + + makePosting := func(color string) *commonpb.Posting { + return &commonpb.Posting{ + Source: "alice", + Destination: "bob", + Amount: commonpb.NewUint256FromUint64(100), + Asset: "USD/2", + Color: color, + } + } + + grantsTok, err := signer.Sign("ledger", 1, []*commonpb.Posting{makePosting("GRANTS")}, nil, 0) + require.NoError(t, err) + + opsTok, err := signer.Sign("ledger", 1, []*commonpb.Posting{makePosting("OPS")}, nil, 0) + require.NoError(t, err) + + require.NotEqual(t, grantsTok, opsTok, + "two postings differing only by Color must produce distinct signed receipts") + + grantsClaims, err := signer.Verify(grantsTok) + require.NoError(t, err) + require.Len(t, grantsClaims.Postings, 1) + require.Equal(t, "GRANTS", grantsClaims.Postings[0].Color) + + // ClaimsToPostings must round-trip the color so the revert path can + // target the same bucket the original transaction touched. + roundTripped := ClaimsToPostings(grantsClaims.Postings) + require.Len(t, roundTripped, 1) + require.Equal(t, "GRANTS", roundTripped[0].GetColor()) +} diff --git a/internal/infra/state/sentinel.go b/internal/infra/state/sentinel.go index 6424a1f6d9..4dbce8a088 100644 --- a/internal/infra/state/sentinel.go +++ b/internal/infra/state/sentinel.go @@ -288,8 +288,8 @@ func verifyVolumeDeltasMatchPostings( for _, posting := range postings { amount := posting.GetAmount().ToBigInt() - srcKey := domain.NewVolumeKey(ledgerName, posting.GetSource(), posting.GetAsset()) - dstKey := domain.NewVolumeKey(ledgerName, posting.GetDestination(), posting.GetAsset()) + srcKey := domain.NewVolumeKey(ledgerName, posting.GetSource(), posting.GetAsset(), posting.GetColor()) + dstKey := domain.NewVolumeKey(ledgerName, posting.GetDestination(), posting.GetAsset(), posting.GetColor()) if _, ok := expected[srcKey]; !ok { expected[srcKey] = &delta{input: big.NewInt(0), output: big.NewInt(0)} diff --git a/internal/infra/state/write_set.go b/internal/infra/state/write_set.go index 92ee5cedd6..ff61486b4b 100644 --- a/internal/infra/state/write_set.go +++ b/internal/infra/state/write_set.go @@ -1620,25 +1620,27 @@ func (b *WriteSet) PurgedVolumeKeys() []domain.VolumeKey { return b.purgedVolumeKeys } -// TransientVolumes returns the unique transient (account, asset) volumes -// per ledger, collected during Merge from the transient volume partition. +// TransientVolumes returns the unique transient (account, asset, color) +// volumes per ledger, collected during Merge from the transient volume +// partition. func (b *WriteSet) TransientVolumes() map[string][]*commonpb.TouchedVolume { return b.transientVolumes } -// collectUniqueVolumes extracts unique (account, asset) tuples per ledger -// from volume updates and emits them as deterministically-ordered -// commonpb.TouchedVolume slices. +// collectUniqueVolumes extracts unique (account, asset, color) tuples per +// ledger from volume updates and emits them as deterministically-ordered +// commonpb.TouchedVolume slices. Color is part of the identity so two color +// buckets of the same (account, asset) stay distinct in the audit log. func collectUniqueVolumes(updates []attributes.Update[domain.VolumeKey, *raftcmdpb.VolumePair]) map[string][]*commonpb.TouchedVolume { - type accAsset struct{ Account, Asset string } - seen := make(map[string]map[accAsset]struct{}) + type accAssetColor struct{ Account, Asset, Color string } + seen := make(map[string]map[accAssetColor]struct{}) for _, update := range updates { ledgerName := update.Key.LedgerName - k := accAsset{Account: update.Key.Account, Asset: update.Key.Asset} + k := accAssetColor{Account: update.Key.Account, Asset: update.Key.Asset, Color: update.Key.Color} if seen[ledgerName] == nil { - seen[ledgerName] = make(map[accAsset]struct{}) + seen[ledgerName] = make(map[accAssetColor]struct{}) } seen[ledgerName][k] = struct{}{} @@ -1646,7 +1648,7 @@ func collectUniqueVolumes(updates []attributes.Update[domain.VolumeKey, *raftcmd result := make(map[string][]*commonpb.TouchedVolume, len(seen)) for ledgerName, vols := range seen { - list := make([]accAsset, 0, len(vols)) + list := make([]accAssetColor, 0, len(vols)) for k := range vols { list = append(list, k) } @@ -1655,13 +1657,16 @@ func collectUniqueVolumes(updates []attributes.Update[domain.VolumeKey, *raftcmd if list[a].Account != list[b].Account { return list[a].Account < list[b].Account } + if list[a].Asset != list[b].Asset { + return list[a].Asset < list[b].Asset + } - return list[a].Asset < list[b].Asset + return list[a].Color < list[b].Color }) out := make([]*commonpb.TouchedVolume, len(list)) for i, k := range list { - out[i] = &commonpb.TouchedVolume{Account: k.Account, Asset: k.Asset} + out[i] = &commonpb.TouchedVolume{Account: k.Account, Asset: k.Asset, Color: k.Color} } result[ledgerName] = out diff --git a/internal/infra/state/write_set_ephemeral_purge.go b/internal/infra/state/write_set_ephemeral_purge.go index c74ff56fad..15bb0b38fe 100644 --- a/internal/infra/state/write_set_ephemeral_purge.go +++ b/internal/infra/state/write_set_ephemeral_purge.go @@ -117,44 +117,46 @@ func (b *WriteSet) partitionVolumes( return result } -// makePurgedKeySet builds a lookup set over the (ledger, account, asset) -// of every purged volume entry. Keeping the asset dimension matters: a -// multi-asset account may have one asset purged while another stays kept — -// dropping the asset would over-attribute purged state to orders touching -// the still-kept asset. +// makePurgedKeySet builds a lookup set over the (ledger, account, asset, color) +// of every purged volume entry. Keeping both asset and color dimensions +// matters: a multi-bucket account may have one (asset, color) purged while +// another stays kept — dropping either would over-attribute purged state to +// orders touching the still-kept bucket. func makePurgedKeySet(purged []attributes.Update[domain.VolumeKey, *raftcmdpb.VolumePair]) map[purgedVolumeKey]struct{} { if len(purged) == 0 { return nil } set := make(map[purgedVolumeKey]struct{}, len(purged)) for _, u := range purged { - set[purgedVolumeKey{Ledger: u.Key.LedgerName, Account: u.Key.Account, Asset: u.Key.Asset}] = struct{}{} + set[purgedVolumeKey{Ledger: u.Key.LedgerName, Account: u.Key.Account, Asset: u.Key.Asset, Color: u.Key.Color}] = struct{}{} } return set } -// purgedVolumeKey is the (ledger, account, asset) tuple used by -// makePurgedKeySet and buildPurgedByLog. The asset dimension is kept to -// avoid over-attribution in multi-asset accounts. +// purgedVolumeKey is the (ledger, account, asset, color) tuple used by +// makePurgedKeySet and buildPurgedByLog. Both dimensions are kept to avoid +// over-attribution in multi-bucket accounts: two color buckets of the same +// (account, asset) can have different purge fates. type purgedVolumeKey struct { Ledger string Account string Asset string + Color string } // buildPurgedByLog produces, for each order index, the deduplicated list of -// (account, asset) tuples that the order touched and that the proposal +// (account, asset, color) tuples that the order touched and that the proposal // classified as purged. Indexed by order_index; entries for orders that // touched nothing purged (or didn't touch volumes at all) are nil. Tuples -// within an entry are sorted (by account then asset) to keep the log payload -// deterministic across runs. +// within an entry are sorted (by account, asset, color) to keep the log +// payload deterministic across runs. func buildPurgedByLog(perOrderVolumeKeys [][]domain.VolumeKey, purged map[purgedVolumeKey]struct{}) [][]*commonpb.TouchedVolume { if len(perOrderVolumeKeys) == 0 || len(purged) == 0 { return nil } - type accAsset struct{ Account, Asset string } + type accAssetColor struct{ Account, Asset, Color string } out := make([][]*commonpb.TouchedVolume, len(perOrderVolumeKeys)) for i, keys := range perOrderVolumeKeys { @@ -162,19 +164,19 @@ func buildPurgedByLog(perOrderVolumeKeys [][]domain.VolumeKey, purged map[purged continue } - seen := make(map[accAsset]struct{}, len(keys)) + seen := make(map[accAssetColor]struct{}, len(keys)) for _, k := range keys { - if _, ok := purged[purgedVolumeKey{Ledger: k.LedgerName, Account: k.Account, Asset: k.Asset}]; !ok { + if _, ok := purged[purgedVolumeKey{Ledger: k.LedgerName, Account: k.Account, Asset: k.Asset, Color: k.Color}]; !ok { continue } - seen[accAsset{Account: k.Account, Asset: k.Asset}] = struct{}{} + seen[accAssetColor{Account: k.Account, Asset: k.Asset, Color: k.Color}] = struct{}{} } if len(seen) == 0 { continue } - ordered := make([]accAsset, 0, len(seen)) + ordered := make([]accAssetColor, 0, len(seen)) for k := range seen { ordered = append(ordered, k) } @@ -182,13 +184,16 @@ func buildPurgedByLog(perOrderVolumeKeys [][]domain.VolumeKey, purged map[purged if ordered[a].Account != ordered[b].Account { return ordered[a].Account < ordered[b].Account } + if ordered[a].Asset != ordered[b].Asset { + return ordered[a].Asset < ordered[b].Asset + } - return ordered[a].Asset < ordered[b].Asset + return ordered[a].Color < ordered[b].Color }) vols := make([]*commonpb.TouchedVolume, len(ordered)) for j, k := range ordered { - vols[j] = &commonpb.TouchedVolume{Account: k.Account, Asset: k.Asset} + vols[j] = &commonpb.TouchedVolume{Account: k.Account, Asset: k.Asset, Color: k.Color} } out[i] = vols } diff --git a/internal/proto/commonpb/account.go b/internal/proto/commonpb/account.go new file mode 100644 index 0000000000..03d91d2222 --- /dev/null +++ b/internal/proto/commonpb/account.go @@ -0,0 +1,45 @@ +package commonpb + +import ( + "github.com/formancehq/ledger/v3/internal/adapter/json" +) + +// MarshalJSON implements json.Marshaler for AccountVolume. Color is always +// emitted (even when empty) so REST clients can distinguish the uncolored +// bucket from an older response shape — same contract as VolumeEntry, +// Posting, and the accountVolumeJSON shim used by Account.MarshalJSON. +// +// Account.MarshalJSON already builds accountVolumeJSON entries by hand, so +// direct serialization through that path is safe. This method covers +// any other call site (gRPC-Gateway, ad-hoc marshaling of a single row) +// that touches *AccountVolume directly. +func (x *AccountVolume) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Asset string `json:"asset"` + Color string `json:"color"` + Volumes *VolumesWithBalance `json:"volumes,omitempty"` + }{ + Asset: x.GetAsset(), + Color: x.GetColor(), + Volumes: x.GetVolumes(), + }) +} + +// FindVolume returns the VolumesWithBalance for a given (asset, color) tuple +// on this account, or nil if absent. Color "" is the uncolored bucket. +// +// Account.Volumes is a sorted list, so this is an O(n) linear scan. For +// repeated lookups, callers should build their own map. This helper exists +// to keep direct lookups ergonomic in tests and CLI rendering. +func (a *Account) FindVolume(asset, color string) *VolumesWithBalance { + if a == nil { + return nil + } + for _, entry := range a.GetVolumes() { + if entry.GetAsset() == asset && entry.GetColor() == color { + return entry.GetVolumes() + } + } + + return nil +} diff --git a/internal/proto/commonpb/account_json_test.go b/internal/proto/commonpb/account_json_test.go new file mode 100644 index 0000000000..4a5e7ef612 --- /dev/null +++ b/internal/proto/commonpb/account_json_test.go @@ -0,0 +1,33 @@ +package commonpb + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestAccountVolume_MarshalJSON_EmitsEmptyColor guards the contract that +// REST responses serializing AccountVolume directly (e.g. via gRPC-Gateway +// or ad-hoc marshaling) emit color:"" for the uncolored bucket rather +// than dropping the field via the generated omitempty tag. +func TestAccountVolume_MarshalJSON_EmitsEmptyColor(t *testing.T) { + t.Parallel() + + av := &AccountVolume{Asset: "USD/2"} + + data, err := json.Marshal(av) + require.NoError(t, err) + require.Contains(t, string(data), `"color":""`, + "uncolored AccountVolume rows must surface color:\"\" rather than dropping the field") +} + +func TestAccountVolume_MarshalJSON_EmitsColor(t *testing.T) { + t.Parallel() + + av := &AccountVolume{Asset: "USD/2", Color: "GRANTS"} + + data, err := json.Marshal(av) + require.NoError(t, err) + require.Contains(t, string(data), `"color":"GRANTS"`) +} diff --git a/internal/proto/commonpb/common.pb.go b/internal/proto/commonpb/common.pb.go index 15b83304ca..1469343456 100644 --- a/internal/proto/commonpb/common.pb.go +++ b/internal/proto/commonpb/common.pb.go @@ -74,46 +74,43 @@ func (TargetType) EnumDescriptor() ([]byte, []int) { type MetadataType int32 const ( - MetadataType_METADATA_TYPE_STRING MetadataType = 0 - MetadataType_METADATA_TYPE_INT64 MetadataType = 1 - MetadataType_METADATA_TYPE_BOOL MetadataType = 2 - MetadataType_METADATA_TYPE_UINT64 MetadataType = 3 - MetadataType_METADATA_TYPE_INT8 MetadataType = 4 - MetadataType_METADATA_TYPE_INT16 MetadataType = 5 - MetadataType_METADATA_TYPE_INT32 MetadataType = 6 - MetadataType_METADATA_TYPE_UINT8 MetadataType = 7 - MetadataType_METADATA_TYPE_UINT16 MetadataType = 8 - MetadataType_METADATA_TYPE_UINT32 MetadataType = 9 - MetadataType_METADATA_TYPE_DATETIME MetadataType = 10 + MetadataType_METADATA_TYPE_STRING MetadataType = 0 + MetadataType_METADATA_TYPE_INT64 MetadataType = 1 + MetadataType_METADATA_TYPE_BOOL MetadataType = 2 + MetadataType_METADATA_TYPE_UINT64 MetadataType = 3 + MetadataType_METADATA_TYPE_INT8 MetadataType = 4 + MetadataType_METADATA_TYPE_INT16 MetadataType = 5 + MetadataType_METADATA_TYPE_INT32 MetadataType = 6 + MetadataType_METADATA_TYPE_UINT8 MetadataType = 7 + MetadataType_METADATA_TYPE_UINT16 MetadataType = 8 + MetadataType_METADATA_TYPE_UINT32 MetadataType = 9 ) // Enum value maps for MetadataType. var ( MetadataType_name = map[int32]string{ - 0: "METADATA_TYPE_STRING", - 1: "METADATA_TYPE_INT64", - 2: "METADATA_TYPE_BOOL", - 3: "METADATA_TYPE_UINT64", - 4: "METADATA_TYPE_INT8", - 5: "METADATA_TYPE_INT16", - 6: "METADATA_TYPE_INT32", - 7: "METADATA_TYPE_UINT8", - 8: "METADATA_TYPE_UINT16", - 9: "METADATA_TYPE_UINT32", - 10: "METADATA_TYPE_DATETIME", + 0: "METADATA_TYPE_STRING", + 1: "METADATA_TYPE_INT64", + 2: "METADATA_TYPE_BOOL", + 3: "METADATA_TYPE_UINT64", + 4: "METADATA_TYPE_INT8", + 5: "METADATA_TYPE_INT16", + 6: "METADATA_TYPE_INT32", + 7: "METADATA_TYPE_UINT8", + 8: "METADATA_TYPE_UINT16", + 9: "METADATA_TYPE_UINT32", } MetadataType_value = map[string]int32{ - "METADATA_TYPE_STRING": 0, - "METADATA_TYPE_INT64": 1, - "METADATA_TYPE_BOOL": 2, - "METADATA_TYPE_UINT64": 3, - "METADATA_TYPE_INT8": 4, - "METADATA_TYPE_INT16": 5, - "METADATA_TYPE_INT32": 6, - "METADATA_TYPE_UINT8": 7, - "METADATA_TYPE_UINT16": 8, - "METADATA_TYPE_UINT32": 9, - "METADATA_TYPE_DATETIME": 10, + "METADATA_TYPE_STRING": 0, + "METADATA_TYPE_INT64": 1, + "METADATA_TYPE_BOOL": 2, + "METADATA_TYPE_UINT64": 3, + "METADATA_TYPE_INT8": 4, + "METADATA_TYPE_INT16": 5, + "METADATA_TYPE_INT32": 6, + "METADATA_TYPE_UINT8": 7, + "METADATA_TYPE_UINT16": 8, + "METADATA_TYPE_UINT32": 9, } ) @@ -260,18 +257,15 @@ type AccountBuiltinIndex int32 const ( AccountBuiltinIndex_ACCT_BUILTIN_INDEX_UNSPECIFIED AccountBuiltinIndex = 0 - AccountBuiltinIndex_ACCT_BUILTIN_INDEX_ASSET AccountBuiltinIndex = 1 ) // Enum value maps for AccountBuiltinIndex. var ( AccountBuiltinIndex_name = map[int32]string{ 0: "ACCT_BUILTIN_INDEX_UNSPECIFIED", - 1: "ACCT_BUILTIN_INDEX_ASSET", } AccountBuiltinIndex_value = map[string]int32{ "ACCT_BUILTIN_INDEX_UNSPECIFIED": 0, - "ACCT_BUILTIN_INDEX_ASSET": 1, } ) @@ -681,6 +675,8 @@ const ( ErrorReason_ERROR_REASON_CLUSTER_UNHEALTHY ErrorReason = 60 ErrorReason_ERROR_REASON_WRITES_BLOCKED_DISK_FULL ErrorReason = 61 ErrorReason_ERROR_REASON_WRITES_BLOCKED_CLOCK_SKEW ErrorReason = 62 + ErrorReason_ERROR_REASON_AGGREGATE_OVERFLOW ErrorReason = 63 + ErrorReason_ERROR_REASON_BALANCE_NOT_FOUND ErrorReason = 64 ) // Enum value maps for ErrorReason. @@ -749,6 +745,8 @@ var ( 60: "ERROR_REASON_CLUSTER_UNHEALTHY", 61: "ERROR_REASON_WRITES_BLOCKED_DISK_FULL", 62: "ERROR_REASON_WRITES_BLOCKED_CLOCK_SKEW", + 63: "ERROR_REASON_AGGREGATE_OVERFLOW", + 64: "ERROR_REASON_BALANCE_NOT_FOUND", } ErrorReason_value = map[string]int32{ "ERROR_REASON_UNSPECIFIED": 0, @@ -814,6 +812,8 @@ var ( "ERROR_REASON_CLUSTER_UNHEALTHY": 60, "ERROR_REASON_WRITES_BLOCKED_DISK_FULL": 61, "ERROR_REASON_WRITES_BLOCKED_CLOCK_SKEW": 62, + "ERROR_REASON_AGGREGATE_OVERFLOW": 63, + "ERROR_REASON_BALANCE_NOT_FOUND": 64, } ) @@ -1185,7 +1185,6 @@ type MetadataValue struct { // *MetadataValue_BoolValue // *MetadataValue_NullValue // *MetadataValue_UintValue - // *MetadataValue_DatetimeValue Type isMetadataValue_Type `protobuf_oneof:"type"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1273,15 +1272,6 @@ func (x *MetadataValue) GetUintValue() uint64 { return 0 } -func (x *MetadataValue) GetDatetimeValue() int64 { - if x != nil { - if x, ok := x.Type.(*MetadataValue_DatetimeValue); ok { - return x.DatetimeValue - } - } - return 0 -} - type isMetadataValue_Type interface { isMetadataValue_Type() } @@ -1306,10 +1296,6 @@ type MetadataValue_UintValue struct { UintValue uint64 `protobuf:"varint,5,opt,name=uint_value,json=uintValue,proto3,oneof"` } -type MetadataValue_DatetimeValue struct { - DatetimeValue int64 `protobuf:"varint,6,opt,name=datetime_value,json=datetimeValue,proto3,oneof"` // microseconds since the Unix epoch (signed; pre-1970 allowed) -} - func (*MetadataValue_StringValue) isMetadataValue_Type() {} func (*MetadataValue_IntValue) isMetadataValue_Type() {} @@ -1320,8 +1306,6 @@ func (*MetadataValue_NullValue) isMetadataValue_Type() {} func (*MetadataValue_UintValue) isMetadataValue_Type() {} -func (*MetadataValue_DatetimeValue) isMetadataValue_Type() {} - // MetadataMap wraps a metadata map for use as a proto map value // (proto3 does not support nested maps like map>). type MetadataMap struct { @@ -1553,11 +1537,15 @@ func (x *Uint256) GetV3() uint64 { // Posting represents a single posting in a transaction type Posting struct { - state protoimpl.MessageState `protogen:"open.v1"` - Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` - Destination string `protobuf:"bytes,2,opt,name=destination,proto3" json:"destination,omitempty"` - Amount *Uint256 `protobuf:"bytes,3,opt,name=amount,proto3" json:"amount,omitempty"` - Asset string `protobuf:"bytes,4,opt,name=asset,proto3" json:"asset,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + Destination string `protobuf:"bytes,2,opt,name=destination,proto3" json:"destination,omitempty"` + Amount *Uint256 `protobuf:"bytes,3,opt,name=amount,proto3" json:"amount,omitempty"` + Asset string `protobuf:"bytes,4,opt,name=asset,proto3" json:"asset,omitempty"` + // Color of the funds being moved. The empty string is the "uncolored" bucket + // and is treated as just another color from a segregation standpoint. + // Color values are constrained to ^[A-Z]*$ at admission time. + Color string `protobuf:"bytes,5,opt,name=color,proto3" json:"color,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1620,6 +1608,13 @@ func (x *Posting) GetAsset() string { return "" } +func (x *Posting) GetColor() string { + if x != nil { + return x.Color + } + return "" +} + // Transaction represents a transaction type Transaction struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1903,10 +1898,12 @@ func (x *VolumesWithBalance) GetBalance() string { return "" } -// VolumesByAssets represents volumes grouped by asset +// VolumesByAssets is a sorted list of post-commit (asset, color) volume +// entries for a single account. Sorted by (asset, color) ascending so the +// serialization is deterministic and stable across reads. type VolumesByAssets struct { state protoimpl.MessageState `protogen:"open.v1"` - Volumes map[string]*Volumes `protobuf:"bytes,1,rep,name=volumes,proto3" json:"volumes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Volumes []*VolumeEntry `protobuf:"bytes,1,rep,name=volumes,proto3" json:"volumes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1941,14 +1938,77 @@ func (*VolumesByAssets) Descriptor() ([]byte, []int) { return file_common_proto_rawDescGZIP(), []int{11} } -func (x *VolumesByAssets) GetVolumes() map[string]*Volumes { +func (x *VolumesByAssets) GetVolumes() []*VolumeEntry { + if x != nil { + return x.Volumes + } + return nil +} + +// VolumeEntry is one (asset, color) row inside VolumesByAssets. The empty +// color is the uncolored bucket and is itself just another segregated bucket. +type VolumeEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Asset string `protobuf:"bytes,1,opt,name=asset,proto3" json:"asset,omitempty"` + Color string `protobuf:"bytes,2,opt,name=color,proto3" json:"color,omitempty"` + Volumes *Volumes `protobuf:"bytes,3,opt,name=volumes,proto3" json:"volumes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *VolumeEntry) Reset() { + *x = VolumeEntry{} + mi := &file_common_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *VolumeEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*VolumeEntry) ProtoMessage() {} + +func (x *VolumeEntry) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use VolumeEntry.ProtoReflect.Descriptor instead. +func (*VolumeEntry) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{12} +} + +func (x *VolumeEntry) GetAsset() string { + if x != nil { + return x.Asset + } + return "" +} + +func (x *VolumeEntry) GetColor() string { + if x != nil { + return x.Color + } + return "" +} + +func (x *VolumeEntry) GetVolumes() *Volumes { if x != nil { return x.Volumes } return nil } -// PostCommitVolumes represents volumes after commit, grouped by account and asset +// PostCommitVolumes represents volumes after commit, grouped by account. +// Within each account, entries are flat-listed per (asset, color) tuple. type PostCommitVolumes struct { state protoimpl.MessageState `protogen:"open.v1"` VolumesByAccount map[string]*VolumesByAssets `protobuf:"bytes,1,rep,name=volumes_by_account,json=volumesByAccount,proto3" json:"volumes_by_account,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` @@ -1958,7 +2018,7 @@ type PostCommitVolumes struct { func (x *PostCommitVolumes) Reset() { *x = PostCommitVolumes{} - mi := &file_common_proto_msgTypes[12] + mi := &file_common_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1970,7 +2030,7 @@ func (x *PostCommitVolumes) String() string { func (*PostCommitVolumes) ProtoMessage() {} func (x *PostCommitVolumes) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[12] + mi := &file_common_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1983,7 +2043,7 @@ func (x *PostCommitVolumes) ProtoReflect() protoreflect.Message { // Deprecated: Use PostCommitVolumes.ProtoReflect.Descriptor instead. func (*PostCommitVolumes) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{12} + return file_common_proto_rawDescGZIP(), []int{13} } func (x *PostCommitVolumes) GetVolumesByAccount() map[string]*VolumesByAssets { @@ -1993,22 +2053,87 @@ func (x *PostCommitVolumes) GetVolumesByAccount() map[string]*VolumesByAssets { return nil } -// Account represents an account in the ledger +// AccountVolume is one (asset, color) row in Account.volumes. +type AccountVolume struct { + state protoimpl.MessageState `protogen:"open.v1"` + Asset string `protobuf:"bytes,1,opt,name=asset,proto3" json:"asset,omitempty"` + Color string `protobuf:"bytes,2,opt,name=color,proto3" json:"color,omitempty"` + Volumes *VolumesWithBalance `protobuf:"bytes,3,opt,name=volumes,proto3" json:"volumes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccountVolume) Reset() { + *x = AccountVolume{} + mi := &file_common_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccountVolume) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccountVolume) ProtoMessage() {} + +func (x *AccountVolume) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccountVolume.ProtoReflect.Descriptor instead. +func (*AccountVolume) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{14} +} + +func (x *AccountVolume) GetAsset() string { + if x != nil { + return x.Asset + } + return "" +} + +func (x *AccountVolume) GetColor() string { + if x != nil { + return x.Color + } + return "" +} + +func (x *AccountVolume) GetVolumes() *VolumesWithBalance { + if x != nil { + return x.Volumes + } + return nil +} + +// Account represents an account in the ledger. type Account struct { - state protoimpl.MessageState `protogen:"open.v1"` - Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` - Metadata map[string]*MetadataValue `protobuf:"bytes,2,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - FirstUsage *Timestamp `protobuf:"bytes,3,opt,name=first_usage,json=firstUsage,proto3" json:"first_usage,omitempty"` - InsertionDate *Timestamp `protobuf:"bytes,4,opt,name=insertion_date,json=insertionDate,proto3" json:"insertion_date,omitempty"` - UpdatedAt *Timestamp `protobuf:"bytes,5,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` - Volumes map[string]*VolumesWithBalance `protobuf:"bytes,6,rep,name=volumes,proto3" json:"volumes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Volumes per asset + state protoimpl.MessageState `protogen:"open.v1"` + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` + Metadata map[string]*MetadataValue `protobuf:"bytes,2,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + FirstUsage *Timestamp `protobuf:"bytes,3,opt,name=first_usage,json=firstUsage,proto3" json:"first_usage,omitempty"` + InsertionDate *Timestamp `protobuf:"bytes,4,opt,name=insertion_date,json=insertionDate,proto3" json:"insertion_date,omitempty"` + UpdatedAt *Timestamp `protobuf:"bytes,5,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"` + // Volumes is a sorted list of per (asset, color) entries. + // Sorted by (asset, color) ascending for stable serialization. + // When the request opts into collapse_colors, the list collapses to one + // entry per asset with color = "" and amounts summed across colors. + Volumes []*AccountVolume `protobuf:"bytes,6,rep,name=volumes,proto3" json:"volumes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *Account) Reset() { *x = Account{} - mi := &file_common_proto_msgTypes[13] + mi := &file_common_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2020,7 +2145,7 @@ func (x *Account) String() string { func (*Account) ProtoMessage() {} func (x *Account) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[13] + mi := &file_common_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2033,7 +2158,7 @@ func (x *Account) ProtoReflect() protoreflect.Message { // Deprecated: Use Account.ProtoReflect.Descriptor instead. func (*Account) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{13} + return file_common_proto_rawDescGZIP(), []int{15} } func (x *Account) GetAddress() string { @@ -2071,7 +2196,7 @@ func (x *Account) GetUpdatedAt() *Timestamp { return nil } -func (x *Account) GetVolumes() map[string]*VolumesWithBalance { +func (x *Account) GetVolumes() []*AccountVolume { if x != nil { return x.Volumes } @@ -2087,7 +2212,7 @@ type TargetAccount struct { func (x *TargetAccount) Reset() { *x = TargetAccount{} - mi := &file_common_proto_msgTypes[14] + mi := &file_common_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2099,7 +2224,7 @@ func (x *TargetAccount) String() string { func (*TargetAccount) ProtoMessage() {} func (x *TargetAccount) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[14] + mi := &file_common_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2112,7 +2237,7 @@ func (x *TargetAccount) ProtoReflect() protoreflect.Message { // Deprecated: Use TargetAccount.ProtoReflect.Descriptor instead. func (*TargetAccount) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{14} + return file_common_proto_rawDescGZIP(), []int{16} } func (x *TargetAccount) GetAddr() string { @@ -2135,7 +2260,7 @@ type Target struct { func (x *Target) Reset() { *x = Target{} - mi := &file_common_proto_msgTypes[15] + mi := &file_common_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2147,7 +2272,7 @@ func (x *Target) String() string { func (*Target) ProtoMessage() {} func (x *Target) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[15] + mi := &file_common_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2160,7 +2285,7 @@ func (x *Target) ProtoReflect() protoreflect.Message { // Deprecated: Use Target.ProtoReflect.Descriptor instead. func (*Target) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{15} + return file_common_proto_rawDescGZIP(), []int{17} } func (x *Target) GetTarget() isTarget_Target { @@ -2213,7 +2338,7 @@ type MetadataFieldSchema struct { func (x *MetadataFieldSchema) Reset() { *x = MetadataFieldSchema{} - mi := &file_common_proto_msgTypes[16] + mi := &file_common_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2225,7 +2350,7 @@ func (x *MetadataFieldSchema) String() string { func (*MetadataFieldSchema) ProtoMessage() {} func (x *MetadataFieldSchema) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[16] + mi := &file_common_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2238,7 +2363,7 @@ func (x *MetadataFieldSchema) ProtoReflect() protoreflect.Message { // Deprecated: Use MetadataFieldSchema.ProtoReflect.Descriptor instead. func (*MetadataFieldSchema) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{16} + return file_common_proto_rawDescGZIP(), []int{18} } func (x *MetadataFieldSchema) GetType() MetadataType { @@ -2259,7 +2384,7 @@ type MetadataSchema struct { func (x *MetadataSchema) Reset() { *x = MetadataSchema{} - mi := &file_common_proto_msgTypes[17] + mi := &file_common_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2271,7 +2396,7 @@ func (x *MetadataSchema) String() string { func (*MetadataSchema) ProtoMessage() {} func (x *MetadataSchema) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[17] + mi := &file_common_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2284,7 +2409,7 @@ func (x *MetadataSchema) ProtoReflect() protoreflect.Message { // Deprecated: Use MetadataSchema.ProtoReflect.Descriptor instead. func (*MetadataSchema) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{17} + return file_common_proto_rawDescGZIP(), []int{19} } func (x *MetadataSchema) GetAccountFields() map[string]*MetadataFieldSchema { @@ -2319,7 +2444,7 @@ type SetMetadataFieldTypeCommand struct { func (x *SetMetadataFieldTypeCommand) Reset() { *x = SetMetadataFieldTypeCommand{} - mi := &file_common_proto_msgTypes[18] + mi := &file_common_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2331,7 +2456,7 @@ func (x *SetMetadataFieldTypeCommand) String() string { func (*SetMetadataFieldTypeCommand) ProtoMessage() {} func (x *SetMetadataFieldTypeCommand) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[18] + mi := &file_common_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2344,7 +2469,7 @@ func (x *SetMetadataFieldTypeCommand) ProtoReflect() protoreflect.Message { // Deprecated: Use SetMetadataFieldTypeCommand.ProtoReflect.Descriptor instead. func (*SetMetadataFieldTypeCommand) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{18} + return file_common_proto_rawDescGZIP(), []int{20} } func (x *SetMetadataFieldTypeCommand) GetTargetType() TargetType { @@ -2379,7 +2504,7 @@ type MetadataIndexID struct { func (x *MetadataIndexID) Reset() { *x = MetadataIndexID{} - mi := &file_common_proto_msgTypes[19] + mi := &file_common_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2391,7 +2516,7 @@ func (x *MetadataIndexID) String() string { func (*MetadataIndexID) ProtoMessage() {} func (x *MetadataIndexID) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[19] + mi := &file_common_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2404,7 +2529,7 @@ func (x *MetadataIndexID) ProtoReflect() protoreflect.Message { // Deprecated: Use MetadataIndexID.ProtoReflect.Descriptor instead. func (*MetadataIndexID) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{19} + return file_common_proto_rawDescGZIP(), []int{21} } func (x *MetadataIndexID) GetTarget() TargetType { @@ -2439,7 +2564,7 @@ type IndexID struct { func (x *IndexID) Reset() { *x = IndexID{} - mi := &file_common_proto_msgTypes[20] + mi := &file_common_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2451,7 +2576,7 @@ func (x *IndexID) String() string { func (*IndexID) ProtoMessage() {} func (x *IndexID) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[20] + mi := &file_common_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2464,7 +2589,7 @@ func (x *IndexID) ProtoReflect() protoreflect.Message { // Deprecated: Use IndexID.ProtoReflect.Descriptor instead. func (*IndexID) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{20} + return file_common_proto_rawDescGZIP(), []int{22} } func (x *IndexID) GetKind() isIndexID_Kind { @@ -2538,43 +2663,22 @@ func (*IndexID_AccountBuiltin) isIndexID_Kind() {} func (*IndexID_Metadata) isIndexID_Kind() {} -// Index is the first-class representation of an index in the bucket-scoped -// index registry. Holds its identifier, scope, build state and audit metadata. -// -// Scope: -// - ledger == "" → bucket-scoped (e.g. audit indexes), single entry across the bucket. -// - ledger == "X" → ledger-scoped, one entry per (ledger, IndexID) tuple. -// -// Entries live in the SubAttrIndex attribute zone, not in LedgerInfo. +// Index is the first-class representation of an index on a ledger. +// Holds its identifier plus build state and audit metadata. type Index struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id *IndexID `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - // Informational: tracks whether the FSM has triggered a rebuild - // (e.g. CreateIndex or SetMetadataFieldType). NOT consulted by the - // query path — see forward_encoding_version below. - BuildStatus IndexBuildStatus `protobuf:"varint,2,opt,name=build_status,json=buildStatus,proto3,enum=common.IndexBuildStatus" json:"build_status,omitempty"` - CreatedAt *Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - LastBuiltAt *Timestamp `protobuf:"bytes,4,opt,name=last_built_at,json=lastBuiltAt,proto3" json:"last_built_at,omitempty"` - LastError string `protobuf:"bytes,5,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` - Ledger string `protobuf:"bytes,6,opt,name=ledger,proto3" json:"ledger,omitempty"` // empty for bucket-scoped indexes - // Cluster-wide forward-encoding version. Bumped at every event that - // requires the indexer to rewrite its forward index (CreateIndex, - // SetMetadataFieldType). The per-replica local view of this value - // lives in `readstore.IndexVersionState.CurrentVersion` (internal) - // and is exposed on the wire as `IndexEntry.current_version` on - // `GetIndexStatusResponse`. Queries read from the replica's local - // current_version, not from this cluster-wide field. Synchronization - // is client-driven via min_log_sequence on the read API — but note - // that min_log_sequence pins log application on this replica, NOT - // local rewrite completion; see api-comparison.md for the contract. - ForwardEncodingVersion uint32 `protobuf:"varint,7,opt,name=forward_encoding_version,json=forwardEncodingVersion,proto3" json:"forward_encoding_version,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id *IndexID `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + BuildStatus IndexBuildStatus `protobuf:"varint,2,opt,name=build_status,json=buildStatus,proto3,enum=common.IndexBuildStatus" json:"build_status,omitempty"` + CreatedAt *Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + LastBuiltAt *Timestamp `protobuf:"bytes,4,opt,name=last_built_at,json=lastBuiltAt,proto3" json:"last_built_at,omitempty"` + LastError string `protobuf:"bytes,5,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Index) Reset() { *x = Index{} - mi := &file_common_proto_msgTypes[21] + mi := &file_common_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2586,7 +2690,7 @@ func (x *Index) String() string { func (*Index) ProtoMessage() {} func (x *Index) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[21] + mi := &file_common_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2599,7 +2703,7 @@ func (x *Index) ProtoReflect() protoreflect.Message { // Deprecated: Use Index.ProtoReflect.Descriptor instead. func (*Index) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{21} + return file_common_proto_rawDescGZIP(), []int{23} } func (x *Index) GetId() *IndexID { @@ -2637,20 +2741,6 @@ func (x *Index) GetLastError() string { return "" } -func (x *Index) GetLedger() string { - if x != nil { - return x.Ledger - } - return "" -} - -func (x *Index) GetForwardEncodingVersion() uint32 { - if x != nil { - return x.ForwardEncodingVersion - } - return 0 -} - type Idempotency struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` @@ -2660,7 +2750,7 @@ type Idempotency struct { func (x *Idempotency) Reset() { *x = Idempotency{} - mi := &file_common_proto_msgTypes[22] + mi := &file_common_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2672,7 +2762,7 @@ func (x *Idempotency) String() string { func (*Idempotency) ProtoMessage() {} func (x *Idempotency) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[22] + mi := &file_common_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2685,7 +2775,7 @@ func (x *Idempotency) ProtoReflect() protoreflect.Message { // Deprecated: Use Idempotency.ProtoReflect.Descriptor instead. func (*Idempotency) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{22} + return file_common_proto_rawDescGZIP(), []int{24} } func (x *Idempotency) GetKey() string { @@ -2706,7 +2796,7 @@ type IdempotencyEntry struct { func (x *IdempotencyEntry) Reset() { *x = IdempotencyEntry{} - mi := &file_common_proto_msgTypes[23] + mi := &file_common_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2718,7 +2808,7 @@ func (x *IdempotencyEntry) String() string { func (*IdempotencyEntry) ProtoMessage() {} func (x *IdempotencyEntry) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[23] + mi := &file_common_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2731,7 +2821,7 @@ func (x *IdempotencyEntry) ProtoReflect() protoreflect.Message { // Deprecated: Use IdempotencyEntry.ProtoReflect.Descriptor instead. func (*IdempotencyEntry) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{23} + return file_common_proto_rawDescGZIP(), []int{25} } func (x *IdempotencyEntry) GetHash() []byte { @@ -2760,7 +2850,7 @@ type Log struct { func (x *Log) Reset() { *x = Log{} - mi := &file_common_proto_msgTypes[24] + mi := &file_common_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2772,7 +2862,7 @@ func (x *Log) String() string { func (*Log) ProtoMessage() {} func (x *Log) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[24] + mi := &file_common_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2785,7 +2875,7 @@ func (x *Log) ProtoReflect() protoreflect.Message { // Deprecated: Use Log.ProtoReflect.Descriptor instead. func (*Log) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{24} + return file_common_proto_rawDescGZIP(), []int{26} } func (x *Log) GetSequence() uint64 { @@ -2854,7 +2944,7 @@ type LogPayload struct { func (x *LogPayload) Reset() { *x = LogPayload{} - mi := &file_common_proto_msgTypes[25] + mi := &file_common_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2866,7 +2956,7 @@ func (x *LogPayload) String() string { func (*LogPayload) ProtoMessage() {} func (x *LogPayload) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[25] + mi := &file_common_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2879,7 +2969,7 @@ func (x *LogPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use LogPayload.ProtoReflect.Descriptor instead. func (*LogPayload) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{25} + return file_common_proto_rawDescGZIP(), []int{27} } func (x *LogPayload) GetType() isLogPayload_Type { @@ -3307,7 +3397,7 @@ type PromotedLedgerLog struct { func (x *PromotedLedgerLog) Reset() { *x = PromotedLedgerLog{} - mi := &file_common_proto_msgTypes[26] + mi := &file_common_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3319,7 +3409,7 @@ func (x *PromotedLedgerLog) String() string { func (*PromotedLedgerLog) ProtoMessage() {} func (x *PromotedLedgerLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[26] + mi := &file_common_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3332,7 +3422,7 @@ func (x *PromotedLedgerLog) ProtoReflect() protoreflect.Message { // Deprecated: Use PromotedLedgerLog.ProtoReflect.Descriptor instead. func (*PromotedLedgerLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{26} + return file_common_proto_rawDescGZIP(), []int{28} } func (x *PromotedLedgerLog) GetName() string { @@ -3354,7 +3444,7 @@ type RegisteredSigningKeyLog struct { func (x *RegisteredSigningKeyLog) Reset() { *x = RegisteredSigningKeyLog{} - mi := &file_common_proto_msgTypes[27] + mi := &file_common_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3366,7 +3456,7 @@ func (x *RegisteredSigningKeyLog) String() string { func (*RegisteredSigningKeyLog) ProtoMessage() {} func (x *RegisteredSigningKeyLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[27] + mi := &file_common_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3379,7 +3469,7 @@ func (x *RegisteredSigningKeyLog) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisteredSigningKeyLog.ProtoReflect.Descriptor instead. func (*RegisteredSigningKeyLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{27} + return file_common_proto_rawDescGZIP(), []int{29} } func (x *RegisteredSigningKeyLog) GetKeyId() string { @@ -3414,7 +3504,7 @@ type RevokedSigningKeyLog struct { func (x *RevokedSigningKeyLog) Reset() { *x = RevokedSigningKeyLog{} - mi := &file_common_proto_msgTypes[28] + mi := &file_common_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3426,7 +3516,7 @@ func (x *RevokedSigningKeyLog) String() string { func (*RevokedSigningKeyLog) ProtoMessage() {} func (x *RevokedSigningKeyLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[28] + mi := &file_common_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3439,7 +3529,7 @@ func (x *RevokedSigningKeyLog) ProtoReflect() protoreflect.Message { // Deprecated: Use RevokedSigningKeyLog.ProtoReflect.Descriptor instead. func (*RevokedSigningKeyLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{28} + return file_common_proto_rawDescGZIP(), []int{30} } func (x *RevokedSigningKeyLog) GetKeyId() string { @@ -3468,7 +3558,7 @@ type SigningKey struct { func (x *SigningKey) Reset() { *x = SigningKey{} - mi := &file_common_proto_msgTypes[29] + mi := &file_common_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3480,7 +3570,7 @@ func (x *SigningKey) String() string { func (*SigningKey) ProtoMessage() {} func (x *SigningKey) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[29] + mi := &file_common_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3493,7 +3583,7 @@ func (x *SigningKey) ProtoReflect() protoreflect.Message { // Deprecated: Use SigningKey.ProtoReflect.Descriptor instead. func (*SigningKey) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{29} + return file_common_proto_rawDescGZIP(), []int{31} } func (x *SigningKey) GetKeyId() string { @@ -3527,7 +3617,7 @@ type SetSigningConfigLog struct { func (x *SetSigningConfigLog) Reset() { *x = SetSigningConfigLog{} - mi := &file_common_proto_msgTypes[30] + mi := &file_common_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3539,7 +3629,7 @@ func (x *SetSigningConfigLog) String() string { func (*SetSigningConfigLog) ProtoMessage() {} func (x *SetSigningConfigLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[30] + mi := &file_common_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3552,7 +3642,7 @@ func (x *SetSigningConfigLog) ProtoReflect() protoreflect.Message { // Deprecated: Use SetSigningConfigLog.ProtoReflect.Descriptor instead. func (*SetSigningConfigLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{30} + return file_common_proto_rawDescGZIP(), []int{32} } func (x *SetSigningConfigLog) GetRequireSignatures() bool { @@ -3572,7 +3662,7 @@ type AddedEventsSinkLog struct { func (x *AddedEventsSinkLog) Reset() { *x = AddedEventsSinkLog{} - mi := &file_common_proto_msgTypes[31] + mi := &file_common_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3584,7 +3674,7 @@ func (x *AddedEventsSinkLog) String() string { func (*AddedEventsSinkLog) ProtoMessage() {} func (x *AddedEventsSinkLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[31] + mi := &file_common_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3597,7 +3687,7 @@ func (x *AddedEventsSinkLog) ProtoReflect() protoreflect.Message { // Deprecated: Use AddedEventsSinkLog.ProtoReflect.Descriptor instead. func (*AddedEventsSinkLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{31} + return file_common_proto_rawDescGZIP(), []int{33} } func (x *AddedEventsSinkLog) GetConfig() *SinkConfig { @@ -3617,7 +3707,7 @@ type RemovedEventsSinkLog struct { func (x *RemovedEventsSinkLog) Reset() { *x = RemovedEventsSinkLog{} - mi := &file_common_proto_msgTypes[32] + mi := &file_common_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3629,7 +3719,7 @@ func (x *RemovedEventsSinkLog) String() string { func (*RemovedEventsSinkLog) ProtoMessage() {} func (x *RemovedEventsSinkLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[32] + mi := &file_common_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3642,7 +3732,7 @@ func (x *RemovedEventsSinkLog) ProtoReflect() protoreflect.Message { // Deprecated: Use RemovedEventsSinkLog.ProtoReflect.Descriptor instead. func (*RemovedEventsSinkLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{32} + return file_common_proto_rawDescGZIP(), []int{34} } func (x *RemovedEventsSinkLog) GetName() string { @@ -3662,7 +3752,7 @@ type SetMaintenanceModeLog struct { func (x *SetMaintenanceModeLog) Reset() { *x = SetMaintenanceModeLog{} - mi := &file_common_proto_msgTypes[33] + mi := &file_common_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3674,7 +3764,7 @@ func (x *SetMaintenanceModeLog) String() string { func (*SetMaintenanceModeLog) ProtoMessage() {} func (x *SetMaintenanceModeLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[33] + mi := &file_common_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3687,7 +3777,7 @@ func (x *SetMaintenanceModeLog) ProtoReflect() protoreflect.Message { // Deprecated: Use SetMaintenanceModeLog.ProtoReflect.Descriptor instead. func (*SetMaintenanceModeLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{33} + return file_common_proto_rawDescGZIP(), []int{35} } func (x *SetMaintenanceModeLog) GetEnabled() bool { @@ -3708,7 +3798,7 @@ type BloomTypeConfig struct { func (x *BloomTypeConfig) Reset() { *x = BloomTypeConfig{} - mi := &file_common_proto_msgTypes[34] + mi := &file_common_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3720,7 +3810,7 @@ func (x *BloomTypeConfig) String() string { func (*BloomTypeConfig) ProtoMessage() {} func (x *BloomTypeConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[34] + mi := &file_common_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3733,7 +3823,7 @@ func (x *BloomTypeConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use BloomTypeConfig.ProtoReflect.Descriptor instead. func (*BloomTypeConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{34} + return file_common_proto_rawDescGZIP(), []int{36} } func (x *BloomTypeConfig) GetExpectedKeys() uint64 { @@ -3766,15 +3856,13 @@ type ClusterConfig struct { BloomNumscriptContents *BloomTypeConfig `protobuf:"bytes,10,opt,name=bloom_numscript_contents,json=bloomNumscriptContents,proto3" json:"bloom_numscript_contents,omitempty"` HashAlgorithm HashAlgorithm `protobuf:"varint,11,opt,name=hash_algorithm,json=hashAlgorithm,proto3,enum=common.HashAlgorithm" json:"hash_algorithm,omitempty"` BloomLedgerMetadata *BloomTypeConfig `protobuf:"bytes,12,opt,name=bloom_ledger_metadata,json=bloomLedgerMetadata,proto3" json:"bloom_ledger_metadata,omitempty"` - BloomPreparedQueries *BloomTypeConfig `protobuf:"bytes,13,opt,name=bloom_prepared_queries,json=bloomPreparedQueries,proto3" json:"bloom_prepared_queries,omitempty"` - BloomIndexes *BloomTypeConfig `protobuf:"bytes,14,opt,name=bloom_indexes,json=bloomIndexes,proto3" json:"bloom_indexes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ClusterConfig) Reset() { *x = ClusterConfig{} - mi := &file_common_proto_msgTypes[35] + mi := &file_common_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3786,7 +3874,7 @@ func (x *ClusterConfig) String() string { func (*ClusterConfig) ProtoMessage() {} func (x *ClusterConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[35] + mi := &file_common_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3799,7 +3887,7 @@ func (x *ClusterConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ClusterConfig.ProtoReflect.Descriptor instead. func (*ClusterConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{35} + return file_common_proto_rawDescGZIP(), []int{37} } func (x *ClusterConfig) GetRotationThreshold() uint64 { @@ -3886,20 +3974,6 @@ func (x *ClusterConfig) GetBloomLedgerMetadata() *BloomTypeConfig { return nil } -func (x *ClusterConfig) GetBloomPreparedQueries() *BloomTypeConfig { - if x != nil { - return x.BloomPreparedQueries - } - return nil -} - -func (x *ClusterConfig) GetBloomIndexes() *BloomTypeConfig { - if x != nil { - return x.BloomIndexes - } - return nil -} - // PersistedClusterState wraps ClusterConfig with internal FSM state that must // be persisted for deterministic restore but is not part of the config proposal. type PersistedClusterState struct { @@ -3912,7 +3986,7 @@ type PersistedClusterState struct { func (x *PersistedClusterState) Reset() { *x = PersistedClusterState{} - mi := &file_common_proto_msgTypes[36] + mi := &file_common_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3924,7 +3998,7 @@ func (x *PersistedClusterState) String() string { func (*PersistedClusterState) ProtoMessage() {} func (x *PersistedClusterState) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[36] + mi := &file_common_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3937,7 +4011,7 @@ func (x *PersistedClusterState) ProtoReflect() protoreflect.Message { // Deprecated: Use PersistedClusterState.ProtoReflect.Descriptor instead. func (*PersistedClusterState) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{36} + return file_common_proto_rawDescGZIP(), []int{38} } func (x *PersistedClusterState) GetConfig() *ClusterConfig { @@ -3964,7 +4038,7 @@ type SetChapterScheduleLog struct { func (x *SetChapterScheduleLog) Reset() { *x = SetChapterScheduleLog{} - mi := &file_common_proto_msgTypes[37] + mi := &file_common_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3976,7 +4050,7 @@ func (x *SetChapterScheduleLog) String() string { func (*SetChapterScheduleLog) ProtoMessage() {} func (x *SetChapterScheduleLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[37] + mi := &file_common_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3989,7 +4063,7 @@ func (x *SetChapterScheduleLog) ProtoReflect() protoreflect.Message { // Deprecated: Use SetChapterScheduleLog.ProtoReflect.Descriptor instead. func (*SetChapterScheduleLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{37} + return file_common_proto_rawDescGZIP(), []int{39} } func (x *SetChapterScheduleLog) GetCron() string { @@ -4008,7 +4082,7 @@ type DeletedChapterScheduleLog struct { func (x *DeletedChapterScheduleLog) Reset() { *x = DeletedChapterScheduleLog{} - mi := &file_common_proto_msgTypes[38] + mi := &file_common_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4020,7 +4094,7 @@ func (x *DeletedChapterScheduleLog) String() string { func (*DeletedChapterScheduleLog) ProtoMessage() {} func (x *DeletedChapterScheduleLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[38] + mi := &file_common_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4033,7 +4107,7 @@ func (x *DeletedChapterScheduleLog) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedChapterScheduleLog.ProtoReflect.Descriptor instead. func (*DeletedChapterScheduleLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{38} + return file_common_proto_rawDescGZIP(), []int{40} } // CreatedPreparedQueryLog records the creation of a prepared query. @@ -4047,7 +4121,7 @@ type CreatedPreparedQueryLog struct { func (x *CreatedPreparedQueryLog) Reset() { *x = CreatedPreparedQueryLog{} - mi := &file_common_proto_msgTypes[39] + mi := &file_common_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4059,7 +4133,7 @@ func (x *CreatedPreparedQueryLog) String() string { func (*CreatedPreparedQueryLog) ProtoMessage() {} func (x *CreatedPreparedQueryLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[39] + mi := &file_common_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4072,7 +4146,7 @@ func (x *CreatedPreparedQueryLog) ProtoReflect() protoreflect.Message { // Deprecated: Use CreatedPreparedQueryLog.ProtoReflect.Descriptor instead. func (*CreatedPreparedQueryLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{39} + return file_common_proto_rawDescGZIP(), []int{41} } func (x *CreatedPreparedQueryLog) GetLedger() string { @@ -4102,7 +4176,7 @@ type UpdatedPreparedQueryLog struct { func (x *UpdatedPreparedQueryLog) Reset() { *x = UpdatedPreparedQueryLog{} - mi := &file_common_proto_msgTypes[40] + mi := &file_common_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4114,7 +4188,7 @@ func (x *UpdatedPreparedQueryLog) String() string { func (*UpdatedPreparedQueryLog) ProtoMessage() {} func (x *UpdatedPreparedQueryLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[40] + mi := &file_common_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4127,7 +4201,7 @@ func (x *UpdatedPreparedQueryLog) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatedPreparedQueryLog.ProtoReflect.Descriptor instead. func (*UpdatedPreparedQueryLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{40} + return file_common_proto_rawDescGZIP(), []int{42} } func (x *UpdatedPreparedQueryLog) GetLedger() string { @@ -4169,7 +4243,7 @@ type DeletedPreparedQueryLog struct { func (x *DeletedPreparedQueryLog) Reset() { *x = DeletedPreparedQueryLog{} - mi := &file_common_proto_msgTypes[41] + mi := &file_common_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4181,7 +4255,7 @@ func (x *DeletedPreparedQueryLog) String() string { func (*DeletedPreparedQueryLog) ProtoMessage() {} func (x *DeletedPreparedQueryLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[41] + mi := &file_common_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4194,7 +4268,7 @@ func (x *DeletedPreparedQueryLog) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedPreparedQueryLog.ProtoReflect.Descriptor instead. func (*DeletedPreparedQueryLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{41} + return file_common_proto_rawDescGZIP(), []int{43} } func (x *DeletedPreparedQueryLog) GetLedger() string { @@ -4222,7 +4296,7 @@ type SavedLedgerMetadataLog struct { func (x *SavedLedgerMetadataLog) Reset() { *x = SavedLedgerMetadataLog{} - mi := &file_common_proto_msgTypes[42] + mi := &file_common_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4234,7 +4308,7 @@ func (x *SavedLedgerMetadataLog) String() string { func (*SavedLedgerMetadataLog) ProtoMessage() {} func (x *SavedLedgerMetadataLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[42] + mi := &file_common_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4247,7 +4321,7 @@ func (x *SavedLedgerMetadataLog) ProtoReflect() protoreflect.Message { // Deprecated: Use SavedLedgerMetadataLog.ProtoReflect.Descriptor instead. func (*SavedLedgerMetadataLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{42} + return file_common_proto_rawDescGZIP(), []int{44} } func (x *SavedLedgerMetadataLog) GetLedger() string { @@ -4275,7 +4349,7 @@ type DeletedLedgerMetadataLog struct { func (x *DeletedLedgerMetadataLog) Reset() { *x = DeletedLedgerMetadataLog{} - mi := &file_common_proto_msgTypes[43] + mi := &file_common_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4287,7 +4361,7 @@ func (x *DeletedLedgerMetadataLog) String() string { func (*DeletedLedgerMetadataLog) ProtoMessage() {} func (x *DeletedLedgerMetadataLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[43] + mi := &file_common_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4300,7 +4374,7 @@ func (x *DeletedLedgerMetadataLog) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedLedgerMetadataLog.ProtoReflect.Descriptor instead. func (*DeletedLedgerMetadataLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{43} + return file_common_proto_rawDescGZIP(), []int{45} } func (x *DeletedLedgerMetadataLog) GetLedger() string { @@ -4331,7 +4405,7 @@ type NumscriptInfo struct { func (x *NumscriptInfo) Reset() { *x = NumscriptInfo{} - mi := &file_common_proto_msgTypes[44] + mi := &file_common_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4343,7 +4417,7 @@ func (x *NumscriptInfo) String() string { func (*NumscriptInfo) ProtoMessage() {} func (x *NumscriptInfo) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[44] + mi := &file_common_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4356,7 +4430,7 @@ func (x *NumscriptInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use NumscriptInfo.ProtoReflect.Descriptor instead. func (*NumscriptInfo) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{44} + return file_common_proto_rawDescGZIP(), []int{46} } func (x *NumscriptInfo) GetName() string { @@ -4404,7 +4478,7 @@ type SavedNumscriptLog struct { func (x *SavedNumscriptLog) Reset() { *x = SavedNumscriptLog{} - mi := &file_common_proto_msgTypes[45] + mi := &file_common_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4416,7 +4490,7 @@ func (x *SavedNumscriptLog) String() string { func (*SavedNumscriptLog) ProtoMessage() {} func (x *SavedNumscriptLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[45] + mi := &file_common_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4429,7 +4503,7 @@ func (x *SavedNumscriptLog) ProtoReflect() protoreflect.Message { // Deprecated: Use SavedNumscriptLog.ProtoReflect.Descriptor instead. func (*SavedNumscriptLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{45} + return file_common_proto_rawDescGZIP(), []int{47} } func (x *SavedNumscriptLog) GetInfo() *NumscriptInfo { @@ -4450,7 +4524,7 @@ type DeletedNumscriptLog struct { func (x *DeletedNumscriptLog) Reset() { *x = DeletedNumscriptLog{} - mi := &file_common_proto_msgTypes[46] + mi := &file_common_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4462,7 +4536,7 @@ func (x *DeletedNumscriptLog) String() string { func (*DeletedNumscriptLog) ProtoMessage() {} func (x *DeletedNumscriptLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[46] + mi := &file_common_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4475,7 +4549,7 @@ func (x *DeletedNumscriptLog) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedNumscriptLog.ProtoReflect.Descriptor instead. func (*DeletedNumscriptLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{46} + return file_common_proto_rawDescGZIP(), []int{48} } func (x *DeletedNumscriptLog) GetName() string { @@ -4502,7 +4576,7 @@ type SetQueryCheckpointScheduleLog struct { func (x *SetQueryCheckpointScheduleLog) Reset() { *x = SetQueryCheckpointScheduleLog{} - mi := &file_common_proto_msgTypes[47] + mi := &file_common_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4514,7 +4588,7 @@ func (x *SetQueryCheckpointScheduleLog) String() string { func (*SetQueryCheckpointScheduleLog) ProtoMessage() {} func (x *SetQueryCheckpointScheduleLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[47] + mi := &file_common_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4527,7 +4601,7 @@ func (x *SetQueryCheckpointScheduleLog) ProtoReflect() protoreflect.Message { // Deprecated: Use SetQueryCheckpointScheduleLog.ProtoReflect.Descriptor instead. func (*SetQueryCheckpointScheduleLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{47} + return file_common_proto_rawDescGZIP(), []int{49} } func (x *SetQueryCheckpointScheduleLog) GetCron() string { @@ -4546,7 +4620,7 @@ type DeletedQueryCheckpointScheduleLog struct { func (x *DeletedQueryCheckpointScheduleLog) Reset() { *x = DeletedQueryCheckpointScheduleLog{} - mi := &file_common_proto_msgTypes[48] + mi := &file_common_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4558,7 +4632,7 @@ func (x *DeletedQueryCheckpointScheduleLog) String() string { func (*DeletedQueryCheckpointScheduleLog) ProtoMessage() {} func (x *DeletedQueryCheckpointScheduleLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[48] + mi := &file_common_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4571,7 +4645,7 @@ func (x *DeletedQueryCheckpointScheduleLog) ProtoReflect() protoreflect.Message // Deprecated: Use DeletedQueryCheckpointScheduleLog.ProtoReflect.Descriptor instead. func (*DeletedQueryCheckpointScheduleLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{48} + return file_common_proto_rawDescGZIP(), []int{50} } // CreatedQueryCheckpointLog records a query checkpoint being created. @@ -4585,7 +4659,7 @@ type CreatedQueryCheckpointLog struct { func (x *CreatedQueryCheckpointLog) Reset() { *x = CreatedQueryCheckpointLog{} - mi := &file_common_proto_msgTypes[49] + mi := &file_common_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4597,7 +4671,7 @@ func (x *CreatedQueryCheckpointLog) String() string { func (*CreatedQueryCheckpointLog) ProtoMessage() {} func (x *CreatedQueryCheckpointLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[49] + mi := &file_common_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4610,7 +4684,7 @@ func (x *CreatedQueryCheckpointLog) ProtoReflect() protoreflect.Message { // Deprecated: Use CreatedQueryCheckpointLog.ProtoReflect.Descriptor instead. func (*CreatedQueryCheckpointLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{49} + return file_common_proto_rawDescGZIP(), []int{51} } func (x *CreatedQueryCheckpointLog) GetCheckpointId() uint64 { @@ -4637,7 +4711,7 @@ type DeletedQueryCheckpointLog struct { func (x *DeletedQueryCheckpointLog) Reset() { *x = DeletedQueryCheckpointLog{} - mi := &file_common_proto_msgTypes[50] + mi := &file_common_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4649,7 +4723,7 @@ func (x *DeletedQueryCheckpointLog) String() string { func (*DeletedQueryCheckpointLog) ProtoMessage() {} func (x *DeletedQueryCheckpointLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[50] + mi := &file_common_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4662,7 +4736,7 @@ func (x *DeletedQueryCheckpointLog) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedQueryCheckpointLog.ProtoReflect.Descriptor instead. func (*DeletedQueryCheckpointLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{50} + return file_common_proto_rawDescGZIP(), []int{52} } func (x *DeletedQueryCheckpointLog) GetCheckpointId() uint64 { @@ -4694,7 +4768,7 @@ type SinkConfig struct { func (x *SinkConfig) Reset() { *x = SinkConfig{} - mi := &file_common_proto_msgTypes[51] + mi := &file_common_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4706,7 +4780,7 @@ func (x *SinkConfig) String() string { func (*SinkConfig) ProtoMessage() {} func (x *SinkConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[51] + mi := &file_common_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4719,7 +4793,7 @@ func (x *SinkConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use SinkConfig.ProtoReflect.Descriptor instead. func (*SinkConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{51} + return file_common_proto_rawDescGZIP(), []int{53} } func (x *SinkConfig) GetName() string { @@ -4855,7 +4929,7 @@ type SinkStatus struct { func (x *SinkStatus) Reset() { *x = SinkStatus{} - mi := &file_common_proto_msgTypes[52] + mi := &file_common_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4867,7 +4941,7 @@ func (x *SinkStatus) String() string { func (*SinkStatus) ProtoMessage() {} func (x *SinkStatus) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[52] + mi := &file_common_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4880,7 +4954,7 @@ func (x *SinkStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use SinkStatus.ProtoReflect.Descriptor instead. func (*SinkStatus) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{52} + return file_common_proto_rawDescGZIP(), []int{54} } func (x *SinkStatus) GetSinkName() string { @@ -4915,7 +4989,7 @@ type SinkError struct { func (x *SinkError) Reset() { *x = SinkError{} - mi := &file_common_proto_msgTypes[53] + mi := &file_common_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4927,7 +5001,7 @@ func (x *SinkError) String() string { func (*SinkError) ProtoMessage() {} func (x *SinkError) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[53] + mi := &file_common_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4940,7 +5014,7 @@ func (x *SinkError) ProtoReflect() protoreflect.Message { // Deprecated: Use SinkError.ProtoReflect.Descriptor instead. func (*SinkError) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{53} + return file_common_proto_rawDescGZIP(), []int{55} } func (x *SinkError) GetMessage() string { @@ -4968,7 +5042,7 @@ type NatsSinkConfig struct { func (x *NatsSinkConfig) Reset() { *x = NatsSinkConfig{} - mi := &file_common_proto_msgTypes[54] + mi := &file_common_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4980,7 +5054,7 @@ func (x *NatsSinkConfig) String() string { func (*NatsSinkConfig) ProtoMessage() {} func (x *NatsSinkConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[54] + mi := &file_common_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4993,7 +5067,7 @@ func (x *NatsSinkConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use NatsSinkConfig.ProtoReflect.Descriptor instead. func (*NatsSinkConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{54} + return file_common_proto_rawDescGZIP(), []int{56} } func (x *NatsSinkConfig) GetUrl() string { @@ -5021,7 +5095,7 @@ type ClickHouseSinkConfig struct { func (x *ClickHouseSinkConfig) Reset() { *x = ClickHouseSinkConfig{} - mi := &file_common_proto_msgTypes[55] + mi := &file_common_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5033,7 +5107,7 @@ func (x *ClickHouseSinkConfig) String() string { func (*ClickHouseSinkConfig) ProtoMessage() {} func (x *ClickHouseSinkConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[55] + mi := &file_common_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5046,7 +5120,7 @@ func (x *ClickHouseSinkConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ClickHouseSinkConfig.ProtoReflect.Descriptor instead. func (*ClickHouseSinkConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{55} + return file_common_proto_rawDescGZIP(), []int{57} } func (x *ClickHouseSinkConfig) GetDsn() string { @@ -5078,7 +5152,7 @@ type KafkaSinkConfig struct { func (x *KafkaSinkConfig) Reset() { *x = KafkaSinkConfig{} - mi := &file_common_proto_msgTypes[56] + mi := &file_common_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5090,7 +5164,7 @@ func (x *KafkaSinkConfig) String() string { func (*KafkaSinkConfig) ProtoMessage() {} func (x *KafkaSinkConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[56] + mi := &file_common_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5103,7 +5177,7 @@ func (x *KafkaSinkConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use KafkaSinkConfig.ProtoReflect.Descriptor instead. func (*KafkaSinkConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{56} + return file_common_proto_rawDescGZIP(), []int{58} } func (x *KafkaSinkConfig) GetBrokers() []string { @@ -5159,7 +5233,7 @@ type HttpSinkConfig struct { func (x *HttpSinkConfig) Reset() { *x = HttpSinkConfig{} - mi := &file_common_proto_msgTypes[57] + mi := &file_common_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5171,7 +5245,7 @@ func (x *HttpSinkConfig) String() string { func (*HttpSinkConfig) ProtoMessage() {} func (x *HttpSinkConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[57] + mi := &file_common_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5184,7 +5258,7 @@ func (x *HttpSinkConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use HttpSinkConfig.ProtoReflect.Descriptor instead. func (*HttpSinkConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{57} + return file_common_proto_rawDescGZIP(), []int{59} } func (x *HttpSinkConfig) GetEndpoint() string { @@ -5222,7 +5296,7 @@ type DatabricksSinkConfig struct { func (x *DatabricksSinkConfig) Reset() { *x = DatabricksSinkConfig{} - mi := &file_common_proto_msgTypes[58] + mi := &file_common_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5234,7 +5308,7 @@ func (x *DatabricksSinkConfig) String() string { func (*DatabricksSinkConfig) ProtoMessage() {} func (x *DatabricksSinkConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[58] + mi := &file_common_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5247,7 +5321,7 @@ func (x *DatabricksSinkConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DatabricksSinkConfig.ProtoReflect.Descriptor instead. func (*DatabricksSinkConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{58} + return file_common_proto_rawDescGZIP(), []int{60} } func (x *DatabricksSinkConfig) GetServerHostname() string { @@ -5344,7 +5418,7 @@ type DatabricksOAuthM2M struct { func (x *DatabricksOAuthM2M) Reset() { *x = DatabricksOAuthM2M{} - mi := &file_common_proto_msgTypes[59] + mi := &file_common_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5356,7 +5430,7 @@ func (x *DatabricksOAuthM2M) String() string { func (*DatabricksOAuthM2M) ProtoMessage() {} func (x *DatabricksOAuthM2M) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[59] + mi := &file_common_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5369,7 +5443,7 @@ func (x *DatabricksOAuthM2M) ProtoReflect() protoreflect.Message { // Deprecated: Use DatabricksOAuthM2M.ProtoReflect.Descriptor instead. func (*DatabricksOAuthM2M) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{59} + return file_common_proto_rawDescGZIP(), []int{61} } func (x *DatabricksOAuthM2M) GetClientId() string { @@ -5405,7 +5479,7 @@ type CreatedLedgerLog struct { func (x *CreatedLedgerLog) Reset() { *x = CreatedLedgerLog{} - mi := &file_common_proto_msgTypes[60] + mi := &file_common_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5417,7 +5491,7 @@ func (x *CreatedLedgerLog) String() string { func (*CreatedLedgerLog) ProtoMessage() {} func (x *CreatedLedgerLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[60] + mi := &file_common_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5430,7 +5504,7 @@ func (x *CreatedLedgerLog) ProtoReflect() protoreflect.Message { // Deprecated: Use CreatedLedgerLog.ProtoReflect.Descriptor instead. func (*CreatedLedgerLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{60} + return file_common_proto_rawDescGZIP(), []int{62} } func (x *CreatedLedgerLog) GetName() string { @@ -5499,7 +5573,7 @@ type DeletedLedgerLog struct { func (x *DeletedLedgerLog) Reset() { *x = DeletedLedgerLog{} - mi := &file_common_proto_msgTypes[61] + mi := &file_common_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5511,7 +5585,7 @@ func (x *DeletedLedgerLog) String() string { func (*DeletedLedgerLog) ProtoMessage() {} func (x *DeletedLedgerLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[61] + mi := &file_common_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5524,7 +5598,7 @@ func (x *DeletedLedgerLog) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedLedgerLog.ProtoReflect.Descriptor instead. func (*DeletedLedgerLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{61} + return file_common_proto_rawDescGZIP(), []int{63} } func (x *DeletedLedgerLog) GetName() string { @@ -5551,7 +5625,7 @@ type ApplyLedgerLog struct { func (x *ApplyLedgerLog) Reset() { *x = ApplyLedgerLog{} - mi := &file_common_proto_msgTypes[62] + mi := &file_common_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5563,7 +5637,7 @@ func (x *ApplyLedgerLog) String() string { func (*ApplyLedgerLog) ProtoMessage() {} func (x *ApplyLedgerLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[62] + mi := &file_common_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5576,7 +5650,7 @@ func (x *ApplyLedgerLog) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyLedgerLog.ProtoReflect.Descriptor instead. func (*ApplyLedgerLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{62} + return file_common_proto_rawDescGZIP(), []int{64} } func (x *ApplyLedgerLog) GetLedgerName() string { @@ -5614,7 +5688,7 @@ type LedgerLog struct { func (x *LedgerLog) Reset() { *x = LedgerLog{} - mi := &file_common_proto_msgTypes[63] + mi := &file_common_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5626,7 +5700,7 @@ func (x *LedgerLog) String() string { func (*LedgerLog) ProtoMessage() {} func (x *LedgerLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[63] + mi := &file_common_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5639,7 +5713,7 @@ func (x *LedgerLog) ProtoReflect() protoreflect.Message { // Deprecated: Use LedgerLog.ProtoReflect.Descriptor instead. func (*LedgerLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{63} + return file_common_proto_rawDescGZIP(), []int{65} } func (x *LedgerLog) GetData() *LedgerLogPayload { @@ -5671,19 +5745,22 @@ func (x *LedgerLog) GetPurgedVolumes() []*TouchedVolume { } // TouchedVolume identifies a (ledger-local) volume cell — an account paired -// with an asset. Used in transient/purged volume exclusion sets where the -// indexer must distinguish per-asset state inside a multi-asset account. +// with an asset and a color. Used in transient/purged volume exclusion sets +// where the indexer must distinguish per-(asset, color) state inside a +// multi-bucket account. The empty color is the uncolored bucket and is itself +// just another segregated cell. type TouchedVolume struct { state protoimpl.MessageState `protogen:"open.v1"` Account string `protobuf:"bytes,1,opt,name=account,proto3" json:"account,omitempty"` Asset string `protobuf:"bytes,2,opt,name=asset,proto3" json:"asset,omitempty"` + Color string `protobuf:"bytes,3,opt,name=color,proto3" json:"color,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *TouchedVolume) Reset() { *x = TouchedVolume{} - mi := &file_common_proto_msgTypes[64] + mi := &file_common_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5695,7 +5772,7 @@ func (x *TouchedVolume) String() string { func (*TouchedVolume) ProtoMessage() {} func (x *TouchedVolume) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[64] + mi := &file_common_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5708,7 +5785,7 @@ func (x *TouchedVolume) ProtoReflect() protoreflect.Message { // Deprecated: Use TouchedVolume.ProtoReflect.Descriptor instead. func (*TouchedVolume) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{64} + return file_common_proto_rawDescGZIP(), []int{66} } func (x *TouchedVolume) GetAccount() string { @@ -5725,6 +5802,13 @@ func (x *TouchedVolume) GetAsset() string { return "" } +func (x *TouchedVolume) GetColor() string { + if x != nil { + return x.Color + } + return "" +} + type LedgerLogPayload struct { state protoimpl.MessageState `protogen:"open.v1"` // Types that are valid to be assigned to Payload: @@ -5748,7 +5832,7 @@ type LedgerLogPayload struct { func (x *LedgerLogPayload) Reset() { *x = LedgerLogPayload{} - mi := &file_common_proto_msgTypes[65] + mi := &file_common_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5760,7 +5844,7 @@ func (x *LedgerLogPayload) String() string { func (*LedgerLogPayload) ProtoMessage() {} func (x *LedgerLogPayload) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[65] + mi := &file_common_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5773,7 +5857,7 @@ func (x *LedgerLogPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use LedgerLogPayload.ProtoReflect.Descriptor instead. func (*LedgerLogPayload) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{65} + return file_common_proto_rawDescGZIP(), []int{67} } func (x *LedgerLogPayload) GetPayload() isLedgerLogPayload_Payload { @@ -5977,7 +6061,7 @@ type CreatedIndexLog struct { func (x *CreatedIndexLog) Reset() { *x = CreatedIndexLog{} - mi := &file_common_proto_msgTypes[66] + mi := &file_common_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5989,7 +6073,7 @@ func (x *CreatedIndexLog) String() string { func (*CreatedIndexLog) ProtoMessage() {} func (x *CreatedIndexLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[66] + mi := &file_common_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6002,7 +6086,7 @@ func (x *CreatedIndexLog) ProtoReflect() protoreflect.Message { // Deprecated: Use CreatedIndexLog.ProtoReflect.Descriptor instead. func (*CreatedIndexLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{66} + return file_common_proto_rawDescGZIP(), []int{68} } func (x *CreatedIndexLog) GetId() *IndexID { @@ -6022,7 +6106,7 @@ type DroppedIndexLog struct { func (x *DroppedIndexLog) Reset() { *x = DroppedIndexLog{} - mi := &file_common_proto_msgTypes[67] + mi := &file_common_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6034,7 +6118,7 @@ func (x *DroppedIndexLog) String() string { func (*DroppedIndexLog) ProtoMessage() {} func (x *DroppedIndexLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[67] + mi := &file_common_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6047,7 +6131,7 @@ func (x *DroppedIndexLog) ProtoReflect() protoreflect.Message { // Deprecated: Use DroppedIndexLog.ProtoReflect.Descriptor instead. func (*DroppedIndexLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{67} + return file_common_proto_rawDescGZIP(), []int{69} } func (x *DroppedIndexLog) GetId() *IndexID { @@ -6066,7 +6150,7 @@ type FilledGapLog struct { func (x *FilledGapLog) Reset() { *x = FilledGapLog{} - mi := &file_common_proto_msgTypes[68] + mi := &file_common_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6078,7 +6162,7 @@ func (x *FilledGapLog) String() string { func (*FilledGapLog) ProtoMessage() {} func (x *FilledGapLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[68] + mi := &file_common_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6091,7 +6175,7 @@ func (x *FilledGapLog) ProtoReflect() protoreflect.Message { // Deprecated: Use FilledGapLog.ProtoReflect.Descriptor instead. func (*FilledGapLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{68} + return file_common_proto_rawDescGZIP(), []int{70} } func (x *FilledGapLog) GetOriginalId() uint64 { @@ -6113,7 +6197,7 @@ type CreatedTransaction struct { func (x *CreatedTransaction) Reset() { *x = CreatedTransaction{} - mi := &file_common_proto_msgTypes[69] + mi := &file_common_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6125,7 +6209,7 @@ func (x *CreatedTransaction) String() string { func (*CreatedTransaction) ProtoMessage() {} func (x *CreatedTransaction) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[69] + mi := &file_common_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6138,7 +6222,7 @@ func (x *CreatedTransaction) ProtoReflect() protoreflect.Message { // Deprecated: Use CreatedTransaction.ProtoReflect.Descriptor instead. func (*CreatedTransaction) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{69} + return file_common_proto_rawDescGZIP(), []int{71} } func (x *CreatedTransaction) GetTransaction() *Transaction { @@ -6180,7 +6264,7 @@ type RevertedTransaction struct { func (x *RevertedTransaction) Reset() { *x = RevertedTransaction{} - mi := &file_common_proto_msgTypes[70] + mi := &file_common_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6192,7 +6276,7 @@ func (x *RevertedTransaction) String() string { func (*RevertedTransaction) ProtoMessage() {} func (x *RevertedTransaction) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[70] + mi := &file_common_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6205,7 +6289,7 @@ func (x *RevertedTransaction) ProtoReflect() protoreflect.Message { // Deprecated: Use RevertedTransaction.ProtoReflect.Descriptor instead. func (*RevertedTransaction) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{70} + return file_common_proto_rawDescGZIP(), []int{72} } func (x *RevertedTransaction) GetRevertedTransactionId() uint64 { @@ -6239,7 +6323,7 @@ type SavedMetadata struct { func (x *SavedMetadata) Reset() { *x = SavedMetadata{} - mi := &file_common_proto_msgTypes[71] + mi := &file_common_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6251,7 +6335,7 @@ func (x *SavedMetadata) String() string { func (*SavedMetadata) ProtoMessage() {} func (x *SavedMetadata) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[71] + mi := &file_common_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6264,7 +6348,7 @@ func (x *SavedMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use SavedMetadata.ProtoReflect.Descriptor instead. func (*SavedMetadata) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{71} + return file_common_proto_rawDescGZIP(), []int{73} } func (x *SavedMetadata) GetTarget() *Target { @@ -6291,7 +6375,7 @@ type DeletedMetadata struct { func (x *DeletedMetadata) Reset() { *x = DeletedMetadata{} - mi := &file_common_proto_msgTypes[72] + mi := &file_common_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6303,7 +6387,7 @@ func (x *DeletedMetadata) String() string { func (*DeletedMetadata) ProtoMessage() {} func (x *DeletedMetadata) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[72] + mi := &file_common_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6316,7 +6400,7 @@ func (x *DeletedMetadata) ProtoReflect() protoreflect.Message { // Deprecated: Use DeletedMetadata.ProtoReflect.Descriptor instead. func (*DeletedMetadata) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{72} + return file_common_proto_rawDescGZIP(), []int{74} } func (x *DeletedMetadata) GetTarget() *Target { @@ -6345,7 +6429,7 @@ type SetMetadataFieldTypeLog struct { func (x *SetMetadataFieldTypeLog) Reset() { *x = SetMetadataFieldTypeLog{} - mi := &file_common_proto_msgTypes[73] + mi := &file_common_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6357,7 +6441,7 @@ func (x *SetMetadataFieldTypeLog) String() string { func (*SetMetadataFieldTypeLog) ProtoMessage() {} func (x *SetMetadataFieldTypeLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[73] + mi := &file_common_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6370,7 +6454,7 @@ func (x *SetMetadataFieldTypeLog) ProtoReflect() protoreflect.Message { // Deprecated: Use SetMetadataFieldTypeLog.ProtoReflect.Descriptor instead. func (*SetMetadataFieldTypeLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{73} + return file_common_proto_rawDescGZIP(), []int{75} } func (x *SetMetadataFieldTypeLog) GetTargetType() TargetType { @@ -6408,7 +6492,7 @@ type RemovedMetadataFieldTypeLog struct { func (x *RemovedMetadataFieldTypeLog) Reset() { *x = RemovedMetadataFieldTypeLog{} - mi := &file_common_proto_msgTypes[74] + mi := &file_common_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6420,7 +6504,7 @@ func (x *RemovedMetadataFieldTypeLog) String() string { func (*RemovedMetadataFieldTypeLog) ProtoMessage() {} func (x *RemovedMetadataFieldTypeLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[74] + mi := &file_common_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6433,7 +6517,7 @@ func (x *RemovedMetadataFieldTypeLog) ProtoReflect() protoreflect.Message { // Deprecated: Use RemovedMetadataFieldTypeLog.ProtoReflect.Descriptor instead. func (*RemovedMetadataFieldTypeLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{74} + return file_common_proto_rawDescGZIP(), []int{76} } func (x *RemovedMetadataFieldTypeLog) GetTargetType() TargetType { @@ -6476,7 +6560,7 @@ type Chapter struct { func (x *Chapter) Reset() { *x = Chapter{} - mi := &file_common_proto_msgTypes[75] + mi := &file_common_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6488,7 +6572,7 @@ func (x *Chapter) String() string { func (*Chapter) ProtoMessage() {} func (x *Chapter) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[75] + mi := &file_common_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6501,7 +6585,7 @@ func (x *Chapter) ProtoReflect() protoreflect.Message { // Deprecated: Use Chapter.ProtoReflect.Descriptor instead. func (*Chapter) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{75} + return file_common_proto_rawDescGZIP(), []int{77} } func (x *Chapter) GetId() uint64 { @@ -6591,7 +6675,7 @@ type ClosedChapterLog struct { func (x *ClosedChapterLog) Reset() { *x = ClosedChapterLog{} - mi := &file_common_proto_msgTypes[76] + mi := &file_common_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6603,7 +6687,7 @@ func (x *ClosedChapterLog) String() string { func (*ClosedChapterLog) ProtoMessage() {} func (x *ClosedChapterLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[76] + mi := &file_common_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6616,7 +6700,7 @@ func (x *ClosedChapterLog) ProtoReflect() protoreflect.Message { // Deprecated: Use ClosedChapterLog.ProtoReflect.Descriptor instead. func (*ClosedChapterLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{76} + return file_common_proto_rawDescGZIP(), []int{78} } func (x *ClosedChapterLog) GetClosedChapter() *Chapter { @@ -6642,7 +6726,7 @@ type SealedChapterLog struct { func (x *SealedChapterLog) Reset() { *x = SealedChapterLog{} - mi := &file_common_proto_msgTypes[77] + mi := &file_common_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6654,7 +6738,7 @@ func (x *SealedChapterLog) String() string { func (*SealedChapterLog) ProtoMessage() {} func (x *SealedChapterLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[77] + mi := &file_common_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6667,7 +6751,7 @@ func (x *SealedChapterLog) ProtoReflect() protoreflect.Message { // Deprecated: Use SealedChapterLog.ProtoReflect.Descriptor instead. func (*SealedChapterLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{77} + return file_common_proto_rawDescGZIP(), []int{79} } func (x *SealedChapterLog) GetChapter() *Chapter { @@ -6686,7 +6770,7 @@ type ArchivedChapterLog struct { func (x *ArchivedChapterLog) Reset() { *x = ArchivedChapterLog{} - mi := &file_common_proto_msgTypes[78] + mi := &file_common_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6698,7 +6782,7 @@ func (x *ArchivedChapterLog) String() string { func (*ArchivedChapterLog) ProtoMessage() {} func (x *ArchivedChapterLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[78] + mi := &file_common_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6711,7 +6795,7 @@ func (x *ArchivedChapterLog) ProtoReflect() protoreflect.Message { // Deprecated: Use ArchivedChapterLog.ProtoReflect.Descriptor instead. func (*ArchivedChapterLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{78} + return file_common_proto_rawDescGZIP(), []int{80} } func (x *ArchivedChapterLog) GetChapter() *Chapter { @@ -6730,7 +6814,7 @@ type ConfirmedArchiveChapterLog struct { func (x *ConfirmedArchiveChapterLog) Reset() { *x = ConfirmedArchiveChapterLog{} - mi := &file_common_proto_msgTypes[79] + mi := &file_common_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6742,7 +6826,7 @@ func (x *ConfirmedArchiveChapterLog) String() string { func (*ConfirmedArchiveChapterLog) ProtoMessage() {} func (x *ConfirmedArchiveChapterLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[79] + mi := &file_common_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6755,7 +6839,7 @@ func (x *ConfirmedArchiveChapterLog) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfirmedArchiveChapterLog.ProtoReflect.Descriptor instead. func (*ConfirmedArchiveChapterLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{79} + return file_common_proto_rawDescGZIP(), []int{81} } func (x *ConfirmedArchiveChapterLog) GetChapter() *Chapter { @@ -6780,7 +6864,7 @@ type MirrorSourceConfig struct { func (x *MirrorSourceConfig) Reset() { *x = MirrorSourceConfig{} - mi := &file_common_proto_msgTypes[80] + mi := &file_common_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6792,7 +6876,7 @@ func (x *MirrorSourceConfig) String() string { func (*MirrorSourceConfig) ProtoMessage() {} func (x *MirrorSourceConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[80] + mi := &file_common_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6805,7 +6889,7 @@ func (x *MirrorSourceConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use MirrorSourceConfig.ProtoReflect.Descriptor instead. func (*MirrorSourceConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{80} + return file_common_proto_rawDescGZIP(), []int{82} } func (x *MirrorSourceConfig) GetLedgerName() string { @@ -6873,7 +6957,7 @@ type HttpMirrorSourceConfig struct { func (x *HttpMirrorSourceConfig) Reset() { *x = HttpMirrorSourceConfig{} - mi := &file_common_proto_msgTypes[81] + mi := &file_common_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6885,7 +6969,7 @@ func (x *HttpMirrorSourceConfig) String() string { func (*HttpMirrorSourceConfig) ProtoMessage() {} func (x *HttpMirrorSourceConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[81] + mi := &file_common_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6898,7 +6982,7 @@ func (x *HttpMirrorSourceConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use HttpMirrorSourceConfig.ProtoReflect.Descriptor instead. func (*HttpMirrorSourceConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{81} + return file_common_proto_rawDescGZIP(), []int{83} } func (x *HttpMirrorSourceConfig) GetBaseUrl() string { @@ -6927,7 +7011,7 @@ type OAuth2ClientCredentials struct { func (x *OAuth2ClientCredentials) Reset() { *x = OAuth2ClientCredentials{} - mi := &file_common_proto_msgTypes[82] + mi := &file_common_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6939,7 +7023,7 @@ func (x *OAuth2ClientCredentials) String() string { func (*OAuth2ClientCredentials) ProtoMessage() {} func (x *OAuth2ClientCredentials) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[82] + mi := &file_common_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6952,7 +7036,7 @@ func (x *OAuth2ClientCredentials) ProtoReflect() protoreflect.Message { // Deprecated: Use OAuth2ClientCredentials.ProtoReflect.Descriptor instead. func (*OAuth2ClientCredentials) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{82} + return file_common_proto_rawDescGZIP(), []int{84} } func (x *OAuth2ClientCredentials) GetClientId() string { @@ -6992,7 +7076,7 @@ type PostgresMirrorSourceConfig struct { func (x *PostgresMirrorSourceConfig) Reset() { *x = PostgresMirrorSourceConfig{} - mi := &file_common_proto_msgTypes[83] + mi := &file_common_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7004,7 +7088,7 @@ func (x *PostgresMirrorSourceConfig) String() string { func (*PostgresMirrorSourceConfig) ProtoMessage() {} func (x *PostgresMirrorSourceConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[83] + mi := &file_common_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7017,7 +7101,7 @@ func (x *PostgresMirrorSourceConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use PostgresMirrorSourceConfig.ProtoReflect.Descriptor instead. func (*PostgresMirrorSourceConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{83} + return file_common_proto_rawDescGZIP(), []int{85} } func (x *PostgresMirrorSourceConfig) GetDsn() string { @@ -7037,7 +7121,7 @@ type MirrorSyncError struct { func (x *MirrorSyncError) Reset() { *x = MirrorSyncError{} - mi := &file_common_proto_msgTypes[84] + mi := &file_common_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7049,7 +7133,7 @@ func (x *MirrorSyncError) String() string { func (*MirrorSyncError) ProtoMessage() {} func (x *MirrorSyncError) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[84] + mi := &file_common_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7062,7 +7146,7 @@ func (x *MirrorSyncError) ProtoReflect() protoreflect.Message { // Deprecated: Use MirrorSyncError.ProtoReflect.Descriptor instead. func (*MirrorSyncError) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{84} + return file_common_proto_rawDescGZIP(), []int{86} } func (x *MirrorSyncError) GetMessage() string { @@ -7092,7 +7176,7 @@ type MirrorSyncProgress struct { func (x *MirrorSyncProgress) Reset() { *x = MirrorSyncProgress{} - mi := &file_common_proto_msgTypes[85] + mi := &file_common_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7104,7 +7188,7 @@ func (x *MirrorSyncProgress) String() string { func (*MirrorSyncProgress) ProtoMessage() {} func (x *MirrorSyncProgress) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[85] + mi := &file_common_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7117,7 +7201,7 @@ func (x *MirrorSyncProgress) ProtoReflect() protoreflect.Message { // Deprecated: Use MirrorSyncProgress.ProtoReflect.Descriptor instead. func (*MirrorSyncProgress) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{85} + return file_common_proto_rawDescGZIP(), []int{87} } func (x *MirrorSyncProgress) GetState() MirrorSyncState { @@ -7155,10 +7239,7 @@ func (x *MirrorSyncProgress) GetError() *MirrorSyncError { return nil } -// LedgerInfo represents information about a ledger. -// -// Indexes do not live here: they are projected through the bucket-scoped -// SubAttrIndex registry and surfaced via BucketService.ListIndexes. +// LedgerInfo represents information about a ledger type LedgerInfo struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Ledger name @@ -7172,13 +7253,14 @@ type LedgerInfo struct { DefaultEnforcementMode ChartEnforcementMode `protobuf:"varint,11,opt,name=default_enforcement_mode,json=defaultEnforcementMode,proto3,enum=common.ChartEnforcementMode" json:"default_enforcement_mode,omitempty"` // Default enforcement for unmatched accounts Metadata map[string]*MetadataValue `protobuf:"bytes,12,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Populated at read time from separate attribute store Id uint32 `protobuf:"varint,13,opt,name=id,proto3" json:"id,omitempty"` // Unique numeric ledger ID (assigned by FSM, used as Pebble key prefix) + Indexes []*Index `protobuf:"bytes,14,rep,name=indexes,proto3" json:"indexes,omitempty"` // All indexes defined on this ledger unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *LedgerInfo) Reset() { *x = LedgerInfo{} - mi := &file_common_proto_msgTypes[86] + mi := &file_common_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7190,7 +7272,7 @@ func (x *LedgerInfo) String() string { func (*LedgerInfo) ProtoMessage() {} func (x *LedgerInfo) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[86] + mi := &file_common_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7203,7 +7285,7 @@ func (x *LedgerInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use LedgerInfo.ProtoReflect.Descriptor instead. func (*LedgerInfo) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{86} + return file_common_proto_rawDescGZIP(), []int{88} } func (x *LedgerInfo) GetName() string { @@ -7283,6 +7365,13 @@ func (x *LedgerInfo) GetId() uint32 { return 0 } +func (x *LedgerInfo) GetIndexes() []*Index { + if x != nil { + return x.Indexes + } + return nil +} + // SaveMetadataCommand is used for adding metadata type SaveMetadataCommand struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -7294,7 +7383,7 @@ type SaveMetadataCommand struct { func (x *SaveMetadataCommand) Reset() { *x = SaveMetadataCommand{} - mi := &file_common_proto_msgTypes[87] + mi := &file_common_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7306,7 +7395,7 @@ func (x *SaveMetadataCommand) String() string { func (*SaveMetadataCommand) ProtoMessage() {} func (x *SaveMetadataCommand) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[87] + mi := &file_common_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7319,7 +7408,7 @@ func (x *SaveMetadataCommand) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveMetadataCommand.ProtoReflect.Descriptor instead. func (*SaveMetadataCommand) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{87} + return file_common_proto_rawDescGZIP(), []int{89} } func (x *SaveMetadataCommand) GetTarget() *Target { @@ -7347,7 +7436,7 @@ type DeleteMetadataCommand struct { func (x *DeleteMetadataCommand) Reset() { *x = DeleteMetadataCommand{} - mi := &file_common_proto_msgTypes[88] + mi := &file_common_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7359,7 +7448,7 @@ func (x *DeleteMetadataCommand) String() string { func (*DeleteMetadataCommand) ProtoMessage() {} func (x *DeleteMetadataCommand) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[88] + mi := &file_common_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7372,7 +7461,7 @@ func (x *DeleteMetadataCommand) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteMetadataCommand.ProtoReflect.Descriptor instead. func (*DeleteMetadataCommand) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{88} + return file_common_proto_rawDescGZIP(), []int{90} } func (x *DeleteMetadataCommand) GetTarget() *Target { @@ -7405,7 +7494,7 @@ type TransactionState struct { func (x *TransactionState) Reset() { *x = TransactionState{} - mi := &file_common_proto_msgTypes[89] + mi := &file_common_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7417,7 +7506,7 @@ func (x *TransactionState) String() string { func (*TransactionState) ProtoMessage() {} func (x *TransactionState) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[89] + mi := &file_common_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7430,7 +7519,7 @@ func (x *TransactionState) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionState.ProtoReflect.Descriptor instead. func (*TransactionState) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{89} + return file_common_proto_rawDescGZIP(), []int{91} } func (x *TransactionState) GetCreatedByLog() uint64 { @@ -7480,7 +7569,7 @@ type IdempotencyKeyValue struct { func (x *IdempotencyKeyValue) Reset() { *x = IdempotencyKeyValue{} - mi := &file_common_proto_msgTypes[90] + mi := &file_common_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7492,7 +7581,7 @@ func (x *IdempotencyKeyValue) String() string { func (*IdempotencyKeyValue) ProtoMessage() {} func (x *IdempotencyKeyValue) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[90] + mi := &file_common_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7505,7 +7594,7 @@ func (x *IdempotencyKeyValue) ProtoReflect() protoreflect.Message { // Deprecated: Use IdempotencyKeyValue.ProtoReflect.Descriptor instead. func (*IdempotencyKeyValue) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{90} + return file_common_proto_rawDescGZIP(), []int{92} } func (x *IdempotencyKeyValue) GetFirstLogSequence() uint64 { @@ -7566,7 +7655,7 @@ type IdempotencyFailure struct { func (x *IdempotencyFailure) Reset() { *x = IdempotencyFailure{} - mi := &file_common_proto_msgTypes[91] + mi := &file_common_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7578,7 +7667,7 @@ func (x *IdempotencyFailure) String() string { func (*IdempotencyFailure) ProtoMessage() {} func (x *IdempotencyFailure) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[91] + mi := &file_common_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7591,7 +7680,7 @@ func (x *IdempotencyFailure) ProtoReflect() protoreflect.Message { // Deprecated: Use IdempotencyFailure.ProtoReflect.Descriptor instead. func (*IdempotencyFailure) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{91} + return file_common_proto_rawDescGZIP(), []int{93} } func (x *IdempotencyFailure) GetReason() ErrorReason { @@ -7625,7 +7714,7 @@ type TransactionReferenceValue struct { func (x *TransactionReferenceValue) Reset() { *x = TransactionReferenceValue{} - mi := &file_common_proto_msgTypes[92] + mi := &file_common_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7637,7 +7726,7 @@ func (x *TransactionReferenceValue) String() string { func (*TransactionReferenceValue) ProtoMessage() {} func (x *TransactionReferenceValue) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[92] + mi := &file_common_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7650,7 +7739,7 @@ func (x *TransactionReferenceValue) ProtoReflect() protoreflect.Message { // Deprecated: Use TransactionReferenceValue.ProtoReflect.Descriptor instead. func (*TransactionReferenceValue) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{92} + return file_common_proto_rawDescGZIP(), []int{94} } func (x *TransactionReferenceValue) GetTransactionId() uint64 { @@ -7670,7 +7759,7 @@ type NumscriptVersionValue struct { func (x *NumscriptVersionValue) Reset() { *x = NumscriptVersionValue{} - mi := &file_common_proto_msgTypes[93] + mi := &file_common_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7682,7 +7771,7 @@ func (x *NumscriptVersionValue) String() string { func (*NumscriptVersionValue) ProtoMessage() {} func (x *NumscriptVersionValue) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[93] + mi := &file_common_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7695,7 +7784,7 @@ func (x *NumscriptVersionValue) ProtoReflect() protoreflect.Message { // Deprecated: Use NumscriptVersionValue.ProtoReflect.Descriptor instead. func (*NumscriptVersionValue) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{93} + return file_common_proto_rawDescGZIP(), []int{95} } func (x *NumscriptVersionValue) GetVersion() string { @@ -7722,7 +7811,7 @@ type SegmentType struct { func (x *SegmentType) Reset() { *x = SegmentType{} - mi := &file_common_proto_msgTypes[94] + mi := &file_common_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7734,7 +7823,7 @@ func (x *SegmentType) String() string { func (*SegmentType) ProtoMessage() {} func (x *SegmentType) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[94] + mi := &file_common_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7747,7 +7836,7 @@ func (x *SegmentType) ProtoReflect() protoreflect.Message { // Deprecated: Use SegmentType.ProtoReflect.Descriptor instead. func (*SegmentType) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{94} + return file_common_proto_rawDescGZIP(), []int{96} } func (x *SegmentType) GetConstraint() isSegmentType_Constraint { @@ -7829,7 +7918,7 @@ type UUIDConstraint struct { func (x *UUIDConstraint) Reset() { *x = UUIDConstraint{} - mi := &file_common_proto_msgTypes[95] + mi := &file_common_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7841,7 +7930,7 @@ func (x *UUIDConstraint) String() string { func (*UUIDConstraint) ProtoMessage() {} func (x *UUIDConstraint) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[95] + mi := &file_common_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7854,7 +7943,7 @@ func (x *UUIDConstraint) ProtoReflect() protoreflect.Message { // Deprecated: Use UUIDConstraint.ProtoReflect.Descriptor instead. func (*UUIDConstraint) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{95} + return file_common_proto_rawDescGZIP(), []int{97} } type Uint64Constraint struct { @@ -7865,7 +7954,7 @@ type Uint64Constraint struct { func (x *Uint64Constraint) Reset() { *x = Uint64Constraint{} - mi := &file_common_proto_msgTypes[96] + mi := &file_common_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7877,7 +7966,7 @@ func (x *Uint64Constraint) String() string { func (*Uint64Constraint) ProtoMessage() {} func (x *Uint64Constraint) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[96] + mi := &file_common_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7890,7 +7979,7 @@ func (x *Uint64Constraint) ProtoReflect() protoreflect.Message { // Deprecated: Use Uint64Constraint.ProtoReflect.Descriptor instead. func (*Uint64Constraint) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{96} + return file_common_proto_rawDescGZIP(), []int{98} } type BytesConstraint struct { @@ -7901,7 +7990,7 @@ type BytesConstraint struct { func (x *BytesConstraint) Reset() { *x = BytesConstraint{} - mi := &file_common_proto_msgTypes[97] + mi := &file_common_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7913,7 +8002,7 @@ func (x *BytesConstraint) String() string { func (*BytesConstraint) ProtoMessage() {} func (x *BytesConstraint) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[97] + mi := &file_common_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7926,7 +8015,7 @@ func (x *BytesConstraint) ProtoReflect() protoreflect.Message { // Deprecated: Use BytesConstraint.ProtoReflect.Descriptor instead. func (*BytesConstraint) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{97} + return file_common_proto_rawDescGZIP(), []int{99} } // AccountType defines a single account address pattern for a ledger. @@ -7942,7 +8031,7 @@ type AccountType struct { func (x *AccountType) Reset() { *x = AccountType{} - mi := &file_common_proto_msgTypes[98] + mi := &file_common_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7954,7 +8043,7 @@ func (x *AccountType) String() string { func (*AccountType) ProtoMessage() {} func (x *AccountType) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[98] + mi := &file_common_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7967,7 +8056,7 @@ func (x *AccountType) ProtoReflect() protoreflect.Message { // Deprecated: Use AccountType.ProtoReflect.Descriptor instead. func (*AccountType) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{98} + return file_common_proto_rawDescGZIP(), []int{100} } func (x *AccountType) GetName() string { @@ -8008,7 +8097,7 @@ type AddedAccountTypeLog struct { func (x *AddedAccountTypeLog) Reset() { *x = AddedAccountTypeLog{} - mi := &file_common_proto_msgTypes[99] + mi := &file_common_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8020,7 +8109,7 @@ func (x *AddedAccountTypeLog) String() string { func (*AddedAccountTypeLog) ProtoMessage() {} func (x *AddedAccountTypeLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[99] + mi := &file_common_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8033,7 +8122,7 @@ func (x *AddedAccountTypeLog) ProtoReflect() protoreflect.Message { // Deprecated: Use AddedAccountTypeLog.ProtoReflect.Descriptor instead. func (*AddedAccountTypeLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{99} + return file_common_proto_rawDescGZIP(), []int{101} } func (x *AddedAccountTypeLog) GetAccountType() *AccountType { @@ -8053,7 +8142,7 @@ type RemovedAccountTypeLog struct { func (x *RemovedAccountTypeLog) Reset() { *x = RemovedAccountTypeLog{} - mi := &file_common_proto_msgTypes[100] + mi := &file_common_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8065,7 +8154,7 @@ func (x *RemovedAccountTypeLog) String() string { func (*RemovedAccountTypeLog) ProtoMessage() {} func (x *RemovedAccountTypeLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[100] + mi := &file_common_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8078,7 +8167,7 @@ func (x *RemovedAccountTypeLog) ProtoReflect() protoreflect.Message { // Deprecated: Use RemovedAccountTypeLog.ProtoReflect.Descriptor instead. func (*RemovedAccountTypeLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{100} + return file_common_proto_rawDescGZIP(), []int{102} } func (x *RemovedAccountTypeLog) GetName() string { @@ -8098,7 +8187,7 @@ type UpdatedDefaultEnforcementModeLog struct { func (x *UpdatedDefaultEnforcementModeLog) Reset() { *x = UpdatedDefaultEnforcementModeLog{} - mi := &file_common_proto_msgTypes[101] + mi := &file_common_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8110,7 +8199,7 @@ func (x *UpdatedDefaultEnforcementModeLog) String() string { func (*UpdatedDefaultEnforcementModeLog) ProtoMessage() {} func (x *UpdatedDefaultEnforcementModeLog) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[101] + mi := &file_common_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8123,7 +8212,7 @@ func (x *UpdatedDefaultEnforcementModeLog) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdatedDefaultEnforcementModeLog.ProtoReflect.Descriptor instead. func (*UpdatedDefaultEnforcementModeLog) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{101} + return file_common_proto_rawDescGZIP(), []int{103} } func (x *UpdatedDefaultEnforcementModeLog) GetEnforcementMode() ChartEnforcementMode { @@ -8147,7 +8236,6 @@ type QueryFilter struct { // *QueryFilter_Ledger // *QueryFilter_LogId // *QueryFilter_LogBuiltinUint - // *QueryFilter_AccountHasAsset Filter isQueryFilter_Filter `protobuf_oneof:"filter"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -8155,7 +8243,7 @@ type QueryFilter struct { func (x *QueryFilter) Reset() { *x = QueryFilter{} - mi := &file_common_proto_msgTypes[102] + mi := &file_common_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8167,7 +8255,7 @@ func (x *QueryFilter) String() string { func (*QueryFilter) ProtoMessage() {} func (x *QueryFilter) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[102] + mi := &file_common_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8180,7 +8268,7 @@ func (x *QueryFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryFilter.ProtoReflect.Descriptor instead. func (*QueryFilter) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{102} + return file_common_proto_rawDescGZIP(), []int{104} } func (x *QueryFilter) GetFilter() isQueryFilter_Filter { @@ -8280,15 +8368,6 @@ func (x *QueryFilter) GetLogBuiltinUint() *LogBuiltinUintCondition { return nil } -func (x *QueryFilter) GetAccountHasAsset() *AccountHasAssetCondition { - if x != nil { - if x, ok := x.Filter.(*QueryFilter_AccountHasAsset); ok { - return x.AccountHasAsset - } - } - return nil -} - type isQueryFilter_Filter interface { isQueryFilter_Filter() } @@ -8333,10 +8412,6 @@ type QueryFilter_LogBuiltinUint struct { LogBuiltinUint *LogBuiltinUintCondition `protobuf:"bytes,10,opt,name=log_builtin_uint,json=logBuiltinUint,proto3,oneof"` } -type QueryFilter_AccountHasAsset struct { - AccountHasAsset *AccountHasAssetCondition `protobuf:"bytes,11,opt,name=account_has_asset,json=accountHasAsset,proto3,oneof"` -} - func (*QueryFilter_Field) isQueryFilter_Filter() {} func (*QueryFilter_Address) isQueryFilter_Filter() {} @@ -8357,8 +8432,6 @@ func (*QueryFilter_LogId) isQueryFilter_Filter() {} func (*QueryFilter_LogBuiltinUint) isQueryFilter_Filter() {} -func (*QueryFilter_AccountHasAsset) isQueryFilter_Filter() {} - // ReferenceCondition filters transactions by reference (exact match). type ReferenceCondition struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -8369,7 +8442,7 @@ type ReferenceCondition struct { func (x *ReferenceCondition) Reset() { *x = ReferenceCondition{} - mi := &file_common_proto_msgTypes[103] + mi := &file_common_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8381,7 +8454,7 @@ func (x *ReferenceCondition) String() string { func (*ReferenceCondition) ProtoMessage() {} func (x *ReferenceCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[103] + mi := &file_common_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8394,7 +8467,7 @@ func (x *ReferenceCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use ReferenceCondition.ProtoReflect.Descriptor instead. func (*ReferenceCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{103} + return file_common_proto_rawDescGZIP(), []int{105} } func (x *ReferenceCondition) GetCond() *StringCondition { @@ -8414,7 +8487,7 @@ type LedgerCondition struct { func (x *LedgerCondition) Reset() { *x = LedgerCondition{} - mi := &file_common_proto_msgTypes[104] + mi := &file_common_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8426,7 +8499,7 @@ func (x *LedgerCondition) String() string { func (*LedgerCondition) ProtoMessage() {} func (x *LedgerCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[104] + mi := &file_common_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8439,7 +8512,7 @@ func (x *LedgerCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use LedgerCondition.ProtoReflect.Descriptor instead. func (*LedgerCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{104} + return file_common_proto_rawDescGZIP(), []int{106} } func (x *LedgerCondition) GetCond() *StringCondition { @@ -8459,7 +8532,7 @@ type LogIdCondition struct { func (x *LogIdCondition) Reset() { *x = LogIdCondition{} - mi := &file_common_proto_msgTypes[105] + mi := &file_common_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8471,7 +8544,7 @@ func (x *LogIdCondition) String() string { func (*LogIdCondition) ProtoMessage() {} func (x *LogIdCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[105] + mi := &file_common_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8484,7 +8557,7 @@ func (x *LogIdCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use LogIdCondition.ProtoReflect.Descriptor instead. func (*LogIdCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{105} + return file_common_proto_rawDescGZIP(), []int{107} } func (x *LogIdCondition) GetCond() *UintCondition { @@ -8505,7 +8578,7 @@ type BuiltinUintCondition struct { func (x *BuiltinUintCondition) Reset() { *x = BuiltinUintCondition{} - mi := &file_common_proto_msgTypes[106] + mi := &file_common_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8517,7 +8590,7 @@ func (x *BuiltinUintCondition) String() string { func (*BuiltinUintCondition) ProtoMessage() {} func (x *BuiltinUintCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[106] + mi := &file_common_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8530,7 +8603,7 @@ func (x *BuiltinUintCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use BuiltinUintCondition.ProtoReflect.Descriptor instead. func (*BuiltinUintCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{106} + return file_common_proto_rawDescGZIP(), []int{108} } func (x *BuiltinUintCondition) GetField() TransactionBuiltinIndex { @@ -8558,7 +8631,7 @@ type LogBuiltinUintCondition struct { func (x *LogBuiltinUintCondition) Reset() { *x = LogBuiltinUintCondition{} - mi := &file_common_proto_msgTypes[107] + mi := &file_common_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8570,7 +8643,7 @@ func (x *LogBuiltinUintCondition) String() string { func (*LogBuiltinUintCondition) ProtoMessage() {} func (x *LogBuiltinUintCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[107] + mi := &file_common_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8583,7 +8656,7 @@ func (x *LogBuiltinUintCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use LogBuiltinUintCondition.ProtoReflect.Descriptor instead. func (*LogBuiltinUintCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{107} + return file_common_proto_rawDescGZIP(), []int{109} } func (x *LogBuiltinUintCondition) GetField() LogBuiltinIndex { @@ -8600,62 +8673,6 @@ func (x *LogBuiltinUintCondition) GetCond() *UintCondition { return nil } -// AccountHasAssetCondition matches accounts that have ever held a volume cell -// for the given asset (base + precision). Volume-cell presence semantics, not -// balance. Resolved exclusively via the ACCT_BUILTIN_INDEX_ASSET readstore -// index; there is no on-scan fallback. -type AccountHasAssetCondition struct { - state protoimpl.MessageState `protogen:"open.v1"` - AssetBase string `protobuf:"bytes,1,opt,name=asset_base,json=assetBase,proto3" json:"asset_base,omitempty"` - Precision uint32 `protobuf:"varint,2,opt,name=precision,proto3" json:"precision,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *AccountHasAssetCondition) Reset() { - *x = AccountHasAssetCondition{} - mi := &file_common_proto_msgTypes[108] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *AccountHasAssetCondition) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*AccountHasAssetCondition) ProtoMessage() {} - -func (x *AccountHasAssetCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[108] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use AccountHasAssetCondition.ProtoReflect.Descriptor instead. -func (*AccountHasAssetCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{108} -} - -func (x *AccountHasAssetCondition) GetAssetBase() string { - if x != nil { - return x.AssetBase - } - return "" -} - -func (x *AccountHasAssetCondition) GetPrecision() uint32 { - if x != nil { - return x.Precision - } - return 0 -} - type AndFilter struct { state protoimpl.MessageState `protogen:"open.v1"` Filters []*QueryFilter `protobuf:"bytes,1,rep,name=filters,proto3" json:"filters,omitempty"` @@ -8665,7 +8682,7 @@ type AndFilter struct { func (x *AndFilter) Reset() { *x = AndFilter{} - mi := &file_common_proto_msgTypes[109] + mi := &file_common_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8677,7 +8694,7 @@ func (x *AndFilter) String() string { func (*AndFilter) ProtoMessage() {} func (x *AndFilter) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[109] + mi := &file_common_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8690,7 +8707,7 @@ func (x *AndFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use AndFilter.ProtoReflect.Descriptor instead. func (*AndFilter) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{109} + return file_common_proto_rawDescGZIP(), []int{110} } func (x *AndFilter) GetFilters() []*QueryFilter { @@ -8709,7 +8726,7 @@ type OrFilter struct { func (x *OrFilter) Reset() { *x = OrFilter{} - mi := &file_common_proto_msgTypes[110] + mi := &file_common_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8721,7 +8738,7 @@ func (x *OrFilter) String() string { func (*OrFilter) ProtoMessage() {} func (x *OrFilter) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[110] + mi := &file_common_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8734,7 +8751,7 @@ func (x *OrFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use OrFilter.ProtoReflect.Descriptor instead. func (*OrFilter) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{110} + return file_common_proto_rawDescGZIP(), []int{111} } func (x *OrFilter) GetFilters() []*QueryFilter { @@ -8753,7 +8770,7 @@ type NotFilter struct { func (x *NotFilter) Reset() { *x = NotFilter{} - mi := &file_common_proto_msgTypes[111] + mi := &file_common_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8765,7 +8782,7 @@ func (x *NotFilter) String() string { func (*NotFilter) ProtoMessage() {} func (x *NotFilter) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[111] + mi := &file_common_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8778,7 +8795,7 @@ func (x *NotFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use NotFilter.ProtoReflect.Descriptor instead. func (*NotFilter) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{111} + return file_common_proto_rawDescGZIP(), []int{112} } func (x *NotFilter) GetFilter() *QueryFilter { @@ -8800,7 +8817,7 @@ type FieldRef struct { func (x *FieldRef) Reset() { *x = FieldRef{} - mi := &file_common_proto_msgTypes[112] + mi := &file_common_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8812,7 +8829,7 @@ func (x *FieldRef) String() string { func (*FieldRef) ProtoMessage() {} func (x *FieldRef) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[112] + mi := &file_common_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8825,7 +8842,7 @@ func (x *FieldRef) ProtoReflect() protoreflect.Message { // Deprecated: Use FieldRef.ProtoReflect.Descriptor instead. func (*FieldRef) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{112} + return file_common_proto_rawDescGZIP(), []int{113} } func (x *FieldRef) GetMetadata() string { @@ -8853,7 +8870,7 @@ type FieldCondition struct { func (x *FieldCondition) Reset() { *x = FieldCondition{} - mi := &file_common_proto_msgTypes[113] + mi := &file_common_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8865,7 +8882,7 @@ func (x *FieldCondition) String() string { func (*FieldCondition) ProtoMessage() {} func (x *FieldCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[113] + mi := &file_common_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8878,7 +8895,7 @@ func (x *FieldCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use FieldCondition.ProtoReflect.Descriptor instead. func (*FieldCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{113} + return file_common_proto_rawDescGZIP(), []int{114} } func (x *FieldCondition) GetField() *FieldRef { @@ -8987,7 +9004,7 @@ type StringCondition struct { func (x *StringCondition) Reset() { *x = StringCondition{} - mi := &file_common_proto_msgTypes[114] + mi := &file_common_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8999,7 +9016,7 @@ func (x *StringCondition) String() string { func (*StringCondition) ProtoMessage() {} func (x *StringCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[114] + mi := &file_common_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9012,7 +9029,7 @@ func (x *StringCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use StringCondition.ProtoReflect.Descriptor instead. func (*StringCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{114} + return file_common_proto_rawDescGZIP(), []int{115} } func (x *StringCondition) GetValue() isStringCondition_Value { @@ -9070,7 +9087,7 @@ type IntCondition struct { func (x *IntCondition) Reset() { *x = IntCondition{} - mi := &file_common_proto_msgTypes[115] + mi := &file_common_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9082,7 +9099,7 @@ func (x *IntCondition) String() string { func (*IntCondition) ProtoMessage() {} func (x *IntCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[115] + mi := &file_common_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9095,7 +9112,7 @@ func (x *IntCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use IntCondition.ProtoReflect.Descriptor instead. func (*IntCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{115} + return file_common_proto_rawDescGZIP(), []int{116} } func (x *IntCondition) GetMin() int64 { @@ -9154,7 +9171,7 @@ type UintCondition struct { func (x *UintCondition) Reset() { *x = UintCondition{} - mi := &file_common_proto_msgTypes[116] + mi := &file_common_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9166,7 +9183,7 @@ func (x *UintCondition) String() string { func (*UintCondition) ProtoMessage() {} func (x *UintCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[116] + mi := &file_common_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9179,7 +9196,7 @@ func (x *UintCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use UintCondition.ProtoReflect.Descriptor instead. func (*UintCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{116} + return file_common_proto_rawDescGZIP(), []int{117} } func (x *UintCondition) GetMin() uint64 { @@ -9237,7 +9254,7 @@ type BoolCondition struct { func (x *BoolCondition) Reset() { *x = BoolCondition{} - mi := &file_common_proto_msgTypes[117] + mi := &file_common_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9249,7 +9266,7 @@ func (x *BoolCondition) String() string { func (*BoolCondition) ProtoMessage() {} func (x *BoolCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[117] + mi := &file_common_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9262,7 +9279,7 @@ func (x *BoolCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use BoolCondition.ProtoReflect.Descriptor instead. func (*BoolCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{117} + return file_common_proto_rawDescGZIP(), []int{118} } func (x *BoolCondition) GetValue() isBoolCondition_Value { @@ -9315,7 +9332,7 @@ type ExistsCondition struct { func (x *ExistsCondition) Reset() { *x = ExistsCondition{} - mi := &file_common_proto_msgTypes[118] + mi := &file_common_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9327,7 +9344,7 @@ func (x *ExistsCondition) String() string { func (*ExistsCondition) ProtoMessage() {} func (x *ExistsCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[118] + mi := &file_common_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9340,7 +9357,7 @@ func (x *ExistsCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use ExistsCondition.ProtoReflect.Descriptor instead. func (*ExistsCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{118} + return file_common_proto_rawDescGZIP(), []int{119} } func (x *ExistsCondition) GetIncludeNull() bool { @@ -9366,7 +9383,7 @@ type AddressMatch struct { func (x *AddressMatch) Reset() { *x = AddressMatch{} - mi := &file_common_proto_msgTypes[119] + mi := &file_common_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9378,7 +9395,7 @@ func (x *AddressMatch) String() string { func (*AddressMatch) ProtoMessage() {} func (x *AddressMatch) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[119] + mi := &file_common_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9391,7 +9408,7 @@ func (x *AddressMatch) ProtoReflect() protoreflect.Message { // Deprecated: Use AddressMatch.ProtoReflect.Descriptor instead. func (*AddressMatch) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{119} + return file_common_proto_rawDescGZIP(), []int{120} } func (x *AddressMatch) GetMatch() isAddressMatch_Match { @@ -9487,7 +9504,7 @@ type PreparedQuery struct { func (x *PreparedQuery) Reset() { *x = PreparedQuery{} - mi := &file_common_proto_msgTypes[120] + mi := &file_common_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9499,7 +9516,7 @@ func (x *PreparedQuery) String() string { func (*PreparedQuery) ProtoMessage() {} func (x *PreparedQuery) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[120] + mi := &file_common_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9512,7 +9529,7 @@ func (x *PreparedQuery) ProtoReflect() protoreflect.Message { // Deprecated: Use PreparedQuery.ProtoReflect.Descriptor instead. func (*PreparedQuery) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{120} + return file_common_proto_rawDescGZIP(), []int{121} } func (x *PreparedQuery) GetName() string { @@ -9536,19 +9553,23 @@ func (x *PreparedQuery) GetTarget() QueryTarget { return QueryTarget_QUERY_TARGET_ACCOUNTS } -// AggregatedVolume represents per-asset aggregated input/output volumes. +// AggregatedVolume represents aggregated input/output volumes for a single +// (asset, color) bucket. When collapse_colors is requested on the aggregate +// query, all entries are produced with color = "" and amounts summed across +// colors. type AggregatedVolume struct { state protoimpl.MessageState `protogen:"open.v1"` Asset string `protobuf:"bytes,1,opt,name=asset,proto3" json:"asset,omitempty"` Input *Uint256 `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` Output *Uint256 `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"` + Color string `protobuf:"bytes,4,opt,name=color,proto3" json:"color,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *AggregatedVolume) Reset() { *x = AggregatedVolume{} - mi := &file_common_proto_msgTypes[121] + mi := &file_common_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9560,7 +9581,7 @@ func (x *AggregatedVolume) String() string { func (*AggregatedVolume) ProtoMessage() {} func (x *AggregatedVolume) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[121] + mi := &file_common_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9573,7 +9594,7 @@ func (x *AggregatedVolume) ProtoReflect() protoreflect.Message { // Deprecated: Use AggregatedVolume.ProtoReflect.Descriptor instead. func (*AggregatedVolume) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{121} + return file_common_proto_rawDescGZIP(), []int{122} } func (x *AggregatedVolume) GetAsset() string { @@ -9597,6 +9618,13 @@ func (x *AggregatedVolume) GetOutput() *Uint256 { return nil } +func (x *AggregatedVolume) GetColor() string { + if x != nil { + return x.Color + } + return "" +} + type AggregateResult struct { state protoimpl.MessageState `protogen:"open.v1"` Volumes []*AggregatedVolume `protobuf:"bytes,1,rep,name=volumes,proto3" json:"volumes,omitempty"` @@ -9608,7 +9636,7 @@ type AggregateResult struct { func (x *AggregateResult) Reset() { *x = AggregateResult{} - mi := &file_common_proto_msgTypes[122] + mi := &file_common_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9620,7 +9648,7 @@ func (x *AggregateResult) String() string { func (*AggregateResult) ProtoMessage() {} func (x *AggregateResult) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[122] + mi := &file_common_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9633,7 +9661,7 @@ func (x *AggregateResult) ProtoReflect() protoreflect.Message { // Deprecated: Use AggregateResult.ProtoReflect.Descriptor instead. func (*AggregateResult) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{122} + return file_common_proto_rawDescGZIP(), []int{123} } func (x *AggregateResult) GetVolumes() []*AggregatedVolume { @@ -9661,7 +9689,7 @@ type GroupedAggregateResult struct { func (x *GroupedAggregateResult) Reset() { *x = GroupedAggregateResult{} - mi := &file_common_proto_msgTypes[123] + mi := &file_common_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9673,7 +9701,7 @@ func (x *GroupedAggregateResult) String() string { func (*GroupedAggregateResult) ProtoMessage() {} func (x *GroupedAggregateResult) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[123] + mi := &file_common_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9686,7 +9714,7 @@ func (x *GroupedAggregateResult) ProtoReflect() protoreflect.Message { // Deprecated: Use GroupedAggregateResult.ProtoReflect.Descriptor instead. func (*GroupedAggregateResult) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{123} + return file_common_proto_rawDescGZIP(), []int{124} } func (x *GroupedAggregateResult) GetPrefix() string { @@ -9718,7 +9746,7 @@ type PreparedQueryCursor struct { func (x *PreparedQueryCursor) Reset() { *x = PreparedQueryCursor{} - mi := &file_common_proto_msgTypes[124] + mi := &file_common_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9730,7 +9758,7 @@ func (x *PreparedQueryCursor) String() string { func (*PreparedQueryCursor) ProtoMessage() {} func (x *PreparedQueryCursor) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[124] + mi := &file_common_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9743,7 +9771,7 @@ func (x *PreparedQueryCursor) ProtoReflect() protoreflect.Message { // Deprecated: Use PreparedQueryCursor.ProtoReflect.Descriptor instead. func (*PreparedQueryCursor) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{124} + return file_common_proto_rawDescGZIP(), []int{125} } func (x *PreparedQueryCursor) GetPageSize() uint32 { @@ -9807,7 +9835,7 @@ type LedgerStats struct { func (x *LedgerStats) Reset() { *x = LedgerStats{} - mi := &file_common_proto_msgTypes[125] + mi := &file_common_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9819,7 +9847,7 @@ func (x *LedgerStats) String() string { func (*LedgerStats) ProtoMessage() {} func (x *LedgerStats) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[125] + mi := &file_common_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9832,7 +9860,7 @@ func (x *LedgerStats) ProtoReflect() protoreflect.Message { // Deprecated: Use LedgerStats.ProtoReflect.Descriptor instead. func (*LedgerStats) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{125} + return file_common_proto_rawDescGZIP(), []int{126} } func (x *LedgerStats) GetTransactionCount() uint64 { @@ -9920,7 +9948,7 @@ type PersistedConfig struct { func (x *PersistedConfig) Reset() { *x = PersistedConfig{} - mi := &file_common_proto_msgTypes[126] + mi := &file_common_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9932,7 +9960,7 @@ func (x *PersistedConfig) String() string { func (*PersistedConfig) ProtoMessage() {} func (x *PersistedConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[126] + mi := &file_common_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9945,7 +9973,7 @@ func (x *PersistedConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use PersistedConfig.ProtoReflect.Descriptor instead. func (*PersistedConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{126} + return file_common_proto_rawDescGZIP(), []int{127} } func (x *PersistedConfig) GetNodeId() uint64 { @@ -9998,7 +10026,7 @@ type CallerIdentity struct { func (x *CallerIdentity) Reset() { *x = CallerIdentity{} - mi := &file_common_proto_msgTypes[127] + mi := &file_common_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10010,7 +10038,7 @@ func (x *CallerIdentity) String() string { func (*CallerIdentity) ProtoMessage() {} func (x *CallerIdentity) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[127] + mi := &file_common_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10023,7 +10051,7 @@ func (x *CallerIdentity) ProtoReflect() protoreflect.Message { // Deprecated: Use CallerIdentity.ProtoReflect.Descriptor instead. func (*CallerIdentity) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{127} + return file_common_proto_rawDescGZIP(), []int{128} } func (x *CallerIdentity) GetSubject() string { @@ -10094,7 +10122,7 @@ type CallerSnapshot struct { func (x *CallerSnapshot) Reset() { *x = CallerSnapshot{} - mi := &file_common_proto_msgTypes[128] + mi := &file_common_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10106,7 +10134,7 @@ func (x *CallerSnapshot) String() string { func (*CallerSnapshot) ProtoMessage() {} func (x *CallerSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[128] + mi := &file_common_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10119,7 +10147,7 @@ func (x *CallerSnapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use CallerSnapshot.ProtoReflect.Descriptor instead. func (*CallerSnapshot) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{128} + return file_common_proto_rawDescGZIP(), []int{129} } func (x *CallerSnapshot) GetIdentity() *CallerIdentity { @@ -10157,7 +10185,7 @@ type S3StorageConfig struct { func (x *S3StorageConfig) Reset() { *x = S3StorageConfig{} - mi := &file_common_proto_msgTypes[129] + mi := &file_common_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10169,7 +10197,7 @@ func (x *S3StorageConfig) String() string { func (*S3StorageConfig) ProtoMessage() {} func (x *S3StorageConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[129] + mi := &file_common_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10182,7 +10210,7 @@ func (x *S3StorageConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use S3StorageConfig.ProtoReflect.Descriptor instead. func (*S3StorageConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{129} + return file_common_proto_rawDescGZIP(), []int{130} } func (x *S3StorageConfig) GetBucket() string { @@ -10233,7 +10261,7 @@ type AzureStorageConfig struct { func (x *AzureStorageConfig) Reset() { *x = AzureStorageConfig{} - mi := &file_common_proto_msgTypes[130] + mi := &file_common_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10245,7 +10273,7 @@ func (x *AzureStorageConfig) String() string { func (*AzureStorageConfig) ProtoMessage() {} func (x *AzureStorageConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[130] + mi := &file_common_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10258,7 +10286,7 @@ func (x *AzureStorageConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use AzureStorageConfig.ProtoReflect.Descriptor instead. func (*AzureStorageConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{130} + return file_common_proto_rawDescGZIP(), []int{131} } func (x *AzureStorageConfig) GetAccountName() string { @@ -10305,7 +10333,7 @@ type BackupStorage struct { func (x *BackupStorage) Reset() { *x = BackupStorage{} - mi := &file_common_proto_msgTypes[131] + mi := &file_common_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10317,7 +10345,7 @@ func (x *BackupStorage) String() string { func (*BackupStorage) ProtoMessage() {} func (x *BackupStorage) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[131] + mi := &file_common_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10330,7 +10358,7 @@ func (x *BackupStorage) ProtoReflect() protoreflect.Message { // Deprecated: Use BackupStorage.ProtoReflect.Descriptor instead. func (*BackupStorage) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{131} + return file_common_proto_rawDescGZIP(), []int{132} } func (x *BackupStorage) GetProvider() isBackupStorage_Provider { @@ -10393,7 +10421,7 @@ type ReadOptions struct { func (x *ReadOptions) Reset() { *x = ReadOptions{} - mi := &file_common_proto_msgTypes[132] + mi := &file_common_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10405,7 +10433,7 @@ func (x *ReadOptions) String() string { func (*ReadOptions) ProtoMessage() {} func (x *ReadOptions) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[132] + mi := &file_common_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10418,7 +10446,7 @@ func (x *ReadOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadOptions.ProtoReflect.Descriptor instead. func (*ReadOptions) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{132} + return file_common_proto_rawDescGZIP(), []int{133} } func (x *ReadOptions) GetCheckpointId() uint64 { @@ -10470,7 +10498,7 @@ type ListOptions struct { func (x *ListOptions) Reset() { *x = ListOptions{} - mi := &file_common_proto_msgTypes[133] + mi := &file_common_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10482,7 +10510,7 @@ func (x *ListOptions) String() string { func (*ListOptions) ProtoMessage() {} func (x *ListOptions) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[133] + mi := &file_common_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10495,7 +10523,7 @@ func (x *ListOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use ListOptions.ProtoReflect.Descriptor instead. func (*ListOptions) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{133} + return file_common_proto_rawDescGZIP(), []int{134} } func (x *ListOptions) GetRead() *ReadOptions { @@ -10541,7 +10569,7 @@ const file_common_proto_rawDesc = "" + "\tTimestamp\x12\x12\n" + "\x04data\x18\x01 \x01(\x06R\x04data\"'\n" + "\tNullValue\x12\x1a\n" + - "\boriginal\x18\x01 \x01(\tR\boriginal\"\xfa\x01\n" + + "\boriginal\x18\x01 \x01(\tR\boriginal\"\xd1\x01\n" + "\rMetadataValue\x12#\n" + "\fstring_value\x18\x01 \x01(\tH\x00R\vstringValue\x12\x1d\n" + "\tint_value\x18\x02 \x01(\x03H\x00R\bintValue\x12\x1f\n" + @@ -10550,8 +10578,7 @@ const file_common_proto_rawDesc = "" + "\n" + "null_value\x18\x04 \x01(\v2\x11.common.NullValueH\x00R\tnullValue\x12\x1f\n" + "\n" + - "uint_value\x18\x05 \x01(\x04H\x00R\tuintValue\x12'\n" + - "\x0edatetime_value\x18\x06 \x01(\x03H\x00R\rdatetimeValueB\x06\n" + + "uint_value\x18\x05 \x01(\x04H\x00R\tuintValueB\x06\n" + "\x04type\"\x98\x01\n" + "\vMetadataMap\x127\n" + "\x06values\x18\x01 \x03(\v2\x1f.common.MetadataMap.ValuesEntryR\x06values\x1aP\n" + @@ -10570,12 +10597,13 @@ const file_common_proto_rawDesc = "" + "\x02v0\x18\x01 \x01(\x06R\x02v0\x12\x0e\n" + "\x02v1\x18\x02 \x01(\x06R\x02v1\x12\x0e\n" + "\x02v2\x18\x03 \x01(\x06R\x02v2\x12\x0e\n" + - "\x02v3\x18\x04 \x01(\x06R\x02v3\"\x82\x01\n" + + "\x02v3\x18\x04 \x01(\x06R\x02v3\"\x98\x01\n" + "\aPosting\x12\x16\n" + "\x06source\x18\x01 \x01(\tR\x06source\x12 \n" + "\vdestination\x18\x02 \x01(\tR\vdestination\x12'\n" + "\x06amount\x18\x03 \x01(\v2\x0f.common.Uint256R\x06amount\x12\x14\n" + - "\x05asset\x18\x04 \x01(\tR\x05asset\"\xe2\x03\n" + + "\x05asset\x18\x04 \x01(\tR\x05asset\x12\x14\n" + + "\x05color\x18\x05 \x01(\tR\x05color\"\xe2\x03\n" + "\vTransaction\x12+\n" + "\bpostings\x18\x01 \x03(\v2\x0f.common.PostingR\bpostings\x12=\n" + "\bmetadata\x18\x02 \x03(\v2!.common.Transaction.MetadataEntryR\bmetadata\x12/\n" + @@ -10605,17 +10633,22 @@ const file_common_proto_rawDesc = "" + "\x12VolumesWithBalance\x12\x14\n" + "\x05input\x18\x01 \x01(\tR\x05input\x12\x16\n" + "\x06output\x18\x02 \x01(\tR\x06output\x12\x18\n" + - "\abalance\x18\x03 \x01(\tR\abalance\"\x9e\x01\n" + - "\x0fVolumesByAssets\x12>\n" + - "\avolumes\x18\x01 \x03(\v2$.common.VolumesByAssets.VolumesEntryR\avolumes\x1aK\n" + - "\fVolumesEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x12%\n" + - "\x05value\x18\x02 \x01(\v2\x0f.common.VolumesR\x05value:\x028\x01\"\xd0\x01\n" + + "\abalance\x18\x03 \x01(\tR\abalance\"@\n" + + "\x0fVolumesByAssets\x12-\n" + + "\avolumes\x18\x01 \x03(\v2\x13.common.VolumeEntryR\avolumes\"d\n" + + "\vVolumeEntry\x12\x14\n" + + "\x05asset\x18\x01 \x01(\tR\x05asset\x12\x14\n" + + "\x05color\x18\x02 \x01(\tR\x05color\x12)\n" + + "\avolumes\x18\x03 \x01(\v2\x0f.common.VolumesR\avolumes\"\xd0\x01\n" + "\x11PostCommitVolumes\x12]\n" + "\x12volumes_by_account\x18\x01 \x03(\v2/.common.PostCommitVolumes.VolumesByAccountEntryR\x10volumesByAccount\x1a\\\n" + "\x15VolumesByAccountEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12-\n" + - "\x05value\x18\x02 \x01(\v2\x17.common.VolumesByAssetsR\x05value:\x028\x01\"\xe2\x03\n" + + "\x05value\x18\x02 \x01(\v2\x17.common.VolumesByAssetsR\x05value:\x028\x01\"q\n" + + "\rAccountVolume\x12\x14\n" + + "\x05asset\x18\x01 \x01(\tR\x05asset\x12\x14\n" + + "\x05color\x18\x02 \x01(\tR\x05color\x124\n" + + "\avolumes\x18\x03 \x01(\v2\x1a.common.VolumesWithBalanceR\avolumes\"\x83\x03\n" + "\aAccount\x12\x18\n" + "\aaddress\x18\x01 \x01(\tR\aaddress\x129\n" + "\bmetadata\x18\x02 \x03(\v2\x1d.common.Account.MetadataEntryR\bmetadata\x122\n" + @@ -10623,14 +10656,11 @@ const file_common_proto_rawDesc = "" + "firstUsage\x128\n" + "\x0einsertion_date\x18\x04 \x01(\v2\x11.common.TimestampR\rinsertionDate\x120\n" + "\n" + - "updated_at\x18\x05 \x01(\v2\x11.common.TimestampR\tupdatedAt\x126\n" + - "\avolumes\x18\x06 \x03(\v2\x1c.common.Account.VolumesEntryR\avolumes\x1aR\n" + + "updated_at\x18\x05 \x01(\v2\x11.common.TimestampR\tupdatedAt\x12/\n" + + "\avolumes\x18\x06 \x03(\v2\x15.common.AccountVolumeR\avolumes\x1aR\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12+\n" + - "\x05value\x18\x02 \x01(\v2\x15.common.MetadataValueR\x05value:\x028\x01\x1aV\n" + - "\fVolumesEntry\x12\x10\n" + - "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + - "\x05value\x18\x02 \x01(\v2\x1a.common.VolumesWithBalanceR\x05value:\x028\x01\"#\n" + + "\x05value\x18\x02 \x01(\v2\x15.common.MetadataValueR\x05value:\x028\x01\"#\n" + "\rTargetAccount\x12\x12\n" + "\x04addr\x18\x01 \x01(\tR\x04addr\"n\n" + "\x06Target\x121\n" + @@ -10668,7 +10698,7 @@ const file_common_proto_rawDesc = "" + "logBuiltin\x12F\n" + "\x0faccount_builtin\x18\x03 \x01(\x0e2\x1b.common.AccountBuiltinIndexH\x00R\x0eaccountBuiltin\x125\n" + "\bmetadata\x18\x04 \x01(\v2\x17.common.MetadataIndexIDH\x00R\bmetadataB\x06\n" + - "\x04kind\"\xbf\x02\n" + + "\x04kind\"\xed\x01\n" + "\x05Index\x12\x1f\n" + "\x02id\x18\x01 \x01(\v2\x0f.common.IndexIDR\x02id\x12;\n" + "\fbuild_status\x18\x02 \x01(\x0e2\x18.common.IndexBuildStatusR\vbuildStatus\x120\n" + @@ -10676,9 +10706,7 @@ const file_common_proto_rawDesc = "" + "created_at\x18\x03 \x01(\v2\x11.common.TimestampR\tcreatedAt\x125\n" + "\rlast_built_at\x18\x04 \x01(\v2\x11.common.TimestampR\vlastBuiltAt\x12\x1d\n" + "\n" + - "last_error\x18\x05 \x01(\tR\tlastError\x12\x16\n" + - "\x06ledger\x18\x06 \x01(\tR\x06ledger\x128\n" + - "\x18forward_encoding_version\x18\a \x01(\rR\x16forwardEncodingVersion\"\x1f\n" + + "last_error\x18\x05 \x01(\tR\tlastError\"\x1f\n" + "\vIdempotency\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\"=\n" + "\x10IdempotencyEntry\x12\x12\n" + @@ -10746,7 +10774,7 @@ const file_common_proto_rawDesc = "" + "\aenabled\x18\x01 \x01(\bR\aenabled\"O\n" + "\x0fBloomTypeConfig\x12#\n" + "\rexpected_keys\x18\x01 \x01(\x04R\fexpectedKeys\x12\x17\n" + - "\afp_rate\x18\x02 \x01(\x01R\x06fpRate\"\xcf\a\n" + + "\afp_rate\x18\x02 \x01(\x01R\x06fpRate\"\xc2\x06\n" + "\rClusterConfig\x12-\n" + "\x12rotation_threshold\x18\x01 \x01(\x04R\x11rotationThreshold\x12<\n" + "\rbloom_volumes\x18\x02 \x01(\v2\x17.common.BloomTypeConfigR\fbloomVolumes\x12>\n" + @@ -10760,9 +10788,7 @@ const file_common_proto_rawDesc = "" + "\x18bloom_numscript_contents\x18\n" + " \x01(\v2\x17.common.BloomTypeConfigR\x16bloomNumscriptContents\x12<\n" + "\x0ehash_algorithm\x18\v \x01(\x0e2\x15.common.HashAlgorithmR\rhashAlgorithm\x12K\n" + - "\x15bloom_ledger_metadata\x18\f \x01(\v2\x17.common.BloomTypeConfigR\x13bloomLedgerMetadata\x12M\n" + - "\x16bloom_prepared_queries\x18\r \x01(\v2\x17.common.BloomTypeConfigR\x14bloomPreparedQueries\x12<\n" + - "\rbloom_indexes\x18\x0e \x01(\v2\x17.common.BloomTypeConfigR\fbloomIndexes\"g\n" + + "\x15bloom_ledger_metadata\x18\f \x01(\v2\x17.common.BloomTypeConfigR\x13bloomLedgerMetadata\"g\n" + "\x15PersistedClusterState\x12-\n" + "\x06config\x18\x01 \x01(\v2\x15.common.ClusterConfigR\x06config\x12\x1f\n" + "\vcache_epoch\x18\x02 \x01(\x06R\n" + @@ -10894,10 +10920,11 @@ const file_common_proto_rawDesc = "" + "\x04data\x18\x01 \x01(\v2\x18.common.LedgerLogPayloadR\x04data\x12%\n" + "\x04date\x18\x02 \x01(\v2\x11.common.TimestampR\x04date\x12\x0e\n" + "\x02id\x18\x03 \x01(\x06R\x02id\x12<\n" + - "\x0epurged_volumes\x18\x04 \x03(\v2\x15.common.TouchedVolumeR\rpurgedVolumes\"?\n" + + "\x0epurged_volumes\x18\x04 \x03(\v2\x15.common.TouchedVolumeR\rpurgedVolumes\"U\n" + "\rTouchedVolume\x12\x18\n" + "\aaccount\x18\x01 \x01(\tR\aaccount\x12\x14\n" + - "\x05asset\x18\x02 \x01(\tR\x05asset\"\xc4\a\n" + + "\x05asset\x18\x02 \x01(\tR\x05asset\x12\x14\n" + + "\x05color\x18\x03 \x01(\tR\x05color\"\xc4\a\n" + "\x10LedgerLogPayload\x12M\n" + "\x13created_transaction\x18\x01 \x01(\v2\x1a.common.CreatedTransactionH\x00R\x12createdTransaction\x12P\n" + "\x14reverted_transaction\x18\x02 \x01(\v2\x1b.common.RevertedTransactionH\x00R\x13revertedTransaction\x12>\n" + @@ -11004,7 +11031,7 @@ const file_common_proto_rawDesc = "" + "\x06cursor\x18\x02 \x01(\x06R\x06cursor\x12(\n" + "\x10source_log_count\x18\x03 \x01(\x06R\x0esourceLogCount\x12%\n" + "\x0eremaining_logs\x18\x04 \x01(\x06R\rremainingLogs\x12-\n" + - "\x05error\x18\x05 \x01(\v2\x17.common.MirrorSyncErrorR\x05error\"\x97\x06\n" + + "\x05error\x18\x05 \x01(\v2\x17.common.MirrorSyncErrorR\x05error\"\xf2\x06\n" + "\n" + "LedgerInfo\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x120\n" + @@ -11020,13 +11047,15 @@ const file_common_proto_rawDesc = "" + " \x03(\v2$.common.LedgerInfo.AccountTypesEntryR\faccountTypes\x12V\n" + "\x18default_enforcement_mode\x18\v \x01(\x0e2\x1c.common.ChartEnforcementModeR\x16defaultEnforcementMode\x12<\n" + "\bmetadata\x18\f \x03(\v2 .common.LedgerInfo.MetadataEntryR\bmetadata\x12\x0e\n" + - "\x02id\x18\r \x01(\rR\x02id\x1aT\n" + + "\x02id\x18\r \x01(\rR\x02id\x12'\n" + + "\aindexes\x18\x0e \x03(\v2\r.common.IndexR\aindexes\x1aT\n" + "\x11AccountTypesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12)\n" + "\x05value\x18\x02 \x01(\v2\x13.common.AccountTypeR\x05value:\x028\x01\x1aR\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12+\n" + - "\x05value\x18\x02 \x01(\v2\x15.common.MetadataValueR\x05value:\x028\x01\"\xd8\x01\n" + + "\x05value\x18\x02 \x01(\v2\x15.common.MetadataValueR\x05value:\x028\x01J\x04\b\b\x10\tJ\x04\b\t\x10\n" + + "R\x0fbuiltin_indexesR\x13log_builtin_indexes\"\xd8\x01\n" + "\x13SaveMetadataCommand\x12&\n" + "\x06target\x18\x01 \x01(\v2\x0e.common.TargetR\x06target\x12E\n" + "\bmetadata\x18\x02 \x03(\v2).common.SaveMetadataCommand.MetadataEntryR\bmetadata\x1aR\n" + @@ -11086,7 +11115,7 @@ const file_common_proto_rawDesc = "" + "\x15RemovedAccountTypeLog\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"k\n" + " UpdatedDefaultEnforcementModeLog\x12G\n" + - "\x10enforcement_mode\x18\x01 \x01(\x0e2\x1c.common.ChartEnforcementModeR\x0fenforcementMode\"\xeb\x04\n" + + "\x10enforcement_mode\x18\x01 \x01(\x0e2\x1c.common.ChartEnforcementModeR\x0fenforcementMode\"\x9b\x04\n" + "\vQueryFilter\x12.\n" + "\x05field\x18\x01 \x01(\v2\x16.common.FieldConditionH\x00R\x05field\x120\n" + "\aaddress\x18\x02 \x01(\v2\x14.common.AddressMatchH\x00R\aaddress\x12%\n" + @@ -11098,8 +11127,7 @@ const file_common_proto_rawDesc = "" + "\x06ledger\x18\b \x01(\v2\x17.common.LedgerConditionH\x00R\x06ledger\x12/\n" + "\x06log_id\x18\t \x01(\v2\x16.common.LogIdConditionH\x00R\x05logId\x12K\n" + "\x10log_builtin_uint\x18\n" + - " \x01(\v2\x1f.common.LogBuiltinUintConditionH\x00R\x0elogBuiltinUint\x12N\n" + - "\x11account_has_asset\x18\v \x01(\v2 .common.AccountHasAssetConditionH\x00R\x0faccountHasAssetB\b\n" + + " \x01(\v2\x1f.common.LogBuiltinUintConditionH\x00R\x0elogBuiltinUintB\b\n" + "\x06filter\"A\n" + "\x12ReferenceCondition\x12+\n" + "\x04cond\x18\x01 \x01(\v2\x17.common.StringConditionR\x04cond\">\n" + @@ -11112,11 +11140,7 @@ const file_common_proto_rawDesc = "" + "\x04cond\x18\x02 \x01(\v2\x15.common.UintConditionR\x04cond\"s\n" + "\x17LogBuiltinUintCondition\x12-\n" + "\x05field\x18\x01 \x01(\x0e2\x17.common.LogBuiltinIndexR\x05field\x12)\n" + - "\x04cond\x18\x02 \x01(\v2\x15.common.UintConditionR\x04cond\"W\n" + - "\x18AccountHasAssetCondition\x12\x1d\n" + - "\n" + - "asset_base\x18\x01 \x01(\tR\tassetBase\x12\x1c\n" + - "\tprecision\x18\x02 \x01(\rR\tprecision\":\n" + + "\x04cond\x18\x02 \x01(\v2\x15.common.UintConditionR\x04cond\":\n" + "\tAndFilter\x12-\n" + "\afilters\x18\x01 \x03(\v2\x13.common.QueryFilterR\afilters\"9\n" + "\bOrFilter\x12-\n" + @@ -11174,11 +11198,12 @@ const file_common_proto_rawDesc = "" + "\rPreparedQuery\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12+\n" + "\x06filter\x18\x02 \x01(\v2\x13.common.QueryFilterR\x06filter\x12+\n" + - "\x06target\x18\x03 \x01(\x0e2\x13.common.QueryTargetR\x06target\"x\n" + + "\x06target\x18\x03 \x01(\x0e2\x13.common.QueryTargetR\x06target\"\x8e\x01\n" + "\x10AggregatedVolume\x12\x14\n" + "\x05asset\x18\x01 \x01(\tR\x05asset\x12%\n" + "\x05input\x18\x02 \x01(\v2\x0f.common.Uint256R\x05input\x12'\n" + - "\x06output\x18\x03 \x01(\v2\x0f.common.Uint256R\x06output\"}\n" + + "\x06output\x18\x03 \x01(\v2\x0f.common.Uint256R\x06output\x12\x14\n" + + "\x05color\x18\x04 \x01(\tR\x05color\"}\n" + "\x0fAggregateResult\x122\n" + "\avolumes\x18\x01 \x03(\v2\x18.common.AggregatedVolumeR\avolumes\x126\n" + "\x06groups\x18\x02 \x03(\v2\x1e.common.GroupedAggregateResultR\x06groups\"d\n" + @@ -11249,7 +11274,7 @@ const file_common_proto_rawDesc = "" + "TargetType\x12\x17\n" + "\x13TARGET_TYPE_ACCOUNT\x10\x00\x12\x1b\n" + "\x17TARGET_TYPE_TRANSACTION\x10\x01\x12\x16\n" + - "\x12TARGET_TYPE_LEDGER\x10\x02*\xa6\x02\n" + + "\x12TARGET_TYPE_LEDGER\x10\x02*\x8a\x02\n" + "\fMetadataType\x12\x18\n" + "\x14METADATA_TYPE_STRING\x10\x00\x12\x17\n" + "\x13METADATA_TYPE_INT64\x10\x01\x12\x16\n" + @@ -11260,9 +11285,7 @@ const file_common_proto_rawDesc = "" + "\x13METADATA_TYPE_INT32\x10\x06\x12\x17\n" + "\x13METADATA_TYPE_UINT8\x10\a\x12\x18\n" + "\x14METADATA_TYPE_UINT16\x10\b\x12\x18\n" + - "\x14METADATA_TYPE_UINT32\x10\t\x12\x1a\n" + - "\x16METADATA_TYPE_DATETIME\x10\n" + - "*u\n" + + "\x14METADATA_TYPE_UINT32\x10\t*u\n" + "\x10IndexBuildStatus\x12\"\n" + "\x1eINDEX_BUILD_STATUS_UNSPECIFIED\x10\x00\x12\x1f\n" + "\x1bINDEX_BUILD_STATUS_BUILDING\x10\x01\x12\x1c\n" + @@ -11274,10 +11297,9 @@ const file_common_proto_rawDesc = "" + "\x18TX_BUILTIN_INDEX_ADDRESS\x10\x03\x12#\n" + "\x1fTX_BUILTIN_INDEX_SOURCE_ADDRESS\x10\x04\x12!\n" + "\x1dTX_BUILTIN_INDEX_DEST_ADDRESS\x10\x05\x12 \n" + - "\x1cTX_BUILTIN_INDEX_INSERTED_AT\x10\x06*W\n" + + "\x1cTX_BUILTIN_INDEX_INSERTED_AT\x10\x06*9\n" + "\x13AccountBuiltinIndex\x12\"\n" + - "\x1eACCT_BUILTIN_INDEX_UNSPECIFIED\x10\x00\x12\x1c\n" + - "\x18ACCT_BUILTIN_INDEX_ASSET\x10\x01*j\n" + + "\x1eACCT_BUILTIN_INDEX_UNSPECIFIED\x10\x00*j\n" + "\x0fLogBuiltinIndex\x12!\n" + "\x1dLOG_BUILTIN_INDEX_UNSPECIFIED\x10\x00\x12\x1a\n" + "\x16LOG_BUILTIN_INDEX_DATE\x10\x01*\x18LOG_BUILTIN_INDEX_LEDGER*C\n" + @@ -11304,7 +11326,7 @@ const file_common_proto_rawDesc = "" + "\x12LEDGER_MODE_MIRROR\x10\x01*Q\n" + "\x0fMirrorSyncState\x12\x1d\n" + "\x19MIRROR_SYNC_STATE_SYNCING\x10\x00\x12\x1f\n" + - "\x1bMIRROR_SYNC_STATE_FOLLOWING\x10\x01*\xd8\x13\n" + + "\x1bMIRROR_SYNC_STATE_FOLLOWING\x10\x01*\xa1\x14\n" + "\vErrorReason\x12\x1c\n" + "\x18ERROR_REASON_UNSPECIFIED\x10\x00\x12&\n" + "\"ERROR_REASON_LEDGER_ALREADY_EXISTS\x10\x01\x12!\n" + @@ -11369,7 +11391,9 @@ const file_common_proto_rawDesc = "" + "%ERROR_REASON_NON_DETERMINISTIC_SCRIPT\x10;\x12\"\n" + "\x1eERROR_REASON_CLUSTER_UNHEALTHY\x10<\x12)\n" + "%ERROR_REASON_WRITES_BLOCKED_DISK_FULL\x10=\x12*\n" + - "&ERROR_REASON_WRITES_BLOCKED_CLOCK_SKEW\x10>*Q\n" + + "&ERROR_REASON_WRITES_BLOCKED_CLOCK_SKEW\x10>\x12#\n" + + "\x1fERROR_REASON_AGGREGATE_OVERFLOW\x10?\x12\"\n" + + "\x1eERROR_REASON_BALANCE_NOT_FOUND\x10@*Q\n" + "\x14ChartEnforcementMode\x12\x1c\n" + "\x18CHART_ENFORCEMENT_STRICT\x10\x00\x12\x1b\n" + "\x17CHART_ENFORCEMENT_AUDIT\x10\x01*i\n" + @@ -11402,7 +11426,7 @@ func file_common_proto_rawDescGZIP() []byte { } var file_common_proto_enumTypes = make([]protoimpl.EnumInfo, 17) -var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 154) +var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 153) var file_common_proto_goTypes = []any{ (TargetType)(0), // 0: common.TargetType (MetadataType)(0), // 1: common.MetadataType @@ -11433,383 +11457,380 @@ var file_common_proto_goTypes = []any{ (*Volumes)(nil), // 26: common.Volumes (*VolumesWithBalance)(nil), // 27: common.VolumesWithBalance (*VolumesByAssets)(nil), // 28: common.VolumesByAssets - (*PostCommitVolumes)(nil), // 29: common.PostCommitVolumes - (*Account)(nil), // 30: common.Account - (*TargetAccount)(nil), // 31: common.TargetAccount - (*Target)(nil), // 32: common.Target - (*MetadataFieldSchema)(nil), // 33: common.MetadataFieldSchema - (*MetadataSchema)(nil), // 34: common.MetadataSchema - (*SetMetadataFieldTypeCommand)(nil), // 35: common.SetMetadataFieldTypeCommand - (*MetadataIndexID)(nil), // 36: common.MetadataIndexID - (*IndexID)(nil), // 37: common.IndexID - (*Index)(nil), // 38: common.Index - (*Idempotency)(nil), // 39: common.Idempotency - (*IdempotencyEntry)(nil), // 40: common.IdempotencyEntry - (*Log)(nil), // 41: common.Log - (*LogPayload)(nil), // 42: common.LogPayload - (*PromotedLedgerLog)(nil), // 43: common.PromotedLedgerLog - (*RegisteredSigningKeyLog)(nil), // 44: common.RegisteredSigningKeyLog - (*RevokedSigningKeyLog)(nil), // 45: common.RevokedSigningKeyLog - (*SigningKey)(nil), // 46: common.SigningKey - (*SetSigningConfigLog)(nil), // 47: common.SetSigningConfigLog - (*AddedEventsSinkLog)(nil), // 48: common.AddedEventsSinkLog - (*RemovedEventsSinkLog)(nil), // 49: common.RemovedEventsSinkLog - (*SetMaintenanceModeLog)(nil), // 50: common.SetMaintenanceModeLog - (*BloomTypeConfig)(nil), // 51: common.BloomTypeConfig - (*ClusterConfig)(nil), // 52: common.ClusterConfig - (*PersistedClusterState)(nil), // 53: common.PersistedClusterState - (*SetChapterScheduleLog)(nil), // 54: common.SetChapterScheduleLog - (*DeletedChapterScheduleLog)(nil), // 55: common.DeletedChapterScheduleLog - (*CreatedPreparedQueryLog)(nil), // 56: common.CreatedPreparedQueryLog - (*UpdatedPreparedQueryLog)(nil), // 57: common.UpdatedPreparedQueryLog - (*DeletedPreparedQueryLog)(nil), // 58: common.DeletedPreparedQueryLog - (*SavedLedgerMetadataLog)(nil), // 59: common.SavedLedgerMetadataLog - (*DeletedLedgerMetadataLog)(nil), // 60: common.DeletedLedgerMetadataLog - (*NumscriptInfo)(nil), // 61: common.NumscriptInfo - (*SavedNumscriptLog)(nil), // 62: common.SavedNumscriptLog - (*DeletedNumscriptLog)(nil), // 63: common.DeletedNumscriptLog - (*SetQueryCheckpointScheduleLog)(nil), // 64: common.SetQueryCheckpointScheduleLog - (*DeletedQueryCheckpointScheduleLog)(nil), // 65: common.DeletedQueryCheckpointScheduleLog - (*CreatedQueryCheckpointLog)(nil), // 66: common.CreatedQueryCheckpointLog - (*DeletedQueryCheckpointLog)(nil), // 67: common.DeletedQueryCheckpointLog - (*SinkConfig)(nil), // 68: common.SinkConfig - (*SinkStatus)(nil), // 69: common.SinkStatus - (*SinkError)(nil), // 70: common.SinkError - (*NatsSinkConfig)(nil), // 71: common.NatsSinkConfig - (*ClickHouseSinkConfig)(nil), // 72: common.ClickHouseSinkConfig - (*KafkaSinkConfig)(nil), // 73: common.KafkaSinkConfig - (*HttpSinkConfig)(nil), // 74: common.HttpSinkConfig - (*DatabricksSinkConfig)(nil), // 75: common.DatabricksSinkConfig - (*DatabricksOAuthM2M)(nil), // 76: common.DatabricksOAuthM2M - (*CreatedLedgerLog)(nil), // 77: common.CreatedLedgerLog - (*DeletedLedgerLog)(nil), // 78: common.DeletedLedgerLog - (*ApplyLedgerLog)(nil), // 79: common.ApplyLedgerLog - (*LedgerLog)(nil), // 80: common.LedgerLog - (*TouchedVolume)(nil), // 81: common.TouchedVolume - (*LedgerLogPayload)(nil), // 82: common.LedgerLogPayload - (*CreatedIndexLog)(nil), // 83: common.CreatedIndexLog - (*DroppedIndexLog)(nil), // 84: common.DroppedIndexLog - (*FilledGapLog)(nil), // 85: common.FilledGapLog - (*CreatedTransaction)(nil), // 86: common.CreatedTransaction - (*RevertedTransaction)(nil), // 87: common.RevertedTransaction - (*SavedMetadata)(nil), // 88: common.SavedMetadata - (*DeletedMetadata)(nil), // 89: common.DeletedMetadata - (*SetMetadataFieldTypeLog)(nil), // 90: common.SetMetadataFieldTypeLog - (*RemovedMetadataFieldTypeLog)(nil), // 91: common.RemovedMetadataFieldTypeLog - (*Chapter)(nil), // 92: common.Chapter - (*ClosedChapterLog)(nil), // 93: common.ClosedChapterLog - (*SealedChapterLog)(nil), // 94: common.SealedChapterLog - (*ArchivedChapterLog)(nil), // 95: common.ArchivedChapterLog - (*ConfirmedArchiveChapterLog)(nil), // 96: common.ConfirmedArchiveChapterLog - (*MirrorSourceConfig)(nil), // 97: common.MirrorSourceConfig - (*HttpMirrorSourceConfig)(nil), // 98: common.HttpMirrorSourceConfig - (*OAuth2ClientCredentials)(nil), // 99: common.OAuth2ClientCredentials - (*PostgresMirrorSourceConfig)(nil), // 100: common.PostgresMirrorSourceConfig - (*MirrorSyncError)(nil), // 101: common.MirrorSyncError - (*MirrorSyncProgress)(nil), // 102: common.MirrorSyncProgress - (*LedgerInfo)(nil), // 103: common.LedgerInfo - (*SaveMetadataCommand)(nil), // 104: common.SaveMetadataCommand - (*DeleteMetadataCommand)(nil), // 105: common.DeleteMetadataCommand - (*TransactionState)(nil), // 106: common.TransactionState - (*IdempotencyKeyValue)(nil), // 107: common.IdempotencyKeyValue - (*IdempotencyFailure)(nil), // 108: common.IdempotencyFailure - (*TransactionReferenceValue)(nil), // 109: common.TransactionReferenceValue - (*NumscriptVersionValue)(nil), // 110: common.NumscriptVersionValue - (*SegmentType)(nil), // 111: common.SegmentType - (*UUIDConstraint)(nil), // 112: common.UUIDConstraint - (*Uint64Constraint)(nil), // 113: common.Uint64Constraint - (*BytesConstraint)(nil), // 114: common.BytesConstraint - (*AccountType)(nil), // 115: common.AccountType - (*AddedAccountTypeLog)(nil), // 116: common.AddedAccountTypeLog - (*RemovedAccountTypeLog)(nil), // 117: common.RemovedAccountTypeLog - (*UpdatedDefaultEnforcementModeLog)(nil), // 118: common.UpdatedDefaultEnforcementModeLog - (*QueryFilter)(nil), // 119: common.QueryFilter - (*ReferenceCondition)(nil), // 120: common.ReferenceCondition - (*LedgerCondition)(nil), // 121: common.LedgerCondition - (*LogIdCondition)(nil), // 122: common.LogIdCondition - (*BuiltinUintCondition)(nil), // 123: common.BuiltinUintCondition - (*LogBuiltinUintCondition)(nil), // 124: common.LogBuiltinUintCondition - (*AccountHasAssetCondition)(nil), // 125: common.AccountHasAssetCondition - (*AndFilter)(nil), // 126: common.AndFilter - (*OrFilter)(nil), // 127: common.OrFilter - (*NotFilter)(nil), // 128: common.NotFilter - (*FieldRef)(nil), // 129: common.FieldRef - (*FieldCondition)(nil), // 130: common.FieldCondition - (*StringCondition)(nil), // 131: common.StringCondition - (*IntCondition)(nil), // 132: common.IntCondition - (*UintCondition)(nil), // 133: common.UintCondition - (*BoolCondition)(nil), // 134: common.BoolCondition - (*ExistsCondition)(nil), // 135: common.ExistsCondition - (*AddressMatch)(nil), // 136: common.AddressMatch - (*PreparedQuery)(nil), // 137: common.PreparedQuery - (*AggregatedVolume)(nil), // 138: common.AggregatedVolume - (*AggregateResult)(nil), // 139: common.AggregateResult - (*GroupedAggregateResult)(nil), // 140: common.GroupedAggregateResult - (*PreparedQueryCursor)(nil), // 141: common.PreparedQueryCursor - (*LedgerStats)(nil), // 142: common.LedgerStats - (*PersistedConfig)(nil), // 143: common.PersistedConfig - (*CallerIdentity)(nil), // 144: common.CallerIdentity - (*CallerSnapshot)(nil), // 145: common.CallerSnapshot - (*S3StorageConfig)(nil), // 146: common.S3StorageConfig - (*AzureStorageConfig)(nil), // 147: common.AzureStorageConfig - (*BackupStorage)(nil), // 148: common.BackupStorage - (*ReadOptions)(nil), // 149: common.ReadOptions - (*ListOptions)(nil), // 150: common.ListOptions - nil, // 151: common.MetadataMap.ValuesEntry - nil, // 152: common.Transaction.MetadataEntry - nil, // 153: common.Script.VarsEntry - nil, // 154: common.VolumesByAssets.VolumesEntry + (*VolumeEntry)(nil), // 29: common.VolumeEntry + (*PostCommitVolumes)(nil), // 30: common.PostCommitVolumes + (*AccountVolume)(nil), // 31: common.AccountVolume + (*Account)(nil), // 32: common.Account + (*TargetAccount)(nil), // 33: common.TargetAccount + (*Target)(nil), // 34: common.Target + (*MetadataFieldSchema)(nil), // 35: common.MetadataFieldSchema + (*MetadataSchema)(nil), // 36: common.MetadataSchema + (*SetMetadataFieldTypeCommand)(nil), // 37: common.SetMetadataFieldTypeCommand + (*MetadataIndexID)(nil), // 38: common.MetadataIndexID + (*IndexID)(nil), // 39: common.IndexID + (*Index)(nil), // 40: common.Index + (*Idempotency)(nil), // 41: common.Idempotency + (*IdempotencyEntry)(nil), // 42: common.IdempotencyEntry + (*Log)(nil), // 43: common.Log + (*LogPayload)(nil), // 44: common.LogPayload + (*PromotedLedgerLog)(nil), // 45: common.PromotedLedgerLog + (*RegisteredSigningKeyLog)(nil), // 46: common.RegisteredSigningKeyLog + (*RevokedSigningKeyLog)(nil), // 47: common.RevokedSigningKeyLog + (*SigningKey)(nil), // 48: common.SigningKey + (*SetSigningConfigLog)(nil), // 49: common.SetSigningConfigLog + (*AddedEventsSinkLog)(nil), // 50: common.AddedEventsSinkLog + (*RemovedEventsSinkLog)(nil), // 51: common.RemovedEventsSinkLog + (*SetMaintenanceModeLog)(nil), // 52: common.SetMaintenanceModeLog + (*BloomTypeConfig)(nil), // 53: common.BloomTypeConfig + (*ClusterConfig)(nil), // 54: common.ClusterConfig + (*PersistedClusterState)(nil), // 55: common.PersistedClusterState + (*SetChapterScheduleLog)(nil), // 56: common.SetChapterScheduleLog + (*DeletedChapterScheduleLog)(nil), // 57: common.DeletedChapterScheduleLog + (*CreatedPreparedQueryLog)(nil), // 58: common.CreatedPreparedQueryLog + (*UpdatedPreparedQueryLog)(nil), // 59: common.UpdatedPreparedQueryLog + (*DeletedPreparedQueryLog)(nil), // 60: common.DeletedPreparedQueryLog + (*SavedLedgerMetadataLog)(nil), // 61: common.SavedLedgerMetadataLog + (*DeletedLedgerMetadataLog)(nil), // 62: common.DeletedLedgerMetadataLog + (*NumscriptInfo)(nil), // 63: common.NumscriptInfo + (*SavedNumscriptLog)(nil), // 64: common.SavedNumscriptLog + (*DeletedNumscriptLog)(nil), // 65: common.DeletedNumscriptLog + (*SetQueryCheckpointScheduleLog)(nil), // 66: common.SetQueryCheckpointScheduleLog + (*DeletedQueryCheckpointScheduleLog)(nil), // 67: common.DeletedQueryCheckpointScheduleLog + (*CreatedQueryCheckpointLog)(nil), // 68: common.CreatedQueryCheckpointLog + (*DeletedQueryCheckpointLog)(nil), // 69: common.DeletedQueryCheckpointLog + (*SinkConfig)(nil), // 70: common.SinkConfig + (*SinkStatus)(nil), // 71: common.SinkStatus + (*SinkError)(nil), // 72: common.SinkError + (*NatsSinkConfig)(nil), // 73: common.NatsSinkConfig + (*ClickHouseSinkConfig)(nil), // 74: common.ClickHouseSinkConfig + (*KafkaSinkConfig)(nil), // 75: common.KafkaSinkConfig + (*HttpSinkConfig)(nil), // 76: common.HttpSinkConfig + (*DatabricksSinkConfig)(nil), // 77: common.DatabricksSinkConfig + (*DatabricksOAuthM2M)(nil), // 78: common.DatabricksOAuthM2M + (*CreatedLedgerLog)(nil), // 79: common.CreatedLedgerLog + (*DeletedLedgerLog)(nil), // 80: common.DeletedLedgerLog + (*ApplyLedgerLog)(nil), // 81: common.ApplyLedgerLog + (*LedgerLog)(nil), // 82: common.LedgerLog + (*TouchedVolume)(nil), // 83: common.TouchedVolume + (*LedgerLogPayload)(nil), // 84: common.LedgerLogPayload + (*CreatedIndexLog)(nil), // 85: common.CreatedIndexLog + (*DroppedIndexLog)(nil), // 86: common.DroppedIndexLog + (*FilledGapLog)(nil), // 87: common.FilledGapLog + (*CreatedTransaction)(nil), // 88: common.CreatedTransaction + (*RevertedTransaction)(nil), // 89: common.RevertedTransaction + (*SavedMetadata)(nil), // 90: common.SavedMetadata + (*DeletedMetadata)(nil), // 91: common.DeletedMetadata + (*SetMetadataFieldTypeLog)(nil), // 92: common.SetMetadataFieldTypeLog + (*RemovedMetadataFieldTypeLog)(nil), // 93: common.RemovedMetadataFieldTypeLog + (*Chapter)(nil), // 94: common.Chapter + (*ClosedChapterLog)(nil), // 95: common.ClosedChapterLog + (*SealedChapterLog)(nil), // 96: common.SealedChapterLog + (*ArchivedChapterLog)(nil), // 97: common.ArchivedChapterLog + (*ConfirmedArchiveChapterLog)(nil), // 98: common.ConfirmedArchiveChapterLog + (*MirrorSourceConfig)(nil), // 99: common.MirrorSourceConfig + (*HttpMirrorSourceConfig)(nil), // 100: common.HttpMirrorSourceConfig + (*OAuth2ClientCredentials)(nil), // 101: common.OAuth2ClientCredentials + (*PostgresMirrorSourceConfig)(nil), // 102: common.PostgresMirrorSourceConfig + (*MirrorSyncError)(nil), // 103: common.MirrorSyncError + (*MirrorSyncProgress)(nil), // 104: common.MirrorSyncProgress + (*LedgerInfo)(nil), // 105: common.LedgerInfo + (*SaveMetadataCommand)(nil), // 106: common.SaveMetadataCommand + (*DeleteMetadataCommand)(nil), // 107: common.DeleteMetadataCommand + (*TransactionState)(nil), // 108: common.TransactionState + (*IdempotencyKeyValue)(nil), // 109: common.IdempotencyKeyValue + (*IdempotencyFailure)(nil), // 110: common.IdempotencyFailure + (*TransactionReferenceValue)(nil), // 111: common.TransactionReferenceValue + (*NumscriptVersionValue)(nil), // 112: common.NumscriptVersionValue + (*SegmentType)(nil), // 113: common.SegmentType + (*UUIDConstraint)(nil), // 114: common.UUIDConstraint + (*Uint64Constraint)(nil), // 115: common.Uint64Constraint + (*BytesConstraint)(nil), // 116: common.BytesConstraint + (*AccountType)(nil), // 117: common.AccountType + (*AddedAccountTypeLog)(nil), // 118: common.AddedAccountTypeLog + (*RemovedAccountTypeLog)(nil), // 119: common.RemovedAccountTypeLog + (*UpdatedDefaultEnforcementModeLog)(nil), // 120: common.UpdatedDefaultEnforcementModeLog + (*QueryFilter)(nil), // 121: common.QueryFilter + (*ReferenceCondition)(nil), // 122: common.ReferenceCondition + (*LedgerCondition)(nil), // 123: common.LedgerCondition + (*LogIdCondition)(nil), // 124: common.LogIdCondition + (*BuiltinUintCondition)(nil), // 125: common.BuiltinUintCondition + (*LogBuiltinUintCondition)(nil), // 126: common.LogBuiltinUintCondition + (*AndFilter)(nil), // 127: common.AndFilter + (*OrFilter)(nil), // 128: common.OrFilter + (*NotFilter)(nil), // 129: common.NotFilter + (*FieldRef)(nil), // 130: common.FieldRef + (*FieldCondition)(nil), // 131: common.FieldCondition + (*StringCondition)(nil), // 132: common.StringCondition + (*IntCondition)(nil), // 133: common.IntCondition + (*UintCondition)(nil), // 134: common.UintCondition + (*BoolCondition)(nil), // 135: common.BoolCondition + (*ExistsCondition)(nil), // 136: common.ExistsCondition + (*AddressMatch)(nil), // 137: common.AddressMatch + (*PreparedQuery)(nil), // 138: common.PreparedQuery + (*AggregatedVolume)(nil), // 139: common.AggregatedVolume + (*AggregateResult)(nil), // 140: common.AggregateResult + (*GroupedAggregateResult)(nil), // 141: common.GroupedAggregateResult + (*PreparedQueryCursor)(nil), // 142: common.PreparedQueryCursor + (*LedgerStats)(nil), // 143: common.LedgerStats + (*PersistedConfig)(nil), // 144: common.PersistedConfig + (*CallerIdentity)(nil), // 145: common.CallerIdentity + (*CallerSnapshot)(nil), // 146: common.CallerSnapshot + (*S3StorageConfig)(nil), // 147: common.S3StorageConfig + (*AzureStorageConfig)(nil), // 148: common.AzureStorageConfig + (*BackupStorage)(nil), // 149: common.BackupStorage + (*ReadOptions)(nil), // 150: common.ReadOptions + (*ListOptions)(nil), // 151: common.ListOptions + nil, // 152: common.MetadataMap.ValuesEntry + nil, // 153: common.Transaction.MetadataEntry + nil, // 154: common.Script.VarsEntry nil, // 155: common.PostCommitVolumes.VolumesByAccountEntry nil, // 156: common.Account.MetadataEntry - nil, // 157: common.Account.VolumesEntry - nil, // 158: common.MetadataSchema.AccountFieldsEntry - nil, // 159: common.MetadataSchema.TransactionFieldsEntry - nil, // 160: common.MetadataSchema.LedgerFieldsEntry - nil, // 161: common.SavedLedgerMetadataLog.MetadataEntry - nil, // 162: common.CreatedLedgerLog.AccountTypesEntry - nil, // 163: common.CreatedTransaction.AccountMetadataEntry - nil, // 164: common.SavedMetadata.MetadataEntry - nil, // 165: common.LedgerInfo.AccountTypesEntry - nil, // 166: common.LedgerInfo.MetadataEntry - nil, // 167: common.SaveMetadataCommand.MetadataEntry - nil, // 168: common.TransactionState.MetadataEntry - nil, // 169: common.IdempotencyFailure.MetadataEntry - nil, // 170: common.AccountType.SegmentTypesEntry - (*signaturepb.SignedLog)(nil), // 171: signature.SignedLog + nil, // 157: common.MetadataSchema.AccountFieldsEntry + nil, // 158: common.MetadataSchema.TransactionFieldsEntry + nil, // 159: common.MetadataSchema.LedgerFieldsEntry + nil, // 160: common.SavedLedgerMetadataLog.MetadataEntry + nil, // 161: common.CreatedLedgerLog.AccountTypesEntry + nil, // 162: common.CreatedTransaction.AccountMetadataEntry + nil, // 163: common.SavedMetadata.MetadataEntry + nil, // 164: common.LedgerInfo.AccountTypesEntry + nil, // 165: common.LedgerInfo.MetadataEntry + nil, // 166: common.SaveMetadataCommand.MetadataEntry + nil, // 167: common.TransactionState.MetadataEntry + nil, // 168: common.IdempotencyFailure.MetadataEntry + nil, // 169: common.AccountType.SegmentTypesEntry + (*signaturepb.SignedLog)(nil), // 170: signature.SignedLog } var file_common_proto_depIdxs = []int32{ 18, // 0: common.MetadataValue.null_value:type_name -> common.NullValue - 151, // 1: common.MetadataMap.values:type_name -> common.MetadataMap.ValuesEntry + 152, // 1: common.MetadataMap.values:type_name -> common.MetadataMap.ValuesEntry 22, // 2: common.Posting.amount:type_name -> common.Uint256 23, // 3: common.Transaction.postings:type_name -> common.Posting - 152, // 4: common.Transaction.metadata:type_name -> common.Transaction.MetadataEntry + 153, // 4: common.Transaction.metadata:type_name -> common.Transaction.MetadataEntry 17, // 5: common.Transaction.timestamp:type_name -> common.Timestamp 17, // 6: common.Transaction.inserted_at:type_name -> common.Timestamp 17, // 7: common.Transaction.updated_at:type_name -> common.Timestamp 17, // 8: common.Transaction.reverted_at:type_name -> common.Timestamp - 153, // 9: common.Script.vars:type_name -> common.Script.VarsEntry - 154, // 10: common.VolumesByAssets.volumes:type_name -> common.VolumesByAssets.VolumesEntry - 155, // 11: common.PostCommitVolumes.volumes_by_account:type_name -> common.PostCommitVolumes.VolumesByAccountEntry - 156, // 12: common.Account.metadata:type_name -> common.Account.MetadataEntry - 17, // 13: common.Account.first_usage:type_name -> common.Timestamp - 17, // 14: common.Account.insertion_date:type_name -> common.Timestamp - 17, // 15: common.Account.updated_at:type_name -> common.Timestamp - 157, // 16: common.Account.volumes:type_name -> common.Account.VolumesEntry - 31, // 17: common.Target.account:type_name -> common.TargetAccount - 1, // 18: common.MetadataFieldSchema.type:type_name -> common.MetadataType - 158, // 19: common.MetadataSchema.account_fields:type_name -> common.MetadataSchema.AccountFieldsEntry - 159, // 20: common.MetadataSchema.transaction_fields:type_name -> common.MetadataSchema.TransactionFieldsEntry - 160, // 21: common.MetadataSchema.ledger_fields:type_name -> common.MetadataSchema.LedgerFieldsEntry - 0, // 22: common.SetMetadataFieldTypeCommand.target_type:type_name -> common.TargetType - 1, // 23: common.SetMetadataFieldTypeCommand.type:type_name -> common.MetadataType - 0, // 24: common.MetadataIndexID.target:type_name -> common.TargetType - 3, // 25: common.IndexID.tx_builtin:type_name -> common.TransactionBuiltinIndex - 5, // 26: common.IndexID.log_builtin:type_name -> common.LogBuiltinIndex - 4, // 27: common.IndexID.account_builtin:type_name -> common.AccountBuiltinIndex - 36, // 28: common.IndexID.metadata:type_name -> common.MetadataIndexID - 37, // 29: common.Index.id:type_name -> common.IndexID - 2, // 30: common.Index.build_status:type_name -> common.IndexBuildStatus - 17, // 31: common.Index.created_at:type_name -> common.Timestamp - 17, // 32: common.Index.last_built_at:type_name -> common.Timestamp - 42, // 33: common.Log.payload:type_name -> common.LogPayload - 171, // 34: common.Log.response_signature:type_name -> signature.SignedLog - 77, // 35: common.LogPayload.create_ledger:type_name -> common.CreatedLedgerLog - 78, // 36: common.LogPayload.delete_ledger:type_name -> common.DeletedLedgerLog - 79, // 37: common.LogPayload.apply:type_name -> common.ApplyLedgerLog - 44, // 38: common.LogPayload.register_signing_key:type_name -> common.RegisteredSigningKeyLog - 45, // 39: common.LogPayload.revoke_signing_key:type_name -> common.RevokedSigningKeyLog - 47, // 40: common.LogPayload.set_signing_config:type_name -> common.SetSigningConfigLog - 48, // 41: common.LogPayload.added_events_sink:type_name -> common.AddedEventsSinkLog - 49, // 42: common.LogPayload.removed_events_sink:type_name -> common.RemovedEventsSinkLog - 93, // 43: common.LogPayload.close_chapter:type_name -> common.ClosedChapterLog - 94, // 44: common.LogPayload.seal_chapter:type_name -> common.SealedChapterLog - 95, // 45: common.LogPayload.archive_chapter:type_name -> common.ArchivedChapterLog - 96, // 46: common.LogPayload.confirm_archive_chapter:type_name -> common.ConfirmedArchiveChapterLog - 50, // 47: common.LogPayload.set_maintenance_mode:type_name -> common.SetMaintenanceModeLog - 54, // 48: common.LogPayload.set_chapter_schedule:type_name -> common.SetChapterScheduleLog - 55, // 49: common.LogPayload.delete_chapter_schedule:type_name -> common.DeletedChapterScheduleLog - 43, // 50: common.LogPayload.promote_ledger:type_name -> common.PromotedLedgerLog - 56, // 51: common.LogPayload.created_prepared_query:type_name -> common.CreatedPreparedQueryLog - 57, // 52: common.LogPayload.updated_prepared_query:type_name -> common.UpdatedPreparedQueryLog - 58, // 53: common.LogPayload.deleted_prepared_query:type_name -> common.DeletedPreparedQueryLog - 62, // 54: common.LogPayload.saved_numscript:type_name -> common.SavedNumscriptLog - 63, // 55: common.LogPayload.deleted_numscript:type_name -> common.DeletedNumscriptLog - 66, // 56: common.LogPayload.created_query_checkpoint:type_name -> common.CreatedQueryCheckpointLog - 67, // 57: common.LogPayload.deleted_query_checkpoint:type_name -> common.DeletedQueryCheckpointLog - 64, // 58: common.LogPayload.set_query_checkpoint_schedule:type_name -> common.SetQueryCheckpointScheduleLog - 65, // 59: common.LogPayload.delete_query_checkpoint_schedule:type_name -> common.DeletedQueryCheckpointScheduleLog - 59, // 60: common.LogPayload.saved_ledger_metadata:type_name -> common.SavedLedgerMetadataLog - 60, // 61: common.LogPayload.deleted_ledger_metadata:type_name -> common.DeletedLedgerMetadataLog - 68, // 62: common.AddedEventsSinkLog.config:type_name -> common.SinkConfig - 51, // 63: common.ClusterConfig.bloom_volumes:type_name -> common.BloomTypeConfig - 51, // 64: common.ClusterConfig.bloom_metadata:type_name -> common.BloomTypeConfig - 51, // 65: common.ClusterConfig.bloom_references:type_name -> common.BloomTypeConfig - 51, // 66: common.ClusterConfig.bloom_ledgers:type_name -> common.BloomTypeConfig - 51, // 67: common.ClusterConfig.bloom_boundaries:type_name -> common.BloomTypeConfig - 51, // 68: common.ClusterConfig.bloom_transactions:type_name -> common.BloomTypeConfig - 51, // 69: common.ClusterConfig.bloom_sink_configs:type_name -> common.BloomTypeConfig - 51, // 70: common.ClusterConfig.bloom_numscript_versions:type_name -> common.BloomTypeConfig - 51, // 71: common.ClusterConfig.bloom_numscript_contents:type_name -> common.BloomTypeConfig - 6, // 72: common.ClusterConfig.hash_algorithm:type_name -> common.HashAlgorithm - 51, // 73: common.ClusterConfig.bloom_ledger_metadata:type_name -> common.BloomTypeConfig - 51, // 74: common.ClusterConfig.bloom_prepared_queries:type_name -> common.BloomTypeConfig - 51, // 75: common.ClusterConfig.bloom_indexes:type_name -> common.BloomTypeConfig - 52, // 76: common.PersistedClusterState.config:type_name -> common.ClusterConfig - 137, // 77: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery - 119, // 78: common.UpdatedPreparedQueryLog.previous_filter:type_name -> common.QueryFilter - 119, // 79: common.UpdatedPreparedQueryLog.new_filter:type_name -> common.QueryFilter - 161, // 80: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry + 154, // 9: common.Script.vars:type_name -> common.Script.VarsEntry + 29, // 10: common.VolumesByAssets.volumes:type_name -> common.VolumeEntry + 26, // 11: common.VolumeEntry.volumes:type_name -> common.Volumes + 155, // 12: common.PostCommitVolumes.volumes_by_account:type_name -> common.PostCommitVolumes.VolumesByAccountEntry + 27, // 13: common.AccountVolume.volumes:type_name -> common.VolumesWithBalance + 156, // 14: common.Account.metadata:type_name -> common.Account.MetadataEntry + 17, // 15: common.Account.first_usage:type_name -> common.Timestamp + 17, // 16: common.Account.insertion_date:type_name -> common.Timestamp + 17, // 17: common.Account.updated_at:type_name -> common.Timestamp + 31, // 18: common.Account.volumes:type_name -> common.AccountVolume + 33, // 19: common.Target.account:type_name -> common.TargetAccount + 1, // 20: common.MetadataFieldSchema.type:type_name -> common.MetadataType + 157, // 21: common.MetadataSchema.account_fields:type_name -> common.MetadataSchema.AccountFieldsEntry + 158, // 22: common.MetadataSchema.transaction_fields:type_name -> common.MetadataSchema.TransactionFieldsEntry + 159, // 23: common.MetadataSchema.ledger_fields:type_name -> common.MetadataSchema.LedgerFieldsEntry + 0, // 24: common.SetMetadataFieldTypeCommand.target_type:type_name -> common.TargetType + 1, // 25: common.SetMetadataFieldTypeCommand.type:type_name -> common.MetadataType + 0, // 26: common.MetadataIndexID.target:type_name -> common.TargetType + 3, // 27: common.IndexID.tx_builtin:type_name -> common.TransactionBuiltinIndex + 5, // 28: common.IndexID.log_builtin:type_name -> common.LogBuiltinIndex + 4, // 29: common.IndexID.account_builtin:type_name -> common.AccountBuiltinIndex + 38, // 30: common.IndexID.metadata:type_name -> common.MetadataIndexID + 39, // 31: common.Index.id:type_name -> common.IndexID + 2, // 32: common.Index.build_status:type_name -> common.IndexBuildStatus + 17, // 33: common.Index.created_at:type_name -> common.Timestamp + 17, // 34: common.Index.last_built_at:type_name -> common.Timestamp + 44, // 35: common.Log.payload:type_name -> common.LogPayload + 170, // 36: common.Log.response_signature:type_name -> signature.SignedLog + 79, // 37: common.LogPayload.create_ledger:type_name -> common.CreatedLedgerLog + 80, // 38: common.LogPayload.delete_ledger:type_name -> common.DeletedLedgerLog + 81, // 39: common.LogPayload.apply:type_name -> common.ApplyLedgerLog + 46, // 40: common.LogPayload.register_signing_key:type_name -> common.RegisteredSigningKeyLog + 47, // 41: common.LogPayload.revoke_signing_key:type_name -> common.RevokedSigningKeyLog + 49, // 42: common.LogPayload.set_signing_config:type_name -> common.SetSigningConfigLog + 50, // 43: common.LogPayload.added_events_sink:type_name -> common.AddedEventsSinkLog + 51, // 44: common.LogPayload.removed_events_sink:type_name -> common.RemovedEventsSinkLog + 95, // 45: common.LogPayload.close_chapter:type_name -> common.ClosedChapterLog + 96, // 46: common.LogPayload.seal_chapter:type_name -> common.SealedChapterLog + 97, // 47: common.LogPayload.archive_chapter:type_name -> common.ArchivedChapterLog + 98, // 48: common.LogPayload.confirm_archive_chapter:type_name -> common.ConfirmedArchiveChapterLog + 52, // 49: common.LogPayload.set_maintenance_mode:type_name -> common.SetMaintenanceModeLog + 56, // 50: common.LogPayload.set_chapter_schedule:type_name -> common.SetChapterScheduleLog + 57, // 51: common.LogPayload.delete_chapter_schedule:type_name -> common.DeletedChapterScheduleLog + 45, // 52: common.LogPayload.promote_ledger:type_name -> common.PromotedLedgerLog + 58, // 53: common.LogPayload.created_prepared_query:type_name -> common.CreatedPreparedQueryLog + 59, // 54: common.LogPayload.updated_prepared_query:type_name -> common.UpdatedPreparedQueryLog + 60, // 55: common.LogPayload.deleted_prepared_query:type_name -> common.DeletedPreparedQueryLog + 64, // 56: common.LogPayload.saved_numscript:type_name -> common.SavedNumscriptLog + 65, // 57: common.LogPayload.deleted_numscript:type_name -> common.DeletedNumscriptLog + 68, // 58: common.LogPayload.created_query_checkpoint:type_name -> common.CreatedQueryCheckpointLog + 69, // 59: common.LogPayload.deleted_query_checkpoint:type_name -> common.DeletedQueryCheckpointLog + 66, // 60: common.LogPayload.set_query_checkpoint_schedule:type_name -> common.SetQueryCheckpointScheduleLog + 67, // 61: common.LogPayload.delete_query_checkpoint_schedule:type_name -> common.DeletedQueryCheckpointScheduleLog + 61, // 62: common.LogPayload.saved_ledger_metadata:type_name -> common.SavedLedgerMetadataLog + 62, // 63: common.LogPayload.deleted_ledger_metadata:type_name -> common.DeletedLedgerMetadataLog + 70, // 64: common.AddedEventsSinkLog.config:type_name -> common.SinkConfig + 53, // 65: common.ClusterConfig.bloom_volumes:type_name -> common.BloomTypeConfig + 53, // 66: common.ClusterConfig.bloom_metadata:type_name -> common.BloomTypeConfig + 53, // 67: common.ClusterConfig.bloom_references:type_name -> common.BloomTypeConfig + 53, // 68: common.ClusterConfig.bloom_ledgers:type_name -> common.BloomTypeConfig + 53, // 69: common.ClusterConfig.bloom_boundaries:type_name -> common.BloomTypeConfig + 53, // 70: common.ClusterConfig.bloom_transactions:type_name -> common.BloomTypeConfig + 53, // 71: common.ClusterConfig.bloom_sink_configs:type_name -> common.BloomTypeConfig + 53, // 72: common.ClusterConfig.bloom_numscript_versions:type_name -> common.BloomTypeConfig + 53, // 73: common.ClusterConfig.bloom_numscript_contents:type_name -> common.BloomTypeConfig + 6, // 74: common.ClusterConfig.hash_algorithm:type_name -> common.HashAlgorithm + 53, // 75: common.ClusterConfig.bloom_ledger_metadata:type_name -> common.BloomTypeConfig + 54, // 76: common.PersistedClusterState.config:type_name -> common.ClusterConfig + 138, // 77: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery + 121, // 78: common.UpdatedPreparedQueryLog.previous_filter:type_name -> common.QueryFilter + 121, // 79: common.UpdatedPreparedQueryLog.new_filter:type_name -> common.QueryFilter + 160, // 80: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry 17, // 81: common.NumscriptInfo.created_at:type_name -> common.Timestamp - 61, // 82: common.SavedNumscriptLog.info:type_name -> common.NumscriptInfo - 71, // 83: common.SinkConfig.nats:type_name -> common.NatsSinkConfig - 72, // 84: common.SinkConfig.clickhouse:type_name -> common.ClickHouseSinkConfig - 73, // 85: common.SinkConfig.kafka:type_name -> common.KafkaSinkConfig - 74, // 86: common.SinkConfig.http:type_name -> common.HttpSinkConfig - 75, // 87: common.SinkConfig.databricks:type_name -> common.DatabricksSinkConfig + 63, // 82: common.SavedNumscriptLog.info:type_name -> common.NumscriptInfo + 73, // 83: common.SinkConfig.nats:type_name -> common.NatsSinkConfig + 74, // 84: common.SinkConfig.clickhouse:type_name -> common.ClickHouseSinkConfig + 75, // 85: common.SinkConfig.kafka:type_name -> common.KafkaSinkConfig + 76, // 86: common.SinkConfig.http:type_name -> common.HttpSinkConfig + 77, // 87: common.SinkConfig.databricks:type_name -> common.DatabricksSinkConfig 7, // 88: common.SinkConfig.event_types:type_name -> common.EventType - 70, // 89: common.SinkStatus.error:type_name -> common.SinkError + 72, // 89: common.SinkStatus.error:type_name -> common.SinkError 17, // 90: common.SinkError.occurred_at:type_name -> common.Timestamp - 76, // 91: common.DatabricksSinkConfig.oauth_m2m:type_name -> common.DatabricksOAuthM2M + 78, // 91: common.DatabricksSinkConfig.oauth_m2m:type_name -> common.DatabricksOAuthM2M 17, // 92: common.CreatedLedgerLog.created_at:type_name -> common.Timestamp - 34, // 93: common.CreatedLedgerLog.metadata_schema:type_name -> common.MetadataSchema + 36, // 93: common.CreatedLedgerLog.metadata_schema:type_name -> common.MetadataSchema 9, // 94: common.CreatedLedgerLog.mode:type_name -> common.LedgerMode - 97, // 95: common.CreatedLedgerLog.mirror_source:type_name -> common.MirrorSourceConfig - 162, // 96: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry + 99, // 95: common.CreatedLedgerLog.mirror_source:type_name -> common.MirrorSourceConfig + 161, // 96: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry 12, // 97: common.CreatedLedgerLog.default_enforcement_mode:type_name -> common.ChartEnforcementMode 17, // 98: common.DeletedLedgerLog.deleted_at:type_name -> common.Timestamp - 80, // 99: common.ApplyLedgerLog.log:type_name -> common.LedgerLog - 82, // 100: common.LedgerLog.data:type_name -> common.LedgerLogPayload + 82, // 99: common.ApplyLedgerLog.log:type_name -> common.LedgerLog + 84, // 100: common.LedgerLog.data:type_name -> common.LedgerLogPayload 17, // 101: common.LedgerLog.date:type_name -> common.Timestamp - 81, // 102: common.LedgerLog.purged_volumes:type_name -> common.TouchedVolume - 86, // 103: common.LedgerLogPayload.created_transaction:type_name -> common.CreatedTransaction - 87, // 104: common.LedgerLogPayload.reverted_transaction:type_name -> common.RevertedTransaction - 88, // 105: common.LedgerLogPayload.saved_metadata:type_name -> common.SavedMetadata - 89, // 106: common.LedgerLogPayload.deleted_metadata:type_name -> common.DeletedMetadata - 90, // 107: common.LedgerLogPayload.set_metadata_field_type:type_name -> common.SetMetadataFieldTypeLog - 91, // 108: common.LedgerLogPayload.removed_metadata_field_type:type_name -> common.RemovedMetadataFieldTypeLog - 85, // 109: common.LedgerLogPayload.fill_gap:type_name -> common.FilledGapLog - 83, // 110: common.LedgerLogPayload.create_index:type_name -> common.CreatedIndexLog - 84, // 111: common.LedgerLogPayload.drop_index:type_name -> common.DroppedIndexLog - 116, // 112: common.LedgerLogPayload.added_account_type:type_name -> common.AddedAccountTypeLog - 117, // 113: common.LedgerLogPayload.removed_account_type:type_name -> common.RemovedAccountTypeLog - 118, // 114: common.LedgerLogPayload.updated_default_enforcement_mode:type_name -> common.UpdatedDefaultEnforcementModeLog - 37, // 115: common.CreatedIndexLog.id:type_name -> common.IndexID - 37, // 116: common.DroppedIndexLog.id:type_name -> common.IndexID + 83, // 102: common.LedgerLog.purged_volumes:type_name -> common.TouchedVolume + 88, // 103: common.LedgerLogPayload.created_transaction:type_name -> common.CreatedTransaction + 89, // 104: common.LedgerLogPayload.reverted_transaction:type_name -> common.RevertedTransaction + 90, // 105: common.LedgerLogPayload.saved_metadata:type_name -> common.SavedMetadata + 91, // 106: common.LedgerLogPayload.deleted_metadata:type_name -> common.DeletedMetadata + 92, // 107: common.LedgerLogPayload.set_metadata_field_type:type_name -> common.SetMetadataFieldTypeLog + 93, // 108: common.LedgerLogPayload.removed_metadata_field_type:type_name -> common.RemovedMetadataFieldTypeLog + 87, // 109: common.LedgerLogPayload.fill_gap:type_name -> common.FilledGapLog + 85, // 110: common.LedgerLogPayload.create_index:type_name -> common.CreatedIndexLog + 86, // 111: common.LedgerLogPayload.drop_index:type_name -> common.DroppedIndexLog + 118, // 112: common.LedgerLogPayload.added_account_type:type_name -> common.AddedAccountTypeLog + 119, // 113: common.LedgerLogPayload.removed_account_type:type_name -> common.RemovedAccountTypeLog + 120, // 114: common.LedgerLogPayload.updated_default_enforcement_mode:type_name -> common.UpdatedDefaultEnforcementModeLog + 39, // 115: common.CreatedIndexLog.id:type_name -> common.IndexID + 39, // 116: common.DroppedIndexLog.id:type_name -> common.IndexID 24, // 117: common.CreatedTransaction.transaction:type_name -> common.Transaction - 163, // 118: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry - 29, // 119: common.CreatedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes + 162, // 118: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry + 30, // 119: common.CreatedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes 24, // 120: common.RevertedTransaction.revert_transaction:type_name -> common.Transaction - 29, // 121: common.RevertedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes - 32, // 122: common.SavedMetadata.target:type_name -> common.Target - 164, // 123: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry - 32, // 124: common.DeletedMetadata.target:type_name -> common.Target + 30, // 121: common.RevertedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes + 34, // 122: common.SavedMetadata.target:type_name -> common.Target + 163, // 123: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry + 34, // 124: common.DeletedMetadata.target:type_name -> common.Target 0, // 125: common.SetMetadataFieldTypeLog.target_type:type_name -> common.TargetType 1, // 126: common.SetMetadataFieldTypeLog.type:type_name -> common.MetadataType 0, // 127: common.RemovedMetadataFieldTypeLog.target_type:type_name -> common.TargetType - 37, // 128: common.RemovedMetadataFieldTypeLog.dropped_index:type_name -> common.IndexID + 39, // 128: common.RemovedMetadataFieldTypeLog.dropped_index:type_name -> common.IndexID 17, // 129: common.Chapter.start:type_name -> common.Timestamp 17, // 130: common.Chapter.end:type_name -> common.Timestamp 8, // 131: common.Chapter.status:type_name -> common.ChapterStatus - 92, // 132: common.ClosedChapterLog.closed_chapter:type_name -> common.Chapter - 92, // 133: common.ClosedChapterLog.new_chapter:type_name -> common.Chapter - 92, // 134: common.SealedChapterLog.chapter:type_name -> common.Chapter - 92, // 135: common.ArchivedChapterLog.chapter:type_name -> common.Chapter - 92, // 136: common.ConfirmedArchiveChapterLog.chapter:type_name -> common.Chapter - 98, // 137: common.MirrorSourceConfig.http:type_name -> common.HttpMirrorSourceConfig - 100, // 138: common.MirrorSourceConfig.postgres:type_name -> common.PostgresMirrorSourceConfig - 99, // 139: common.HttpMirrorSourceConfig.oauth2_client_credentials:type_name -> common.OAuth2ClientCredentials + 94, // 132: common.ClosedChapterLog.closed_chapter:type_name -> common.Chapter + 94, // 133: common.ClosedChapterLog.new_chapter:type_name -> common.Chapter + 94, // 134: common.SealedChapterLog.chapter:type_name -> common.Chapter + 94, // 135: common.ArchivedChapterLog.chapter:type_name -> common.Chapter + 94, // 136: common.ConfirmedArchiveChapterLog.chapter:type_name -> common.Chapter + 100, // 137: common.MirrorSourceConfig.http:type_name -> common.HttpMirrorSourceConfig + 102, // 138: common.MirrorSourceConfig.postgres:type_name -> common.PostgresMirrorSourceConfig + 101, // 139: common.HttpMirrorSourceConfig.oauth2_client_credentials:type_name -> common.OAuth2ClientCredentials 17, // 140: common.MirrorSyncError.occurred_at:type_name -> common.Timestamp 10, // 141: common.MirrorSyncProgress.state:type_name -> common.MirrorSyncState - 101, // 142: common.MirrorSyncProgress.error:type_name -> common.MirrorSyncError + 103, // 142: common.MirrorSyncProgress.error:type_name -> common.MirrorSyncError 17, // 143: common.LedgerInfo.created_at:type_name -> common.Timestamp 17, // 144: common.LedgerInfo.deleted_at:type_name -> common.Timestamp - 34, // 145: common.LedgerInfo.metadata_schema:type_name -> common.MetadataSchema + 36, // 145: common.LedgerInfo.metadata_schema:type_name -> common.MetadataSchema 9, // 146: common.LedgerInfo.mode:type_name -> common.LedgerMode - 97, // 147: common.LedgerInfo.mirror_source:type_name -> common.MirrorSourceConfig - 102, // 148: common.LedgerInfo.mirror_sync_progress:type_name -> common.MirrorSyncProgress - 165, // 149: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry + 99, // 147: common.LedgerInfo.mirror_source:type_name -> common.MirrorSourceConfig + 104, // 148: common.LedgerInfo.mirror_sync_progress:type_name -> common.MirrorSyncProgress + 164, // 149: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry 12, // 150: common.LedgerInfo.default_enforcement_mode:type_name -> common.ChartEnforcementMode - 166, // 151: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry - 32, // 152: common.SaveMetadataCommand.target:type_name -> common.Target - 167, // 153: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry - 32, // 154: common.DeleteMetadataCommand.target:type_name -> common.Target - 168, // 155: common.TransactionState.metadata:type_name -> common.TransactionState.MetadataEntry - 17, // 156: common.TransactionState.timestamp:type_name -> common.Timestamp - 108, // 157: common.IdempotencyKeyValue.failure:type_name -> common.IdempotencyFailure - 11, // 158: common.IdempotencyFailure.reason:type_name -> common.ErrorReason - 169, // 159: common.IdempotencyFailure.metadata:type_name -> common.IdempotencyFailure.MetadataEntry - 112, // 160: common.SegmentType.uuid:type_name -> common.UUIDConstraint - 113, // 161: common.SegmentType.uint64:type_name -> common.Uint64Constraint - 114, // 162: common.SegmentType.bytes:type_name -> common.BytesConstraint - 13, // 163: common.AccountType.persistence:type_name -> common.AccountTypePersistence - 170, // 164: common.AccountType.segment_types:type_name -> common.AccountType.SegmentTypesEntry - 115, // 165: common.AddedAccountTypeLog.account_type:type_name -> common.AccountType - 12, // 166: common.UpdatedDefaultEnforcementModeLog.enforcement_mode:type_name -> common.ChartEnforcementMode - 130, // 167: common.QueryFilter.field:type_name -> common.FieldCondition - 136, // 168: common.QueryFilter.address:type_name -> common.AddressMatch - 126, // 169: common.QueryFilter.and:type_name -> common.AndFilter - 127, // 170: common.QueryFilter.or:type_name -> common.OrFilter - 128, // 171: common.QueryFilter.not:type_name -> common.NotFilter - 120, // 172: common.QueryFilter.reference:type_name -> common.ReferenceCondition - 123, // 173: common.QueryFilter.builtin_uint:type_name -> common.BuiltinUintCondition - 121, // 174: common.QueryFilter.ledger:type_name -> common.LedgerCondition - 122, // 175: common.QueryFilter.log_id:type_name -> common.LogIdCondition - 124, // 176: common.QueryFilter.log_builtin_uint:type_name -> common.LogBuiltinUintCondition - 125, // 177: common.QueryFilter.account_has_asset:type_name -> common.AccountHasAssetCondition - 131, // 178: common.ReferenceCondition.cond:type_name -> common.StringCondition - 131, // 179: common.LedgerCondition.cond:type_name -> common.StringCondition - 133, // 180: common.LogIdCondition.cond:type_name -> common.UintCondition + 165, // 151: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry + 40, // 152: common.LedgerInfo.indexes:type_name -> common.Index + 34, // 153: common.SaveMetadataCommand.target:type_name -> common.Target + 166, // 154: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry + 34, // 155: common.DeleteMetadataCommand.target:type_name -> common.Target + 167, // 156: common.TransactionState.metadata:type_name -> common.TransactionState.MetadataEntry + 17, // 157: common.TransactionState.timestamp:type_name -> common.Timestamp + 110, // 158: common.IdempotencyKeyValue.failure:type_name -> common.IdempotencyFailure + 11, // 159: common.IdempotencyFailure.reason:type_name -> common.ErrorReason + 168, // 160: common.IdempotencyFailure.metadata:type_name -> common.IdempotencyFailure.MetadataEntry + 114, // 161: common.SegmentType.uuid:type_name -> common.UUIDConstraint + 115, // 162: common.SegmentType.uint64:type_name -> common.Uint64Constraint + 116, // 163: common.SegmentType.bytes:type_name -> common.BytesConstraint + 13, // 164: common.AccountType.persistence:type_name -> common.AccountTypePersistence + 169, // 165: common.AccountType.segment_types:type_name -> common.AccountType.SegmentTypesEntry + 117, // 166: common.AddedAccountTypeLog.account_type:type_name -> common.AccountType + 12, // 167: common.UpdatedDefaultEnforcementModeLog.enforcement_mode:type_name -> common.ChartEnforcementMode + 131, // 168: common.QueryFilter.field:type_name -> common.FieldCondition + 137, // 169: common.QueryFilter.address:type_name -> common.AddressMatch + 127, // 170: common.QueryFilter.and:type_name -> common.AndFilter + 128, // 171: common.QueryFilter.or:type_name -> common.OrFilter + 129, // 172: common.QueryFilter.not:type_name -> common.NotFilter + 122, // 173: common.QueryFilter.reference:type_name -> common.ReferenceCondition + 125, // 174: common.QueryFilter.builtin_uint:type_name -> common.BuiltinUintCondition + 123, // 175: common.QueryFilter.ledger:type_name -> common.LedgerCondition + 124, // 176: common.QueryFilter.log_id:type_name -> common.LogIdCondition + 126, // 177: common.QueryFilter.log_builtin_uint:type_name -> common.LogBuiltinUintCondition + 132, // 178: common.ReferenceCondition.cond:type_name -> common.StringCondition + 132, // 179: common.LedgerCondition.cond:type_name -> common.StringCondition + 134, // 180: common.LogIdCondition.cond:type_name -> common.UintCondition 3, // 181: common.BuiltinUintCondition.field:type_name -> common.TransactionBuiltinIndex - 133, // 182: common.BuiltinUintCondition.cond:type_name -> common.UintCondition + 134, // 182: common.BuiltinUintCondition.cond:type_name -> common.UintCondition 5, // 183: common.LogBuiltinUintCondition.field:type_name -> common.LogBuiltinIndex - 133, // 184: common.LogBuiltinUintCondition.cond:type_name -> common.UintCondition - 119, // 185: common.AndFilter.filters:type_name -> common.QueryFilter - 119, // 186: common.OrFilter.filters:type_name -> common.QueryFilter - 119, // 187: common.NotFilter.filter:type_name -> common.QueryFilter - 129, // 188: common.FieldCondition.field:type_name -> common.FieldRef - 131, // 189: common.FieldCondition.string_cond:type_name -> common.StringCondition - 132, // 190: common.FieldCondition.int_cond:type_name -> common.IntCondition - 133, // 191: common.FieldCondition.uint_cond:type_name -> common.UintCondition - 134, // 192: common.FieldCondition.bool_cond:type_name -> common.BoolCondition - 135, // 193: common.FieldCondition.exists_cond:type_name -> common.ExistsCondition + 134, // 184: common.LogBuiltinUintCondition.cond:type_name -> common.UintCondition + 121, // 185: common.AndFilter.filters:type_name -> common.QueryFilter + 121, // 186: common.OrFilter.filters:type_name -> common.QueryFilter + 121, // 187: common.NotFilter.filter:type_name -> common.QueryFilter + 130, // 188: common.FieldCondition.field:type_name -> common.FieldRef + 132, // 189: common.FieldCondition.string_cond:type_name -> common.StringCondition + 133, // 190: common.FieldCondition.int_cond:type_name -> common.IntCondition + 134, // 191: common.FieldCondition.uint_cond:type_name -> common.UintCondition + 135, // 192: common.FieldCondition.bool_cond:type_name -> common.BoolCondition + 136, // 193: common.FieldCondition.exists_cond:type_name -> common.ExistsCondition 14, // 194: common.AddressMatch.role:type_name -> common.AddressRole - 119, // 195: common.PreparedQuery.filter:type_name -> common.QueryFilter + 121, // 195: common.PreparedQuery.filter:type_name -> common.QueryFilter 15, // 196: common.PreparedQuery.target:type_name -> common.QueryTarget 22, // 197: common.AggregatedVolume.input:type_name -> common.Uint256 22, // 198: common.AggregatedVolume.output:type_name -> common.Uint256 - 138, // 199: common.AggregateResult.volumes:type_name -> common.AggregatedVolume - 140, // 200: common.AggregateResult.groups:type_name -> common.GroupedAggregateResult - 138, // 201: common.GroupedAggregateResult.volumes:type_name -> common.AggregatedVolume - 30, // 202: common.PreparedQueryCursor.account_data:type_name -> common.Account + 139, // 199: common.AggregateResult.volumes:type_name -> common.AggregatedVolume + 141, // 200: common.AggregateResult.groups:type_name -> common.GroupedAggregateResult + 139, // 201: common.GroupedAggregateResult.volumes:type_name -> common.AggregatedVolume + 32, // 202: common.PreparedQueryCursor.account_data:type_name -> common.Account 24, // 203: common.PreparedQueryCursor.transaction_data:type_name -> common.Transaction - 144, // 204: common.CallerSnapshot.identity:type_name -> common.CallerIdentity - 146, // 205: common.BackupStorage.s3:type_name -> common.S3StorageConfig - 147, // 206: common.BackupStorage.azure:type_name -> common.AzureStorageConfig - 149, // 207: common.ListOptions.read:type_name -> common.ReadOptions - 119, // 208: common.ListOptions.filter:type_name -> common.QueryFilter + 145, // 204: common.CallerSnapshot.identity:type_name -> common.CallerIdentity + 147, // 205: common.BackupStorage.s3:type_name -> common.S3StorageConfig + 148, // 206: common.BackupStorage.azure:type_name -> common.AzureStorageConfig + 150, // 207: common.ListOptions.read:type_name -> common.ReadOptions + 121, // 208: common.ListOptions.filter:type_name -> common.QueryFilter 19, // 209: common.MetadataMap.ValuesEntry.value:type_name -> common.MetadataValue 19, // 210: common.Transaction.MetadataEntry.value:type_name -> common.MetadataValue - 26, // 211: common.VolumesByAssets.VolumesEntry.value:type_name -> common.Volumes - 28, // 212: common.PostCommitVolumes.VolumesByAccountEntry.value:type_name -> common.VolumesByAssets - 19, // 213: common.Account.MetadataEntry.value:type_name -> common.MetadataValue - 27, // 214: common.Account.VolumesEntry.value:type_name -> common.VolumesWithBalance - 33, // 215: common.MetadataSchema.AccountFieldsEntry.value:type_name -> common.MetadataFieldSchema - 33, // 216: common.MetadataSchema.TransactionFieldsEntry.value:type_name -> common.MetadataFieldSchema - 33, // 217: common.MetadataSchema.LedgerFieldsEntry.value:type_name -> common.MetadataFieldSchema - 19, // 218: common.SavedLedgerMetadataLog.MetadataEntry.value:type_name -> common.MetadataValue - 115, // 219: common.CreatedLedgerLog.AccountTypesEntry.value:type_name -> common.AccountType - 20, // 220: common.CreatedTransaction.AccountMetadataEntry.value:type_name -> common.MetadataMap - 19, // 221: common.SavedMetadata.MetadataEntry.value:type_name -> common.MetadataValue - 115, // 222: common.LedgerInfo.AccountTypesEntry.value:type_name -> common.AccountType - 19, // 223: common.LedgerInfo.MetadataEntry.value:type_name -> common.MetadataValue - 19, // 224: common.SaveMetadataCommand.MetadataEntry.value:type_name -> common.MetadataValue - 19, // 225: common.TransactionState.MetadataEntry.value:type_name -> common.MetadataValue - 111, // 226: common.AccountType.SegmentTypesEntry.value:type_name -> common.SegmentType - 227, // [227:227] is the sub-list for method output_type - 227, // [227:227] is the sub-list for method input_type - 227, // [227:227] is the sub-list for extension type_name - 227, // [227:227] is the sub-list for extension extendee - 0, // [0:227] is the sub-list for field type_name + 28, // 211: common.PostCommitVolumes.VolumesByAccountEntry.value:type_name -> common.VolumesByAssets + 19, // 212: common.Account.MetadataEntry.value:type_name -> common.MetadataValue + 35, // 213: common.MetadataSchema.AccountFieldsEntry.value:type_name -> common.MetadataFieldSchema + 35, // 214: common.MetadataSchema.TransactionFieldsEntry.value:type_name -> common.MetadataFieldSchema + 35, // 215: common.MetadataSchema.LedgerFieldsEntry.value:type_name -> common.MetadataFieldSchema + 19, // 216: common.SavedLedgerMetadataLog.MetadataEntry.value:type_name -> common.MetadataValue + 117, // 217: common.CreatedLedgerLog.AccountTypesEntry.value:type_name -> common.AccountType + 20, // 218: common.CreatedTransaction.AccountMetadataEntry.value:type_name -> common.MetadataMap + 19, // 219: common.SavedMetadata.MetadataEntry.value:type_name -> common.MetadataValue + 117, // 220: common.LedgerInfo.AccountTypesEntry.value:type_name -> common.AccountType + 19, // 221: common.LedgerInfo.MetadataEntry.value:type_name -> common.MetadataValue + 19, // 222: common.SaveMetadataCommand.MetadataEntry.value:type_name -> common.MetadataValue + 19, // 223: common.TransactionState.MetadataEntry.value:type_name -> common.MetadataValue + 113, // 224: common.AccountType.SegmentTypesEntry.value:type_name -> common.SegmentType + 225, // [225:225] is the sub-list for method output_type + 225, // [225:225] is the sub-list for method input_type + 225, // [225:225] is the sub-list for extension type_name + 225, // [225:225] is the sub-list for extension extendee + 0, // [0:225] is the sub-list for field type_name } func init() { file_common_proto_init() } @@ -11823,7 +11844,6 @@ func file_common_proto_init() { (*MetadataValue_BoolValue)(nil), (*MetadataValue_NullValue)(nil), (*MetadataValue_UintValue)(nil), - (*MetadataValue_DatetimeValue)(nil), } file_common_proto_msgTypes[4].OneofWrappers = []any{ (*ParameterValue_StringValue)(nil), @@ -11831,17 +11851,17 @@ func file_common_proto_init() { (*ParameterValue_Uint64Value)(nil), (*ParameterValue_BoolValue)(nil), } - file_common_proto_msgTypes[15].OneofWrappers = []any{ + file_common_proto_msgTypes[17].OneofWrappers = []any{ (*Target_Account)(nil), (*Target_TransactionId)(nil), } - file_common_proto_msgTypes[20].OneofWrappers = []any{ + file_common_proto_msgTypes[22].OneofWrappers = []any{ (*IndexID_TxBuiltin)(nil), (*IndexID_LogBuiltin)(nil), (*IndexID_AccountBuiltin)(nil), (*IndexID_Metadata)(nil), } - file_common_proto_msgTypes[25].OneofWrappers = []any{ + file_common_proto_msgTypes[27].OneofWrappers = []any{ (*LogPayload_CreateLedger)(nil), (*LogPayload_DeleteLedger)(nil), (*LogPayload_Apply)(nil), @@ -11870,18 +11890,18 @@ func file_common_proto_init() { (*LogPayload_SavedLedgerMetadata)(nil), (*LogPayload_DeletedLedgerMetadata)(nil), } - file_common_proto_msgTypes[51].OneofWrappers = []any{ + file_common_proto_msgTypes[53].OneofWrappers = []any{ (*SinkConfig_Nats)(nil), (*SinkConfig_Clickhouse)(nil), (*SinkConfig_Kafka)(nil), (*SinkConfig_Http)(nil), (*SinkConfig_Databricks)(nil), } - file_common_proto_msgTypes[58].OneofWrappers = []any{ + file_common_proto_msgTypes[60].OneofWrappers = []any{ (*DatabricksSinkConfig_Token)(nil), (*DatabricksSinkConfig_OauthM2M)(nil), } - file_common_proto_msgTypes[65].OneofWrappers = []any{ + file_common_proto_msgTypes[67].OneofWrappers = []any{ (*LedgerLogPayload_CreatedTransaction)(nil), (*LedgerLogPayload_RevertedTransaction)(nil), (*LedgerLogPayload_SavedMetadata)(nil), @@ -11895,17 +11915,17 @@ func file_common_proto_init() { (*LedgerLogPayload_RemovedAccountType)(nil), (*LedgerLogPayload_UpdatedDefaultEnforcementMode)(nil), } - file_common_proto_msgTypes[80].OneofWrappers = []any{ + file_common_proto_msgTypes[82].OneofWrappers = []any{ (*MirrorSourceConfig_Http)(nil), (*MirrorSourceConfig_Postgres)(nil), } - file_common_proto_msgTypes[94].OneofWrappers = []any{ + file_common_proto_msgTypes[96].OneofWrappers = []any{ (*SegmentType_Regex)(nil), (*SegmentType_Uuid)(nil), (*SegmentType_Uint64)(nil), (*SegmentType_Bytes)(nil), } - file_common_proto_msgTypes[102].OneofWrappers = []any{ + file_common_proto_msgTypes[104].OneofWrappers = []any{ (*QueryFilter_Field)(nil), (*QueryFilter_Address)(nil), (*QueryFilter_And)(nil), @@ -11916,36 +11936,35 @@ func file_common_proto_init() { (*QueryFilter_Ledger)(nil), (*QueryFilter_LogId)(nil), (*QueryFilter_LogBuiltinUint)(nil), - (*QueryFilter_AccountHasAsset)(nil), } - file_common_proto_msgTypes[113].OneofWrappers = []any{ + file_common_proto_msgTypes[114].OneofWrappers = []any{ (*FieldCondition_StringCond)(nil), (*FieldCondition_IntCond)(nil), (*FieldCondition_UintCond)(nil), (*FieldCondition_BoolCond)(nil), (*FieldCondition_ExistsCond)(nil), } - file_common_proto_msgTypes[114].OneofWrappers = []any{ + file_common_proto_msgTypes[115].OneofWrappers = []any{ (*StringCondition_Hardcoded)(nil), (*StringCondition_Param)(nil), } - file_common_proto_msgTypes[115].OneofWrappers = []any{} file_common_proto_msgTypes[116].OneofWrappers = []any{} - file_common_proto_msgTypes[117].OneofWrappers = []any{ + file_common_proto_msgTypes[117].OneofWrappers = []any{} + file_common_proto_msgTypes[118].OneofWrappers = []any{ (*BoolCondition_Hardcoded)(nil), (*BoolCondition_Param)(nil), } - file_common_proto_msgTypes[119].OneofWrappers = []any{ + file_common_proto_msgTypes[120].OneofWrappers = []any{ (*AddressMatch_HardcodedPrefix)(nil), (*AddressMatch_HardcodedExact)(nil), (*AddressMatch_ParamPrefix)(nil), (*AddressMatch_ParamExact)(nil), } - file_common_proto_msgTypes[127].OneofWrappers = []any{ + file_common_proto_msgTypes[128].OneofWrappers = []any{ (*CallerIdentity_Issuer)(nil), (*CallerIdentity_KeyId)(nil), } - file_common_proto_msgTypes[131].OneofWrappers = []any{ + file_common_proto_msgTypes[132].OneofWrappers = []any{ (*BackupStorage_S3)(nil), (*BackupStorage_Azure)(nil), } @@ -11955,7 +11974,7 @@ func file_common_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)), NumEnums: 17, - NumMessages: 154, + NumMessages: 153, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/proto/commonpb/common.pb.json.go b/internal/proto/commonpb/common.pb.json.go index b602ca3c3a..4465d2317b 100644 --- a/internal/proto/commonpb/common.pb.json.go +++ b/internal/proto/commonpb/common.pb.json.go @@ -149,17 +149,54 @@ func (x *PostCommitVolumes) MarshalJSON() ([]byte, error) { }) } +// MarshalJSON implements json.Marshaler for VolumeEntry. Color is always +// emitted (even when empty) so clients can distinguish the uncolored bucket +// from an older response shape — same contract as accountVolumeJSON and +// aggregatedVolumeJSON in the REST handler layer. +func (x *VolumeEntry) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Asset string `json:"asset"` + Color string `json:"color"` + Volumes *Volumes `json:"volumes,omitempty"` + }{ + Asset: x.GetAsset(), + Color: x.GetColor(), + Volumes: x.GetVolumes(), + }) +} + +// accountVolumeJSON is the JSON shape for AccountVolume. Color is always +// emitted (even empty) because the API treats the empty bucket as a +// first-class entry and clients cannot otherwise tell "uncolored bucket" +// from "field absent in an older response shape". +type accountVolumeJSON struct { + Asset string `json:"asset"` + Color string `json:"color"` + Volumes *VolumesWithBalance `json:"volumes,omitempty"` +} + // MarshalJSON implements json.Marshaler for Account. func (x *Account) MarshalJSON() ([]byte, error) { + volumes := make([]*accountVolumeJSON, 0, len(x.GetVolumes())) + for _, v := range x.GetVolumes() { + volumes = append(volumes, &accountVolumeJSON{ + Asset: v.GetAsset(), + Color: v.GetColor(), + Volumes: v.GetVolumes(), + }) + } + return json.Marshal(&struct { - Address string `json:"address,omitempty"` - Metadata map[string]any `json:"metadata,omitempty"` - FirstUsage *Timestamp `json:"firstUsage,omitempty"` - InsertionDate *Timestamp `json:"insertionDate,omitempty"` - UpdatedAt *Timestamp `json:"updatedAt,omitempty"` + Address string `json:"address,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` + Volumes []*accountVolumeJSON `json:"volumes"` + FirstUsage *Timestamp `json:"firstUsage,omitempty"` + InsertionDate *Timestamp `json:"insertionDate,omitempty"` + UpdatedAt *Timestamp `json:"updatedAt,omitempty"` }{ Address: x.GetAddress(), Metadata: MetadataToAnyMap(x.GetMetadata()), + Volumes: volumes, FirstUsage: x.GetFirstUsage(), InsertionDate: x.GetInsertionDate(), UpdatedAt: x.GetUpdatedAt(), diff --git a/internal/proto/commonpb/common_dethash.pb.go b/internal/proto/commonpb/common_dethash.pb.go index e7393dd040..84e56d58eb 100644 --- a/internal/proto/commonpb/common_dethash.pb.go +++ b/internal/proto/commonpb/common_dethash.pb.go @@ -299,41 +299,22 @@ func (m *VolumesByAssets) MarshalDeterministicVT(dAtA []byte) []byte { if m == nil { return dAtA } - sz := m.SizeVT() - buf := make([]byte, sz) - n, _ := m.MarshalToSizedBufferDeterministicVT(buf) - return append(dAtA, buf[sz-n:]...) + b, err := m.MarshalVT() + if err != nil { + panic("MarshalDeterministicVT: " + err.Error()) + } + return append(dAtA, b...) } -func (m *VolumesByAssets) MarshalToSizedBufferDeterministicVT(dAtA []byte) (int, error) { +func (m *VolumeEntry) MarshalDeterministicVT(dAtA []byte) []byte { if m == nil { - return 0, nil - } - i := len(dAtA) - if m.unknownFields != nil { - i -= len(m.unknownFields) - copy(dAtA[i:], m.unknownFields) + return dAtA } - if len(m.Volumes) > 0 { - for _, k := range slices.Sorted(maps.Keys(m.Volumes)) { - v := m.Volumes[k] - baseI := i - size, _ := v.MarshalToSizedBufferVT(dAtA[:i]) - i -= size - i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) - i-- - dAtA[i] = 0x12 - i -= len(k) - copy(dAtA[i:], k) - i = protohelpers.EncodeVarint(dAtA, i, uint64(len(k))) - i-- - dAtA[i] = 0xa - i = protohelpers.EncodeVarint(dAtA, i, uint64(baseI-i)) - i-- - dAtA[i] = 0xa - } + b, err := m.MarshalVT() + if err != nil { + panic("MarshalDeterministicVT: " + err.Error()) } - return len(dAtA) - i, nil + return append(dAtA, b...) } func (m *PostCommitVolumes) MarshalDeterministicVT(dAtA []byte) []byte { @@ -359,7 +340,7 @@ func (m *PostCommitVolumes) MarshalToSizedBufferDeterministicVT(dAtA []byte) (in for _, k := range slices.Sorted(maps.Keys(m.VolumesByAccount)) { v := m.VolumesByAccount[k] baseI := i - size, _ := v.MarshalToSizedBufferDeterministicVT(dAtA[:i]) + size, _ := v.MarshalToSizedBufferVT(dAtA[:i]) i -= size i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- @@ -377,6 +358,17 @@ func (m *PostCommitVolumes) MarshalToSizedBufferDeterministicVT(dAtA []byte) (in return len(dAtA) - i, nil } +func (m *AccountVolume) MarshalDeterministicVT(dAtA []byte) []byte { + if m == nil { + return dAtA + } + b, err := m.MarshalVT() + if err != nil { + panic("MarshalDeterministicVT: " + err.Error()) + } + return append(dAtA, b...) +} + func (m *Account) MarshalDeterministicVT(dAtA []byte) []byte { if m == nil { return dAtA @@ -397,21 +389,11 @@ func (m *Account) MarshalToSizedBufferDeterministicVT(dAtA []byte) (int, error) copy(dAtA[i:], m.unknownFields) } if len(m.Volumes) > 0 { - for _, k := range slices.Sorted(maps.Keys(m.Volumes)) { - v := m.Volumes[k] - baseI := i - size, _ := v.MarshalToSizedBufferVT(dAtA[:i]) + for iNdEx := len(m.Volumes) - 1; iNdEx >= 0; iNdEx-- { + size, _ := m.Volumes[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) i -= size i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- - dAtA[i] = 0x12 - i -= len(k) - copy(dAtA[i:], k) - i = protohelpers.EncodeVarint(dAtA, i, uint64(len(k))) - i-- - dAtA[i] = 0xa - i = protohelpers.EncodeVarint(dAtA, i, uint64(baseI-i)) - i-- dAtA[i] = 0x32 } } diff --git a/internal/proto/commonpb/common_reader.pb.go b/internal/proto/commonpb/common_reader.pb.go index d87417dfff..fbe6f69a8d 100644 --- a/internal/proto/commonpb/common_reader.pb.go +++ b/internal/proto/commonpb/common_reader.pb.go @@ -469,6 +469,7 @@ type PostingReader interface { GetDestination() string GetAmount() Uint256Reader GetAsset() string + GetColor() string Mutate() *Posting } @@ -494,6 +495,10 @@ func (r *postingReadonly) GetAsset() string { return r.v.GetAsset() } +func (r *postingReadonly) GetColor() string { + return r.v.GetColor() +} + func (r *postingReadonly) Mutate() *Posting { return r.v.CloneVT() } @@ -949,14 +954,14 @@ func NewVolumesWithBalanceListReader(s []*VolumesWithBalance) VolumesWithBalance // VolumesByAssetsReader provides read-only access to VolumesByAssets. // Call Mutate() to obtain a mutable clone. type VolumesByAssetsReader interface { - GetVolumes() VolumesByAssets_VolumesMapReader + GetVolumes() VolumeEntryListReader Mutate() *VolumesByAssets } type volumesByAssetsReadonly struct{ v *VolumesByAssets } -func (r *volumesByAssetsReadonly) GetVolumes() VolumesByAssets_VolumesMapReader { - return volumesByAssets_volumesMapReadonly(r.v.GetVolumes()) +func (r *volumesByAssetsReadonly) GetVolumes() VolumeEntryListReader { + return NewVolumeEntryListReader(r.v.GetVolumes()) } func (r *volumesByAssetsReadonly) Mutate() *VolumesByAssets { @@ -1013,37 +1018,87 @@ func NewVolumesByAssetsListReader(s []*VolumesByAssets) VolumesByAssetsListReade return volumesByAssetsListReadonly(s) } -// VolumesByAssets_VolumesMapReader provides read-only access to VolumesByAssets.Volumes. -type VolumesByAssets_VolumesMapReader interface { +// VolumeEntryReader provides read-only access to VolumeEntry. +// Call Mutate() to obtain a mutable clone. +type VolumeEntryReader interface { + GetAsset() string + GetColor() string + GetVolumes() VolumesReader + Mutate() *VolumeEntry +} + +type volumeEntryReadonly struct{ v *VolumeEntry } + +func (r *volumeEntryReadonly) GetAsset() string { + return r.v.GetAsset() +} + +func (r *volumeEntryReadonly) GetColor() string { + return r.v.GetColor() +} + +func (r *volumeEntryReadonly) GetVolumes() VolumesReader { + v := r.v.GetVolumes() + if v == nil { + return nil + } + return v.AsReader() +} + +func (r *volumeEntryReadonly) Mutate() *VolumeEntry { + return r.v.CloneVT() +} + +// AsReader returns a read-only view of this VolumeEntry. +func (m *VolumeEntry) AsReader() VolumeEntryReader { + if m == nil { + return nil + } + return &volumeEntryReadonly{v: m} +} + +// Mutate returns a mutable deep clone of this VolumeEntry. +func (m *VolumeEntry) Mutate() *VolumeEntry { + return m.CloneVT() +} + +// VolumeEntryListReader provides read-only iteration over []*VolumeEntry. +type VolumeEntryListReader interface { Len() int - Get(k string) (VolumesReader, bool) - Range(yield func(string, VolumesReader) bool) + Get(i int) VolumeEntryReader + Range(yield func(int, VolumeEntryReader) bool) } -type volumesByAssets_volumesMapReadonly map[string]*Volumes +type volumeEntryListReadonly []*VolumeEntry -func (m volumesByAssets_volumesMapReadonly) Len() int { return len(m) } +func (l volumeEntryListReadonly) Len() int { return len(l) } -func (m volumesByAssets_volumesMapReadonly) Get(k string) (VolumesReader, bool) { - v, ok := m[k] - if !ok || v == nil { - return nil, ok +func (l volumeEntryListReadonly) Get(i int) VolumeEntryReader { + v := l[i] + if v == nil { + return nil } - return v.AsReader(), true + return v.AsReader() } -func (m volumesByAssets_volumesMapReadonly) Range(yield func(string, VolumesReader) bool) { - for k, v := range m { - var r VolumesReader +func (l volumeEntryListReadonly) Range(yield func(int, VolumeEntryReader) bool) { + for i, v := range l { + var r VolumeEntryReader if v != nil { r = v.AsReader() } - if !yield(k, r) { + if !yield(i, r) { return } } } +// NewVolumeEntryListReader wraps s for read-only iteration. The returned +// view aliases the underlying slice; do not mutate s afterwards. +func NewVolumeEntryListReader(s []*VolumeEntry) VolumeEntryListReader { + return volumeEntryListReadonly(s) +} + // PostCommitVolumesReader provides read-only access to PostCommitVolumes. // Call Mutate() to obtain a mutable clone. type PostCommitVolumesReader interface { @@ -1142,6 +1197,87 @@ func (m postCommitVolumes_volumesByAccountMapReadonly) Range(yield func(string, } } +// AccountVolumeReader provides read-only access to AccountVolume. +// Call Mutate() to obtain a mutable clone. +type AccountVolumeReader interface { + GetAsset() string + GetColor() string + GetVolumes() VolumesWithBalanceReader + Mutate() *AccountVolume +} + +type accountVolumeReadonly struct{ v *AccountVolume } + +func (r *accountVolumeReadonly) GetAsset() string { + return r.v.GetAsset() +} + +func (r *accountVolumeReadonly) GetColor() string { + return r.v.GetColor() +} + +func (r *accountVolumeReadonly) GetVolumes() VolumesWithBalanceReader { + v := r.v.GetVolumes() + if v == nil { + return nil + } + return v.AsReader() +} + +func (r *accountVolumeReadonly) Mutate() *AccountVolume { + return r.v.CloneVT() +} + +// AsReader returns a read-only view of this AccountVolume. +func (m *AccountVolume) AsReader() AccountVolumeReader { + if m == nil { + return nil + } + return &accountVolumeReadonly{v: m} +} + +// Mutate returns a mutable deep clone of this AccountVolume. +func (m *AccountVolume) Mutate() *AccountVolume { + return m.CloneVT() +} + +// AccountVolumeListReader provides read-only iteration over []*AccountVolume. +type AccountVolumeListReader interface { + Len() int + Get(i int) AccountVolumeReader + Range(yield func(int, AccountVolumeReader) bool) +} + +type accountVolumeListReadonly []*AccountVolume + +func (l accountVolumeListReadonly) Len() int { return len(l) } + +func (l accountVolumeListReadonly) Get(i int) AccountVolumeReader { + v := l[i] + if v == nil { + return nil + } + return v.AsReader() +} + +func (l accountVolumeListReadonly) Range(yield func(int, AccountVolumeReader) bool) { + for i, v := range l { + var r AccountVolumeReader + if v != nil { + r = v.AsReader() + } + if !yield(i, r) { + return + } + } +} + +// NewAccountVolumeListReader wraps s for read-only iteration. The returned +// view aliases the underlying slice; do not mutate s afterwards. +func NewAccountVolumeListReader(s []*AccountVolume) AccountVolumeListReader { + return accountVolumeListReadonly(s) +} + // AccountReader provides read-only access to Account. // Call Mutate() to obtain a mutable clone. type AccountReader interface { @@ -1150,7 +1286,7 @@ type AccountReader interface { GetFirstUsage() TimestampReader GetInsertionDate() TimestampReader GetUpdatedAt() TimestampReader - GetVolumes() Account_VolumesMapReader + GetVolumes() AccountVolumeListReader Mutate() *Account } @@ -1188,8 +1324,8 @@ func (r *accountReadonly) GetUpdatedAt() TimestampReader { return v.AsReader() } -func (r *accountReadonly) GetVolumes() Account_VolumesMapReader { - return account_volumesMapReadonly(r.v.GetVolumes()) +func (r *accountReadonly) GetVolumes() AccountVolumeListReader { + return NewAccountVolumeListReader(r.v.GetVolumes()) } func (r *accountReadonly) Mutate() *Account { @@ -1275,37 +1411,6 @@ func (m account_metadataMapReadonly) Range(yield func(string, MetadataValueReade } } -// Account_VolumesMapReader provides read-only access to Account.Volumes. -type Account_VolumesMapReader interface { - Len() int - Get(k string) (VolumesWithBalanceReader, bool) - Range(yield func(string, VolumesWithBalanceReader) bool) -} - -type account_volumesMapReadonly map[string]*VolumesWithBalance - -func (m account_volumesMapReadonly) Len() int { return len(m) } - -func (m account_volumesMapReadonly) Get(k string) (VolumesWithBalanceReader, bool) { - v, ok := m[k] - if !ok || v == nil { - return nil, ok - } - return v.AsReader(), true -} - -func (m account_volumesMapReadonly) Range(yield func(string, VolumesWithBalanceReader) bool) { - for k, v := range m { - var r VolumesWithBalanceReader - if v != nil { - r = v.AsReader() - } - if !yield(k, r) { - return - } - } -} - // TargetAccountReader provides read-only access to TargetAccount. // Call Mutate() to obtain a mutable clone. type TargetAccountReader interface { @@ -5337,6 +5442,7 @@ func NewLedgerLogListReader(s []*LedgerLog) LedgerLogListReader { return ledgerL type TouchedVolumeReader interface { GetAccount() string GetAsset() string + GetColor() string Mutate() *TouchedVolume } @@ -5350,6 +5456,10 @@ func (r *touchedVolumeReadonly) GetAsset() string { return r.v.GetAsset() } +func (r *touchedVolumeReadonly) GetColor() string { + return r.v.GetColor() +} + func (r *touchedVolumeReadonly) Mutate() *TouchedVolume { return r.v.CloneVT() } @@ -9901,6 +10011,7 @@ type AggregatedVolumeReader interface { GetAsset() string GetInput() Uint256Reader GetOutput() Uint256Reader + GetColor() string Mutate() *AggregatedVolume } @@ -9926,6 +10037,10 @@ func (r *aggregatedVolumeReadonly) GetOutput() Uint256Reader { return v.AsReader() } +func (r *aggregatedVolumeReadonly) GetColor() string { + return r.v.GetColor() +} + func (r *aggregatedVolumeReadonly) Mutate() *AggregatedVolume { return r.v.CloneVT() } diff --git a/internal/proto/commonpb/common_vtproto.pb.go b/internal/proto/commonpb/common_vtproto.pb.go index 641fad8fda..8d3de703c1 100644 --- a/internal/proto/commonpb/common_vtproto.pb.go +++ b/internal/proto/commonpb/common_vtproto.pb.go @@ -236,6 +236,7 @@ func (m *Posting) CloneVT() *Posting { r.Destination = m.Destination r.Amount = m.Amount.CloneVT() r.Asset = m.Asset + r.Color = m.Color if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -356,7 +357,7 @@ func (m *VolumesByAssets) CloneVT() *VolumesByAssets { } r := new(VolumesByAssets) if rhs := m.Volumes; rhs != nil { - tmpContainer := make(map[string]*Volumes, len(rhs)) + tmpContainer := make([]*VolumeEntry, len(rhs)) for k, v := range rhs { tmpContainer[k] = v.CloneVT() } @@ -373,6 +374,25 @@ func (m *VolumesByAssets) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *VolumeEntry) CloneVT() *VolumeEntry { + if m == nil { + return (*VolumeEntry)(nil) + } + r := new(VolumeEntry) + r.Asset = m.Asset + r.Color = m.Color + r.Volumes = m.Volumes.CloneVT() + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *VolumeEntry) CloneMessageVT() proto.Message { + return m.CloneVT() +} + func (m *PostCommitVolumes) CloneVT() *PostCommitVolumes { if m == nil { return (*PostCommitVolumes)(nil) @@ -396,6 +416,25 @@ func (m *PostCommitVolumes) CloneMessageVT() proto.Message { return m.CloneVT() } +func (m *AccountVolume) CloneVT() *AccountVolume { + if m == nil { + return (*AccountVolume)(nil) + } + r := new(AccountVolume) + r.Asset = m.Asset + r.Color = m.Color + r.Volumes = m.Volumes.CloneVT() + if len(m.unknownFields) > 0 { + r.unknownFields = make([]byte, len(m.unknownFields)) + copy(r.unknownFields, m.unknownFields) + } + return r +} + +func (m *AccountVolume) CloneMessageVT() proto.Message { + return m.CloneVT() +} + func (m *Account) CloneVT() *Account { if m == nil { return (*Account)(nil) @@ -413,7 +452,7 @@ func (m *Account) CloneVT() *Account { r.Metadata = tmpContainer } if rhs := m.Volumes; rhs != nil { - tmpContainer := make(map[string]*VolumesWithBalance, len(rhs)) + tmpContainer := make([]*AccountVolume, len(rhs)) for k, v := range rhs { tmpContainer[k] = v.CloneVT() } @@ -1800,6 +1839,7 @@ func (m *TouchedVolume) CloneVT() *TouchedVolume { r := new(TouchedVolume) r.Account = m.Account r.Asset = m.Asset + r.Color = m.Color if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -3339,6 +3379,7 @@ func (m *AggregatedVolume) CloneVT() *AggregatedVolume { r.Asset = m.Asset r.Input = m.Input.CloneVT() r.Output = m.Output.CloneVT() + r.Color = m.Color if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -4020,6 +4061,9 @@ func (this *Posting) EqualVT(that *Posting) bool { if this.Asset != that.Asset { return false } + if this.Color != that.Color { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -4195,16 +4239,13 @@ func (this *VolumesByAssets) EqualVT(that *VolumesByAssets) bool { return false } for i, vx := range this.Volumes { - vy, ok := that.Volumes[i] - if !ok { - return false - } + vy := that.Volumes[i] if p, q := vx, vy; p != q { if p == nil { - p = &Volumes{} + p = &VolumeEntry{} } if q == nil { - q = &Volumes{} + q = &VolumeEntry{} } if !p.EqualVT(q) { return false @@ -4221,6 +4262,31 @@ func (this *VolumesByAssets) EqualMessageVT(thatMsg proto.Message) bool { } return this.EqualVT(that) } +func (this *VolumeEntry) EqualVT(that *VolumeEntry) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.Asset != that.Asset { + return false + } + if this.Color != that.Color { + return false + } + if !this.Volumes.EqualVT(that.Volumes) { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *VolumeEntry) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*VolumeEntry) + if !ok { + return false + } + return this.EqualVT(that) +} func (this *PostCommitVolumes) EqualVT(that *PostCommitVolumes) bool { if this == that { return true @@ -4257,6 +4323,31 @@ func (this *PostCommitVolumes) EqualMessageVT(thatMsg proto.Message) bool { } return this.EqualVT(that) } +func (this *AccountVolume) EqualVT(that *AccountVolume) bool { + if this == that { + return true + } else if this == nil || that == nil { + return false + } + if this.Asset != that.Asset { + return false + } + if this.Color != that.Color { + return false + } + if !this.Volumes.EqualVT(that.Volumes) { + return false + } + return string(this.unknownFields) == string(that.unknownFields) +} + +func (this *AccountVolume) EqualMessageVT(thatMsg proto.Message) bool { + that, ok := thatMsg.(*AccountVolume) + if !ok { + return false + } + return this.EqualVT(that) +} func (this *Account) EqualVT(that *Account) bool { if this == that { return true @@ -4299,16 +4390,13 @@ func (this *Account) EqualVT(that *Account) bool { return false } for i, vx := range this.Volumes { - vy, ok := that.Volumes[i] - if !ok { - return false - } + vy := that.Volumes[i] if p, q := vx, vy; p != q { if p == nil { - p = &VolumesWithBalance{} + p = &AccountVolume{} } if q == nil { - q = &VolumesWithBalance{} + q = &AccountVolume{} } if !p.EqualVT(q) { return false @@ -6644,6 +6732,9 @@ func (this *TouchedVolume) EqualVT(that *TouchedVolume) bool { if this.Asset != that.Asset { return false } + if this.Color != that.Color { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -9161,6 +9252,9 @@ func (this *AggregatedVolume) EqualVT(that *AggregatedVolume) bool { if !this.Output.EqualVT(that.Output) { return false } + if this.Color != that.Color { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -10125,6 +10219,13 @@ func (m *Posting) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.Color) > 0 { + i -= len(m.Color) + copy(dAtA[i:], m.Color) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Color))) + i-- + dAtA[i] = 0x2a + } if len(m.Asset) > 0 { i -= len(m.Asset) copy(dAtA[i:], m.Asset) @@ -10487,30 +10588,77 @@ func (m *VolumesByAssets) MarshalToSizedBufferVT(dAtA []byte) (int, error) { copy(dAtA[i:], m.unknownFields) } if len(m.Volumes) > 0 { - for k := range m.Volumes { - v := m.Volumes[k] - baseI := i - size, err := v.MarshalToSizedBufferVT(dAtA[:i]) + for iNdEx := len(m.Volumes) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Volumes[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) if err != nil { return 0, err } i -= size i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- - dAtA[i] = 0x12 - i -= len(k) - copy(dAtA[i:], k) - i = protohelpers.EncodeVarint(dAtA, i, uint64(len(k))) - i-- - dAtA[i] = 0xa - i = protohelpers.EncodeVarint(dAtA, i, uint64(baseI-i)) - i-- dAtA[i] = 0xa } } return len(dAtA) - i, nil } +func (m *VolumeEntry) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *VolumeEntry) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *VolumeEntry) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.Volumes != nil { + size, err := m.Volumes.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1a + } + if len(m.Color) > 0 { + i -= len(m.Color) + copy(dAtA[i:], m.Color) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Color))) + i-- + dAtA[i] = 0x12 + } + if len(m.Asset) > 0 { + i -= len(m.Asset) + copy(dAtA[i:], m.Asset) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Asset))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *PostCommitVolumes) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -10566,6 +10714,63 @@ func (m *PostCommitVolumes) MarshalToSizedBufferVT(dAtA []byte) (int, error) { return len(dAtA) - i, nil } +func (m *AccountVolume) MarshalVT() (dAtA []byte, err error) { + if m == nil { + return nil, nil + } + size := m.SizeVT() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBufferVT(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *AccountVolume) MarshalToVT(dAtA []byte) (int, error) { + size := m.SizeVT() + return m.MarshalToSizedBufferVT(dAtA[:size]) +} + +func (m *AccountVolume) MarshalToSizedBufferVT(dAtA []byte) (int, error) { + if m == nil { + return 0, nil + } + i := len(dAtA) + _ = i + var l int + _ = l + if m.unknownFields != nil { + i -= len(m.unknownFields) + copy(dAtA[i:], m.unknownFields) + } + if m.Volumes != nil { + size, err := m.Volumes.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x1a + } + if len(m.Color) > 0 { + i -= len(m.Color) + copy(dAtA[i:], m.Color) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Color))) + i-- + dAtA[i] = 0x12 + } + if len(m.Asset) > 0 { + i -= len(m.Asset) + copy(dAtA[i:], m.Asset) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Asset))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + func (m *Account) MarshalVT() (dAtA []byte, err error) { if m == nil { return nil, nil @@ -10597,24 +10802,14 @@ func (m *Account) MarshalToSizedBufferVT(dAtA []byte) (int, error) { copy(dAtA[i:], m.unknownFields) } if len(m.Volumes) > 0 { - for k := range m.Volumes { - v := m.Volumes[k] - baseI := i - size, err := v.MarshalToSizedBufferVT(dAtA[:i]) + for iNdEx := len(m.Volumes) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.Volumes[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) if err != nil { return 0, err } i -= size i = protohelpers.EncodeVarint(dAtA, i, uint64(size)) i-- - dAtA[i] = 0x12 - i -= len(k) - copy(dAtA[i:], k) - i = protohelpers.EncodeVarint(dAtA, i, uint64(len(k))) - i-- - dAtA[i] = 0xa - i = protohelpers.EncodeVarint(dAtA, i, uint64(baseI-i)) - i-- dAtA[i] = 0x32 } } @@ -14206,6 +14401,13 @@ func (m *TouchedVolume) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.Color) > 0 { + i -= len(m.Color) + copy(dAtA[i:], m.Color) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Color))) + i-- + dAtA[i] = 0x1a + } if len(m.Asset) > 0 { i -= len(m.Asset) copy(dAtA[i:], m.Asset) @@ -17964,6 +18166,13 @@ func (m *AggregatedVolume) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.Color) > 0 { + i -= len(m.Color) + copy(dAtA[i:], m.Color) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Color))) + i-- + dAtA[i] = 0x22 + } if m.Output != nil { size, err := m.Output.MarshalToSizedBufferVT(dAtA[:i]) if err != nil { @@ -19020,6 +19229,10 @@ func (m *Posting) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + l = len(m.Color) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } n += len(m.unknownFields) return n } @@ -19152,22 +19365,37 @@ func (m *VolumesByAssets) SizeVT() (n int) { var l int _ = l if len(m.Volumes) > 0 { - for k, v := range m.Volumes { - _ = k - _ = v - l = 0 - if v != nil { - l = v.SizeVT() - } - l += 1 + protohelpers.SizeOfVarint(uint64(l)) - mapEntrySize := 1 + len(k) + protohelpers.SizeOfVarint(uint64(len(k))) + l - n += mapEntrySize + 1 + protohelpers.SizeOfVarint(uint64(mapEntrySize)) + for _, e := range m.Volumes { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } } n += len(m.unknownFields) return n } +func (m *VolumeEntry) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Asset) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.Color) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Volumes != nil { + l = m.Volumes.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + func (m *PostCommitVolumes) SizeVT() (n int) { if m == nil { return 0 @@ -19191,6 +19419,28 @@ func (m *PostCommitVolumes) SizeVT() (n int) { return n } +func (m *AccountVolume) SizeVT() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Asset) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + l = len(m.Color) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + if m.Volumes != nil { + l = m.Volumes.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } + n += len(m.unknownFields) + return n +} + func (m *Account) SizeVT() (n int) { if m == nil { return 0 @@ -19227,16 +19477,9 @@ func (m *Account) SizeVT() (n int) { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } if len(m.Volumes) > 0 { - for k, v := range m.Volumes { - _ = k - _ = v - l = 0 - if v != nil { - l = v.SizeVT() - } - l += 1 + protohelpers.SizeOfVarint(uint64(l)) - mapEntrySize := 1 + len(k) + protohelpers.SizeOfVarint(uint64(len(k))) + l - n += mapEntrySize + 1 + protohelpers.SizeOfVarint(uint64(mapEntrySize)) + for _, e := range m.Volumes { + l = e.SizeVT() + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } } n += len(m.unknownFields) @@ -20769,6 +21012,10 @@ func (m *TouchedVolume) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + l = len(m.Color) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } n += len(m.unknownFields) return n } @@ -22369,6 +22616,10 @@ func (m *AggregatedVolume) SizeVT() (n int) { l = m.Output.SizeVT() n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + l = len(m.Color) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } n += len(m.unknownFields) return n } @@ -23636,259 +23887,291 @@ func (m *Posting) UnmarshalVT(dAtA []byte) error { } m.Asset = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := protohelpers.Skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return protohelpers.ErrInvalidLength - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *Transaction) UnmarshalVT(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: Transaction: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: Transaction: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Postings", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return protohelpers.ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Postings = append(m.Postings, &Posting{}) - if err := m.Postings[len(m.Postings)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Metadata", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return protohelpers.ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Metadata == nil { - m.Metadata = make(map[string]*MetadataValue) - } - var mapkey string - var mapvalue *MetadataValue - for iNdEx < postIndex { - entryPreIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - if fieldNum == 1 { - var stringLenmapkey uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapkey |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapkey := int(stringLenmapkey) - if intStringLenmapkey < 0 { - return protohelpers.ErrInvalidLength - } - postStringIndexmapkey := iNdEx + intStringLenmapkey - if postStringIndexmapkey < 0 { - return protohelpers.ErrInvalidLength - } - if postStringIndexmapkey > l { - return io.ErrUnexpectedEOF - } - mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) - iNdEx = postStringIndexmapkey - } else if fieldNum == 2 { - var mapmsglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - mapmsglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if mapmsglen < 0 { - return protohelpers.ErrInvalidLength - } - postmsgIndex := iNdEx + mapmsglen - if postmsgIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postmsgIndex > l { - return io.ErrUnexpectedEOF - } - mapvalue = &MetadataValue{} - if err := mapvalue.UnmarshalVT(dAtA[iNdEx:postmsgIndex]); err != nil { - return err - } - iNdEx = postmsgIndex - } else { - iNdEx = entryPreIndex - skippy, err := protohelpers.Skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return protohelpers.ErrInvalidLength - } - if (iNdEx + skippy) > postIndex { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - m.Metadata[mapkey] = mapvalue - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return protohelpers.ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Timestamp == nil { - m.Timestamp = &Timestamp{} - } - if err := m.Timestamp.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 4: + case 5: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Reference", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Color", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Color = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *Transaction) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Transaction: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Transaction: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Postings", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Postings = append(m.Postings, &Posting{}) + if err := m.Postings[len(m.Postings)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Metadata", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Metadata == nil { + m.Metadata = make(map[string]*MetadataValue) + } + var mapkey string + var mapvalue *MetadataValue + for iNdEx < postIndex { + entryPreIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + if fieldNum == 1 { + var stringLenmapkey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLenmapkey |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLenmapkey := int(stringLenmapkey) + if intStringLenmapkey < 0 { + return protohelpers.ErrInvalidLength + } + postStringIndexmapkey := iNdEx + intStringLenmapkey + if postStringIndexmapkey < 0 { + return protohelpers.ErrInvalidLength + } + if postStringIndexmapkey > l { + return io.ErrUnexpectedEOF + } + mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) + iNdEx = postStringIndexmapkey + } else if fieldNum == 2 { + var mapmsglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapmsglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if mapmsglen < 0 { + return protohelpers.ErrInvalidLength + } + postmsgIndex := iNdEx + mapmsglen + if postmsgIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postmsgIndex > l { + return io.ErrUnexpectedEOF + } + mapvalue = &MetadataValue{} + if err := mapvalue.UnmarshalVT(dAtA[iNdEx:postmsgIndex]); err != nil { + return err + } + iNdEx = postmsgIndex + } else { + iNdEx = entryPreIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > postIndex { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + m.Metadata[mapkey] = mapvalue + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Timestamp", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Timestamp == nil { + m.Timestamp = &Timestamp{} + } + if err := m.Timestamp.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Reference", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -24642,105 +24925,161 @@ func (m *VolumesByAssets) UnmarshalVT(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - if m.Volumes == nil { - m.Volumes = make(map[string]*Volumes) + m.Volumes = append(m.Volumes, &VolumeEntry{}) + if err := m.Volumes[len(m.Volumes)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err } - var mapkey string - var mapvalue *Volumes - for iNdEx < postIndex { - entryPreIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *VolumeEntry) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: VolumeEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: VolumeEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Asset", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow } - fieldNum := int32(wire >> 3) - if fieldNum == 1 { - var stringLenmapkey uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapkey |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapkey := int(stringLenmapkey) - if intStringLenmapkey < 0 { - return protohelpers.ErrInvalidLength - } - postStringIndexmapkey := iNdEx + intStringLenmapkey - if postStringIndexmapkey < 0 { - return protohelpers.ErrInvalidLength - } - if postStringIndexmapkey > l { - return io.ErrUnexpectedEOF - } - mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) - iNdEx = postStringIndexmapkey - } else if fieldNum == 2 { - var mapmsglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - mapmsglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if mapmsglen < 0 { - return protohelpers.ErrInvalidLength - } - postmsgIndex := iNdEx + mapmsglen - if postmsgIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postmsgIndex > l { - return io.ErrUnexpectedEOF - } - mapvalue = &Volumes{} - if err := mapvalue.UnmarshalVT(dAtA[iNdEx:postmsgIndex]); err != nil { - return err - } - iNdEx = postmsgIndex - } else { - iNdEx = entryPreIndex - skippy, err := protohelpers.Skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return protohelpers.ErrInvalidLength - } - if (iNdEx + skippy) > postIndex { - return io.ErrUnexpectedEOF - } - iNdEx += skippy + if iNdEx >= l { + return io.ErrUnexpectedEOF } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Asset = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Color", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Color = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Volumes", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Volumes == nil { + m.Volumes = &Volumes{} + } + if err := m.Volumes.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err } - m.Volumes[mapkey] = mapvalue iNdEx = postIndex default: iNdEx = preIndex @@ -24944,6 +25283,157 @@ func (m *PostCommitVolumes) UnmarshalVT(dAtA []byte) error { } return nil } +func (m *AccountVolume) UnmarshalVT(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: AccountVolume: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: AccountVolume: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Asset", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Asset = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Color", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Color = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Volumes", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.Volumes == nil { + m.Volumes = &VolumesWithBalance{} + } + if err := m.Volumes.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := protohelpers.Skip(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return protohelpers.ErrInvalidLength + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *Account) UnmarshalVT(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -25163,214 +25653,119 @@ func (m *Account) UnmarshalVT(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - if m.FirstUsage == nil { - m.FirstUsage = &Timestamp{} - } - if err := m.FirstUsage.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field InsertionDate", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return protohelpers.ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.InsertionDate == nil { - m.InsertionDate = &Timestamp{} - } - if err := m.InsertionDate.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field UpdatedAt", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return protohelpers.ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.UpdatedAt == nil { - m.UpdatedAt = &Timestamp{} - } - if err := m.UpdatedAt.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + if m.FirstUsage == nil { + m.FirstUsage = &Timestamp{} + } + if err := m.FirstUsage.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field InsertionDate", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.InsertionDate == nil { + m.InsertionDate = &Timestamp{} + } + if err := m.InsertionDate.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field UpdatedAt", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.UpdatedAt == nil { + m.UpdatedAt = &Timestamp{} + } + if err := m.UpdatedAt.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Volumes", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Volumes = append(m.Volumes, &AccountVolume{}) + if err := m.Volumes[len(m.Volumes)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex - case 6: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Volumes", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return protohelpers.ErrInvalidLength - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.Volumes == nil { - m.Volumes = make(map[string]*VolumesWithBalance) - } - var mapkey string - var mapvalue *VolumesWithBalance - for iNdEx < postIndex { - entryPreIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - if fieldNum == 1 { - var stringLenmapkey uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLenmapkey |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLenmapkey := int(stringLenmapkey) - if intStringLenmapkey < 0 { - return protohelpers.ErrInvalidLength - } - postStringIndexmapkey := iNdEx + intStringLenmapkey - if postStringIndexmapkey < 0 { - return protohelpers.ErrInvalidLength - } - if postStringIndexmapkey > l { - return io.ErrUnexpectedEOF - } - mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) - iNdEx = postStringIndexmapkey - } else if fieldNum == 2 { - var mapmsglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return protohelpers.ErrIntOverflow - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - mapmsglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if mapmsglen < 0 { - return protohelpers.ErrInvalidLength - } - postmsgIndex := iNdEx + mapmsglen - if postmsgIndex < 0 { - return protohelpers.ErrInvalidLength - } - if postmsgIndex > l { - return io.ErrUnexpectedEOF - } - mapvalue = &VolumesWithBalance{} - if err := mapvalue.UnmarshalVT(dAtA[iNdEx:postmsgIndex]); err != nil { - return err - } - iNdEx = postmsgIndex - } else { - iNdEx = entryPreIndex - skippy, err := protohelpers.Skip(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return protohelpers.ErrInvalidLength - } - if (iNdEx + skippy) > postIndex { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - m.Volumes[mapkey] = mapvalue - iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -33840,6 +34235,38 @@ func (m *TouchedVolume) UnmarshalVT(dAtA []byte) error { } m.Asset = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Color", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Color = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -42530,6 +42957,38 @@ func (m *AggregatedVolume) UnmarshalVT(dAtA []byte) error { return err } iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Color", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Color = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/internal/proto/commonpb/posting.go b/internal/proto/commonpb/posting.go index 206c9490a9..1785faffb8 100644 --- a/internal/proto/commonpb/posting.go +++ b/internal/proto/commonpb/posting.go @@ -4,41 +4,43 @@ import ( "math/big" "github.com/holiman/uint256" -) - -// Postings is a slice of Posting pointers. -type Postings []*Posting - -// Reverse reverses the order of postings and swaps source/destination. -func (p Postings) Reverse() Postings { - postings := make(Postings, len(p)) - copy(postings, p) - - for i := range p { - if postings[i] != nil { - postings[i] = &Posting{ - Source: p[i].GetDestination(), - Destination: p[i].GetSource(), - Amount: p[i].GetAmount(), - Asset: p[i].GetAsset(), - } - } - } - // Reverse the order - for i := range len(p) / 2 { - postings[i], postings[len(postings)-i-1] = postings[len(postings)-i-1], postings[i] - } + "github.com/formancehq/ledger/v3/internal/adapter/json" +) - return postings +// MarshalJSON implements json.Marshaler for Posting. Color is always emitted +// (even when empty) so clients can distinguish the uncolored bucket from an +// older response shape that predates the dimension — same contract as +// VolumeEntry and accountVolumeJSON. +func (x *Posting) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + Source string `json:"source"` + Destination string `json:"destination"` + Amount *Uint256 `json:"amount,omitempty"` + Asset string `json:"asset"` + Color string `json:"color"` + }{ + Source: x.GetSource(), + Destination: x.GetDestination(), + Amount: x.GetAmount(), + Asset: x.GetAsset(), + Color: x.GetColor(), + }) } -// NewPosting creates a new Posting from the given parameters. -// Converts the *big.Int amount to *Uint256 via uint256.Int intermediary. +// NewPosting creates a new uncolored Posting. Use NewColoredPosting to set a +// non-empty color. Converts the *big.Int amount to *Uint256 via uint256.Int +// intermediary. func NewPosting(source, destination, asset string, amount *big.Int) *Posting { + return NewColoredPosting(source, destination, asset, "", amount) +} + +// NewColoredPosting creates a new Posting with an explicit color. Color is +// the empty string for the uncolored bucket. +func NewColoredPosting(source, destination, asset, color string, amount *big.Int) *Posting { var u uint256.Int if overflow := u.SetFromBig(amount); overflow { - panic("commonpb.NewPosting: amount exceeds 256 bits") + panic("commonpb.NewColoredPosting: amount exceeds 256 bits") } return &Posting{ @@ -46,5 +48,6 @@ func NewPosting(source, destination, asset string, amount *big.Int) *Posting { Destination: destination, Amount: NewUint256(&u), Asset: asset, + Color: color, } } diff --git a/internal/proto/commonpb/posting_json_test.go b/internal/proto/commonpb/posting_json_test.go new file mode 100644 index 0000000000..21d9a20d8f --- /dev/null +++ b/internal/proto/commonpb/posting_json_test.go @@ -0,0 +1,35 @@ +package commonpb + +import ( + "encoding/json" + "math/big" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestPosting_MarshalJSON_EmitsEmptyColor guards against the regression where +// REST transaction/create/revert responses omit `color` for the uncolored +// bucket because the generated struct tag is `json:"color,omitempty"`. The +// REST layer must surface `color: ""` so clients can distinguish "uncolored" +// from "field absent in an older response shape". +func TestPosting_MarshalJSON_EmitsEmptyColor(t *testing.T) { + t.Parallel() + + p := NewPosting("world", "users:alice", "USD/2", big.NewInt(100)) + + data, err := json.Marshal(p) + require.NoError(t, err) + require.Contains(t, string(data), `"color":""`, + "uncolored postings must surface color:\"\" rather than dropping the field") +} + +func TestPosting_MarshalJSON_EmitsColor(t *testing.T) { + t.Parallel() + + p := NewColoredPosting("world", "users:alice", "USD/2", "GRANTS", big.NewInt(100)) + + data, err := json.Marshal(p) + require.NoError(t, err) + require.Contains(t, string(data), `"color":"GRANTS"`) +} diff --git a/internal/proto/commonpb/transaction.go b/internal/proto/commonpb/transaction.go index 5180d1a27c..78ad81fab2 100644 --- a/internal/proto/commonpb/transaction.go +++ b/internal/proto/commonpb/transaction.go @@ -115,40 +115,6 @@ func (tx *Transaction) IsReverted() bool { return tx.GetReverted() || tx.GetRevertedAt() != nil } -// Reverse creates a reversed copy of the transaction with swapped source/destination in postings. -func (tx *Transaction) Reverse() *Transaction { - if tx == nil { - return NewTransaction() - } - - postings := Postings(tx.GetPostings()).Reverse() - ret := NewTransaction().WithPostings(postings...) - - // Copy other fields - copy the metadata map reference - ret.Metadata = tx.GetMetadata() - if tx.GetTimestamp() != nil { - ret.Timestamp = tx.GetTimestamp() - } - - ret.Reference = tx.GetReference() - ret.Id = tx.GetId() - - ret.Reverted = tx.GetReverted() - if tx.GetInsertedAt() != nil { - ret.InsertedAt = tx.GetInsertedAt() - } - - if tx.GetUpdatedAt() != nil { - ret.UpdatedAt = tx.GetUpdatedAt() - } - - if tx.GetRevertedAt() != nil { - ret.RevertedAt = tx.GetRevertedAt() - } - - return ret -} - // InvolvedDestinations returns a map of destination accounts to their assets. func (tx *Transaction) InvolvedDestinations() map[string][]string { ret := make(map[string][]string) diff --git a/internal/proto/commonpb/volumes.go b/internal/proto/commonpb/volumes.go index efebfc8235..a2c2e95d08 100644 --- a/internal/proto/commonpb/volumes.go +++ b/internal/proto/commonpb/volumes.go @@ -4,6 +4,7 @@ import ( "database/sql/driver" "fmt" "math/big" + "sort" "strings" "github.com/invopop/jsonschema" @@ -44,18 +45,6 @@ func (*Volumes) JSONSchemaExtend(schema *jsonschema.Schema) { schema.Properties.Set("balance", inputProperty) } -// Copy creates a deep copy of Volumes. -func (v *Volumes) Copy() *Volumes { - if v == nil { - return &Volumes{} - } - - return &Volumes{ - Input: v.GetInput(), - Output: v.GetOutput(), - } -} - // NewEmptyVolumes creates new empty volumes. func NewEmptyVolumes() *Volumes { return NewVolumesInt64(0, 0) @@ -114,198 +103,55 @@ func (v *Volumes) MarshalJSON() ([]byte, error) { }) } -// BalancesByAssets is a type alias for map[string]*big.Int. -type BalancesByAssets = map[string]*big.Int - -// BalancesByAssetsByAccounts is a type alias for map[string]BalancesByAssets. -type BalancesByAssetsByAccounts = map[string]BalancesByAssets - -// Balances calculates balances from VolumesByAssets. -func (v *VolumesByAssets) Balances() BalancesByAssets { - if v == nil || v.Volumes == nil { - return BalancesByAssets{} - } - - balances := BalancesByAssets{} - - for asset, vol := range v.GetVolumes() { - if vol != nil { - input, _ := new(big.Int).SetString(vol.GetInput(), 10) - output, _ := new(big.Int).SetString(vol.GetOutput(), 10) - - if input == nil { - input = big.NewInt(0) - } - - if output == nil { - output = big.NewInt(0) - } - - balances[asset] = new(big.Int).Sub(input, output) - } - } - - return balances -} - -// Copy creates a deep copy of VolumesByAssets. -func (v *VolumesByAssets) Copy() *VolumesByAssets { +// SortVolumes orders the inner volumes list by (asset, color) ascending. +// Stable order is required so JSON / proto output is deterministic across +// reads and so snapshot tests don't flap. +func (v *VolumesByAssets) SortVolumes() { if v == nil { - return &VolumesByAssets{Volumes: make(map[string]*Volumes)} - } - - ret := &VolumesByAssets{ - Volumes: make(map[string]*Volumes), - } - for key, volumes := range v.GetVolumes() { - ret.Volumes[key] = volumes.Copy() - } - - return ret -} - -// AddInput adds an input volume to the specified account and asset. -func (a *PostCommitVolumes) AddInput(account, asset string, input *big.Int) { - if a == nil { return } - - if a.VolumesByAccount == nil { - a.VolumesByAccount = make(map[string]*VolumesByAssets) - } - - if _, ok := a.GetVolumesByAccount()[account]; !ok { - a.VolumesByAccount[account] = &VolumesByAssets{ - Volumes: make(map[string]*Volumes), + sort.Slice(v.GetVolumes(), func(i, j int) bool { + if a, b := v.GetVolumes()[i].GetAsset(), v.GetVolumes()[j].GetAsset(); a != b { + return a < b } - } - if _, ok := a.GetVolumesByAccount()[account].GetVolumes()[asset]; !ok { - a.VolumesByAccount[account].Volumes[asset] = NewEmptyVolumes() - } - - currentInput, _ := new(big.Int).SetString(a.GetVolumesByAccount()[account].GetVolumes()[asset].GetInput(), 10) - if currentInput == nil { - currentInput = big.NewInt(0) - } - - a.VolumesByAccount[account].Volumes[asset].Input = currentInput.Add(currentInput, input).String() -} - -// AddOutput adds an output volume to the specified account and asset. -func (a *PostCommitVolumes) AddOutput(account, asset string, output *big.Int) { - if a == nil { - return - } - - if a.VolumesByAccount == nil { - a.VolumesByAccount = make(map[string]*VolumesByAssets) - } - - if _, ok := a.GetVolumesByAccount()[account]; !ok { - a.VolumesByAccount[account] = &VolumesByAssets{ - Volumes: make(map[string]*Volumes), - } - } - - if _, ok := a.GetVolumesByAccount()[account].GetVolumes()[asset]; !ok { - a.VolumesByAccount[account].Volumes[asset] = NewEmptyVolumes() - } - - currentOutput, _ := new(big.Int).SetString(a.GetVolumesByAccount()[account].GetVolumes()[asset].GetOutput(), 10) - if currentOutput == nil { - currentOutput = big.NewInt(0) - } - - a.VolumesByAccount[account].Volumes[asset].Output = currentOutput.Add(currentOutput, output).String() + return v.GetVolumes()[i].GetColor() < v.GetVolumes()[j].GetColor() + }) } -// Copy creates a deep copy of PostCommitVolumes. -func (a *PostCommitVolumes) Copy() *PostCommitVolumes { - if a == nil || len(a.GetVolumesByAccount()) == 0 { - return &PostCommitVolumes{VolumesByAccount: make(map[string]*VolumesByAssets)} +// FindVolume returns the *Volumes for a given (asset, color) tuple, or nil +// when no entry matches. Color "" is the uncolored bucket. +// +// VolumesByAssets is a sorted list, so this is an O(n) linear scan. For +// repeated lookups, callers should build their own map. +func (v *VolumesByAssets) FindVolume(asset, color string) *Volumes { + if entry := v.findVolumeEntry(asset, color); entry != nil { + return entry.GetVolumes() } - ret := &PostCommitVolumes{ - VolumesByAccount: make(map[string]*VolumesByAssets), - } - for key, volumes := range a.GetVolumesByAccount() { - ret.VolumesByAccount[key] = volumes.Copy() - } - - return ret + return nil } -// SubtractPostings subtracts postings from PostCommitVolumes. -func (a *PostCommitVolumes) SubtractPostings(postings []*Posting) *PostCommitVolumes { - if a == nil || len(a.GetVolumesByAccount()) == 0 { - return &PostCommitVolumes{VolumesByAccount: make(map[string]*VolumesByAssets)} +// findVolumeEntry returns the *VolumeEntry matching (asset, color), or nil. +func (v *VolumesByAssets) findVolumeEntry(asset, color string) *VolumeEntry { + if v == nil { + return nil } - - ret := a.Copy() - - for _, posting := range postings { - if posting == nil { - continue + for _, entry := range v.GetVolumes() { + if entry.GetAsset() == asset && entry.GetColor() == color { + return entry } - - ret.AddOutput(posting.GetSource(), posting.GetAsset(), big.NewInt(0).Neg(posting.GetAmount().ToBigInt())) - ret.AddInput(posting.GetDestination(), posting.GetAsset(), big.NewInt(0).Neg(posting.GetAmount().ToBigInt())) } - return ret + return nil } -// Merge merges volumes into PostCommitVolumes. -func (a *PostCommitVolumes) Merge(volumes *PostCommitVolumes) *PostCommitVolumes { +// SortVolumes sorts every per-account volume list deterministically. +func (a *PostCommitVolumes) SortVolumes() { if a == nil { - a = &PostCommitVolumes{VolumesByAccount: make(map[string]*VolumesByAssets)} + return } - - if volumes == nil || volumes.VolumesByAccount == nil { - return a + for _, vba := range a.GetVolumesByAccount() { + vba.SortVolumes() } - - for account, volumesByAssets := range volumes.GetVolumesByAccount() { - if _, ok := a.GetVolumesByAccount()[account]; !ok { - a.VolumesByAccount[account] = &VolumesByAssets{ - Volumes: make(map[string]*Volumes), - } - } - - for asset, vol := range volumesByAssets.GetVolumes() { - if _, ok := a.GetVolumesByAccount()[account].GetVolumes()[asset]; !ok { - a.VolumesByAccount[account].Volumes[asset] = NewEmptyVolumes() - } - - currentInput, _ := new(big.Int).SetString(a.GetVolumesByAccount()[account].GetVolumes()[asset].GetInput(), 10) - currentOutput, _ := new(big.Int).SetString(a.GetVolumesByAccount()[account].GetVolumes()[asset].GetOutput(), 10) - volInput, _ := new(big.Int).SetString(vol.GetInput(), 10) - volOutput, _ := new(big.Int).SetString(vol.GetOutput(), 10) - - if currentInput == nil { - currentInput = big.NewInt(0) - } - - if currentOutput == nil { - currentOutput = big.NewInt(0) - } - - if volInput == nil { - volInput = big.NewInt(0) - } - - if volOutput == nil { - volOutput = big.NewInt(0) - } - - a.VolumesByAccount[account].Volumes[asset].Input = currentInput.Add(currentInput, volInput).String() - a.VolumesByAccount[account].Volumes[asset].Output = currentOutput.Add(currentOutput, volOutput).String() - } - } - - return a } - -// Balances is a type alias for map[string]map[string]*big.Int. -type Balances = map[string]map[string]*big.Int diff --git a/internal/proto/servicepb/bucket.pb.go b/internal/proto/servicepb/bucket.pb.go index 78dbf02a23..b0712f4a29 100644 --- a/internal/proto/servicepb/bucket.pb.go +++ b/internal/proto/servicepb/bucket.pb.go @@ -318,9 +318,13 @@ type GetAccountRequest struct { Ledger string `protobuf:"bytes,1,opt,name=ledger,proto3" json:"ledger,omitempty"` Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"` // checkpoint_id, when non-zero, reads from a query checkpoint instead of the live store - CheckpointId uint64 `protobuf:"fixed64,3,opt,name=checkpoint_id,json=checkpointId,proto3" json:"checkpoint_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + CheckpointId uint64 `protobuf:"fixed64,3,opt,name=checkpoint_id,json=checkpointId,proto3" json:"checkpoint_id,omitempty"` + // When true, colored buckets are summed into a single entry per asset + // with color = "" in the returned Account.volumes list. By default each + // (asset, color) tuple gets its own entry. + CollapseColors bool `protobuf:"varint,4,opt,name=collapse_colors,json=collapseColors,proto3" json:"collapse_colors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetAccountRequest) Reset() { @@ -374,6 +378,13 @@ func (x *GetAccountRequest) GetCheckpointId() uint64 { return 0 } +func (x *GetAccountRequest) GetCollapseColors() bool { + if x != nil { + return x.CollapseColors + } + return false +} + type GetTransactionRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Ledger string `protobuf:"bytes,1,opt,name=ledger,proto3" json:"ledger,omitempty"` @@ -6729,8 +6740,13 @@ type NormalizedPosting struct { SourcePattern string `protobuf:"bytes,1,opt,name=source_pattern,json=sourcePattern,proto3" json:"source_pattern,omitempty"` // e.g. "users:{id}:main" DestinationPattern string `protobuf:"bytes,2,opt,name=destination_pattern,json=destinationPattern,proto3" json:"destination_pattern,omitempty"` // e.g. "bank:fees" Asset string `protobuf:"bytes,3,opt,name=asset,proto3" json:"asset,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // color discriminates postings on the same (source,destination,asset) but + // different color buckets in the FSM. Two flows that share addresses and + // asset but differ in color must produce distinct flow signatures so the + // analysis endpoint does not silently merge segregated buckets. + Color string `protobuf:"bytes,4,opt,name=color,proto3" json:"color,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *NormalizedPosting) Reset() { @@ -6784,6 +6800,13 @@ func (x *NormalizedPosting) GetAsset() string { return "" } +func (x *NormalizedPosting) GetColor() string { + if x != nil { + return x.Color + } + return "" +} + type TemporalStats struct { state protoimpl.MessageState `protogen:"open.v1"` FirstSeen *commonpb.Timestamp `protobuf:"bytes,1,opt,name=first_seen,json=firstSeen,proto3" json:"first_seen,omitempty"` @@ -7864,9 +7887,13 @@ type AggregateVolumesRequest struct { // to the first matching prefix. Accounts not matching any prefix are excluded. GroupByPrefixes []string `protobuf:"bytes,5,rep,name=group_by_prefixes,json=groupByPrefixes,proto3" json:"group_by_prefixes,omitempty"` // checkpoint_id, when non-zero, reads from a query checkpoint instead of the live store - CheckpointId uint64 `protobuf:"fixed64,6,opt,name=checkpoint_id,json=checkpointId,proto3" json:"checkpoint_id,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + CheckpointId uint64 `protobuf:"fixed64,6,opt,name=checkpoint_id,json=checkpointId,proto3" json:"checkpoint_id,omitempty"` + // When true, amounts in colored buckets are summed into the uncolored bucket + // ("" color). Result entries are produced with color = "". By default each + // (asset, color) bucket yields its own AggregatedVolume entry. + CollapseColors bool `protobuf:"varint,7,opt,name=collapse_colors,json=collapseColors,proto3" json:"collapse_colors,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AggregateVolumesRequest) Reset() { @@ -7941,6 +7968,13 @@ func (x *AggregateVolumesRequest) GetCheckpointId() uint64 { return 0 } +func (x *AggregateVolumesRequest) GetCollapseColors() bool { + if x != nil { + return x.CollapseColors + } + return false +} + // QueryProfile contains execution statistics for a read query. // Returned to clients via gRPC trailing metadata when requested. type QueryProfile struct { @@ -8691,11 +8725,12 @@ var File_bucket_proto protoreflect.FileDescriptor const file_bucket_proto_rawDesc = "" + "\n" + - "\fbucket.proto\x12\x06ledger\x1a\fcommon.proto\x1a\vaudit.proto\x1a\x0fsignature.proto\"j\n" + + "\fbucket.proto\x12\x06ledger\x1a\fcommon.proto\x1a\vaudit.proto\x1a\x0fsignature.proto\"\x93\x01\n" + "\x11GetAccountRequest\x12\x16\n" + "\x06ledger\x18\x01 \x01(\tR\x06ledger\x12\x18\n" + "\aaddress\x18\x02 \x01(\tR\aaddress\x12#\n" + - "\rcheckpoint_id\x18\x03 \x01(\x06R\fcheckpointId\"{\n" + + "\rcheckpoint_id\x18\x03 \x01(\x06R\fcheckpointId\x12'\n" + + "\x0fcollapse_colors\x18\x04 \x01(\bR\x0ecollapseColors\"{\n" + "\x15GetTransactionRequest\x12\x16\n" + "\x06ledger\x18\x01 \x01(\tR\x06ledger\x12%\n" + "\x0etransaction_id\x18\x02 \x01(\x06R\rtransactionId\x12#\n" + @@ -9145,11 +9180,12 @@ const file_bucket_proto_rawDesc = "" + "\bpostings\x18\x04 \x03(\v2\x19.ledger.NormalizedPostingR\bpostings\x121\n" + "\btemporal\x18\x05 \x01(\v2\x15.ledger.TemporalStatsR\btemporal\x12;\n" + "\fvolume_stats\x18\x06 \x03(\v2\x18.ledger.AssetVolumeStatsR\vvolumeStats\x12#\n" + - "\rmetadata_keys\x18\a \x03(\tR\fmetadataKeys\"\x81\x01\n" + + "\rmetadata_keys\x18\a \x03(\tR\fmetadataKeys\"\x97\x01\n" + "\x11NormalizedPosting\x12%\n" + "\x0esource_pattern\x18\x01 \x01(\tR\rsourcePattern\x12/\n" + "\x13destination_pattern\x18\x02 \x01(\tR\x12destinationPattern\x12\x14\n" + - "\x05asset\x18\x03 \x01(\tR\x05asset\"\xd6\x01\n" + + "\x05asset\x18\x03 \x01(\tR\x05asset\x12\x14\n" + + "\x05color\x18\x04 \x01(\tR\x05color\"\xd6\x01\n" + "\rTemporalStats\x120\n" + "\n" + "first_seen\x18\x01 \x01(\v2\x11.common.TimestampR\tfirstSeen\x12.\n" + @@ -9229,14 +9265,15 @@ const file_bucket_proto_rawDesc = "" + "\fSCOPE_LEDGER\x10\x02\"T\n" + "\x15GetLedgerStatsRequest\x12\x16\n" + "\x06ledger\x18\x01 \x01(\tR\x06ledger\x12#\n" + - "\rcheckpoint_id\x18\x02 \x01(\x06R\fcheckpointId\"\x85\x02\n" + + "\rcheckpoint_id\x18\x02 \x01(\x06R\fcheckpointId\"\xae\x02\n" + "\x17AggregateVolumesRequest\x12\x16\n" + "\x06ledger\x18\x01 \x01(\tR\x06ledger\x12+\n" + "\x06filter\x18\x02 \x01(\v2\x13.common.QueryFilterR\x06filter\x12(\n" + "\x10min_log_sequence\x18\x03 \x01(\x06R\x0eminLogSequence\x12*\n" + "\x11use_max_precision\x18\x04 \x01(\bR\x0fuseMaxPrecision\x12*\n" + "\x11group_by_prefixes\x18\x05 \x03(\tR\x0fgroupByPrefixes\x12#\n" + - "\rcheckpoint_id\x18\x06 \x01(\x06R\fcheckpointId\"\xde\x02\n" + + "\rcheckpoint_id\x18\x06 \x01(\x06R\fcheckpointId\x12'\n" + + "\x0fcollapse_colors\x18\a \x01(\bR\x0ecollapseColors\"\xde\x02\n" + "\fQueryProfile\x12*\n" + "\x11index_duration_us\x18\x01 \x01(\x03R\x0findexDurationUs\x124\n" + "\x16enrichment_duration_us\x18\x02 \x01(\x03R\x14enrichmentDurationUs\x12'\n" + diff --git a/internal/proto/servicepb/bucket_reader.pb.go b/internal/proto/servicepb/bucket_reader.pb.go index 96cad93d68..07739fbe92 100644 --- a/internal/proto/servicepb/bucket_reader.pb.go +++ b/internal/proto/servicepb/bucket_reader.pb.go @@ -15,6 +15,7 @@ type GetAccountRequestReader interface { GetLedger() string GetAddress() string GetCheckpointId() uint64 + GetCollapseColors() bool Mutate() *GetAccountRequest } @@ -32,6 +33,10 @@ func (r *getAccountRequestReadonly) GetCheckpointId() uint64 { return r.v.GetCheckpointId() } +func (r *getAccountRequestReadonly) GetCollapseColors() bool { + return r.v.GetCollapseColors() +} + func (r *getAccountRequestReadonly) Mutate() *GetAccountRequest { return r.v.CloneVT() } @@ -7750,6 +7755,7 @@ type NormalizedPostingReader interface { GetSourcePattern() string GetDestinationPattern() string GetAsset() string + GetColor() string Mutate() *NormalizedPosting } @@ -7767,6 +7773,10 @@ func (r *normalizedPostingReadonly) GetAsset() string { return r.v.GetAsset() } +func (r *normalizedPostingReadonly) GetColor() string { + return r.v.GetColor() +} + func (r *normalizedPostingReadonly) Mutate() *NormalizedPosting { return r.v.CloneVT() } @@ -9213,6 +9223,7 @@ type AggregateVolumesRequestReader interface { GetUseMaxPrecision() bool GetGroupByPrefixes() []string GetCheckpointId() uint64 + GetCollapseColors() bool Mutate() *AggregateVolumesRequest } @@ -9246,6 +9257,10 @@ func (r *aggregateVolumesRequestReadonly) GetCheckpointId() uint64 { return r.v.GetCheckpointId() } +func (r *aggregateVolumesRequestReadonly) GetCollapseColors() bool { + return r.v.GetCollapseColors() +} + func (r *aggregateVolumesRequestReadonly) Mutate() *AggregateVolumesRequest { return r.v.CloneVT() } diff --git a/internal/proto/servicepb/bucket_vtproto.pb.go b/internal/proto/servicepb/bucket_vtproto.pb.go index bcdbfe8035..4ea74b06b0 100644 --- a/internal/proto/servicepb/bucket_vtproto.pb.go +++ b/internal/proto/servicepb/bucket_vtproto.pb.go @@ -31,6 +31,7 @@ func (m *GetAccountRequest) CloneVT() *GetAccountRequest { r.Ledger = m.Ledger r.Address = m.Address r.CheckpointId = m.CheckpointId + r.CollapseColors = m.CollapseColors if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -2461,6 +2462,7 @@ func (m *NormalizedPosting) CloneVT() *NormalizedPosting { r.SourcePattern = m.SourcePattern r.DestinationPattern = m.DestinationPattern r.Asset = m.Asset + r.Color = m.Color if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) copy(r.unknownFields, m.unknownFields) @@ -2860,6 +2862,7 @@ func (m *AggregateVolumesRequest) CloneVT() *AggregateVolumesRequest { r.MinLogSequence = m.MinLogSequence r.UseMaxPrecision = m.UseMaxPrecision r.CheckpointId = m.CheckpointId + r.CollapseColors = m.CollapseColors if rhs := m.GroupByPrefixes; rhs != nil { tmpContainer := make([]string, len(rhs)) copy(tmpContainer, rhs) @@ -3140,6 +3143,9 @@ func (this *GetAccountRequest) EqualVT(that *GetAccountRequest) bool { if this.CheckpointId != that.CheckpointId { return false } + if this.CollapseColors != that.CollapseColors { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -7063,6 +7069,9 @@ func (this *NormalizedPosting) EqualVT(that *NormalizedPosting) bool { if this.Asset != that.Asset { return false } + if this.Color != that.Color { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -7641,6 +7650,9 @@ func (this *AggregateVolumesRequest) EqualVT(that *AggregateVolumesRequest) bool if this.CheckpointId != that.CheckpointId { return false } + if this.CollapseColors != that.CollapseColors { + return false + } return string(this.unknownFields) == string(that.unknownFields) } @@ -8087,6 +8099,16 @@ func (m *GetAccountRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.CollapseColors { + i-- + if m.CollapseColors { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } if m.CheckpointId != 0 { i -= 8 binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CheckpointId)) @@ -14235,6 +14257,13 @@ func (m *NormalizedPosting) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if len(m.Color) > 0 { + i -= len(m.Color) + copy(dAtA[i:], m.Color) + i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Color))) + i-- + dAtA[i] = 0x22 + } if len(m.Asset) > 0 { i -= len(m.Asset) copy(dAtA[i:], m.Asset) @@ -15254,6 +15283,16 @@ func (m *AggregateVolumesRequest) MarshalToSizedBufferVT(dAtA []byte) (int, erro i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } + if m.CollapseColors { + i-- + if m.CollapseColors { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x38 + } if m.CheckpointId != 0 { i -= 8 binary.LittleEndian.PutUint64(dAtA[i:], uint64(m.CheckpointId)) @@ -15986,6 +16025,9 @@ func (m *GetAccountRequest) SizeVT() (n int) { if m.CheckpointId != 0 { n += 9 } + if m.CollapseColors { + n += 2 + } n += len(m.unknownFields) return n } @@ -18535,6 +18577,10 @@ func (m *NormalizedPosting) SizeVT() (n int) { if l > 0 { n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) } + l = len(m.Color) + if l > 0 { + n += 1 + l + protohelpers.SizeOfVarint(uint64(l)) + } n += len(m.unknownFields) return n } @@ -18946,6 +18992,9 @@ func (m *AggregateVolumesRequest) SizeVT() (n int) { if m.CheckpointId != 0 { n += 9 } + if m.CollapseColors { + n += 2 + } n += len(m.unknownFields) return n } @@ -19330,6 +19379,26 @@ func (m *GetAccountRequest) UnmarshalVT(dAtA []byte) error { } m.CheckpointId = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CollapseColors", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.CollapseColors = bool(v != 0) default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -33233,6 +33302,38 @@ func (m *NormalizedPosting) UnmarshalVT(dAtA []byte) error { } m.Asset = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Color", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return protohelpers.ErrInvalidLength + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return protohelpers.ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Color = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) @@ -35626,6 +35727,26 @@ func (m *AggregateVolumesRequest) UnmarshalVT(dAtA []byte) error { } m.CheckpointId = uint64(binary.LittleEndian.Uint64(dAtA[iNdEx:])) iNdEx += 8 + case 7: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CollapseColors", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return protohelpers.ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.CollapseColors = bool(v != 0) default: iNdEx = preIndex skippy, err := protohelpers.Skip(dAtA[iNdEx:]) diff --git a/internal/query/aggregate.go b/internal/query/aggregate.go index 124d1279e4..d12ef0f9f0 100644 --- a/internal/query/aggregate.go +++ b/internal/query/aggregate.go @@ -15,14 +15,16 @@ import ( "github.com/formancehq/ledger/v3/internal/storage/readstore" ) -// assetKey identifies an asset by its base name and precision, used as map key -// in the aggregator to avoid string formatting/parsing round-trips. +// assetKey identifies a balance bucket by (base, precision, color). Color is +// always part of the key — different colors are distinct buckets. To collapse +// across colors at the result stage, use AggregateOptions.CollapseColors. type assetKey struct { base string precision uint8 + color string } -// aggregatedVol accumulates input/output volumes for a single asset. +// aggregatedVol accumulates input/output volumes for a single bucket. type aggregatedVol struct { input *uint256.Int output *uint256.Int @@ -32,12 +34,14 @@ type aggregatedVol struct { type volumeAggregator struct { byAsset map[assetKey]*aggregatedVol useMaxPrecision bool + collapseColors bool } -func newVolumeAggregator(useMaxPrecision bool) *volumeAggregator { +func newVolumeAggregator(useMaxPrecision, collapseColors bool) *volumeAggregator { return &volumeAggregator{ byAsset: make(map[assetKey]*aggregatedVol), useMaxPrecision: useMaxPrecision, + collapseColors: collapseColors, } } @@ -49,13 +53,11 @@ func (va *volumeAggregator) accumulate(entry attributes.ComputedEntry[*raftcmdpb return fmt.Errorf("unmarshaling volume key: %w", err) } - va.accumulateAsset(vk.AssetBase, vk.AssetPrecision, entry.Value) - - return nil + return va.accumulateAsset(vk.AssetBase, vk.AssetPrecision, vk.Color, entry.Value) } -func (va *volumeAggregator) accumulateAsset(base string, precision uint8, value *raftcmdpb.VolumePair) { - key := assetKey{base: base, precision: precision} +func (va *volumeAggregator) accumulateAsset(base string, precision uint8, color string, value *raftcmdpb.VolumePair) error { + key := assetKey{base: base, precision: precision, color: color} agg, ok := va.byAsset[key] if !ok { @@ -70,14 +72,20 @@ func (va *volumeAggregator) accumulateAsset(base string, precision uint8, value var tmp uint256.Int if value.GetInput() != nil { value.GetInput().IntoUint256(&tmp) - agg.input.Add(agg.input, &tmp) + if _, overflow := agg.input.AddOverflow(agg.input, &tmp); overflow { + return &domain.ErrAggregateOverflow{Stage: "accumulate", Side: "input"} + } } if value.GetOutput() != nil { value.GetOutput().IntoUint256(&tmp) - agg.output.Add(agg.output, &tmp) + if _, overflow := agg.output.AddOverflow(agg.output, &tmp); overflow { + return &domain.ErrAggregateOverflow{Stage: "accumulate", Side: "output"} + } } } + + return nil } // pow10 returns 10^exp as a uint256.Int. @@ -92,30 +100,77 @@ func pow10(exp uint8) *uint256.Int { return result } -func (va *volumeAggregator) result() *commonpb.AggregateResult { +func (va *volumeAggregator) result() (*commonpb.AggregateResult, error) { if va.useMaxPrecision { return va.resultWithMaxPrecision() } - volumes := make([]*commonpb.AggregatedVolume, 0, len(va.byAsset)) - for key, agg := range va.byAsset { + buckets := va.byAsset + if va.collapseColors { + var err error + buckets, err = collapseColorBuckets(buckets) + if err != nil { + return nil, err + } + } + + volumes := make([]*commonpb.AggregatedVolume, 0, len(buckets)) + for key, agg := range buckets { volumes = append(volumes, &commonpb.AggregatedVolume{ Asset: domain.FormatAsset(key.base, key.precision), + Color: key.color, Input: commonpb.NewUint256(agg.input), Output: commonpb.NewUint256(agg.output), }) } + sortAggregatedVolumes(volumes) + + return &commonpb.AggregateResult{Volumes: volumes}, nil +} + +// collapseColorBuckets sums all color buckets of the same (base, precision) +// into the empty-color bucket, returning a new map. Used when the caller +// explicitly requests collapse_colors. Overflow on the cross-color sum +// surfaces ErrAggregateOverflow rather than silently wrapping (the FSM +// already rejects per-bucket overflow on write, #321). +func collapseColorBuckets(in map[assetKey]*aggregatedVol) (map[assetKey]*aggregatedVol, error) { + out := make(map[assetKey]*aggregatedVol, len(in)) + for key, agg := range in { + k := assetKey{base: key.base, precision: key.precision} + merged, ok := out[k] + if !ok { + merged = &aggregatedVol{ + input: new(uint256.Int), + output: new(uint256.Int), + } + out[k] = merged + } + if _, overflow := merged.input.AddOverflow(merged.input, agg.input); overflow { + return nil, &domain.ErrAggregateOverflow{Stage: "collapse-colors", Side: "input"} + } + if _, overflow := merged.output.AddOverflow(merged.output, agg.output); overflow { + return nil, &domain.ErrAggregateOverflow{Stage: "collapse-colors", Side: "output"} + } + } + + return out, nil +} + +func sortAggregatedVolumes(volumes []*commonpb.AggregatedVolume) { sort.Slice(volumes, func(i, j int) bool { - return volumes[i].GetAsset() < volumes[j].GetAsset() - }) + if a, b := volumes[i].GetAsset(), volumes[j].GetAsset(); a != b { + return a < b + } - return &commonpb.AggregateResult{Volumes: volumes} + return volumes[i].GetColor() < volumes[j].GetColor() + }) } // resultWithMaxPrecision merges assets sharing the same base under the highest -// precision observed, rescaling lower-precision amounts. -func (va *volumeAggregator) resultWithMaxPrecision() *commonpb.AggregateResult { +// precision observed, rescaling lower-precision amounts. Color is preserved as +// part of the bucket key (and optionally collapsed afterwards). +func (va *volumeAggregator) resultWithMaxPrecision() (*commonpb.AggregateResult, error) { // First pass: find max precision per asset base. maxPrec := make(map[string]uint8) for key := range va.byAsset { @@ -124,12 +179,12 @@ func (va *volumeAggregator) resultWithMaxPrecision() *commonpb.AggregateResult { } } - // Second pass: rescale and merge under target precision. + // Second pass: rescale and merge under target precision, keeping color. merged := make(map[assetKey]*aggregatedVol) for key, agg := range va.byAsset { target := maxPrec[key.base] - mergedKey := assetKey{base: key.base, precision: target} + mergedKey := assetKey{base: key.base, precision: target, color: key.color} m, ok := merged[mergedKey] if !ok { @@ -141,17 +196,37 @@ func (va *volumeAggregator) resultWithMaxPrecision() *commonpb.AggregateResult { } if key.precision == target { - m.input.Add(m.input, agg.input) - m.output.Add(m.output, agg.output) + if _, overflow := m.input.AddOverflow(m.input, agg.input); overflow { + return nil, &domain.ErrAggregateOverflow{Stage: "max-precision-merge", Side: "input"} + } + if _, overflow := m.output.AddOverflow(m.output, agg.output); overflow { + return nil, &domain.ErrAggregateOverflow{Stage: "max-precision-merge", Side: "output"} + } } else { factor := pow10(target - key.precision) var scaled uint256.Int - scaled.Mul(agg.input, factor) - m.input.Add(m.input, &scaled) + if _, overflow := scaled.MulOverflow(agg.input, factor); overflow { + return nil, &domain.ErrAggregateOverflow{Stage: "max-precision-rescale", Side: "input"} + } + if _, overflow := m.input.AddOverflow(m.input, &scaled); overflow { + return nil, &domain.ErrAggregateOverflow{Stage: "max-precision-rescale", Side: "input"} + } + + if _, overflow := scaled.MulOverflow(agg.output, factor); overflow { + return nil, &domain.ErrAggregateOverflow{Stage: "max-precision-rescale", Side: "output"} + } + if _, overflow := m.output.AddOverflow(m.output, &scaled); overflow { + return nil, &domain.ErrAggregateOverflow{Stage: "max-precision-rescale", Side: "output"} + } + } + } - scaled.Mul(agg.output, factor) - m.output.Add(m.output, &scaled) + if va.collapseColors { + var err error + merged, err = collapseColorBuckets(merged) + if err != nil { + return nil, err } } @@ -159,22 +234,25 @@ func (va *volumeAggregator) resultWithMaxPrecision() *commonpb.AggregateResult { for key, agg := range merged { volumes = append(volumes, &commonpb.AggregatedVolume{ Asset: domain.FormatAsset(key.base, key.precision), + Color: key.color, Input: commonpb.NewUint256(agg.input), Output: commonpb.NewUint256(agg.output), }) } - sort.Slice(volumes, func(i, j int) bool { - return volumes[i].GetAsset() < volumes[j].GetAsset() - }) + sortAggregatedVolumes(volumes) - return &commonpb.AggregateResult{Volumes: volumes} + return &commonpb.AggregateResult{Volumes: volumes}, nil } // AggregateOptions configures volume aggregation behavior. type AggregateOptions struct { UseMaxPrecision bool GroupByPrefixes []string + // CollapseColors, when true, sums all color buckets of the same + // (asset_base, precision) into the empty-color bucket in the result. + // By default each (asset, color) tuple yields its own AggregatedVolume. + CollapseColors bool } // groupedAggregator dispatches volume entries to per-prefix volumeAggregators. @@ -187,7 +265,7 @@ type groupedAggregator struct { func newGroupedAggregator(opts AggregateOptions) *groupedAggregator { aggs := make(map[string]*volumeAggregator, len(opts.GroupByPrefixes)) for _, p := range opts.GroupByPrefixes { - aggs[p] = newVolumeAggregator(opts.UseMaxPrecision) + aggs[p] = newVolumeAggregator(opts.UseMaxPrecision, opts.CollapseColors) } return &groupedAggregator{ @@ -217,21 +295,23 @@ func (ga *groupedAggregator) accumulate(entry attributes.ComputedEntry[*raftcmdp return nil // account doesn't match any prefix, skip } - ga.aggregators[prefix].accumulateAsset(vk.AssetBase, vk.AssetPrecision, entry.Value) - - return nil + return ga.aggregators[prefix].accumulateAsset(vk.AssetBase, vk.AssetPrecision, vk.Color, entry.Value) } -func (ga *groupedAggregator) result() *commonpb.AggregateResult { +func (ga *groupedAggregator) result() (*commonpb.AggregateResult, error) { groups := make([]*commonpb.GroupedAggregateResult, 0, len(ga.prefixes)) for _, p := range ga.prefixes { + res, err := ga.aggregators[p].result() + if err != nil { + return nil, err + } groups = append(groups, &commonpb.GroupedAggregateResult{ Prefix: p, - Volumes: ga.aggregators[p].result().GetVolumes(), + Volumes: res.GetVolumes(), }) } - return &commonpb.AggregateResult{Groups: groups} + return &commonpb.AggregateResult{Groups: groups}, nil } // AggregateVolumes executes a cross-store merge-scan for filtered aggregation: @@ -280,7 +360,7 @@ func AggregateVolumes( } } - return acc.result(), nil + return acc.result() } // AggregateAllVolumes performs unfiltered volume aggregation in a single Pebble @@ -326,13 +406,13 @@ func AggregateAllVolumes( return nil, fmt.Errorf("scanning all volumes: %w", err) } - return acc.result(), nil + return acc.result() } // accumulator is the common interface for flat and grouped aggregation. type accumulator interface { accumulate(entry attributes.ComputedEntry[*raftcmdpb.VolumePair]) error - result() *commonpb.AggregateResult + result() (*commonpb.AggregateResult, error) } func newAccumulator(opts AggregateOptions) accumulator { @@ -340,5 +420,5 @@ func newAccumulator(opts AggregateOptions) accumulator { return newGroupedAggregator(opts) } - return newVolumeAggregator(opts.UseMaxPrecision) + return newVolumeAggregator(opts.UseMaxPrecision, opts.CollapseColors) } diff --git a/internal/query/aggregate_test.go b/internal/query/aggregate_test.go index 19c3150df1..8bbc556fb0 100644 --- a/internal/query/aggregate_test.go +++ b/internal/query/aggregate_test.go @@ -30,12 +30,12 @@ func makeEntry(ledgerName string, account, asset string, input, output uint64) a func TestVolumeAggregator_NoRescaling(t *testing.T) { t.Parallel() - va := newVolumeAggregator(false) + va := newVolumeAggregator(false, false) require.NoError(t, va.accumulate(makeEntry("test", "a", "USD/2", 100, 50))) require.NoError(t, va.accumulate(makeEntry("test", "a", "USD/4", 10000, 5000))) - result := va.result() + result, _ := va.result() require.Len(t, result.GetVolumes(), 2) require.Equal(t, "USD/2", result.GetVolumes()[0].GetAsset()) require.Equal(t, "USD/4", result.GetVolumes()[1].GetAsset()) @@ -44,14 +44,14 @@ func TestVolumeAggregator_NoRescaling(t *testing.T) { func TestVolumeAggregator_UseMaxPrecision(t *testing.T) { t.Parallel() - va := newVolumeAggregator(true) + va := newVolumeAggregator(true, false) // USD/2: 100 in, 50 out → rescaled to /4: 10000 in, 5000 out require.NoError(t, va.accumulate(makeEntry("test", "a", "USD/2", 100, 50))) // USD/4: 10000 in, 5000 out → stays as is require.NoError(t, va.accumulate(makeEntry("test", "b", "USD/4", 10000, 5000))) - result := va.result() + result, _ := va.result() require.Len(t, result.GetVolumes(), 1) require.Equal(t, "USD/4", result.GetVolumes()[0].GetAsset()) @@ -66,12 +66,12 @@ func TestVolumeAggregator_UseMaxPrecision(t *testing.T) { func TestVolumeAggregator_UseMaxPrecision_SamePrecision(t *testing.T) { t.Parallel() - va := newVolumeAggregator(true) + va := newVolumeAggregator(true, false) require.NoError(t, va.accumulate(makeEntry("test", "a", "EUR/2", 200, 100))) require.NoError(t, va.accumulate(makeEntry("test", "b", "EUR/2", 300, 150))) - result := va.result() + result, _ := va.result() require.Len(t, result.GetVolumes(), 1) require.Equal(t, "EUR/2", result.GetVolumes()[0].GetAsset()) @@ -83,13 +83,13 @@ func TestVolumeAggregator_UseMaxPrecision_SamePrecision(t *testing.T) { func TestVolumeAggregator_UseMaxPrecision_MixedAssets(t *testing.T) { t.Parallel() - va := newVolumeAggregator(true) + va := newVolumeAggregator(true, false) require.NoError(t, va.accumulate(makeEntry("test", "a", "USD/2", 100, 0))) require.NoError(t, va.accumulate(makeEntry("test", "a", "USD/4", 10000, 0))) require.NoError(t, va.accumulate(makeEntry("test", "a", "EUR/2", 200, 0))) - result := va.result() + result, _ := va.result() require.Len(t, result.GetVolumes(), 2) // Sorted: EUR/2, USD/4 require.Equal(t, "EUR/2", result.GetVolumes()[0].GetAsset()) @@ -99,12 +99,12 @@ func TestVolumeAggregator_UseMaxPrecision_MixedAssets(t *testing.T) { func TestVolumeAggregator_UseMaxPrecision_NoPrecision(t *testing.T) { t.Parallel() - va := newVolumeAggregator(true) + va := newVolumeAggregator(true, false) require.NoError(t, va.accumulate(makeEntry("test", "a", "GOLD", 500, 100))) require.NoError(t, va.accumulate(makeEntry("test", "b", "GOLD", 300, 200))) - result := va.result() + result, _ := va.result() require.Len(t, result.GetVolumes(), 1) require.Equal(t, "GOLD", result.GetVolumes()[0].GetAsset()) @@ -124,7 +124,7 @@ func TestGroupedAggregator_BasicPrefixes(t *testing.T) { require.NoError(t, ga.accumulate(makeEntry("test", "users:bob", "USD/2", 200, 100))) require.NoError(t, ga.accumulate(makeEntry("test", "merchants:shop1", "USD/2", 500, 250))) - result := ga.result() + result, _ := ga.result() require.Empty(t, result.GetVolumes(), "flat volumes should be empty for grouped result") require.Len(t, result.GetGroups(), 2) @@ -154,7 +154,7 @@ func TestGroupedAggregator_UnmatchedAccountSkipped(t *testing.T) { require.NoError(t, ga.accumulate(makeEntry("test", "users:alice", "USD/2", 100, 50))) require.NoError(t, ga.accumulate(makeEntry("test", "world", "USD/2", 9999, 9999))) // no match - result := ga.result() + result, _ := ga.result() require.Len(t, result.GetGroups(), 1) var input uint256.Int @@ -173,7 +173,7 @@ func TestGroupedAggregator_WithMaxPrecision(t *testing.T) { require.NoError(t, ga.accumulate(makeEntry("test", "users:alice", "USD/2", 100, 50))) require.NoError(t, ga.accumulate(makeEntry("test", "users:bob", "USD/4", 10000, 5000))) - result := ga.result() + result, _ := ga.result() require.Len(t, result.GetGroups(), 1) require.Len(t, result.GetGroups()[0].GetVolumes(), 1) require.Equal(t, "USD/4", result.GetGroups()[0].GetVolumes()[0].GetAsset()) @@ -192,7 +192,7 @@ func TestGroupedAggregator_FirstPrefixWins(t *testing.T) { require.NoError(t, ga.accumulate(makeEntry("test", "users:vip1", "USD/2", 100, 50))) - result := ga.result() + result, _ := ga.result() // "users:vip1" matches "users:" first. var input uint256.Int result.GetGroups()[0].GetVolumes()[0].GetInput().IntoUint256(&input) @@ -208,7 +208,7 @@ func TestNewAccumulator_FlatByDefault(t *testing.T) { acc := newAccumulator(AggregateOptions{}) require.NoError(t, acc.accumulate(makeEntry("test", "a", "USD/2", 100, 50))) - result := acc.result() + result, _ := acc.result() require.Len(t, result.GetVolumes(), 1) require.Empty(t, result.GetGroups()) } @@ -219,7 +219,7 @@ func TestNewAccumulator_GroupedWhenPrefixes(t *testing.T) { acc := newAccumulator(AggregateOptions{GroupByPrefixes: []string{"a:"}}) require.NoError(t, acc.accumulate(makeEntry("test", "a:1", "USD/2", 100, 50))) - result := acc.result() + result, _ := acc.result() require.Empty(t, result.GetVolumes()) require.Len(t, result.GetGroups(), 1) } @@ -232,3 +232,113 @@ func TestPow10(t *testing.T) { require.Equal(t, uint256.NewInt(100), pow10(2)) require.Equal(t, uint256.NewInt(1000000), pow10(6)) } + +// makeColoredEntry builds a volume entry with a non-empty color, used to +// verify the aggregator keeps color-segregated buckets distinct. +func makeColoredEntry(ledgerName, account, asset, color string, input, output uint64) attributes.ComputedEntry[*raftcmdpb.VolumePair] { + vk := domain.VolumeKey{ + AccountKey: domain.AccountKey{LedgerName: ledgerName, Account: account}, + Asset: asset, + Color: color, + } + + return attributes.ComputedEntry[*raftcmdpb.VolumePair]{ + CanonicalKey: vk.Bytes(), + Value: &raftcmdpb.VolumePair{ + Input: commonpb.NewUint256(uint256.NewInt(input)), + Output: commonpb.NewUint256(uint256.NewInt(output)), + }, + } +} + +func TestVolumeAggregator_SegregatesColorsByDefault(t *testing.T) { + t.Parallel() + + va := newVolumeAggregator(false, false) + + require.NoError(t, va.accumulate(makeColoredEntry("test", "a", "USD/2", "", 100, 50))) + require.NoError(t, va.accumulate(makeColoredEntry("test", "a", "USD/2", "GRANTS", 200, 80))) + require.NoError(t, va.accumulate(makeColoredEntry("test", "a", "USD/2", "OPS", 30, 10))) + + result, _ := va.result() + require.Len(t, result.GetVolumes(), 3, + "each (asset, color) bucket must yield its own AggregatedVolume entry by default") + + byColor := map[string]*commonpb.AggregatedVolume{} + for _, v := range result.GetVolumes() { + byColor[v.GetColor()] = v + } + + require.Contains(t, byColor, "") + require.Contains(t, byColor, "GRANTS") + require.Contains(t, byColor, "OPS") + + var got uint256.Int + byColor["GRANTS"].GetInput().IntoUint256(&got) + require.Equal(t, uint256.NewInt(200), &got, "GRANTS bucket must keep its own input") +} + +func TestVolumeAggregator_CollapseColors(t *testing.T) { + t.Parallel() + + va := newVolumeAggregator(false, true) // collapseColors=true + + require.NoError(t, va.accumulate(makeColoredEntry("test", "a", "USD/2", "", 100, 50))) + require.NoError(t, va.accumulate(makeColoredEntry("test", "a", "USD/2", "GRANTS", 200, 80))) + require.NoError(t, va.accumulate(makeColoredEntry("test", "a", "USD/2", "OPS", 30, 10))) + + result, _ := va.result() + require.Len(t, result.GetVolumes(), 1, "collapse_colors must produce a single per-asset entry") + + vol := result.GetVolumes()[0] + require.Equal(t, "USD/2", vol.GetAsset()) + require.Equal(t, "", vol.GetColor(), "collapsed entries are produced under the empty color") + + var input, output uint256.Int + vol.GetInput().IntoUint256(&input) + vol.GetOutput().IntoUint256(&output) + require.Equal(t, uint256.NewInt(330), &input, "100+200+30") + require.Equal(t, uint256.NewInt(140), &output, "50+80+10") +} + +func TestVolumeAggregator_CollapseColors_WithMaxPrecision(t *testing.T) { + t.Parallel() + + va := newVolumeAggregator(true, true) + + // USD/2 RED: 100 in, 50 out → rescaled to /4: 10000, 5000 + require.NoError(t, va.accumulate(makeColoredEntry("test", "a", "USD/2", "RED", 100, 50))) + // USD/4 BLUE: 1, 0 + require.NoError(t, va.accumulate(makeColoredEntry("test", "a", "USD/4", "BLUE", 1, 0))) + // USD/4 (uncolored): 2, 1 + require.NoError(t, va.accumulate(makeColoredEntry("test", "a", "USD/4", "", 2, 1))) + + result, _ := va.result() + require.Len(t, result.GetVolumes(), 1, "collapse_colors + max_precision must collapse all to one") + + vol := result.GetVolumes()[0] + require.Equal(t, "USD/4", vol.GetAsset()) + require.Empty(t, vol.GetColor()) + + var input, output uint256.Int + vol.GetInput().IntoUint256(&input) + vol.GetOutput().IntoUint256(&output) + require.Equal(t, uint256.NewInt(10003), &input, "10000 + 1 + 2") + require.Equal(t, uint256.NewInt(5001), &output, "5000 + 0 + 1") +} + +func TestGroupedAggregator_RespectsColor(t *testing.T) { + t.Parallel() + + ga := newGroupedAggregator(AggregateOptions{ + GroupByPrefixes: []string{"users:"}, + }) + + require.NoError(t, ga.accumulate(makeColoredEntry("test", "users:alice", "USD/2", "RED", 100, 0))) + require.NoError(t, ga.accumulate(makeColoredEntry("test", "users:alice", "USD/2", "BLUE", 50, 0))) + + result, _ := ga.result() + require.Len(t, result.GetGroups(), 1) + require.Len(t, result.GetGroups()[0].GetVolumes(), 2, + "grouped aggregator must keep colors segregated within a prefix bucket") +} diff --git a/internal/query/compact_account.go b/internal/query/compact_account.go index 81852669ae..deb0cdddce 100644 --- a/internal/query/compact_account.go +++ b/internal/query/compact_account.go @@ -32,12 +32,18 @@ type compactSubIter struct { // Current account state — populated by advance(). addr []byte - assets []string // only for V type + assets []string // only for V type — deduped asset names (color-collapsed) + seenAssets map[string]struct{} metadataKeys []string // only for M type lastCanonical []byte // for dedup valid bool started bool + + // err is set when the iterator encounters a malformed key (a "should not + // happen" branch per CLAUDE.md invariant #7) so the outer Next() can + // surface the violation instead of silently dropping the entry. + err error } // NewCompactAccountIterator creates an iterator that yields CompactAccount @@ -89,6 +95,9 @@ func (si *compactSubIter) advance() bool { si.metadataKeys = si.metadataKeys[:0] si.lastCanonical = si.lastCanonical[:0] si.addr = nil + if si.seenAssets != nil { + clear(si.seenAssets) + } if !si.started { si.started = true @@ -138,8 +147,40 @@ func (si *compactSubIter) advance() bool { switch si.attrType { case dal.SubAttrVolume: - if len(nameBytes) >= 2 { - si.assets = append(si.assets, domain.FormatAsset(string(nameBytes[:len(nameBytes)-1]), nameBytes[len(nameBytes)-1])) + // Volume key layout after the account separator is + // [color]\x00[asset_base][precision_byte] + // We surface deduped asset names (color is collapsed at the + // analysis layer — pattern discovery does not care about + // color), so split on the second 0x00 to skip the color. + colorSep := bytes.IndexByte(nameBytes, dal.CanonicalKeySepVolume) + if colorSep < 0 { + // "Should not happen": every volume key written by the + // FSM carries the color separator (CLAUDE.md invariant + // #7). Surface the violation loudly instead of silently + // skipping the entry — a missing separator means either + // pre-color legacy data or storage corruption. + si.err = fmt.Errorf("compact_account: volume key missing color separator (canonical=%x)", canonical) + si.valid = false + + return false + } + assetBytes := nameBytes[colorSep+1:] + if len(assetBytes) < 2 { + // Same class of invariant: asset_base must be at least + // one byte plus the trailing precision byte. Anything + // shorter is corrupt. + si.err = fmt.Errorf("compact_account: volume key asset section too short (canonical=%x)", canonical) + si.valid = false + + return false + } + asset := domain.FormatAsset(string(assetBytes[:len(assetBytes)-1]), assetBytes[len(assetBytes)-1]) + if si.seenAssets == nil { + si.seenAssets = make(map[string]struct{}) + } + if _, ok := si.seenAssets[asset]; !ok { + si.seenAssets[asset] = struct{}{} + si.assets = append(si.assets, asset) } case dal.SubAttrMetadata: si.metadataKeys = append(si.metadataKeys, string(nameBytes)) @@ -169,6 +210,20 @@ func (it *CompactAccountIterator) Next() (analysis.CompactAccount, error) { it.m.advance() } + // An invariant violation surfaced by advance() must not be silently + // dropped: the upstream key layout is wrong and the consumer needs to + // know rather than silently miss volume entries. + if it.v.err != nil { + it.done = true + + return analysis.CompactAccount{}, it.v.err + } + if it.m.err != nil { + it.done = true + + return analysis.CompactAccount{}, it.m.err + } + if !it.v.valid && !it.m.valid { it.done = true diff --git a/misc/proto/bucket.proto b/misc/proto/bucket.proto index d766c8aac2..085ef87111 100644 --- a/misc/proto/bucket.proto +++ b/misc/proto/bucket.proto @@ -99,6 +99,10 @@ message GetAccountRequest { string address = 2; // checkpoint_id, when non-zero, reads from a query checkpoint instead of the live store fixed64 checkpoint_id = 3; + // When true, colored buckets are summed into a single entry per asset + // with color = "" in the returned Account.volumes list. By default each + // (asset, color) tuple gets its own entry. + bool collapse_colors = 4; } message GetTransactionRequest { @@ -919,6 +923,11 @@ message NormalizedPosting { string source_pattern = 1; // e.g. "users:{id}:main" string destination_pattern = 2; // e.g. "bank:fees" string asset = 3; + // color discriminates postings on the same (source,destination,asset) but + // different color buckets in the FSM. Two flows that share addresses and + // asset but differ in color must produce distinct flow signatures so the + // analysis endpoint does not silently merge segregated buckets. + string color = 4; } enum PostingStructure { @@ -1083,6 +1092,10 @@ message AggregateVolumesRequest { repeated string group_by_prefixes = 5; // checkpoint_id, when non-zero, reads from a query checkpoint instead of the live store fixed64 checkpoint_id = 6; + // When true, amounts in colored buckets are summed into the uncolored bucket + // ("" color). Result entries are produced with color = "". By default each + // (asset, color) bucket yields its own AggregatedVolume entry. + bool collapse_colors = 7; } // ============================================================================ diff --git a/misc/proto/common.proto b/misc/proto/common.proto index 6c809acb46..5cbd9cd863 100644 --- a/misc/proto/common.proto +++ b/misc/proto/common.proto @@ -61,6 +61,10 @@ message Posting { string destination = 2; Uint256 amount = 3; string asset = 4; + // Color of the funds being moved. The empty string is the "uncolored" bucket + // and is treated as just another color from a segregation standpoint. + // Color values are constrained to ^[A-Z]*$ at admission time. + string color = 5; } // Transaction represents a transaction @@ -99,12 +103,23 @@ message VolumesWithBalance { string balance = 3; // big.Int as string } -// VolumesByAssets represents volumes grouped by asset +// VolumesByAssets is a sorted list of post-commit (asset, color) volume +// entries for a single account. Sorted by (asset, color) ascending so the +// serialization is deterministic and stable across reads. message VolumesByAssets { - map volumes = 1; + repeated VolumeEntry volumes = 1; } -// PostCommitVolumes represents volumes after commit, grouped by account and asset +// VolumeEntry is one (asset, color) row inside VolumesByAssets. The empty +// color is the uncolored bucket and is itself just another segregated bucket. +message VolumeEntry { + string asset = 1; + string color = 2; + Volumes volumes = 3; +} + +// PostCommitVolumes represents volumes after commit, grouped by account. +// Within each account, entries are flat-listed per (asset, color) tuple. message PostCommitVolumes { map volumes_by_account = 1; } @@ -113,14 +128,25 @@ message PostCommitVolumes { // Account // ============================================================================ -// Account represents an account in the ledger +// AccountVolume is one (asset, color) row in Account.volumes. +message AccountVolume { + string asset = 1; + string color = 2; + VolumesWithBalance volumes = 3; +} + +// Account represents an account in the ledger. message Account { string address = 1; map metadata = 2; Timestamp first_usage = 3; Timestamp insertion_date = 4; Timestamp updated_at = 5; - map volumes = 6; // Volumes per asset + // Volumes is a sorted list of per (asset, color) entries. + // Sorted by (asset, color) ascending for stable serialization. + // When the request opts into collapse_colors, the list collapses to one + // entry per asset with color = "" and amounts summed across colors. + repeated AccountVolume volumes = 6; } // ============================================================================ @@ -636,11 +662,14 @@ message LedgerLog { } // TouchedVolume identifies a (ledger-local) volume cell — an account paired -// with an asset. Used in transient/purged volume exclusion sets where the -// indexer must distinguish per-asset state inside a multi-asset account. +// with an asset and a color. Used in transient/purged volume exclusion sets +// where the indexer must distinguish per-(asset, color) state inside a +// multi-bucket account. The empty color is the uncolored bucket and is itself +// just another segregated cell. message TouchedVolume { string account = 1; string asset = 2; + string color = 3; } message LedgerLogPayload { @@ -949,6 +978,8 @@ enum ErrorReason { ERROR_REASON_CLUSTER_UNHEALTHY = 60; ERROR_REASON_WRITES_BLOCKED_DISK_FULL = 61; ERROR_REASON_WRITES_BLOCKED_CLOCK_SKEW = 62; + ERROR_REASON_AGGREGATE_OVERFLOW = 63; + ERROR_REASON_BALANCE_NOT_FOUND = 64; } // IdempotencyFailure captures a definitive business rejection so a retried key @@ -1187,11 +1218,15 @@ message PreparedQuery { QueryTarget target = 3; } -// AggregatedVolume represents per-asset aggregated input/output volumes. +// AggregatedVolume represents aggregated input/output volumes for a single +// (asset, color) bucket. When collapse_colors is requested on the aggregate +// query, all entries are produced with color = "" and amounts summed across +// colors. message AggregatedVolume { string asset = 1; Uint256 input = 2; Uint256 output = 3; + string color = 4; } message AggregateResult { diff --git a/openapi.yml b/openapi.yml index 7644ebed4c..0a57d17537 100644 --- a/openapi.yml +++ b/openapi.yml @@ -586,6 +586,16 @@ paths: parameters: - $ref: '#/components/parameters/LedgerName' - $ref: '#/components/parameters/AccountAddress' + - name: collapseColors + in: query + required: false + schema: + type: boolean + default: false + description: | + When true, every colored bucket of the same asset is summed + into a single AccountVolume entry with color = "". By default + each (asset, color) tuple is returned as its own entry. responses: '200': description: Account retrieved successfully @@ -639,6 +649,15 @@ paths: description: Comma-separated list of account prefixes to group volumes by schema: type: string + - name: collapseColors + in: query + description: | + When true, every colored bucket of the same (asset, precision) is + summed and the resulting AggregatedVolume entry carries color = "". + By default the response is segregated per (asset, color). + schema: + type: boolean + default: false responses: '200': description: Aggregated volumes retrieved successfully @@ -2299,6 +2318,15 @@ components: asset: type: string description: Asset identifier + color: + type: string + pattern: '^[A-Z]*$' + description: | + Optional segregation key for "color of money". The empty string + (or omitted field) means the uncolored bucket. Two postings on the + same (account, asset) but different colors operate on strictly + isolated balances. Color is constrained to `^[A-Z]*$` and is + immutable once carried by funds. Script: type: object @@ -2574,10 +2602,33 @@ components: $ref: '#/components/schemas/MetadataValue' description: Account metadata (typed values) volumes: - type: object - additionalProperties: - $ref: '#/components/schemas/VolumesWithBalance' - description: Account volumes per asset (input, output, balance) + type: array + items: + $ref: '#/components/schemas/AccountVolume' + description: | + Account volumes listed per (asset, color). Sorted by (asset, color) + ascending for stable serialization. The empty color is the + uncolored bucket; colored buckets are strictly isolated. Use the + `collapseColors=true` query parameter on GET /accounts to sum + every colored bucket of the same asset into a single uncolored + entry. + + AccountVolume: + type: object + required: + - asset + - color + - volumes + properties: + asset: + type: string + description: Asset identifier (e.g. "USD/2") + color: + type: string + pattern: '^[A-Z]*$' + description: Color of the bucket. Empty string is the uncolored bucket. + volumes: + $ref: '#/components/schemas/VolumesWithBalance' VolumesWithBalance: type: object @@ -2631,6 +2682,15 @@ components: asset: type: string description: Asset identifier (e.g. "USD/2") + color: + type: string + pattern: '^[A-Z]*$' + description: | + Color of the aggregated bucket. By default the response contains + one AggregatedVolume per (asset, color) tuple. With + `collapseColors=true`, every colored bucket of the same + (asset, precision) is summed and the resulting entry carries + color = "". input: type: string description: Total amount received (as big integer string) @@ -3101,11 +3161,39 @@ components: PostCommitVolumes: type: object - additionalProperties: - type: object - additionalProperties: + description: Post-commit volumes grouped by account address. + properties: + volumesByAccount: + type: object + description: Map from account address to its per-(asset, color) volume entries. + additionalProperties: + $ref: '#/components/schemas/VolumesByAssets' + + VolumesByAssets: + type: object + description: List of post-commit volume entries for a single account, one per (asset, color) bucket. + properties: + volumes: + type: array + items: + $ref: '#/components/schemas/VolumeEntry' + + VolumeEntry: + type: object + description: A single (asset, color) bucket inside a VolumesByAssets list. + required: + - asset + - color + - volumes + properties: + asset: + type: string + color: + type: string + pattern: '^[A-Z]*$' + description: Empty string for the uncolored bucket; always emitted. + volumes: $ref: '#/components/schemas/Volumes' - description: Post-commit volumes grouped by account address, then by asset Volumes: type: object @@ -3361,6 +3449,14 @@ components: type: string asset: type: string + color: + type: string + pattern: '^[A-Z]*$' + description: | + Color discriminates postings on the same (source, destination, asset) + but different color buckets in the FSM. Always emitted (even empty) + so clients can distinguish the uncolored bucket from patterns that + differ only by color. TemporalStats: type: object diff --git a/pkg/actions/actions.go b/pkg/actions/actions.go index 58df454b78..4087be5497 100644 --- a/pkg/actions/actions.go +++ b/pkg/actions/actions.go @@ -323,11 +323,16 @@ func WithExpandVolumes(req *servicepb.Request) *servicepb.Request { return req } -// NewPosting creates a new posting protobuf message. +// NewPosting creates a new uncolored posting protobuf message. func NewPosting(source, destination string, amount *big.Int, asset string) *commonpb.Posting { return commonpb.NewPosting(source, destination, asset, amount) } +// NewColoredPosting creates a new posting with an explicit color. +func NewColoredPosting(source, destination string, amount *big.Int, asset, color string) *commonpb.Posting { + return commonpb.NewColoredPosting(source, destination, asset, color, amount) +} + // RegisterSigningKeyAction creates a RegisterSigningKey request. func RegisterSigningKeyAction(keyID string, pubKey ed25519.PublicKey) *servicepb.Request { return &servicepb.Request{ diff --git a/pkg/scenario/block.go b/pkg/scenario/block.go index eadf1be646..fe7d76cd70 100644 --- a/pkg/scenario/block.go +++ b/pkg/scenario/block.go @@ -60,9 +60,16 @@ func BlockScenario(name string) string { // Helper functions for blocks to read ledger state. -// GetAccountBalance reads the balance of an account for a given asset. -// Returns (balance, true) if available, or (zero, false) otherwise. +// GetAccountBalance reads the uncolored balance of an account for a given asset. +// Returns (balance, true) if available, or (zero, false) otherwise. For a +// colored balance, see GetColoredAccountBalance. func GetAccountBalance(ctx context.Context, client servicepb.BucketServiceClient, ledger, address, asset string) (*big.Int, bool) { + return GetColoredAccountBalance(ctx, client, ledger, address, asset, "") +} + +// GetColoredAccountBalance reads the balance of an account for a given +// (asset, color) bucket. Color "" is the uncolored bucket. +func GetColoredAccountBalance(ctx context.Context, client servicepb.BucketServiceClient, ledger, address, asset, color string) (*big.Int, bool) { acct, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{ Ledger: ledger, Address: address, @@ -70,16 +77,19 @@ func GetAccountBalance(ctx context.Context, client servicepb.BucketServiceClient if err != nil { return big.NewInt(0), false } - vol, ok := acct.GetVolumes()[asset] - if !ok { - return big.NewInt(0), false - } - balance, ok := new(big.Int).SetString(vol.GetBalance(), 10) - if !ok { - return big.NewInt(0), false + for _, entry := range acct.GetVolumes() { + if entry.GetAsset() != asset || entry.GetColor() != color { + continue + } + balance, ok := new(big.Int).SetString(entry.GetVolumes().GetBalance(), 10) + if !ok { + return big.NewInt(0), false + } + + return balance, true } - return balance, true + return big.NewInt(0), false } // GetNonRevertedTransaction finds a random non-reverted transaction in a ledger. diff --git a/tests/antithesis/workload/bin/cmds/main/eventually_correct/main.go b/tests/antithesis/workload/bin/cmds/main/eventually_correct/main.go index be906971e5..783c32de36 100644 --- a/tests/antithesis/workload/bin/cmds/main/eventually_correct/main.go +++ b/tests/antithesis/workload/bin/cmds/main/eventually_correct/main.go @@ -135,14 +135,17 @@ func checkBalanced(ctx context.Context, client servicepb.BucketServiceClient, le }) } - aggregated := make(map[string]*big.Int) + // Double-entry holds per (asset, color) bucket. + type aggKey struct{ asset, color string } + aggregated := make(map[aggKey]*big.Int) for _, account := range accounts { - for asset, vol := range account.Volumes { - if aggregated[asset] == nil { - aggregated[asset] = big.NewInt(0) + for _, entry := range account.GetVolumes() { + k := aggKey{asset: entry.GetAsset(), color: entry.GetColor()} + if aggregated[k] == nil { + aggregated[k] = big.NewInt(0) } - aggregated[asset].Add(aggregated[asset], parseBalance(vol.GetBalance())) + aggregated[k].Add(aggregated[k], parseBalance(entry.GetVolumes().GetBalance())) } } @@ -218,7 +221,10 @@ func checkVolumesConsistent(ctx context.Context, client servicepb.BucketServiceC checked := false for _, account := range accounts { - for asset, vol := range account.Volumes { + for _, entry := range account.GetVolumes() { + asset := entry.GetAsset() + color := entry.GetColor() + vol := entry.GetVolumes() input := parseBalance(vol.GetInput()) output := parseBalance(vol.GetOutput()) balance := parseBalance(vol.GetBalance()) @@ -226,6 +232,7 @@ func checkVolumesConsistent(ctx context.Context, client servicepb.BucketServiceC internal.CheckVolume(input, output, balance, details.With(internal.Details{ "account": account.Address, "asset": asset, + "color": color, })) getAcc, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{ @@ -243,11 +250,13 @@ func checkVolumesConsistent(ctx context.Context, client servicepb.BucketServiceC continue } - actualVol, ok := getAcc.Volumes[asset] - if !ok { + // Cross-check the same (asset, color) bucket as the list result. + actualVol := getAcc.FindVolume(asset, color) + if actualVol == nil { assert.Unreachable("should get requested volumes", details.With(internal.Details{ "account": account.Address, "asset": asset, + "color": color, })) continue diff --git a/tests/antithesis/workload/bin/cmds/main/eventually_cross_node_identity/main.go b/tests/antithesis/workload/bin/cmds/main/eventually_cross_node_identity/main.go index 1c48f2cc9c..cd75a2ab48 100644 --- a/tests/antithesis/workload/bin/cmds/main/eventually_cross_node_identity/main.go +++ b/tests/antithesis/workload/bin/cmds/main/eventually_cross_node_identity/main.go @@ -357,8 +357,8 @@ func volumesString(acc *commonpb.Account) string { } var out strings.Builder - for asset, v := range acc.GetVolumes() { - fmt.Fprintf(&out, "%s=%s ", asset, v.GetBalance()) + for _, v := range acc.GetVolumes() { + fmt.Fprintf(&out, "%s/%s=%s ", v.GetAsset(), v.GetColor(), v.GetVolumes().GetBalance()) } return out.String() diff --git a/tests/antithesis/workload/bin/cmds/main/parallel_driver_stale_reads/main.go b/tests/antithesis/workload/bin/cmds/main/parallel_driver_stale_reads/main.go index 4dfbec0bb8..4eba18d86c 100644 --- a/tests/antithesis/workload/bin/cmds/main/parallel_driver_stale_reads/main.go +++ b/tests/antithesis/workload/bin/cmds/main/parallel_driver_stale_reads/main.go @@ -136,7 +136,7 @@ func main() { "acked": acked, } - vol := account.GetVolumes()[probeAsset] + vol := account.FindVolume(probeAsset, "") if vol == nil { // Valid prefix: account materialized, no write applied yet. continue diff --git a/tests/antithesis/workload/go.mod b/tests/antithesis/workload/go.mod index 78694ce9e8..e3ec2d1650 100644 --- a/tests/antithesis/workload/go.mod +++ b/tests/antithesis/workload/go.mod @@ -61,7 +61,7 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/getsentry/sentry-go v0.35.1 // indirect + github.com/getsentry/sentry-go v0.43.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -112,7 +112,7 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect + golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect golang.org/x/oauth2 v0.35.0 // indirect golang.org/x/sys v0.45.0 // indirect golang.org/x/term v0.43.0 // indirect diff --git a/tests/antithesis/workload/go.sum b/tests/antithesis/workload/go.sum index ff15df4fb0..8503ad24cc 100644 --- a/tests/antithesis/workload/go.sum +++ b/tests/antithesis/workload/go.sum @@ -99,12 +99,12 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/formancehq/go-libs/v5 v5.3.0 h1:1+rXYjCNGZG0tQayESllEassrejzI/vc4Gz3p568tnU= github.com/formancehq/go-libs/v5 v5.3.0/go.mod h1:ms6tCGw1yqB4qtEbAuqPOQegWo4rU48vDobNkK7Ak6U= -github.com/formancehq/numscript v0.0.24 h1:YBiDZ9zLVxTZVhtQ+taRcb6q2jArAvznWMfoWRVYGT0= -github.com/formancehq/numscript v0.0.24/go.mod h1:hC/VY5Vg04F5QkgdPPc6z/YsS/vh8V1qVJVa1VWnYMA= +github.com/formancehq/numscript v0.0.25-0.20260603094112-f4fae4573bc5 h1:AEjUhUAS3Ly40NaXGI1PTLtTFO7odqDC8GaVjOCYc+I= +github.com/formancehq/numscript v0.0.25-0.20260603094112-f4fae4573bc5/go.mod h1:vQZOSwJItpYeCwUYNtV7bwu9naQoh2pdkkqs0ewkTiI= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/getsentry/sentry-go v0.35.1 h1:iopow6UVLE2aXu46xKVIs8Z9D/YZkJrHkgozrxa+tOQ= -github.com/getsentry/sentry-go v0.35.1/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= +github.com/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4= +github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0= github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9 h1:r5GgOLGbza2wVHRzK7aAj6lWZjfbAwiu/RDCVOKjRyM= github.com/ghemawat/stream v0.0.0-20171120220530-696b145b53b9/go.mod h1:106OIgooyS7OzLDOpUGgm9fA3bQENb/cFSyyBmMoJDs= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= @@ -285,8 +285,8 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= -golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= +golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= diff --git a/tests/antithesis/workload/internal/checks.go b/tests/antithesis/workload/internal/checks.go index 1ac36547aa..c076db6e15 100644 --- a/tests/antithesis/workload/internal/checks.go +++ b/tests/antithesis/workload/internal/checks.go @@ -19,9 +19,11 @@ func CheckVolume(input, output, balance *big.Int, details Details) { })) } -// CheckAccountVolumes verifies volume consistency for all assets of an account. -func CheckAccountVolumes(volumes map[string]*commonpb.VolumesWithBalance, details Details) { - for asset, vol := range volumes { +// CheckAccountVolumes verifies volume consistency for every (asset, color) +// bucket on an account. +func CheckAccountVolumes(volumes []*commonpb.AccountVolume, details Details) { + for _, entry := range volumes { + vol := entry.GetVolumes() input, _ := new(big.Int).SetString(vol.GetInput(), 10) output, _ := new(big.Int).SetString(vol.GetOutput(), 10) balance, _ := new(big.Int).SetString(vol.GetBalance(), 10) @@ -35,18 +37,21 @@ func CheckAccountVolumes(volumes map[string]*commonpb.VolumesWithBalance, detail balance = big.NewInt(0) } CheckVolume(input, output, balance, details.With(Details{ - "asset": asset, + "asset": entry.GetAsset(), + "color": entry.GetColor(), })) } } // CheckPostCommitVolumes verifies volume consistency for post-commit volumes from a transaction response. +// Each (asset, color) bucket is verified independently. func CheckPostCommitVolumes(pcv *commonpb.PostCommitVolumes, details Details) { if pcv == nil { return } for account, volumesByAssets := range pcv.GetVolumesByAccount() { - for asset, vol := range volumesByAssets.GetVolumes() { + for _, entry := range volumesByAssets.GetVolumes() { + vol := entry.GetVolumes() input, _ := new(big.Int).SetString(vol.GetInput(), 10) output, _ := new(big.Int).SetString(vol.GetOutput(), 10) if input == nil { @@ -58,7 +63,8 @@ func CheckPostCommitVolumes(pcv *commonpb.PostCommitVolumes, details Details) { balance := new(big.Int).Sub(input, output) CheckVolume(input, output, balance, details.With(Details{ "account": account, - "asset": asset, + "asset": entry.GetAsset(), + "color": entry.GetColor(), })) } } diff --git a/tests/e2e/business/barrier_test.go b/tests/e2e/business/barrier_test.go index 02392d7f91..190a207b1f 100644 --- a/tests/e2e/business/barrier_test.go +++ b/tests/e2e/business/barrier_test.go @@ -43,7 +43,8 @@ var _ = Describe("Barrier", Ordered, func() { }) Expect(err).To(Succeed()) Expect(account).NotTo(BeNil()) - Expect(account.Volumes).To(HaveKey("USD")) - Expect(account.Volumes["USD"].GetBalance()).To(Equal("500")) + usdVol := account.FindVolume("USD", "") + Expect(usdVol).NotTo(BeNil(), "expected USD volumes on barrier-test-account") + Expect(usdVol.GetBalance()).To(Equal("500")) }) }) diff --git a/tests/e2e/business/color_numscript_test.go b/tests/e2e/business/color_numscript_test.go new file mode 100644 index 0000000000..150511a3d9 --- /dev/null +++ b/tests/e2e/business/color_numscript_test.go @@ -0,0 +1,340 @@ +//go:build e2e + +package business + +import ( + "math/big" + "time" + + "github.com/formancehq/ledger/v3/internal/proto/commonpb" + "github.com/formancehq/ledger/v3/internal/proto/servicepb" + "github.com/formancehq/ledger/v3/pkg/actions" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Color × Numscript: the color dimension flows through the Numscript adapter +// the same way it flows through native postings. A script restricting its +// source to a color must consume from exactly that bucket, and the resulting +// posting must carry the color all the way to the FSM and back through the +// read side. +var _ = Describe("ColorNumscript", Ordered, func() { + const ledgerName = "color-numscript" + + BeforeAll(func() { + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes(actions.CreateLedgerAction(ledgerName, nil)), + }) + Expect(err).To(Succeed()) + + // Seed alice with three segregated buckets on USD/2: + // uncolored "" : 300 + // GRANTS : 200 + // OPS : 100 + _, err = sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(300), "USD/2", ""), + actions.NewColoredPosting("world", "alice", big.NewInt(200), "USD/2", "GRANTS"), + actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", "OPS"), + }, nil, nil), + ), + }) + Expect(err).To(Succeed()) + }) + + It("Should restrict the numscript source to a single color bucket", func() { + // Draw 60 from alice's GRANTS bucket via Numscript. The resulting + // posting must carry color = "GRANTS", and only the GRANTS bucket + // must move — uncolored and OPS must be untouched. + script := ` +#![feature("experimental-asset-colors")] + +send [USD/2 60] ( + source = @alice \ "GRANTS" + destination = @bob +) +` + resp, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateScriptTransactionAction(ledgerName, script, nil, nil), + ), + }) + Expect(err).To(Succeed()) + Expect(resp.Logs).To(HaveLen(1)) + + createdTx := resp.Logs[0].Payload.GetApply().Log.Data.GetCreatedTransaction() + Expect(createdTx).NotTo(BeNil()) + Expect(createdTx.Transaction.Postings).To(HaveLen(1)) + Expect(createdTx.Transaction.Postings[0].GetSource()).To(Equal("alice")) + Expect(createdTx.Transaction.Postings[0].GetDestination()).To(Equal("bob")) + Expect(createdTx.Transaction.Postings[0].GetAsset()).To(Equal("USD/2")) + Expect(createdTx.Transaction.Postings[0].GetColor()).To(Equal("GRANTS"), + "the color restriction on the source must propagate to the emitted posting") + Expect(createdTx.Transaction.Postings[0].Amount.ToBigInt().Int64()).To(Equal(int64(60))) + + Eventually(func(g Gomega) { + alice, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "alice", + }) + g.Expect(err).To(Succeed()) + + // GRANTS drained by 60 (200 - 60 = 140); others unchanged. + g.Expect(alice.FindVolume("USD/2", "GRANTS").GetBalance()).To(Equal("140")) + g.Expect(alice.FindVolume("USD/2", "").GetBalance()).To(Equal("300")) + g.Expect(alice.FindVolume("USD/2", "OPS").GetBalance()).To(Equal("100")) + + // bob received under the same color and only under that color. + bob, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "bob", + }) + g.Expect(err).To(Succeed()) + g.Expect(bob.FindVolume("USD/2", "GRANTS").GetBalance()).To(Equal("60")) + g.Expect(bob.FindVolume("USD/2", "")).To(BeNil(), + "the color stays with the funds — bob must not have an uncolored bucket") + }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) + }) + + It("Should refuse a numscript draw when the colored bucket is empty", func() { + // MISSING is a color alice never received; the script must fail + // rather than fall back to other buckets. + script := ` +#![feature("experimental-asset-colors")] + +send [USD/2 1] ( + source = @alice \ "MISSING" + destination = @bob +) +` + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateScriptTransactionAction(ledgerName, script, nil, nil), + ), + }) + Expect(err).To(HaveOccurred(), + "a colored draw must not be satisfied from uncolored funds even via numscript") + }) + + It("Should treat the uncolored bucket as its own color in numscript", func() { + // Drawing from @alice without a restrict must consume the + // uncolored bucket only — the colored buckets stay segregated + // from the default scope. + script := ` +send [USD/2 90] ( + source = @alice + destination = @bob +) +` + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateScriptTransactionAction(ledgerName, script, nil, nil), + ), + }) + Expect(err).To(Succeed()) + + Eventually(func(g Gomega) { + alice, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "alice", + }) + g.Expect(err).To(Succeed()) + + // Uncolored shrunk by 90 (300 - 90 = 210); colored stays intact. + g.Expect(alice.FindVolume("USD/2", "").GetBalance()).To(Equal("210")) + g.Expect(alice.FindVolume("USD/2", "GRANTS").GetBalance()).To(Equal("140")) + g.Expect(alice.FindVolume("USD/2", "OPS").GetBalance()).To(Equal("100")) + }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) + }) + + It("Should expose every (asset, color) bucket when GetAccount is called without collapse", func() { + acct, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "alice", + }) + Expect(err).To(Succeed()) + + // Volumes list is sorted (asset, color); three buckets for alice. + vols := acct.GetVolumes() + Expect(vols).To(HaveLen(3)) + Expect(vols[0].GetAsset()).To(Equal("USD/2")) + Expect(vols[0].GetColor()).To(Equal("")) + Expect(vols[0].GetVolumes().GetBalance()).To(Equal("210")) + Expect(vols[1].GetColor()).To(Equal("GRANTS")) + Expect(vols[1].GetVolumes().GetBalance()).To(Equal("140")) + Expect(vols[2].GetColor()).To(Equal("OPS")) + Expect(vols[2].GetVolumes().GetBalance()).To(Equal("100")) + }) + + It("Should collapse colors on GetAccount when collapseColors=true", func() { + acct, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "alice", + CollapseColors: true, + }) + Expect(err).To(Succeed()) + + // All three buckets summed under color = "" (210 + 140 + 100 = 450) + Expect(acct.GetVolumes()).To(HaveLen(1)) + entry := acct.GetVolumes()[0] + Expect(entry.GetAsset()).To(Equal("USD/2")) + Expect(entry.GetColor()).To(Equal("")) + Expect(entry.GetVolumes().GetBalance()).To(Equal("450")) + }) +}) + +// AggregateVolumes × color: the aggregate endpoint preserves the color +// dimension by default, and collapses it to a single per-asset entry when +// asked. These tests pin both shapes to lock the contract. +var _ = Describe("AggregateVolumesColor", Ordered, func() { + const ledgerName = "agg-vol-color" + + BeforeAll(func() { + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes(actions.CreateLedgerAction(ledgerName, nil)), + }) + Expect(err).To(Succeed()) + + // Fund three (asset, color) buckets on alice. We mix assets and + // colors so the aggregator must handle both axes: + // alice / USD/2 / "" : 10 + // alice / USD/2 / "GRANTS" : 100 + // alice / USD/2 / "OPS" : 40 + // alice / EUR/2 / "GRANTS" : 50 + _, err = sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(10), "USD/2", ""), + actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", "GRANTS"), + actions.NewColoredPosting("world", "alice", big.NewInt(40), "USD/2", "OPS"), + actions.NewColoredPosting("world", "alice", big.NewInt(50), "EUR/2", "GRANTS"), + }, nil, nil), + ), + }) + Expect(err).To(Succeed()) + }) + + It("Should return one entry per (asset, color) by default", func() { + Eventually(func(g Gomega) { + result, err := sharedClient.AggregateVolumes(sharedCtx, &servicepb.AggregateVolumesRequest{ + Ledger: ledgerName, + }) + g.Expect(err).To(Succeed()) + // 3 USD/2 buckets + 1 EUR/2 bucket = 4 entries. + g.Expect(result.Volumes).To(HaveLen(4)) + + type key struct{ asset, color string } + byKey := make(map[key]*commonpb.AggregatedVolume, len(result.Volumes)) + for _, v := range result.Volumes { + byKey[key{v.GetAsset(), v.GetColor()}] = v + } + + usdUncolored := byKey[key{"USD/2", ""}] + g.Expect(usdUncolored).NotTo(BeNil()) + g.Expect(usdUncolored.Input.ToBigInt().Int64()).To(Equal(int64(10))) + g.Expect(usdUncolored.Output.ToBigInt().Int64()).To(Equal(int64(10)), + "world's output for USD/2 uncolored must match alice's input") + + usdGrants := byKey[key{"USD/2", "GRANTS"}] + g.Expect(usdGrants).NotTo(BeNil()) + g.Expect(usdGrants.Input.ToBigInt().Int64()).To(Equal(int64(100))) + + usdOps := byKey[key{"USD/2", "OPS"}] + g.Expect(usdOps).NotTo(BeNil()) + g.Expect(usdOps.Input.ToBigInt().Int64()).To(Equal(int64(40))) + + eurGrants := byKey[key{"EUR/2", "GRANTS"}] + g.Expect(eurGrants).NotTo(BeNil()) + g.Expect(eurGrants.Input.ToBigInt().Int64()).To(Equal(int64(50))) + }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) + }) + + It("Should collapse colors to one entry per asset when collapseColors=true", func() { + Eventually(func(g Gomega) { + result, err := sharedClient.AggregateVolumes(sharedCtx, &servicepb.AggregateVolumesRequest{ + Ledger: ledgerName, + CollapseColors: true, + }) + g.Expect(err).To(Succeed()) + // USD/2 (10+100+40 = 150) and EUR/2 (50), each under color = "". + g.Expect(result.Volumes).To(HaveLen(2)) + + byAsset := make(map[string]*commonpb.AggregatedVolume, len(result.Volumes)) + for _, v := range result.Volumes { + g.Expect(v.GetColor()).To(Equal(""), + "collapse must surface every aggregated entry under the empty color bucket") + byAsset[v.GetAsset()] = v + } + + usd := byAsset["USD/2"] + g.Expect(usd).NotTo(BeNil()) + g.Expect(usd.Input.ToBigInt().Int64()).To(Equal(int64(150))) + g.Expect(usd.Output.ToBigInt().Int64()).To(Equal(int64(150)), + "world's output must collapse the same way alice's input does") + + eur := byAsset["EUR/2"] + g.Expect(eur).NotTo(BeNil()) + g.Expect(eur.Input.ToBigInt().Int64()).To(Equal(int64(50))) + }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) + }) +}) + +// Reverting a colored transaction must return the funds to the same color +// bucket they came from — not to the uncolored default. Pins the contract +// that Color carries through the revert path. +var _ = Describe("ColorRevert", Ordered, func() { + const ledgerName = "color-revert" + + var revertTargetTxID uint64 + + BeforeAll(func() { + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes(actions.CreateLedgerAction(ledgerName, nil)), + }) + Expect(err).To(Succeed()) + + // world → alice 200 USD/2 color=GRANTS, world → alice 100 USD/2 uncolored. + _, err = sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", ""), + }, nil, nil), + ), + }) + Expect(err).To(Succeed()) + + // The colored transaction is the one we'll revert. + resp, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(200), "USD/2", "GRANTS"), + }, nil, nil), + ), + }) + Expect(err).To(Succeed()) + Expect(resp.Logs).To(HaveLen(1)) + createdTx := resp.Logs[0].Payload.GetApply().Log.Data.GetCreatedTransaction() + Expect(createdTx).NotTo(BeNil()) + revertTargetTxID = createdTx.Transaction.GetId() + }) + + It("Should drive the revert against the same color bucket", func() { + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes(actions.RevertTransactionAction(ledgerName, revertTargetTxID, false, false, nil)), + }) + Expect(err).To(Succeed()) + + Eventually(func(g Gomega) { + alice, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "alice", + }) + g.Expect(err).To(Succeed()) + + // GRANTS bucket back to 0 (200 - 200); uncolored untouched at 100. + g.Expect(alice.FindVolume("USD/2", "GRANTS").GetBalance()).To(Equal("0")) + g.Expect(alice.FindVolume("USD/2", "").GetBalance()).To(Equal("100")) + }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) + }) +}) diff --git a/tests/e2e/business/color_segregation_test.go b/tests/e2e/business/color_segregation_test.go new file mode 100644 index 0000000000..ab9c088b9e --- /dev/null +++ b/tests/e2e/business/color_segregation_test.go @@ -0,0 +1,174 @@ +//go:build e2e + +package business + +import ( + "math/big" + "time" + + "github.com/formancehq/ledger/v3/internal/proto/commonpb" + "github.com/formancehq/ledger/v3/internal/proto/servicepb" + "github.com/formancehq/ledger/v3/pkg/actions" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// Color segregation invariant: a posting on (account, asset, color) only ever +// touches that bucket. Funds in color=GRANTS cannot satisfy a draw from +// color=OPS, and the uncolored bucket is itself segregated from any colored +// bucket. This test drives the invariant end to end via gRPC. +var _ = Describe("ColorSegregation", Ordered, func() { + const ledgerName = "color-segregation" + + BeforeAll(func() { + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes(actions.CreateLedgerAction(ledgerName, nil)), + }) + Expect(err).To(Succeed()) + + // Seed alice with three segregated buckets on USD/2: + // uncolored "" : 100 + // GRANTS : 50 + // OPS : 25 + _, err = sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", ""), + actions.NewColoredPosting("world", "alice", big.NewInt(50), "USD/2", "GRANTS"), + actions.NewColoredPosting("world", "alice", big.NewInt(25), "USD/2", "OPS"), + }, nil, nil), + ), + }) + Expect(err).To(Succeed()) + }) + + It("Should expose every (asset, color) bucket on GetAccount", func() { + Eventually(func(g Gomega) { + acct, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "alice", + }) + g.Expect(err).To(Succeed()) + + uncolored := acct.FindVolume("USD/2", "") + grants := acct.FindVolume("USD/2", "GRANTS") + ops := acct.FindVolume("USD/2", "OPS") + + g.Expect(uncolored).NotTo(BeNil()) + g.Expect(grants).NotTo(BeNil()) + g.Expect(ops).NotTo(BeNil()) + + g.Expect(uncolored.GetBalance()).To(Equal("100")) + g.Expect(grants.GetBalance()).To(Equal("50")) + g.Expect(ops.GetBalance()).To(Equal("25")) + + // volumes list must be sorted deterministically by (asset, color) + vols := acct.GetVolumes() + g.Expect(vols).To(HaveLen(3)) + g.Expect(vols[0].GetColor()).To(Equal("")) + g.Expect(vols[1].GetColor()).To(Equal("GRANTS")) + g.Expect(vols[2].GetColor()).To(Equal("OPS")) + }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) + }) + + It("Should collapse colors into a single per-asset entry when requested", func() { + acct, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "alice", + CollapseColors: true, + }) + Expect(err).To(Succeed()) + + // All three buckets summed under color = "" + Expect(acct.GetVolumes()).To(HaveLen(1)) + entry := acct.GetVolumes()[0] + Expect(entry.GetAsset()).To(Equal("USD/2")) + Expect(entry.GetColor()).To(Equal("")) + Expect(entry.GetVolumes().GetBalance()).To(Equal("175")) // 100 + 50 + 25 + }) + + It("Should reject a draw from a color that has insufficient funds", func() { + // alice's OPS bucket has 25; ask for 100 OPS → MissingFunds. + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("alice", "bob", big.NewInt(100), "USD/2", "OPS"), + }, nil, nil), + ), + }) + Expect(err).NotTo(BeNil(), "expected color isolation to refuse spending more than the bucket holds") + }) + + It("Should refuse drawing colored funds from the uncolored bucket", func() { + // alice's uncolored bucket has 100; drawing 100 from "" should succeed. + // But drawing 100 from "GRANTS" must NOT dip into the 100 uncolored. + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("alice", "bob", big.NewInt(100), "USD/2", "GRANTS"), + }, nil, nil), + ), + }) + Expect(err).NotTo(BeNil(), "uncolored funds must not satisfy a GRANTS-colored draw") + }) + + It("Should drain a color independently of the others", func() { + // Spend exactly the GRANTS bucket (50) → success. + _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ + Envelopes: servicepb.UnsignedEnvelopes( + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("alice", "bob", big.NewInt(50), "USD/2", "GRANTS"), + }, nil, nil), + ), + }) + Expect(err).To(Succeed()) + + Eventually(func(g Gomega) { + alice, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "alice", + }) + g.Expect(err).To(Succeed()) + + // GRANTS is drained; other buckets are untouched. + g.Expect(alice.FindVolume("USD/2", "GRANTS").GetBalance()).To(Equal("0")) + g.Expect(alice.FindVolume("USD/2", "").GetBalance()).To(Equal("100")) + g.Expect(alice.FindVolume("USD/2", "OPS").GetBalance()).To(Equal("25")) + + // bob received under GRANTS, color preserved on the destination side. + bob, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ + Ledger: ledgerName, + Address: "bob", + }) + g.Expect(err).To(Succeed()) + g.Expect(bob.FindVolume("USD/2", "GRANTS").GetBalance()).To(Equal("50")) + g.Expect(bob.FindVolume("USD/2", "")).To(BeNil(), + "bob must not have an uncolored USD/2 bucket — color stays with the funds") + }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) + }) + + It("Should expose color on the emitted posting", func() { + // Re-fetch the GRANTS transfer we just executed and verify the + // posting still carries color = "GRANTS" on the wire. + resp, err := sharedClient.ListTransactions(sharedCtx, &servicepb.ListTransactionsRequest{ + Ledger: ledgerName, + Options: &commonpb.ListOptions{PageSize: 32}, + }) + Expect(err).To(Succeed()) + + var foundColored bool + for { + tx, recvErr := resp.Recv() + if recvErr != nil { + break + } + for _, p := range tx.GetPostings() { + if p.GetColor() == "GRANTS" { + foundColored = true + } + } + } + Expect(foundColored).To(BeTrue(), + "expected at least one persisted posting with color = GRANTS") + }) +}) diff --git a/tests/e2e/business/ephemeral_purge_test.go b/tests/e2e/business/ephemeral_purge_test.go index b263baf1c4..b039611bee 100644 --- a/tests/e2e/business/ephemeral_purge_test.go +++ b/tests/e2e/business/ephemeral_purge_test.go @@ -88,8 +88,8 @@ var _ = Describe("EphemeralPurge", Ordered, func() { }) g.Expect(err).To(Succeed()) - usdVol, ok := account.GetVolumes()["USD"] - g.Expect(ok).To(BeTrue(), "expected USD volumes on bank:main") + usdVol := account.FindVolume("USD", "") + g.Expect(usdVol).NotTo(BeNil(), "expected USD volumes on bank:main") g.Expect(usdVol.GetInput()).To(Equal("100")) g.Expect(usdVol.GetBalance()).To(Equal("100")) }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) @@ -110,8 +110,8 @@ var _ = Describe("EphemeralPurge", Ordered, func() { }) g.Expect(err).To(Succeed()) - usdVol, ok := account.GetVolumes()["USD"] - g.Expect(ok).To(BeTrue(), "expected USD volumes after reuse") + usdVol := account.FindVolume("USD", "") + g.Expect(usdVol).NotTo(BeNil(), "expected USD volumes after reuse") g.Expect(usdVol.GetInput()).To(Equal("50")) }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) }) @@ -143,8 +143,8 @@ var _ = Describe("EphemeralPurge", Ordered, func() { }) g.Expect(err).To(Succeed()) - usdVol, ok := account.GetVolumes()["USD"] - g.Expect(ok).To(BeTrue(), "expected USD volumes on non-ephemeral account") + usdVol := account.FindVolume("USD", "") + g.Expect(usdVol).NotTo(BeNil(), "expected USD volumes on non-ephemeral account") g.Expect(usdVol.GetInput()).To(Equal("100")) g.Expect(usdVol.GetOutput()).To(Equal("100")) g.Expect(usdVol.GetBalance()).To(Equal("0")) diff --git a/tests/e2e/business/force_test.go b/tests/e2e/business/force_test.go index b4f4261d58..52ed85e88d 100644 --- a/tests/e2e/business/force_test.go +++ b/tests/e2e/business/force_test.go @@ -35,7 +35,7 @@ var _ = Describe("Force Transactions", Ordered, func() { Address: "limited-account", }) Expect(err).To(Succeed()) - Expect(account.Volumes["USD"].Balance).To(Equal("100")) + Expect(account.FindVolume("USD", "").Balance).To(Equal("100")) // Try to send more than available without force - should fail _, err = sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ @@ -82,7 +82,7 @@ var _ = Describe("Force Transactions", Ordered, func() { Address: "empty-account", }) Expect(err).To(Succeed()) - Expect(sourceAccount.Volumes["USD"].Balance).To(Equal("-100")) + Expect(sourceAccount.FindVolume("USD", "").Balance).To(Equal("-100")) // The destination account should have positive balance destAccount, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ @@ -90,7 +90,7 @@ var _ = Describe("Force Transactions", Ordered, func() { Address: "zero-dest", }) Expect(err).To(Succeed()) - Expect(destAccount.Volumes["USD"].Balance).To(Equal("100")) + Expect(destAccount.FindVolume("USD", "").Balance).To(Equal("100")) }) It("Should create multiple postings with force=true", func() { @@ -119,7 +119,7 @@ var _ = Describe("Force Transactions", Ordered, func() { Address: tc.addr, }) Expect(err).To(Succeed()) - Expect(account.Volumes[tc.asset].Balance).To(Equal(tc.balance)) + Expect(account.FindVolume(tc.asset, "").GetBalance()).To(Equal(tc.balance)) } }) @@ -275,9 +275,9 @@ var _ = Describe("Force Transactions", Ordered, func() { Address: "empty-source", }) Expect(err).To(Succeed()) - Expect(source.Volumes["USD"].Input).To(Equal("0")) - Expect(source.Volumes["USD"].Output).To(Equal("500")) - Expect(source.Volumes["USD"].Balance).To(Equal("-500")) + Expect(source.FindVolume("USD", "").Input).To(Equal("0")) + Expect(source.FindVolume("USD", "").Output).To(Equal("500")) + Expect(source.FindVolume("USD", "").Balance).To(Equal("-500")) // Check target account has positive balance target, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ @@ -285,9 +285,9 @@ var _ = Describe("Force Transactions", Ordered, func() { Address: "target", }) Expect(err).To(Succeed()) - Expect(target.Volumes["USD"].Input).To(Equal("500")) - Expect(target.Volumes["USD"].Output).To(Equal("0")) - Expect(target.Volumes["USD"].Balance).To(Equal("500")) + Expect(target.FindVolume("USD", "").Input).To(Equal("500")) + Expect(target.FindVolume("USD", "").Output).To(Equal("0")) + Expect(target.FindVolume("USD", "").Balance).To(Equal("500")) }) It("Should allow subsequent force transactions to accumulate debt", func() { @@ -305,8 +305,8 @@ var _ = Describe("Force Transactions", Ordered, func() { Address: "debt-source", }) Expect(err).To(Succeed()) - Expect(source.Volumes["USD"].Output).To(Equal("300")) - Expect(source.Volumes["USD"].Balance).To(Equal("-300")) + Expect(source.FindVolume("USD", "").Output).To(Equal("300")) + Expect(source.FindVolume("USD", "").Balance).To(Equal("-300")) }) It("Should allow force transactions to recover from negative balance", func() { @@ -322,7 +322,7 @@ var _ = Describe("Force Transactions", Ordered, func() { Address: "recovery-account", }) Expect(err).To(Succeed()) - Expect(account.Volumes["USD"].Balance).To(Equal("-500")) + Expect(account.FindVolume("USD", "").Balance).To(Equal("-500")) // Fund the account to recover _, err = sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ @@ -336,9 +336,9 @@ var _ = Describe("Force Transactions", Ordered, func() { Address: "recovery-account", }) Expect(err).To(Succeed()) - Expect(account.Volumes["USD"].Input).To(Equal("1000")) - Expect(account.Volumes["USD"].Output).To(Equal("500")) - Expect(account.Volumes["USD"].Balance).To(Equal("500")) + Expect(account.FindVolume("USD", "").Input).To(Equal("1000")) + Expect(account.FindVolume("USD", "").Output).To(Equal("500")) + Expect(account.FindVolume("USD", "").Balance).To(Equal("500")) }) }) }) diff --git a/tests/e2e/business/idempotency_test.go b/tests/e2e/business/idempotency_test.go index 5ddfe1f674..dc6c4e4cb4 100644 --- a/tests/e2e/business/idempotency_test.go +++ b/tests/e2e/business/idempotency_test.go @@ -114,7 +114,7 @@ var _ = Describe("Idempotency Keys", Ordered, func() { Address: "account-dup", }) Expect(err).To(Succeed()) - Expect(account.Volumes["USD"].Input).To(Equal("100")) + Expect(account.FindVolume("USD", "").Input).To(Equal("100")) }) It("Should fail when reusing idempotency key with different transaction content", func() { @@ -159,7 +159,7 @@ var _ = Describe("Idempotency Keys", Ordered, func() { Address: "account-multi", }) Expect(err).To(Succeed()) - Expect(account.Volumes["USD"].Input).To(Equal("200")) + Expect(account.FindVolume("USD", "").Input).To(Equal("200")) }) }) @@ -225,7 +225,7 @@ var _ = Describe("Idempotency Keys", Ordered, func() { Address: "ttl-account", }) Expect(err).To(Succeed()) - Expect(account.Volumes["USD"].Input).To(Equal("200")) + Expect(account.FindVolume("USD", "").Input).To(Equal("200")) }) }) @@ -282,14 +282,14 @@ var _ = Describe("Idempotency Keys", Ordered, func() { Address: "bulk-account-1", }) Expect(err).To(Succeed()) - Expect(account1.Volumes["USD"].Input).To(Equal("100")) + Expect(account1.FindVolume("USD", "").Input).To(Equal("100")) account2, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ Ledger: ledgerName, Address: "bulk-account-2", }) Expect(err).To(Succeed()) - Expect(account2.Volumes["USD"].Input).To(Equal("200")) + Expect(account2.FindVolume("USD", "").Input).To(Equal("200")) }) }) }) diff --git a/tests/e2e/business/ledger_delete_cleanup_test.go b/tests/e2e/business/ledger_delete_cleanup_test.go index 7302039229..65b9e82767 100644 --- a/tests/e2e/business/ledger_delete_cleanup_test.go +++ b/tests/e2e/business/ledger_delete_cleanup_test.go @@ -46,8 +46,8 @@ var _ = Describe("Ledger Deletion Data Cleanup", Ordered, func() { Address: "user-0", }) g.Expect(err).To(Succeed()) - g.Expect(account.Volumes).To(HaveKey("USD")) - g.Expect(account.Volumes["USD"].Balance).To(Equal("100")) + g.Expect(account.FindVolume("USD", "")).NotTo(BeNil(), "expected USD entry on account") + g.Expect(account.FindVolume("USD", "").Balance).To(Equal("100")) }).Within(15 * time.Second).WithPolling(500 * time.Millisecond).Should(Succeed()) }) @@ -107,8 +107,8 @@ var _ = Describe("Ledger Deletion Data Cleanup", Ordered, func() { Address: "alice", }) g.Expect(err).To(Succeed()) - g.Expect(account.Volumes).To(HaveKey("USD")) - g.Expect(account.Volumes["USD"].Balance).To(Equal("0")) + g.Expect(account.FindVolume("USD", "")).NotTo(BeNil(), "expected USD entry on account") + g.Expect(account.FindVolume("USD", "").Balance).To(Equal("0")) }).Within(15 * time.Second).WithPolling(500 * time.Millisecond).Should(Succeed()) }) diff --git a/tests/e2e/business/numscript_library_test.go b/tests/e2e/business/numscript_library_test.go index 7f29579ef3..4aee4afa12 100644 --- a/tests/e2e/business/numscript_library_test.go +++ b/tests/e2e/business/numscript_library_test.go @@ -450,8 +450,8 @@ send $amount ( Address: "users:alice", }) g.Expect(err).To(Succeed()) - g.Expect(account.Volumes).To(HaveKey("USD/2")) - g.Expect(account.Volumes["USD/2"].Balance).To(Equal("1000")) + g.Expect(account.FindVolume("USD/2", "")).NotTo(BeNil(), "expected USD/2 entry on account") + g.Expect(account.FindVolume("USD/2", "").Balance).To(Equal("1000")) }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) }) diff --git a/tests/e2e/business/numscript_test.go b/tests/e2e/business/numscript_test.go index 1cd5a2374d..545fb09b37 100644 --- a/tests/e2e/business/numscript_test.go +++ b/tests/e2e/business/numscript_test.go @@ -69,8 +69,8 @@ send $amount ( Address: "bank", }) g.Expect(err).To(Succeed()) - g.Expect(account.Volumes).To(HaveKey("USD/2")) - g.Expect(account.Volumes["USD/2"].Balance).To(Equal("1000")) + g.Expect(account.FindVolume("USD/2", "")).NotTo(BeNil(), "expected USD/2 entry on account") + g.Expect(account.FindVolume("USD/2", "").Balance).To(Equal("1000")) }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) }) @@ -101,8 +101,8 @@ send $amount ( Address: "users:alice", }) g.Expect(err).To(Succeed()) - g.Expect(account.Volumes).To(HaveKey("EUR/2")) - g.Expect(account.Volumes["EUR/2"].Balance).To(Equal("5000")) + g.Expect(account.FindVolume("EUR/2", "")).NotTo(BeNil(), "expected EUR/2 entry on account") + g.Expect(account.FindVolume("EUR/2", "").Balance).To(Equal("5000")) }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) }) @@ -156,8 +156,8 @@ send $amount ( Address: "taxes:vat", }) g.Expect(err).To(Succeed()) - g.Expect(taxAccount.Volumes).To(HaveKey("USD/2")) - g.Expect(taxAccount.Volumes["USD/2"].Balance).To(Equal("200")) // 20% of 1000 + g.Expect(taxAccount.FindVolume("USD/2", "")).NotTo(BeNil(), "expected USD/2 entry on taxAccount") + g.Expect(taxAccount.FindVolume("USD/2", "").Balance).To(Equal("200")) // 20% of 1000 }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) Eventually(func(g Gomega) { @@ -166,8 +166,8 @@ send $amount ( Address: "bank:main", }) g.Expect(err).To(Succeed()) - g.Expect(mainAccount.Volumes).To(HaveKey("USD/2")) - g.Expect(mainAccount.Volumes["USD/2"].Balance).To(Equal("800")) // 80% of 1000 + g.Expect(mainAccount.FindVolume("USD/2", "")).NotTo(BeNil(), "expected USD/2 entry on mainAccount") + g.Expect(mainAccount.FindVolume("USD/2", "").Balance).To(Equal("800")) // 80% of 1000 }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) }) @@ -221,8 +221,8 @@ send $amount ( Address: "users:bob:wallet", }) g.Expect(err).To(Succeed()) - g.Expect(wallet.Volumes).To(HaveKey("USD/2")) - g.Expect(wallet.Volumes["USD/2"].Balance).To(Equal("0")) // Fully drained + g.Expect(wallet.FindVolume("USD/2", "")).NotTo(BeNil(), "expected USD/2 entry on wallet") + g.Expect(wallet.FindVolume("USD/2", "").Balance).To(Equal("0")) // Fully drained }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) Eventually(func(g Gomega) { @@ -231,8 +231,8 @@ send $amount ( Address: "users:bob:bank", }) g.Expect(err).To(Succeed()) - g.Expect(bank.Volumes).To(HaveKey("USD/2")) - g.Expect(bank.Volumes["USD/2"].Balance).To(Equal("100")) // 200 - 100 (remainder) + g.Expect(bank.FindVolume("USD/2", "")).NotTo(BeNil(), "expected USD/2 entry on bank") + g.Expect(bank.FindVolume("USD/2", "").Balance).To(Equal("100")) // 200 - 100 (remainder) }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) Eventually(func(g Gomega) { @@ -241,8 +241,8 @@ send $amount ( Address: "merchants:shop", }) g.Expect(err).To(Succeed()) - g.Expect(shop.Volumes).To(HaveKey("USD/2")) - g.Expect(shop.Volumes["USD/2"].Balance).To(Equal("150")) + g.Expect(shop.FindVolume("USD/2", "")).NotTo(BeNil(), "expected USD/2 entry on shop") + g.Expect(shop.FindVolume("USD/2", "").Balance).To(Equal("150")) }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) }) @@ -287,8 +287,8 @@ send $amount ( Address: "users:charlie", }) g.Expect(err).To(Succeed()) - g.Expect(charlie.Volumes).To(HaveKey("EUR/2")) - g.Expect(charlie.Volumes["EUR/2"].Balance).To(Equal("-300")) + g.Expect(charlie.FindVolume("EUR/2", "")).NotTo(BeNil(), "expected EUR/2 entry on charlie") + g.Expect(charlie.FindVolume("EUR/2", "").Balance).To(Equal("-300")) }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) }) @@ -363,8 +363,8 @@ send $amount ( Address: "credit:eve", }) g.Expect(err).To(Succeed()) - g.Expect(creditLine.Volumes).To(HaveKey("USD/2")) - g.Expect(creditLine.Volumes["USD/2"].Balance).To(Equal("-100000")) + g.Expect(creditLine.FindVolume("USD/2", "")).NotTo(BeNil(), "expected USD/2 entry on creditLine") + g.Expect(creditLine.FindVolume("USD/2", "").Balance).To(Equal("-100000")) }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) }) @@ -475,7 +475,7 @@ send [USD/2 1000] ( Address: "escrow:order-12345", }) Expect(err).To(Succeed()) - Expect(escrow.Volumes["USD/2"].Balance).To(Equal("500")) + Expect(escrow.FindVolume("USD/2", "").Balance).To(Equal("500")) }) It("Should fail with invalid Numscript syntax", func() { @@ -556,8 +556,8 @@ send $amount ( Address: address, }) g.Expect(err).To(Succeed()) - g.Expect(account.Volumes).To(HaveKey("USD/2")) - g.Expect(account.Volumes["USD/2"].Balance).To(Equal(expectedBalance)) + g.Expect(account.FindVolume("USD/2", "")).NotTo(BeNil(), "expected USD/2 entry on account") + g.Expect(account.FindVolume("USD/2", "").Balance).To(Equal(expectedBalance)) }).Within(10 * time.Second).WithPolling(100 * time.Millisecond).Should(Succeed()) } }) diff --git a/tests/e2e/business/reversions_test.go b/tests/e2e/business/reversions_test.go index 9f9f01e35f..ebbcef6aba 100644 --- a/tests/e2e/business/reversions_test.go +++ b/tests/e2e/business/reversions_test.go @@ -242,7 +242,7 @@ var _ = Describe("Reversions", Ordered, func() { Address: "account-1", }) Expect(err).To(Succeed()) - Expect(account1.Volumes["USD"].Balance).To(Equal("100")) + Expect(account1.FindVolume("USD", "").Balance).To(Equal("100")) // Revert the transaction log := createResp.Logs[0] @@ -258,7 +258,7 @@ var _ = Describe("Reversions", Ordered, func() { Address: "account-1", }) Expect(err).To(Succeed()) - Expect(account1After.Volumes["USD"].Balance).To(Equal("0")) + Expect(account1After.FindVolume("USD", "").Balance).To(Equal("0")) }) It("Should restore balances for multi-posting transaction", func() { @@ -275,14 +275,14 @@ var _ = Describe("Reversions", Ordered, func() { Address: "account-a", }) Expect(err).To(Succeed()) - Expect(accountA.Volumes["USD"].Balance).To(Equal("100")) + Expect(accountA.FindVolume("USD", "").Balance).To(Equal("100")) accountB, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ Ledger: ledgerName, Address: "account-b", }) Expect(err).To(Succeed()) - Expect(accountB.Volumes["USD"].Balance).To(Equal("200")) + Expect(accountB.FindVolume("USD", "").Balance).To(Equal("200")) // Revert the transaction log := createResp.Logs[0] @@ -298,14 +298,14 @@ var _ = Describe("Reversions", Ordered, func() { Address: "account-a", }) Expect(err).To(Succeed()) - Expect(accountAAfter.Volumes["USD"].Balance).To(Equal("0")) + Expect(accountAAfter.FindVolume("USD", "").Balance).To(Equal("0")) accountBAfter, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ Ledger: ledgerName, Address: "account-b", }) Expect(err).To(Succeed()) - Expect(accountBAfter.Volumes["USD"].Balance).To(Equal("0")) + Expect(accountBAfter.FindVolume("USD", "").Balance).To(Equal("0")) }) It("Should correctly track volumes after revert", func() { @@ -329,9 +329,9 @@ var _ = Describe("Reversions", Ordered, func() { Address: "volume-account", }) Expect(err).To(Succeed()) - Expect(account.Volumes["USD"].Input).To(Equal("100")) - Expect(account.Volumes["USD"].Output).To(Equal("100")) - Expect(account.Volumes["USD"].Balance).To(Equal("0")) + Expect(account.FindVolume("USD", "").Input).To(Equal("100")) + Expect(account.FindVolume("USD", "").Output).To(Equal("100")) + Expect(account.FindVolume("USD", "").Balance).To(Equal("0")) }) }) @@ -467,7 +467,7 @@ var _ = Describe("Reversions", Ordered, func() { Address: "account-1", }) Expect(err).To(Succeed()) - Expect(account1.Volumes["USD"].Balance).To(Equal("-100")) + Expect(account1.FindVolume("USD", "").Balance).To(Equal("-100")) }) }) @@ -518,8 +518,8 @@ var _ = Describe("Reversions", Ordered, func() { pcv := revertedTx.PostCommitVolumes.VolumesByAccount // After revert: ev-rv-expand sent 100 back to world -> input=100, output=100 Expect(pcv).To(HaveKey("ev-rv-expand")) - Expect(pcv["ev-rv-expand"].Volumes["USD"].Input).To(Equal("100")) - Expect(pcv["ev-rv-expand"].Volumes["USD"].Output).To(Equal("100")) + Expect(pcv["ev-rv-expand"].FindVolume("USD", "").Input).To(Equal("100")) + Expect(pcv["ev-rv-expand"].FindVolume("USD", "").Output).To(Equal("100")) Expect(pcv).To(HaveKey("world")) }) @@ -549,8 +549,8 @@ var _ = Describe("Reversions", Ordered, func() { pcv := revertedTx.PostCommitVolumes.VolumesByAccount // ev-rv-force: input=100 (original), output=200 (100 spent + 100 reverted) Expect(pcv).To(HaveKey("ev-rv-force")) - Expect(pcv["ev-rv-force"].Volumes["USD"].Input).To(Equal("100")) - Expect(pcv["ev-rv-force"].Volumes["USD"].Output).To(Equal("200")) + Expect(pcv["ev-rv-force"].FindVolume("USD", "").Input).To(Equal("100")) + Expect(pcv["ev-rv-force"].FindVolume("USD", "").Output).To(Equal("200")) }) }) }) diff --git a/tests/e2e/business/transactions_test.go b/tests/e2e/business/transactions_test.go index c4e55c74af..657eafd445 100644 --- a/tests/e2e/business/transactions_test.go +++ b/tests/e2e/business/transactions_test.go @@ -142,7 +142,7 @@ var _ = Describe("Transactions", Ordered, func() { }) Expect(err).To(Succeed()) Expect(account.Address).To(Equal("account-with-meta")) - Expect(account.Volumes["USD"].Balance).To(Equal("100")) + Expect(account.FindVolume("USD", "").Balance).To(Equal("100")) }) It("Should create multiple transactions sequentially", func() { @@ -172,14 +172,14 @@ var _ = Describe("Transactions", Ordered, func() { Address: "seq-account-1", }) Expect(err).To(Succeed()) - Expect(account1.Volumes["USD"].Balance).To(Equal("50")) // 100 - 50 + Expect(account1.FindVolume("USD", "").Balance).To(Equal("50")) // 100 - 50 account2, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ Ledger: ledgerName, Address: "seq-account-2", }) Expect(err).To(Succeed()) - Expect(account2.Volumes["USD"].Balance).To(Equal("250")) // 200 + 50 + Expect(account2.FindVolume("USD", "").Balance).To(Equal("250")) // 200 + 50 }) It("Should create a transaction with multiple postings", func() { @@ -204,21 +204,21 @@ var _ = Describe("Transactions", Ordered, func() { Address: "account-a", }) Expect(err).To(Succeed()) - Expect(accountA.Volumes["USD"].Balance).To(Equal("100")) + Expect(accountA.FindVolume("USD", "").Balance).To(Equal("100")) accountB, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ Ledger: ledgerName, Address: "account-b", }) Expect(err).To(Succeed()) - Expect(accountB.Volumes["USD"].Balance).To(Equal("200")) + Expect(accountB.FindVolume("USD", "").Balance).To(Equal("200")) accountC, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ Ledger: ledgerName, Address: "account-c", }) Expect(err).To(Succeed()) - Expect(accountC.Volumes["USD"].Balance).To(Equal("300")) + Expect(accountC.FindVolume("USD", "").Balance).To(Equal("300")) }) It("Should create a transaction with multiple assets", func() { @@ -238,9 +238,9 @@ var _ = Describe("Transactions", Ordered, func() { }) Expect(err).To(Succeed()) Expect(account.Volumes).To(HaveLen(3)) - Expect(account.Volumes["USD"].Balance).To(Equal("100")) - Expect(account.Volumes["EUR"].Balance).To(Equal("50")) - Expect(account.Volumes["JPY"].Balance).To(Equal("1000")) + Expect(account.FindVolume("USD", "").Balance).To(Equal("100")) + Expect(account.FindVolume("EUR", "").Balance).To(Equal("50")) + Expect(account.FindVolume("JPY", "").Balance).To(Equal("1000")) }) It("Should create multiple transactions in bulk", func() { @@ -281,7 +281,7 @@ var _ = Describe("Transactions", Ordered, func() { }) Expect(err).To(Succeed()) Expect(account.Address).To(Equal("implicit-account")) - Expect(account.Volumes["USD"].Balance).To(Equal("100")) + Expect(account.FindVolume("USD", "").Balance).To(Equal("100")) }) It("Should handle large amounts correctly", func() { @@ -302,7 +302,7 @@ var _ = Describe("Transactions", Ordered, func() { Address: "large-amount-account", }) Expect(err).To(Succeed()) - Expect(account.Volumes["USD"].Balance).To(Equal("99999999999999999999999999999")) + Expect(account.FindVolume("USD", "").Balance).To(Equal("99999999999999999999999999999")) }) }) @@ -369,7 +369,7 @@ var _ = Describe("Transactions", Ordered, func() { Address: "recipient", }) Expect(err).To(Succeed()) - Expect(recipient.Volumes["USD"].Balance).To(Equal("1000000")) + Expect(recipient.FindVolume("USD", "").Balance).To(Equal("1000000")) // World's balance should be negative world, err := sharedClient.GetAccount(sharedCtx, &servicepb.GetAccountRequest{ @@ -377,7 +377,7 @@ var _ = Describe("Transactions", Ordered, func() { Address: "world", }) Expect(err).To(Succeed()) - Expect(world.Volumes["USD"].Balance).To(HavePrefix("-")) + Expect(world.FindVolume("USD", "").Balance).To(HavePrefix("-")) }) }) @@ -453,9 +453,9 @@ var _ = Describe("Transactions", Ordered, func() { Address: "volume-account", }) Expect(err).To(Succeed()) - Expect(account.Volumes["USD"].Input).To(Equal("1000")) - Expect(account.Volumes["USD"].Output).To(Equal("300")) - Expect(account.Volumes["USD"].Balance).To(Equal("700")) + Expect(account.FindVolume("USD", "").Input).To(Equal("1000")) + Expect(account.FindVolume("USD", "").Output).To(Equal("300")) + Expect(account.FindVolume("USD", "").Balance).To(Equal("700")) }) It("Should handle circular transactions correctly", func() { @@ -486,9 +486,9 @@ var _ = Describe("Transactions", Ordered, func() { Address: "cycle-a", }) Expect(err).To(Succeed()) - Expect(accountA.Volumes["USD"].Input).To(Equal("200")) // from world + cycle-c - Expect(accountA.Volumes["USD"].Output).To(Equal("100")) // to cycle-b - Expect(accountA.Volumes["USD"].Balance).To(Equal("100")) + Expect(accountA.FindVolume("USD", "").Input).To(Equal("200")) // from world + cycle-c + Expect(accountA.FindVolume("USD", "").Output).To(Equal("100")) // to cycle-b + Expect(accountA.FindVolume("USD", "").Balance).To(Equal("100")) }) }) @@ -684,12 +684,13 @@ var _ = Describe("Transactions", Ordered, func() { Expect(pcv).To(HaveKey("ev-simple")) // world is shared across tests in this Ordered context, so only check presence - Expect(pcv["world"].Volumes).To(HaveKey("USD")) + Expect(pcv["world"].FindVolume("USD", "")).NotTo(BeNil(), "expected USD entry on world") // ev-simple is fresh — exact values are predictable - Expect(pcv["ev-simple"].Volumes).To(HaveKey("USD")) - Expect(pcv["ev-simple"].Volumes["USD"].Input).To(Equal("100")) - Expect(pcv["ev-simple"].Volumes["USD"].Output).To(Equal("0")) + evSimple := pcv["ev-simple"].FindVolume("USD", "") + Expect(evSimple).NotTo(BeNil(), "expected USD entry on ev-simple") + Expect(evSimple.GetInput()).To(Equal("100")) + Expect(evSimple.GetOutput()).To(Equal("0")) }) It("Should include correct volumes for multiple postings", func() { @@ -704,10 +705,10 @@ var _ = Describe("Transactions", Ordered, func() { Expect(pcv).To(HaveKey("ev-multi-a")) Expect(pcv).To(HaveKey("ev-multi-b")) - Expect(pcv["ev-multi-a"].Volumes["USD"].Input).To(Equal("100")) - Expect(pcv["ev-multi-a"].Volumes["USD"].Output).To(Equal("0")) - Expect(pcv["ev-multi-b"].Volumes["USD"].Input).To(Equal("200")) - Expect(pcv["ev-multi-b"].Volumes["USD"].Output).To(Equal("0")) + Expect(pcv["ev-multi-a"].FindVolume("USD", "").Input).To(Equal("100")) + Expect(pcv["ev-multi-a"].FindVolume("USD", "").Output).To(Equal("0")) + Expect(pcv["ev-multi-b"].FindVolume("USD", "").Input).To(Equal("200")) + Expect(pcv["ev-multi-b"].FindVolume("USD", "").Output).To(Equal("0")) }) It("Should include correct volumes for multiple assets", func() { @@ -720,13 +721,15 @@ var _ = Describe("Transactions", Ordered, func() { pcv := resp.Logs[0].Payload.GetApply().Log.Data.GetCreatedTransaction().PostCommitVolumes.VolumesByAccount Expect(pcv).To(HaveKey("ev-multi-asset")) - vols := pcv["ev-multi-asset"].Volumes - Expect(vols).To(HaveKey("USD")) - Expect(vols).To(HaveKey("EUR")) - Expect(vols["USD"].Input).To(Equal("100")) - Expect(vols["USD"].Output).To(Equal("0")) - Expect(vols["EUR"].Input).To(Equal("50")) - Expect(vols["EUR"].Output).To(Equal("0")) + vba := pcv["ev-multi-asset"] + usd := vba.FindVolume("USD", "") + eur := vba.FindVolume("EUR", "") + Expect(usd).NotTo(BeNil(), "expected USD entry on ev-multi-asset") + Expect(eur).NotTo(BeNil(), "expected EUR entry on ev-multi-asset") + Expect(usd.GetInput()).To(Equal("100")) + Expect(usd.GetOutput()).To(Equal("0")) + Expect(eur.GetInput()).To(Equal("50")) + Expect(eur.GetOutput()).To(Equal("0")) }) It("Should reflect cumulative volumes across sequential transactions", func() { @@ -736,8 +739,8 @@ var _ = Describe("Transactions", Ordered, func() { Expect(err).To(Succeed()) pcv1 := resp1.Logs[0].Payload.GetApply().Log.Data.GetCreatedTransaction().PostCommitVolumes.VolumesByAccount - Expect(pcv1["ev-cumul"].Volumes["USD"].Input).To(Equal("500")) - Expect(pcv1["ev-cumul"].Volumes["USD"].Output).To(Equal("0")) + Expect(pcv1["ev-cumul"].FindVolume("USD", "").Input).To(Equal("500")) + Expect(pcv1["ev-cumul"].FindVolume("USD", "").Output).To(Equal("0")) resp2, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", actions.WithExpandVolumes(actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ actions.NewPosting("ev-cumul", "ev-cumul-dest", big.NewInt(200), "USD"), @@ -745,10 +748,10 @@ var _ = Describe("Transactions", Ordered, func() { Expect(err).To(Succeed()) pcv2 := resp2.Logs[0].Payload.GetApply().Log.Data.GetCreatedTransaction().PostCommitVolumes.VolumesByAccount - Expect(pcv2["ev-cumul"].Volumes["USD"].Input).To(Equal("500")) - Expect(pcv2["ev-cumul"].Volumes["USD"].Output).To(Equal("200")) - Expect(pcv2["ev-cumul-dest"].Volumes["USD"].Input).To(Equal("200")) - Expect(pcv2["ev-cumul-dest"].Volumes["USD"].Output).To(Equal("0")) + Expect(pcv2["ev-cumul"].FindVolume("USD", "").Input).To(Equal("500")) + Expect(pcv2["ev-cumul"].FindVolume("USD", "").Output).To(Equal("200")) + Expect(pcv2["ev-cumul-dest"].FindVolume("USD", "").Input).To(Equal("200")) + Expect(pcv2["ev-cumul-dest"].FindVolume("USD", "").Output).To(Equal("0")) }) It("Should work with force flag and expandVolumes", func() { @@ -764,10 +767,10 @@ var _ = Describe("Transactions", Ordered, func() { Expect(pcv).To(HaveKey("ev-force-src")) Expect(pcv).To(HaveKey("ev-force-dst")) - Expect(pcv["ev-force-src"].Volumes["USD"].Input).To(Equal("0")) - Expect(pcv["ev-force-src"].Volumes["USD"].Output).To(Equal("100")) - Expect(pcv["ev-force-dst"].Volumes["USD"].Input).To(Equal("100")) - Expect(pcv["ev-force-dst"].Volumes["USD"].Output).To(Equal("0")) + Expect(pcv["ev-force-src"].FindVolume("USD", "").Input).To(Equal("0")) + Expect(pcv["ev-force-src"].FindVolume("USD", "").Output).To(Equal("100")) + Expect(pcv["ev-force-dst"].FindVolume("USD", "").Input).To(Equal("100")) + Expect(pcv["ev-force-dst"].FindVolume("USD", "").Output).To(Equal("0")) }) It("Should include postCommitVolumes with Numscript transaction", func() { @@ -787,8 +790,8 @@ var _ = Describe("Transactions", Ordered, func() { pcv := createdTx.PostCommitVolumes.VolumesByAccount Expect(pcv).To(HaveKey("world")) Expect(pcv).To(HaveKey("user:001")) - Expect(pcv["user:001"].Volumes["USD/2"].Input).To(Equal("100")) - Expect(pcv["user:001"].Volumes["USD/2"].Output).To(Equal("0")) + Expect(pcv["user:001"].FindVolume("USD/2", "").Input).To(Equal("100")) + Expect(pcv["user:001"].FindVolume("USD/2", "").Output).To(Equal("0")) }) It("Should include postCommitVolumes for each transaction in a bulk request", func() { @@ -802,10 +805,10 @@ var _ = Describe("Transactions", Ordered, func() { Expect(resp.Logs).To(HaveLen(2)) pcv1 := resp.Logs[0].Payload.GetApply().Log.Data.GetCreatedTransaction().PostCommitVolumes.VolumesByAccount - Expect(pcv1["ev-bulk-a"].Volumes["USD"].Input).To(Equal("100")) + Expect(pcv1["ev-bulk-a"].FindVolume("USD", "").Input).To(Equal("100")) pcv2 := resp.Logs[1].Payload.GetApply().Log.Data.GetCreatedTransaction().PostCommitVolumes.VolumesByAccount - Expect(pcv2["ev-bulk-b"].Volumes["USD"].Input).To(Equal("200")) + Expect(pcv2["ev-bulk-b"].FindVolume("USD", "").Input).To(Equal("200")) }) It("Should allow mixing expandVolumes=true and expandVolumes=false in bulk", func() { diff --git a/tests/e2e/business/transient_accounts_test.go b/tests/e2e/business/transient_accounts_test.go index 0b2b31962b..63aad23643 100644 --- a/tests/e2e/business/transient_accounts_test.go +++ b/tests/e2e/business/transient_accounts_test.go @@ -82,8 +82,8 @@ var _ = Describe("TransientAccounts", Ordered, func() { }) g.Expect(err).To(Succeed()) - usdVol, ok := account.GetVolumes()["USD"] - g.Expect(ok).To(BeTrue(), "expected USD volumes on wallet:main") + usdVol := account.FindVolume("USD", "") + g.Expect(usdVol).NotTo(BeNil(), "expected USD volumes on wallet:main") g.Expect(usdVol.GetInput()).To(Equal("100")) g.Expect(usdVol.GetBalance()).To(Equal("100")) }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) @@ -224,8 +224,8 @@ var _ = Describe("TransientAccounts", Ordered, func() { Address: "staging:a", }) g.Expect(err).To(Succeed()) - usdVol, ok := account.GetVolumes()["USD"] - g.Expect(ok).To(BeTrue()) + usdVol := account.FindVolume("USD", "") + g.Expect(usdVol).NotTo(BeNil()) g.Expect(usdVol.GetInput()).To(Equal("100")) }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) @@ -286,8 +286,8 @@ var _ = Describe("TransientAccounts", Ordered, func() { Address: "wallet:b", }) g.Expect(err).To(Succeed()) - usdVol, ok := account.GetVolumes()["USD"] - g.Expect(ok).To(BeTrue()) + usdVol := account.FindVolume("USD", "") + g.Expect(usdVol).NotTo(BeNil()) g.Expect(usdVol.GetInput()).To(Equal("100")) g.Expect(usdVol.GetBalance()).To(Equal("100")) }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) @@ -386,8 +386,8 @@ var _ = Describe("TransientAccounts", Ordered, func() { Address: "wallet:y", }) g.Expect(err).To(Succeed()) - usdVol, ok := account.GetVolumes()["USD"] - g.Expect(ok).To(BeTrue()) + usdVol := account.FindVolume("USD", "") + g.Expect(usdVol).NotTo(BeNil()) g.Expect(usdVol.GetInput()).To(Equal("200")) g.Expect(usdVol.GetBalance()).To(Equal("200")) }).Within(5 * time.Second).ProbeEvery(200 * time.Millisecond).Should(Succeed()) @@ -436,20 +436,20 @@ var _ = Describe("TransientAccounts", Ordered, func() { // staging:reuse should read {50, 0} — fresh, not cumulative {150, 100}. pcv1 := resp.Logs[0].Payload.GetApply().Log.Data.GetCreatedTransaction().PostCommitVolumes.VolumesByAccount Expect(pcv1).To(HaveKey("staging:reuse")) - Expect(pcv1["staging:reuse"].Volumes["USD"].Input).To(Equal("50"), + Expect(pcv1["staging:reuse"].FindVolume("USD", "").Input).To(Equal("50"), "transient input should reflect this batch only, not accumulate across batches") - Expect(pcv1["staging:reuse"].Volumes["USD"].Output).To(Equal("0")) + Expect(pcv1["staging:reuse"].FindVolume("USD", "").Output).To(Equal("0")) // Second transaction's PCV: staging:reuse → wallet:b 50. // staging:reuse now {50, 50} — the per-batch zero-balance — not {150, 150}. pcv2 := resp.Logs[1].Payload.GetApply().Log.Data.GetCreatedTransaction().PostCommitVolumes.VolumesByAccount Expect(pcv2).To(HaveKey("staging:reuse")) - Expect(pcv2["staging:reuse"].Volumes["USD"].Input).To(Equal("50")) - Expect(pcv2["staging:reuse"].Volumes["USD"].Output).To(Equal("50")) + Expect(pcv2["staging:reuse"].FindVolume("USD", "").Input).To(Equal("50")) + Expect(pcv2["staging:reuse"].FindVolume("USD", "").Output).To(Equal("50")) // And the wallet sees its fresh +50. - Expect(pcv2["wallet:b"].Volumes["USD"].Input).To(Equal("50")) - Expect(pcv2["wallet:b"].Volumes["USD"].Output).To(Equal("0")) + Expect(pcv2["wallet:b"].FindVolume("USD", "").Input).To(Equal("50")) + Expect(pcv2["wallet:b"].FindVolume("USD", "").Output).To(Equal("0")) }) }) }) diff --git a/tests/e2e/cluster/bloom_config_test.go b/tests/e2e/cluster/bloom_config_test.go index 4ecbe440bd..df3079df34 100644 --- a/tests/e2e/cluster/bloom_config_test.go +++ b/tests/e2e/cluster/bloom_config_test.go @@ -210,7 +210,9 @@ var _ = Describe("Bloom filter config change preserves data", Ordered, func() { Eventually(func(g Gomega) { account, err := actions.GetAccount(ctx, srv.Client, "post-bloom-change", "user:1") g.Expect(err).To(Succeed()) - g.Expect(account.Volumes["EUR"].Input).To(Equal("999")) + eurVol := account.FindVolume("EUR", "") + g.Expect(eurVol).NotTo(BeNil(), "expected EUR volumes on user:1") + g.Expect(eurVol.GetInput()).To(Equal("999")) }). WithTimeout(30 * time.Second). WithPolling(500 * time.Millisecond). diff --git a/tests/e2e/cluster/bootstrap_test.go b/tests/e2e/cluster/bootstrap_test.go index 88a9372657..343a149d4c 100644 --- a/tests/e2e/cluster/bootstrap_test.go +++ b/tests/e2e/cluster/bootstrap_test.go @@ -345,12 +345,12 @@ var _ = Describe("Bootstrap from backup", Ordered, func() { It("should have the correct account balances", func() { aliceResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "alice"}) Expect(err).To(Succeed()) - Expect(aliceResp.Volumes["USD"].Input).To(Equal("3000")) + Expect(aliceResp.FindVolume("USD", "").Input).To(Equal("3000")) bankResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "bank"}) Expect(err).To(Succeed()) - Expect(bankResp.Volumes["USD"].Input).To(Equal("10000")) - Expect(bankResp.Volumes["USD"].Output).To(Equal("5000")) + Expect(bankResp.FindVolume("USD", "").Input).To(Equal("10000")) + Expect(bankResp.FindVolume("USD", "").Output).To(Equal("5000")) }) It("should have the correct account metadata", func() { @@ -362,7 +362,7 @@ var _ = Describe("Bootstrap from backup", Ordered, func() { It("should have the data added after the first backup (via second full backup)", func() { eveResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "eve"}) Expect(err).To(Succeed()) - Expect(eveResp.Volumes["USD"].Input).To(Equal("500")) + Expect(eveResp.FindVolume("USD", "").Input).To(Equal("500")) }) It("should accept new transactions after bootstrap", func() { @@ -373,7 +373,7 @@ var _ = Describe("Bootstrap from backup", Ordered, func() { charlieResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "charlie"}) Expect(err).To(Succeed()) - Expect(charlieResp.Volumes["USD"].Input).To(Equal("1000")) + Expect(charlieResp.FindVolume("USD", "").Input).To(Equal("1000")) }) }) }) diff --git a/tests/e2e/cluster/cache_divergence_test.go b/tests/e2e/cluster/cache_divergence_test.go index dc7f86319a..b5066e1210 100644 --- a/tests/e2e/cluster/cache_divergence_test.go +++ b/tests/e2e/cluster/cache_divergence_test.go @@ -189,8 +189,12 @@ func verifyVolumesConsistent(ctx context.Context, servers []*testutil.ServiceWit } for _, acct := range accounts { - for asset, vol := range acct.GetVolumes() { - key := fmt.Sprintf("%s/%s", acct.GetAddress(), asset) + for _, entry := range acct.GetVolumes() { + vol := entry.GetVolumes() + // Key on (account, asset, color) so colored buckets stay + // distinct in the divergence snapshot. Empty color is the + // uncolored bucket. + key := fmt.Sprintf("%s/%s/%s", acct.GetAddress(), entry.GetAsset(), entry.GetColor()) snap.volumes[key] = fmt.Sprintf("%s:%s", vol.GetInput(), vol.GetOutput()) } } diff --git a/tests/e2e/cluster/query_checkpoint_test.go b/tests/e2e/cluster/query_checkpoint_test.go index f3dc18b81f..1bcbf5d468 100644 --- a/tests/e2e/cluster/query_checkpoint_test.go +++ b/tests/e2e/cluster/query_checkpoint_test.go @@ -126,7 +126,7 @@ var _ = Describe("Query Checkpoints", func() { // bob was funded (500 EUR) AFTER the checkpoint. liveBob, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "bob"}) Expect(err).To(Succeed()) - Expect(liveBob.GetVolumes()).To(HaveKey("EUR"), "live store has the post-checkpoint balance") + Expect(liveBob.FindVolume("EUR", "")).NotTo(BeNil(), "live store has the post-checkpoint balance") cpBob, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{ Ledger: ledgerName, @@ -134,7 +134,7 @@ var _ = Describe("Query Checkpoints", func() { CheckpointId: checkpointID, }) Expect(err).To(Succeed()) - Expect(cpBob.GetVolumes()).NotTo(HaveKey("EUR"), + Expect(cpBob.FindVolume("EUR", "")).To(BeNil(), "checkpoint predates bob; reading it must not return live data") }) @@ -344,8 +344,8 @@ var _ = Describe("Query Checkpoints", func() { }) Expect(err).To(Succeed()) - vols, ok := resp.GetVolumes()[asset] - Expect(ok).To(BeTrue(), "expected %s volumes at checkpoint %d", asset, cp) + vols := resp.FindVolume(asset, "") + Expect(vols).NotTo(BeNil(), "expected %s volumes at checkpoint %d", asset, cp) Expect(vols.GetBalance()).To(Equal(expected), "balance at checkpoint %d should be frozen at %s", cp, expected) } @@ -369,7 +369,7 @@ var _ = Describe("Query Checkpoints", func() { }) Expect(err).To(Succeed()) - Expect(resp.GetVolumes()[asset].GetBalance()).To(Equal("400")) + Expect(resp.FindVolume(asset, "").GetBalance()).To(Equal("400")) }) }) }) diff --git a/tests/e2e/cluster/restore_test.go b/tests/e2e/cluster/restore_test.go index 824df1cf11..e846a6ce85 100644 --- a/tests/e2e/cluster/restore_test.go +++ b/tests/e2e/cluster/restore_test.go @@ -425,16 +425,16 @@ var _ = Describe("Restore", Ordered, func() { It("should have the correct account balances on ledger 1", func() { aliceResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "alice"}) Expect(err).To(Succeed()) - Expect(aliceResp.Volumes["USD"].Input).To(Equal("3000")) + Expect(aliceResp.FindVolume("USD", "").Input).To(Equal("3000")) bobResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "bob"}) Expect(err).To(Succeed()) - Expect(bobResp.Volumes["USD"].Input).To(Equal("2000")) + Expect(bobResp.FindVolume("USD", "").Input).To(Equal("2000")) bankResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "bank"}) Expect(err).To(Succeed()) - Expect(bankResp.Volumes["USD"].Input).To(Equal("10000")) - Expect(bankResp.Volumes["USD"].Output).To(Equal("5000")) + Expect(bankResp.FindVolume("USD", "").Input).To(Equal("10000")) + Expect(bankResp.FindVolume("USD", "").Output).To(Equal("5000")) }) It("should have the correct account metadata", func() { @@ -446,7 +446,7 @@ var _ = Describe("Restore", Ordered, func() { It("should have the correct data on ledger 2", func() { treasuryResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledger2, Address: "treasury"}) Expect(err).To(Succeed()) - Expect(treasuryResp.Volumes["EUR"].Input).To(Equal("50000")) + Expect(treasuryResp.FindVolume("EUR", "").Input).To(Equal("50000")) }) It("should have post-checkpoint data restored from export segments", func() { @@ -454,7 +454,7 @@ var _ = Describe("Restore", Ordered, func() { // only be present if the restore applied the incremental exports. daveResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "dave"}) Expect(err).To(Succeed()) - Expect(daveResp.Volumes["USD"].Input).To(Equal("1500"), + Expect(daveResp.FindVolume("USD", "").Input).To(Equal("1500"), "transaction written after the checkpoint must be restored from export segments") }) @@ -474,7 +474,7 @@ var _ = Describe("Restore", Ordered, func() { daveResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "dave"}) Expect(err).To(Succeed()) - Expect(daveResp.Volumes["USD"].Input).To(Equal("2000"), + Expect(daveResp.FindVolume("USD", "").Input).To(Equal("2000"), "apply must see dave's restored balance via the cache; a cache/bloom-blind apply yields 500") }) @@ -486,7 +486,7 @@ var _ = Describe("Restore", Ordered, func() { charlieResp, err := client.GetAccount(ctx, &servicepb.GetAccountRequest{Ledger: ledgerName, Address: "charlie"}) Expect(err).To(Succeed()) - Expect(charlieResp.Volumes["USD"].Input).To(Equal("1000")) + Expect(charlieResp.FindVolume("USD", "").Input).To(Equal("1000")) }) }) }) diff --git a/tests/e2e/cluster/rolling_config_test.go b/tests/e2e/cluster/rolling_config_test.go index 42242ab988..3200bf9c7f 100644 --- a/tests/e2e/cluster/rolling_config_test.go +++ b/tests/e2e/cluster/rolling_config_test.go @@ -108,7 +108,7 @@ func expectVolume(ctx context.Context, client servicepb.BucketServiceClient, led Eventually(func(g Gomega) { account, err := actions.GetAccount(ctx, client, ledger, "bank") g.Expect(err).To(Succeed()) - g.Expect(account.Volumes["USD"].Input).To(Equal(expectedInput)) + g.Expect(account.FindVolume("USD", "").Input).To(Equal(expectedInput)) }).WithTimeout(30 * time.Second).WithPolling(500 * time.Millisecond).Should(Succeed()) } @@ -118,7 +118,7 @@ func expectVolumeAllNodes(ctx context.Context, servers []*testutil.ServiceWithCl Eventually(func(g Gomega) { account, err := actions.GetAccount(ctx, srv.Client, ledger, "bank") g.Expect(err).To(Succeed()) - g.Expect(account.Volumes["USD"].Input).To(Equal(expectedInput)) + g.Expect(account.FindVolume("USD", "").Input).To(Equal(expectedInput)) }). WithTimeout(30*time.Second). WithPolling(500*time.Millisecond). diff --git a/tests/scenarios/scenariotest/scenariotest.go b/tests/scenarios/scenariotest/scenariotest.go index af24200f0f..5edb146798 100644 --- a/tests/scenarios/scenariotest/scenariotest.go +++ b/tests/scenarios/scenariotest/scenariotest.go @@ -210,68 +210,87 @@ func CloseChapterAndWait(t *testing.T, ctx context.Context, client servicepb.Buc // Invariant checks (Antithesis-ready) // --------------------------------------------------------------------------- -// CheckPositiveBalance verifies that an account has a strictly positive balance for a given asset. +// CheckPositiveBalance verifies that the uncolored balance of an account +// for a given asset is strictly positive. func CheckPositiveBalance(t *testing.T, ctx context.Context, client servicepb.BucketServiceClient, ledgerName, address, asset string) { t.Helper() acct, err := actions.GetAccount(ctx, client, ledgerName, address) require.NoError(t, err, "failed to get account %s", address) - vol, ok := acct.Volumes[asset] - require.True(t, ok, "account %s has no volumes for asset %s", address, asset) + vol := acct.FindVolume(asset, "") + require.NotNil(t, vol, "account %s has no volumes for asset %s (uncolored)", address, asset) - balance, ok := new(big.Int).SetString(vol.Balance, 10) - require.True(t, ok, "invalid balance %q for account %s asset %s", vol.Balance, address, asset) + balance, ok := new(big.Int).SetString(vol.GetBalance(), 10) + require.True(t, ok, "invalid balance %q for account %s asset %s", vol.GetBalance(), address, asset) require.True(t, balance.Sign() > 0, "account %s asset %s: expected positive balance, got %s", address, asset, balance.String()) } -// CheckDoubleEntryBalance verifies that for every asset in the ledger, -// the sum of all account balances equals zero (double-entry invariant). +// CheckDoubleEntryBalance verifies that for every (asset, color) tuple, the +// sum of all account balances equals zero (double-entry invariant). Each +// (asset, color) bucket is its own segregated double-entry universe. func CheckDoubleEntryBalance(t *testing.T, ctx context.Context, client servicepb.BucketServiceClient, ledgerName string) { t.Helper() accounts, err := actions.ListAllAccounts(ctx, client, ledgerName) require.NoError(t, err, "failed to list accounts for double-entry check") - sums := make(map[string]*big.Int) // asset -> sum of balances + type bucket struct{ asset, color string } + sums := make(map[bucket]*big.Int) for _, acct := range accounts { - for asset, vol := range acct.Volumes { - balance, ok := new(big.Int).SetString(vol.Balance, 10) - require.True(t, ok, "invalid balance %q for account %s asset %s", vol.Balance, acct.Address, asset) - - if sums[asset] == nil { - sums[asset] = new(big.Int) + for _, entry := range acct.GetVolumes() { + vol := entry.GetVolumes() + balance, ok := new(big.Int).SetString(vol.GetBalance(), 10) + require.True(t, ok, "invalid balance %q for account %s asset %s color %q", + vol.GetBalance(), acct.GetAddress(), entry.GetAsset(), entry.GetColor()) + + k := bucket{asset: entry.GetAsset(), color: entry.GetColor()} + if sums[k] == nil { + sums[k] = new(big.Int) } - sums[asset].Add(sums[asset], balance) + sums[k].Add(sums[k], balance) } } - for asset, sum := range sums { + for k, sum := range sums { require.Equal(t, 0, sum.Sign(), - "double-entry violated for asset %s: sum of balances = %s (expected 0)", asset, sum.String()) + "double-entry violated for asset %s color %q: sum of balances = %s (expected 0)", + k.asset, k.color, sum.String()) } } -// CheckAccountBalance verifies that a specific account has the expected balance for a given asset. +// CheckAccountBalance verifies the uncolored balance of an account for a +// given asset matches the expected amount. For colored buckets, use +// CheckColoredAccountBalance. func CheckAccountBalance(t *testing.T, ctx context.Context, client servicepb.BucketServiceClient, ledgerName, address, asset string, expected *big.Int) { t.Helper() + CheckColoredAccountBalance(t, ctx, client, ledgerName, address, asset, "", expected) +} + +// CheckColoredAccountBalance verifies the balance for a specific +// (account, asset, color) bucket. Color "" is the uncolored bucket. +func CheckColoredAccountBalance(t *testing.T, ctx context.Context, client servicepb.BucketServiceClient, ledgerName, address, asset, color string, expected *big.Int) { + t.Helper() acct, err := actions.GetAccount(ctx, client, ledgerName, address) require.NoError(t, err, "failed to get account %s", address) - vol, ok := acct.Volumes[asset] - require.True(t, ok, "account %s has no volumes for asset %s", address, asset) + vol := acct.FindVolume(asset, color) + require.NotNil(t, vol, "account %s has no volumes for asset %s color %q", address, asset, color) - balance, ok := new(big.Int).SetString(vol.Balance, 10) - require.True(t, ok, "invalid balance %q for account %s asset %s", vol.Balance, address, asset) + balance, ok := new(big.Int).SetString(vol.GetBalance(), 10) + require.True(t, ok, "invalid balance %q for account %s asset %s color %q", + vol.GetBalance(), address, asset, color) require.Equal(t, 0, expected.Cmp(balance), - "account %s asset %s: expected balance %s, got %s", address, asset, expected.String(), balance.String()) + "account %s asset %s color %q: expected balance %s, got %s", + address, asset, color, expected.String(), balance.String()) } -// CheckNoNegativeBalances verifies no account has a negative balance, -// except for explicitly listed exceptions (e.g., @world, overdraft accounts). +// CheckNoNegativeBalances verifies no (account, asset, color) bucket has a +// negative balance, except for explicitly listed exceptions (e.g., @world, +// overdraft accounts). func CheckNoNegativeBalances(t *testing.T, ctx context.Context, client servicepb.BucketServiceClient, ledgerName string, exceptions []string) { t.Helper() @@ -284,14 +303,17 @@ func CheckNoNegativeBalances(t *testing.T, ctx context.Context, client servicepb require.NoError(t, err, "failed to list accounts for negative balance check") for _, acct := range accounts { - if exceptionSet[acct.Address] { + if exceptionSet[acct.GetAddress()] { continue } - for asset, vol := range acct.Volumes { - balance, ok := new(big.Int).SetString(vol.Balance, 10) - require.True(t, ok, "invalid balance %q for account %s asset %s", vol.Balance, acct.Address, asset) + for _, entry := range acct.GetVolumes() { + vol := entry.GetVolumes() + balance, ok := new(big.Int).SetString(vol.GetBalance(), 10) + require.True(t, ok, "invalid balance %q for account %s asset %s color %q", + vol.GetBalance(), acct.GetAddress(), entry.GetAsset(), entry.GetColor()) require.True(t, balance.Sign() >= 0, - "negative balance on account %s asset %s: %s", acct.Address, asset, balance.String()) + "negative balance on account %s asset %s color %q: %s", + acct.GetAddress(), entry.GetAsset(), entry.GetColor(), balance.String()) } } } From 299187c103d4eae07a858cb275785ce02538302c Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 24 Jun 2026 21:48:55 +0200 Subject: [PATCH 2/9] fix(e2e): drop residual GetVolumes()["USD"] + adapt color specs to UnsignedApplyRequest - idempotency_failure_test.go used the old map-style Account.Volumes lookup; switch to FindVolume("USD", ""). The e2e package fails the build under `-tags e2e` otherwise. - color_segregation_test.go / color_numscript_test.go: master replaced the inline `&servicepb.ApplyRequest{Envelopes: UnsignedEnvelopes(...)}` builder with the `servicepb.UnsignedApplyRequest("", ...)` helper. Migrate every Apply call accordingly. Addresses NumaryBot review on common.proto:148. --- internal/domain/errors.go | 1 - tests/e2e/business/color_numscript_test.go | 106 ++++++++---------- tests/e2e/business/color_segregation_test.go | 58 +++++----- .../e2e/business/idempotency_failure_test.go | 4 +- 4 files changed, 73 insertions(+), 96 deletions(-) diff --git a/internal/domain/errors.go b/internal/domain/errors.go index 442d3c0ae7..8ef3aac82e 100644 --- a/internal/domain/errors.go +++ b/internal/domain/errors.go @@ -596,7 +596,6 @@ func (e *ErrBalanceNotFound) Metadata() map[string]string { return map[string]string{"account": e.Account, "asset": e.Asset} } - // ErrSinkAlreadyExists — adding a sink that already exists. type ErrSinkAlreadyExists struct { Name string diff --git a/tests/e2e/business/color_numscript_test.go b/tests/e2e/business/color_numscript_test.go index 150511a3d9..095f16aafb 100644 --- a/tests/e2e/business/color_numscript_test.go +++ b/tests/e2e/business/color_numscript_test.go @@ -22,24 +22,22 @@ var _ = Describe("ColorNumscript", Ordered, func() { const ledgerName = "color-numscript" BeforeAll(func() { - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes(actions.CreateLedgerAction(ledgerName, nil)), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateLedgerAction(ledgerName, nil), + )) Expect(err).To(Succeed()) // Seed alice with three segregated buckets on USD/2: // uncolored "" : 300 // GRANTS : 200 // OPS : 100 - _, err = sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ - actions.NewColoredPosting("world", "alice", big.NewInt(300), "USD/2", ""), - actions.NewColoredPosting("world", "alice", big.NewInt(200), "USD/2", "GRANTS"), - actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", "OPS"), - }, nil, nil), - ), - }) + _, err = sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(300), "USD/2", ""), + actions.NewColoredPosting("world", "alice", big.NewInt(200), "USD/2", "GRANTS"), + actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", "OPS"), + }, nil, nil), + )) Expect(err).To(Succeed()) }) @@ -55,11 +53,9 @@ send [USD/2 60] ( destination = @bob ) ` - resp, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateScriptTransactionAction(ledgerName, script, nil, nil), - ), - }) + resp, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateScriptTransactionAction(ledgerName, script, nil, nil), + )) Expect(err).To(Succeed()) Expect(resp.Logs).To(HaveLen(1)) @@ -108,11 +104,9 @@ send [USD/2 1] ( destination = @bob ) ` - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateScriptTransactionAction(ledgerName, script, nil, nil), - ), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateScriptTransactionAction(ledgerName, script, nil, nil), + )) Expect(err).To(HaveOccurred(), "a colored draw must not be satisfied from uncolored funds even via numscript") }) @@ -127,11 +121,9 @@ send [USD/2 90] ( destination = @bob ) ` - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateScriptTransactionAction(ledgerName, script, nil, nil), - ), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateScriptTransactionAction(ledgerName, script, nil, nil), + )) Expect(err).To(Succeed()) Eventually(func(g Gomega) { @@ -191,9 +183,9 @@ var _ = Describe("AggregateVolumesColor", Ordered, func() { const ledgerName = "agg-vol-color" BeforeAll(func() { - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes(actions.CreateLedgerAction(ledgerName, nil)), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateLedgerAction(ledgerName, nil), + )) Expect(err).To(Succeed()) // Fund three (asset, color) buckets on alice. We mix assets and @@ -202,16 +194,14 @@ var _ = Describe("AggregateVolumesColor", Ordered, func() { // alice / USD/2 / "GRANTS" : 100 // alice / USD/2 / "OPS" : 40 // alice / EUR/2 / "GRANTS" : 50 - _, err = sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ - actions.NewColoredPosting("world", "alice", big.NewInt(10), "USD/2", ""), - actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", "GRANTS"), - actions.NewColoredPosting("world", "alice", big.NewInt(40), "USD/2", "OPS"), - actions.NewColoredPosting("world", "alice", big.NewInt(50), "EUR/2", "GRANTS"), - }, nil, nil), - ), - }) + _, err = sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(10), "USD/2", ""), + actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", "GRANTS"), + actions.NewColoredPosting("world", "alice", big.NewInt(40), "USD/2", "OPS"), + actions.NewColoredPosting("world", "alice", big.NewInt(50), "EUR/2", "GRANTS"), + }, nil, nil), + )) Expect(err).To(Succeed()) }) @@ -289,29 +279,25 @@ var _ = Describe("ColorRevert", Ordered, func() { var revertTargetTxID uint64 BeforeAll(func() { - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes(actions.CreateLedgerAction(ledgerName, nil)), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateLedgerAction(ledgerName, nil), + )) Expect(err).To(Succeed()) // world → alice 200 USD/2 color=GRANTS, world → alice 100 USD/2 uncolored. - _, err = sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ - actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", ""), - }, nil, nil), - ), - }) + _, err = sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", ""), + }, nil, nil), + )) Expect(err).To(Succeed()) // The colored transaction is the one we'll revert. - resp, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ - actions.NewColoredPosting("world", "alice", big.NewInt(200), "USD/2", "GRANTS"), - }, nil, nil), - ), - }) + resp, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(200), "USD/2", "GRANTS"), + }, nil, nil), + )) Expect(err).To(Succeed()) Expect(resp.Logs).To(HaveLen(1)) createdTx := resp.Logs[0].Payload.GetApply().Log.Data.GetCreatedTransaction() @@ -320,9 +306,9 @@ var _ = Describe("ColorRevert", Ordered, func() { }) It("Should drive the revert against the same color bucket", func() { - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes(actions.RevertTransactionAction(ledgerName, revertTargetTxID, false, false, nil)), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.RevertTransactionAction(ledgerName, revertTargetTxID, false, false, nil), + )) Expect(err).To(Succeed()) Eventually(func(g Gomega) { diff --git a/tests/e2e/business/color_segregation_test.go b/tests/e2e/business/color_segregation_test.go index ab9c088b9e..1ff027e6cf 100644 --- a/tests/e2e/business/color_segregation_test.go +++ b/tests/e2e/business/color_segregation_test.go @@ -21,24 +21,22 @@ var _ = Describe("ColorSegregation", Ordered, func() { const ledgerName = "color-segregation" BeforeAll(func() { - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes(actions.CreateLedgerAction(ledgerName, nil)), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateLedgerAction(ledgerName, nil), + )) Expect(err).To(Succeed()) // Seed alice with three segregated buckets on USD/2: // uncolored "" : 100 // GRANTS : 50 // OPS : 25 - _, err = sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ - actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", ""), - actions.NewColoredPosting("world", "alice", big.NewInt(50), "USD/2", "GRANTS"), - actions.NewColoredPosting("world", "alice", big.NewInt(25), "USD/2", "OPS"), - }, nil, nil), - ), - }) + _, err = sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("world", "alice", big.NewInt(100), "USD/2", ""), + actions.NewColoredPosting("world", "alice", big.NewInt(50), "USD/2", "GRANTS"), + actions.NewColoredPosting("world", "alice", big.NewInt(25), "USD/2", "OPS"), + }, nil, nil), + )) Expect(err).To(Succeed()) }) @@ -89,38 +87,32 @@ var _ = Describe("ColorSegregation", Ordered, func() { It("Should reject a draw from a color that has insufficient funds", func() { // alice's OPS bucket has 25; ask for 100 OPS → MissingFunds. - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ - actions.NewColoredPosting("alice", "bob", big.NewInt(100), "USD/2", "OPS"), - }, nil, nil), - ), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("alice", "bob", big.NewInt(100), "USD/2", "OPS"), + }, nil, nil), + )) Expect(err).NotTo(BeNil(), "expected color isolation to refuse spending more than the bucket holds") }) It("Should refuse drawing colored funds from the uncolored bucket", func() { // alice's uncolored bucket has 100; drawing 100 from "" should succeed. // But drawing 100 from "GRANTS" must NOT dip into the 100 uncolored. - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ - actions.NewColoredPosting("alice", "bob", big.NewInt(100), "USD/2", "GRANTS"), - }, nil, nil), - ), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("alice", "bob", big.NewInt(100), "USD/2", "GRANTS"), + }, nil, nil), + )) Expect(err).NotTo(BeNil(), "uncolored funds must not satisfy a GRANTS-colored draw") }) It("Should drain a color independently of the others", func() { // Spend exactly the GRANTS bucket (50) → success. - _, err := sharedClient.Apply(sharedCtx, &servicepb.ApplyRequest{ - Envelopes: servicepb.UnsignedEnvelopes( - actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ - actions.NewColoredPosting("alice", "bob", big.NewInt(50), "USD/2", "GRANTS"), - }, nil, nil), - ), - }) + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + actions.CreateTransactionAction(ledgerName, []*commonpb.Posting{ + actions.NewColoredPosting("alice", "bob", big.NewInt(50), "USD/2", "GRANTS"), + }, nil, nil), + )) Expect(err).To(Succeed()) Eventually(func(g Gomega) { diff --git a/tests/e2e/business/idempotency_failure_test.go b/tests/e2e/business/idempotency_failure_test.go index a8abf154df..0d861f1189 100644 --- a/tests/e2e/business/idempotency_failure_test.go +++ b/tests/e2e/business/idempotency_failure_test.go @@ -152,8 +152,8 @@ var _ = Describe("Idempotency preserves committed outcomes", Ordered, func() { Address: "wallet:1", }) Expect(err).To(Succeed()) - usdVol, ok := acct.GetVolumes()["USD"] - Expect(ok).To(BeTrue()) + usdVol := acct.FindVolume("USD", "") + Expect(usdVol).NotTo(BeNil()) Expect(usdVol.GetInput()).To(Equal("50")) }) }) From ba3e02a76f6d5b1b21e1a5a86329ab4b73873d98 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 25 Jun 2026 11:53:22 +0200 Subject: [PATCH 3/9] chore: post-rebase pass version.Info to NewHandler in aggregate test (#561) --- .../http/handlers_aggregate_volumes_test.go | 2 +- internal/proto/commonpb/common.pb.go | 322 +++++++++--------- 2 files changed, 167 insertions(+), 157 deletions(-) diff --git a/internal/adapter/http/handlers_aggregate_volumes_test.go b/internal/adapter/http/handlers_aggregate_volumes_test.go index da1d568d75..c8b20a9808 100644 --- a/internal/adapter/http/handlers_aggregate_volumes_test.go +++ b/internal/adapter/http/handlers_aggregate_volumes_test.go @@ -261,7 +261,7 @@ func TestHandleAggregateVolumes_EmitsColorAlways(t *testing.T) { }, nil }) - handler := NewHandler(logging.Testing(), backend, internalauth.AuthConfig{}) + handler := NewHandler(logging.Testing(), backend, internalauth.AuthConfig{}, version.Info{}) w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodGet, "/my-ledger/volumes?collapseColors=true", nil) diff --git a/internal/proto/commonpb/common.pb.go b/internal/proto/commonpb/common.pb.go index 1469343456..c4be75aec9 100644 --- a/internal/proto/commonpb/common.pb.go +++ b/internal/proto/commonpb/common.pb.go @@ -3856,6 +3856,7 @@ type ClusterConfig struct { BloomNumscriptContents *BloomTypeConfig `protobuf:"bytes,10,opt,name=bloom_numscript_contents,json=bloomNumscriptContents,proto3" json:"bloom_numscript_contents,omitempty"` HashAlgorithm HashAlgorithm `protobuf:"varint,11,opt,name=hash_algorithm,json=hashAlgorithm,proto3,enum=common.HashAlgorithm" json:"hash_algorithm,omitempty"` BloomLedgerMetadata *BloomTypeConfig `protobuf:"bytes,12,opt,name=bloom_ledger_metadata,json=bloomLedgerMetadata,proto3" json:"bloom_ledger_metadata,omitempty"` + BloomPreparedQueries *BloomTypeConfig `protobuf:"bytes,13,opt,name=bloom_prepared_queries,json=bloomPreparedQueries,proto3" json:"bloom_prepared_queries,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3974,6 +3975,13 @@ func (x *ClusterConfig) GetBloomLedgerMetadata() *BloomTypeConfig { return nil } +func (x *ClusterConfig) GetBloomPreparedQueries() *BloomTypeConfig { + if x != nil { + return x.BloomPreparedQueries + } + return nil +} + // PersistedClusterState wraps ClusterConfig with internal FSM state that must // be persisted for deterministic restore but is not part of the config proposal. type PersistedClusterState struct { @@ -10774,7 +10782,7 @@ const file_common_proto_rawDesc = "" + "\aenabled\x18\x01 \x01(\bR\aenabled\"O\n" + "\x0fBloomTypeConfig\x12#\n" + "\rexpected_keys\x18\x01 \x01(\x04R\fexpectedKeys\x12\x17\n" + - "\afp_rate\x18\x02 \x01(\x01R\x06fpRate\"\xc2\x06\n" + + "\afp_rate\x18\x02 \x01(\x01R\x06fpRate\"\x91\a\n" + "\rClusterConfig\x12-\n" + "\x12rotation_threshold\x18\x01 \x01(\x04R\x11rotationThreshold\x12<\n" + "\rbloom_volumes\x18\x02 \x01(\v2\x17.common.BloomTypeConfigR\fbloomVolumes\x12>\n" + @@ -10788,7 +10796,8 @@ const file_common_proto_rawDesc = "" + "\x18bloom_numscript_contents\x18\n" + " \x01(\v2\x17.common.BloomTypeConfigR\x16bloomNumscriptContents\x12<\n" + "\x0ehash_algorithm\x18\v \x01(\x0e2\x15.common.HashAlgorithmR\rhashAlgorithm\x12K\n" + - "\x15bloom_ledger_metadata\x18\f \x01(\v2\x17.common.BloomTypeConfigR\x13bloomLedgerMetadata\"g\n" + + "\x15bloom_ledger_metadata\x18\f \x01(\v2\x17.common.BloomTypeConfigR\x13bloomLedgerMetadata\x12M\n" + + "\x16bloom_prepared_queries\x18\r \x01(\v2\x17.common.BloomTypeConfigR\x14bloomPreparedQueries\"g\n" + "\x15PersistedClusterState\x12-\n" + "\x06config\x18\x01 \x01(\v2\x15.common.ClusterConfigR\x06config\x12\x1f\n" + "\vcache_epoch\x18\x02 \x01(\x06R\n" + @@ -11677,160 +11686,161 @@ var file_common_proto_depIdxs = []int32{ 53, // 73: common.ClusterConfig.bloom_numscript_contents:type_name -> common.BloomTypeConfig 6, // 74: common.ClusterConfig.hash_algorithm:type_name -> common.HashAlgorithm 53, // 75: common.ClusterConfig.bloom_ledger_metadata:type_name -> common.BloomTypeConfig - 54, // 76: common.PersistedClusterState.config:type_name -> common.ClusterConfig - 138, // 77: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery - 121, // 78: common.UpdatedPreparedQueryLog.previous_filter:type_name -> common.QueryFilter - 121, // 79: common.UpdatedPreparedQueryLog.new_filter:type_name -> common.QueryFilter - 160, // 80: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry - 17, // 81: common.NumscriptInfo.created_at:type_name -> common.Timestamp - 63, // 82: common.SavedNumscriptLog.info:type_name -> common.NumscriptInfo - 73, // 83: common.SinkConfig.nats:type_name -> common.NatsSinkConfig - 74, // 84: common.SinkConfig.clickhouse:type_name -> common.ClickHouseSinkConfig - 75, // 85: common.SinkConfig.kafka:type_name -> common.KafkaSinkConfig - 76, // 86: common.SinkConfig.http:type_name -> common.HttpSinkConfig - 77, // 87: common.SinkConfig.databricks:type_name -> common.DatabricksSinkConfig - 7, // 88: common.SinkConfig.event_types:type_name -> common.EventType - 72, // 89: common.SinkStatus.error:type_name -> common.SinkError - 17, // 90: common.SinkError.occurred_at:type_name -> common.Timestamp - 78, // 91: common.DatabricksSinkConfig.oauth_m2m:type_name -> common.DatabricksOAuthM2M - 17, // 92: common.CreatedLedgerLog.created_at:type_name -> common.Timestamp - 36, // 93: common.CreatedLedgerLog.metadata_schema:type_name -> common.MetadataSchema - 9, // 94: common.CreatedLedgerLog.mode:type_name -> common.LedgerMode - 99, // 95: common.CreatedLedgerLog.mirror_source:type_name -> common.MirrorSourceConfig - 161, // 96: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry - 12, // 97: common.CreatedLedgerLog.default_enforcement_mode:type_name -> common.ChartEnforcementMode - 17, // 98: common.DeletedLedgerLog.deleted_at:type_name -> common.Timestamp - 82, // 99: common.ApplyLedgerLog.log:type_name -> common.LedgerLog - 84, // 100: common.LedgerLog.data:type_name -> common.LedgerLogPayload - 17, // 101: common.LedgerLog.date:type_name -> common.Timestamp - 83, // 102: common.LedgerLog.purged_volumes:type_name -> common.TouchedVolume - 88, // 103: common.LedgerLogPayload.created_transaction:type_name -> common.CreatedTransaction - 89, // 104: common.LedgerLogPayload.reverted_transaction:type_name -> common.RevertedTransaction - 90, // 105: common.LedgerLogPayload.saved_metadata:type_name -> common.SavedMetadata - 91, // 106: common.LedgerLogPayload.deleted_metadata:type_name -> common.DeletedMetadata - 92, // 107: common.LedgerLogPayload.set_metadata_field_type:type_name -> common.SetMetadataFieldTypeLog - 93, // 108: common.LedgerLogPayload.removed_metadata_field_type:type_name -> common.RemovedMetadataFieldTypeLog - 87, // 109: common.LedgerLogPayload.fill_gap:type_name -> common.FilledGapLog - 85, // 110: common.LedgerLogPayload.create_index:type_name -> common.CreatedIndexLog - 86, // 111: common.LedgerLogPayload.drop_index:type_name -> common.DroppedIndexLog - 118, // 112: common.LedgerLogPayload.added_account_type:type_name -> common.AddedAccountTypeLog - 119, // 113: common.LedgerLogPayload.removed_account_type:type_name -> common.RemovedAccountTypeLog - 120, // 114: common.LedgerLogPayload.updated_default_enforcement_mode:type_name -> common.UpdatedDefaultEnforcementModeLog - 39, // 115: common.CreatedIndexLog.id:type_name -> common.IndexID - 39, // 116: common.DroppedIndexLog.id:type_name -> common.IndexID - 24, // 117: common.CreatedTransaction.transaction:type_name -> common.Transaction - 162, // 118: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry - 30, // 119: common.CreatedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes - 24, // 120: common.RevertedTransaction.revert_transaction:type_name -> common.Transaction - 30, // 121: common.RevertedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes - 34, // 122: common.SavedMetadata.target:type_name -> common.Target - 163, // 123: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry - 34, // 124: common.DeletedMetadata.target:type_name -> common.Target - 0, // 125: common.SetMetadataFieldTypeLog.target_type:type_name -> common.TargetType - 1, // 126: common.SetMetadataFieldTypeLog.type:type_name -> common.MetadataType - 0, // 127: common.RemovedMetadataFieldTypeLog.target_type:type_name -> common.TargetType - 39, // 128: common.RemovedMetadataFieldTypeLog.dropped_index:type_name -> common.IndexID - 17, // 129: common.Chapter.start:type_name -> common.Timestamp - 17, // 130: common.Chapter.end:type_name -> common.Timestamp - 8, // 131: common.Chapter.status:type_name -> common.ChapterStatus - 94, // 132: common.ClosedChapterLog.closed_chapter:type_name -> common.Chapter - 94, // 133: common.ClosedChapterLog.new_chapter:type_name -> common.Chapter - 94, // 134: common.SealedChapterLog.chapter:type_name -> common.Chapter - 94, // 135: common.ArchivedChapterLog.chapter:type_name -> common.Chapter - 94, // 136: common.ConfirmedArchiveChapterLog.chapter:type_name -> common.Chapter - 100, // 137: common.MirrorSourceConfig.http:type_name -> common.HttpMirrorSourceConfig - 102, // 138: common.MirrorSourceConfig.postgres:type_name -> common.PostgresMirrorSourceConfig - 101, // 139: common.HttpMirrorSourceConfig.oauth2_client_credentials:type_name -> common.OAuth2ClientCredentials - 17, // 140: common.MirrorSyncError.occurred_at:type_name -> common.Timestamp - 10, // 141: common.MirrorSyncProgress.state:type_name -> common.MirrorSyncState - 103, // 142: common.MirrorSyncProgress.error:type_name -> common.MirrorSyncError - 17, // 143: common.LedgerInfo.created_at:type_name -> common.Timestamp - 17, // 144: common.LedgerInfo.deleted_at:type_name -> common.Timestamp - 36, // 145: common.LedgerInfo.metadata_schema:type_name -> common.MetadataSchema - 9, // 146: common.LedgerInfo.mode:type_name -> common.LedgerMode - 99, // 147: common.LedgerInfo.mirror_source:type_name -> common.MirrorSourceConfig - 104, // 148: common.LedgerInfo.mirror_sync_progress:type_name -> common.MirrorSyncProgress - 164, // 149: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry - 12, // 150: common.LedgerInfo.default_enforcement_mode:type_name -> common.ChartEnforcementMode - 165, // 151: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry - 40, // 152: common.LedgerInfo.indexes:type_name -> common.Index - 34, // 153: common.SaveMetadataCommand.target:type_name -> common.Target - 166, // 154: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry - 34, // 155: common.DeleteMetadataCommand.target:type_name -> common.Target - 167, // 156: common.TransactionState.metadata:type_name -> common.TransactionState.MetadataEntry - 17, // 157: common.TransactionState.timestamp:type_name -> common.Timestamp - 110, // 158: common.IdempotencyKeyValue.failure:type_name -> common.IdempotencyFailure - 11, // 159: common.IdempotencyFailure.reason:type_name -> common.ErrorReason - 168, // 160: common.IdempotencyFailure.metadata:type_name -> common.IdempotencyFailure.MetadataEntry - 114, // 161: common.SegmentType.uuid:type_name -> common.UUIDConstraint - 115, // 162: common.SegmentType.uint64:type_name -> common.Uint64Constraint - 116, // 163: common.SegmentType.bytes:type_name -> common.BytesConstraint - 13, // 164: common.AccountType.persistence:type_name -> common.AccountTypePersistence - 169, // 165: common.AccountType.segment_types:type_name -> common.AccountType.SegmentTypesEntry - 117, // 166: common.AddedAccountTypeLog.account_type:type_name -> common.AccountType - 12, // 167: common.UpdatedDefaultEnforcementModeLog.enforcement_mode:type_name -> common.ChartEnforcementMode - 131, // 168: common.QueryFilter.field:type_name -> common.FieldCondition - 137, // 169: common.QueryFilter.address:type_name -> common.AddressMatch - 127, // 170: common.QueryFilter.and:type_name -> common.AndFilter - 128, // 171: common.QueryFilter.or:type_name -> common.OrFilter - 129, // 172: common.QueryFilter.not:type_name -> common.NotFilter - 122, // 173: common.QueryFilter.reference:type_name -> common.ReferenceCondition - 125, // 174: common.QueryFilter.builtin_uint:type_name -> common.BuiltinUintCondition - 123, // 175: common.QueryFilter.ledger:type_name -> common.LedgerCondition - 124, // 176: common.QueryFilter.log_id:type_name -> common.LogIdCondition - 126, // 177: common.QueryFilter.log_builtin_uint:type_name -> common.LogBuiltinUintCondition - 132, // 178: common.ReferenceCondition.cond:type_name -> common.StringCondition - 132, // 179: common.LedgerCondition.cond:type_name -> common.StringCondition - 134, // 180: common.LogIdCondition.cond:type_name -> common.UintCondition - 3, // 181: common.BuiltinUintCondition.field:type_name -> common.TransactionBuiltinIndex - 134, // 182: common.BuiltinUintCondition.cond:type_name -> common.UintCondition - 5, // 183: common.LogBuiltinUintCondition.field:type_name -> common.LogBuiltinIndex - 134, // 184: common.LogBuiltinUintCondition.cond:type_name -> common.UintCondition - 121, // 185: common.AndFilter.filters:type_name -> common.QueryFilter - 121, // 186: common.OrFilter.filters:type_name -> common.QueryFilter - 121, // 187: common.NotFilter.filter:type_name -> common.QueryFilter - 130, // 188: common.FieldCondition.field:type_name -> common.FieldRef - 132, // 189: common.FieldCondition.string_cond:type_name -> common.StringCondition - 133, // 190: common.FieldCondition.int_cond:type_name -> common.IntCondition - 134, // 191: common.FieldCondition.uint_cond:type_name -> common.UintCondition - 135, // 192: common.FieldCondition.bool_cond:type_name -> common.BoolCondition - 136, // 193: common.FieldCondition.exists_cond:type_name -> common.ExistsCondition - 14, // 194: common.AddressMatch.role:type_name -> common.AddressRole - 121, // 195: common.PreparedQuery.filter:type_name -> common.QueryFilter - 15, // 196: common.PreparedQuery.target:type_name -> common.QueryTarget - 22, // 197: common.AggregatedVolume.input:type_name -> common.Uint256 - 22, // 198: common.AggregatedVolume.output:type_name -> common.Uint256 - 139, // 199: common.AggregateResult.volumes:type_name -> common.AggregatedVolume - 141, // 200: common.AggregateResult.groups:type_name -> common.GroupedAggregateResult - 139, // 201: common.GroupedAggregateResult.volumes:type_name -> common.AggregatedVolume - 32, // 202: common.PreparedQueryCursor.account_data:type_name -> common.Account - 24, // 203: common.PreparedQueryCursor.transaction_data:type_name -> common.Transaction - 145, // 204: common.CallerSnapshot.identity:type_name -> common.CallerIdentity - 147, // 205: common.BackupStorage.s3:type_name -> common.S3StorageConfig - 148, // 206: common.BackupStorage.azure:type_name -> common.AzureStorageConfig - 150, // 207: common.ListOptions.read:type_name -> common.ReadOptions - 121, // 208: common.ListOptions.filter:type_name -> common.QueryFilter - 19, // 209: common.MetadataMap.ValuesEntry.value:type_name -> common.MetadataValue - 19, // 210: common.Transaction.MetadataEntry.value:type_name -> common.MetadataValue - 28, // 211: common.PostCommitVolumes.VolumesByAccountEntry.value:type_name -> common.VolumesByAssets - 19, // 212: common.Account.MetadataEntry.value:type_name -> common.MetadataValue - 35, // 213: common.MetadataSchema.AccountFieldsEntry.value:type_name -> common.MetadataFieldSchema - 35, // 214: common.MetadataSchema.TransactionFieldsEntry.value:type_name -> common.MetadataFieldSchema - 35, // 215: common.MetadataSchema.LedgerFieldsEntry.value:type_name -> common.MetadataFieldSchema - 19, // 216: common.SavedLedgerMetadataLog.MetadataEntry.value:type_name -> common.MetadataValue - 117, // 217: common.CreatedLedgerLog.AccountTypesEntry.value:type_name -> common.AccountType - 20, // 218: common.CreatedTransaction.AccountMetadataEntry.value:type_name -> common.MetadataMap - 19, // 219: common.SavedMetadata.MetadataEntry.value:type_name -> common.MetadataValue - 117, // 220: common.LedgerInfo.AccountTypesEntry.value:type_name -> common.AccountType - 19, // 221: common.LedgerInfo.MetadataEntry.value:type_name -> common.MetadataValue - 19, // 222: common.SaveMetadataCommand.MetadataEntry.value:type_name -> common.MetadataValue - 19, // 223: common.TransactionState.MetadataEntry.value:type_name -> common.MetadataValue - 113, // 224: common.AccountType.SegmentTypesEntry.value:type_name -> common.SegmentType - 225, // [225:225] is the sub-list for method output_type - 225, // [225:225] is the sub-list for method input_type - 225, // [225:225] is the sub-list for extension type_name - 225, // [225:225] is the sub-list for extension extendee - 0, // [0:225] is the sub-list for field type_name + 53, // 76: common.ClusterConfig.bloom_prepared_queries:type_name -> common.BloomTypeConfig + 54, // 77: common.PersistedClusterState.config:type_name -> common.ClusterConfig + 138, // 78: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery + 121, // 79: common.UpdatedPreparedQueryLog.previous_filter:type_name -> common.QueryFilter + 121, // 80: common.UpdatedPreparedQueryLog.new_filter:type_name -> common.QueryFilter + 160, // 81: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry + 17, // 82: common.NumscriptInfo.created_at:type_name -> common.Timestamp + 63, // 83: common.SavedNumscriptLog.info:type_name -> common.NumscriptInfo + 73, // 84: common.SinkConfig.nats:type_name -> common.NatsSinkConfig + 74, // 85: common.SinkConfig.clickhouse:type_name -> common.ClickHouseSinkConfig + 75, // 86: common.SinkConfig.kafka:type_name -> common.KafkaSinkConfig + 76, // 87: common.SinkConfig.http:type_name -> common.HttpSinkConfig + 77, // 88: common.SinkConfig.databricks:type_name -> common.DatabricksSinkConfig + 7, // 89: common.SinkConfig.event_types:type_name -> common.EventType + 72, // 90: common.SinkStatus.error:type_name -> common.SinkError + 17, // 91: common.SinkError.occurred_at:type_name -> common.Timestamp + 78, // 92: common.DatabricksSinkConfig.oauth_m2m:type_name -> common.DatabricksOAuthM2M + 17, // 93: common.CreatedLedgerLog.created_at:type_name -> common.Timestamp + 36, // 94: common.CreatedLedgerLog.metadata_schema:type_name -> common.MetadataSchema + 9, // 95: common.CreatedLedgerLog.mode:type_name -> common.LedgerMode + 99, // 96: common.CreatedLedgerLog.mirror_source:type_name -> common.MirrorSourceConfig + 161, // 97: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry + 12, // 98: common.CreatedLedgerLog.default_enforcement_mode:type_name -> common.ChartEnforcementMode + 17, // 99: common.DeletedLedgerLog.deleted_at:type_name -> common.Timestamp + 82, // 100: common.ApplyLedgerLog.log:type_name -> common.LedgerLog + 84, // 101: common.LedgerLog.data:type_name -> common.LedgerLogPayload + 17, // 102: common.LedgerLog.date:type_name -> common.Timestamp + 83, // 103: common.LedgerLog.purged_volumes:type_name -> common.TouchedVolume + 88, // 104: common.LedgerLogPayload.created_transaction:type_name -> common.CreatedTransaction + 89, // 105: common.LedgerLogPayload.reverted_transaction:type_name -> common.RevertedTransaction + 90, // 106: common.LedgerLogPayload.saved_metadata:type_name -> common.SavedMetadata + 91, // 107: common.LedgerLogPayload.deleted_metadata:type_name -> common.DeletedMetadata + 92, // 108: common.LedgerLogPayload.set_metadata_field_type:type_name -> common.SetMetadataFieldTypeLog + 93, // 109: common.LedgerLogPayload.removed_metadata_field_type:type_name -> common.RemovedMetadataFieldTypeLog + 87, // 110: common.LedgerLogPayload.fill_gap:type_name -> common.FilledGapLog + 85, // 111: common.LedgerLogPayload.create_index:type_name -> common.CreatedIndexLog + 86, // 112: common.LedgerLogPayload.drop_index:type_name -> common.DroppedIndexLog + 118, // 113: common.LedgerLogPayload.added_account_type:type_name -> common.AddedAccountTypeLog + 119, // 114: common.LedgerLogPayload.removed_account_type:type_name -> common.RemovedAccountTypeLog + 120, // 115: common.LedgerLogPayload.updated_default_enforcement_mode:type_name -> common.UpdatedDefaultEnforcementModeLog + 39, // 116: common.CreatedIndexLog.id:type_name -> common.IndexID + 39, // 117: common.DroppedIndexLog.id:type_name -> common.IndexID + 24, // 118: common.CreatedTransaction.transaction:type_name -> common.Transaction + 162, // 119: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry + 30, // 120: common.CreatedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes + 24, // 121: common.RevertedTransaction.revert_transaction:type_name -> common.Transaction + 30, // 122: common.RevertedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes + 34, // 123: common.SavedMetadata.target:type_name -> common.Target + 163, // 124: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry + 34, // 125: common.DeletedMetadata.target:type_name -> common.Target + 0, // 126: common.SetMetadataFieldTypeLog.target_type:type_name -> common.TargetType + 1, // 127: common.SetMetadataFieldTypeLog.type:type_name -> common.MetadataType + 0, // 128: common.RemovedMetadataFieldTypeLog.target_type:type_name -> common.TargetType + 39, // 129: common.RemovedMetadataFieldTypeLog.dropped_index:type_name -> common.IndexID + 17, // 130: common.Chapter.start:type_name -> common.Timestamp + 17, // 131: common.Chapter.end:type_name -> common.Timestamp + 8, // 132: common.Chapter.status:type_name -> common.ChapterStatus + 94, // 133: common.ClosedChapterLog.closed_chapter:type_name -> common.Chapter + 94, // 134: common.ClosedChapterLog.new_chapter:type_name -> common.Chapter + 94, // 135: common.SealedChapterLog.chapter:type_name -> common.Chapter + 94, // 136: common.ArchivedChapterLog.chapter:type_name -> common.Chapter + 94, // 137: common.ConfirmedArchiveChapterLog.chapter:type_name -> common.Chapter + 100, // 138: common.MirrorSourceConfig.http:type_name -> common.HttpMirrorSourceConfig + 102, // 139: common.MirrorSourceConfig.postgres:type_name -> common.PostgresMirrorSourceConfig + 101, // 140: common.HttpMirrorSourceConfig.oauth2_client_credentials:type_name -> common.OAuth2ClientCredentials + 17, // 141: common.MirrorSyncError.occurred_at:type_name -> common.Timestamp + 10, // 142: common.MirrorSyncProgress.state:type_name -> common.MirrorSyncState + 103, // 143: common.MirrorSyncProgress.error:type_name -> common.MirrorSyncError + 17, // 144: common.LedgerInfo.created_at:type_name -> common.Timestamp + 17, // 145: common.LedgerInfo.deleted_at:type_name -> common.Timestamp + 36, // 146: common.LedgerInfo.metadata_schema:type_name -> common.MetadataSchema + 9, // 147: common.LedgerInfo.mode:type_name -> common.LedgerMode + 99, // 148: common.LedgerInfo.mirror_source:type_name -> common.MirrorSourceConfig + 104, // 149: common.LedgerInfo.mirror_sync_progress:type_name -> common.MirrorSyncProgress + 164, // 150: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry + 12, // 151: common.LedgerInfo.default_enforcement_mode:type_name -> common.ChartEnforcementMode + 165, // 152: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry + 40, // 153: common.LedgerInfo.indexes:type_name -> common.Index + 34, // 154: common.SaveMetadataCommand.target:type_name -> common.Target + 166, // 155: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry + 34, // 156: common.DeleteMetadataCommand.target:type_name -> common.Target + 167, // 157: common.TransactionState.metadata:type_name -> common.TransactionState.MetadataEntry + 17, // 158: common.TransactionState.timestamp:type_name -> common.Timestamp + 110, // 159: common.IdempotencyKeyValue.failure:type_name -> common.IdempotencyFailure + 11, // 160: common.IdempotencyFailure.reason:type_name -> common.ErrorReason + 168, // 161: common.IdempotencyFailure.metadata:type_name -> common.IdempotencyFailure.MetadataEntry + 114, // 162: common.SegmentType.uuid:type_name -> common.UUIDConstraint + 115, // 163: common.SegmentType.uint64:type_name -> common.Uint64Constraint + 116, // 164: common.SegmentType.bytes:type_name -> common.BytesConstraint + 13, // 165: common.AccountType.persistence:type_name -> common.AccountTypePersistence + 169, // 166: common.AccountType.segment_types:type_name -> common.AccountType.SegmentTypesEntry + 117, // 167: common.AddedAccountTypeLog.account_type:type_name -> common.AccountType + 12, // 168: common.UpdatedDefaultEnforcementModeLog.enforcement_mode:type_name -> common.ChartEnforcementMode + 131, // 169: common.QueryFilter.field:type_name -> common.FieldCondition + 137, // 170: common.QueryFilter.address:type_name -> common.AddressMatch + 127, // 171: common.QueryFilter.and:type_name -> common.AndFilter + 128, // 172: common.QueryFilter.or:type_name -> common.OrFilter + 129, // 173: common.QueryFilter.not:type_name -> common.NotFilter + 122, // 174: common.QueryFilter.reference:type_name -> common.ReferenceCondition + 125, // 175: common.QueryFilter.builtin_uint:type_name -> common.BuiltinUintCondition + 123, // 176: common.QueryFilter.ledger:type_name -> common.LedgerCondition + 124, // 177: common.QueryFilter.log_id:type_name -> common.LogIdCondition + 126, // 178: common.QueryFilter.log_builtin_uint:type_name -> common.LogBuiltinUintCondition + 132, // 179: common.ReferenceCondition.cond:type_name -> common.StringCondition + 132, // 180: common.LedgerCondition.cond:type_name -> common.StringCondition + 134, // 181: common.LogIdCondition.cond:type_name -> common.UintCondition + 3, // 182: common.BuiltinUintCondition.field:type_name -> common.TransactionBuiltinIndex + 134, // 183: common.BuiltinUintCondition.cond:type_name -> common.UintCondition + 5, // 184: common.LogBuiltinUintCondition.field:type_name -> common.LogBuiltinIndex + 134, // 185: common.LogBuiltinUintCondition.cond:type_name -> common.UintCondition + 121, // 186: common.AndFilter.filters:type_name -> common.QueryFilter + 121, // 187: common.OrFilter.filters:type_name -> common.QueryFilter + 121, // 188: common.NotFilter.filter:type_name -> common.QueryFilter + 130, // 189: common.FieldCondition.field:type_name -> common.FieldRef + 132, // 190: common.FieldCondition.string_cond:type_name -> common.StringCondition + 133, // 191: common.FieldCondition.int_cond:type_name -> common.IntCondition + 134, // 192: common.FieldCondition.uint_cond:type_name -> common.UintCondition + 135, // 193: common.FieldCondition.bool_cond:type_name -> common.BoolCondition + 136, // 194: common.FieldCondition.exists_cond:type_name -> common.ExistsCondition + 14, // 195: common.AddressMatch.role:type_name -> common.AddressRole + 121, // 196: common.PreparedQuery.filter:type_name -> common.QueryFilter + 15, // 197: common.PreparedQuery.target:type_name -> common.QueryTarget + 22, // 198: common.AggregatedVolume.input:type_name -> common.Uint256 + 22, // 199: common.AggregatedVolume.output:type_name -> common.Uint256 + 139, // 200: common.AggregateResult.volumes:type_name -> common.AggregatedVolume + 141, // 201: common.AggregateResult.groups:type_name -> common.GroupedAggregateResult + 139, // 202: common.GroupedAggregateResult.volumes:type_name -> common.AggregatedVolume + 32, // 203: common.PreparedQueryCursor.account_data:type_name -> common.Account + 24, // 204: common.PreparedQueryCursor.transaction_data:type_name -> common.Transaction + 145, // 205: common.CallerSnapshot.identity:type_name -> common.CallerIdentity + 147, // 206: common.BackupStorage.s3:type_name -> common.S3StorageConfig + 148, // 207: common.BackupStorage.azure:type_name -> common.AzureStorageConfig + 150, // 208: common.ListOptions.read:type_name -> common.ReadOptions + 121, // 209: common.ListOptions.filter:type_name -> common.QueryFilter + 19, // 210: common.MetadataMap.ValuesEntry.value:type_name -> common.MetadataValue + 19, // 211: common.Transaction.MetadataEntry.value:type_name -> common.MetadataValue + 28, // 212: common.PostCommitVolumes.VolumesByAccountEntry.value:type_name -> common.VolumesByAssets + 19, // 213: common.Account.MetadataEntry.value:type_name -> common.MetadataValue + 35, // 214: common.MetadataSchema.AccountFieldsEntry.value:type_name -> common.MetadataFieldSchema + 35, // 215: common.MetadataSchema.TransactionFieldsEntry.value:type_name -> common.MetadataFieldSchema + 35, // 216: common.MetadataSchema.LedgerFieldsEntry.value:type_name -> common.MetadataFieldSchema + 19, // 217: common.SavedLedgerMetadataLog.MetadataEntry.value:type_name -> common.MetadataValue + 117, // 218: common.CreatedLedgerLog.AccountTypesEntry.value:type_name -> common.AccountType + 20, // 219: common.CreatedTransaction.AccountMetadataEntry.value:type_name -> common.MetadataMap + 19, // 220: common.SavedMetadata.MetadataEntry.value:type_name -> common.MetadataValue + 117, // 221: common.LedgerInfo.AccountTypesEntry.value:type_name -> common.AccountType + 19, // 222: common.LedgerInfo.MetadataEntry.value:type_name -> common.MetadataValue + 19, // 223: common.SaveMetadataCommand.MetadataEntry.value:type_name -> common.MetadataValue + 19, // 224: common.TransactionState.MetadataEntry.value:type_name -> common.MetadataValue + 113, // 225: common.AccountType.SegmentTypesEntry.value:type_name -> common.SegmentType + 226, // [226:226] is the sub-list for method output_type + 226, // [226:226] is the sub-list for method input_type + 226, // [226:226] is the sub-list for extension type_name + 226, // [226:226] is the sub-list for extension extendee + 0, // [0:226] is the sub-list for field type_name } func init() { file_common_proto_init() } From 2c7f646c65a8ae2991c79f516f6af6e8c679837d Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 25 Jun 2026 13:28:19 +0200 Subject: [PATCH 4/9] chore: post-rebase proto regen after Index registry extraction (#453) --- internal/proto/commonpb/common.pb.go | 211 +++++++++++++++------------ 1 file changed, 114 insertions(+), 97 deletions(-) diff --git a/internal/proto/commonpb/common.pb.go b/internal/proto/commonpb/common.pb.go index c4be75aec9..b796537ff8 100644 --- a/internal/proto/commonpb/common.pb.go +++ b/internal/proto/commonpb/common.pb.go @@ -2663,8 +2663,14 @@ func (*IndexID_AccountBuiltin) isIndexID_Kind() {} func (*IndexID_Metadata) isIndexID_Kind() {} -// Index is the first-class representation of an index on a ledger. -// Holds its identifier plus build state and audit metadata. +// Index is the first-class representation of an index in the bucket-scoped +// index registry. Holds its identifier, scope, build state and audit metadata. +// +// Scope: +// - ledger == "" → bucket-scoped (e.g. audit indexes), single entry across the bucket. +// - ledger == "X" → ledger-scoped, one entry per (ledger, IndexID) tuple. +// +// Entries live in the SubAttrIndex attribute zone, not in LedgerInfo. type Index struct { state protoimpl.MessageState `protogen:"open.v1"` Id *IndexID `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -2672,6 +2678,7 @@ type Index struct { CreatedAt *Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` LastBuiltAt *Timestamp `protobuf:"bytes,4,opt,name=last_built_at,json=lastBuiltAt,proto3" json:"last_built_at,omitempty"` LastError string `protobuf:"bytes,5,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` + Ledger string `protobuf:"bytes,6,opt,name=ledger,proto3" json:"ledger,omitempty"` // empty for bucket-scoped indexes unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2741,6 +2748,13 @@ func (x *Index) GetLastError() string { return "" } +func (x *Index) GetLedger() string { + if x != nil { + return x.Ledger + } + return "" +} + type Idempotency struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` @@ -3857,6 +3871,7 @@ type ClusterConfig struct { HashAlgorithm HashAlgorithm `protobuf:"varint,11,opt,name=hash_algorithm,json=hashAlgorithm,proto3,enum=common.HashAlgorithm" json:"hash_algorithm,omitempty"` BloomLedgerMetadata *BloomTypeConfig `protobuf:"bytes,12,opt,name=bloom_ledger_metadata,json=bloomLedgerMetadata,proto3" json:"bloom_ledger_metadata,omitempty"` BloomPreparedQueries *BloomTypeConfig `protobuf:"bytes,13,opt,name=bloom_prepared_queries,json=bloomPreparedQueries,proto3" json:"bloom_prepared_queries,omitempty"` + BloomIndexes *BloomTypeConfig `protobuf:"bytes,14,opt,name=bloom_indexes,json=bloomIndexes,proto3" json:"bloom_indexes,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -3982,6 +3997,13 @@ func (x *ClusterConfig) GetBloomPreparedQueries() *BloomTypeConfig { return nil } +func (x *ClusterConfig) GetBloomIndexes() *BloomTypeConfig { + if x != nil { + return x.BloomIndexes + } + return nil +} + // PersistedClusterState wraps ClusterConfig with internal FSM state that must // be persisted for deterministic restore but is not part of the config proposal. type PersistedClusterState struct { @@ -7247,7 +7269,10 @@ func (x *MirrorSyncProgress) GetError() *MirrorSyncError { return nil } -// LedgerInfo represents information about a ledger +// LedgerInfo represents information about a ledger. +// +// Indexes do not live here: they are projected through the bucket-scoped +// SubAttrIndex registry and surfaced via BucketService.ListIndexes. type LedgerInfo struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Ledger name @@ -7261,7 +7286,6 @@ type LedgerInfo struct { DefaultEnforcementMode ChartEnforcementMode `protobuf:"varint,11,opt,name=default_enforcement_mode,json=defaultEnforcementMode,proto3,enum=common.ChartEnforcementMode" json:"default_enforcement_mode,omitempty"` // Default enforcement for unmatched accounts Metadata map[string]*MetadataValue `protobuf:"bytes,12,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Populated at read time from separate attribute store Id uint32 `protobuf:"varint,13,opt,name=id,proto3" json:"id,omitempty"` // Unique numeric ledger ID (assigned by FSM, used as Pebble key prefix) - Indexes []*Index `protobuf:"bytes,14,rep,name=indexes,proto3" json:"indexes,omitempty"` // All indexes defined on this ledger unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -7373,13 +7397,6 @@ func (x *LedgerInfo) GetId() uint32 { return 0 } -func (x *LedgerInfo) GetIndexes() []*Index { - if x != nil { - return x.Indexes - } - return nil -} - // SaveMetadataCommand is used for adding metadata type SaveMetadataCommand struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -10706,7 +10723,7 @@ const file_common_proto_rawDesc = "" + "logBuiltin\x12F\n" + "\x0faccount_builtin\x18\x03 \x01(\x0e2\x1b.common.AccountBuiltinIndexH\x00R\x0eaccountBuiltin\x125\n" + "\bmetadata\x18\x04 \x01(\v2\x17.common.MetadataIndexIDH\x00R\bmetadataB\x06\n" + - "\x04kind\"\xed\x01\n" + + "\x04kind\"\x85\x02\n" + "\x05Index\x12\x1f\n" + "\x02id\x18\x01 \x01(\v2\x0f.common.IndexIDR\x02id\x12;\n" + "\fbuild_status\x18\x02 \x01(\x0e2\x18.common.IndexBuildStatusR\vbuildStatus\x120\n" + @@ -10714,7 +10731,8 @@ const file_common_proto_rawDesc = "" + "created_at\x18\x03 \x01(\v2\x11.common.TimestampR\tcreatedAt\x125\n" + "\rlast_built_at\x18\x04 \x01(\v2\x11.common.TimestampR\vlastBuiltAt\x12\x1d\n" + "\n" + - "last_error\x18\x05 \x01(\tR\tlastError\"\x1f\n" + + "last_error\x18\x05 \x01(\tR\tlastError\x12\x16\n" + + "\x06ledger\x18\x06 \x01(\tR\x06ledger\"\x1f\n" + "\vIdempotency\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\"=\n" + "\x10IdempotencyEntry\x12\x12\n" + @@ -10782,7 +10800,7 @@ const file_common_proto_rawDesc = "" + "\aenabled\x18\x01 \x01(\bR\aenabled\"O\n" + "\x0fBloomTypeConfig\x12#\n" + "\rexpected_keys\x18\x01 \x01(\x04R\fexpectedKeys\x12\x17\n" + - "\afp_rate\x18\x02 \x01(\x01R\x06fpRate\"\x91\a\n" + + "\afp_rate\x18\x02 \x01(\x01R\x06fpRate\"\xcf\a\n" + "\rClusterConfig\x12-\n" + "\x12rotation_threshold\x18\x01 \x01(\x04R\x11rotationThreshold\x12<\n" + "\rbloom_volumes\x18\x02 \x01(\v2\x17.common.BloomTypeConfigR\fbloomVolumes\x12>\n" + @@ -10797,7 +10815,8 @@ const file_common_proto_rawDesc = "" + " \x01(\v2\x17.common.BloomTypeConfigR\x16bloomNumscriptContents\x12<\n" + "\x0ehash_algorithm\x18\v \x01(\x0e2\x15.common.HashAlgorithmR\rhashAlgorithm\x12K\n" + "\x15bloom_ledger_metadata\x18\f \x01(\v2\x17.common.BloomTypeConfigR\x13bloomLedgerMetadata\x12M\n" + - "\x16bloom_prepared_queries\x18\r \x01(\v2\x17.common.BloomTypeConfigR\x14bloomPreparedQueries\"g\n" + + "\x16bloom_prepared_queries\x18\r \x01(\v2\x17.common.BloomTypeConfigR\x14bloomPreparedQueries\x12<\n" + + "\rbloom_indexes\x18\x0e \x01(\v2\x17.common.BloomTypeConfigR\fbloomIndexes\"g\n" + "\x15PersistedClusterState\x12-\n" + "\x06config\x18\x01 \x01(\v2\x15.common.ClusterConfigR\x06config\x12\x1f\n" + "\vcache_epoch\x18\x02 \x01(\x06R\n" + @@ -11040,7 +11059,7 @@ const file_common_proto_rawDesc = "" + "\x06cursor\x18\x02 \x01(\x06R\x06cursor\x12(\n" + "\x10source_log_count\x18\x03 \x01(\x06R\x0esourceLogCount\x12%\n" + "\x0eremaining_logs\x18\x04 \x01(\x06R\rremainingLogs\x12-\n" + - "\x05error\x18\x05 \x01(\v2\x17.common.MirrorSyncErrorR\x05error\"\xf2\x06\n" + + "\x05error\x18\x05 \x01(\v2\x17.common.MirrorSyncErrorR\x05error\"\x97\x06\n" + "\n" + "LedgerInfo\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x120\n" + @@ -11056,15 +11075,13 @@ const file_common_proto_rawDesc = "" + " \x03(\v2$.common.LedgerInfo.AccountTypesEntryR\faccountTypes\x12V\n" + "\x18default_enforcement_mode\x18\v \x01(\x0e2\x1c.common.ChartEnforcementModeR\x16defaultEnforcementMode\x12<\n" + "\bmetadata\x18\f \x03(\v2 .common.LedgerInfo.MetadataEntryR\bmetadata\x12\x0e\n" + - "\x02id\x18\r \x01(\rR\x02id\x12'\n" + - "\aindexes\x18\x0e \x03(\v2\r.common.IndexR\aindexes\x1aT\n" + + "\x02id\x18\r \x01(\rR\x02id\x1aT\n" + "\x11AccountTypesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12)\n" + "\x05value\x18\x02 \x01(\v2\x13.common.AccountTypeR\x05value:\x028\x01\x1aR\n" + "\rMetadataEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12+\n" + - "\x05value\x18\x02 \x01(\v2\x15.common.MetadataValueR\x05value:\x028\x01J\x04\b\b\x10\tJ\x04\b\t\x10\n" + - "R\x0fbuiltin_indexesR\x13log_builtin_indexes\"\xd8\x01\n" + + "\x05value\x18\x02 \x01(\v2\x15.common.MetadataValueR\x05value:\x028\x01\"\xd8\x01\n" + "\x13SaveMetadataCommand\x12&\n" + "\x06target\x18\x01 \x01(\v2\x0e.common.TargetR\x06target\x12E\n" + "\bmetadata\x18\x02 \x03(\v2).common.SaveMetadataCommand.MetadataEntryR\bmetadata\x1aR\n" + @@ -11687,83 +11704,83 @@ var file_common_proto_depIdxs = []int32{ 6, // 74: common.ClusterConfig.hash_algorithm:type_name -> common.HashAlgorithm 53, // 75: common.ClusterConfig.bloom_ledger_metadata:type_name -> common.BloomTypeConfig 53, // 76: common.ClusterConfig.bloom_prepared_queries:type_name -> common.BloomTypeConfig - 54, // 77: common.PersistedClusterState.config:type_name -> common.ClusterConfig - 138, // 78: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery - 121, // 79: common.UpdatedPreparedQueryLog.previous_filter:type_name -> common.QueryFilter - 121, // 80: common.UpdatedPreparedQueryLog.new_filter:type_name -> common.QueryFilter - 160, // 81: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry - 17, // 82: common.NumscriptInfo.created_at:type_name -> common.Timestamp - 63, // 83: common.SavedNumscriptLog.info:type_name -> common.NumscriptInfo - 73, // 84: common.SinkConfig.nats:type_name -> common.NatsSinkConfig - 74, // 85: common.SinkConfig.clickhouse:type_name -> common.ClickHouseSinkConfig - 75, // 86: common.SinkConfig.kafka:type_name -> common.KafkaSinkConfig - 76, // 87: common.SinkConfig.http:type_name -> common.HttpSinkConfig - 77, // 88: common.SinkConfig.databricks:type_name -> common.DatabricksSinkConfig - 7, // 89: common.SinkConfig.event_types:type_name -> common.EventType - 72, // 90: common.SinkStatus.error:type_name -> common.SinkError - 17, // 91: common.SinkError.occurred_at:type_name -> common.Timestamp - 78, // 92: common.DatabricksSinkConfig.oauth_m2m:type_name -> common.DatabricksOAuthM2M - 17, // 93: common.CreatedLedgerLog.created_at:type_name -> common.Timestamp - 36, // 94: common.CreatedLedgerLog.metadata_schema:type_name -> common.MetadataSchema - 9, // 95: common.CreatedLedgerLog.mode:type_name -> common.LedgerMode - 99, // 96: common.CreatedLedgerLog.mirror_source:type_name -> common.MirrorSourceConfig - 161, // 97: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry - 12, // 98: common.CreatedLedgerLog.default_enforcement_mode:type_name -> common.ChartEnforcementMode - 17, // 99: common.DeletedLedgerLog.deleted_at:type_name -> common.Timestamp - 82, // 100: common.ApplyLedgerLog.log:type_name -> common.LedgerLog - 84, // 101: common.LedgerLog.data:type_name -> common.LedgerLogPayload - 17, // 102: common.LedgerLog.date:type_name -> common.Timestamp - 83, // 103: common.LedgerLog.purged_volumes:type_name -> common.TouchedVolume - 88, // 104: common.LedgerLogPayload.created_transaction:type_name -> common.CreatedTransaction - 89, // 105: common.LedgerLogPayload.reverted_transaction:type_name -> common.RevertedTransaction - 90, // 106: common.LedgerLogPayload.saved_metadata:type_name -> common.SavedMetadata - 91, // 107: common.LedgerLogPayload.deleted_metadata:type_name -> common.DeletedMetadata - 92, // 108: common.LedgerLogPayload.set_metadata_field_type:type_name -> common.SetMetadataFieldTypeLog - 93, // 109: common.LedgerLogPayload.removed_metadata_field_type:type_name -> common.RemovedMetadataFieldTypeLog - 87, // 110: common.LedgerLogPayload.fill_gap:type_name -> common.FilledGapLog - 85, // 111: common.LedgerLogPayload.create_index:type_name -> common.CreatedIndexLog - 86, // 112: common.LedgerLogPayload.drop_index:type_name -> common.DroppedIndexLog - 118, // 113: common.LedgerLogPayload.added_account_type:type_name -> common.AddedAccountTypeLog - 119, // 114: common.LedgerLogPayload.removed_account_type:type_name -> common.RemovedAccountTypeLog - 120, // 115: common.LedgerLogPayload.updated_default_enforcement_mode:type_name -> common.UpdatedDefaultEnforcementModeLog - 39, // 116: common.CreatedIndexLog.id:type_name -> common.IndexID - 39, // 117: common.DroppedIndexLog.id:type_name -> common.IndexID - 24, // 118: common.CreatedTransaction.transaction:type_name -> common.Transaction - 162, // 119: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry - 30, // 120: common.CreatedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes - 24, // 121: common.RevertedTransaction.revert_transaction:type_name -> common.Transaction - 30, // 122: common.RevertedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes - 34, // 123: common.SavedMetadata.target:type_name -> common.Target - 163, // 124: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry - 34, // 125: common.DeletedMetadata.target:type_name -> common.Target - 0, // 126: common.SetMetadataFieldTypeLog.target_type:type_name -> common.TargetType - 1, // 127: common.SetMetadataFieldTypeLog.type:type_name -> common.MetadataType - 0, // 128: common.RemovedMetadataFieldTypeLog.target_type:type_name -> common.TargetType - 39, // 129: common.RemovedMetadataFieldTypeLog.dropped_index:type_name -> common.IndexID - 17, // 130: common.Chapter.start:type_name -> common.Timestamp - 17, // 131: common.Chapter.end:type_name -> common.Timestamp - 8, // 132: common.Chapter.status:type_name -> common.ChapterStatus - 94, // 133: common.ClosedChapterLog.closed_chapter:type_name -> common.Chapter - 94, // 134: common.ClosedChapterLog.new_chapter:type_name -> common.Chapter - 94, // 135: common.SealedChapterLog.chapter:type_name -> common.Chapter - 94, // 136: common.ArchivedChapterLog.chapter:type_name -> common.Chapter - 94, // 137: common.ConfirmedArchiveChapterLog.chapter:type_name -> common.Chapter - 100, // 138: common.MirrorSourceConfig.http:type_name -> common.HttpMirrorSourceConfig - 102, // 139: common.MirrorSourceConfig.postgres:type_name -> common.PostgresMirrorSourceConfig - 101, // 140: common.HttpMirrorSourceConfig.oauth2_client_credentials:type_name -> common.OAuth2ClientCredentials - 17, // 141: common.MirrorSyncError.occurred_at:type_name -> common.Timestamp - 10, // 142: common.MirrorSyncProgress.state:type_name -> common.MirrorSyncState - 103, // 143: common.MirrorSyncProgress.error:type_name -> common.MirrorSyncError - 17, // 144: common.LedgerInfo.created_at:type_name -> common.Timestamp - 17, // 145: common.LedgerInfo.deleted_at:type_name -> common.Timestamp - 36, // 146: common.LedgerInfo.metadata_schema:type_name -> common.MetadataSchema - 9, // 147: common.LedgerInfo.mode:type_name -> common.LedgerMode - 99, // 148: common.LedgerInfo.mirror_source:type_name -> common.MirrorSourceConfig - 104, // 149: common.LedgerInfo.mirror_sync_progress:type_name -> common.MirrorSyncProgress - 164, // 150: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry - 12, // 151: common.LedgerInfo.default_enforcement_mode:type_name -> common.ChartEnforcementMode - 165, // 152: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry - 40, // 153: common.LedgerInfo.indexes:type_name -> common.Index + 53, // 77: common.ClusterConfig.bloom_indexes:type_name -> common.BloomTypeConfig + 54, // 78: common.PersistedClusterState.config:type_name -> common.ClusterConfig + 138, // 79: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery + 121, // 80: common.UpdatedPreparedQueryLog.previous_filter:type_name -> common.QueryFilter + 121, // 81: common.UpdatedPreparedQueryLog.new_filter:type_name -> common.QueryFilter + 160, // 82: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry + 17, // 83: common.NumscriptInfo.created_at:type_name -> common.Timestamp + 63, // 84: common.SavedNumscriptLog.info:type_name -> common.NumscriptInfo + 73, // 85: common.SinkConfig.nats:type_name -> common.NatsSinkConfig + 74, // 86: common.SinkConfig.clickhouse:type_name -> common.ClickHouseSinkConfig + 75, // 87: common.SinkConfig.kafka:type_name -> common.KafkaSinkConfig + 76, // 88: common.SinkConfig.http:type_name -> common.HttpSinkConfig + 77, // 89: common.SinkConfig.databricks:type_name -> common.DatabricksSinkConfig + 7, // 90: common.SinkConfig.event_types:type_name -> common.EventType + 72, // 91: common.SinkStatus.error:type_name -> common.SinkError + 17, // 92: common.SinkError.occurred_at:type_name -> common.Timestamp + 78, // 93: common.DatabricksSinkConfig.oauth_m2m:type_name -> common.DatabricksOAuthM2M + 17, // 94: common.CreatedLedgerLog.created_at:type_name -> common.Timestamp + 36, // 95: common.CreatedLedgerLog.metadata_schema:type_name -> common.MetadataSchema + 9, // 96: common.CreatedLedgerLog.mode:type_name -> common.LedgerMode + 99, // 97: common.CreatedLedgerLog.mirror_source:type_name -> common.MirrorSourceConfig + 161, // 98: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry + 12, // 99: common.CreatedLedgerLog.default_enforcement_mode:type_name -> common.ChartEnforcementMode + 17, // 100: common.DeletedLedgerLog.deleted_at:type_name -> common.Timestamp + 82, // 101: common.ApplyLedgerLog.log:type_name -> common.LedgerLog + 84, // 102: common.LedgerLog.data:type_name -> common.LedgerLogPayload + 17, // 103: common.LedgerLog.date:type_name -> common.Timestamp + 83, // 104: common.LedgerLog.purged_volumes:type_name -> common.TouchedVolume + 88, // 105: common.LedgerLogPayload.created_transaction:type_name -> common.CreatedTransaction + 89, // 106: common.LedgerLogPayload.reverted_transaction:type_name -> common.RevertedTransaction + 90, // 107: common.LedgerLogPayload.saved_metadata:type_name -> common.SavedMetadata + 91, // 108: common.LedgerLogPayload.deleted_metadata:type_name -> common.DeletedMetadata + 92, // 109: common.LedgerLogPayload.set_metadata_field_type:type_name -> common.SetMetadataFieldTypeLog + 93, // 110: common.LedgerLogPayload.removed_metadata_field_type:type_name -> common.RemovedMetadataFieldTypeLog + 87, // 111: common.LedgerLogPayload.fill_gap:type_name -> common.FilledGapLog + 85, // 112: common.LedgerLogPayload.create_index:type_name -> common.CreatedIndexLog + 86, // 113: common.LedgerLogPayload.drop_index:type_name -> common.DroppedIndexLog + 118, // 114: common.LedgerLogPayload.added_account_type:type_name -> common.AddedAccountTypeLog + 119, // 115: common.LedgerLogPayload.removed_account_type:type_name -> common.RemovedAccountTypeLog + 120, // 116: common.LedgerLogPayload.updated_default_enforcement_mode:type_name -> common.UpdatedDefaultEnforcementModeLog + 39, // 117: common.CreatedIndexLog.id:type_name -> common.IndexID + 39, // 118: common.DroppedIndexLog.id:type_name -> common.IndexID + 24, // 119: common.CreatedTransaction.transaction:type_name -> common.Transaction + 162, // 120: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry + 30, // 121: common.CreatedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes + 24, // 122: common.RevertedTransaction.revert_transaction:type_name -> common.Transaction + 30, // 123: common.RevertedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes + 34, // 124: common.SavedMetadata.target:type_name -> common.Target + 163, // 125: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry + 34, // 126: common.DeletedMetadata.target:type_name -> common.Target + 0, // 127: common.SetMetadataFieldTypeLog.target_type:type_name -> common.TargetType + 1, // 128: common.SetMetadataFieldTypeLog.type:type_name -> common.MetadataType + 0, // 129: common.RemovedMetadataFieldTypeLog.target_type:type_name -> common.TargetType + 39, // 130: common.RemovedMetadataFieldTypeLog.dropped_index:type_name -> common.IndexID + 17, // 131: common.Chapter.start:type_name -> common.Timestamp + 17, // 132: common.Chapter.end:type_name -> common.Timestamp + 8, // 133: common.Chapter.status:type_name -> common.ChapterStatus + 94, // 134: common.ClosedChapterLog.closed_chapter:type_name -> common.Chapter + 94, // 135: common.ClosedChapterLog.new_chapter:type_name -> common.Chapter + 94, // 136: common.SealedChapterLog.chapter:type_name -> common.Chapter + 94, // 137: common.ArchivedChapterLog.chapter:type_name -> common.Chapter + 94, // 138: common.ConfirmedArchiveChapterLog.chapter:type_name -> common.Chapter + 100, // 139: common.MirrorSourceConfig.http:type_name -> common.HttpMirrorSourceConfig + 102, // 140: common.MirrorSourceConfig.postgres:type_name -> common.PostgresMirrorSourceConfig + 101, // 141: common.HttpMirrorSourceConfig.oauth2_client_credentials:type_name -> common.OAuth2ClientCredentials + 17, // 142: common.MirrorSyncError.occurred_at:type_name -> common.Timestamp + 10, // 143: common.MirrorSyncProgress.state:type_name -> common.MirrorSyncState + 103, // 144: common.MirrorSyncProgress.error:type_name -> common.MirrorSyncError + 17, // 145: common.LedgerInfo.created_at:type_name -> common.Timestamp + 17, // 146: common.LedgerInfo.deleted_at:type_name -> common.Timestamp + 36, // 147: common.LedgerInfo.metadata_schema:type_name -> common.MetadataSchema + 9, // 148: common.LedgerInfo.mode:type_name -> common.LedgerMode + 99, // 149: common.LedgerInfo.mirror_source:type_name -> common.MirrorSourceConfig + 104, // 150: common.LedgerInfo.mirror_sync_progress:type_name -> common.MirrorSyncProgress + 164, // 151: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry + 12, // 152: common.LedgerInfo.default_enforcement_mode:type_name -> common.ChartEnforcementMode + 165, // 153: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry 34, // 154: common.SaveMetadataCommand.target:type_name -> common.Target 166, // 155: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry 34, // 156: common.DeleteMetadataCommand.target:type_name -> common.Target From ed4b9ef3e726d89585278091bae965aeecf54f68 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 25 Jun 2026 16:33:59 +0200 Subject: [PATCH 5/9] chore: post-rebase proto regen after per-replica forward index (#553) --- internal/proto/commonpb/common.pb.go | 44 +++++++++++++++++++++------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/internal/proto/commonpb/common.pb.go b/internal/proto/commonpb/common.pb.go index b796537ff8..1846adc964 100644 --- a/internal/proto/commonpb/common.pb.go +++ b/internal/proto/commonpb/common.pb.go @@ -2672,15 +2672,29 @@ func (*IndexID_Metadata) isIndexID_Kind() {} // // Entries live in the SubAttrIndex attribute zone, not in LedgerInfo. type Index struct { - state protoimpl.MessageState `protogen:"open.v1"` - Id *IndexID `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - BuildStatus IndexBuildStatus `protobuf:"varint,2,opt,name=build_status,json=buildStatus,proto3,enum=common.IndexBuildStatus" json:"build_status,omitempty"` - CreatedAt *Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` - LastBuiltAt *Timestamp `protobuf:"bytes,4,opt,name=last_built_at,json=lastBuiltAt,proto3" json:"last_built_at,omitempty"` - LastError string `protobuf:"bytes,5,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` - Ledger string `protobuf:"bytes,6,opt,name=ledger,proto3" json:"ledger,omitempty"` // empty for bucket-scoped indexes - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Id *IndexID `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Informational: tracks whether the FSM has triggered a rebuild + // (e.g. CreateIndex or SetMetadataFieldType). NOT consulted by the + // query path — see forward_encoding_version below. + BuildStatus IndexBuildStatus `protobuf:"varint,2,opt,name=build_status,json=buildStatus,proto3,enum=common.IndexBuildStatus" json:"build_status,omitempty"` + CreatedAt *Timestamp `protobuf:"bytes,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + LastBuiltAt *Timestamp `protobuf:"bytes,4,opt,name=last_built_at,json=lastBuiltAt,proto3" json:"last_built_at,omitempty"` + LastError string `protobuf:"bytes,5,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` + Ledger string `protobuf:"bytes,6,opt,name=ledger,proto3" json:"ledger,omitempty"` // empty for bucket-scoped indexes + // Cluster-wide forward-encoding version. Bumped at every event that + // requires the indexer to rewrite its forward index (CreateIndex, + // SetMetadataFieldType). The per-replica local view of this value + // lives in `readstore.IndexVersionState.CurrentVersion` (internal) + // and is exposed on the wire as `IndexEntry.current_version` on + // `GetIndexStatusResponse`. Queries read from the replica's local + // current_version, not from this cluster-wide field. Synchronization + // is client-driven via min_log_sequence on the read API — but note + // that min_log_sequence pins log application on this replica, NOT + // local rewrite completion; see api-comparison.md for the contract. + ForwardEncodingVersion uint32 `protobuf:"varint,7,opt,name=forward_encoding_version,json=forwardEncodingVersion,proto3" json:"forward_encoding_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Index) Reset() { @@ -2755,6 +2769,13 @@ func (x *Index) GetLedger() string { return "" } +func (x *Index) GetForwardEncodingVersion() uint32 { + if x != nil { + return x.ForwardEncodingVersion + } + return 0 +} + type Idempotency struct { state protoimpl.MessageState `protogen:"open.v1"` Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` @@ -10723,7 +10744,7 @@ const file_common_proto_rawDesc = "" + "logBuiltin\x12F\n" + "\x0faccount_builtin\x18\x03 \x01(\x0e2\x1b.common.AccountBuiltinIndexH\x00R\x0eaccountBuiltin\x125\n" + "\bmetadata\x18\x04 \x01(\v2\x17.common.MetadataIndexIDH\x00R\bmetadataB\x06\n" + - "\x04kind\"\x85\x02\n" + + "\x04kind\"\xbf\x02\n" + "\x05Index\x12\x1f\n" + "\x02id\x18\x01 \x01(\v2\x0f.common.IndexIDR\x02id\x12;\n" + "\fbuild_status\x18\x02 \x01(\x0e2\x18.common.IndexBuildStatusR\vbuildStatus\x120\n" + @@ -10732,7 +10753,8 @@ const file_common_proto_rawDesc = "" + "\rlast_built_at\x18\x04 \x01(\v2\x11.common.TimestampR\vlastBuiltAt\x12\x1d\n" + "\n" + "last_error\x18\x05 \x01(\tR\tlastError\x12\x16\n" + - "\x06ledger\x18\x06 \x01(\tR\x06ledger\"\x1f\n" + + "\x06ledger\x18\x06 \x01(\tR\x06ledger\x128\n" + + "\x18forward_encoding_version\x18\a \x01(\rR\x16forwardEncodingVersion\"\x1f\n" + "\vIdempotency\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\"=\n" + "\x10IdempotencyEntry\x12\x12\n" + From 0cf9af320f3c6d679e8102175eee656150569735 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Thu, 25 Jun 2026 16:41:53 +0200 Subject: [PATCH 6/9] fix(clickhouse): add color column to typed JSON posting schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sink JSON now always emits posting.color (commit 60aef1a18 dropped omitempty for the proto type, b858501db did the same for sinkPosting). The ClickHouse table DDL still declared posting columns as source/destination/amount/asset only, so colored postings would either be rejected by strict JSON typing or land in untyped/dynamic storage depending on ClickHouse settings. Add the color column so warehouse queries can reconstruct the segregated buckets exactly as the API surfaces them. Databricks is unaffected — its DDL uses STRING for the whole data column. Addresses NumaryBot review on sink_data_common.go:73. --- internal/application/events/clickhouse_data_test.go | 4 ++++ internal/application/events/sink_clickhouse.go | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/application/events/clickhouse_data_test.go b/internal/application/events/clickhouse_data_test.go index 0462161b8a..46e3849593 100644 --- a/internal/application/events/clickhouse_data_test.go +++ b/internal/application/events/clickhouse_data_test.go @@ -706,6 +706,10 @@ func TestClickHouseCreateTableDDL(t *testing.T) { require.Contains(t, ddl, "CREATE TABLE IF NOT EXISTS test_events") require.Contains(t, ddl, "log_sequence UInt64") require.Contains(t, ddl, "MergeTree()") + // color is part of the posting sub-schema so warehouse queries can + // reconstruct the segregated buckets — see sinkPosting.MarshalJSON. + require.Contains(t, ddl, "color String", + "ClickHouse posting columns must include color to match the sink JSON shape") } func TestSinkTime_MarshalJSON(t *testing.T) { diff --git a/internal/application/events/sink_clickhouse.go b/internal/application/events/sink_clickhouse.go index d242b7eef8..b2b7cd7dd0 100644 --- a/internal/application/events/sink_clickhouse.go +++ b/internal/application/events/sink_clickhouse.go @@ -36,7 +36,8 @@ const clickhouseTransactionColumns = `JSON( source String, destination String, amount UInt256, - asset String + asset String, + color String )), metadata Map(String, String), reference Nullable(String), From 217f69643173894fb65609802d0a3f7e6288cf94 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Fri, 26 Jun 2026 09:46:36 +0200 Subject: [PATCH 7/9] fix(antithesis): migrate workload to flat Account/PostCommitVolumes shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The antithesis workload module replaces ledger via go.mod, so it consumes our proto directly. The volume API changed from map to repeated entries: - reads.go accountAssetVolumes: switch to acct.FindVolume(asset, ""). - validate.go postCommitVolume: scan VolumesByAssets.volumes for the (asset, color="") tuple. The workload only ever exercises uncolored postings, so both call sites match the uncolored bucket explicitly — colored buckets stay out of scope for this driver model. Addresses NumaryBot reviews on common.proto:109 and :148. --- .../cmds/model/singleton_driver_model/reads.go | 8 +++++--- .../model/singleton_driver_model/validate.go | 17 ++++++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/antithesis/workload/bin/cmds/model/singleton_driver_model/reads.go b/tests/antithesis/workload/bin/cmds/model/singleton_driver_model/reads.go index 8105541e96..6b874faec1 100644 --- a/tests/antithesis/workload/bin/cmds/model/singleton_driver_model/reads.go +++ b/tests/antithesis/workload/bin/cmds/model/singleton_driver_model/reads.go @@ -122,14 +122,16 @@ func pickCell(g GlobalState) (ledger, addr, asset string, ok bool) { } // accountAssetVolumes extracts (input, output) for one asset from a GetAccount -// response. found=false when the asset entry is missing. +// response. The workload only ever exercises uncolored postings, so we look +// up the uncolored bucket (color="") explicitly — colored buckets are out of +// scope for this driver model. found=false when the bucket is missing. func accountAssetVolumes(acct *commonpb.Account, asset string) (in, out uint256.Int, found bool) { if acct == nil { return in, out, false } - v, ok := acct.GetVolumes()[asset] - if !ok { + v := acct.FindVolume(asset, "") + if v == nil { return in, out, false } diff --git a/tests/antithesis/workload/bin/cmds/model/singleton_driver_model/validate.go b/tests/antithesis/workload/bin/cmds/model/singleton_driver_model/validate.go index f2a2e22f71..f781274605 100644 --- a/tests/antithesis/workload/bin/cmds/model/singleton_driver_model/validate.go +++ b/tests/antithesis/workload/bin/cmds/model/singleton_driver_model/validate.go @@ -530,15 +530,26 @@ func metaMapEqual(a, b map[string]*commonpb.MetadataValue) bool { // postCommitVolume extracts (input, output) for one cell from a server response, // parsing the decimal-string volumes into uint256 — the ledger's native volume -// type. ok is false when the cell is absent or the values don't parse. +// type. The workload only ever exercises uncolored postings, so we match the +// uncolored bucket (color="") explicitly — colored buckets are out of scope +// for this driver model. ok is false when the cell is absent or the values +// don't parse. func postCommitVolume(pcv *commonpb.PostCommitVolumes, key VolumeKey) (in, out uint256.Int, ok bool) { byAsset, found := pcv.GetVolumesByAccount()[key.Address] if !found { return in, out, false } - vol, found := byAsset.GetVolumes()[key.Asset] - if !found { + var vol *commonpb.Volumes + for _, entry := range byAsset.GetVolumes() { + if entry.GetAsset() == key.Asset && entry.GetColor() == "" { + vol = entry.GetVolumes() + + break + } + } + + if vol == nil { return in, out, false } From 46fe279ad96af8e18e632bcdbbac1b76b17793bc Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 29 Jun 2026 15:19:54 +0200 Subject: [PATCH 8/9] chore: post-rebase fixups after EN-1378 + EN-868 + #569 + #573 Master landed several major refactors that intersect color-of-money: - #573 (EN-1378): admission now emits Declare for absent volume keys and Scope.Volumes().Get returns ErrNotFound which readVolumeOrZero turns into a fresh zero balance. Drop our explicit ErrBalanceNotPreloaded branches around readVolumeOrZero; replace the no-longer-applicable TestGetBalances_VolumeNotFound_ReturnsError with the new contract test TreatedAsZero. Adapt the numscript adapter accordingly. - #569: Scope per-attribute trios collapsed into generic Accessor[K,V,R]. Switch buildPostCommitVolumes to s.Volumes().Get(...). - #587 (EN-868): business API now mounts under /v3 only. Update the collapseColors HTTP test URL accordingly. - #571 (mockgen helpers): pick up master's expectGetVolume / expectPutVolume helpers in tests. - Fan out the additional cfg/color parameters that processor and indexer signatures accumulated across master commits. --- .../http/handlers_aggregate_volumes_test.go | 2 +- .../application/indexbuilder/backfill_test.go | 18 +- .../processing/processor_mirror_test.go | 12 +- .../processing/processor_posting_test.go | 6 +- .../state/sentinel_undefined_old_test.go | 4 +- internal/proto/commonpb/common.pb.go | 595 +++++++++++------- 6 files changed, 372 insertions(+), 265 deletions(-) diff --git a/internal/adapter/http/handlers_aggregate_volumes_test.go b/internal/adapter/http/handlers_aggregate_volumes_test.go index c8b20a9808..06a73947c0 100644 --- a/internal/adapter/http/handlers_aggregate_volumes_test.go +++ b/internal/adapter/http/handlers_aggregate_volumes_test.go @@ -264,7 +264,7 @@ func TestHandleAggregateVolumes_EmitsColorAlways(t *testing.T) { handler := NewHandler(logging.Testing(), backend, internalauth.AuthConfig{}, version.Info{}) w := httptest.NewRecorder() - r := httptest.NewRequest(http.MethodGet, "/my-ledger/volumes?collapseColors=true", nil) + r := httptest.NewRequest(http.MethodGet, "/v3/my-ledger/volumes?collapseColors=true", nil) handler.ServeHTTP(w, r) diff --git a/internal/application/indexbuilder/backfill_test.go b/internal/application/indexbuilder/backfill_test.go index 0fabdafd16..cd384e15a5 100644 --- a/internal/application/indexbuilder/backfill_test.go +++ b/internal/application/indexbuilder/backfill_test.go @@ -408,7 +408,7 @@ func TestIndexPostingAddressMappingsWritesAccountByAsset(t *testing.T) { // source=accounts:alice dest=accounts:bob asset="USD/2". require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", + b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", "", false, false, false, nil, )) require.NoError(t, b.wb.Flush()) @@ -441,11 +441,11 @@ func TestIndexPostingAddressMappingsAccountByAssetDedup(t *testing.T) { // Feed the same posting twice within the same batch. require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", + b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", "", false, false, false, nil, )) require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 2, "accounts:alice", "accounts:bob", "USD/2", + b.kb, cfg, "test", 2, "accounts:alice", "accounts:bob", "USD/2", "", false, false, false, nil, )) require.NoError(t, b.wb.Flush()) @@ -488,7 +488,7 @@ func TestIndexPostingAddressMappingsAccountByAssetSurvivesInBatchDelete(t *testi batch1 := store.NewBatch() b.initBatch(batch1) require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", + b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", "", false, false, false, nil, )) require.NoError(t, b.wb.Flush()) @@ -502,7 +502,7 @@ func TestIndexPostingAddressMappingsAccountByAssetSurvivesInBatchDelete(t *testi require.NoError(t, readstore.DeleteLedgerIndexes(b.wb.Batch(), "test")) b.markLedgerDeletedInBatch("test") require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 2, "accounts:alice", "accounts:carol", "USD/2", + b.kb, cfg, "test", 2, "accounts:alice", "accounts:carol", "USD/2", "", false, false, false, nil, )) require.NoError(t, b.wb.Flush()) @@ -537,7 +537,7 @@ func TestIndexPostingAddressMappingsAccountByAssetSurvivesInBatchDelete(t *testi // Old generation writes (queued, uncommitted) populate seenAcctAsset ... require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", + b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", "", false, false, false, nil, )) // ... the ledger is deleted in the same batch (range delete queued, @@ -547,7 +547,7 @@ func TestIndexPostingAddressMappingsAccountByAssetSurvivesInBatchDelete(t *testi // ... and the recreated ledger re-touches alice. Without clearing // seenAcctAsset on delete, this Put would be skipped and lost. require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 2, "accounts:alice", "accounts:carol", "USD/2", + b.kb, cfg, "test", 2, "accounts:alice", "accounts:carol", "USD/2", "", false, false, false, nil, )) require.NoError(t, b.wb.Flush()) @@ -584,7 +584,7 @@ func TestIndexPostingAddressMappingsAccountByAssetExcludesTransient(t *testing.T } require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", + b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", "", false, false, false, excludedVolumes, )) require.NoError(t, b.wb.Flush()) @@ -616,7 +616,7 @@ func TestIndexPostingAddressMappingsAccountByAssetDisabled(t *testing.T) { cfg := newLedgerIndexConfig() require.NoError(t, b.indexPostingAddressMappings( - b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", + b.kb, cfg, "test", 1, "accounts:alice", "accounts:bob", "USD/2", "", false, false, false, nil, )) require.NoError(t, b.wb.Flush()) diff --git a/internal/domain/processing/processor_mirror_test.go b/internal/domain/processing/processor_mirror_test.go index fe95e32940..3cfe69626d 100644 --- a/internal/domain/processing/processor_mirror_test.go +++ b/internal/domain/processing/processor_mirror_test.go @@ -110,8 +110,8 @@ func TestMirrorIngest_CreatedTransaction(t *testing.T) { Output: commonpb.NewUint256FromUint64(0), } volumes := setupVolumesStub(mockStore) - volumes.expectGet(domain.NewVolumeKey("mirror-ledger", "world", "USD/2"), zeroVol.AsReader(), nil) - volumes.expectGet(domain.NewVolumeKey("mirror-ledger", "users:001", "USD/2"), zeroVol.AsReader(), nil) + volumes.expectGet(domain.NewVolumeKey("mirror-ledger", "world", "USD/2", ""), zeroVol.AsReader(), nil) + volumes.expectGet(domain.NewVolumeKey("mirror-ledger", "users:001", "USD/2", ""), zeroVol.AsReader(), nil) // Transaction state update expectPutTransactionState(t, mockStore, @@ -402,8 +402,8 @@ func TestMirrorIngest_CreatedTransaction_AbsentVolumes(t *testing.T) { // readVolumeOrZero synthesises a zero balance. expectPutVolume both // wires the stub lazily AND pins that the apply path writes both // fresh balances back through Scope.Volumes().Put. - expectPutVolume(t, mockStore, domain.NewVolumeKey("mirror-ledger", "world", "USD/2"), nil) - expectPutVolume(t, mockStore, domain.NewVolumeKey("mirror-ledger", "users:rare-account", "USD/2"), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("mirror-ledger", "world", "USD/2", ""), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("mirror-ledger", "users:rare-account", "USD/2", ""), nil) expectPutTransactionState(t, mockStore, domain.TransactionKey{LedgerName: "mirror-ledger", ID: 42}, nil) @@ -472,8 +472,8 @@ func TestMirrorIngest_RevertedTransaction_AbsentVolumes(t *testing.T) { mockStore.EXPECT().PutReverted(domain.TransactionKey{LedgerName: "mirror-ledger", ID: 5}, true) expectGetTransactionState(mockStore, domain.TransactionKey{LedgerName: "mirror-ledger", ID: 5}, nil, domain.ErrNotFound) - expectPutVolume(t, mockStore, domain.NewVolumeKey("mirror-ledger", "users:rare-account", "USD/2"), nil) - expectPutVolume(t, mockStore, domain.NewVolumeKey("mirror-ledger", "world", "USD/2"), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("mirror-ledger", "users:rare-account", "USD/2", ""), nil) + expectPutVolume(t, mockStore, domain.NewVolumeKey("mirror-ledger", "world", "USD/2", ""), nil) expectPutTransactionState(t, mockStore, domain.TransactionKey{LedgerName: "mirror-ledger", ID: 42}, nil) expectPutBoundaries(t, mockStore, domain.LedgerKey{Name: "mirror-ledger"}, nil) diff --git a/internal/domain/processing/processor_posting_test.go b/internal/domain/processing/processor_posting_test.go index b048d15b9d..ec5d0e494f 100644 --- a/internal/domain/processing/processor_posting_test.go +++ b/internal/domain/processing/processor_posting_test.go @@ -188,8 +188,8 @@ func TestApplyPosting_AbsentVolumes_TreatedAsZero(t *testing.T) { defer ctrl.Finish() mockStore := NewMockScope(ctrl) - sourceKey := domain.NewVolumeKey("test", "world", "USD") - destKey := domain.NewVolumeKey("test", "users:001", "USD") + sourceKey := domain.NewVolumeKey("test", "world", "USD", "") + destKey := domain.NewVolumeKey("test", "users:001", "USD", "") // Both source (world) and destination are absent in the cache. Apply // must still succeed: world skips the balance check, dest receives the @@ -222,7 +222,7 @@ func TestApplyPosting_AbsentNonWorldSource_InsufficientFunds(t *testing.T) { defer ctrl.Finish() mockStore := NewMockScope(ctrl) - sourceKey := domain.NewVolumeKey("test", "bank", "USD") + sourceKey := domain.NewVolumeKey("test", "bank", "USD", "") // kindStub's default for an unregistered Get is ErrNotFound — exactly // the "absent in cache" state readVolumeOrZero must treat as a zero diff --git a/internal/infra/state/sentinel_undefined_old_test.go b/internal/infra/state/sentinel_undefined_old_test.go index ffac7f9f5b..d36142b279 100644 --- a/internal/infra/state/sentinel_undefined_old_test.go +++ b/internal/infra/state/sentinel_undefined_old_test.go @@ -59,8 +59,8 @@ func TestVerifyVolumeDeltasMatchPostings_UndefinedOldZeroBaseline(t *testing.T) // Use NewVolumeKey so AssetBase / AssetPrecision are pre-parsed and the // keys compare equal to the ones the sentinel rebuilds from postings. - srcKey := domain.NewVolumeKey(ledger, "world", "USD") - dstKey := domain.NewVolumeKey(ledger, "users:bob", "USD") + srcKey := domain.NewVolumeKey(ledger, "world", "USD", "") + dstKey := domain.NewVolumeKey(ledger, "users:bob", "USD", "") updates := []attributes.Update[domain.VolumeKey, *raftcmdpb.VolumePair]{ { diff --git a/internal/proto/commonpb/common.pb.go b/internal/proto/commonpb/common.pb.go index 1846adc964..34576227b3 100644 --- a/internal/proto/commonpb/common.pb.go +++ b/internal/proto/commonpb/common.pb.go @@ -74,43 +74,46 @@ func (TargetType) EnumDescriptor() ([]byte, []int) { type MetadataType int32 const ( - MetadataType_METADATA_TYPE_STRING MetadataType = 0 - MetadataType_METADATA_TYPE_INT64 MetadataType = 1 - MetadataType_METADATA_TYPE_BOOL MetadataType = 2 - MetadataType_METADATA_TYPE_UINT64 MetadataType = 3 - MetadataType_METADATA_TYPE_INT8 MetadataType = 4 - MetadataType_METADATA_TYPE_INT16 MetadataType = 5 - MetadataType_METADATA_TYPE_INT32 MetadataType = 6 - MetadataType_METADATA_TYPE_UINT8 MetadataType = 7 - MetadataType_METADATA_TYPE_UINT16 MetadataType = 8 - MetadataType_METADATA_TYPE_UINT32 MetadataType = 9 + MetadataType_METADATA_TYPE_STRING MetadataType = 0 + MetadataType_METADATA_TYPE_INT64 MetadataType = 1 + MetadataType_METADATA_TYPE_BOOL MetadataType = 2 + MetadataType_METADATA_TYPE_UINT64 MetadataType = 3 + MetadataType_METADATA_TYPE_INT8 MetadataType = 4 + MetadataType_METADATA_TYPE_INT16 MetadataType = 5 + MetadataType_METADATA_TYPE_INT32 MetadataType = 6 + MetadataType_METADATA_TYPE_UINT8 MetadataType = 7 + MetadataType_METADATA_TYPE_UINT16 MetadataType = 8 + MetadataType_METADATA_TYPE_UINT32 MetadataType = 9 + MetadataType_METADATA_TYPE_DATETIME MetadataType = 10 ) // Enum value maps for MetadataType. var ( MetadataType_name = map[int32]string{ - 0: "METADATA_TYPE_STRING", - 1: "METADATA_TYPE_INT64", - 2: "METADATA_TYPE_BOOL", - 3: "METADATA_TYPE_UINT64", - 4: "METADATA_TYPE_INT8", - 5: "METADATA_TYPE_INT16", - 6: "METADATA_TYPE_INT32", - 7: "METADATA_TYPE_UINT8", - 8: "METADATA_TYPE_UINT16", - 9: "METADATA_TYPE_UINT32", + 0: "METADATA_TYPE_STRING", + 1: "METADATA_TYPE_INT64", + 2: "METADATA_TYPE_BOOL", + 3: "METADATA_TYPE_UINT64", + 4: "METADATA_TYPE_INT8", + 5: "METADATA_TYPE_INT16", + 6: "METADATA_TYPE_INT32", + 7: "METADATA_TYPE_UINT8", + 8: "METADATA_TYPE_UINT16", + 9: "METADATA_TYPE_UINT32", + 10: "METADATA_TYPE_DATETIME", } MetadataType_value = map[string]int32{ - "METADATA_TYPE_STRING": 0, - "METADATA_TYPE_INT64": 1, - "METADATA_TYPE_BOOL": 2, - "METADATA_TYPE_UINT64": 3, - "METADATA_TYPE_INT8": 4, - "METADATA_TYPE_INT16": 5, - "METADATA_TYPE_INT32": 6, - "METADATA_TYPE_UINT8": 7, - "METADATA_TYPE_UINT16": 8, - "METADATA_TYPE_UINT32": 9, + "METADATA_TYPE_STRING": 0, + "METADATA_TYPE_INT64": 1, + "METADATA_TYPE_BOOL": 2, + "METADATA_TYPE_UINT64": 3, + "METADATA_TYPE_INT8": 4, + "METADATA_TYPE_INT16": 5, + "METADATA_TYPE_INT32": 6, + "METADATA_TYPE_UINT8": 7, + "METADATA_TYPE_UINT16": 8, + "METADATA_TYPE_UINT32": 9, + "METADATA_TYPE_DATETIME": 10, } ) @@ -257,15 +260,18 @@ type AccountBuiltinIndex int32 const ( AccountBuiltinIndex_ACCT_BUILTIN_INDEX_UNSPECIFIED AccountBuiltinIndex = 0 + AccountBuiltinIndex_ACCT_BUILTIN_INDEX_ASSET AccountBuiltinIndex = 1 ) // Enum value maps for AccountBuiltinIndex. var ( AccountBuiltinIndex_name = map[int32]string{ 0: "ACCT_BUILTIN_INDEX_UNSPECIFIED", + 1: "ACCT_BUILTIN_INDEX_ASSET", } AccountBuiltinIndex_value = map[string]int32{ "ACCT_BUILTIN_INDEX_UNSPECIFIED": 0, + "ACCT_BUILTIN_INDEX_ASSET": 1, } ) @@ -1185,6 +1191,7 @@ type MetadataValue struct { // *MetadataValue_BoolValue // *MetadataValue_NullValue // *MetadataValue_UintValue + // *MetadataValue_DatetimeValue Type isMetadataValue_Type `protobuf_oneof:"type"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -1272,6 +1279,15 @@ func (x *MetadataValue) GetUintValue() uint64 { return 0 } +func (x *MetadataValue) GetDatetimeValue() int64 { + if x != nil { + if x, ok := x.Type.(*MetadataValue_DatetimeValue); ok { + return x.DatetimeValue + } + } + return 0 +} + type isMetadataValue_Type interface { isMetadataValue_Type() } @@ -1296,6 +1312,10 @@ type MetadataValue_UintValue struct { UintValue uint64 `protobuf:"varint,5,opt,name=uint_value,json=uintValue,proto3,oneof"` } +type MetadataValue_DatetimeValue struct { + DatetimeValue int64 `protobuf:"varint,6,opt,name=datetime_value,json=datetimeValue,proto3,oneof"` // microseconds since the Unix epoch (signed; pre-1970 allowed) +} + func (*MetadataValue_StringValue) isMetadataValue_Type() {} func (*MetadataValue_IntValue) isMetadataValue_Type() {} @@ -1306,6 +1326,8 @@ func (*MetadataValue_NullValue) isMetadataValue_Type() {} func (*MetadataValue_UintValue) isMetadataValue_Type() {} +func (*MetadataValue_DatetimeValue) isMetadataValue_Type() {} + // MetadataMap wraps a metadata map for use as a proto map value // (proto3 does not support nested maps like map>). type MetadataMap struct { @@ -8282,6 +8304,7 @@ type QueryFilter struct { // *QueryFilter_Ledger // *QueryFilter_LogId // *QueryFilter_LogBuiltinUint + // *QueryFilter_AccountHasAsset Filter isQueryFilter_Filter `protobuf_oneof:"filter"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache @@ -8414,6 +8437,15 @@ func (x *QueryFilter) GetLogBuiltinUint() *LogBuiltinUintCondition { return nil } +func (x *QueryFilter) GetAccountHasAsset() *AccountHasAssetCondition { + if x != nil { + if x, ok := x.Filter.(*QueryFilter_AccountHasAsset); ok { + return x.AccountHasAsset + } + } + return nil +} + type isQueryFilter_Filter interface { isQueryFilter_Filter() } @@ -8458,6 +8490,10 @@ type QueryFilter_LogBuiltinUint struct { LogBuiltinUint *LogBuiltinUintCondition `protobuf:"bytes,10,opt,name=log_builtin_uint,json=logBuiltinUint,proto3,oneof"` } +type QueryFilter_AccountHasAsset struct { + AccountHasAsset *AccountHasAssetCondition `protobuf:"bytes,11,opt,name=account_has_asset,json=accountHasAsset,proto3,oneof"` +} + func (*QueryFilter_Field) isQueryFilter_Filter() {} func (*QueryFilter_Address) isQueryFilter_Filter() {} @@ -8478,6 +8514,8 @@ func (*QueryFilter_LogId) isQueryFilter_Filter() {} func (*QueryFilter_LogBuiltinUint) isQueryFilter_Filter() {} +func (*QueryFilter_AccountHasAsset) isQueryFilter_Filter() {} + // ReferenceCondition filters transactions by reference (exact match). type ReferenceCondition struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -8719,6 +8757,62 @@ func (x *LogBuiltinUintCondition) GetCond() *UintCondition { return nil } +// AccountHasAssetCondition matches accounts that have ever held a volume cell +// for the given asset (base + precision). Volume-cell presence semantics, not +// balance. Resolved exclusively via the ACCT_BUILTIN_INDEX_ASSET readstore +// index; there is no on-scan fallback. +type AccountHasAssetCondition struct { + state protoimpl.MessageState `protogen:"open.v1"` + AssetBase string `protobuf:"bytes,1,opt,name=asset_base,json=assetBase,proto3" json:"asset_base,omitempty"` + Precision uint32 `protobuf:"varint,2,opt,name=precision,proto3" json:"precision,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccountHasAssetCondition) Reset() { + *x = AccountHasAssetCondition{} + mi := &file_common_proto_msgTypes[110] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccountHasAssetCondition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccountHasAssetCondition) ProtoMessage() {} + +func (x *AccountHasAssetCondition) ProtoReflect() protoreflect.Message { + mi := &file_common_proto_msgTypes[110] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccountHasAssetCondition.ProtoReflect.Descriptor instead. +func (*AccountHasAssetCondition) Descriptor() ([]byte, []int) { + return file_common_proto_rawDescGZIP(), []int{110} +} + +func (x *AccountHasAssetCondition) GetAssetBase() string { + if x != nil { + return x.AssetBase + } + return "" +} + +func (x *AccountHasAssetCondition) GetPrecision() uint32 { + if x != nil { + return x.Precision + } + return 0 +} + type AndFilter struct { state protoimpl.MessageState `protogen:"open.v1"` Filters []*QueryFilter `protobuf:"bytes,1,rep,name=filters,proto3" json:"filters,omitempty"` @@ -8728,7 +8822,7 @@ type AndFilter struct { func (x *AndFilter) Reset() { *x = AndFilter{} - mi := &file_common_proto_msgTypes[110] + mi := &file_common_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8740,7 +8834,7 @@ func (x *AndFilter) String() string { func (*AndFilter) ProtoMessage() {} func (x *AndFilter) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[110] + mi := &file_common_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8753,7 +8847,7 @@ func (x *AndFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use AndFilter.ProtoReflect.Descriptor instead. func (*AndFilter) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{110} + return file_common_proto_rawDescGZIP(), []int{111} } func (x *AndFilter) GetFilters() []*QueryFilter { @@ -8772,7 +8866,7 @@ type OrFilter struct { func (x *OrFilter) Reset() { *x = OrFilter{} - mi := &file_common_proto_msgTypes[111] + mi := &file_common_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8784,7 +8878,7 @@ func (x *OrFilter) String() string { func (*OrFilter) ProtoMessage() {} func (x *OrFilter) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[111] + mi := &file_common_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8797,7 +8891,7 @@ func (x *OrFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use OrFilter.ProtoReflect.Descriptor instead. func (*OrFilter) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{111} + return file_common_proto_rawDescGZIP(), []int{112} } func (x *OrFilter) GetFilters() []*QueryFilter { @@ -8816,7 +8910,7 @@ type NotFilter struct { func (x *NotFilter) Reset() { *x = NotFilter{} - mi := &file_common_proto_msgTypes[112] + mi := &file_common_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8828,7 +8922,7 @@ func (x *NotFilter) String() string { func (*NotFilter) ProtoMessage() {} func (x *NotFilter) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[112] + mi := &file_common_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8841,7 +8935,7 @@ func (x *NotFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use NotFilter.ProtoReflect.Descriptor instead. func (*NotFilter) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{112} + return file_common_proto_rawDescGZIP(), []int{113} } func (x *NotFilter) GetFilter() *QueryFilter { @@ -8863,7 +8957,7 @@ type FieldRef struct { func (x *FieldRef) Reset() { *x = FieldRef{} - mi := &file_common_proto_msgTypes[113] + mi := &file_common_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8875,7 +8969,7 @@ func (x *FieldRef) String() string { func (*FieldRef) ProtoMessage() {} func (x *FieldRef) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[113] + mi := &file_common_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8888,7 +8982,7 @@ func (x *FieldRef) ProtoReflect() protoreflect.Message { // Deprecated: Use FieldRef.ProtoReflect.Descriptor instead. func (*FieldRef) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{113} + return file_common_proto_rawDescGZIP(), []int{114} } func (x *FieldRef) GetMetadata() string { @@ -8916,7 +9010,7 @@ type FieldCondition struct { func (x *FieldCondition) Reset() { *x = FieldCondition{} - mi := &file_common_proto_msgTypes[114] + mi := &file_common_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8928,7 +9022,7 @@ func (x *FieldCondition) String() string { func (*FieldCondition) ProtoMessage() {} func (x *FieldCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[114] + mi := &file_common_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8941,7 +9035,7 @@ func (x *FieldCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use FieldCondition.ProtoReflect.Descriptor instead. func (*FieldCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{114} + return file_common_proto_rawDescGZIP(), []int{115} } func (x *FieldCondition) GetField() *FieldRef { @@ -9050,7 +9144,7 @@ type StringCondition struct { func (x *StringCondition) Reset() { *x = StringCondition{} - mi := &file_common_proto_msgTypes[115] + mi := &file_common_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9062,7 +9156,7 @@ func (x *StringCondition) String() string { func (*StringCondition) ProtoMessage() {} func (x *StringCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[115] + mi := &file_common_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9075,7 +9169,7 @@ func (x *StringCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use StringCondition.ProtoReflect.Descriptor instead. func (*StringCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{115} + return file_common_proto_rawDescGZIP(), []int{116} } func (x *StringCondition) GetValue() isStringCondition_Value { @@ -9133,7 +9227,7 @@ type IntCondition struct { func (x *IntCondition) Reset() { *x = IntCondition{} - mi := &file_common_proto_msgTypes[116] + mi := &file_common_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9145,7 +9239,7 @@ func (x *IntCondition) String() string { func (*IntCondition) ProtoMessage() {} func (x *IntCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[116] + mi := &file_common_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9158,7 +9252,7 @@ func (x *IntCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use IntCondition.ProtoReflect.Descriptor instead. func (*IntCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{116} + return file_common_proto_rawDescGZIP(), []int{117} } func (x *IntCondition) GetMin() int64 { @@ -9217,7 +9311,7 @@ type UintCondition struct { func (x *UintCondition) Reset() { *x = UintCondition{} - mi := &file_common_proto_msgTypes[117] + mi := &file_common_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9229,7 +9323,7 @@ func (x *UintCondition) String() string { func (*UintCondition) ProtoMessage() {} func (x *UintCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[117] + mi := &file_common_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9242,7 +9336,7 @@ func (x *UintCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use UintCondition.ProtoReflect.Descriptor instead. func (*UintCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{117} + return file_common_proto_rawDescGZIP(), []int{118} } func (x *UintCondition) GetMin() uint64 { @@ -9300,7 +9394,7 @@ type BoolCondition struct { func (x *BoolCondition) Reset() { *x = BoolCondition{} - mi := &file_common_proto_msgTypes[118] + mi := &file_common_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9312,7 +9406,7 @@ func (x *BoolCondition) String() string { func (*BoolCondition) ProtoMessage() {} func (x *BoolCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[118] + mi := &file_common_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9325,7 +9419,7 @@ func (x *BoolCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use BoolCondition.ProtoReflect.Descriptor instead. func (*BoolCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{118} + return file_common_proto_rawDescGZIP(), []int{119} } func (x *BoolCondition) GetValue() isBoolCondition_Value { @@ -9378,7 +9472,7 @@ type ExistsCondition struct { func (x *ExistsCondition) Reset() { *x = ExistsCondition{} - mi := &file_common_proto_msgTypes[119] + mi := &file_common_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9390,7 +9484,7 @@ func (x *ExistsCondition) String() string { func (*ExistsCondition) ProtoMessage() {} func (x *ExistsCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[119] + mi := &file_common_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9403,7 +9497,7 @@ func (x *ExistsCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use ExistsCondition.ProtoReflect.Descriptor instead. func (*ExistsCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{119} + return file_common_proto_rawDescGZIP(), []int{120} } func (x *ExistsCondition) GetIncludeNull() bool { @@ -9429,7 +9523,7 @@ type AddressMatch struct { func (x *AddressMatch) Reset() { *x = AddressMatch{} - mi := &file_common_proto_msgTypes[120] + mi := &file_common_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9441,7 +9535,7 @@ func (x *AddressMatch) String() string { func (*AddressMatch) ProtoMessage() {} func (x *AddressMatch) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[120] + mi := &file_common_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9454,7 +9548,7 @@ func (x *AddressMatch) ProtoReflect() protoreflect.Message { // Deprecated: Use AddressMatch.ProtoReflect.Descriptor instead. func (*AddressMatch) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{120} + return file_common_proto_rawDescGZIP(), []int{121} } func (x *AddressMatch) GetMatch() isAddressMatch_Match { @@ -9550,7 +9644,7 @@ type PreparedQuery struct { func (x *PreparedQuery) Reset() { *x = PreparedQuery{} - mi := &file_common_proto_msgTypes[121] + mi := &file_common_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9562,7 +9656,7 @@ func (x *PreparedQuery) String() string { func (*PreparedQuery) ProtoMessage() {} func (x *PreparedQuery) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[121] + mi := &file_common_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9575,7 +9669,7 @@ func (x *PreparedQuery) ProtoReflect() protoreflect.Message { // Deprecated: Use PreparedQuery.ProtoReflect.Descriptor instead. func (*PreparedQuery) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{121} + return file_common_proto_rawDescGZIP(), []int{122} } func (x *PreparedQuery) GetName() string { @@ -9615,7 +9709,7 @@ type AggregatedVolume struct { func (x *AggregatedVolume) Reset() { *x = AggregatedVolume{} - mi := &file_common_proto_msgTypes[122] + mi := &file_common_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9627,7 +9721,7 @@ func (x *AggregatedVolume) String() string { func (*AggregatedVolume) ProtoMessage() {} func (x *AggregatedVolume) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[122] + mi := &file_common_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9640,7 +9734,7 @@ func (x *AggregatedVolume) ProtoReflect() protoreflect.Message { // Deprecated: Use AggregatedVolume.ProtoReflect.Descriptor instead. func (*AggregatedVolume) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{122} + return file_common_proto_rawDescGZIP(), []int{123} } func (x *AggregatedVolume) GetAsset() string { @@ -9682,7 +9776,7 @@ type AggregateResult struct { func (x *AggregateResult) Reset() { *x = AggregateResult{} - mi := &file_common_proto_msgTypes[123] + mi := &file_common_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9694,7 +9788,7 @@ func (x *AggregateResult) String() string { func (*AggregateResult) ProtoMessage() {} func (x *AggregateResult) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[123] + mi := &file_common_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9707,7 +9801,7 @@ func (x *AggregateResult) ProtoReflect() protoreflect.Message { // Deprecated: Use AggregateResult.ProtoReflect.Descriptor instead. func (*AggregateResult) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{123} + return file_common_proto_rawDescGZIP(), []int{124} } func (x *AggregateResult) GetVolumes() []*AggregatedVolume { @@ -9735,7 +9829,7 @@ type GroupedAggregateResult struct { func (x *GroupedAggregateResult) Reset() { *x = GroupedAggregateResult{} - mi := &file_common_proto_msgTypes[124] + mi := &file_common_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9747,7 +9841,7 @@ func (x *GroupedAggregateResult) String() string { func (*GroupedAggregateResult) ProtoMessage() {} func (x *GroupedAggregateResult) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[124] + mi := &file_common_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9760,7 +9854,7 @@ func (x *GroupedAggregateResult) ProtoReflect() protoreflect.Message { // Deprecated: Use GroupedAggregateResult.ProtoReflect.Descriptor instead. func (*GroupedAggregateResult) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{124} + return file_common_proto_rawDescGZIP(), []int{125} } func (x *GroupedAggregateResult) GetPrefix() string { @@ -9792,7 +9886,7 @@ type PreparedQueryCursor struct { func (x *PreparedQueryCursor) Reset() { *x = PreparedQueryCursor{} - mi := &file_common_proto_msgTypes[125] + mi := &file_common_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9804,7 +9898,7 @@ func (x *PreparedQueryCursor) String() string { func (*PreparedQueryCursor) ProtoMessage() {} func (x *PreparedQueryCursor) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[125] + mi := &file_common_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9817,7 +9911,7 @@ func (x *PreparedQueryCursor) ProtoReflect() protoreflect.Message { // Deprecated: Use PreparedQueryCursor.ProtoReflect.Descriptor instead. func (*PreparedQueryCursor) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{125} + return file_common_proto_rawDescGZIP(), []int{126} } func (x *PreparedQueryCursor) GetPageSize() uint32 { @@ -9881,7 +9975,7 @@ type LedgerStats struct { func (x *LedgerStats) Reset() { *x = LedgerStats{} - mi := &file_common_proto_msgTypes[126] + mi := &file_common_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9893,7 +9987,7 @@ func (x *LedgerStats) String() string { func (*LedgerStats) ProtoMessage() {} func (x *LedgerStats) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[126] + mi := &file_common_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9906,7 +10000,7 @@ func (x *LedgerStats) ProtoReflect() protoreflect.Message { // Deprecated: Use LedgerStats.ProtoReflect.Descriptor instead. func (*LedgerStats) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{126} + return file_common_proto_rawDescGZIP(), []int{127} } func (x *LedgerStats) GetTransactionCount() uint64 { @@ -9994,7 +10088,7 @@ type PersistedConfig struct { func (x *PersistedConfig) Reset() { *x = PersistedConfig{} - mi := &file_common_proto_msgTypes[127] + mi := &file_common_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10006,7 +10100,7 @@ func (x *PersistedConfig) String() string { func (*PersistedConfig) ProtoMessage() {} func (x *PersistedConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[127] + mi := &file_common_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10019,7 +10113,7 @@ func (x *PersistedConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use PersistedConfig.ProtoReflect.Descriptor instead. func (*PersistedConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{127} + return file_common_proto_rawDescGZIP(), []int{128} } func (x *PersistedConfig) GetNodeId() uint64 { @@ -10072,7 +10166,7 @@ type CallerIdentity struct { func (x *CallerIdentity) Reset() { *x = CallerIdentity{} - mi := &file_common_proto_msgTypes[128] + mi := &file_common_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10084,7 +10178,7 @@ func (x *CallerIdentity) String() string { func (*CallerIdentity) ProtoMessage() {} func (x *CallerIdentity) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[128] + mi := &file_common_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10097,7 +10191,7 @@ func (x *CallerIdentity) ProtoReflect() protoreflect.Message { // Deprecated: Use CallerIdentity.ProtoReflect.Descriptor instead. func (*CallerIdentity) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{128} + return file_common_proto_rawDescGZIP(), []int{129} } func (x *CallerIdentity) GetSubject() string { @@ -10168,7 +10262,7 @@ type CallerSnapshot struct { func (x *CallerSnapshot) Reset() { *x = CallerSnapshot{} - mi := &file_common_proto_msgTypes[129] + mi := &file_common_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10180,7 +10274,7 @@ func (x *CallerSnapshot) String() string { func (*CallerSnapshot) ProtoMessage() {} func (x *CallerSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[129] + mi := &file_common_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10193,7 +10287,7 @@ func (x *CallerSnapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use CallerSnapshot.ProtoReflect.Descriptor instead. func (*CallerSnapshot) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{129} + return file_common_proto_rawDescGZIP(), []int{130} } func (x *CallerSnapshot) GetIdentity() *CallerIdentity { @@ -10231,7 +10325,7 @@ type S3StorageConfig struct { func (x *S3StorageConfig) Reset() { *x = S3StorageConfig{} - mi := &file_common_proto_msgTypes[130] + mi := &file_common_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10243,7 +10337,7 @@ func (x *S3StorageConfig) String() string { func (*S3StorageConfig) ProtoMessage() {} func (x *S3StorageConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[130] + mi := &file_common_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10256,7 +10350,7 @@ func (x *S3StorageConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use S3StorageConfig.ProtoReflect.Descriptor instead. func (*S3StorageConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{130} + return file_common_proto_rawDescGZIP(), []int{131} } func (x *S3StorageConfig) GetBucket() string { @@ -10307,7 +10401,7 @@ type AzureStorageConfig struct { func (x *AzureStorageConfig) Reset() { *x = AzureStorageConfig{} - mi := &file_common_proto_msgTypes[131] + mi := &file_common_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10319,7 +10413,7 @@ func (x *AzureStorageConfig) String() string { func (*AzureStorageConfig) ProtoMessage() {} func (x *AzureStorageConfig) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[131] + mi := &file_common_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10332,7 +10426,7 @@ func (x *AzureStorageConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use AzureStorageConfig.ProtoReflect.Descriptor instead. func (*AzureStorageConfig) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{131} + return file_common_proto_rawDescGZIP(), []int{132} } func (x *AzureStorageConfig) GetAccountName() string { @@ -10379,7 +10473,7 @@ type BackupStorage struct { func (x *BackupStorage) Reset() { *x = BackupStorage{} - mi := &file_common_proto_msgTypes[132] + mi := &file_common_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10391,7 +10485,7 @@ func (x *BackupStorage) String() string { func (*BackupStorage) ProtoMessage() {} func (x *BackupStorage) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[132] + mi := &file_common_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10404,7 +10498,7 @@ func (x *BackupStorage) ProtoReflect() protoreflect.Message { // Deprecated: Use BackupStorage.ProtoReflect.Descriptor instead. func (*BackupStorage) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{132} + return file_common_proto_rawDescGZIP(), []int{133} } func (x *BackupStorage) GetProvider() isBackupStorage_Provider { @@ -10467,7 +10561,7 @@ type ReadOptions struct { func (x *ReadOptions) Reset() { *x = ReadOptions{} - mi := &file_common_proto_msgTypes[133] + mi := &file_common_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10479,7 +10573,7 @@ func (x *ReadOptions) String() string { func (*ReadOptions) ProtoMessage() {} func (x *ReadOptions) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[133] + mi := &file_common_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10492,7 +10586,7 @@ func (x *ReadOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadOptions.ProtoReflect.Descriptor instead. func (*ReadOptions) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{133} + return file_common_proto_rawDescGZIP(), []int{134} } func (x *ReadOptions) GetCheckpointId() uint64 { @@ -10544,7 +10638,7 @@ type ListOptions struct { func (x *ListOptions) Reset() { *x = ListOptions{} - mi := &file_common_proto_msgTypes[134] + mi := &file_common_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10556,7 +10650,7 @@ func (x *ListOptions) String() string { func (*ListOptions) ProtoMessage() {} func (x *ListOptions) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[134] + mi := &file_common_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10569,7 +10663,7 @@ func (x *ListOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use ListOptions.ProtoReflect.Descriptor instead. func (*ListOptions) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{134} + return file_common_proto_rawDescGZIP(), []int{135} } func (x *ListOptions) GetRead() *ReadOptions { @@ -10615,7 +10709,7 @@ const file_common_proto_rawDesc = "" + "\tTimestamp\x12\x12\n" + "\x04data\x18\x01 \x01(\x06R\x04data\"'\n" + "\tNullValue\x12\x1a\n" + - "\boriginal\x18\x01 \x01(\tR\boriginal\"\xd1\x01\n" + + "\boriginal\x18\x01 \x01(\tR\boriginal\"\xfa\x01\n" + "\rMetadataValue\x12#\n" + "\fstring_value\x18\x01 \x01(\tH\x00R\vstringValue\x12\x1d\n" + "\tint_value\x18\x02 \x01(\x03H\x00R\bintValue\x12\x1f\n" + @@ -10624,7 +10718,8 @@ const file_common_proto_rawDesc = "" + "\n" + "null_value\x18\x04 \x01(\v2\x11.common.NullValueH\x00R\tnullValue\x12\x1f\n" + "\n" + - "uint_value\x18\x05 \x01(\x04H\x00R\tuintValueB\x06\n" + + "uint_value\x18\x05 \x01(\x04H\x00R\tuintValue\x12'\n" + + "\x0edatetime_value\x18\x06 \x01(\x03H\x00R\rdatetimeValueB\x06\n" + "\x04type\"\x98\x01\n" + "\vMetadataMap\x127\n" + "\x06values\x18\x01 \x03(\v2\x1f.common.MetadataMap.ValuesEntryR\x06values\x1aP\n" + @@ -11163,7 +11258,7 @@ const file_common_proto_rawDesc = "" + "\x15RemovedAccountTypeLog\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\"k\n" + " UpdatedDefaultEnforcementModeLog\x12G\n" + - "\x10enforcement_mode\x18\x01 \x01(\x0e2\x1c.common.ChartEnforcementModeR\x0fenforcementMode\"\x9b\x04\n" + + "\x10enforcement_mode\x18\x01 \x01(\x0e2\x1c.common.ChartEnforcementModeR\x0fenforcementMode\"\xeb\x04\n" + "\vQueryFilter\x12.\n" + "\x05field\x18\x01 \x01(\v2\x16.common.FieldConditionH\x00R\x05field\x120\n" + "\aaddress\x18\x02 \x01(\v2\x14.common.AddressMatchH\x00R\aaddress\x12%\n" + @@ -11175,7 +11270,8 @@ const file_common_proto_rawDesc = "" + "\x06ledger\x18\b \x01(\v2\x17.common.LedgerConditionH\x00R\x06ledger\x12/\n" + "\x06log_id\x18\t \x01(\v2\x16.common.LogIdConditionH\x00R\x05logId\x12K\n" + "\x10log_builtin_uint\x18\n" + - " \x01(\v2\x1f.common.LogBuiltinUintConditionH\x00R\x0elogBuiltinUintB\b\n" + + " \x01(\v2\x1f.common.LogBuiltinUintConditionH\x00R\x0elogBuiltinUint\x12N\n" + + "\x11account_has_asset\x18\v \x01(\v2 .common.AccountHasAssetConditionH\x00R\x0faccountHasAssetB\b\n" + "\x06filter\"A\n" + "\x12ReferenceCondition\x12+\n" + "\x04cond\x18\x01 \x01(\v2\x17.common.StringConditionR\x04cond\">\n" + @@ -11188,7 +11284,11 @@ const file_common_proto_rawDesc = "" + "\x04cond\x18\x02 \x01(\v2\x15.common.UintConditionR\x04cond\"s\n" + "\x17LogBuiltinUintCondition\x12-\n" + "\x05field\x18\x01 \x01(\x0e2\x17.common.LogBuiltinIndexR\x05field\x12)\n" + - "\x04cond\x18\x02 \x01(\v2\x15.common.UintConditionR\x04cond\":\n" + + "\x04cond\x18\x02 \x01(\v2\x15.common.UintConditionR\x04cond\"W\n" + + "\x18AccountHasAssetCondition\x12\x1d\n" + + "\n" + + "asset_base\x18\x01 \x01(\tR\tassetBase\x12\x1c\n" + + "\tprecision\x18\x02 \x01(\rR\tprecision\":\n" + "\tAndFilter\x12-\n" + "\afilters\x18\x01 \x03(\v2\x13.common.QueryFilterR\afilters\"9\n" + "\bOrFilter\x12-\n" + @@ -11322,7 +11422,7 @@ const file_common_proto_rawDesc = "" + "TargetType\x12\x17\n" + "\x13TARGET_TYPE_ACCOUNT\x10\x00\x12\x1b\n" + "\x17TARGET_TYPE_TRANSACTION\x10\x01\x12\x16\n" + - "\x12TARGET_TYPE_LEDGER\x10\x02*\x8a\x02\n" + + "\x12TARGET_TYPE_LEDGER\x10\x02*\xa6\x02\n" + "\fMetadataType\x12\x18\n" + "\x14METADATA_TYPE_STRING\x10\x00\x12\x17\n" + "\x13METADATA_TYPE_INT64\x10\x01\x12\x16\n" + @@ -11333,7 +11433,9 @@ const file_common_proto_rawDesc = "" + "\x13METADATA_TYPE_INT32\x10\x06\x12\x17\n" + "\x13METADATA_TYPE_UINT8\x10\a\x12\x18\n" + "\x14METADATA_TYPE_UINT16\x10\b\x12\x18\n" + - "\x14METADATA_TYPE_UINT32\x10\t*u\n" + + "\x14METADATA_TYPE_UINT32\x10\t\x12\x1a\n" + + "\x16METADATA_TYPE_DATETIME\x10\n" + + "*u\n" + "\x10IndexBuildStatus\x12\"\n" + "\x1eINDEX_BUILD_STATUS_UNSPECIFIED\x10\x00\x12\x1f\n" + "\x1bINDEX_BUILD_STATUS_BUILDING\x10\x01\x12\x1c\n" + @@ -11345,9 +11447,10 @@ const file_common_proto_rawDesc = "" + "\x18TX_BUILTIN_INDEX_ADDRESS\x10\x03\x12#\n" + "\x1fTX_BUILTIN_INDEX_SOURCE_ADDRESS\x10\x04\x12!\n" + "\x1dTX_BUILTIN_INDEX_DEST_ADDRESS\x10\x05\x12 \n" + - "\x1cTX_BUILTIN_INDEX_INSERTED_AT\x10\x06*9\n" + + "\x1cTX_BUILTIN_INDEX_INSERTED_AT\x10\x06*W\n" + "\x13AccountBuiltinIndex\x12\"\n" + - "\x1eACCT_BUILTIN_INDEX_UNSPECIFIED\x10\x00*j\n" + + "\x1eACCT_BUILTIN_INDEX_UNSPECIFIED\x10\x00\x12\x1c\n" + + "\x18ACCT_BUILTIN_INDEX_ASSET\x10\x01*j\n" + "\x0fLogBuiltinIndex\x12!\n" + "\x1dLOG_BUILTIN_INDEX_UNSPECIFIED\x10\x00\x12\x1a\n" + "\x16LOG_BUILTIN_INDEX_DATE\x10\x01*\x18LOG_BUILTIN_INDEX_LEDGER*C\n" + @@ -11474,7 +11577,7 @@ func file_common_proto_rawDescGZIP() []byte { } var file_common_proto_enumTypes = make([]protoimpl.EnumInfo, 17) -var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 153) +var file_common_proto_msgTypes = make([]protoimpl.MessageInfo, 154) var file_common_proto_goTypes = []any{ (TargetType)(0), // 0: common.TargetType (MetadataType)(0), // 1: common.MetadataType @@ -11603,76 +11706,77 @@ var file_common_proto_goTypes = []any{ (*LogIdCondition)(nil), // 124: common.LogIdCondition (*BuiltinUintCondition)(nil), // 125: common.BuiltinUintCondition (*LogBuiltinUintCondition)(nil), // 126: common.LogBuiltinUintCondition - (*AndFilter)(nil), // 127: common.AndFilter - (*OrFilter)(nil), // 128: common.OrFilter - (*NotFilter)(nil), // 129: common.NotFilter - (*FieldRef)(nil), // 130: common.FieldRef - (*FieldCondition)(nil), // 131: common.FieldCondition - (*StringCondition)(nil), // 132: common.StringCondition - (*IntCondition)(nil), // 133: common.IntCondition - (*UintCondition)(nil), // 134: common.UintCondition - (*BoolCondition)(nil), // 135: common.BoolCondition - (*ExistsCondition)(nil), // 136: common.ExistsCondition - (*AddressMatch)(nil), // 137: common.AddressMatch - (*PreparedQuery)(nil), // 138: common.PreparedQuery - (*AggregatedVolume)(nil), // 139: common.AggregatedVolume - (*AggregateResult)(nil), // 140: common.AggregateResult - (*GroupedAggregateResult)(nil), // 141: common.GroupedAggregateResult - (*PreparedQueryCursor)(nil), // 142: common.PreparedQueryCursor - (*LedgerStats)(nil), // 143: common.LedgerStats - (*PersistedConfig)(nil), // 144: common.PersistedConfig - (*CallerIdentity)(nil), // 145: common.CallerIdentity - (*CallerSnapshot)(nil), // 146: common.CallerSnapshot - (*S3StorageConfig)(nil), // 147: common.S3StorageConfig - (*AzureStorageConfig)(nil), // 148: common.AzureStorageConfig - (*BackupStorage)(nil), // 149: common.BackupStorage - (*ReadOptions)(nil), // 150: common.ReadOptions - (*ListOptions)(nil), // 151: common.ListOptions - nil, // 152: common.MetadataMap.ValuesEntry - nil, // 153: common.Transaction.MetadataEntry - nil, // 154: common.Script.VarsEntry - nil, // 155: common.PostCommitVolumes.VolumesByAccountEntry - nil, // 156: common.Account.MetadataEntry - nil, // 157: common.MetadataSchema.AccountFieldsEntry - nil, // 158: common.MetadataSchema.TransactionFieldsEntry - nil, // 159: common.MetadataSchema.LedgerFieldsEntry - nil, // 160: common.SavedLedgerMetadataLog.MetadataEntry - nil, // 161: common.CreatedLedgerLog.AccountTypesEntry - nil, // 162: common.CreatedTransaction.AccountMetadataEntry - nil, // 163: common.SavedMetadata.MetadataEntry - nil, // 164: common.LedgerInfo.AccountTypesEntry - nil, // 165: common.LedgerInfo.MetadataEntry - nil, // 166: common.SaveMetadataCommand.MetadataEntry - nil, // 167: common.TransactionState.MetadataEntry - nil, // 168: common.IdempotencyFailure.MetadataEntry - nil, // 169: common.AccountType.SegmentTypesEntry - (*signaturepb.SignedLog)(nil), // 170: signature.SignedLog + (*AccountHasAssetCondition)(nil), // 127: common.AccountHasAssetCondition + (*AndFilter)(nil), // 128: common.AndFilter + (*OrFilter)(nil), // 129: common.OrFilter + (*NotFilter)(nil), // 130: common.NotFilter + (*FieldRef)(nil), // 131: common.FieldRef + (*FieldCondition)(nil), // 132: common.FieldCondition + (*StringCondition)(nil), // 133: common.StringCondition + (*IntCondition)(nil), // 134: common.IntCondition + (*UintCondition)(nil), // 135: common.UintCondition + (*BoolCondition)(nil), // 136: common.BoolCondition + (*ExistsCondition)(nil), // 137: common.ExistsCondition + (*AddressMatch)(nil), // 138: common.AddressMatch + (*PreparedQuery)(nil), // 139: common.PreparedQuery + (*AggregatedVolume)(nil), // 140: common.AggregatedVolume + (*AggregateResult)(nil), // 141: common.AggregateResult + (*GroupedAggregateResult)(nil), // 142: common.GroupedAggregateResult + (*PreparedQueryCursor)(nil), // 143: common.PreparedQueryCursor + (*LedgerStats)(nil), // 144: common.LedgerStats + (*PersistedConfig)(nil), // 145: common.PersistedConfig + (*CallerIdentity)(nil), // 146: common.CallerIdentity + (*CallerSnapshot)(nil), // 147: common.CallerSnapshot + (*S3StorageConfig)(nil), // 148: common.S3StorageConfig + (*AzureStorageConfig)(nil), // 149: common.AzureStorageConfig + (*BackupStorage)(nil), // 150: common.BackupStorage + (*ReadOptions)(nil), // 151: common.ReadOptions + (*ListOptions)(nil), // 152: common.ListOptions + nil, // 153: common.MetadataMap.ValuesEntry + nil, // 154: common.Transaction.MetadataEntry + nil, // 155: common.Script.VarsEntry + nil, // 156: common.PostCommitVolumes.VolumesByAccountEntry + nil, // 157: common.Account.MetadataEntry + nil, // 158: common.MetadataSchema.AccountFieldsEntry + nil, // 159: common.MetadataSchema.TransactionFieldsEntry + nil, // 160: common.MetadataSchema.LedgerFieldsEntry + nil, // 161: common.SavedLedgerMetadataLog.MetadataEntry + nil, // 162: common.CreatedLedgerLog.AccountTypesEntry + nil, // 163: common.CreatedTransaction.AccountMetadataEntry + nil, // 164: common.SavedMetadata.MetadataEntry + nil, // 165: common.LedgerInfo.AccountTypesEntry + nil, // 166: common.LedgerInfo.MetadataEntry + nil, // 167: common.SaveMetadataCommand.MetadataEntry + nil, // 168: common.TransactionState.MetadataEntry + nil, // 169: common.IdempotencyFailure.MetadataEntry + nil, // 170: common.AccountType.SegmentTypesEntry + (*signaturepb.SignedLog)(nil), // 171: signature.SignedLog } var file_common_proto_depIdxs = []int32{ 18, // 0: common.MetadataValue.null_value:type_name -> common.NullValue - 152, // 1: common.MetadataMap.values:type_name -> common.MetadataMap.ValuesEntry + 153, // 1: common.MetadataMap.values:type_name -> common.MetadataMap.ValuesEntry 22, // 2: common.Posting.amount:type_name -> common.Uint256 23, // 3: common.Transaction.postings:type_name -> common.Posting - 153, // 4: common.Transaction.metadata:type_name -> common.Transaction.MetadataEntry + 154, // 4: common.Transaction.metadata:type_name -> common.Transaction.MetadataEntry 17, // 5: common.Transaction.timestamp:type_name -> common.Timestamp 17, // 6: common.Transaction.inserted_at:type_name -> common.Timestamp 17, // 7: common.Transaction.updated_at:type_name -> common.Timestamp 17, // 8: common.Transaction.reverted_at:type_name -> common.Timestamp - 154, // 9: common.Script.vars:type_name -> common.Script.VarsEntry + 155, // 9: common.Script.vars:type_name -> common.Script.VarsEntry 29, // 10: common.VolumesByAssets.volumes:type_name -> common.VolumeEntry 26, // 11: common.VolumeEntry.volumes:type_name -> common.Volumes - 155, // 12: common.PostCommitVolumes.volumes_by_account:type_name -> common.PostCommitVolumes.VolumesByAccountEntry + 156, // 12: common.PostCommitVolumes.volumes_by_account:type_name -> common.PostCommitVolumes.VolumesByAccountEntry 27, // 13: common.AccountVolume.volumes:type_name -> common.VolumesWithBalance - 156, // 14: common.Account.metadata:type_name -> common.Account.MetadataEntry + 157, // 14: common.Account.metadata:type_name -> common.Account.MetadataEntry 17, // 15: common.Account.first_usage:type_name -> common.Timestamp 17, // 16: common.Account.insertion_date:type_name -> common.Timestamp 17, // 17: common.Account.updated_at:type_name -> common.Timestamp 31, // 18: common.Account.volumes:type_name -> common.AccountVolume 33, // 19: common.Target.account:type_name -> common.TargetAccount 1, // 20: common.MetadataFieldSchema.type:type_name -> common.MetadataType - 157, // 21: common.MetadataSchema.account_fields:type_name -> common.MetadataSchema.AccountFieldsEntry - 158, // 22: common.MetadataSchema.transaction_fields:type_name -> common.MetadataSchema.TransactionFieldsEntry - 159, // 23: common.MetadataSchema.ledger_fields:type_name -> common.MetadataSchema.LedgerFieldsEntry + 158, // 21: common.MetadataSchema.account_fields:type_name -> common.MetadataSchema.AccountFieldsEntry + 159, // 22: common.MetadataSchema.transaction_fields:type_name -> common.MetadataSchema.TransactionFieldsEntry + 160, // 23: common.MetadataSchema.ledger_fields:type_name -> common.MetadataSchema.LedgerFieldsEntry 0, // 24: common.SetMetadataFieldTypeCommand.target_type:type_name -> common.TargetType 1, // 25: common.SetMetadataFieldTypeCommand.type:type_name -> common.MetadataType 0, // 26: common.MetadataIndexID.target:type_name -> common.TargetType @@ -11685,7 +11789,7 @@ var file_common_proto_depIdxs = []int32{ 17, // 33: common.Index.created_at:type_name -> common.Timestamp 17, // 34: common.Index.last_built_at:type_name -> common.Timestamp 44, // 35: common.Log.payload:type_name -> common.LogPayload - 170, // 36: common.Log.response_signature:type_name -> signature.SignedLog + 171, // 36: common.Log.response_signature:type_name -> signature.SignedLog 79, // 37: common.LogPayload.create_ledger:type_name -> common.CreatedLedgerLog 80, // 38: common.LogPayload.delete_ledger:type_name -> common.DeletedLedgerLog 81, // 39: common.LogPayload.apply:type_name -> common.ApplyLedgerLog @@ -11728,10 +11832,10 @@ var file_common_proto_depIdxs = []int32{ 53, // 76: common.ClusterConfig.bloom_prepared_queries:type_name -> common.BloomTypeConfig 53, // 77: common.ClusterConfig.bloom_indexes:type_name -> common.BloomTypeConfig 54, // 78: common.PersistedClusterState.config:type_name -> common.ClusterConfig - 138, // 79: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery + 139, // 79: common.CreatedPreparedQueryLog.query:type_name -> common.PreparedQuery 121, // 80: common.UpdatedPreparedQueryLog.previous_filter:type_name -> common.QueryFilter 121, // 81: common.UpdatedPreparedQueryLog.new_filter:type_name -> common.QueryFilter - 160, // 82: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry + 161, // 82: common.SavedLedgerMetadataLog.metadata:type_name -> common.SavedLedgerMetadataLog.MetadataEntry 17, // 83: common.NumscriptInfo.created_at:type_name -> common.Timestamp 63, // 84: common.SavedNumscriptLog.info:type_name -> common.NumscriptInfo 73, // 85: common.SinkConfig.nats:type_name -> common.NatsSinkConfig @@ -11747,7 +11851,7 @@ var file_common_proto_depIdxs = []int32{ 36, // 95: common.CreatedLedgerLog.metadata_schema:type_name -> common.MetadataSchema 9, // 96: common.CreatedLedgerLog.mode:type_name -> common.LedgerMode 99, // 97: common.CreatedLedgerLog.mirror_source:type_name -> common.MirrorSourceConfig - 161, // 98: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry + 162, // 98: common.CreatedLedgerLog.account_types:type_name -> common.CreatedLedgerLog.AccountTypesEntry 12, // 99: common.CreatedLedgerLog.default_enforcement_mode:type_name -> common.ChartEnforcementMode 17, // 100: common.DeletedLedgerLog.deleted_at:type_name -> common.Timestamp 82, // 101: common.ApplyLedgerLog.log:type_name -> common.LedgerLog @@ -11769,12 +11873,12 @@ var file_common_proto_depIdxs = []int32{ 39, // 117: common.CreatedIndexLog.id:type_name -> common.IndexID 39, // 118: common.DroppedIndexLog.id:type_name -> common.IndexID 24, // 119: common.CreatedTransaction.transaction:type_name -> common.Transaction - 162, // 120: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry + 163, // 120: common.CreatedTransaction.account_metadata:type_name -> common.CreatedTransaction.AccountMetadataEntry 30, // 121: common.CreatedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes 24, // 122: common.RevertedTransaction.revert_transaction:type_name -> common.Transaction 30, // 123: common.RevertedTransaction.post_commit_volumes:type_name -> common.PostCommitVolumes 34, // 124: common.SavedMetadata.target:type_name -> common.Target - 163, // 125: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry + 164, // 125: common.SavedMetadata.metadata:type_name -> common.SavedMetadata.MetadataEntry 34, // 126: common.DeletedMetadata.target:type_name -> common.Target 0, // 127: common.SetMetadataFieldTypeLog.target_type:type_name -> common.TargetType 1, // 128: common.SetMetadataFieldTypeLog.type:type_name -> common.MetadataType @@ -11800,86 +11904,87 @@ var file_common_proto_depIdxs = []int32{ 9, // 148: common.LedgerInfo.mode:type_name -> common.LedgerMode 99, // 149: common.LedgerInfo.mirror_source:type_name -> common.MirrorSourceConfig 104, // 150: common.LedgerInfo.mirror_sync_progress:type_name -> common.MirrorSyncProgress - 164, // 151: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry + 165, // 151: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry 12, // 152: common.LedgerInfo.default_enforcement_mode:type_name -> common.ChartEnforcementMode - 165, // 153: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry + 166, // 153: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry 34, // 154: common.SaveMetadataCommand.target:type_name -> common.Target - 166, // 155: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry + 167, // 155: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry 34, // 156: common.DeleteMetadataCommand.target:type_name -> common.Target - 167, // 157: common.TransactionState.metadata:type_name -> common.TransactionState.MetadataEntry + 168, // 157: common.TransactionState.metadata:type_name -> common.TransactionState.MetadataEntry 17, // 158: common.TransactionState.timestamp:type_name -> common.Timestamp 110, // 159: common.IdempotencyKeyValue.failure:type_name -> common.IdempotencyFailure 11, // 160: common.IdempotencyFailure.reason:type_name -> common.ErrorReason - 168, // 161: common.IdempotencyFailure.metadata:type_name -> common.IdempotencyFailure.MetadataEntry + 169, // 161: common.IdempotencyFailure.metadata:type_name -> common.IdempotencyFailure.MetadataEntry 114, // 162: common.SegmentType.uuid:type_name -> common.UUIDConstraint 115, // 163: common.SegmentType.uint64:type_name -> common.Uint64Constraint 116, // 164: common.SegmentType.bytes:type_name -> common.BytesConstraint 13, // 165: common.AccountType.persistence:type_name -> common.AccountTypePersistence - 169, // 166: common.AccountType.segment_types:type_name -> common.AccountType.SegmentTypesEntry + 170, // 166: common.AccountType.segment_types:type_name -> common.AccountType.SegmentTypesEntry 117, // 167: common.AddedAccountTypeLog.account_type:type_name -> common.AccountType 12, // 168: common.UpdatedDefaultEnforcementModeLog.enforcement_mode:type_name -> common.ChartEnforcementMode - 131, // 169: common.QueryFilter.field:type_name -> common.FieldCondition - 137, // 170: common.QueryFilter.address:type_name -> common.AddressMatch - 127, // 171: common.QueryFilter.and:type_name -> common.AndFilter - 128, // 172: common.QueryFilter.or:type_name -> common.OrFilter - 129, // 173: common.QueryFilter.not:type_name -> common.NotFilter + 132, // 169: common.QueryFilter.field:type_name -> common.FieldCondition + 138, // 170: common.QueryFilter.address:type_name -> common.AddressMatch + 128, // 171: common.QueryFilter.and:type_name -> common.AndFilter + 129, // 172: common.QueryFilter.or:type_name -> common.OrFilter + 130, // 173: common.QueryFilter.not:type_name -> common.NotFilter 122, // 174: common.QueryFilter.reference:type_name -> common.ReferenceCondition 125, // 175: common.QueryFilter.builtin_uint:type_name -> common.BuiltinUintCondition 123, // 176: common.QueryFilter.ledger:type_name -> common.LedgerCondition 124, // 177: common.QueryFilter.log_id:type_name -> common.LogIdCondition 126, // 178: common.QueryFilter.log_builtin_uint:type_name -> common.LogBuiltinUintCondition - 132, // 179: common.ReferenceCondition.cond:type_name -> common.StringCondition - 132, // 180: common.LedgerCondition.cond:type_name -> common.StringCondition - 134, // 181: common.LogIdCondition.cond:type_name -> common.UintCondition - 3, // 182: common.BuiltinUintCondition.field:type_name -> common.TransactionBuiltinIndex - 134, // 183: common.BuiltinUintCondition.cond:type_name -> common.UintCondition - 5, // 184: common.LogBuiltinUintCondition.field:type_name -> common.LogBuiltinIndex - 134, // 185: common.LogBuiltinUintCondition.cond:type_name -> common.UintCondition - 121, // 186: common.AndFilter.filters:type_name -> common.QueryFilter - 121, // 187: common.OrFilter.filters:type_name -> common.QueryFilter - 121, // 188: common.NotFilter.filter:type_name -> common.QueryFilter - 130, // 189: common.FieldCondition.field:type_name -> common.FieldRef - 132, // 190: common.FieldCondition.string_cond:type_name -> common.StringCondition - 133, // 191: common.FieldCondition.int_cond:type_name -> common.IntCondition - 134, // 192: common.FieldCondition.uint_cond:type_name -> common.UintCondition - 135, // 193: common.FieldCondition.bool_cond:type_name -> common.BoolCondition - 136, // 194: common.FieldCondition.exists_cond:type_name -> common.ExistsCondition - 14, // 195: common.AddressMatch.role:type_name -> common.AddressRole - 121, // 196: common.PreparedQuery.filter:type_name -> common.QueryFilter - 15, // 197: common.PreparedQuery.target:type_name -> common.QueryTarget - 22, // 198: common.AggregatedVolume.input:type_name -> common.Uint256 - 22, // 199: common.AggregatedVolume.output:type_name -> common.Uint256 - 139, // 200: common.AggregateResult.volumes:type_name -> common.AggregatedVolume - 141, // 201: common.AggregateResult.groups:type_name -> common.GroupedAggregateResult - 139, // 202: common.GroupedAggregateResult.volumes:type_name -> common.AggregatedVolume - 32, // 203: common.PreparedQueryCursor.account_data:type_name -> common.Account - 24, // 204: common.PreparedQueryCursor.transaction_data:type_name -> common.Transaction - 145, // 205: common.CallerSnapshot.identity:type_name -> common.CallerIdentity - 147, // 206: common.BackupStorage.s3:type_name -> common.S3StorageConfig - 148, // 207: common.BackupStorage.azure:type_name -> common.AzureStorageConfig - 150, // 208: common.ListOptions.read:type_name -> common.ReadOptions - 121, // 209: common.ListOptions.filter:type_name -> common.QueryFilter - 19, // 210: common.MetadataMap.ValuesEntry.value:type_name -> common.MetadataValue - 19, // 211: common.Transaction.MetadataEntry.value:type_name -> common.MetadataValue - 28, // 212: common.PostCommitVolumes.VolumesByAccountEntry.value:type_name -> common.VolumesByAssets - 19, // 213: common.Account.MetadataEntry.value:type_name -> common.MetadataValue - 35, // 214: common.MetadataSchema.AccountFieldsEntry.value:type_name -> common.MetadataFieldSchema - 35, // 215: common.MetadataSchema.TransactionFieldsEntry.value:type_name -> common.MetadataFieldSchema - 35, // 216: common.MetadataSchema.LedgerFieldsEntry.value:type_name -> common.MetadataFieldSchema - 19, // 217: common.SavedLedgerMetadataLog.MetadataEntry.value:type_name -> common.MetadataValue - 117, // 218: common.CreatedLedgerLog.AccountTypesEntry.value:type_name -> common.AccountType - 20, // 219: common.CreatedTransaction.AccountMetadataEntry.value:type_name -> common.MetadataMap - 19, // 220: common.SavedMetadata.MetadataEntry.value:type_name -> common.MetadataValue - 117, // 221: common.LedgerInfo.AccountTypesEntry.value:type_name -> common.AccountType - 19, // 222: common.LedgerInfo.MetadataEntry.value:type_name -> common.MetadataValue - 19, // 223: common.SaveMetadataCommand.MetadataEntry.value:type_name -> common.MetadataValue - 19, // 224: common.TransactionState.MetadataEntry.value:type_name -> common.MetadataValue - 113, // 225: common.AccountType.SegmentTypesEntry.value:type_name -> common.SegmentType - 226, // [226:226] is the sub-list for method output_type - 226, // [226:226] is the sub-list for method input_type - 226, // [226:226] is the sub-list for extension type_name - 226, // [226:226] is the sub-list for extension extendee - 0, // [0:226] is the sub-list for field type_name + 127, // 179: common.QueryFilter.account_has_asset:type_name -> common.AccountHasAssetCondition + 133, // 180: common.ReferenceCondition.cond:type_name -> common.StringCondition + 133, // 181: common.LedgerCondition.cond:type_name -> common.StringCondition + 135, // 182: common.LogIdCondition.cond:type_name -> common.UintCondition + 3, // 183: common.BuiltinUintCondition.field:type_name -> common.TransactionBuiltinIndex + 135, // 184: common.BuiltinUintCondition.cond:type_name -> common.UintCondition + 5, // 185: common.LogBuiltinUintCondition.field:type_name -> common.LogBuiltinIndex + 135, // 186: common.LogBuiltinUintCondition.cond:type_name -> common.UintCondition + 121, // 187: common.AndFilter.filters:type_name -> common.QueryFilter + 121, // 188: common.OrFilter.filters:type_name -> common.QueryFilter + 121, // 189: common.NotFilter.filter:type_name -> common.QueryFilter + 131, // 190: common.FieldCondition.field:type_name -> common.FieldRef + 133, // 191: common.FieldCondition.string_cond:type_name -> common.StringCondition + 134, // 192: common.FieldCondition.int_cond:type_name -> common.IntCondition + 135, // 193: common.FieldCondition.uint_cond:type_name -> common.UintCondition + 136, // 194: common.FieldCondition.bool_cond:type_name -> common.BoolCondition + 137, // 195: common.FieldCondition.exists_cond:type_name -> common.ExistsCondition + 14, // 196: common.AddressMatch.role:type_name -> common.AddressRole + 121, // 197: common.PreparedQuery.filter:type_name -> common.QueryFilter + 15, // 198: common.PreparedQuery.target:type_name -> common.QueryTarget + 22, // 199: common.AggregatedVolume.input:type_name -> common.Uint256 + 22, // 200: common.AggregatedVolume.output:type_name -> common.Uint256 + 140, // 201: common.AggregateResult.volumes:type_name -> common.AggregatedVolume + 142, // 202: common.AggregateResult.groups:type_name -> common.GroupedAggregateResult + 140, // 203: common.GroupedAggregateResult.volumes:type_name -> common.AggregatedVolume + 32, // 204: common.PreparedQueryCursor.account_data:type_name -> common.Account + 24, // 205: common.PreparedQueryCursor.transaction_data:type_name -> common.Transaction + 146, // 206: common.CallerSnapshot.identity:type_name -> common.CallerIdentity + 148, // 207: common.BackupStorage.s3:type_name -> common.S3StorageConfig + 149, // 208: common.BackupStorage.azure:type_name -> common.AzureStorageConfig + 151, // 209: common.ListOptions.read:type_name -> common.ReadOptions + 121, // 210: common.ListOptions.filter:type_name -> common.QueryFilter + 19, // 211: common.MetadataMap.ValuesEntry.value:type_name -> common.MetadataValue + 19, // 212: common.Transaction.MetadataEntry.value:type_name -> common.MetadataValue + 28, // 213: common.PostCommitVolumes.VolumesByAccountEntry.value:type_name -> common.VolumesByAssets + 19, // 214: common.Account.MetadataEntry.value:type_name -> common.MetadataValue + 35, // 215: common.MetadataSchema.AccountFieldsEntry.value:type_name -> common.MetadataFieldSchema + 35, // 216: common.MetadataSchema.TransactionFieldsEntry.value:type_name -> common.MetadataFieldSchema + 35, // 217: common.MetadataSchema.LedgerFieldsEntry.value:type_name -> common.MetadataFieldSchema + 19, // 218: common.SavedLedgerMetadataLog.MetadataEntry.value:type_name -> common.MetadataValue + 117, // 219: common.CreatedLedgerLog.AccountTypesEntry.value:type_name -> common.AccountType + 20, // 220: common.CreatedTransaction.AccountMetadataEntry.value:type_name -> common.MetadataMap + 19, // 221: common.SavedMetadata.MetadataEntry.value:type_name -> common.MetadataValue + 117, // 222: common.LedgerInfo.AccountTypesEntry.value:type_name -> common.AccountType + 19, // 223: common.LedgerInfo.MetadataEntry.value:type_name -> common.MetadataValue + 19, // 224: common.SaveMetadataCommand.MetadataEntry.value:type_name -> common.MetadataValue + 19, // 225: common.TransactionState.MetadataEntry.value:type_name -> common.MetadataValue + 113, // 226: common.AccountType.SegmentTypesEntry.value:type_name -> common.SegmentType + 227, // [227:227] is the sub-list for method output_type + 227, // [227:227] is the sub-list for method input_type + 227, // [227:227] is the sub-list for extension type_name + 227, // [227:227] is the sub-list for extension extendee + 0, // [0:227] is the sub-list for field type_name } func init() { file_common_proto_init() } @@ -11893,6 +11998,7 @@ func file_common_proto_init() { (*MetadataValue_BoolValue)(nil), (*MetadataValue_NullValue)(nil), (*MetadataValue_UintValue)(nil), + (*MetadataValue_DatetimeValue)(nil), } file_common_proto_msgTypes[4].OneofWrappers = []any{ (*ParameterValue_StringValue)(nil), @@ -11985,35 +12091,36 @@ func file_common_proto_init() { (*QueryFilter_Ledger)(nil), (*QueryFilter_LogId)(nil), (*QueryFilter_LogBuiltinUint)(nil), + (*QueryFilter_AccountHasAsset)(nil), } - file_common_proto_msgTypes[114].OneofWrappers = []any{ + file_common_proto_msgTypes[115].OneofWrappers = []any{ (*FieldCondition_StringCond)(nil), (*FieldCondition_IntCond)(nil), (*FieldCondition_UintCond)(nil), (*FieldCondition_BoolCond)(nil), (*FieldCondition_ExistsCond)(nil), } - file_common_proto_msgTypes[115].OneofWrappers = []any{ + file_common_proto_msgTypes[116].OneofWrappers = []any{ (*StringCondition_Hardcoded)(nil), (*StringCondition_Param)(nil), } - file_common_proto_msgTypes[116].OneofWrappers = []any{} file_common_proto_msgTypes[117].OneofWrappers = []any{} - file_common_proto_msgTypes[118].OneofWrappers = []any{ + file_common_proto_msgTypes[118].OneofWrappers = []any{} + file_common_proto_msgTypes[119].OneofWrappers = []any{ (*BoolCondition_Hardcoded)(nil), (*BoolCondition_Param)(nil), } - file_common_proto_msgTypes[120].OneofWrappers = []any{ + file_common_proto_msgTypes[121].OneofWrappers = []any{ (*AddressMatch_HardcodedPrefix)(nil), (*AddressMatch_HardcodedExact)(nil), (*AddressMatch_ParamPrefix)(nil), (*AddressMatch_ParamExact)(nil), } - file_common_proto_msgTypes[128].OneofWrappers = []any{ + file_common_proto_msgTypes[129].OneofWrappers = []any{ (*CallerIdentity_Issuer)(nil), (*CallerIdentity_KeyId)(nil), } - file_common_proto_msgTypes[132].OneofWrappers = []any{ + file_common_proto_msgTypes[133].OneofWrappers = []any{ (*BackupStorage_S3)(nil), (*BackupStorage_Azure)(nil), } @@ -12023,7 +12130,7 @@ func file_common_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_common_proto_rawDesc), len(file_common_proto_rawDesc)), NumEnums: 17, - NumMessages: 153, + NumMessages: 154, NumExtensions: 0, NumServices: 0, }, From 4db2d5c2d95b0a7b3ea4f6459fd36df0cca9d2ce Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Mon, 29 Jun 2026 16:48:35 +0200 Subject: [PATCH 9/9] fix(color): surface malformed volume keys + emit empty color in receipts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two adjacent NumaryBot findings: 1. ctrl/store.go: buildAccountVolumes did a silent continue on a malformed canonical volume key, dropping balances from GetAccount without telling the caller. Every other Pebble scan path propagates unmarshal errors; this one was inconsistent. Surface a hard error instead — CLAUDE.md invariant #7 (no silent should-not-happen). assembleAccount/scanAccount now return the error up the chain; buildAccountVolumes too. New TestAssembleAccount_MalformedKeyReturnsError pins the contract with a too-short canonical key payload. 2. receipt/receipt.go: PostingClaim.Color had json:"color,omitempty" so the signed JWT dropped color:"" for uncolored postings, making v3 uncolored claims indistinguishable from pre-color claims. Drop omitempty to match the contract already enforced by commonpb.Posting / sinkPosting / AccountVolume. New TestPostingClaim_AlwaysEmitsColor pins the JSON shape. --- internal/application/ctrl/store.go | 25 ++++++++++----- internal/application/ctrl/store_color_test.go | 31 +++++++++++++++++-- internal/infra/receipt/receipt.go | 6 +++- internal/infra/receipt/receipt_test.go | 23 ++++++++++++++ 4 files changed, 74 insertions(+), 11 deletions(-) diff --git a/internal/application/ctrl/store.go b/internal/application/ctrl/store.go index 88e2d68000..eed18a39dc 100644 --- a/internal/application/ctrl/store.go +++ b/internal/application/ctrl/store.go @@ -22,14 +22,19 @@ func assembleAccount( volEntries []attributes.ComputedEntry[*raftcmdpb.VolumePair], metaEntries []attributes.ComputedEntry[*commonpb.MetadataValue], collapseColors bool, -) *commonpb.Account { +) (*commonpb.Account, error) { account := &commonpb.Account{ Address: address, Metadata: map[string]*commonpb.MetadataValue{}, } if len(volEntries) > 0 { - account.Volumes = buildAccountVolumes(volEntries, collapseColors) + vols, err := buildAccountVolumes(volEntries, collapseColors) + if err != nil { + return nil, err + } + + account.Volumes = vols } if len(metaEntries) > 0 { @@ -48,14 +53,20 @@ func assembleAccount( } } - return account + return account, nil } // buildAccountVolumes turns the flushed volume entries into the // `repeated AccountVolume` list carried by Account.volumes. The list is // sorted by (asset, color) ascending. If collapseColors is true, entries // with the same asset (different colors) are summed under color = "". -func buildAccountVolumes(volEntries []attributes.ComputedEntry[*raftcmdpb.VolumePair], collapseColors bool) []*commonpb.AccountVolume { +// +// A malformed canonical key surfaces a hard error rather than a silent +// `continue`: every other Pebble scan path in the codebase propagates +// unmarshal errors, and silently dropping a row from GetAccount would +// return a truncated balance the caller has no way to detect (CLAUDE.md +// invariant #7). +func buildAccountVolumes(volEntries []attributes.ComputedEntry[*raftcmdpb.VolumePair], collapseColors bool) ([]*commonpb.AccountVolume, error) { type key struct { asset string color string @@ -66,7 +77,7 @@ func buildAccountVolumes(volEntries []attributes.ComputedEntry[*raftcmdpb.Volume for _, entry := range volEntries { var vk domain.VolumeKey if err := vk.Unmarshal(entry.CanonicalKey); err != nil { - continue + return nil, fmt.Errorf("malformed volume canonical key in account scan: %w", err) } input := big.NewInt(0) @@ -121,7 +132,7 @@ func buildAccountVolumes(volEntries []attributes.ComputedEntry[*raftcmdpb.Volume return out[i].GetColor() < out[j].GetColor() }) - return out + return out, nil } // scanAccount performs two forward scans — one for Volume and one for Metadata — @@ -175,5 +186,5 @@ func scanAccount( }).Infof("scanAccount complete") } - return assembleAccount(address, volEntries, metaEntries, collapseColors), nil + return assembleAccount(address, volEntries, metaEntries, collapseColors) } diff --git a/internal/application/ctrl/store_color_test.go b/internal/application/ctrl/store_color_test.go index 1ede1b0bd6..e617c136e6 100644 --- a/internal/application/ctrl/store_color_test.go +++ b/internal/application/ctrl/store_color_test.go @@ -38,7 +38,8 @@ func TestAssembleAccount_SegregatesColorsByDefault(t *testing.T) { volEntry(t, "test", "alice", "EUR/2", "", 10, 0), } - acct := assembleAccount("alice", entries, nil, false) + acct, err := assembleAccount("alice", entries, nil, false) + require.NoError(t, err) // Order: (EUR/2,""), (USD/2,""), (USD/2,"GRANTS"), (USD/2,"OPS") got := acct.GetVolumes() @@ -72,7 +73,8 @@ func TestAssembleAccount_CollapseColors(t *testing.T) { volEntry(t, "test", "alice", "USD/2", "OPS", 25, 5), } - acct := assembleAccount("alice", entries, nil, true) + acct, err := assembleAccount("alice", entries, nil, true) + require.NoError(t, err) require.Len(t, acct.GetVolumes(), 1) entry := acct.GetVolumes()[0] @@ -92,10 +94,33 @@ func TestAssembleAccount_FindVolume(t *testing.T) { volEntry(t, "test", "alice", "USD/2", "", 100, 0), volEntry(t, "test", "alice", "USD/2", "GRANTS", 50, 0), } - acct := assembleAccount("alice", entries, nil, false) + acct, err := assembleAccount("alice", entries, nil, false) + require.NoError(t, err) require.Equal(t, "100", acct.FindVolume("USD/2", "").GetBalance()) require.Equal(t, "50", acct.FindVolume("USD/2", "GRANTS").GetBalance()) require.Nil(t, acct.FindVolume("USD/2", "MISSING")) require.Nil(t, acct.FindVolume("EUR/2", "")) } + +// TestAssembleAccount_MalformedKeyReturnsError pins the contract that a +// malformed canonical volume key surfaces a hard error rather than being +// silently dropped from the GetAccount output. Silent skip would return a +// truncated balance the caller cannot detect — CLAUDE.md invariant #7. +func TestAssembleAccount_MalformedKeyReturnsError(t *testing.T) { + t.Parallel() + + entries := []attributes.ComputedEntry[*raftcmdpb.VolumePair]{ + { + // A byte slice too short to decode as a VolumeKey (the canonical + // shape begins with a 64-byte padded ledger name) — VolumeKey.Unmarshal + // rejects it. + CanonicalKey: []byte{0xAB, 0xCD}, + Value: &raftcmdpb.VolumePair{}, + }, + } + + _, err := assembleAccount("alice", entries, nil, false) + require.Error(t, err, "malformed canonical key must surface as an error, not be silently skipped") + require.Contains(t, err.Error(), "malformed volume canonical key") +} diff --git a/internal/infra/receipt/receipt.go b/internal/infra/receipt/receipt.go index c92a88860a..78bcec09a7 100644 --- a/internal/infra/receipt/receipt.go +++ b/internal/infra/receipt/receipt.go @@ -25,12 +25,16 @@ func NewSigner(key []byte) *Signer { // Color is part of the receipt-bound identity because balances are // segregated per (account, asset, color); a receipt that did not bind // the color would be verifiable against the wrong bucket on revert. +// Color is always emitted (no `omitempty`) so uncolored claims serialize +// as `color:""` rather than dropping the field — matches the contract +// enforced by commonpb.Posting / sinkPosting / AccountVolume, and keeps +// the receipt JWT distinguishable from pre-color claim shapes. type PostingClaim struct { Source string `json:"source"` Destination string `json:"destination"` Amount string `json:"amount"` Asset string `json:"asset"` - Color string `json:"color,omitempty"` + Color string `json:"color"` } // Claims are the custom JWT claims for a transaction receipt. diff --git a/internal/infra/receipt/receipt_test.go b/internal/infra/receipt/receipt_test.go index e5b5bb80e7..6cd0948bcc 100644 --- a/internal/infra/receipt/receipt_test.go +++ b/internal/infra/receipt/receipt_test.go @@ -1,6 +1,7 @@ package receipt import ( + "encoding/json" "math/big" "testing" @@ -251,3 +252,25 @@ func TestSignBindsColor(t *testing.T) { require.Len(t, roundTripped, 1) require.Equal(t, "GRANTS", roundTripped[0].GetColor()) } + +// TestPostingClaim_AlwaysEmitsColor pins the receipt-JSON contract: the +// uncolored bucket must surface as `color:""` in the signed JWT claim, +// not be dropped via omitempty. Matches the contract enforced on +// commonpb.Posting, sinkPosting, and AccountVolume — keeps the v3 +// uncolored claim distinguishable from a pre-color claim shape. +func TestPostingClaim_AlwaysEmitsColor(t *testing.T) { + t.Parallel() + + claim := PostingClaim{ + Source: "world", + Destination: "users:alice", + Amount: "100", + Asset: "USD/2", + // Color intentionally left empty — uncolored bucket. + } + + data, err := json.Marshal(claim) + require.NoError(t, err) + require.Contains(t, string(data), `"color":""`, + `uncolored receipt claims must surface "color":"" — receipt JWTs bind the (account, asset, color) identity, so the empty bucket cannot be omitted`) +}