Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f46f4bc
feat!: color of money — segregated balances per (account, asset, color)
gfyrag Jun 24, 2026
db6aa45
fix(e2e): drop residual GetVolumes()["USD"] + adapt color specs to Un…
gfyrag Jun 24, 2026
243173e
chore: post-rebase pass version.Info to NewHandler in aggregate test …
gfyrag Jun 25, 2026
84d1e8c
fix(clickhouse): add color column to typed JSON posting schema
gfyrag Jun 25, 2026
d701f51
fix(antithesis): migrate workload to flat Account/PostCommitVolumes s…
gfyrag Jun 26, 2026
6f7dc29
chore: post-rebase fixups after EN-1378 + EN-868 + #569 + #573
gfyrag Jun 29, 2026
1ad83dd
fix(color): surface malformed volume keys + emit empty color in receipts
gfyrag Jun 29, 2026
a9d00ad
feat(color-of-money): address NumaryBot review findings
Jul 10, 2026
6eb6b4e
test(color-of-money): assert result() error in aggregate tests (review)
Jul 10, 2026
03c2ce9
fix(color-of-money): address flemzord + NumaryBot review findings
Jul 10, 2026
fbd6ac0
fix(color-of-money): mark color required on always-emitting response …
Jul 10, 2026
9e53a1a
fix(color-of-money): disambiguate unresolved Numscript insufficient-f…
Jul 12, 2026
0873c86
fix(color-of-money): always emit color on prepared-query aggregate + …
Jul 12, 2026
4bbcb9e
fix(color-of-money): prepared-query envelope + flow-signature legacy …
Jul 12, 2026
4b8c1d1
docs(color-of-money): name prepared-query response discriminator as e…
Jul 12, 2026
ebcace2
docs(color-of-money): drop LOGS from prepared-query cursor row types
Jul 12, 2026
ce9d795
Merge remote-tracking branch 'origin/release/v3.0' into feat/color-of…
Jul 12, 2026
3c19d80
Merge remote-tracking branch 'origin/release/v3.0' into feat/color-of…
Jul 13, 2026
40dd6ad
test(EN-1465): assert EN-1503 LOGS execute over the camelCase cursor …
Jul 13, 2026
dcba389
Merge remote-tracking branch 'origin/release/v3.0' into feat/color-of…
Jul 13, 2026
af88ca8
test: pass color arg to NewVolumeKey in overlay_scope_test
Jul 13, 2026
270429b
test(e2e): use Account.FindVolume in reference_test after color merge
Jul 13, 2026
89ea1f9
Merge remote-tracking branch 'origin/release/v3.0' into feat/color-of…
Jul 13, 2026
31abb8e
Merge remote-tracking branch 'origin/release/v3.0' into feat/color-of…
Jul 14, 2026
4ab2ac3
fix(color-of-money): address review nits — dead code, dedup comparato…
Jul 14, 2026
f1b94bd
fix(color-of-money): migrate restore_test volumes assertions to FindV…
Jul 14, 2026
697bf34
Merge remote-tracking branch 'origin/release/v3.0' into feat/color-of…
Jul 14, 2026
775c886
merge release/v3.0: adopt oracle.VolumeKey in antithesis driver model
Jul 15, 2026
e216edd
fix(test): update processor_mirror_test NewVolumeKey calls to 4-arg c…
Jul 16, 2026
2b24b5c
fix(test): migrate restore e2e cluster tests to Account.FindVolume (s…
Jul 16, 2026
7b95ec8
fix(color): checker fails loud on unparseable volume key + doc sync
Jul 16, 2026
b28f297
docs(openapi): document logData in prepared-query cursor + finish vol…
Jul 16, 2026
7388020
docs(storage): sync volume/ledger-scoped key layout to color-bearing …
Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/ledgerctl/accounts/aggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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(),
Expand All @@ -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() {
Expand All @@ -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),
Expand Down
24 changes: 12 additions & 12 deletions cmd/ledgerctl/accounts/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package accounts
import (
"errors"
"fmt"
"sort"

"github.com/pterm/pterm"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -129,27 +128,28 @@ 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
if balance != "" && balance[0] == '-' {
balanceColor = pterm.Red
}

displayColor := entry.GetColor()
if displayColor == "" {
displayColor = "-"
}

volumesTable = append(volumesTable, []string{
asset,
entry.GetAsset(),
displayColor,
vol.GetInput(),
vol.GetOutput(),
balanceColor(balance),
Expand Down
6 changes: 4 additions & 2 deletions cmd/ledgerctl/queries/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,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(),
})
Expand All @@ -249,10 +250,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(),
})
Expand Down
29 changes: 21 additions & 8 deletions cmd/ledgerctl/transactions/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
Expand Down Expand Up @@ -460,17 +460,22 @@ 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(),
"→",
posting.GetDestination(),
posting.GetAmount().Dec(),
posting.GetAsset(),
color,
})
}

Expand Down Expand Up @@ -512,28 +517,36 @@ 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)
if !ok {
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.
Expand Down
27 changes: 12 additions & 15 deletions cmd/ledgerctl/transactions/display_volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(),
})
Expand Down
10 changes: 7 additions & 3 deletions cmd/ledgerctl/transactions/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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]
Expand All @@ -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,
})
}
}
Expand Down
7 changes: 6 additions & 1 deletion cmd/ledgerctl/transactions/revert.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,17 +233,22 @@ 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(),
"→",
posting.GetDestination(),
posting.GetAmount().Dec(),
posting.GetAsset(),
color,
})
}

Expand Down
20 changes: 19 additions & 1 deletion docs/ops/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1341,7 +1341,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 |
Expand Down Expand Up @@ -1372,8 +1372,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
Expand Down
1 change: 1 addition & 0 deletions docs/technical/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Cross-cutting concerns (overall design, data flows shared across subsystems, the
|----------|-----|
| [data-flows.md](data-flows.md) | Sequence diagrams that span multiple subsystems. |
| [data-model.md](data-model.md) | Ledgers, transactions, postings — the core domain shape. |
| [data-model/color-of-money.md](data-model/color-of-money.md) | Per-bucket "color of money" balance segregation — key encoding, numscript contract, collapse modes. |
| [audit-vs-technical-state.md](audit-vs-technical-state.md) | Boundary between business truth, governance truth, operational state, and rebuildable projections. |
| [primitives/uint256-wire-format.md](primitives/uint256-wire-format.md) | Fixed-size monetary-amount wire format used everywhere on the boundary. |

Expand Down
Loading
Loading