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..06a73947c0 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{}, version.Info{}) + + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/v3/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..eed18a39dc 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,47 +15,26 @@ 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], -) *commonpb.Account { + collapseColors bool, +) (*commonpb.Account, error) { account := &commonpb.Account{ Address: address, Metadata: map[string]*commonpb.MetadataValue{}, } 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(), - } + vols, err := buildAccountVolumes(volEntries, collapseColors) + if err != nil { + return nil, err } - account.Volumes = volumes + account.Volumes = vols } if len(metaEntries) > 0 { @@ -73,7 +53,86 @@ 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 = "". +// +// 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 + } + + totals := make(map[key]*commonpb.AccountVolume, len(volEntries)) + + for _, entry := range volEntries { + var vk domain.VolumeKey + if err := vk.Unmarshal(entry.CanonicalKey); err != nil { + return nil, fmt.Errorf("malformed volume canonical key in account scan: %w", err) + } + + 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, nil } // scanAccount performs two forward scans — one for Volume and one for Metadata — @@ -87,6 +146,7 @@ func scanAccount( attrs *attributes.Attributes, ledgerName string, address string, + collapseColors bool, diagLogger ...logging.Logger, ) (*commonpb.Account, error) { var logger logging.Logger @@ -126,5 +186,5 @@ func scanAccount( }).Infof("scanAccount complete") } - return assembleAccount(address, volEntries, metaEntries), 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 new file mode 100644 index 0000000000..e617c136e6 --- /dev/null +++ b/internal/application/ctrl/store_color_test.go @@ -0,0 +1,126 @@ +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, err := assembleAccount("alice", entries, nil, false) + require.NoError(t, err) + + // 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, err := assembleAccount("alice", entries, nil, true) + require.NoError(t, err) + + 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, 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/application/events/clickhouse_data_test.go b/internal/application/events/clickhouse_data_test.go index daa58eff4b..46e3849593 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() @@ -646,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), 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..cd384e15a5 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()) @@ -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/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..8ef3aac82e 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,45 @@ 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 +998,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 +1194,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_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.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..ec5d0e494f 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 @@ -183,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 @@ -217,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 @@ -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..78bcec09a7 100644 --- a/internal/infra/receipt/receipt.go +++ b/internal/infra/receipt/receipt.go @@ -22,11 +22,19 @@ 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. +// 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"` } // Claims are the custom JWT claims for a transaction receipt. @@ -48,6 +56,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 +126,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..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" @@ -211,3 +212,65 @@ 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()) +} + +// 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`) +} 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/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/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..34576227b3 100644 --- a/internal/proto/commonpb/common.pb.go +++ b/internal/proto/commonpb/common.pb.go @@ -681,6 +681,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 +751,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 +818,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, } ) @@ -1553,11 +1559,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 +1630,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 +1920,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 +1960,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 +2040,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 +2052,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 +2065,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 +2075,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 +2167,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 +2180,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 +2218,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 +2234,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 +2246,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 +2259,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 +2282,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 +2294,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 +2307,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 +2360,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 +2372,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 +2385,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 +2406,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 +2418,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 +2431,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 +2466,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 +2478,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 +2491,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 +2526,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 +2538,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 +2551,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 +2586,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 +2598,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 +2611,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 { @@ -2574,7 +2721,7 @@ type Index struct { 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 +2733,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 +2746,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 { @@ -2660,7 +2807,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 +2819,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 +2832,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 +2853,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 +2865,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 +2878,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 +2907,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 +2919,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 +2932,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 +3001,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 +3013,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 +3026,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 +3454,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 +3466,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 +3479,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 +3501,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 +3513,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 +3526,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 +3561,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 +3573,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 +3586,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 +3615,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 +3627,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 +3640,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 +3674,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 +3686,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 +3699,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 +3719,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 +3731,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 +3744,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 +3764,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 +3776,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 +3789,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 +3809,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 +3821,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 +3834,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 +3855,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 +3867,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 +3880,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 { @@ -3774,7 +3921,7 @@ type ClusterConfig struct { 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 +3933,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 +3946,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 { @@ -3912,7 +4059,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 +4071,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 +4084,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 +4111,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 +4123,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 +4136,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 +4155,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 +4167,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 +4180,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 +4194,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 +4206,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 +4219,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 +4249,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 +4261,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 +4274,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 +4316,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 +4328,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 +4341,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 +4369,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 +4381,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 +4394,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 +4422,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 +4434,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 +4447,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 +4478,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 +4490,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 +4503,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 +4551,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 +4563,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 +4576,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 +4597,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 +4609,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 +4622,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 +4649,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 +4661,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 +4674,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 +4693,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 +4705,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 +4718,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 +4732,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 +4744,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 +4757,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 +4784,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 +4796,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 +4809,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 +4841,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 +4853,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 +4866,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 +5002,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 +5014,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 +5027,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 +5062,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 +5074,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 +5087,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 +5115,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 +5127,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 +5140,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 +5168,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 +5180,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 +5193,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 +5225,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 +5237,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 +5250,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 +5306,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 +5318,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 +5331,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 +5369,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 +5381,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 +5394,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 +5491,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 +5503,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 +5516,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 +5552,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 +5564,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 +5577,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 +5646,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 +5658,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 +5671,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 +5698,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 +5710,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 +5723,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 +5761,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 +5773,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 +5786,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 +5818,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 +5845,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 +5858,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 +5875,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 +5905,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 +5917,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 +5930,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 +6134,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 +6146,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 +6159,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 +6179,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 +6191,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 +6204,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 +6223,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 +6235,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 +6248,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 +6270,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 +6282,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 +6295,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 +6337,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 +6349,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 +6362,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 +6396,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 +6408,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 +6421,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 +6448,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 +6460,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 +6473,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 +6502,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 +6514,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 +6527,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 +6565,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 +6577,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 +6590,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 +6633,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 +6645,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 +6658,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 +6748,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 +6760,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 +6773,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 +6799,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 +6811,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 +6824,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 +6843,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 +6855,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 +6868,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 +6887,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 +6899,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 +6912,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 +6937,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 +6949,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 +6962,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 +7030,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 +7042,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 +7055,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 +7084,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 +7096,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 +7109,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 +7149,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 +7161,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 +7174,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 +7194,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 +7206,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 +7219,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 +7249,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 +7261,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 +7274,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 { @@ -7178,7 +7335,7 @@ type LedgerInfo struct { 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 +7347,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 +7360,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 { @@ -7294,7 +7451,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 +7463,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 +7476,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 +7504,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 +7516,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 +7529,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 +7562,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 +7574,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 +7587,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 +7637,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 +7649,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 +7662,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 +7723,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 +7735,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 +7748,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 +7782,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 +7794,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 +7807,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 +7827,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 +7839,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 +7852,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 +7879,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 +7891,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 +7904,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 +7986,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 +7998,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 +8011,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 +8022,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 +8034,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 +8047,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 +8058,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 +8070,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 +8083,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 +8099,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 +8111,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 +8124,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 +8165,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 +8177,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 +8190,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 +8210,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 +8222,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 +8235,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 +8255,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 +8267,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 +8280,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 { @@ -8155,7 +8312,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 +8324,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 +8337,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 { @@ -8369,7 +8526,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 +8538,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 +8551,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 +8571,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 +8583,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 +8596,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 +8616,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 +8628,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 +8641,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 +8662,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 +8674,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 +8687,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 +8715,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 +8727,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 +8740,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 { @@ -8614,7 +8771,7 @@ type AccountHasAssetCondition struct { func (x *AccountHasAssetCondition) Reset() { *x = AccountHasAssetCondition{} - mi := &file_common_proto_msgTypes[108] + mi := &file_common_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8626,7 +8783,7 @@ func (x *AccountHasAssetCondition) String() string { func (*AccountHasAssetCondition) ProtoMessage() {} func (x *AccountHasAssetCondition) ProtoReflect() protoreflect.Message { - mi := &file_common_proto_msgTypes[108] + mi := &file_common_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8639,7 +8796,7 @@ func (x *AccountHasAssetCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use AccountHasAssetCondition.ProtoReflect.Descriptor instead. func (*AccountHasAssetCondition) Descriptor() ([]byte, []int) { - return file_common_proto_rawDescGZIP(), []int{108} + return file_common_proto_rawDescGZIP(), []int{110} } func (x *AccountHasAssetCondition) GetAssetBase() string { @@ -8665,7 +8822,7 @@ type AndFilter struct { func (x *AndFilter) Reset() { *x = AndFilter{} - mi := &file_common_proto_msgTypes[109] + mi := &file_common_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8677,7 +8834,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[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8690,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{109} + return file_common_proto_rawDescGZIP(), []int{111} } func (x *AndFilter) GetFilters() []*QueryFilter { @@ -8709,7 +8866,7 @@ type OrFilter struct { func (x *OrFilter) Reset() { *x = OrFilter{} - mi := &file_common_proto_msgTypes[110] + mi := &file_common_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8721,7 +8878,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[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8734,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{110} + return file_common_proto_rawDescGZIP(), []int{112} } func (x *OrFilter) GetFilters() []*QueryFilter { @@ -8753,7 +8910,7 @@ type NotFilter struct { func (x *NotFilter) Reset() { *x = NotFilter{} - mi := &file_common_proto_msgTypes[111] + mi := &file_common_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8765,7 +8922,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[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8778,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{111} + return file_common_proto_rawDescGZIP(), []int{113} } func (x *NotFilter) GetFilter() *QueryFilter { @@ -8800,7 +8957,7 @@ type FieldRef struct { func (x *FieldRef) Reset() { *x = FieldRef{} - mi := &file_common_proto_msgTypes[112] + mi := &file_common_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8812,7 +8969,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[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8825,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{112} + return file_common_proto_rawDescGZIP(), []int{114} } func (x *FieldRef) GetMetadata() string { @@ -8853,7 +9010,7 @@ type FieldCondition struct { func (x *FieldCondition) Reset() { *x = FieldCondition{} - mi := &file_common_proto_msgTypes[113] + mi := &file_common_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8865,7 +9022,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[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8878,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{113} + return file_common_proto_rawDescGZIP(), []int{115} } func (x *FieldCondition) GetField() *FieldRef { @@ -8987,7 +9144,7 @@ type StringCondition struct { func (x *StringCondition) Reset() { *x = StringCondition{} - mi := &file_common_proto_msgTypes[114] + mi := &file_common_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8999,7 +9156,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[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9012,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{114} + return file_common_proto_rawDescGZIP(), []int{116} } func (x *StringCondition) GetValue() isStringCondition_Value { @@ -9070,7 +9227,7 @@ type IntCondition struct { func (x *IntCondition) Reset() { *x = IntCondition{} - mi := &file_common_proto_msgTypes[115] + mi := &file_common_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9082,7 +9239,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[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9095,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{115} + return file_common_proto_rawDescGZIP(), []int{117} } func (x *IntCondition) GetMin() int64 { @@ -9154,7 +9311,7 @@ type UintCondition struct { func (x *UintCondition) Reset() { *x = UintCondition{} - mi := &file_common_proto_msgTypes[116] + mi := &file_common_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9166,7 +9323,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[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9179,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{116} + return file_common_proto_rawDescGZIP(), []int{118} } func (x *UintCondition) GetMin() uint64 { @@ -9237,7 +9394,7 @@ type BoolCondition struct { func (x *BoolCondition) Reset() { *x = BoolCondition{} - mi := &file_common_proto_msgTypes[117] + mi := &file_common_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9249,7 +9406,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[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9262,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{117} + return file_common_proto_rawDescGZIP(), []int{119} } func (x *BoolCondition) GetValue() isBoolCondition_Value { @@ -9315,7 +9472,7 @@ type ExistsCondition struct { func (x *ExistsCondition) Reset() { *x = ExistsCondition{} - mi := &file_common_proto_msgTypes[118] + mi := &file_common_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9327,7 +9484,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[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9340,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{118} + return file_common_proto_rawDescGZIP(), []int{120} } func (x *ExistsCondition) GetIncludeNull() bool { @@ -9366,7 +9523,7 @@ type AddressMatch struct { func (x *AddressMatch) Reset() { *x = AddressMatch{} - mi := &file_common_proto_msgTypes[119] + mi := &file_common_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9378,7 +9535,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[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9391,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{119} + return file_common_proto_rawDescGZIP(), []int{121} } func (x *AddressMatch) GetMatch() isAddressMatch_Match { @@ -9487,7 +9644,7 @@ type PreparedQuery struct { func (x *PreparedQuery) Reset() { *x = PreparedQuery{} - mi := &file_common_proto_msgTypes[120] + mi := &file_common_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9499,7 +9656,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[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9512,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{120} + return file_common_proto_rawDescGZIP(), []int{122} } func (x *PreparedQuery) GetName() string { @@ -9536,19 +9693,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[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9560,7 +9721,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[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9573,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{121} + return file_common_proto_rawDescGZIP(), []int{123} } func (x *AggregatedVolume) GetAsset() string { @@ -9597,6 +9758,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 +9776,7 @@ type AggregateResult struct { func (x *AggregateResult) Reset() { *x = AggregateResult{} - mi := &file_common_proto_msgTypes[122] + mi := &file_common_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9620,7 +9788,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[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9633,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{122} + return file_common_proto_rawDescGZIP(), []int{124} } func (x *AggregateResult) GetVolumes() []*AggregatedVolume { @@ -9661,7 +9829,7 @@ type GroupedAggregateResult struct { func (x *GroupedAggregateResult) Reset() { *x = GroupedAggregateResult{} - mi := &file_common_proto_msgTypes[123] + mi := &file_common_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9673,7 +9841,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[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9686,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{123} + return file_common_proto_rawDescGZIP(), []int{125} } func (x *GroupedAggregateResult) GetPrefix() string { @@ -9718,7 +9886,7 @@ type PreparedQueryCursor struct { func (x *PreparedQueryCursor) Reset() { *x = PreparedQueryCursor{} - mi := &file_common_proto_msgTypes[124] + mi := &file_common_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9730,7 +9898,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[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9743,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{124} + return file_common_proto_rawDescGZIP(), []int{126} } func (x *PreparedQueryCursor) GetPageSize() uint32 { @@ -9807,7 +9975,7 @@ type LedgerStats struct { func (x *LedgerStats) Reset() { *x = LedgerStats{} - mi := &file_common_proto_msgTypes[125] + mi := &file_common_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9819,7 +9987,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[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9832,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{125} + return file_common_proto_rawDescGZIP(), []int{127} } func (x *LedgerStats) GetTransactionCount() uint64 { @@ -9920,7 +10088,7 @@ type PersistedConfig struct { func (x *PersistedConfig) Reset() { *x = PersistedConfig{} - mi := &file_common_proto_msgTypes[126] + mi := &file_common_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9932,7 +10100,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[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9945,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{126} + return file_common_proto_rawDescGZIP(), []int{128} } func (x *PersistedConfig) GetNodeId() uint64 { @@ -9998,7 +10166,7 @@ type CallerIdentity struct { func (x *CallerIdentity) Reset() { *x = CallerIdentity{} - mi := &file_common_proto_msgTypes[127] + mi := &file_common_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10010,7 +10178,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[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10023,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{127} + return file_common_proto_rawDescGZIP(), []int{129} } func (x *CallerIdentity) GetSubject() string { @@ -10094,7 +10262,7 @@ type CallerSnapshot struct { func (x *CallerSnapshot) Reset() { *x = CallerSnapshot{} - mi := &file_common_proto_msgTypes[128] + mi := &file_common_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10106,7 +10274,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[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10119,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{128} + return file_common_proto_rawDescGZIP(), []int{130} } func (x *CallerSnapshot) GetIdentity() *CallerIdentity { @@ -10157,7 +10325,7 @@ type S3StorageConfig struct { func (x *S3StorageConfig) Reset() { *x = S3StorageConfig{} - mi := &file_common_proto_msgTypes[129] + mi := &file_common_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10169,7 +10337,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[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10182,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{129} + return file_common_proto_rawDescGZIP(), []int{131} } func (x *S3StorageConfig) GetBucket() string { @@ -10233,7 +10401,7 @@ type AzureStorageConfig struct { func (x *AzureStorageConfig) Reset() { *x = AzureStorageConfig{} - mi := &file_common_proto_msgTypes[130] + mi := &file_common_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10245,7 +10413,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[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10258,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{130} + return file_common_proto_rawDescGZIP(), []int{132} } func (x *AzureStorageConfig) GetAccountName() string { @@ -10305,7 +10473,7 @@ type BackupStorage struct { func (x *BackupStorage) Reset() { *x = BackupStorage{} - mi := &file_common_proto_msgTypes[131] + mi := &file_common_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10317,7 +10485,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[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10330,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{131} + return file_common_proto_rawDescGZIP(), []int{133} } func (x *BackupStorage) GetProvider() isBackupStorage_Provider { @@ -10393,7 +10561,7 @@ type ReadOptions struct { func (x *ReadOptions) Reset() { *x = ReadOptions{} - mi := &file_common_proto_msgTypes[132] + mi := &file_common_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10405,7 +10573,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[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10418,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{132} + return file_common_proto_rawDescGZIP(), []int{134} } func (x *ReadOptions) GetCheckpointId() uint64 { @@ -10470,7 +10638,7 @@ type ListOptions struct { func (x *ListOptions) Reset() { *x = ListOptions{} - mi := &file_common_proto_msgTypes[133] + mi := &file_common_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10482,7 +10650,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[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10495,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{133} + return file_common_proto_rawDescGZIP(), []int{135} } func (x *ListOptions) GetRead() *ReadOptions { @@ -10570,12 +10738,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 +10774,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 +10797,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" + @@ -10894,10 +11065,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" + @@ -11174,11 +11346,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" + @@ -11304,7 +11477,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 +11542,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" + @@ -11433,135 +11608,135 @@ 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 - nil, // 155: common.PostCommitVolumes.VolumesByAccountEntry - nil, // 156: common.Account.MetadataEntry - nil, // 157: common.Account.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 + (*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 @@ -11579,232 +11754,232 @@ var file_common_proto_goTypes = []any{ } 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 + 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 - 152, // 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 - 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 - 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 - 7, // 88: common.SinkConfig.event_types:type_name -> common.EventType - 70, // 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 - 17, // 92: common.CreatedLedgerLog.created_at:type_name -> common.Timestamp - 34, // 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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 - 3, // 181: common.BuiltinUintCondition.field:type_name -> common.TransactionBuiltinIndex - 133, // 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 - 14, // 194: common.AddressMatch.role:type_name -> common.AddressRole - 119, // 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 - 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 - 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 + 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 + 156, // 12: common.PostCommitVolumes.volumes_by_account:type_name -> common.PostCommitVolumes.VolumesByAccountEntry + 27, // 13: common.AccountVolume.volumes:type_name -> common.VolumesWithBalance + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 165, // 151: common.LedgerInfo.account_types:type_name -> common.LedgerInfo.AccountTypesEntry + 12, // 152: common.LedgerInfo.default_enforcement_mode:type_name -> common.ChartEnforcementMode + 166, // 153: common.LedgerInfo.metadata:type_name -> common.LedgerInfo.MetadataEntry + 34, // 154: common.SaveMetadataCommand.target:type_name -> common.Target + 167, // 155: common.SaveMetadataCommand.metadata:type_name -> common.SaveMetadataCommand.MetadataEntry + 34, // 156: common.DeleteMetadataCommand.target:type_name -> common.Target + 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 + 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 + 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 + 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 + 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 - 115, // 219: common.CreatedLedgerLog.AccountTypesEntry.value:type_name -> common.AccountType + 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 - 115, // 222: common.LedgerInfo.AccountTypesEntry.value:type_name -> common.AccountType + 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 - 111, // 226: common.AccountType.SegmentTypesEntry.value:type_name -> common.SegmentType + 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 @@ -11831,17 +12006,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 +12045,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 +12070,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), @@ -11918,34 +12093,34 @@ func file_common_proto_init() { (*QueryFilter_LogBuiltinUint)(nil), (*QueryFilter_AccountHasAsset)(nil), } - file_common_proto_msgTypes[113].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[114].OneofWrappers = []any{ + file_common_proto_msgTypes[116].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{} + file_common_proto_msgTypes[119].OneofWrappers = []any{ (*BoolCondition_Hardcoded)(nil), (*BoolCondition_Param)(nil), } - file_common_proto_msgTypes[119].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[127].OneofWrappers = []any{ + file_common_proto_msgTypes[129].OneofWrappers = []any{ (*CallerIdentity_Issuer)(nil), (*CallerIdentity_KeyId)(nil), } - file_common_proto_msgTypes[131].OneofWrappers = []any{ + file_common_proto_msgTypes[133].OneofWrappers = []any{ (*BackupStorage_S3)(nil), (*BackupStorage_Azure)(nil), } 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/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 } 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..095f16aafb --- /dev/null +++ b/tests/e2e/business/color_numscript_test.go @@ -0,0 +1,326 @@ +//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.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.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()) + }) + + 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.UnsignedApplyRequest("", + 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.UnsignedApplyRequest("", + 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.UnsignedApplyRequest("", + 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.UnsignedApplyRequest("", + 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.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()) + }) + + 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.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.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.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() + Expect(createdTx).NotTo(BeNil()) + revertTargetTxID = createdTx.Transaction.GetId() + }) + + It("Should drive the revert against the same color bucket", func() { + _, err := sharedClient.Apply(sharedCtx, servicepb.UnsignedApplyRequest("", + 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..1ff027e6cf --- /dev/null +++ b/tests/e2e/business/color_segregation_test.go @@ -0,0 +1,166 @@ +//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.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.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()) + }) + + 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.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.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.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) { + 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_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")) }) }) 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()) } } }