From 1b159d6bf4a6848e0cc9eaca21eef5986c464cc3 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 3 Jun 2026 10:05:30 +0200 Subject: [PATCH 01/12] feat!: expose color as a first-class Posting property MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Color is now carried as a dedicated field on the output Posting type and as a separate dimension throughout the interpreter, instead of being encoded as an asset suffix (`USD_RED`). This makes the numscript ↔ host contract clean: downstream consumers (ledgers, indexers, reporting) can segregate balances by (account, asset, color) without having to parse asset strings. BREAKING CHANGES: - Posting gains `Color string` (json `"color"`). - BalanceQuery shape: `map[string][]string` → `map[string][]AssetColor` where `AssetColor{Asset, Color string}`. Store implementations must honor color as a separate filter rather than splitting suffixed assets. - Balances shape: `map[account]map[asset]*big.Int` → `map[account]map[asset]map[color]*big.Int`. The (asset, color) pair is now an explicit key. `Balances.UnmarshalJSON` accepts both the new colored form and the legacy `{asset: amount}` shorthand (parsed as the uncolored bucket) so existing JSON fixtures keep working without a forced migration. - `coloredAsset()` helper removed; nothing in the public or internal API encodes color into the asset string anymore. - New public helper `Uncolored(amount)` to ergonomically build a single-bucket `ColorBalance` in tests / static fixtures. The semantics already exercised by the experimental-asset-colors feature flag stay the same — only the wire format changes. Existing colored .num specs were migrated to the new shape and now assert on `posting.color` in addition to `posting.asset`. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/cmd/test_init.go | 19 +- internal/cmd/test_init_test.go | 2 +- .../__snapshots__/balances_test.snap | 8 +- internal/interpreter/asset_scaling.go | 16 +- internal/interpreter/balances.go | 167 ++++-- internal/interpreter/balances_test.go | 208 ++++++- internal/interpreter/batch_balances_query.go | 10 +- internal/interpreter/color_semantics_test.go | 548 ++++++++++++++++++ .../interpreter/funds_queue_color_test.go | 77 +++ internal/interpreter/interpreter.go | 57 +- internal/interpreter/interpreter_test.go | 187 ++++-- .../color-inorder-send-all.num.specs.json | 17 +- .../asset-colors/color-inorder.num.specs.json | 17 +- ...-balance-when-missing-funds.num.specs.json | 6 +- .../color-restrict-balance.num.specs.json | 9 +- ...lor-restriction-in-send-all.num.specs.json | 7 +- .../color-send-overdrat.num.specs.json | 3 +- .../asset-colors/color-send.num.specs.json | 3 +- .../color-with-asset-precision.num.specs.json | 3 +- ...pending-in-colored-send-all.num.specs.json | 12 +- ...le-spending-in-colored-send.num.specs.json | 12 +- internal/mcp_impl/handlers.go | 29 +- .../__snapshots__/runner_test.snap | 21 +- internal/specs_format/index.go | 20 +- internal/specs_format/parse_test.go | 4 +- internal/specs_format/run_test.go | 16 +- numscript.go | 11 +- numscript_test.go | 46 +- 28 files changed, 1303 insertions(+), 232 deletions(-) create mode 100644 internal/interpreter/color_semantics_test.go create mode 100644 internal/interpreter/funds_queue_color_test.go diff --git a/internal/cmd/test_init.go b/internal/cmd/test_init.go index 9027b2c6..d98d025f 100644 --- a/internal/cmd/test_init.go +++ b/internal/cmd/test_init.go @@ -206,18 +206,25 @@ type TestInitStore struct { func (s TestInitStore) GetBalances(_ context.Context, q interpreter.BalanceQuery) (interpreter.Balances, error) { outputBalance := interpreter.Balances{} - for queriedAccount, queriedCurrencies := range q { - - for _, curr := range queriedCurrencies { - amt := utils.NestedMapGetOrPutDefault(s.Balances, queriedAccount, curr, func() *big.Int { + for queriedAccount, queriedItems := range q { + for _, item := range queriedItems { + accBalance := utils.MapGetOrPutDefault(s.Balances, queriedAccount, func() interpreter.AccountBalance { + return interpreter.AccountBalance{} + }) + colorMap := utils.MapGetOrPutDefault(accBalance, item.Asset, func() interpreter.ColorBalance { + return interpreter.ColorBalance{} + }) + amt := utils.MapGetOrPutDefault(colorMap, item.Color, func() *big.Int { return new(big.Int).Set(s.DefaultBalance) }) outputAccountBalance := utils.MapGetOrPutDefault(outputBalance, queriedAccount, func() interpreter.AccountBalance { return interpreter.AccountBalance{} }) - - outputAccountBalance[curr] = new(big.Int).Set(amt) + outputColorMap := utils.MapGetOrPutDefault(outputAccountBalance, item.Asset, func() interpreter.ColorBalance { + return interpreter.ColorBalance{} + }) + outputColorMap[item.Color] = new(big.Int).Set(amt) } } diff --git a/internal/cmd/test_init_test.go b/internal/cmd/test_init_test.go index f6fa865f..13b88aca 100644 --- a/internal/cmd/test_init_test.go +++ b/internal/cmd/test_init_test.go @@ -20,7 +20,7 @@ func TestMakeSpecsFileRetryForMissingFunds(t *testing.T) { require.Nil(t, err) require.Equal(t, interpreter.Balances{ "alice": interpreter.AccountBalance{ - "USD/2": big.NewInt(10000), + "USD/2": interpreter.Uncolored(big.NewInt(10000)), }, }, out.Balances) } diff --git a/internal/interpreter/__snapshots__/balances_test.snap b/internal/interpreter/__snapshots__/balances_test.snap index b3ac285e..7c7b89b1 100755 --- a/internal/interpreter/__snapshots__/balances_test.snap +++ b/internal/interpreter/__snapshots__/balances_test.snap @@ -1,7 +1,7 @@ [TestPrettyPrintBalance - 1] -| Account | Asset  | Balance | -| alice | EUR/2 | 1 | -| alice | USD/1234 | 999999 | -| bob | BTC | 3 | +| Account | Asset  | Color | Balance | +| alice | EUR/2 | | 1 | +| alice | USD/1234 | | 999999 | +| bob | BTC | | 3 | --- diff --git a/internal/interpreter/asset_scaling.go b/internal/interpreter/asset_scaling.go index 48c5fd41..12a46569 100644 --- a/internal/interpreter/asset_scaling.go +++ b/internal/interpreter/asset_scaling.go @@ -38,13 +38,21 @@ func getAssetScale(asset string) (string, int64) { return asset, 0 } +// getAssets returns, for a given baseAsset, the per-scale uncolored balance. +// Asset scaling operates on the uncolored bucket only — colored funds are not +// implicitly converted across scales. func getAssets(balance AccountBalance, baseAsset string) map[int64]*big.Int { result := make(map[int64]*big.Int) - for asset, amount := range balance { - if strings.HasPrefix(asset, baseAsset) { - _, scale := getAssetScale(asset) - result[scale] = amount + for asset, colorMap := range balance { + if !strings.HasPrefix(asset, baseAsset) { + continue + } + amount, ok := colorMap[""] + if !ok { + continue } + _, scale := getAssetScale(asset) + result[scale] = amount } return result } diff --git a/internal/interpreter/balances.go b/internal/interpreter/balances.go index 36f63c61..8ef33f13 100644 --- a/internal/interpreter/balances.go +++ b/internal/interpreter/balances.go @@ -1,111 +1,186 @@ package interpreter import ( + "encoding/json" + "fmt" "math/big" - "strings" "github.com/formancehq/numscript/internal/utils" ) -func (b Balances) DeepClone() Balances { - cloned := make(Balances) - for account, accountBalances := range b { - for asset, amount := range accountBalances { - utils.NestedMapGetOrPutDefault(cloned, account, asset, func() *big.Int { - return new(big.Int).Set(amount) - }) +// Uncolored builds a ColorBalance holding a single amount under the empty +// color key (the "no color" bucket). It is meant to keep test setup terse. +func Uncolored(amount *big.Int) ColorBalance { + return ColorBalance{"": amount} +} + +// UnmarshalJSON accepts two JSON shapes for a Balances value: +// +// 1. Flat (uncolored shorthand): +// {"alice": {"USD/2": 100, "EUR/2": -42}} +// Every asset gets a single entry under the "" (no color) bucket. +// +// 2. Colored (full form): +// {"alice": {"USD/2": {"": 100, "GRANTS": 50}}} +// +// Shapes can be mixed across assets within the same document. +func (b *Balances) UnmarshalJSON(data []byte) error { + raw := map[string]map[string]json.RawMessage{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + out := Balances{} + for account, assets := range raw { + accB := AccountBalance{} + out[account] = accB + for asset, rawVal := range assets { + colorMap, err := decodeColorBalance(rawVal) + if err != nil { + return fmt.Errorf("balances[%s][%s]: %w", account, asset, err) + } + accB[asset] = colorMap } } - return cloned + *b = out + return nil } -func coloredAsset(asset string, color *string) string { - if color == nil || *color == "" { - return asset +func decodeColorBalance(data json.RawMessage) (ColorBalance, error) { + // Try the shorthand: a single number meaning the uncolored bucket. + var amount json.Number + if err := json.Unmarshal(data, &amount); err == nil { + n, ok := new(big.Int).SetString(amount.String(), 10) + if !ok { + return nil, fmt.Errorf("invalid integer amount %q", amount.String()) + } + return ColorBalance{"": n}, nil } - // note: 1 <= len(parts) <= 2 - parts := strings.Split(asset, "/") - - coloredAsset := parts[0] + "_" + *color - if len(parts) > 1 { - coloredAsset += "/" + parts[1] + // Otherwise expect a {color: amount} object. + raw := map[string]json.Number{} + if err := json.Unmarshal(data, &raw); err != nil { + return nil, fmt.Errorf("expected integer or {color: amount} object, got %s", string(data)) + } + out := ColorBalance{} + for color, amt := range raw { + n, ok := new(big.Int).SetString(amt.String(), 10) + if !ok { + return nil, fmt.Errorf("color %q: invalid integer amount %q", color, amt.String()) + } + out[color] = n } - return coloredAsset + return out, nil } -// Get the (account, asset) tuple from the Balances -// if the tuple is not present, it will write a big.NewInt(0) in it and return it -func (b Balances) fetchBalance(account string, uncoloredAsset string, color string) *big.Int { - return utils.NestedMapGetOrPutDefault(b, account, coloredAsset(uncoloredAsset, &color), func() *big.Int { - return new(big.Int) - }) +func (b Balances) DeepClone() Balances { + cloned := make(Balances) + for account, accountBalances := range b { + clonedAcc := AccountBalance{} + cloned[account] = clonedAcc + for asset, colorMap := range accountBalances { + clonedColors := ColorBalance{} + clonedAcc[asset] = clonedColors + for color, amount := range colorMap { + clonedColors[color] = new(big.Int).Set(amount) + } + } + } + return cloned } -func (b Balances) has(account string, asset string) bool { - accountBalances := utils.MapGetOrPutDefault(b, account, func() AccountBalance { +// Get the (account, asset, color) balance from Balances. +// If the entry is not present, it will write a big.NewInt(0) in it and return it. +func (b Balances) fetchBalance(account string, asset string, color string) *big.Int { + accBalance := utils.MapGetOrPutDefault(b, account, func() AccountBalance { return AccountBalance{} }) + colorMap := utils.MapGetOrPutDefault(accBalance, asset, func() ColorBalance { + return ColorBalance{} + }) + return utils.MapGetOrPutDefault(colorMap, color, func() *big.Int { + return new(big.Int) + }) +} - _, ok := accountBalances[asset] +func (b Balances) has(account string, asset string, color string) bool { + accountBalances, ok := b[account] + if !ok { + return false + } + colorMap, ok := accountBalances[asset] + if !ok { + return false + } + _, ok = colorMap[color] return ok } -// given a BalanceQuery, return a new query which only contains needed (asset, account) pairs +// given a BalanceQuery, return a new query which only contains needed (asset, color) pairs // (that is, the ones that aren't already cached) func (b Balances) filterQuery(q BalanceQuery) BalanceQuery { filteredQuery := BalanceQuery{} - for accountName, queriedCurrencies := range q { - filteredCurrencies := utils.Filter(queriedCurrencies, func(currency string) bool { - return !b.has(accountName, currency) + for accountName, queriedItems := range q { + filteredItems := utils.Filter(queriedItems, func(item AssetColor) bool { + return !b.has(accountName, item.Asset, item.Color) }) - if len(filteredCurrencies) > 0 { - filteredQuery[accountName] = filteredCurrencies + if len(filteredItems) > 0 { + filteredQuery[accountName] = filteredItems } - } return filteredQuery } // Merge balances by adding balances in the "update" arg func (b Balances) Merge(update Balances) { - // merge queried balance for acc, accBalances := range update { cachedAcc := utils.MapGetOrPutDefault(b, acc, func() AccountBalance { return AccountBalance{} }) - for curr, amt := range accBalances { - cachedAcc[curr] = amt + for asset, colorMap := range accBalances { + cachedColors := utils.MapGetOrPutDefault(cachedAcc, asset, func() ColorBalance { + return ColorBalance{} + }) + for color, amt := range colorMap { + cachedColors[color] = amt + } } } } func (b Balances) PrettyPrint() string { - header := []string{"Account", "Asset", "Balance"} + header := []string{"Account", "Asset", "Color", "Balance"} var rows [][]string for account, accBalances := range b { - for asset, balance := range accBalances { - row := []string{account, asset, balance.String()} - rows = append(rows, row) + for asset, colorMap := range accBalances { + for color, balance := range colorMap { + rows = append(rows, []string{account, asset, color, balance.String()}) + } } } return utils.CsvPretty(header, rows, true) } func CompareBalances(b1 Balances, b2 Balances) bool { - return utils.Map2Cmp(b1, b2, func(ab1, ab2 *big.Int) bool { - return ab1.Cmp(ab2) == 0 + return utils.MapCmp(b1, b2, func(ab1, ab2 AccountBalance) bool { + return utils.MapCmp(ab1, ab2, func(cm1, cm2 ColorBalance) bool { + return utils.MapCmp(cm1, cm2, func(a1, a2 *big.Int) bool { + return a1.Cmp(a2) == 0 + }) + }) }) } // Returns whether the first value is a subset of the second one func CompareBalancesIncluding(b1 Balances, b2 Balances) bool { return utils.MapIncludes(b2, b1, func(a2 AccountBalance, a1 AccountBalance) bool { - return utils.MapIncludes(a2, a1, func(a2 *big.Int, a1 *big.Int) bool { - return a2.Cmp(a1) == 0 + return utils.MapIncludes(a2, a1, func(cm2, cm1 ColorBalance) bool { + return utils.MapIncludes(cm2, cm1, func(x2, x1 *big.Int) bool { + return x2.Cmp(x1) == 0 + }) }) }) } diff --git a/internal/interpreter/balances_test.go b/internal/interpreter/balances_test.go index d3d094be..6e73312f 100644 --- a/internal/interpreter/balances_test.go +++ b/internal/interpreter/balances_test.go @@ -11,52 +11,96 @@ import ( func TestFilterQuery(t *testing.T) { fullBalance := Balances{ "alice": AccountBalance{ - "EUR/2": big.NewInt(1), - "USD/2": big.NewInt(2), + "EUR/2": Uncolored(big.NewInt(1)), + "USD/2": Uncolored(big.NewInt(2)), }, "bob": AccountBalance{ - "BTC": big.NewInt(3), + "BTC": Uncolored(big.NewInt(3)), }, } filteredQuery := fullBalance.filterQuery(BalanceQuery{ - "alice": []string{"GBP/2", "YEN", "EUR/2"}, - "bob": []string{"BTC"}, - "charlie": []string{"ETH"}, + "alice": []AssetColor{{Asset: "GBP/2"}, {Asset: "YEN"}, {Asset: "EUR/2"}}, + "bob": []AssetColor{{Asset: "BTC"}}, + "charlie": []AssetColor{{Asset: "ETH"}}, }) require.Equal(t, BalanceQuery{ - "alice": []string{"GBP/2", "YEN"}, - "charlie": []string{"ETH"}, + "alice": []AssetColor{{Asset: "GBP/2"}, {Asset: "YEN"}}, + "charlie": []AssetColor{{Asset: "ETH"}}, + }, filteredQuery) +} + +func TestFilterQueryDistinguishesColors(t *testing.T) { + t.Parallel() + + fullBalance := Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{ + "": big.NewInt(1), + "GRANTS": big.NewInt(2), + }, + }, + } + + filteredQuery := fullBalance.filterQuery(BalanceQuery{ + "alice": []AssetColor{ + {Asset: "USD/2"}, + {Asset: "USD/2", Color: "GRANTS"}, + {Asset: "USD/2", Color: "OPS"}, + }, + }) + + require.Equal(t, BalanceQuery{ + "alice": []AssetColor{{Asset: "USD/2", Color: "OPS"}}, }, filteredQuery) } func TestCloneBalances(t *testing.T) { fullBalance := Balances{ "alice": AccountBalance{ - "EUR/2": big.NewInt(1), - "USD/2": big.NewInt(2), + "EUR/2": Uncolored(big.NewInt(1)), + "USD/2": Uncolored(big.NewInt(2)), }, "bob": AccountBalance{ - "BTC": big.NewInt(3), + "BTC": Uncolored(big.NewInt(3)), }, } cloned := fullBalance.DeepClone() - fullBalance["alice"]["USD/2"].Set(big.NewInt(42)) + fullBalance["alice"]["USD/2"][""].Set(big.NewInt(42)) - require.Equal(t, big.NewInt(2), cloned["alice"]["USD/2"]) + require.Equal(t, big.NewInt(2), cloned["alice"]["USD/2"][""]) +} + +func TestCloneBalancesPreservesColors(t *testing.T) { + t.Parallel() + + fullBalance := Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{ + "RED": big.NewInt(10), + "BLUE": big.NewInt(20), + }, + }, + } + + cloned := fullBalance.DeepClone() + fullBalance["alice"]["USD/2"]["RED"].Set(big.NewInt(999)) + + require.Equal(t, big.NewInt(10), cloned["alice"]["USD/2"]["RED"]) + require.Equal(t, big.NewInt(20), cloned["alice"]["USD/2"]["BLUE"]) } func TestPrettyPrintBalance(t *testing.T) { fullBalance := Balances{ "alice": AccountBalance{ - "EUR/2": big.NewInt(1), - "USD/1234": big.NewInt(999999), + "EUR/2": Uncolored(big.NewInt(1)), + "USD/1234": Uncolored(big.NewInt(999999)), }, "bob": AccountBalance{ - "BTC": big.NewInt(3), + "BTC": Uncolored(big.NewInt(3)), }, } @@ -67,34 +111,53 @@ func TestCmpMaps(t *testing.T) { b1 := Balances{ "alice": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, } b2 := Balances{ "alice": AccountBalance{ - "EUR": big.NewInt(42), + "EUR": Uncolored(big.NewInt(42)), }, } require.Equal(t, false, CompareBalances(b1, b2)) } +func TestCmpMapsDistinguishesColors(t *testing.T) { + t.Parallel() + + b1 := Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{"RED": big.NewInt(100)}, + }, + } + + b2 := Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{"BLUE": big.NewInt(100)}, + }, + } + + require.False(t, CompareBalances(b1, b2), + "same asset but different colors must not compare equal") +} + func TestCmpMapsIncluding(t *testing.T) { t.Run("including (subset)", func(t *testing.T) { b2 := Balances{ "alice": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, "bob": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, } b1 := Balances{ "alice": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, } @@ -104,16 +167,16 @@ func TestCmpMapsIncluding(t *testing.T) { t.Run("different value", func(t *testing.T) { b2 := Balances{ "alice": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, "bob": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, } b1 := Balances{ "alice": AccountBalance{ - "EUR": big.NewInt(0), + "EUR": Uncolored(big.NewInt(0)), }, } @@ -123,23 +186,112 @@ func TestCmpMapsIncluding(t *testing.T) { t.Run("extra value", func(t *testing.T) { b2 := Balances{ "alice": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, "bob": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, } b1 := Balances{ "alice": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, "extra-value": AccountBalance{ - "EUR": big.NewInt(100), + "EUR": Uncolored(big.NewInt(100)), }, } require.Equal(t, false, CompareBalancesIncluding(b1, b2)) }) + + t.Run("color-aware subset", func(t *testing.T) { + b2 := Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{ + "": big.NewInt(100), + "RED": big.NewInt(50), + "BLUE": big.NewInt(25), + }, + }, + } + + b1 := Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{"RED": big.NewInt(50)}, + }, + } + + require.True(t, CompareBalancesIncluding(b1, b2), + "colored subset of an asset must be considered included") + }) + + t.Run("missing color", func(t *testing.T) { + b2 := Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{"": big.NewInt(100)}, + }, + } + + b1 := Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{"RED": big.NewInt(50)}, + }, + } + + require.False(t, CompareBalancesIncluding(b1, b2), + "color present in subset but missing in superset must not be considered included") + }) +} + +func TestFetchBalanceCreatesEntriesLazily(t *testing.T) { + t.Parallel() + + b := Balances{} + got := b.fetchBalance("alice", "USD/2", "RED") + require.NotNil(t, got) + require.Equal(t, 0, got.Sign(), "freshly created entry should be zero") + + // mutating the returned big.Int should be reflected in subsequent fetches + got.SetInt64(42) + require.Equal(t, big.NewInt(42), b.fetchBalance("alice", "USD/2", "RED")) + + // distinct colors must be independent + require.Equal(t, big.NewInt(0), b.fetchBalance("alice", "USD/2", "BLUE")) +} + +func TestMergeBalancesIsColorAware(t *testing.T) { + t.Parallel() + + b := Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{"RED": big.NewInt(1)}, + }, + } + + b.Merge(Balances{ + "alice": AccountBalance{ + "USD/2": ColorBalance{ + "RED": big.NewInt(99), // overwrites + "BLUE": big.NewInt(50), // adds new color under existing asset + }, + "EUR/2": Uncolored(big.NewInt(7)), // adds new asset + }, + "bob": AccountBalance{ + "BTC": Uncolored(big.NewInt(3)), // adds new account + }, + }) + + require.Equal(t, big.NewInt(99), b["alice"]["USD/2"]["RED"]) + require.Equal(t, big.NewInt(50), b["alice"]["USD/2"]["BLUE"]) + require.Equal(t, big.NewInt(7), b["alice"]["EUR/2"][""]) + require.Equal(t, big.NewInt(3), b["bob"]["BTC"][""]) +} + +func TestUncoloredHelper(t *testing.T) { + t.Parallel() + + got := Uncolored(big.NewInt(42)) + require.Equal(t, ColorBalance{"": big.NewInt(42)}, got) } diff --git a/internal/interpreter/batch_balances_query.go b/internal/interpreter/batch_balances_query.go index 141d17ab..dd3de3a6 100644 --- a/internal/interpreter/batch_balances_query.go +++ b/internal/interpreter/batch_balances_query.go @@ -54,11 +54,15 @@ func (st *programState) batchQuery(account string, asset string, color *string) if account == "world" { return } - asset = coloredAsset(asset, color) + colorStr := "" + if color != nil { + colorStr = *color + } + item := AssetColor{Asset: asset, Color: colorStr} previousValues := st.CurrentBalanceQuery[account] - if !slices.Contains(previousValues, asset) { - st.CurrentBalanceQuery[account] = append(previousValues, asset) + if !slices.Contains(previousValues, item) { + st.CurrentBalanceQuery[account] = append(previousValues, item) } } diff --git a/internal/interpreter/color_semantics_test.go b/internal/interpreter/color_semantics_test.go new file mode 100644 index 00000000..4033dee7 --- /dev/null +++ b/internal/interpreter/color_semantics_test.go @@ -0,0 +1,548 @@ +package interpreter_test + +import ( + "context" + "encoding/json" + "math/big" + "testing" + + "github.com/formancehq/numscript/internal/flags" + machine "github.com/formancehq/numscript/internal/interpreter" + "github.com/formancehq/numscript/internal/parser" + "github.com/stretchr/testify/require" +) + +// runColored is a small helper that parses a script, evaluates it with the +// experimental-asset-colors feature flag enabled, and returns the postings. +// All test cases below exercise the public color-of-money interface — the +// idea is to exercise it from many angles so any future regression in the +// numscript ↔ ledger contract is caught here, not downstream. +func runColored(t *testing.T, src string, store machine.StaticStore) ([]machine.Posting, error) { + t.Helper() + parsed := parser.Parse(src) + require.Empty(t, parsed.Errors, "unexpected parser errors: %v", parsed.Errors) + + result, err := machine.RunProgram( + context.Background(), + parsed.Value, + machine.VariablesMap{}, + store, + map[string]struct{}{flags.ExperimentalAssetColors: {}}, + ) + if err != nil { + return nil, err + } + return result.Postings, nil +} + +// Single colored send: a posting emitted from a "RED"-restricted source must +// carry Color="RED". +func TestColorSendPropagatesColor(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 100] ( + source = @world \ "RED" + destination = @alice + ) + ` + store := machine.StaticStore{Balances: machine.Balances{}} + postings, err := runColored(t, src, store) + require.NoError(t, err) + + require.Equal(t, []machine.Posting{ + {Source: "world", Destination: "alice", Amount: big.NewInt(100), Asset: "COIN", Color: "RED"}, + }, postings) +} + +// Source-side color constraint: when a source restricts to a color, the +// emitted posting must carry that color and the funds must come from that +// exact bucket. +func TestColorSourceRestrictionEmitsColoredPosting(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 20] ( + source = @acc \ "RED" + destination = @dest + ) + ` + store := machine.StaticStore{Balances: machine.Balances{ + "acc": machine.AccountBalance{ + "COIN": machine.ColorBalance{ + "": big.NewInt(1000), + "RED": big.NewInt(50), + }, + }, + }} + + postings, err := runColored(t, src, store) + require.NoError(t, err) + + require.Equal(t, []machine.Posting{ + {Source: "acc", Destination: "dest", Amount: big.NewInt(20), Asset: "COIN", Color: "RED"}, + }, postings) +} + +// Colored funds are strictly segregated: insufficient funds in a color must +// fail even when other colors (and the uncolored bucket) have plenty. +func TestColorIsolationRejectsSpendFromWrongColor(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 100] ( + source = @acc \ "RED" + destination = @dest + ) + ` + store := machine.StaticStore{Balances: machine.Balances{ + "acc": machine.AccountBalance{ + "COIN": machine.ColorBalance{ + "": big.NewInt(10_000), + "BLUE": big.NewInt(10_000), + "RED": big.NewInt(20), + }, + }, + }} + + _, err := runColored(t, src, store) + require.Error(t, err) + var missing machine.MissingFundsErr + require.ErrorAs(t, err, &missing) + require.Equal(t, "COIN", missing.Asset) +} + +// "Color of money" is immutable: a posting emitted under color X stays +// color X end to end. We verify this by chaining sources and watching the +// emitted postings retain their original colors. +func TestColorImmutabilityThroughInorderSource(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 150] ( + source = { + @acc \ "RED" + @acc \ "BLUE" + @acc + } + destination = @dest + ) + ` + store := machine.StaticStore{Balances: machine.Balances{ + "acc": machine.AccountBalance{ + "COIN": machine.ColorBalance{ + "": big.NewInt(100), + "BLUE": big.NewInt(30), + "RED": big.NewInt(20), + }, + }, + }} + + postings, err := runColored(t, src, store) + require.NoError(t, err) + + require.Equal(t, []machine.Posting{ + {Source: "acc", Destination: "dest", Amount: big.NewInt(20), Asset: "COIN", Color: "RED"}, + {Source: "acc", Destination: "dest", Amount: big.NewInt(30), Asset: "COIN", Color: "BLUE"}, + {Source: "acc", Destination: "dest", Amount: big.NewInt(100), Asset: "COIN", Color: ""}, + }, postings) +} + +// The uncolored bucket (Color="") is not pooled with any colored bucket — +// asking for 100 with color="" fails when only colored funds are available. +func TestUncoloredCannotDrawFromColoredFunds(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 100] ( + source = @acc + destination = @dest + ) + ` + store := machine.StaticStore{Balances: machine.Balances{ + "acc": machine.AccountBalance{ + "COIN": machine.ColorBalance{ + "RED": big.NewInt(10_000), + }, + }, + }} + + _, err := runColored(t, src, store) + require.Error(t, err) + var missing machine.MissingFundsErr + require.ErrorAs(t, err, &missing) +} + +// Two adjacent colored postings with the same color from the same source +// should compact into a single posting (the funds queue logic). +func TestColoredPostingsCompactByColor(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 30] ( + source = { + @acc \ "RED" + @acc \ "RED" + } + destination = @dest + ) + ` + store := machine.StaticStore{Balances: machine.Balances{ + "acc": machine.AccountBalance{ + "COIN": machine.ColorBalance{ + "RED": big.NewInt(100), + }, + }, + }} + + postings, err := runColored(t, src, store) + require.NoError(t, err) + require.Len(t, postings, 1) + require.Equal(t, "RED", postings[0].Color) + require.Equal(t, big.NewInt(30), postings[0].Amount) +} + +// Colored balance queries must include color in BalanceQuery items. +// We observe the store to verify the contract that gets sent across the +// numscript ↔ ledger boundary. +func TestBalanceQueryIncludesColor(t *testing.T) { + t.Parallel() + + type observed struct { + machine.StaticStore + got []machine.BalanceQuery + } + store := &observed{StaticStore: machine.StaticStore{ + Balances: machine.Balances{ + "acc": machine.AccountBalance{ + "COIN": machine.ColorBalance{"RED": big.NewInt(1000)}, + }, + }, + }} + + src := ` + send [COIN 100] ( + source = @acc \ "RED" + destination = @dest + ) + ` + parsed := parser.Parse(src) + require.Empty(t, parsed.Errors) + + type spyStore struct{ inner machine.Store } + _ = spyStore{} + + // Wrap the store with an inline implementation that records GetBalances + // calls so we can assert what crosses the boundary. + var got []machine.BalanceQuery + spy := storeFunc{ + getBalances: func(ctx context.Context, q machine.BalanceQuery) (machine.Balances, error) { + cloned := machine.BalanceQuery{} + for acc, items := range q { + cloned[acc] = append([]machine.AssetColor(nil), items...) + } + got = append(got, cloned) + return store.StaticStore.GetBalances(ctx, q) + }, + getMetadata: store.StaticStore.GetAccountsMetadata, + } + + _, err := machine.RunProgram( + context.Background(), + parsed.Value, + machine.VariablesMap{}, + spy, + map[string]struct{}{flags.ExperimentalAssetColors: {}}, + ) + require.NoError(t, err) + + require.Len(t, got, 1, "expected exactly one batched balance query") + require.Equal(t, machine.BalanceQuery{ + "acc": {{Asset: "COIN", Color: "RED"}}, + }, got[0]) +} + +// storeFunc is a minimal machine.Store adapter built around function values. +type storeFunc struct { + getBalances func(context.Context, machine.BalanceQuery) (machine.Balances, error) + getMetadata func(context.Context, machine.MetadataQuery) (machine.AccountsMetadata, error) +} + +func (s storeFunc) GetBalances(ctx context.Context, q machine.BalanceQuery) (machine.Balances, error) { + return s.getBalances(ctx, q) +} + +func (s storeFunc) GetAccountsMetadata(ctx context.Context, q machine.MetadataQuery) (machine.AccountsMetadata, error) { + return s.getMetadata(ctx, q) +} + +// The legal color charset (^[A-Z]*$) is enforced — anything else must be +// rejected as a bad color literal. +func TestColorLiteralCharsetIsEnforced(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + color string + wantErr bool + }{ + {name: "uppercase ok", color: "RED", wantErr: false}, + {name: "empty ok (no color)", color: "", wantErr: false}, + {name: "lowercase rejected", color: "red", wantErr: true}, + {name: "digits rejected", color: "RED1", wantErr: true}, + {name: "punctuation rejected", color: "RED-FOO", wantErr: true}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + src := `send [COIN 1] (source = @world \ "` + tc.color + `" destination = @dest)` + parsed := parser.Parse(src) + require.Empty(t, parsed.Errors) + + _, err := machine.RunProgram( + context.Background(), + parsed.Value, + machine.VariablesMap{}, + machine.StaticStore{Balances: machine.Balances{}}, + map[string]struct{}{flags.ExperimentalAssetColors: {}}, + ) + + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +// Color survives a JSON marshal/unmarshal roundtrip on the Posting type. +func TestPostingJSONRoundtripPreservesColor(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 100] ( + source = @world \ "GRANTS" + destination = @alice + ) + ` + postings, err := runColored(t, src, machine.StaticStore{Balances: machine.Balances{}}) + require.NoError(t, err) + require.Len(t, postings, 1) + require.Equal(t, "GRANTS", postings[0].Color) + + encoded, err := json.Marshal(postings[0]) + require.NoError(t, err) + require.Contains(t, string(encoded), `"color":"GRANTS"`, + "marshaled posting must carry a non-empty color field, got: %s", string(encoded)) + + var decoded machine.Posting + require.NoError(t, json.Unmarshal(encoded, &decoded)) + require.Equal(t, postings[0].Color, decoded.Color) +} + +// Color is always present in JSON output, even when empty, so downstream +// consumers can rely on its presence to distinguish "uncolored" from +// "missing field". +func TestPostingJSONAlwaysIncludesColor(t *testing.T) { + t.Parallel() + + p := machine.Posting{ + Source: "world", + Destination: "dest", + Asset: "COIN", + Amount: big.NewInt(1), + } + encoded, err := json.Marshal(p) + require.NoError(t, err) + require.Contains(t, string(encoded), `"color":""`, + "uncolored postings must still expose the color field, got: %s", string(encoded)) +} + +// Allocation-style send: one source feeding multiple destinations under a +// color constraint. Every emitted posting must carry the source's color. +func TestColoredAllocationPropagatesToEachLeg(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 100] ( + source = @bank \ "GRANTS" + destination = { + 50% to @alice + remaining to @bob + } + ) + ` + store := machine.StaticStore{Balances: machine.Balances{ + "bank": machine.AccountBalance{ + "COIN": machine.ColorBalance{"GRANTS": big.NewInt(1000)}, + }, + }} + + postings, err := runColored(t, src, store) + require.NoError(t, err) + require.Len(t, postings, 2) + for _, p := range postings { + require.Equal(t, "GRANTS", p.Color, "every leg of the allocation must keep the color") + require.Equal(t, "COIN", p.Asset) + } +} + +// send * (send-all) from a colored bucket drains exactly that bucket and +// emits postings carrying the bucket's color. +func TestColoredSendAllDrainsOnlyTheTargetColor(t *testing.T) { + t.Parallel() + + src := ` + send [COIN *] ( + source = @vault \ "RED" + destination = @dest + ) + ` + store := machine.StaticStore{Balances: machine.Balances{ + "vault": machine.AccountBalance{ + "COIN": machine.ColorBalance{ + "": big.NewInt(1_000_000), + "RED": big.NewInt(42), + "BLUE": big.NewInt(999), + }, + }, + }} + + postings, err := runColored(t, src, store) + require.NoError(t, err) + require.Len(t, postings, 1) + require.Equal(t, "RED", postings[0].Color) + require.Equal(t, big.NewInt(42), postings[0].Amount) +} + +// Sending an empty-color amount must NOT pull from any colored bucket, +// even when the script forms the source from the union of accounts. +func TestUncoloredSourceIgnoresColoredFunds(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 50] ( + source = @vault + destination = @dest + ) + ` + store := machine.StaticStore{Balances: machine.Balances{ + "vault": machine.AccountBalance{ + "COIN": machine.ColorBalance{ + "": big.NewInt(20), // only 20 here — should fail + "RED": big.NewInt(1_000_000), + }, + }, + }} + + _, err := runColored(t, src, store) + require.Error(t, err, "uncolored source must not be able to dip into colored funds") + var missing machine.MissingFundsErr + require.ErrorAs(t, err, &missing) +} + +// Two distinct sources with two distinct colors must each contribute a +// posting bearing their own color (no accidental coalescing). +func TestTwoColoredSourcesYieldTwoColoredPostings(t *testing.T) { + t.Parallel() + + src := ` + send [COIN 60] ( + source = { + @a \ "RED" + @b \ "BLUE" + } + destination = @dest + ) + ` + store := machine.StaticStore{Balances: machine.Balances{ + "a": machine.AccountBalance{"COIN": machine.ColorBalance{"RED": big.NewInt(25)}}, + "b": machine.AccountBalance{"COIN": machine.ColorBalance{"BLUE": big.NewInt(100)}}, + }} + + postings, err := runColored(t, src, store) + require.NoError(t, err) + require.Len(t, postings, 2) + + require.Equal(t, "RED", postings[0].Color) + require.Equal(t, big.NewInt(25), postings[0].Amount) + + require.Equal(t, "BLUE", postings[1].Color) + require.Equal(t, big.NewInt(35), postings[1].Amount) +} + +// Colors play orthogonally with the asset precision suffix (e.g. USD/4) — +// the suffix stays on the asset string, the color rides separately. +func TestColorComposesWithAssetPrecision(t *testing.T) { + t.Parallel() + + src := ` + send [USD/4 10] ( + source = @src \ "COL" allowing unbounded overdraft + destination = @dest + ) + ` + postings, err := runColored(t, src, machine.StaticStore{Balances: machine.Balances{}}) + require.NoError(t, err) + require.Len(t, postings, 1) + require.Equal(t, "USD/4", postings[0].Asset) + require.Equal(t, "COL", postings[0].Color) +} + +// JSON unmarshal accepts both shapes (flat + colored), as documented in +// Balances.UnmarshalJSON. +func TestBalancesUnmarshalAcceptsBothShapes(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + src string + want machine.Balances + }{ + { + name: "flat shorthand", + src: `{"alice": {"USD/2": 100, "EUR/2": -42}}`, + want: machine.Balances{ + "alice": machine.AccountBalance{ + "USD/2": machine.Uncolored(big.NewInt(100)), + "EUR/2": machine.Uncolored(big.NewInt(-42)), + }, + }, + }, + { + name: "colored", + src: `{"alice": {"USD/2": {"": 100, "RED": 50}}}`, + want: machine.Balances{ + "alice": machine.AccountBalance{ + "USD/2": machine.ColorBalance{"": big.NewInt(100), "RED": big.NewInt(50)}, + }, + }, + }, + { + name: "mixed across assets", + src: `{"alice": {"USD/2": 100, "EUR/2": {"RED": 5}}}`, + want: machine.Balances{ + "alice": machine.AccountBalance{ + "USD/2": machine.Uncolored(big.NewInt(100)), + "EUR/2": machine.ColorBalance{"RED": big.NewInt(5)}, + }, + }, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var got machine.Balances + require.NoError(t, got.UnmarshalJSON([]byte(tc.src))) + require.True(t, machine.CompareBalances(tc.want, got), + "unexpected balances: want %v, got %v", tc.want, got) + }) + } +} diff --git a/internal/interpreter/funds_queue_color_test.go b/internal/interpreter/funds_queue_color_test.go new file mode 100644 index 00000000..9b09aa7a --- /dev/null +++ b/internal/interpreter/funds_queue_color_test.go @@ -0,0 +1,77 @@ +package interpreter + +// White-box tests for the color-awareness of the internal funds queue. +// These live in `package interpreter` because they reach into unexported +// types (Sender, newFundsQueue). The public color semantics — what the +// numscript ↔ ledger contract actually exposes — are covered in +// color_semantics_test.go (black-box). + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" +) + +// compactTop() relies on (Name, Color) equality. Adjacent senders that +// match on both dimensions collapse; those that disagree on either don't. +func TestFundsQueueCompactRespectsColor(t *testing.T) { + t.Parallel() + + queue := newFundsQueue([]Sender{ + {Name: "a", Color: "RED", Amount: big.NewInt(10)}, + {Name: "a", Color: "RED", Amount: big.NewInt(5)}, + {Name: "a", Color: "BLUE", Amount: big.NewInt(7)}, + }) + + out := queue.PullAnything(big.NewInt(22)) + require.Equal(t, []Sender{ + {Name: "a", Color: "RED", Amount: big.NewInt(15)}, + {Name: "a", Color: "BLUE", Amount: big.NewInt(7)}, + }, out) +} + +// PullColored only pulls senders that match the requested color, leaving +// the rest of the queue untouched and still drainable on subsequent pulls. +func TestFundsQueuePullColoredIsSelective(t *testing.T) { + t.Parallel() + + queue := newFundsQueue([]Sender{ + {Name: "a", Color: "RED", Amount: big.NewInt(10)}, + {Name: "b", Color: "BLUE", Amount: big.NewInt(20)}, + {Name: "c", Color: "RED", Amount: big.NewInt(30)}, + }) + + out := queue.PullColored(big.NewInt(35), "RED") + require.Equal(t, []Sender{ + {Name: "a", Color: "RED", Amount: big.NewInt(10)}, + {Name: "c", Color: "RED", Amount: big.NewInt(25)}, + }, out) + + remaining := queue.PullColored(big.NewInt(20), "BLUE") + require.Equal(t, []Sender{ + {Name: "b", Color: "BLUE", Amount: big.NewInt(20)}, + }, remaining) +} + +// PullUncolored is just PullColored("") — the empty bucket is the same as +// any other color from the queue's perspective. +func TestFundsQueuePullUncoloredIgnoresColored(t *testing.T) { + t.Parallel() + + queue := newFundsQueue([]Sender{ + {Name: "a", Color: "RED", Amount: big.NewInt(100)}, + {Name: "b", Color: "", Amount: big.NewInt(40)}, + }) + + out := queue.PullUncolored(big.NewInt(40)) + require.Equal(t, []Sender{ + {Name: "b", Color: "", Amount: big.NewInt(40)}, + }, out) + + // the RED sender is still there + remaining := queue.PullColored(big.NewInt(50), "RED") + require.Equal(t, []Sender{ + {Name: "a", Color: "RED", Amount: big.NewInt(50)}, + }, remaining) +} diff --git a/internal/interpreter/interpreter.go b/internal/interpreter/interpreter.go index 94b634af..0b7f7c9c 100644 --- a/internal/interpreter/interpreter.go +++ b/internal/interpreter/interpreter.go @@ -16,13 +16,26 @@ import ( type VariablesMap map[string]string -// For each account, list of the needed assets -type BalanceQuery map[string][]string +// AssetColor identifies a (asset, color) pair to query. +// Color is "" when no color is involved. +type AssetColor struct { + Asset string + Color string +} + +// For each account, list of the needed (asset, color) pairs +type BalanceQuery map[string][]AssetColor // For each account, list of the needed keys type MetadataQuery map[string][]string -type AccountBalance = map[string]*big.Int +// ColorBalance maps a color (or "" for the uncolored bucket) to an amount. +type ColorBalance = map[string]*big.Int + +// AccountBalance maps an asset to its per-color balances. +type AccountBalance = map[string]ColorBalance + +// Balances maps an account to its (asset, color) → amount table. type Balances map[string]AccountBalance type AccountMetadata = map[string]string @@ -44,7 +57,7 @@ func (s StaticStore) GetBalances(_ context.Context, q BalanceQuery) (Balances, e } outputBalance := Balances{} - for queriedAccount, queriedCurrencies := range q { + for queriedAccount, queriedItems := range q { outputAccountBalance := AccountBalance{} outputBalance[queriedAccount] = outputAccountBalance @@ -52,27 +65,35 @@ func (s StaticStore) GetBalances(_ context.Context, q BalanceQuery) (Balances, e return AccountBalance{} }) - for _, curr := range queriedCurrencies { - baseAsset, isCatchAll := strings.CutSuffix(curr, "/*") + for _, item := range queriedItems { + baseAsset, isCatchAll := strings.CutSuffix(item.Asset, "/*") if isCatchAll { - - for k, v := range accountBalanceLookup { - matchesAsset := k == baseAsset || strings.HasPrefix(k, baseAsset+"/") + for asset, colorMap := range accountBalanceLookup { + matchesAsset := asset == baseAsset || strings.HasPrefix(asset, baseAsset+"/") if !matchesAsset { continue } - outputAccountBalance[k] = new(big.Int).Set(v) + amt, ok := colorMap[item.Color] + if !ok { + continue + } + out := utils.MapGetOrPutDefault(outputAccountBalance, asset, func() ColorBalance { + return ColorBalance{} + }) + out[item.Color] = new(big.Int).Set(amt) } - } else { + out := utils.MapGetOrPutDefault(outputAccountBalance, item.Asset, func() ColorBalance { + return ColorBalance{} + }) n := new(big.Int) - outputAccountBalance[curr] = n - - if i, ok := accountBalanceLookup[curr]; ok { - n.Set(i) + out[item.Color] = n + if colorMap, ok := accountBalanceLookup[item.Asset]; ok { + if i, ok := colorMap[item.Color]; ok { + n.Set(i) + } } } - } } @@ -98,6 +119,7 @@ type Posting struct { Destination string `json:"destination"` Amount *big.Int `json:"amount"` Asset string `json:"asset"` + Color string `json:"color"` } type ExecutionResult struct { @@ -431,7 +453,8 @@ func (st *programState) pushReceiver(name string, monetary *big.Int) { postings := Posting{ Source: sender.Name, Destination: name, - Asset: coloredAsset(st.CurrentAsset, &sender.Color), + Asset: st.CurrentAsset, + Color: sender.Color, Amount: sender.Amount, } diff --git a/internal/interpreter/interpreter_test.go b/internal/interpreter/interpreter_test.go index 651d17b7..b927cd4a 100644 --- a/internal/interpreter/interpreter_test.go +++ b/internal/interpreter/interpreter_test.go @@ -35,25 +35,69 @@ func TestScripts(t *testing.T) { } type TestCase struct { - source string - program *parser.Program - vars map[string]string - meta machine.AccountsMetadata - balances map[string]map[string]*big.Int - expected CaseResult + source string + // program is set by compile() + program *parser.Program + vars map[string]string + meta machine.AccountsMetadata + // balances holds uncolored balances for tests that don't care about color. + // A non-empty color is conveyed via coloredBalances. + balances map[string]map[string]*big.Int + coloredBalances machine.Balances + expected CaseResult } func NewTestCase() TestCase { return TestCase{ - vars: make(map[string]string), - meta: machine.AccountsMetadata{}, - balances: make(map[string]map[string]*big.Int), + vars: make(map[string]string), + meta: machine.AccountsMetadata{}, + balances: make(map[string]map[string]*big.Int), + coloredBalances: machine.Balances{}, expected: CaseResult{ Error: nil, }, } } +// builtBalances merges the uncolored shorthand with any colored balances +// configured on the test case into a single machine.Balances value. +func (c *TestCase) builtBalances() machine.Balances { + out := machine.Balances{} + for acc, assets := range c.balances { + accB, ok := out[acc] + if !ok { + accB = machine.AccountBalance{} + out[acc] = accB + } + for asset, amt := range assets { + colorMap, ok := accB[asset] + if !ok { + colorMap = machine.ColorBalance{} + accB[asset] = colorMap + } + colorMap[""] = amt + } + } + for acc, assets := range c.coloredBalances { + accB, ok := out[acc] + if !ok { + accB = machine.AccountBalance{} + out[acc] = accB + } + for asset, colorMap := range assets { + merged, ok := accB[asset] + if !ok { + merged = machine.ColorBalance{} + accB[asset] = merged + } + for color, amt := range colorMap { + merged[color] = amt + } + } + } + return out +} + // returns a version of the error in which the range is normalized // to golang's default value func removeRange(e machine.InterpreterError) machine.InterpreterError { @@ -104,6 +148,20 @@ func (c *TestCase) setBalance(account string, asset string, amount int64) { c.balances[account][asset] = big.NewInt(amount) } +func (c *TestCase) setColoredBalance(account string, asset string, color string, amount int64) { + accB, ok := c.coloredBalances[account] + if !ok { + accB = machine.AccountBalance{} + c.coloredBalances[account] = accB + } + colorMap, ok := accB[asset] + if !ok { + colorMap = machine.ColorBalance{} + accB[asset] = colorMap + } + colorMap[color] = big.NewInt(amount) +} + func test(t *testing.T, testCase TestCase) { testWithFeatureFlag(t, testCase, "") } @@ -126,8 +184,8 @@ func testWithFeatureFlag(t *testing.T, testCase TestCase, flagName string) { *prog, testCase.vars, machine.StaticStore{ - testCase.balances, - testCase.meta, + Balances: testCase.builtBalances(), + Meta: testCase.meta, }, nil, ) @@ -142,8 +200,8 @@ func testWithFeatureFlag(t *testing.T, testCase TestCase, flagName string) { *prog, testCase.vars, machine.StaticStore{ - testCase.balances, - testCase.meta, + Balances: testCase.builtBalances(), + Meta: testCase.meta, }, featureFlags, ) @@ -161,32 +219,32 @@ func TestStaticStore(t *testing.T) { store := machine.StaticStore{ Balances: machine.Balances{ "a": machine.AccountBalance{ - "USD/2": big.NewInt(10), - "EUR/2": big.NewInt(1), + "USD/2": machine.Uncolored(big.NewInt(10)), + "EUR/2": machine.Uncolored(big.NewInt(1)), }, "b": machine.AccountBalance{ - "USD/2": big.NewInt(10), - "COIN": big.NewInt(11), + "USD/2": machine.Uncolored(big.NewInt(10)), + "COIN": machine.Uncolored(big.NewInt(11)), }, }, } q1, _ := store.GetBalances(context.TODO(), machine.BalanceQuery{ - "a": []string{"USD/2"}, + "a": []machine.AssetColor{{Asset: "USD/2"}}, }) require.Equal(t, machine.Balances{ "a": machine.AccountBalance{ - "USD/2": big.NewInt(10), + "USD/2": machine.Uncolored(big.NewInt(10)), }, }, q1) q2, _ := store.GetBalances(context.TODO(), machine.BalanceQuery{ - "b": []string{"USD/2", "COIN"}, + "b": []machine.AssetColor{{Asset: "USD/2"}, {Asset: "COIN"}}, }) require.Equal(t, machine.Balances{ "b": machine.AccountBalance{ - "USD/2": big.NewInt(10), - "COIN": big.NewInt(11), + "USD/2": machine.Uncolored(big.NewInt(10)), + "COIN": machine.Uncolored(big.NewInt(11)), }, }, q2) }) @@ -195,25 +253,78 @@ func TestStaticStore(t *testing.T) { store := machine.StaticStore{ Balances: machine.Balances{ "a": machine.AccountBalance{ - "USD": big.NewInt(1), - "USD/2": big.NewInt(2), - "USD/3": big.NewInt(3), + "USD": machine.Uncolored(big.NewInt(1)), + "USD/2": machine.Uncolored(big.NewInt(2)), + "USD/3": machine.Uncolored(big.NewInt(3)), }, }, } balances, err := store.GetBalances(context.Background(), machine.BalanceQuery{ - "a": []string{"USD/*"}, + "a": []machine.AssetColor{{Asset: "USD/*"}}, }) require.Nil(t, err) require.Equal(t, machine.Balances{ "a": machine.AccountBalance{ - "USD": big.NewInt(1), - "USD/2": big.NewInt(2), - "USD/3": big.NewInt(3), + "USD": machine.Uncolored(big.NewInt(1)), + "USD/2": machine.Uncolored(big.NewInt(2)), + "USD/3": machine.Uncolored(big.NewInt(3)), + }, + }, balances) + + }) + + t.Run("color dimension is honored", func(t *testing.T) { + store := machine.StaticStore{ + Balances: machine.Balances{ + "a": machine.AccountBalance{ + "USD/2": machine.ColorBalance{ + "": big.NewInt(100), + "GRANTS": big.NewInt(50), + "OPS": big.NewInt(25), + }, + }, + }, + } + + balances, err := store.GetBalances(context.Background(), machine.BalanceQuery{ + "a": []machine.AssetColor{ + {Asset: "USD/2"}, + {Asset: "USD/2", Color: "GRANTS"}, + }, + }) + require.NoError(t, err) + require.Equal(t, machine.Balances{ + "a": machine.AccountBalance{ + "USD/2": machine.ColorBalance{ + "": big.NewInt(100), + "GRANTS": big.NewInt(50), + }, }, }, balances) + }) + + t.Run("color filter is independent across assets in catchall", func(t *testing.T) { + store := machine.StaticStore{ + Balances: machine.Balances{ + "a": machine.AccountBalance{ + "USD": machine.ColorBalance{"": big.NewInt(1), "RED": big.NewInt(10)}, + "USD/2": machine.ColorBalance{"": big.NewInt(2), "RED": big.NewInt(20)}, + "USD/3": machine.ColorBalance{"": big.NewInt(3)}, + }, + }, + } + balances, err := store.GetBalances(context.Background(), machine.BalanceQuery{ + "a": []machine.AssetColor{{Asset: "USD/*", Color: "RED"}}, + }) + require.NoError(t, err) + require.Equal(t, machine.Balances{ + "a": machine.AccountBalance{ + "USD": machine.ColorBalance{"RED": big.NewInt(10)}, + "USD/2": machine.ColorBalance{"RED": big.NewInt(20)}, + }, + }, balances) }) } @@ -727,16 +838,16 @@ func TestTrackBalancesTricky(t *testing.T) { tc.expected = CaseResult{ Postings: []machine.Posting{ { - "world", - "src", - big.NewInt(10), - "GEM", + Source: "world", + Destination: "src", + Amount: big.NewInt(10), + Asset: "GEM", }, { - "src", - "dest", - big.NewInt(15), - "GEM", + Source: "src", + Destination: "dest", + Amount: big.NewInt(15), + Asset: "GEM", }, }, } @@ -1001,7 +1112,7 @@ func TestColorRestrictBalanceWhenMissingFunds(t *testing.T) { tc := NewTestCase() tc.setBalance("acc", "COIN", 100) - tc.setBalance("acc", "COIN_RED", 1) + tc.setColoredBalance("acc", "COIN", "RED", 1) tc.compile(t, script) tc.expected = CaseResult{ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json index 4efe1917..35835d81 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json @@ -7,9 +7,11 @@ "it": "-", "balances": { "src": { - "COIN": 100, - "COIN_BLUE": 30, - "COIN_RED": 20 + "COIN": { + "": 100, + "BLUE": 30, + "RED": 20 + } } }, "expect.postings": [ @@ -17,19 +19,22 @@ "source": "src", "destination": "dest", "amount": 20, - "asset": "COIN_RED" + "asset": "COIN", + "color": "RED" }, { "source": "src", "destination": "dest", "amount": 30, - "asset": "COIN_BLUE" + "asset": "COIN", + "color": "BLUE" }, { "source": "src", "destination": "dest", "amount": 100, - "asset": "COIN" + "asset": "COIN", + "color": "" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json index 2b306b8f..e95510b6 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json @@ -7,9 +7,11 @@ "it": "-", "balances": { "src": { - "COIN": 100, - "COIN_BLUE": 30, - "COIN_RED": 20 + "COIN": { + "": 100, + "BLUE": 30, + "RED": 20 + } } }, "expect.postings": [ @@ -17,19 +19,22 @@ "source": "src", "destination": "dest", "amount": 20, - "asset": "COIN_RED" + "asset": "COIN", + "color": "RED" }, { "source": "src", "destination": "dest", "amount": 30, - "asset": "COIN_BLUE" + "asset": "COIN", + "color": "BLUE" }, { "source": "src", "destination": "dest", "amount": 50, - "asset": "COIN" + "asset": "COIN", + "color": "" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json index f71564aa..73d828d2 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json @@ -5,8 +5,10 @@ "it": "-", "balances": { "acc": { - "COIN": 100, - "COIN_RED": 1 + "COIN": { + "": 100, + "RED": 1 + } } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance.num.specs.json index a4d32470..93c4e32c 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance.num.specs.json @@ -7,8 +7,10 @@ "it": "-", "balances": { "acc": { - "COIN": 1, - "COIN_RED": 100 + "COIN": { + "": 1, + "RED": 100 + } } }, "expect.postings": [ @@ -16,7 +18,8 @@ "source": "acc", "destination": "dest", "amount": 20, - "asset": "COIN_RED" + "asset": "COIN", + "color": "RED" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restriction-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restriction-in-send-all.num.specs.json index 8bde6bea..00cde547 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restriction-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restriction-in-send-all.num.specs.json @@ -7,7 +7,9 @@ "it": "-", "balances": { "src": { - "COIN_RED": 42 + "COIN": { + "RED": 42 + } } }, "expect.postings": [ @@ -15,7 +17,8 @@ "source": "src", "destination": "dest", "amount": 42, - "asset": "COIN_RED" + "asset": "COIN", + "color": "RED" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send-overdrat.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send-overdrat.num.specs.json index 5cfde80f..1c824d8e 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send-overdrat.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send-overdrat.num.specs.json @@ -10,7 +10,8 @@ "source": "acc", "destination": "dest", "amount": 100, - "asset": "COIN_RED" + "asset": "COIN", + "color": "RED" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send.num.specs.json index 4288aed7..31d2e41b 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-send.num.specs.json @@ -10,7 +10,8 @@ "source": "world", "destination": "dest", "amount": 100, - "asset": "COIN_RED" + "asset": "COIN", + "color": "RED" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-with-asset-precision.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-with-asset-precision.num.specs.json index f9b3821e..3bbe0218 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-with-asset-precision.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-with-asset-precision.num.specs.json @@ -10,7 +10,8 @@ "source": "src", "destination": "dest", "amount": 10, - "asset": "USD_COL/4" + "asset": "USD/4", + "color": "COL" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json index a1b7d2f9..bf1d2ad1 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json @@ -7,8 +7,10 @@ "it": "-", "balances": { "src": { - "COIN": 100, - "COIN_X": 20 + "COIN": { + "": 100, + "X": 20 + } } }, "expect.postings": [ @@ -16,13 +18,15 @@ "source": "src", "destination": "dest", "amount": 20, - "asset": "COIN_X" + "asset": "COIN", + "color": "X" }, { "source": "src", "destination": "dest", "amount": 100, - "asset": "COIN" + "asset": "COIN", + "color": "" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json index da4b831a..aafdd511 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json @@ -7,8 +7,10 @@ "it": "-", "balances": { "src": { - "COIN": 99999, - "COIN_X": 20 + "COIN": { + "": 99999, + "X": 20 + } } }, "expect.postings": [ @@ -16,13 +18,15 @@ "source": "src", "destination": "dest", "amount": 20, - "asset": "COIN_X" + "asset": "COIN", + "color": "X" }, { "source": "src", "destination": "dest", "amount": 80, - "asset": "COIN" + "asset": "COIN", + "color": "" } ] } diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index 93139eb9..11b38467 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -30,14 +30,31 @@ func parseBalancesJson(balancesRaw any) (interpreter.Balances, *mcp.CallToolResu return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected nested object for account %v", account)) } - for asset, amountRaw := range assets { - amount, ok := amountRaw.(float64) - if !ok { - return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected float for amount: %v", amountRaw)) + for asset, perAssetRaw := range assets { + if iBalances[account][asset] == nil { + iBalances[account][asset] = interpreter.ColorBalance{} + } + + // Accept either { "USD/2": 100 } (uncolored shorthand) or + // { "USD/2": { "": 100, "RED": 50 } } (full color form). + if amount, ok := perAssetRaw.(float64); ok { + n, _ := big.NewFloat(amount).Int(new(big.Int)) + iBalances[account][asset][""] = n + continue } - n, _ := big.NewFloat(amount).Int(new(big.Int)) - iBalances[account][asset] = n + colorMap, ok := perAssetRaw.(map[string]any) + if !ok { + return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected number or color map for %s/%s, got: <%#v>", account, asset, perAssetRaw)) + } + for color, amountRaw := range colorMap { + amount, ok := amountRaw.(float64) + if !ok { + return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected float for amount: %v", amountRaw)) + } + n, _ := big.NewFloat(amount).Int(new(big.Int)) + iBalances[account][asset][color] = n + } } } return iBalances, nil diff --git a/internal/specs_format/__snapshots__/runner_test.snap b/internal/specs_format/__snapshots__/runner_test.snap index 8eab7c2a..05f31887 100755 --- a/internal/specs_format/__snapshots__/runner_test.snap +++ b/internal/specs_format/__snapshots__/runner_test.snap @@ -33,7 +33,8 @@ GOT: + "source": "world",  "destination": "dest",  "amount": 100, - "asset": "USD/2" + "asset": "USD/2", + "color": ""  }  ] @@ -53,8 +54,8 @@ GOT:  GIVEN: -| Account | Asset | Balance | -| alice | USD/2 | 9999 | +| Account | Asset | Color | Balance | +| alice | USD/2 | | 9999 |  GOT: @@ -75,12 +76,16 @@ GOT:  {  "alice": { -- "USD/2": -100 -+ "USD/2": 9899 + "USD/2": { +- "": -100 ++ "": 9899 + }  },  "dest": { -- "USD/2": 1 -+ "USD/2": 100 + "USD/2": { +- "": 1 ++ "": 100 + }  }  } @@ -118,7 +123,7 @@ Error: example.num.specs.json  Error: example.num.specs.json -json: cannot unmarshal number into Go struct field Specs.balances of type interpreter.Balances +json: cannot unmarshal number into Go struct field Specs.balances of type map[string]map[string]json.RawMessage --- diff --git a/internal/specs_format/index.go b/internal/specs_format/index.go index cb9b48d3..c42ffe98 100644 --- a/internal/specs_format/index.go +++ b/internal/specs_format/index.go @@ -299,17 +299,25 @@ func getMovements(postings []interpreter.Posting) Movements { return m } +func fetchOrInsertBalance(b interpreter.Balances, account, asset, color string) *big.Int { + accBalance := utils.MapGetOrPutDefault(b, account, func() interpreter.AccountBalance { + return interpreter.AccountBalance{} + }) + colorMap := utils.MapGetOrPutDefault(accBalance, asset, func() interpreter.ColorBalance { + return interpreter.ColorBalance{} + }) + return utils.MapGetOrPutDefault(colorMap, color, func() *big.Int { + return new(big.Int) + }) +} + func getBalances(postings []interpreter.Posting, initialBalances interpreter.Balances) interpreter.Balances { balances := initialBalances.DeepClone() for _, posting := range postings { - sourceBalance := utils.NestedMapGetOrPutDefault(balances, posting.Source, posting.Asset, func() *big.Int { - return new(big.Int) - }) + sourceBalance := fetchOrInsertBalance(balances, posting.Source, posting.Asset, posting.Color) sourceBalance.Sub(sourceBalance, posting.Amount) - destinationBalance := utils.NestedMapGetOrPutDefault(balances, posting.Destination, posting.Asset, func() *big.Int { - return new(big.Int) - }) + destinationBalance := fetchOrInsertBalance(balances, posting.Destination, posting.Asset, posting.Color) destinationBalance.Add(destinationBalance, posting.Amount) } diff --git a/internal/specs_format/parse_test.go b/internal/specs_format/parse_test.go index 71a91f6f..ce7f2e14 100644 --- a/internal/specs_format/parse_test.go +++ b/internal/specs_format/parse_test.go @@ -47,7 +47,7 @@ func TestParseSpecs(t *testing.T) { require.Equal(t, specs_format.Specs{ Balances: interpreter.Balances{ "alice": { - "EUR": big.NewInt(200), + "EUR": interpreter.Uncolored(big.NewInt(200)), }, }, Vars: interpreter.VariablesMap{ @@ -58,7 +58,7 @@ func TestParseSpecs(t *testing.T) { It: "d1", Balances: interpreter.Balances{ "bob": { - "EUR": big.NewInt(42), + "EUR": interpreter.Uncolored(big.NewInt(42)), }, }, ExpectPostings: []interpreter.Posting{ diff --git a/internal/specs_format/run_test.go b/internal/specs_format/run_test.go index dc7be8bd..0bb505e5 100644 --- a/internal/specs_format/run_test.go +++ b/internal/specs_format/run_test.go @@ -58,7 +58,7 @@ func TestRunSpecsSimple(t *testing.T) { }, Balances: interpreter.Balances{ "src": interpreter.AccountBalance{ - "USD": big.NewInt(9999), + "USD": interpreter.Uncolored(big.NewInt(9999)), }, }, Meta: interpreter.AccountsMetadata{}, @@ -119,11 +119,11 @@ func TestRunSpecsMergeOuter(t *testing.T) { Meta: interpreter.AccountsMetadata{}, Balances: interpreter.Balances{ "src": interpreter.AccountBalance{ - "USD": big.NewInt(10), - "EUR": big.NewInt(2), + "USD": interpreter.Uncolored(big.NewInt(10)), + "EUR": interpreter.Uncolored(big.NewInt(2)), }, "dest": interpreter.AccountBalance{ - "USD": big.NewInt(1), + "USD": interpreter.Uncolored(big.NewInt(1)), }, }, FailedAssertions: nil, @@ -175,7 +175,7 @@ func TestRunWithMissingBalance(t *testing.T) { }, Balances: interpreter.Balances{ "src": interpreter.AccountBalance{ - "USD": big.NewInt(1), + "USD": interpreter.Uncolored(big.NewInt(1)), }, }, Meta: interpreter.AccountsMetadata{}, @@ -229,7 +229,7 @@ func TestRunWithMissingBalanceWhenExpectedPostings(t *testing.T) { }, Balances: interpreter.Balances{ "src": interpreter.AccountBalance{ - "USD": big.NewInt(1), + "USD": interpreter.Uncolored(big.NewInt(1)), }, }, Meta: interpreter.AccountsMetadata{}, @@ -281,7 +281,7 @@ func TestNullPostingsIsNoop(t *testing.T) { }, Balances: interpreter.Balances{ "src": interpreter.AccountBalance{ - "USD": big.NewInt(1), + "USD": interpreter.Uncolored(big.NewInt(1)), }, }, Meta: interpreter.AccountsMetadata{}, @@ -422,7 +422,7 @@ func TestFocus(t *testing.T) { }, Balances: interpreter.Balances{ "src": interpreter.AccountBalance{ - "USD": big.NewInt(9999), + "USD": interpreter.Uncolored(big.NewInt(9999)), }, }, Meta: interpreter.AccountsMetadata{}, diff --git a/numscript.go b/numscript.go index e7fa0437..a74b4a34 100644 --- a/numscript.go +++ b/numscript.go @@ -52,10 +52,13 @@ type ( VariablesMap = interpreter.VariablesMap Posting = interpreter.Posting ExecutionResult = interpreter.ExecutionResult - // For each account, list of the needed assets - BalanceQuery = interpreter.BalanceQuery + // For each account, list of the needed (asset, color) pairs + BalanceQuery = interpreter.BalanceQuery + // AssetColor identifies a (asset, color) pair to query. + AssetColor = interpreter.AssetColor MetadataQuery = interpreter.MetadataQuery AccountBalance = interpreter.AccountBalance + ColorBalance = interpreter.ColorBalance Balances = interpreter.Balances AccountMetadata = interpreter.AccountMetadata @@ -76,6 +79,10 @@ type ( MissingFundsErr = interpreter.MissingFundsErr ) +// Uncolored wraps an amount as a ColorBalance under the empty color key +// (the "no color" bucket). Useful when building a Balances literal in tests. +var Uncolored = interpreter.Uncolored + func (p ParseResult) Run(ctx context.Context, vars VariablesMap, store Store) (ExecutionResult, InterpreterError) { return p.RunWithFeatureFlags(ctx, vars, store, nil) } diff --git a/numscript_test.go b/numscript_test.go index b4da92d6..81bdc1b1 100644 --- a/numscript_test.go +++ b/numscript_test.go @@ -128,17 +128,17 @@ send [COIN 100] ( // TODO maybe those calls can be batched together { // this is required by the balance() call - "account_that_needs_balance": {"USD/2"}, + "account_that_needs_balance": {{Asset: "USD/2"}}, }, { // this is defined in the variables - "source1": {"COIN"}, + "source1": {{Asset: "COIN"}}, // this is defined in account metadata - "source2": {"COIN"}, + "source2": {{Asset: "COIN"}}, // this appears as literal - "source3": {"COIN"}, + "source3": {{Asset: "COIN"}}, }, }, store.GetBalancesCalls) @@ -174,8 +174,8 @@ send [COIN 100] ( require.Equal(t, []numscript.BalanceQuery{ { - "a": {"COIN"}, - "b": {"COIN"}, + "a": {{Asset: "COIN"}}, + "b": {{Asset: "COIN"}}, }, }, store.GetBalancesCalls) @@ -205,7 +205,7 @@ func TestDoNotGetBalancesTwice(t *testing.T) { require.Equal(t, []numscript.BalanceQuery{ { - "alice": {"COIN"}, + "alice": {{Asset: "COIN"}}, }, }, store.GetBalancesCalls) @@ -226,8 +226,8 @@ func TestGetBalancesAllotment(t *testing.T) { store := ObservableStore{ StaticStore: interpreter.StaticStore{ Balances: interpreter.Balances{ - "a": {"COIN": big.NewInt(10000)}, - "b": {"COIN": big.NewInt(10000)}, + "a": {"COIN": numscript.Uncolored(big.NewInt(10000))}, + "b": {"COIN": numscript.Uncolored(big.NewInt(10000))}, }, }, } @@ -241,8 +241,8 @@ func TestGetBalancesAllotment(t *testing.T) { require.Equal(t, []numscript.BalanceQuery{ { - "a": {"COIN"}, - "b": {"COIN"}, + "a": {{Asset: "COIN"}}, + "b": {{Asset: "COIN"}}, }, }, store.GetBalancesCalls) @@ -268,7 +268,7 @@ func TestGetBalancesOverdraft(t *testing.T) { require.Equal(t, []numscript.BalanceQuery{ { - "a": {"COIN"}, + "a": {{Asset: "COIN"}}, }, }, store.GetBalancesCalls) @@ -288,7 +288,7 @@ func TestDoNotFetchBalanceTwice(t *testing.T) { require.Equal(t, []numscript.BalanceQuery{ { - "src": {"COIN"}, + "src": {{Asset: "COIN"}}, }, }, store.GetBalancesCalls, @@ -314,10 +314,10 @@ func TestDoNotFetchBalanceTwice2(t *testing.T) { require.Equal(t, []numscript.BalanceQuery{ { - "src1": {"COIN"}, + "src1": {{Asset: "COIN"}}, }, { - "src2": {"COIN"}, + "src2": {{Asset: "COIN"}}, }, }, store.GetBalancesCalls, @@ -343,10 +343,10 @@ func TestDoNotFetchBalanceTwice3(t *testing.T) { require.Equal(t, []numscript.BalanceQuery{ { - "src": {"EUR/2"}, + "src": {{Asset: "EUR/2"}}, }, { - "src": {"USD/2"}, + "src": {{Asset: "USD/2"}}, }, }, store.GetBalancesCalls, @@ -410,7 +410,7 @@ send [USD/2 30] ( require.Equal(t, []numscript.BalanceQuery{ { - "alice": {"USD/2"}, + "alice": {{Asset: "USD/2"}}, }, }, store.GetBalancesCalls, @@ -437,7 +437,7 @@ set_tx_meta( StaticStore: interpreter.StaticStore{ Balances: interpreter.Balances{ "alice": interpreter.AccountBalance{ - "USD/2": big.NewInt(20), + "USD/2": numscript.Uncolored(big.NewInt(20)), }, }, }, @@ -485,10 +485,10 @@ send [USD/2 10] ( }, Balances: interpreter.Balances{ "a": interpreter.AccountBalance{ - "USD/2": big.NewInt(100), + "USD/2": numscript.Uncolored(big.NewInt(100)), }, "a2": interpreter.AccountBalance{ - "USD/2": big.NewInt(1), + "USD/2": numscript.Uncolored(big.NewInt(1)), }, }, }, @@ -519,10 +519,10 @@ send [USD/2 10] ( require.Equal(t, []numscript.BalanceQuery{ { - "a": {"USD/2"}, + "a": {{Asset: "USD/2"}}, }, { - "a2": {"USD/2"}, + "a2": {{Asset: "USD/2"}}, }, }, store.GetBalancesCalls, From 378a303753a089c109a038db8e32b5cc9c7524e0 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 3 Jun 2026 11:05:53 +0200 Subject: [PATCH 02/12] chore: retrigger ci From 0fa5ddd2129ffe62e7ed7dcab598339b8473d8f6 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 3 Jun 2026 11:06:01 +0200 Subject: [PATCH 03/12] chore: retrigger ci From f4fae4573bc5c41c3363d8b680a38d66281f8f28 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 3 Jun 2026 11:41:12 +0200 Subject: [PATCH 04/12] test: drop dead struct fields flagged by golangci-lint unused Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/interpreter/color_semantics_test.go | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/internal/interpreter/color_semantics_test.go b/internal/interpreter/color_semantics_test.go index 4033dee7..f4088daa 100644 --- a/internal/interpreter/color_semantics_test.go +++ b/internal/interpreter/color_semantics_test.go @@ -208,17 +208,13 @@ func TestColoredPostingsCompactByColor(t *testing.T) { func TestBalanceQueryIncludesColor(t *testing.T) { t.Parallel() - type observed struct { - machine.StaticStore - got []machine.BalanceQuery - } - store := &observed{StaticStore: machine.StaticStore{ + store := machine.StaticStore{ Balances: machine.Balances{ "acc": machine.AccountBalance{ "COIN": machine.ColorBalance{"RED": big.NewInt(1000)}, }, }, - }} + } src := ` send [COIN 100] ( @@ -229,9 +225,6 @@ func TestBalanceQueryIncludesColor(t *testing.T) { parsed := parser.Parse(src) require.Empty(t, parsed.Errors) - type spyStore struct{ inner machine.Store } - _ = spyStore{} - // Wrap the store with an inline implementation that records GetBalances // calls so we can assert what crosses the boundary. var got []machine.BalanceQuery @@ -242,9 +235,9 @@ func TestBalanceQueryIncludesColor(t *testing.T) { cloned[acc] = append([]machine.AssetColor(nil), items...) } got = append(got, cloned) - return store.StaticStore.GetBalances(ctx, q) + return store.GetBalances(ctx, q) }, - getMetadata: store.StaticStore.GetAccountsMetadata, + getMetadata: store.GetAccountsMetadata, } _, err := machine.RunProgram( From 6fa2898251f92fd669f5dfc0556edc02eb4c3bd3 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 3 Jun 2026 13:11:45 +0200 Subject: [PATCH 05/12] refactor!: drop the flat-shorthand JSON shape for Balances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per ledger #234 feedback: the dual JSON parse path was a backward-compat shim that masked the schema break. With the new (asset, color) keying, the only canonical shape is {"": {"": {"": , ...}}} where color "" is the uncolored bucket. The flat shorthand \`{"": }\` is no longer accepted; balances must spell out the empty-color key explicitly. Changes: - Remove \`Balances.UnmarshalJSON\` and \`decodeColorBalance\`. Go's default JSON unmarshal walks the natural 3-level map directly. - \`mcp_impl.parseBalancesJson\` rejects the shorthand with a clearer error message (the helper still hand-parses a generic \`map[string]any\` coming from the MCP transport). - Sweep the 121 \`.num.specs.json\` fixtures via jq to convert every \`"": \` to \`"": {"": }\`. The walk only touches \`balances\` (top-level and \`testCases[].balances\`) — movements and post-commit volumes keep their native shapes. - Update \`specs_format\` table tests for the new shape (run_test.go, runner_test.go, parse_test.go). - Refresh the runner_test snapshot (\`TestSchemaErrSpecs\` now reports the friendlier \`cannot unmarshal number into ColorBalance\` instead of going through the bespoke shorthand path). - Update \`color_semantics_test.go\`: a single \`TestBalancesJSONShape\` asserts the canonical parse, plus \`TestBalancesJSONRejectsFlatShorthand\` pins the rejection. All numscript test packages green; pre-commit clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/interpreter/balances.go | 61 ---------------- internal/interpreter/color_semantics_test.go | 73 +++++++------------ ...source-prefer-single-source.num.specs.json | 32 ++++++-- .../experimental/top-up-many.num.specs.json | 49 +++++++++++-- .../experimental/top-up.num.specs.json | 32 ++++++-- .../transfer-example.num.specs.json | 36 +++++++-- ...mixed-source-dest-allotment.num.specs.json | 8 +- .../send-max.num.specs.json | 12 ++- .../top-up-max.num.specs.json | 16 +++- ...allocate-dont-take-too-much.num.specs.json | 8 +- .../script-tests/allocation.num.specs.json | 4 +- .../ask-balance-twice.num.specs.json | 4 +- .../balance-not-found.num.specs.json | 2 +- .../balance-simple.num.specs.json | 4 +- .../script-tests/balance.num.specs.json | 4 +- .../bigint-literal.num.specs.json | 2 +- ...apped-when-less-than-needed.num.specs.json | 8 +- ...pped-when-more-than-balance.num.specs.json | 4 +- .../cascading-sources.num.specs.json | 16 +++- ...xceed-overdraft-on-send-all.num.specs.json | 4 +- .../do-not-exceed-overdraft.num.specs.json | 4 +- .../dynamic-allocation.num.specs.json | 4 +- .../empty-postings.num.specs.json | 6 +- .../account-interp.num.specs.json | 4 +- ...-balance-when-missing-funds.num.specs.json | 4 +- .../asset-colors/empty-color.num.specs.json | 4 +- .../asset-scaling/no-solution.num.specs.json | 30 ++++++-- .../scaling-all-allotment.num.specs.json | 10 ++- .../scaling-allotment.num.specs.json | 18 +++-- .../asset-scaling/scaling-kept.num.specs.json | 10 ++- .../scaling-send-all.num.specs.json | 18 +++-- .../scaling-with-oneof.num.specs.json | 14 +++- .../asset-scaling/scaling.num.specs.json | 34 ++++++--- ...update-swap-account-balance.num.specs.json | 6 +- .../get-amount-function.num.specs.json | 4 +- .../get-asset-function.num.specs.json | 4 +- .../expr-in-var-origin.num.specs.json | 2 +- ...ript-balance-after-decrease.num.specs.json | 4 +- .../midscript-balance.num.specs.json | 4 +- .../oneof/oneof-all-failing.num.specs.json | 4 +- .../oneof/oneof-in-send-all.num.specs.json | 4 +- .../oneof/oneof-singleton.num.specs.json | 4 +- .../update-balances-with-oneof.num.specs.json | 4 +- ...nction-use-case-remove-debt.num.specs.json | 4 +- ...raft-function-when-negative.num.specs.json | 4 +- ...raft-function-when-positive.num.specs.json | 6 +- ...verdraft-function-when-zero.num.specs.json | 2 +- .../reach-zero.num.specs.json | 34 +++++++-- .../feature-flag-syntax.num.specs.json | 6 +- .../script-tests/floating-perc.num.specs.json | 2 +- .../floating-perc2.num.specs.json | 2 +- .../inoder-destination.num.specs.json | 4 +- .../insufficient-funds.num.specs.json | 8 +- .../kept-in-send-all-inorder.num.specs.json | 4 +- .../kept-with-balance.num.specs.json | 4 +- .../many-kept-dest.num.specs.json | 4 +- .../script-tests/many-max-dest.num.specs.json | 4 +- ...ax-with-unbounded-overdraft.num.specs.json | 8 +- .../script-tests/metadata.num.specs.json | 8 +- .../minus-infix-monetary.num.specs.json | 2 +- .../minus-infix-number.num.specs.json | 2 +- .../minus-prefix-number.num.specs.json | 2 +- .../script-tests/neg-max-dest.num.specs.json | 6 +- .../negative-max-send-all.num.specs.json | 6 +- .../script-tests/negative-max.num.specs.json | 4 +- .../nested-remaining-complex.num.specs.json | 4 +- ...vedrafts-playground-example.num.specs.json | 4 +- ...draft-in-send-all-when-noop.num.specs.json | 4 +- .../overdraft-in-send-all.num.specs.json | 4 +- .../overdraft-not-enough-funds.num.specs.json | 4 +- ...egative-balance-in-send-all.num.specs.json | 4 +- ...draft-when-negative-balance.num.specs.json | 4 +- ...gative-ovedraft-in-send-all.num.specs.json | 4 +- ...draft-when-not-enough-funds.num.specs.json | 4 +- ...ng-kept-in-send-all-inorder.num.specs.json | 4 +- .../remaining-none-in-send-all.num.specs.json | 4 +- ...rom-account__multi-postings.num.specs.json | 4 +- ...unt__save-a-different-asset.num.specs.json | 8 +- ...__save-all-negative-balance.num.specs.json | 4 +- ...save-from-account__save-all.num.specs.json | 4 +- ...ccount__save-causes-failure.num.specs.json | 4 +- ...unt__save-more-than-balance.num.specs.json | 4 +- .../save-from-account__simple.num.specs.json | 4 +- ...rom-account__with-asset-var.num.specs.json | 4 +- ...-account__with-monetary-var.num.specs.json | 4 +- ...ll-destinatio-allot-complex.num.specs.json | 8 +- .../send-all-destinatio-allot.num.specs.json | 4 +- .../send-all-many-max-in-dest.num.specs.json | 4 +- .../send-all-multi.num.specs.json | 8 +- .../send-all-variable.num.specs.json | 4 +- ...hen-negative-with-overdraft.num.specs.json | 4 +- .../send-all-when-negative.num.specs.json | 6 +- .../script-tests/send-all.num.specs.json | 4 +- .../send-allt-max-in-dest.num.specs.json | 4 +- .../send-allt-max-in-src.num.specs.json | 8 +- ...end-allt-max-when-no-amount.num.specs.json | 6 +- .../send-when-negative-balance.num.specs.json | 4 +- .../script-tests/send-zero.num.specs.json | 2 +- .../testdata/script-tests/send.num.specs.json | 4 +- ...ource-allotment-invalid-amt.num.specs.json | 4 +- .../source-allotment.num.specs.json | 12 ++- .../source-complex.num.specs.json | 16 +++- .../source-overlapping.num.specs.json | 8 +- .../script-tests/source.num.specs.json | 8 +- .../track-balances-send-all.num.specs.json | 4 +- .../track-balances.num.specs.json | 4 +- .../track-balances2.num.specs.json | 4 +- .../track-balances3.num.specs.json | 4 +- ...draft-when-not-enough-funds.num.specs.json | 4 +- .../update-balances.num.specs.json | 4 +- .../use-balance-twice.num.specs.json | 4 +- ...ts-with-same-source-account.num.specs.json | 8 +- .../variable-asset.num.specs.json | 8 +- .../variable-balance__1.num.specs.json | 8 +- .../variable-balance__2.num.specs.json | 8 +- .../variable-balance__3.num.specs.json | 8 +- .../variable-balance__4.num.specs.json | 8 +- .../variable-balance__5.num.specs.json | 20 +++-- .../variables-json.num.specs.json | 4 +- .../script-tests/variables.num.specs.json | 4 +- .../script-tests/world-source.num.specs.json | 4 +- ...postings-explicit-allotment.num.specs.json | 2 +- ...o-postings-explicit-inorder.num.specs.json | 2 +- internal/mcp_impl/handlers.go | 12 +-- .../__snapshots__/runner_test.snap | 2 +- internal/specs_format/parse_test.go | 4 +- internal/specs_format/run_test.go | 18 ++--- internal/specs_format/runner_test.go | 14 ++-- 128 files changed, 715 insertions(+), 354 deletions(-) diff --git a/internal/interpreter/balances.go b/internal/interpreter/balances.go index 8ef33f13..111535e3 100644 --- a/internal/interpreter/balances.go +++ b/internal/interpreter/balances.go @@ -1,8 +1,6 @@ package interpreter import ( - "encoding/json" - "fmt" "math/big" "github.com/formancehq/numscript/internal/utils" @@ -14,65 +12,6 @@ func Uncolored(amount *big.Int) ColorBalance { return ColorBalance{"": amount} } -// UnmarshalJSON accepts two JSON shapes for a Balances value: -// -// 1. Flat (uncolored shorthand): -// {"alice": {"USD/2": 100, "EUR/2": -42}} -// Every asset gets a single entry under the "" (no color) bucket. -// -// 2. Colored (full form): -// {"alice": {"USD/2": {"": 100, "GRANTS": 50}}} -// -// Shapes can be mixed across assets within the same document. -func (b *Balances) UnmarshalJSON(data []byte) error { - raw := map[string]map[string]json.RawMessage{} - if err := json.Unmarshal(data, &raw); err != nil { - return err - } - - out := Balances{} - for account, assets := range raw { - accB := AccountBalance{} - out[account] = accB - for asset, rawVal := range assets { - colorMap, err := decodeColorBalance(rawVal) - if err != nil { - return fmt.Errorf("balances[%s][%s]: %w", account, asset, err) - } - accB[asset] = colorMap - } - } - *b = out - return nil -} - -func decodeColorBalance(data json.RawMessage) (ColorBalance, error) { - // Try the shorthand: a single number meaning the uncolored bucket. - var amount json.Number - if err := json.Unmarshal(data, &amount); err == nil { - n, ok := new(big.Int).SetString(amount.String(), 10) - if !ok { - return nil, fmt.Errorf("invalid integer amount %q", amount.String()) - } - return ColorBalance{"": n}, nil - } - - // Otherwise expect a {color: amount} object. - raw := map[string]json.Number{} - if err := json.Unmarshal(data, &raw); err != nil { - return nil, fmt.Errorf("expected integer or {color: amount} object, got %s", string(data)) - } - out := ColorBalance{} - for color, amt := range raw { - n, ok := new(big.Int).SetString(amt.String(), 10) - if !ok { - return nil, fmt.Errorf("color %q: invalid integer amount %q", color, amt.String()) - } - out[color] = n - } - return out, nil -} - func (b Balances) DeepClone() Balances { cloned := make(Balances) for account, accountBalances := range b { diff --git a/internal/interpreter/color_semantics_test.go b/internal/interpreter/color_semantics_test.go index f4088daa..30b4ee87 100644 --- a/internal/interpreter/color_semantics_test.go +++ b/internal/interpreter/color_semantics_test.go @@ -487,55 +487,38 @@ func TestColorComposesWithAssetPrecision(t *testing.T) { } // JSON unmarshal accepts both shapes (flat + colored), as documented in -// Balances.UnmarshalJSON. -func TestBalancesUnmarshalAcceptsBothShapes(t *testing.T) { +// Balances JSON shape: a {color: amount} object under each (account, asset). +// Color "" is the uncolored bucket. No shorthand is accepted — uncolored +// balances must be explicit. +func TestBalancesJSONShape(t *testing.T) { t.Parallel() - cases := []struct { - name string - src string - want machine.Balances - }{ - { - name: "flat shorthand", - src: `{"alice": {"USD/2": 100, "EUR/2": -42}}`, - want: machine.Balances{ - "alice": machine.AccountBalance{ - "USD/2": machine.Uncolored(big.NewInt(100)), - "EUR/2": machine.Uncolored(big.NewInt(-42)), - }, - }, - }, - { - name: "colored", - src: `{"alice": {"USD/2": {"": 100, "RED": 50}}}`, - want: machine.Balances{ - "alice": machine.AccountBalance{ - "USD/2": machine.ColorBalance{"": big.NewInt(100), "RED": big.NewInt(50)}, - }, - }, - }, - { - name: "mixed across assets", - src: `{"alice": {"USD/2": 100, "EUR/2": {"RED": 5}}}`, - want: machine.Balances{ - "alice": machine.AccountBalance{ - "USD/2": machine.Uncolored(big.NewInt(100)), - "EUR/2": machine.ColorBalance{"RED": big.NewInt(5)}, - }, - }, + src := `{ + "alice": { + "USD/2": {"": 100, "RED": 50}, + "EUR/2": {"": -42} + } + }` + want := machine.Balances{ + "alice": machine.AccountBalance{ + "USD/2": machine.ColorBalance{"": big.NewInt(100), "RED": big.NewInt(50)}, + "EUR/2": machine.Uncolored(big.NewInt(-42)), }, } - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() + var got machine.Balances + require.NoError(t, json.Unmarshal([]byte(src), &got)) + require.True(t, machine.CompareBalances(want, got), + "unexpected balances: want %v, got %v", want, got) +} - var got machine.Balances - require.NoError(t, got.UnmarshalJSON([]byte(tc.src))) - require.True(t, machine.CompareBalances(tc.want, got), - "unexpected balances: want %v, got %v", tc.want, got) - }) - } +// The flat shorthand `{"USD/2": 100}` is no longer supported — uncolored +// balances must spell out the empty-color key explicitly. We assert the +// rejection here so callers never accidentally drop into a permissive parse. +func TestBalancesJSONRejectsFlatShorthand(t *testing.T) { + t.Parallel() + + var got machine.Balances + err := json.Unmarshal([]byte(`{"alice": {"USD/2": 100}}`), &got) + require.Error(t, err, "flat shorthand must be rejected") } diff --git a/internal/interpreter/testdata/numscript-cookbook/experimental/mixed-source-prefer-single-source.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/experimental/mixed-source-prefer-single-source.num.specs.json index c54424ab..40f3d94e 100644 --- a/internal/interpreter/testdata/numscript-cookbook/experimental/mixed-source-prefer-single-source.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/experimental/mixed-source-prefer-single-source.num.specs.json @@ -3,13 +3,17 @@ "variables": { "amt": "10" }, - "featureFlags": ["experimental-oneof"], + "featureFlags": [ + "experimental-oneof" + ], "testCases": [ { "it": "sends from the first source when there is enough balance", "balances": { "s1": { - "USD": 999 + "USD": { + "": 999 + } } }, "expect.postings": [ @@ -25,10 +29,14 @@ "it": "sends from the second one when it has enough balance but the first one doesn't", "balances": { "s1": { - "USD": 9 + "USD": { + "": 9 + } }, "s2": { - "USD": 999 + "USD": { + "": 999 + } } }, "expect.postings": [ @@ -44,10 +52,14 @@ "it": "sends partially from both when none of them has enough balance on its own", "balances": { "s1": { - "USD": 6 + "USD": { + "": 6 + } }, "s2": { - "USD": 9 + "USD": { + "": 9 + } } }, "expect.postings": [ @@ -69,10 +81,14 @@ "it": "fails if there aren't enough funds between all sources", "balances": { "s1": { - "USD": 5 + "USD": { + "": 5 + } }, "s2": { - "USD": 4 + "USD": { + "": 4 + } } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/numscript-cookbook/experimental/top-up-many.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/experimental/top-up-many.num.specs.json index 2c1cbc95..3f2e5963 100644 --- a/internal/interpreter/testdata/numscript-cookbook/experimental/top-up-many.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/experimental/top-up-many.num.specs.json @@ -11,14 +11,33 @@ "testCases": [ { "it": "should be a noop when all balances are >= 0", - "balances": { "alice": { "EUR": 100 }, "bob": { "EUR": 200 } }, + "balances": { + "alice": { + "EUR": { + "": 100 + } + }, + "bob": { + "EUR": { + "": 200 + } + } + }, "expect.postings": [] }, { "it": "should prioritize alice when both have missing funds", "balances": { - "alice": { "EUR": -120 }, - "bob": { "EUR": -120 } + "alice": { + "EUR": { + "": -120 + } + }, + "bob": { + "EUR": { + "": -120 + } + } }, "expect.postings": [ { @@ -32,8 +51,16 @@ { "it": "doesn't send funds to alice if there aren't enough funds for the account to be topped-up", "balances": { - "alice": { "EUR": -80 }, - "bob": { "EUR": -120 } + "alice": { + "EUR": { + "": -80 + } + }, + "bob": { + "EUR": { + "": -120 + } + } }, "expect.postings": [ { @@ -47,8 +74,16 @@ { "it": "funds are kept if there are spare funds", "balances": { - "alice": { "EUR": -10 }, - "bob": { "EUR": -20 } + "alice": { + "EUR": { + "": -10 + } + }, + "bob": { + "EUR": { + "": -20 + } + } }, "expect.postings": [] } diff --git a/internal/interpreter/testdata/numscript-cookbook/experimental/top-up.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/experimental/top-up.num.specs.json index 05fcf681..535fbc73 100644 --- a/internal/interpreter/testdata/numscript-cookbook/experimental/top-up.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/experimental/top-up.num.specs.json @@ -1,22 +1,42 @@ { "$schema": "https://raw.githubusercontent.com/formancehq/numscript/main/specs.schema.json", - "featureFlags": ["experimental-overdraft-function"], + "featureFlags": [ + "experimental-overdraft-function" + ], "testCases": [ { "it": "should not emit postings", - "balances": { "alice": { "EUR": 100 } }, + "balances": { + "alice": { + "EUR": { + "": 100 + } + } + }, "expect.postings": [] }, { "it": "should send the missing amount to an overdraft account", - "balances": { "alice": { "EUR": -100 } }, + "balances": { + "alice": { + "EUR": { + "": -100 + } + } + }, "expect.endBalances": { - "alice": { "EUR": 0 }, - "world": { "EUR": -100 } + "alice": { + "EUR": 0 + }, + "world": { + "EUR": -100 + } }, "expect.movements": { "world": { - "alice": { "EUR": 100 } + "alice": { + "EUR": 100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/numscript-cookbook/experimental/transfer-example.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/experimental/transfer-example.num.specs.json index c0e92c9a..62c4120c 100644 --- a/internal/interpreter/testdata/numscript-cookbook/experimental/transfer-example.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/experimental/transfer-example.num.specs.json @@ -13,8 +13,16 @@ { "it": "should authorize transfer if both wallet and bank accounts display enough balance", "balances": { - "wallet": { "EUR": 100 }, - "bank_account": { "EUR": -100 } + "wallet": { + "EUR": { + "": 100 + } + }, + "bank_account": { + "EUR": { + "": -100 + } + } }, "expect.postings": [ { @@ -28,16 +36,32 @@ { "it": "should not authorize transfer if wallet does not display enough balance and bank account does", "balances": { - "wallet": { "EUR": 50 }, - "bank_account": { "EUR": -100 } + "wallet": { + "EUR": { + "": 50 + } + }, + "bank_account": { + "EUR": { + "": -100 + } + } }, "expect.error.missingFunds": true }, { "it": "should not authorize transfer if bank account does not display enough balance and wallet does", "balances": { - "wallet": { "EUR": 100 }, - "bank_account": { "EUR": -50 } + "wallet": { + "EUR": { + "": 100 + } + }, + "bank_account": { + "EUR": { + "": -50 + } + } }, "expect.error.missingFunds": true } diff --git a/internal/interpreter/testdata/numscript-cookbook/mixed-source-dest-allotment.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/mixed-source-dest-allotment.num.specs.json index cd0bc9e9..45b05f1e 100644 --- a/internal/interpreter/testdata/numscript-cookbook/mixed-source-dest-allotment.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/mixed-source-dest-allotment.num.specs.json @@ -5,10 +5,14 @@ "it": "matches sources and destinations allotments", "balances": { "src1": { - "USD": 999 + "USD": { + "": 999 + } }, "src2": { - "USD": 999 + "USD": { + "": 999 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/numscript-cookbook/send-max.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/send-max.num.specs.json index d2e32539..d6ed98fb 100644 --- a/internal/interpreter/testdata/numscript-cookbook/send-max.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/send-max.num.specs.json @@ -4,7 +4,11 @@ { "it": "is capped to USD100 when balance is higher", "balances": { - "src": { "USD": 999 } + "src": { + "USD": { + "": 999 + } + } }, "expect.postings": [ { @@ -18,7 +22,11 @@ { "it": "allows sending less than the cap when balance is not enough", "balances": { - "src": { "USD": 42 } + "src": { + "USD": { + "": 42 + } + } }, "expect.postings": [ { diff --git a/internal/interpreter/testdata/numscript-cookbook/top-up-max.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/top-up-max.num.specs.json index 00098115..c0743385 100644 --- a/internal/interpreter/testdata/numscript-cookbook/top-up-max.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/top-up-max.num.specs.json @@ -6,7 +6,9 @@ }, "balances": { "alice": { - "EUR/2": 9999 + "EUR/2": { + "": 9999 + } } }, "testCases": [ @@ -14,7 +16,9 @@ "it": "should send the amount when destination doesn't reach 150", "balances": { "jon": { - "EUR/2": 0 + "EUR/2": { + "": 0 + } } }, "expect.postings": [ @@ -30,7 +34,9 @@ "it": "should send the amount when destination doesn't reach 150 (2)", "balances": { "jon": { - "EUR/2": 50 + "EUR/2": { + "": 50 + } } }, "expect.postings": [ @@ -46,7 +52,9 @@ "it": "should fail if the end balance would exceed 150", "balances": { "jon": { - "EUR/2": 51 + "EUR/2": { + "": 51 + } } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/allocate-dont-take-too-much.num.specs.json b/internal/interpreter/testdata/script-tests/allocate-dont-take-too-much.num.specs.json index 1fc6d9f3..ebadeadd 100644 --- a/internal/interpreter/testdata/script-tests/allocate-dont-take-too-much.num.specs.json +++ b/internal/interpreter/testdata/script-tests/allocate-dont-take-too-much.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "users:001": { - "CREDIT": 100 + "CREDIT": { + "": 100 + } }, "users:002": { - "CREDIT": 110 + "CREDIT": { + "": 110 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/allocation.num.specs.json b/internal/interpreter/testdata/script-tests/allocation.num.specs.json index 02b08a47..7b1b7f87 100644 --- a/internal/interpreter/testdata/script-tests/allocation.num.specs.json +++ b/internal/interpreter/testdata/script-tests/allocation.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "users:001": { - "GEM": 15 + "GEM": { + "": 15 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/ask-balance-twice.num.specs.json b/internal/interpreter/testdata/script-tests/ask-balance-twice.num.specs.json index 435146cf..aeee81c2 100644 --- a/internal/interpreter/testdata/script-tests/ask-balance-twice.num.specs.json +++ b/internal/interpreter/testdata/script-tests/ask-balance-twice.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD/2": 10 + "USD/2": { + "": 10 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/balance-not-found.num.specs.json b/internal/interpreter/testdata/script-tests/balance-not-found.num.specs.json index c535912d..0ef4b1a7 100644 --- a/internal/interpreter/testdata/script-tests/balance-not-found.num.specs.json +++ b/internal/interpreter/testdata/script-tests/balance-not-found.num.specs.json @@ -8,4 +8,4 @@ "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/balance-simple.num.specs.json b/internal/interpreter/testdata/script-tests/balance-simple.num.specs.json index 53ffac19..5072b86c 100644 --- a/internal/interpreter/testdata/script-tests/balance-simple.num.specs.json +++ b/internal/interpreter/testdata/script-tests/balance-simple.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD/2": 10 + "USD/2": { + "": 10 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/balance.num.specs.json b/internal/interpreter/testdata/script-tests/balance.num.specs.json index fc6986c0..4e86ccee 100644 --- a/internal/interpreter/testdata/script-tests/balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/balance.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "a": { - "EUR/2": 123 + "EUR/2": { + "": 123 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/bigint-literal.num.specs.json b/internal/interpreter/testdata/script-tests/bigint-literal.num.specs.json index a33b882a..9bfb1cb8 100644 --- a/internal/interpreter/testdata/script-tests/bigint-literal.num.specs.json +++ b/internal/interpreter/testdata/script-tests/bigint-literal.num.specs.json @@ -13,4 +13,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/capped-when-less-than-needed.num.specs.json b/internal/interpreter/testdata/script-tests/capped-when-less-than-needed.num.specs.json index c519e2f0..ec5c972b 100644 --- a/internal/interpreter/testdata/script-tests/capped-when-less-than-needed.num.specs.json +++ b/internal/interpreter/testdata/script-tests/capped-when-less-than-needed.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "src1": { - "COIN": 1000 + "COIN": { + "": 1000 + } }, "src2": { - "COIN": 1000 + "COIN": { + "": 1000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/capped-when-more-than-balance.num.specs.json b/internal/interpreter/testdata/script-tests/capped-when-more-than-balance.num.specs.json index 2f7cdcf9..05c4e205 100644 --- a/internal/interpreter/testdata/script-tests/capped-when-more-than-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/capped-when-more-than-balance.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "COIN": 1000 + "COIN": { + "": 1000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/cascading-sources.num.specs.json b/internal/interpreter/testdata/script-tests/cascading-sources.num.specs.json index 7fe60a30..f7d8c0bd 100644 --- a/internal/interpreter/testdata/script-tests/cascading-sources.num.specs.json +++ b/internal/interpreter/testdata/script-tests/cascading-sources.num.specs.json @@ -4,16 +4,24 @@ "it": "-", "balances": { "users:1234:main": { - "USD/2": 5000 + "USD/2": { + "": 5000 + } }, "users:1234:vouchers:2024-01-31": { - "USD/2": 1000 + "USD/2": { + "": 1000 + } }, "users:1234:vouchers:2024-02-17": { - "USD/2": 3000 + "USD/2": { + "": 3000 + } }, "users:1234:vouchers:2024-03-22": { - "USD/2": 10000 + "USD/2": { + "": 10000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json index e9c46c85..050ecdef 100644 --- a/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "s": { - "COIN": -4 + "COIN": { + "": -4 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft.num.specs.json b/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft.num.specs.json index d83e602c..5ca85025 100644 --- a/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft.num.specs.json +++ b/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "s": { - "COIN": -2 + "COIN": { + "": -2 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/dynamic-allocation.num.specs.json b/internal/interpreter/testdata/script-tests/dynamic-allocation.num.specs.json index ef002331..fe0cb7bb 100644 --- a/internal/interpreter/testdata/script-tests/dynamic-allocation.num.specs.json +++ b/internal/interpreter/testdata/script-tests/dynamic-allocation.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "a": { - "GEM": 15 + "GEM": { + "": 15 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/empty-postings.num.specs.json b/internal/interpreter/testdata/script-tests/empty-postings.num.specs.json index e087b2e8..63a9701c 100644 --- a/internal/interpreter/testdata/script-tests/empty-postings.num.specs.json +++ b/internal/interpreter/testdata/script-tests/empty-postings.num.specs.json @@ -4,10 +4,12 @@ "it": "-", "balances": { "foo": { - "GEM": 0 + "GEM": { + "": 0 + } } }, "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/account-interpolation/account-interp.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/account-interpolation/account-interp.num.specs.json index 97effe85..fe8165f8 100644 --- a/internal/interpreter/testdata/script-tests/experimental/account-interpolation/account-interp.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/account-interpolation/account-interp.num.specs.json @@ -1,5 +1,7 @@ { - "featureFlags": ["experimental-account-interpolation"], + "featureFlags": [ + "experimental-account-interpolation" + ], "testCases": [ { "it": "-", diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json index 73d828d2..37e3f88f 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json @@ -1,5 +1,7 @@ { - "featureFlags": ["experimental-asset-colors"], + "featureFlags": [ + "experimental-asset-colors" + ], "testCases": [ { "it": "-", diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/empty-color.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/empty-color.num.specs.json index 571f280c..0d039cb1 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/empty-color.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/empty-color.num.specs.json @@ -7,7 +7,9 @@ "it": "-", "balances": { "src": { - "COIN": 100 + "COIN": { + "": 100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/no-solution.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/no-solution.num.specs.json index 0d5137db..57f164ab 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/no-solution.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/no-solution.num.specs.json @@ -8,7 +8,9 @@ "it": "allow solution with spare money", "balances": { "src": { - "EUR": 100 + "EUR": { + "": 100 + } } }, "expect.postings": [ @@ -36,8 +38,12 @@ "it": "allow solution with spare money (2)", "balances": { "src": { - "EUR": 100, - "EUR/1": 100 + "EUR": { + "": 100 + }, + "EUR/1": { + "": 100 + } } }, "expect.postings": [ @@ -65,13 +71,21 @@ "it": "no solution", "balances": { "src": { - "EUR": 0, - "EUR/2": 1, - "EUR/3": 1, - "EUR/4": 10 + "EUR": { + "": 0 + }, + "EUR/2": { + "": 1 + }, + "EUR/3": { + "": 1 + }, + "EUR/4": { + "": 10 + } } }, "expect.error.missingFunds": true } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-all-allotment.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-all-allotment.num.specs.json index ce35e9ea..18cc1c86 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-all-allotment.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-all-allotment.num.specs.json @@ -5,7 +5,9 @@ ], "balances": { "acc1": { - "EUR/2": 50 + "EUR/2": { + "": 50 + } } }, "testCases": [ @@ -13,7 +15,9 @@ "it": "casts all the avlb", "balances": { "acc2": { - "EUR/3": 500 + "EUR/3": { + "": 500 + } } }, "expect.postings": [ @@ -44,4 +48,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-allotment.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-allotment.num.specs.json index bb464f4c..4fe9a876 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-allotment.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-allotment.num.specs.json @@ -5,7 +5,9 @@ ], "balances": { "acc1": { - "EUR/2": 50 + "EUR/2": { + "": 50 + } } }, "testCases": [ @@ -13,7 +15,9 @@ "it": "casts all the avlb", "balances": { "acc2": { - "EUR/3": 500 + "EUR/3": { + "": 500 + } } }, "expect.postings": [ @@ -47,7 +51,9 @@ "it": "doesn't send more than needed", "balances": { "acc2": { - "EUR/3": 999999 + "EUR/3": { + "": 999999 + } } }, "expect.postings": [ @@ -81,7 +87,9 @@ "it": "doesn't swap already owned asset", "balances": { "acc2": { - "EUR/2": 999999 + "EUR/2": { + "": 999999 + } } }, "expect.postings": [ @@ -100,4 +108,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-kept.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-kept.num.specs.json index 102fc196..9bf3a3cf 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-kept.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-kept.num.specs.json @@ -5,8 +5,12 @@ ], "balances": { "acc": { - "BTC/2": 2, - "BTC/8": 1000000 + "BTC/2": { + "": 2 + }, + "BTC/8": { + "": 1000000 + } } }, "testCases": [ @@ -28,4 +32,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-send-all.num.specs.json index 75c32eea..db84bef6 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-send-all.num.specs.json @@ -8,9 +8,15 @@ "it": "sends all the available assets that can be cast", "balances": { "src": { - "EUR": 2, - "EUR/2": 1, - "EUR/3": 30 + "EUR": { + "": 2 + }, + "EUR/2": { + "": 1 + }, + "EUR/3": { + "": 30 + } } }, "expect.postings": [ @@ -44,7 +50,9 @@ "it": "avoids casting the remainder", "balances": { "src": { - "EUR/3": 21 + "EUR/3": { + "": 21 + } } }, "expect.postings": [ @@ -69,4 +77,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-with-oneof.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-with-oneof.num.specs.json index 68bb6697..fe084267 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-with-oneof.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-with-oneof.num.specs.json @@ -7,7 +7,9 @@ "balances": { "acc1": {}, "acc2": { - "EUR/2": 100 + "EUR/2": { + "": 100 + } } }, "testCases": [ @@ -15,7 +17,9 @@ "it": "fallbacks to the second branch when there's not enough", "balances": { "acc1": { - "EUR/2": 1 + "EUR/2": { + "": 1 + } } }, "expect.postings": [ @@ -37,7 +41,9 @@ "it": "succeeds in the first branch", "balances": { "acc1": { - "EUR/2": 100 + "EUR/2": { + "": 100 + } } }, "expect.postings": [ @@ -50,4 +56,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling.num.specs.json index 1799fe48..974fda80 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling.num.specs.json @@ -8,8 +8,12 @@ "it": "upscales to an higher currency if possible", "balances": { "src": { - "EUR": 99, - "EUR/2": 1 + "EUR": { + "": 99 + }, + "EUR/2": { + "": 1 + } } }, "expect.postings": [ @@ -37,7 +41,9 @@ "it": "downscales to an smaller currency if possible", "balances": { "src": { - "EUR/3": 4000 + "EUR/3": { + "": 4000 + } } }, "expect.postings": [ @@ -65,8 +71,12 @@ "it": "mix", "balances": { "src": { - "EUR": 2, - "EUR/2": 200 + "EUR": { + "": 2 + }, + "EUR/2": { + "": 200 + } } }, "expect.postings": [ @@ -94,12 +104,18 @@ "it": "fails when there aren't enough funds", "balances": { "src": { - "EUR": 1, - "EUR/2": 200, - "EUR/3": 10 + "EUR": { + "": 1 + }, + "EUR/2": { + "": 200 + }, + "EUR/3": { + "": 10 + } } }, "expect.error.missingFunds": true } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/update-swap-account-balance.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/update-swap-account-balance.num.specs.json index 52c0e5bf..f96627fa 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/update-swap-account-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/update-swap-account-balance.num.specs.json @@ -8,7 +8,9 @@ "it": "updates the swap account balances", "balances": { "src": { - "EUR": 1 + "EUR": { + "": 1 + } } }, "expect.postings": [ @@ -51,4 +53,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/get-amount-function/get-amount-function.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/get-amount-function/get-amount-function.num.specs.json index d52e8c6e..32d21063 100644 --- a/internal/interpreter/testdata/script-tests/experimental/get-amount-function/get-amount-function.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/get-amount-function/get-amount-function.num.specs.json @@ -1,5 +1,7 @@ { - "featureFlags": ["experimental-get-amount-function"], + "featureFlags": [ + "experimental-get-amount-function" + ], "testCases": [ { "it": "-", diff --git a/internal/interpreter/testdata/script-tests/experimental/get-asset-function/get-asset-function.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/get-asset-function/get-asset-function.num.specs.json index 3b3997b7..3e83b43e 100644 --- a/internal/interpreter/testdata/script-tests/experimental/get-asset-function/get-asset-function.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/get-asset-function/get-asset-function.num.specs.json @@ -1,5 +1,7 @@ { - "featureFlags": ["experimental-get-asset-function"], + "featureFlags": [ + "experimental-get-asset-function" + ], "testCases": [ { "it": "-", diff --git a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/expr-in-var-origin.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/expr-in-var-origin.num.specs.json index 84cb2b44..d0853e40 100644 --- a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/expr-in-var-origin.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/expr-in-var-origin.num.specs.json @@ -8,4 +8,4 @@ "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance-after-decrease.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance-after-decrease.num.specs.json index 03d6d9c3..dd090730 100644 --- a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance-after-decrease.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance-after-decrease.num.specs.json @@ -7,7 +7,9 @@ "it": "-", "balances": { "acc": { - "USD/2": 10 + "USD/2": { + "": 10 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance.num.specs.json index 3436e8ce..d395a0e4 100644 --- a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance.num.specs.json @@ -7,7 +7,9 @@ "it": "-", "balances": { "acc": { - "USD/2": 42 + "USD/2": { + "": 42 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-all-failing.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-all-failing.num.specs.json index a506b5eb..18f41f81 100644 --- a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-all-failing.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-all-failing.num.specs.json @@ -1,5 +1,7 @@ { - "featureFlags": ["experimental-oneof"], + "featureFlags": [ + "experimental-oneof" + ], "testCases": [ { "it": "-", diff --git a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-send-all.num.specs.json index 11f21592..13b44c4e 100644 --- a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-send-all.num.specs.json @@ -7,7 +7,9 @@ "it": "-", "balances": { "s1": { - "GEM": 10 + "GEM": { + "": 10 + } }, "s2": {}, "s3": {} diff --git a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-singleton.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-singleton.num.specs.json index 617d51e0..7c64f9e5 100644 --- a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-singleton.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-singleton.num.specs.json @@ -7,7 +7,9 @@ "it": "-", "balances": { "a": { - "GEM": 10 + "GEM": { + "": 10 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/oneof/update-balances-with-oneof.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/oneof/update-balances-with-oneof.num.specs.json index a768236f..a8f3b30f 100644 --- a/internal/interpreter/testdata/script-tests/experimental/oneof/update-balances-with-oneof.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/oneof/update-balances-with-oneof.num.specs.json @@ -7,7 +7,9 @@ "it": "-", "balances": { "alice": { - "USD": 100 + "USD": { + "": 100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-use-case-remove-debt.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-use-case-remove-debt.num.specs.json index b31e3688..427bdae6 100644 --- a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-use-case-remove-debt.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-use-case-remove-debt.num.specs.json @@ -7,7 +7,9 @@ "it": "-", "balances": { "user:001": { - "USD/2": -100 + "USD/2": { + "": -100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-negative.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-negative.num.specs.json index 071e38dc..658fc702 100644 --- a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-negative.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-negative.num.specs.json @@ -7,7 +7,9 @@ "it": "-", "balances": { "acc": { - "EUR/2": -100 + "EUR/2": { + "": -100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-positive.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-positive.num.specs.json index d3093d3d..dbee73b2 100644 --- a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-positive.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-positive.num.specs.json @@ -7,10 +7,12 @@ "it": "-", "balances": { "acc": { - "EUR/2": 100 + "EUR/2": { + "": 100 + } } }, "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-zero.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-zero.num.specs.json index 201383c1..7db029e3 100644 --- a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-zero.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-zero.num.specs.json @@ -11,4 +11,4 @@ "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/reach-zero.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/reach-zero.num.specs.json index 7cc09268..c91e5f14 100644 --- a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/reach-zero.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/reach-zero.num.specs.json @@ -3,11 +3,17 @@ "variables": { "amt": "100" }, - "featureFlags": ["experimental-overdraft-function"], + "featureFlags": [ + "experimental-overdraft-function" + ], "testCases": [ { "balances": { - "invoice:001": { "USD/2": -999 } + "invoice:001": { + "USD/2": { + "": -999 + } + } }, "it": "only sends to the first one if there's not enough funds to top-up its balance", "expect.postings": [ @@ -21,8 +27,16 @@ }, { "balances": { - "invoice:001": { "USD/2": -99 }, - "invoice:002": { "USD/2": -999 } + "invoice:001": { + "USD/2": { + "": -99 + } + }, + "invoice:002": { + "USD/2": { + "": -999 + } + } }, "it": "it sends to the second source after topping-up the first one", "expect.postings": [ @@ -42,8 +56,16 @@ }, { "balances": { - "invoice:001": { "USD/2": -2 }, - "invoice:002": { "USD/2": -3 } + "invoice:001": { + "USD/2": { + "": -2 + } + }, + "invoice:002": { + "USD/2": { + "": -3 + } + } }, "it": "it keeps the spare amount", "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/feature-flag-syntax.num.specs.json b/internal/interpreter/testdata/script-tests/feature-flag-syntax.num.specs.json index db6f647b..92df2f90 100644 --- a/internal/interpreter/testdata/script-tests/feature-flag-syntax.num.specs.json +++ b/internal/interpreter/testdata/script-tests/feature-flag-syntax.num.specs.json @@ -2,7 +2,9 @@ "$schema": "https://raw.githubusercontent.com/formancehq/numscript/main/specs.schema.json", "balances": { "acc": { - "TK": -100 + "TK": { + "": -100 + } } }, "testCases": [ @@ -18,4 +20,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/floating-perc.num.specs.json b/internal/interpreter/testdata/script-tests/floating-perc.num.specs.json index 185ccf3d..67eba0dd 100644 --- a/internal/interpreter/testdata/script-tests/floating-perc.num.specs.json +++ b/internal/interpreter/testdata/script-tests/floating-perc.num.specs.json @@ -13,4 +13,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/floating-perc2.num.specs.json b/internal/interpreter/testdata/script-tests/floating-perc2.num.specs.json index 4c512b52..6aa8f68e 100644 --- a/internal/interpreter/testdata/script-tests/floating-perc2.num.specs.json +++ b/internal/interpreter/testdata/script-tests/floating-perc2.num.specs.json @@ -13,4 +13,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/inoder-destination.num.specs.json b/internal/interpreter/testdata/script-tests/inoder-destination.num.specs.json index 06db229a..7d0632a5 100644 --- a/internal/interpreter/testdata/script-tests/inoder-destination.num.specs.json +++ b/internal/interpreter/testdata/script-tests/inoder-destination.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "a": { - "COIN": 123 + "COIN": { + "": 123 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/insufficient-funds.num.specs.json b/internal/interpreter/testdata/script-tests/insufficient-funds.num.specs.json index 3a98515e..34a02c8f 100644 --- a/internal/interpreter/testdata/script-tests/insufficient-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/insufficient-funds.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "payments:001": { - "GEM": 12 + "GEM": { + "": 12 + } }, "users:001": { - "GEM": 3 + "GEM": { + "": 3 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/kept-in-send-all-inorder.num.specs.json b/internal/interpreter/testdata/script-tests/kept-in-send-all-inorder.num.specs.json index 49e83472..51897c9c 100644 --- a/internal/interpreter/testdata/script-tests/kept-in-send-all-inorder.num.specs.json +++ b/internal/interpreter/testdata/script-tests/kept-in-send-all-inorder.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "COIN": 10 + "COIN": { + "": 10 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/kept-with-balance.num.specs.json b/internal/interpreter/testdata/script-tests/kept-with-balance.num.specs.json index 5caac63f..c7d1bade 100644 --- a/internal/interpreter/testdata/script-tests/kept-with-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/kept-with-balance.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "COIN": 1000 + "COIN": { + "": 1000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/many-kept-dest.num.specs.json b/internal/interpreter/testdata/script-tests/many-kept-dest.num.specs.json index 74715cb9..ef0522b6 100644 --- a/internal/interpreter/testdata/script-tests/many-kept-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/many-kept-dest.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "USD/2": 100 + "USD/2": { + "": 100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/many-max-dest.num.specs.json b/internal/interpreter/testdata/script-tests/many-max-dest.num.specs.json index d5638cb9..61f7c5ac 100644 --- a/internal/interpreter/testdata/script-tests/many-max-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/many-max-dest.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "USD/2": 100 + "USD/2": { + "": 100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/max-with-unbounded-overdraft.num.specs.json b/internal/interpreter/testdata/script-tests/max-with-unbounded-overdraft.num.specs.json index ab69b837..da238d2c 100644 --- a/internal/interpreter/testdata/script-tests/max-with-unbounded-overdraft.num.specs.json +++ b/internal/interpreter/testdata/script-tests/max-with-unbounded-overdraft.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "account1": { - "COIN": 10000 + "COIN": { + "": 10000 + } }, "account2": { - "COIN": 10000 + "COIN": { + "": 10000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/metadata.num.specs.json b/internal/interpreter/testdata/script-tests/metadata.num.specs.json index 3f0788ad..2c295c18 100644 --- a/internal/interpreter/testdata/script-tests/metadata.num.specs.json +++ b/internal/interpreter/testdata/script-tests/metadata.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "sales:042": { - "EUR/2": 2500 + "EUR/2": { + "": 2500 + } }, "users:053": { - "EUR/2": 500 + "EUR/2": { + "": 500 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/minus-infix-monetary.num.specs.json b/internal/interpreter/testdata/script-tests/minus-infix-monetary.num.specs.json index 0727d9b2..50c15bdb 100644 --- a/internal/interpreter/testdata/script-tests/minus-infix-monetary.num.specs.json +++ b/internal/interpreter/testdata/script-tests/minus-infix-monetary.num.specs.json @@ -13,4 +13,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/minus-infix-number.num.specs.json b/internal/interpreter/testdata/script-tests/minus-infix-number.num.specs.json index 336bb433..492863a4 100644 --- a/internal/interpreter/testdata/script-tests/minus-infix-number.num.specs.json +++ b/internal/interpreter/testdata/script-tests/minus-infix-number.num.specs.json @@ -13,4 +13,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/minus-prefix-number.num.specs.json b/internal/interpreter/testdata/script-tests/minus-prefix-number.num.specs.json index 68dac993..0e3abba8 100644 --- a/internal/interpreter/testdata/script-tests/minus-prefix-number.num.specs.json +++ b/internal/interpreter/testdata/script-tests/minus-prefix-number.num.specs.json @@ -13,4 +13,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json b/internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json index 51120ad9..9403714c 100644 --- a/internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json @@ -3,7 +3,11 @@ { "it": "-", "balances": { - "memo:main": { "EUR/2": 99999999 } + "memo:main": { + "EUR/2": { + "": 99999999 + } + } }, "variables": {}, "metadata": {}, diff --git a/internal/interpreter/testdata/script-tests/negative-max-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/negative-max-send-all.num.specs.json index 6f5984b0..156af908 100644 --- a/internal/interpreter/testdata/script-tests/negative-max-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/negative-max-send-all.num.specs.json @@ -4,10 +4,12 @@ "it": "-", "balances": { "src": { - "USD/2": 0 + "USD/2": { + "": 0 + } } }, "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/negative-max.num.specs.json b/internal/interpreter/testdata/script-tests/negative-max.num.specs.json index 39455ca4..f368aded 100644 --- a/internal/interpreter/testdata/script-tests/negative-max.num.specs.json +++ b/internal/interpreter/testdata/script-tests/negative-max.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "USD/2": 0 + "USD/2": { + "": 0 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/nested-remaining-complex.num.specs.json b/internal/interpreter/testdata/script-tests/nested-remaining-complex.num.specs.json index 22af2c9f..967fdeee 100644 --- a/internal/interpreter/testdata/script-tests/nested-remaining-complex.num.specs.json +++ b/internal/interpreter/testdata/script-tests/nested-remaining-complex.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "orders:1234": { - "EUR/2": 10000 + "EUR/2": { + "": 10000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/ovedrafts-playground-example.num.specs.json b/internal/interpreter/testdata/script-tests/ovedrafts-playground-example.num.specs.json index 2d281438..16b26496 100644 --- a/internal/interpreter/testdata/script-tests/ovedrafts-playground-example.num.specs.json +++ b/internal/interpreter/testdata/script-tests/ovedrafts-playground-example.num.specs.json @@ -5,7 +5,9 @@ "balances": { "users:2345:credit": {}, "users:2345:main": { - "USD/2": 8000 + "USD/2": { + "": 8000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-in-send-all-when-noop.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-in-send-all-when-noop.num.specs.json index 54c7c930..29a714ca 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-in-send-all-when-noop.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-in-send-all-when-noop.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "USD/2": 1 + "USD/2": { + "": 1 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-in-send-all.num.specs.json index 66ff0af4..af092f35 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-in-send-all.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "USD/2": 1000 + "USD/2": { + "": 1000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-not-enough-funds.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-not-enough-funds.num.specs.json index 2889d33d..8dfa59ca 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-not-enough-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-not-enough-funds.num.specs.json @@ -5,7 +5,9 @@ "balances": { "users:2345:credit": {}, "users:2345:main": { - "USD/2": 8000 + "USD/2": { + "": 8000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json index d8f1b287..bb574ee8 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "s": { - "COIN": -1 + "COIN": { + "": -1 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance.num.specs.json index 8b2e8d48..9944527a 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "s": { - "COIN": 11 + "COIN": { + "": 11 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-when-negative-ovedraft-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-when-negative-ovedraft-in-send-all.num.specs.json index 8c9c6c6f..24a1304d 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-when-negative-ovedraft-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-when-negative-ovedraft-in-send-all.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "s": { - "COIN": 1 + "COIN": { + "": 1 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-when-not-enough-funds.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-when-not-enough-funds.num.specs.json index 540a5447..e5e70bfc 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-when-not-enough-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-when-not-enough-funds.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "users:1234": { - "COIN": 1 + "COIN": { + "": 1 + } } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/remaining-kept-in-send-all-inorder.num.specs.json b/internal/interpreter/testdata/script-tests/remaining-kept-in-send-all-inorder.num.specs.json index b77df05a..ab39872e 100644 --- a/internal/interpreter/testdata/script-tests/remaining-kept-in-send-all-inorder.num.specs.json +++ b/internal/interpreter/testdata/script-tests/remaining-kept-in-send-all-inorder.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "COIN": 1000 + "COIN": { + "": 1000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/remaining-none-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/remaining-none-in-send-all.num.specs.json index 4b60aec5..176c4bba 100644 --- a/internal/interpreter/testdata/script-tests/remaining-none-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/remaining-none-in-send-all.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "COIN": 10 + "COIN": { + "": 10 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__multi-postings.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__multi-postings.num.specs.json index 1882701e..f9fb00d0 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__multi-postings.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__multi-postings.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD": 20 + "USD": { + "": 20 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-a-different-asset.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-a-different-asset.num.specs.json index 8adbde0f..75399d0b 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-a-different-asset.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-a-different-asset.num.specs.json @@ -4,8 +4,12 @@ "it": "-", "balances": { "alice": { - "COIN": 100, - "USD": 20 + "COIN": { + "": 100 + }, + "USD": { + "": 20 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-all-negative-balance.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-all-negative-balance.num.specs.json index 6d9c6bd9..b3f04bf3 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-all-negative-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-all-negative-balance.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD": -100 + "USD": { + "": -100 + } } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-all.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-all.num.specs.json index 15a5cc24..cc4678d3 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-all.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD": 20 + "USD": { + "": 20 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-causes-failure.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-causes-failure.num.specs.json index b714008c..115ebac7 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-causes-failure.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-causes-failure.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD/2": 30 + "USD/2": { + "": 30 + } } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-more-than-balance.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-more-than-balance.num.specs.json index 15a5cc24..cc4678d3 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-more-than-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-more-than-balance.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD": 20 + "USD": { + "": 20 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__simple.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__simple.num.specs.json index 89dcf05e..5fb02e9d 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__simple.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__simple.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD": 20 + "USD": { + "": 20 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__with-asset-var.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__with-asset-var.num.specs.json index 035c7709..c03edea1 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__with-asset-var.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__with-asset-var.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD": 20 + "USD": { + "": 20 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__with-monetary-var.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__with-monetary-var.num.specs.json index 697b1729..1ffab7ec 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__with-monetary-var.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__with-monetary-var.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD": 20 + "USD": { + "": 20 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/send-all-destinatio-allot-complex.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-destinatio-allot-complex.num.specs.json index a44dc64e..9ffcf68d 100644 --- a/internal/interpreter/testdata/script-tests/send-all-destinatio-allot-complex.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-destinatio-allot-complex.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "users:001": { - "USD/2": 15 + "USD/2": { + "": 15 + } }, "users:002": { - "USD/2": 15 + "USD/2": { + "": 15 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-destinatio-allot.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-destinatio-allot.num.specs.json index 3db9ef2f..51afa74d 100644 --- a/internal/interpreter/testdata/script-tests/send-all-destinatio-allot.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-destinatio-allot.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "users:001": { - "USD/2": 30 + "USD/2": { + "": 30 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-many-max-in-dest.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-many-max-in-dest.num.specs.json index b7be78cb..1c36b28c 100644 --- a/internal/interpreter/testdata/script-tests/send-all-many-max-in-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-many-max-in-dest.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "USD/2": 15 + "USD/2": { + "": 15 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-multi.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-multi.num.specs.json index b30bee30..d8c1e628 100644 --- a/internal/interpreter/testdata/script-tests/send-all-multi.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-multi.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "users:001:credit": { - "USD/2": 22 + "USD/2": { + "": 22 + } }, "users:001:wallet": { - "USD/2": 19 + "USD/2": { + "": 19 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-variable.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-variable.num.specs.json index 4ab2bdf6..856ce239 100644 --- a/internal/interpreter/testdata/script-tests/send-all-variable.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-variable.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "users:001": { - "USD/2": 17 + "USD/2": { + "": 17 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/send-all-when-negative-with-overdraft.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-when-negative-with-overdraft.num.specs.json index 86428b45..a59dfcba 100644 --- a/internal/interpreter/testdata/script-tests/send-all-when-negative-with-overdraft.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-when-negative-with-overdraft.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "users:001": { - "USD/2": -100 + "USD/2": { + "": -100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-when-negative.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-when-negative.num.specs.json index 98bdf829..f2d21c6c 100644 --- a/internal/interpreter/testdata/script-tests/send-all-when-negative.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-when-negative.num.specs.json @@ -4,10 +4,12 @@ "it": "-", "balances": { "users:001": { - "USD/2": -100 + "USD/2": { + "": -100 + } } }, "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/send-all.num.specs.json b/internal/interpreter/testdata/script-tests/send-all.num.specs.json index 72b5d1d7..62da332c 100644 --- a/internal/interpreter/testdata/script-tests/send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "users:001": { - "USD/2": 17 + "USD/2": { + "": 17 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-allt-max-in-dest.num.specs.json b/internal/interpreter/testdata/script-tests/send-allt-max-in-dest.num.specs.json index c7ac32a1..eced1e2e 100644 --- a/internal/interpreter/testdata/script-tests/send-allt-max-in-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-allt-max-in-dest.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "USD/2": 100 + "USD/2": { + "": 100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-allt-max-in-src.num.specs.json b/internal/interpreter/testdata/script-tests/send-allt-max-in-src.num.specs.json index 50090e85..197986ab 100644 --- a/internal/interpreter/testdata/script-tests/send-allt-max-in-src.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-allt-max-in-src.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "src1": { - "USD/2": 100 + "USD/2": { + "": 100 + } }, "src2": { - "USD/2": 200 + "USD/2": { + "": 200 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-allt-max-when-no-amount.num.specs.json b/internal/interpreter/testdata/script-tests/send-allt-max-when-no-amount.num.specs.json index 98e41836..e908dd15 100644 --- a/internal/interpreter/testdata/script-tests/send-allt-max-when-no-amount.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-allt-max-when-no-amount.num.specs.json @@ -5,10 +5,12 @@ "balances": { "src": {}, "src1": { - "USD/2": 0 + "USD/2": { + "": 0 + } } }, "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/send-when-negative-balance.num.specs.json b/internal/interpreter/testdata/script-tests/send-when-negative-balance.num.specs.json index 847f6882..a9cc74a9 100644 --- a/internal/interpreter/testdata/script-tests/send-when-negative-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-when-negative-balance.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "s": { - "COIN": -5 + "COIN": { + "": -5 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-zero.num.specs.json b/internal/interpreter/testdata/script-tests/send-zero.num.specs.json index 5d6aaaa7..78c8bd3d 100644 --- a/internal/interpreter/testdata/script-tests/send-zero.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-zero.num.specs.json @@ -8,4 +8,4 @@ "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/send.num.specs.json b/internal/interpreter/testdata/script-tests/send.num.specs.json index c58a4fc7..38f4eb15 100644 --- a/internal/interpreter/testdata/script-tests/send.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "EUR/2": 100 + "EUR/2": { + "": 100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/source-allotment-invalid-amt.num.specs.json b/internal/interpreter/testdata/script-tests/source-allotment-invalid-amt.num.specs.json index 15efe3cb..2173b37f 100644 --- a/internal/interpreter/testdata/script-tests/source-allotment-invalid-amt.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source-allotment-invalid-amt.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "a": { - "COIN": 1 + "COIN": { + "": 1 + } } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/source-allotment.num.specs.json b/internal/interpreter/testdata/script-tests/source-allotment.num.specs.json index d8c2cd01..fe26f154 100644 --- a/internal/interpreter/testdata/script-tests/source-allotment.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source-allotment.num.specs.json @@ -4,13 +4,19 @@ "it": "-", "balances": { "a": { - "COIN": 100 + "COIN": { + "": 100 + } }, "b": { - "COIN": 100 + "COIN": { + "": 100 + } }, "c": { - "COIN": 100 + "COIN": { + "": 100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/source-complex.num.specs.json b/internal/interpreter/testdata/script-tests/source-complex.num.specs.json index 02642dca..c3964d96 100644 --- a/internal/interpreter/testdata/script-tests/source-complex.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source-complex.num.specs.json @@ -4,16 +4,24 @@ "it": "-", "balances": { "a": { - "COIN": 1000 + "COIN": { + "": 1000 + } }, "b": { - "COIN": 40 + "COIN": { + "": 40 + } }, "c": { - "COIN": 1000 + "COIN": { + "": 1000 + } }, "d": { - "COIN": 1000 + "COIN": { + "": 1000 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/source-overlapping.num.specs.json b/internal/interpreter/testdata/script-tests/source-overlapping.num.specs.json index 6e4830d1..858f6e46 100644 --- a/internal/interpreter/testdata/script-tests/source-overlapping.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source-overlapping.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "a": { - "COIN": 99 + "COIN": { + "": 99 + } }, "b": { - "COIN": 3 + "COIN": { + "": 3 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/source.num.specs.json b/internal/interpreter/testdata/script-tests/source.num.specs.json index 011726b8..20f45613 100644 --- a/internal/interpreter/testdata/script-tests/source.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "payments:001": { - "GEM": 12 + "GEM": { + "": 12 + } }, "users:001": { - "GEM": 3 + "GEM": { + "": 3 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/track-balances-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/track-balances-send-all.num.specs.json index 388bed24..6748a374 100644 --- a/internal/interpreter/testdata/script-tests/track-balances-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/track-balances-send-all.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "COIN": 42 + "COIN": { + "": 42 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/track-balances.num.specs.json b/internal/interpreter/testdata/script-tests/track-balances.num.specs.json index 46bd9309..fb3bcb22 100644 --- a/internal/interpreter/testdata/script-tests/track-balances.num.specs.json +++ b/internal/interpreter/testdata/script-tests/track-balances.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "a": { - "COIN": 50 + "COIN": { + "": 50 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/track-balances2.num.specs.json b/internal/interpreter/testdata/script-tests/track-balances2.num.specs.json index fe4851a6..a9b3cb13 100644 --- a/internal/interpreter/testdata/script-tests/track-balances2.num.specs.json +++ b/internal/interpreter/testdata/script-tests/track-balances2.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "a": { - "COIN": 60 + "COIN": { + "": 60 + } } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/track-balances3.num.specs.json b/internal/interpreter/testdata/script-tests/track-balances3.num.specs.json index ec9ec158..40c0739a 100644 --- a/internal/interpreter/testdata/script-tests/track-balances3.num.specs.json +++ b/internal/interpreter/testdata/script-tests/track-balances3.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "foo": { - "COIN": 2000 + "COIN": { + "": 2000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json b/internal/interpreter/testdata/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json index 2429a124..3dce9391 100644 --- a/internal/interpreter/testdata/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "users:2345:main": { - "USD/2": 8000 + "USD/2": { + "": 8000 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/update-balances.num.specs.json b/internal/interpreter/testdata/script-tests/update-balances.num.specs.json index d02b3426..5a07dc72 100644 --- a/internal/interpreter/testdata/script-tests/update-balances.num.specs.json +++ b/internal/interpreter/testdata/script-tests/update-balances.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "alice": { - "USD": 100 + "USD": { + "": 100 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/use-balance-twice.num.specs.json b/internal/interpreter/testdata/script-tests/use-balance-twice.num.specs.json index 8481ec76..e9af3a54 100644 --- a/internal/interpreter/testdata/script-tests/use-balance-twice.num.specs.json +++ b/internal/interpreter/testdata/script-tests/use-balance-twice.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "src": { - "COIN": 50 + "COIN": { + "": 50 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/use-different-assets-with-same-source-account.num.specs.json b/internal/interpreter/testdata/script-tests/use-different-assets-with-same-source-account.num.specs.json index c14650e6..56249440 100644 --- a/internal/interpreter/testdata/script-tests/use-different-assets-with-same-source-account.num.specs.json +++ b/internal/interpreter/testdata/script-tests/use-different-assets-with-same-source-account.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "account1": { - "A": 100 + "A": { + "": 100 + } }, "account2": { - "B": 100 + "B": { + "": 100 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/variable-asset.num.specs.json b/internal/interpreter/testdata/script-tests/variable-asset.num.specs.json index 36028aa3..d04831a1 100644 --- a/internal/interpreter/testdata/script-tests/variable-asset.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-asset.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "alice": { - "USD": 10 + "USD": { + "": 10 + } }, "bob": { - "USD": 10 + "USD": { + "": 10 + } }, "swap": {} }, diff --git a/internal/interpreter/testdata/script-tests/variable-balance__1.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__1.num.specs.json index 835adb53..fa51de70 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__1.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__1.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "A": { - "USD/2": 40 + "USD/2": { + "": 40 + } }, "C": { - "USD/2": 90 + "USD/2": { + "": 90 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/variable-balance__2.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__2.num.specs.json index 99695ecb..f35d2ff4 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__2.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__2.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "A": { - "USD/2": 400 + "USD/2": { + "": 400 + } }, "C": { - "USD/2": 90 + "USD/2": { + "": 90 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/variable-balance__3.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__3.num.specs.json index ec5a6f04..26d1bb6e 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__3.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__3.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "A": { - "USD/2": 40 + "USD/2": { + "": 40 + } }, "C": { - "USD/2": 90 + "USD/2": { + "": 90 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/variable-balance__4.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__4.num.specs.json index 308be728..1e35cb05 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__4.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__4.num.specs.json @@ -4,10 +4,14 @@ "it": "-", "balances": { "A": { - "USD/2": 400 + "USD/2": { + "": 400 + } }, "C": { - "USD/2": 90 + "USD/2": { + "": 90 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/variable-balance__5.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__5.num.specs.json index 9170724f..a84df651 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__5.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__5.num.specs.json @@ -4,19 +4,29 @@ "it": "-", "balances": { "a": { - "COIN": 1000 + "COIN": { + "": 1000 + } }, "b": { - "COIN": 40 + "COIN": { + "": 40 + } }, "c": { - "COIN": 1000 + "COIN": { + "": 1000 + } }, "d": { - "COIN": 1000 + "COIN": { + "": 1000 + } }, "maxAcc": { - "COIN": 120 + "COIN": { + "": 120 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/variables-json.num.specs.json b/internal/interpreter/testdata/script-tests/variables-json.num.specs.json index 54900447..d55ccf83 100644 --- a/internal/interpreter/testdata/script-tests/variables-json.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variables-json.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "users:001": { - "EUR/2": 1000 + "EUR/2": { + "": 1000 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/variables.num.specs.json b/internal/interpreter/testdata/script-tests/variables.num.specs.json index 6705d59b..7a41c415 100644 --- a/internal/interpreter/testdata/script-tests/variables.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variables.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "users:001": { - "EUR/2": 1000 + "EUR/2": { + "": 1000 + } } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/world-source.num.specs.json b/internal/interpreter/testdata/script-tests/world-source.num.specs.json index 551e59f9..504195e0 100644 --- a/internal/interpreter/testdata/script-tests/world-source.num.specs.json +++ b/internal/interpreter/testdata/script-tests/world-source.num.specs.json @@ -4,7 +4,9 @@ "it": "-", "balances": { "a": { - "GEM": 1 + "GEM": { + "": 1 + } } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/zero-postings-explicit-allotment.num.specs.json b/internal/interpreter/testdata/script-tests/zero-postings-explicit-allotment.num.specs.json index 14cd606a..46ef4203 100644 --- a/internal/interpreter/testdata/script-tests/zero-postings-explicit-allotment.num.specs.json +++ b/internal/interpreter/testdata/script-tests/zero-postings-explicit-allotment.num.specs.json @@ -9,4 +9,4 @@ "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/interpreter/testdata/script-tests/zero-postings-explicit-inorder.num.specs.json b/internal/interpreter/testdata/script-tests/zero-postings-explicit-inorder.num.specs.json index 23428b37..32812803 100644 --- a/internal/interpreter/testdata/script-tests/zero-postings-explicit-inorder.num.specs.json +++ b/internal/interpreter/testdata/script-tests/zero-postings-explicit-inorder.num.specs.json @@ -10,4 +10,4 @@ "expect.postings": [] } ] -} \ No newline at end of file +} diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index 11b38467..61652644 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -35,17 +35,11 @@ func parseBalancesJson(balancesRaw any) (interpreter.Balances, *mcp.CallToolResu iBalances[account][asset] = interpreter.ColorBalance{} } - // Accept either { "USD/2": 100 } (uncolored shorthand) or - // { "USD/2": { "": 100, "RED": 50 } } (full color form). - if amount, ok := perAssetRaw.(float64); ok { - n, _ := big.NewFloat(amount).Int(new(big.Int)) - iBalances[account][asset][""] = n - continue - } - + // Expected shape: { "USD/2": { "": 100, "RED": 50 } }. + // Color "" is the uncolored bucket. colorMap, ok := perAssetRaw.(map[string]any) if !ok { - return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected number or color map for %s/%s, got: <%#v>", account, asset, perAssetRaw)) + return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected {color: amount} object for %s/%s, got: <%#v>", account, asset, perAssetRaw)) } for color, amountRaw := range colorMap { amount, ok := amountRaw.(float64) diff --git a/internal/specs_format/__snapshots__/runner_test.snap b/internal/specs_format/__snapshots__/runner_test.snap index 05f31887..c36e6830 100755 --- a/internal/specs_format/__snapshots__/runner_test.snap +++ b/internal/specs_format/__snapshots__/runner_test.snap @@ -123,7 +123,7 @@ Error: example.num.specs.json  Error: example.num.specs.json -json: cannot unmarshal number into Go struct field Specs.balances of type map[string]map[string]json.RawMessage +json: cannot unmarshal number into Go struct field Specs.balances of type map[string]map[string]*big.Int --- diff --git a/internal/specs_format/parse_test.go b/internal/specs_format/parse_test.go index ce7f2e14..951ce929 100644 --- a/internal/specs_format/parse_test.go +++ b/internal/specs_format/parse_test.go @@ -15,7 +15,7 @@ func TestParseSpecs(t *testing.T) { raw := ` { "balances": { - "alice": { "EUR": 200 } + "alice": { "EUR": { "": 200 } } }, "variables": { "amt": "200" @@ -24,7 +24,7 @@ func TestParseSpecs(t *testing.T) { { "it": "d1", "balances": { - "bob": { "EUR": 42 } + "bob": { "EUR": { "": 42 } } }, "expect.postings": [ { diff --git a/internal/specs_format/run_test.go b/internal/specs_format/run_test.go index 0bb505e5..311fc0e7 100644 --- a/internal/specs_format/run_test.go +++ b/internal/specs_format/run_test.go @@ -29,7 +29,7 @@ func TestRunSpecsSimple(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": 9999 } }, + "balances": { "src": { "USD": { "": 9999 } } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] @@ -81,13 +81,13 @@ func TestRunSpecsSimple(t *testing.T) { func TestRunSpecsMergeOuter(t *testing.T) { j := `{ "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": 10 } }, + "balances": { "src": { "USD": { "": 10 } } }, "testCases": [ { "variables": { "amount": "1" }, "balances": { - "src": { "EUR": 2 }, - "dest": { "USD": 1 } + "src": { "EUR": { "": 2 } }, + "dest": { "USD": { "": 1 } } }, "it": "t1", "expect.postings": [ @@ -147,7 +147,7 @@ func TestRunWithMissingBalance(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": 1 } }, + "balances": { "src": { "USD": { "": 1 } } }, "expect.error.missingFunds": false, "expect.postings": null } @@ -200,7 +200,7 @@ func TestRunWithMissingBalanceWhenExpectedPostings(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": 1 } }, + "balances": { "src": { "USD": { "": 1 } } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 1 } ] @@ -254,7 +254,7 @@ func TestNullPostingsIsNoop(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": 1 } }, + "balances": { "src": { "USD": { "": 1 } } }, "expect.postings": null } ] @@ -378,7 +378,7 @@ func TestFocus(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "10" }, - "balances": { "src": { "USD": 9999 } }, + "balances": { "src": { "USD": { "": 9999 } } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] @@ -387,7 +387,7 @@ func TestFocus(t *testing.T) { "it": "t2", "focus": true, "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": 9999 } }, + "balances": { "src": { "USD": { "": 9999 } } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] diff --git a/internal/specs_format/runner_test.go b/internal/specs_format/runner_test.go index cb543b02..f13d43dc 100644 --- a/internal/specs_format/runner_test.go +++ b/internal/specs_format/runner_test.go @@ -90,11 +90,11 @@ func TestComplexAssertions(t *testing.T) { { "it": "send when there are enough funds", "balances": { - "alice": { "USD/2": 9999 } + "alice": { "USD/2": { "": 9999 } } }, "expect.endBalances": { - "alice": { "USD/2": -100 }, - "dest": { "USD/2": 1 } + "alice": { "USD/2": { "": -100 } }, + "dest": { "USD/2": { "": 1 } } }, "expect.movements": { "alice": { @@ -106,7 +106,7 @@ func TestComplexAssertions(t *testing.T) { { "it": "tpassing", "balances": { - "alice": { "USD/2": 0 } + "alice": { "USD/2": { "": 0 } } }, "expect.error.missingFunds": true } @@ -161,7 +161,7 @@ func TestSchemaErrSpecs(t *testing.T) { SpecsPath: "example.num.specs.json", NumscriptContent: "", SpecsFileContent: []byte(` - { "balances": 42 } + { "balances": { "": 42 } } `), }, }) @@ -219,7 +219,7 @@ func TestFocusUi(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "10" }, - "balances": { "src": { "USD": 9999 } }, + "balances": { "src": { "USD": { "": 9999 } } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] @@ -228,7 +228,7 @@ func TestFocusUi(t *testing.T) { "it": "t2", "focus": true, "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": 9999 } }, + "balances": { "src": { "USD": { "": 9999 } } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] From cb5e8ce1478ca3b2b4e1c42e809f99bb3f2a01b6 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Wed, 3 Jun 2026 13:36:34 +0200 Subject: [PATCH 06/12] refactor(funds_queue): drop dead color-filter API + document upstream invariant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Pull(amount, color *string) signature accepted an optional color filter that is never used by the interpreter — the only caller is pushReceiver, which always passes nil through PullAnything. Whatever balance check a colored source needs has already happened in tryTakingFromAccount before any Sender is pushed onto the queue: CalculateSafeWithdraw caps the pushed amount at the source's (asset, color) balance, and tryTakingExact raises MissingFundsErr if the queue can't supply the requested total. Cleaning up: - Pull's signature is now Pull(requiredAmount *big.Int) — the unused color filter branch is removed and the function gets a doc-comment spelling out the upstream-bounded / caller-validated contract. - PullColored and PullUncolored wrappers are dropped (no in-tree callers outside the tests that exercised them directly). - funds_queue_test.go: the TestPullColored* + TestReconcileColored* tests are removed (they covered the removed branch). TestPush switches from PullUncolored to PullAnything (functionally identical). - funds_queue_color_test.go: collapses to a single TestFundsQueueCompactDoesNotMergeAcrossColors that still pins the invariant we actually care about — compactTop never merges across colors. The PullColored / PullUncolored selectivity tests went away with their subject. No behavioral change for the interpreter: the removed branch was unreachable from anywhere except the deleted tests, and all color-segregation integration tests (color_semantics_test.go, testdata/script-tests/experimental/asset-colors/) still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/interpreter/funds_queue.go | 35 +++++------ .../interpreter/funds_queue_color_test.go | 55 +++--------------- internal/interpreter/funds_queue_test.go | 58 +------------------ 3 files changed, 25 insertions(+), 123 deletions(-) diff --git a/internal/interpreter/funds_queue.go b/internal/interpreter/funds_queue.go index 007c791a..2e4d18c8 100644 --- a/internal/interpreter/funds_queue.go +++ b/internal/interpreter/funds_queue.go @@ -110,18 +110,25 @@ func (s *fundsQueue) Push(senders ...Sender) { } } +// PullAnything is the entry point used by pushReceiver to drain senders +// for a given amount, regardless of color. Each pulled Sender keeps its own +// Color so the receiver-side posting can carry it untouched. func (s *fundsQueue) PullAnything(requiredAmount *big.Int) []Sender { - return s.Pull(requiredAmount, nil) + return s.Pull(requiredAmount) } -func (s *fundsQueue) PullColored(requiredAmount *big.Int, color string) []Sender { - return s.Pull(requiredAmount, &color) -} -func (s *fundsQueue) PullUncolored(requiredAmount *big.Int) []Sender { - return s.PullColored(requiredAmount, "") -} - -func (s *fundsQueue) Pull(requiredAmount *big.Int, color *string) []Sender { +// Pull drains up to requiredAmount from the head of the queue. It does NOT +// verify that the queue holds at least requiredAmount — it simply returns +// whatever is there. The caller is responsible for checking completeness: +// tryTakingExact raises MissingFundsErr when the total sent is below the +// requested amount. +// +// The queue itself is bounded upstream: every pushSender call has already +// been capped by CalculateSafeWithdraw against the source's (asset, color) +// balance, so the queue never holds more than the source can legitimately +// commit. Color is carried by each Sender — Pull preserves it on the way out +// without ever inspecting it. +func (s *fundsQueue) Pull(requiredAmount *big.Int) []Sender { // clone so that we can manipulate this arg requiredAmount = new(big.Int).Set(requiredAmount) @@ -134,16 +141,6 @@ func (s *fundsQueue) Pull(requiredAmount *big.Int, color *string) []Sender { available := s.senders.Head s.senders = s.senders.Tail - if color != nil && available.Color != *color { - out1 := s.Pull(requiredAmount, color) - s.senders = &queue[Sender]{ - Head: available, - Tail: s.senders, - } - out = append(out, out1...) - break - } - switch available.Amount.Cmp(requiredAmount) { case -1: // not enough: out = append(out, available) diff --git a/internal/interpreter/funds_queue_color_test.go b/internal/interpreter/funds_queue_color_test.go index 9b09aa7a..94c20a76 100644 --- a/internal/interpreter/funds_queue_color_test.go +++ b/internal/interpreter/funds_queue_color_test.go @@ -13,9 +13,11 @@ import ( "github.com/stretchr/testify/require" ) -// compactTop() relies on (Name, Color) equality. Adjacent senders that -// match on both dimensions collapse; those that disagree on either don't. -func TestFundsQueueCompactRespectsColor(t *testing.T) { +// compactTop() relies on (Name, Color) equality: adjacent senders that match +// on both dimensions collapse, those that disagree on either don't. Color +// must NEVER be collapsed across — that would silently merge buckets that +// the source-level segregation is meant to keep apart. +func TestFundsQueueCompactDoesNotMergeAcrossColors(t *testing.T) { t.Parallel() queue := newFundsQueue([]Sender{ @@ -26,52 +28,9 @@ func TestFundsQueueCompactRespectsColor(t *testing.T) { out := queue.PullAnything(big.NewInt(22)) require.Equal(t, []Sender{ + // (a, RED) entries compact into a single 15 ✓ {Name: "a", Color: "RED", Amount: big.NewInt(15)}, + // (a, BLUE) stays distinct ✓ {Name: "a", Color: "BLUE", Amount: big.NewInt(7)}, }, out) } - -// PullColored only pulls senders that match the requested color, leaving -// the rest of the queue untouched and still drainable on subsequent pulls. -func TestFundsQueuePullColoredIsSelective(t *testing.T) { - t.Parallel() - - queue := newFundsQueue([]Sender{ - {Name: "a", Color: "RED", Amount: big.NewInt(10)}, - {Name: "b", Color: "BLUE", Amount: big.NewInt(20)}, - {Name: "c", Color: "RED", Amount: big.NewInt(30)}, - }) - - out := queue.PullColored(big.NewInt(35), "RED") - require.Equal(t, []Sender{ - {Name: "a", Color: "RED", Amount: big.NewInt(10)}, - {Name: "c", Color: "RED", Amount: big.NewInt(25)}, - }, out) - - remaining := queue.PullColored(big.NewInt(20), "BLUE") - require.Equal(t, []Sender{ - {Name: "b", Color: "BLUE", Amount: big.NewInt(20)}, - }, remaining) -} - -// PullUncolored is just PullColored("") — the empty bucket is the same as -// any other color from the queue's perspective. -func TestFundsQueuePullUncoloredIgnoresColored(t *testing.T) { - t.Parallel() - - queue := newFundsQueue([]Sender{ - {Name: "a", Color: "RED", Amount: big.NewInt(100)}, - {Name: "b", Color: "", Amount: big.NewInt(40)}, - }) - - out := queue.PullUncolored(big.NewInt(40)) - require.Equal(t, []Sender{ - {Name: "b", Color: "", Amount: big.NewInt(40)}, - }, out) - - // the RED sender is still there - remaining := queue.PullColored(big.NewInt(50), "RED") - require.Equal(t, []Sender{ - {Name: "a", Color: "RED", Amount: big.NewInt(50)}, - }, remaining) -} diff --git a/internal/interpreter/funds_queue_test.go b/internal/interpreter/funds_queue_test.go index 87672be9..c469a662 100644 --- a/internal/interpreter/funds_queue_test.go +++ b/internal/interpreter/funds_queue_test.go @@ -23,7 +23,7 @@ func TestPush(t *testing.T) { queue := newFundsQueue(nil) queue.Push(Sender{Name: "acc", Amount: big.NewInt(100)}) - out := queue.PullUncolored(big.NewInt(20)) + out := queue.PullAnything(big.NewInt(20)) require.Equal(t, []Sender{ {Name: "acc", Amount: big.NewInt(20)}, }, out) @@ -121,58 +121,6 @@ func TestNoZeroLeftovers(t *testing.T) { }, out) } -func TestReconcileColoredManyDestPerSender(t *testing.T) { - queue := newFundsQueue([]Sender{ - {"src", big.NewInt(10), "X"}, - }) - - out := queue.PullColored(big.NewInt(5), "X") - require.Equal(t, []Sender{ - {Name: "src", Amount: big.NewInt(5), Color: "X"}, - }, out) - - out = queue.PullColored(big.NewInt(5), "X") - require.Equal(t, []Sender{ - {Name: "src", Amount: big.NewInt(5), Color: "X"}, - }, out) - -} - -func TestPullColored(t *testing.T) { - queue := newFundsQueue([]Sender{ - {Name: "s1", Amount: big.NewInt(5)}, - {Name: "s2", Amount: big.NewInt(1), Color: "red"}, - {Name: "s3", Amount: big.NewInt(10)}, - {Name: "s4", Amount: big.NewInt(2), Color: "red"}, - {Name: "s5", Amount: big.NewInt(5)}, - }) - - out := queue.PullColored(big.NewInt(2), "red") - require.Equal(t, []Sender{ - {Name: "s2", Amount: big.NewInt(1), Color: "red"}, - {Name: "s4", Amount: big.NewInt(1), Color: "red"}, - }, out) - - require.Equal(t, []Sender{ - {Name: "s1", Amount: big.NewInt(5)}, - {Name: "s3", Amount: big.NewInt(10)}, - {Name: "s4", Amount: big.NewInt(1), Color: "red"}, - {Name: "s5", Amount: big.NewInt(5)}, - }, queue.PullAll()) -} - -func TestPullColoredComplex(t *testing.T) { - queue := newFundsQueue([]Sender{ - {"s1", big.NewInt(1), "c1"}, - {"s2", big.NewInt(1), "c2"}, - }) - - out := queue.PullColored(big.NewInt(1), "c2") - require.Equal(t, []Sender{ - {Name: "s2", Amount: big.NewInt(1), Color: "c2"}, - }, out) -} - func TestClone(t *testing.T) { fq := newFundsQueue([]Sender{ @@ -190,14 +138,12 @@ func TestClone(t *testing.T) { } func TestCompactFundsAndPush(t *testing.T) { - noCol := "" - queue := newFundsQueue([]Sender{ {Name: "s1", Amount: big.NewInt(2)}, {Name: "s1", Amount: big.NewInt(10)}, }) - queue.Pull(big.NewInt(1), &noCol) + queue.Pull(big.NewInt(1)) queue.Push(Sender{ Name: "pushed", From 90dd2acc1103418caec16a443443a59762629ccc Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 6 Jun 2026 08:53:12 +0200 Subject: [PATCH 07/12] refactor!: balances JSON in value-object form, tolerant decoder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous shape forced every balance entry into a nested {color: amount} map and rejected the bare-number shorthand. Two problems surfaced in the PR thread: 1. `""` is an awkward JSON key for the uncolored bucket — it leaks an internal representation choice into every specs.json / inputs.json. 2. Adding orthogonal dimensions later (scopes, scales) would require another level of nesting and break readers again. The wire format becomes value-object based: - bare integer for the uncolored shorthand: "USD/2": 100 - single value-object: "USD/2": { "color": "RED", "amount": 50 } - array of value-objects (canonical multi-color): "USD/2": [{ "amount": 100 }, { "color": "RED", "amount": 50 }] Reading is tolerant of all three forms; writing always emits the canonical form (bare number when uncolored-only, sorted array otherwise). Future dimensions land as new fields on the value-object, not as new map levels. The 116 .num.specs.json fixtures + inline test strings are updated. JSON schemas (specs / inputs) gain a BalanceEntry definition. --- inputs.schema.json | 20 ++- internal/interpreter/balances_json.go | 142 ++++++++++++++++++ internal/interpreter/color_semantics_test.go | 77 ++++++++-- ...source-prefer-single-source.num.specs.json | 28 +--- .../experimental/top-up-many.num.specs.json | 32 +--- .../experimental/top-up.num.specs.json | 8 +- .../transfer-example.num.specs.json | 24 +-- ...mixed-source-dest-allotment.num.specs.json | 8 +- .../send-max.num.specs.json | 8 +- .../top-up-max.num.specs.json | 16 +- ...allocate-dont-take-too-much.num.specs.json | 8 +- .../script-tests/allocation.num.specs.json | 4 +- .../ask-balance-twice.num.specs.json | 4 +- .../balance-simple.num.specs.json | 4 +- .../script-tests/balance.num.specs.json | 4 +- ...apped-when-less-than-needed.num.specs.json | 8 +- ...pped-when-more-than-balance.num.specs.json | 4 +- .../cascading-sources.num.specs.json | 16 +- ...xceed-overdraft-on-send-all.num.specs.json | 4 +- .../do-not-exceed-overdraft.num.specs.json | 4 +- .../dynamic-allocation.num.specs.json | 4 +- .../empty-postings.num.specs.json | 4 +- .../color-inorder-send-all.num.specs.json | 18 ++- .../asset-colors/color-inorder.num.specs.json | 18 ++- ...-balance-when-missing-funds.num.specs.json | 13 +- .../color-restrict-balance.num.specs.json | 13 +- ...lor-restriction-in-send-all.num.specs.json | 3 +- .../asset-colors/empty-color.num.specs.json | 4 +- ...pending-in-colored-send-all.num.specs.json | 13 +- ...le-spending-in-colored-send.num.specs.json | 13 +- .../asset-scaling/no-solution.num.specs.json | 28 +--- .../scaling-all-allotment.num.specs.json | 8 +- .../scaling-allotment.num.specs.json | 16 +- .../asset-scaling/scaling-kept.num.specs.json | 8 +- .../scaling-send-all.num.specs.json | 16 +- .../scaling-with-oneof.num.specs.json | 12 +- .../asset-scaling/scaling.num.specs.json | 32 +--- ...update-swap-account-balance.num.specs.json | 4 +- ...ript-balance-after-decrease.num.specs.json | 4 +- .../midscript-balance.num.specs.json | 4 +- .../oneof/oneof-in-send-all.num.specs.json | 4 +- .../oneof/oneof-singleton.num.specs.json | 4 +- .../update-balances-with-oneof.num.specs.json | 4 +- ...nction-use-case-remove-debt.num.specs.json | 4 +- ...raft-function-when-negative.num.specs.json | 4 +- ...raft-function-when-positive.num.specs.json | 4 +- .../reach-zero.num.specs.json | 20 +-- .../feature-flag-syntax.num.specs.json | 4 +- .../inoder-destination.num.specs.json | 4 +- .../insufficient-funds.num.specs.json | 8 +- .../kept-in-send-all-inorder.num.specs.json | 4 +- .../kept-with-balance.num.specs.json | 4 +- .../many-kept-dest.num.specs.json | 4 +- .../script-tests/many-max-dest.num.specs.json | 4 +- ...ax-with-unbounded-overdraft.num.specs.json | 8 +- .../script-tests/metadata.num.specs.json | 8 +- .../script-tests/neg-max-dest.num.specs.json | 4 +- .../negative-max-send-all.num.specs.json | 4 +- .../script-tests/negative-max.num.specs.json | 4 +- .../nested-remaining-complex.num.specs.json | 4 +- ...vedrafts-playground-example.num.specs.json | 4 +- ...draft-in-send-all-when-noop.num.specs.json | 4 +- .../overdraft-in-send-all.num.specs.json | 4 +- .../overdraft-not-enough-funds.num.specs.json | 4 +- ...egative-balance-in-send-all.num.specs.json | 4 +- ...draft-when-negative-balance.num.specs.json | 4 +- ...gative-ovedraft-in-send-all.num.specs.json | 4 +- ...draft-when-not-enough-funds.num.specs.json | 4 +- ...ng-kept-in-send-all-inorder.num.specs.json | 4 +- .../remaining-none-in-send-all.num.specs.json | 4 +- ...rom-account__multi-postings.num.specs.json | 4 +- ...unt__save-a-different-asset.num.specs.json | 8 +- ...__save-all-negative-balance.num.specs.json | 4 +- ...save-from-account__save-all.num.specs.json | 4 +- ...ccount__save-causes-failure.num.specs.json | 4 +- ...unt__save-more-than-balance.num.specs.json | 4 +- .../save-from-account__simple.num.specs.json | 4 +- ...rom-account__with-asset-var.num.specs.json | 4 +- ...-account__with-monetary-var.num.specs.json | 4 +- ...ll-destinatio-allot-complex.num.specs.json | 8 +- .../send-all-destinatio-allot.num.specs.json | 4 +- .../send-all-many-max-in-dest.num.specs.json | 4 +- .../send-all-multi.num.specs.json | 8 +- .../send-all-variable.num.specs.json | 4 +- ...hen-negative-with-overdraft.num.specs.json | 4 +- .../send-all-when-negative.num.specs.json | 4 +- .../script-tests/send-all.num.specs.json | 4 +- .../send-allt-max-in-dest.num.specs.json | 4 +- .../send-allt-max-in-src.num.specs.json | 8 +- ...end-allt-max-when-no-amount.num.specs.json | 4 +- .../send-when-negative-balance.num.specs.json | 4 +- .../testdata/script-tests/send.num.specs.json | 4 +- ...ource-allotment-invalid-amt.num.specs.json | 4 +- .../source-allotment.num.specs.json | 12 +- .../source-complex.num.specs.json | 16 +- .../source-overlapping.num.specs.json | 8 +- .../script-tests/source.num.specs.json | 8 +- .../track-balances-send-all.num.specs.json | 4 +- .../track-balances.num.specs.json | 4 +- .../track-balances2.num.specs.json | 4 +- .../track-balances3.num.specs.json | 4 +- ...draft-when-not-enough-funds.num.specs.json | 4 +- .../update-balances.num.specs.json | 4 +- .../use-balance-twice.num.specs.json | 4 +- ...ts-with-same-source-account.num.specs.json | 8 +- .../variable-asset.num.specs.json | 8 +- .../variable-balance__1.num.specs.json | 8 +- .../variable-balance__2.num.specs.json | 8 +- .../variable-balance__3.num.specs.json | 8 +- .../variable-balance__4.num.specs.json | 8 +- .../variable-balance__5.num.specs.json | 20 +-- .../variables-json.num.specs.json | 4 +- .../script-tests/variables.num.specs.json | 4 +- .../script-tests/world-source.num.specs.json | 4 +- .../__snapshots__/runner_test.snap | 14 +- internal/specs_format/parse_test.go | 4 +- internal/specs_format/run_test.go | 18 +-- internal/specs_format/runner_test.go | 14 +- specs.schema.json | 20 ++- 119 files changed, 516 insertions(+), 620 deletions(-) create mode 100644 internal/interpreter/balances_json.go diff --git a/inputs.schema.json b/inputs.schema.json index ee73b798..d607d4e1 100644 --- a/inputs.schema.json +++ b/inputs.schema.json @@ -30,13 +30,31 @@ "additionalProperties": false, "patternProperties": { "^([A-Z]+(/[0-9]+)?)$": { - "type": "number" + "oneOf": [ + { "type": "number", "description": "Uncolored shorthand" }, + { "$ref": "#/definitions/BalanceEntry" }, + { "type": "array", "items": { "$ref": "#/definitions/BalanceEntry" } } + ] } } } } }, + "BalanceEntry": { + "type": "object", + "additionalProperties": false, + "required": ["amount"], + "properties": { + "color": { + "type": "string", + "pattern": "^[A-Z]*$", + "description": "Color bucket. Omit (or set to \"\") for the uncolored bucket." + }, + "amount": { "type": "number" } + } + }, + "VariablesMap": { "type": "object", "description": "Map of variable name to variable stringified value", diff --git a/internal/interpreter/balances_json.go b/internal/interpreter/balances_json.go new file mode 100644 index 00000000..77551265 --- /dev/null +++ b/internal/interpreter/balances_json.go @@ -0,0 +1,142 @@ +package interpreter + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "sort" +) + +// Balances JSON shape. +// +// Canonical write form per (account, asset) entry: +// +// - a bare integer when only the uncolored bucket is set: +// "USD/2": 100 +// +// - an array of value-objects when one or more colors are present: +// "USD/2": [ +// { "amount": 100 }, // uncolored — no "color" field +// { "color": "RED", "amount": 50 } // colored +// ] +// +// Tolerant read accepts three forms per (account, asset) entry: +// 1. JSON number → single uncolored bucket +// 2. JSON object → single value-object with optional "color" and required "amount" +// 3. JSON array → list of value-objects (canonical multi-color form) +// +// Adding orthogonal dimensions in the future (scopes, scales, …) is done by +// extending the value-object schema rather than adding a new nesting level. + +type balanceEntry struct { + Color string `json:"color,omitempty"` + Amount *big.Int `json:"amount"` +} + +func (b Balances) MarshalJSON() ([]byte, error) { + type assetWire = json.RawMessage + + out := make(map[string]map[string]assetWire, len(b)) + for account, accBalances := range b { + assets := make(map[string]assetWire, len(accBalances)) + for asset, colorMap := range accBalances { + raw, err := marshalColorBalance(colorMap) + if err != nil { + return nil, fmt.Errorf("balances[%q][%q]: %w", account, asset, err) + } + assets[asset] = raw + } + out[account] = assets + } + return json.Marshal(out) +} + +func marshalColorBalance(cb ColorBalance) (json.RawMessage, error) { + if len(cb) == 1 { + if amount, ok := cb[""]; ok { + return json.Marshal(amount) + } + } + + colors := make([]string, 0, len(cb)) + for c := range cb { + colors = append(colors, c) + } + sort.Strings(colors) + + entries := make([]balanceEntry, len(colors)) + for i, c := range colors { + entries[i] = balanceEntry{Color: c, Amount: cb[c]} + } + return json.Marshal(entries) +} + +func (b *Balances) UnmarshalJSON(data []byte) error { + var raw map[string]map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + out := make(Balances, len(raw)) + for account, assets := range raw { + accBalance := make(AccountBalance, len(assets)) + for asset, rawValue := range assets { + cb, err := unmarshalColorBalance(rawValue) + if err != nil { + return fmt.Errorf("balances[%q][%q]: %w", account, asset, err) + } + accBalance[asset] = cb + } + out[account] = accBalance + } + *b = out + return nil +} + +func unmarshalColorBalance(data []byte) (ColorBalance, error) { + trimmed := bytes.TrimSpace(data) + if len(trimmed) == 0 { + return nil, fmt.Errorf("empty balance entry") + } + + switch trimmed[0] { + case '[': + var entries []balanceEntry + dec := json.NewDecoder(bytes.NewReader(trimmed)) + dec.DisallowUnknownFields() + if err := dec.Decode(&entries); err != nil { + return nil, err + } + cb := make(ColorBalance, len(entries)) + for i, e := range entries { + if e.Amount == nil { + return nil, fmt.Errorf("entry %d: missing \"amount\"", i) + } + if _, exists := cb[e.Color]; exists { + return nil, fmt.Errorf("entry %d: duplicate color %q", i, e.Color) + } + cb[e.Color] = e.Amount + } + return cb, nil + + case '{': + var entry balanceEntry + dec := json.NewDecoder(bytes.NewReader(trimmed)) + dec.DisallowUnknownFields() + if err := dec.Decode(&entry); err != nil { + return nil, err + } + if entry.Amount == nil { + return nil, fmt.Errorf("missing \"amount\" field") + } + return ColorBalance{entry.Color: entry.Amount}, nil + + default: + amount := new(big.Int) + if err := json.Unmarshal(trimmed, amount); err != nil { + return nil, fmt.Errorf("expected number, value-object, or array; got %s", string(trimmed)) + } + return ColorBalance{"": amount}, nil + } +} diff --git a/internal/interpreter/color_semantics_test.go b/internal/interpreter/color_semantics_test.go index 30b4ee87..91486160 100644 --- a/internal/interpreter/color_semantics_test.go +++ b/internal/interpreter/color_semantics_test.go @@ -486,23 +486,29 @@ func TestColorComposesWithAssetPrecision(t *testing.T) { require.Equal(t, "COL", postings[0].Color) } -// JSON unmarshal accepts both shapes (flat + colored), as documented in -// Balances JSON shape: a {color: amount} object under each (account, asset). -// Color "" is the uncolored bucket. No shorthand is accepted — uncolored -// balances must be explicit. +// Balances JSON shape — see balances_json.go. +// Three forms are accepted under each (account, asset) entry: +// 1. bare number (uncolored shorthand) +// 2. single value-object {amount, color?} +// 3. array of value-objects (canonical multi-color form) func TestBalancesJSONShape(t *testing.T) { t.Parallel() src := `{ "alice": { - "USD/2": {"": 100, "RED": 50}, - "EUR/2": {"": -42} + "USD/2": [ + { "amount": 100 }, + { "color": "RED", "amount": 50 } + ], + "EUR/2": -42, + "GBP": { "color": "BLUE", "amount": 7 } } }` want := machine.Balances{ "alice": machine.AccountBalance{ "USD/2": machine.ColorBalance{"": big.NewInt(100), "RED": big.NewInt(50)}, "EUR/2": machine.Uncolored(big.NewInt(-42)), + "GBP": machine.ColorBalance{"BLUE": big.NewInt(7)}, }, } @@ -512,13 +518,60 @@ func TestBalancesJSONShape(t *testing.T) { "unexpected balances: want %v, got %v", want, got) } -// The flat shorthand `{"USD/2": 100}` is no longer supported — uncolored -// balances must spell out the empty-color key explicitly. We assert the -// rejection here so callers never accidentally drop into a permissive parse. -func TestBalancesJSONRejectsFlatShorthand(t *testing.T) { +// Canonical write: uncolored-only collapses to a bare number; multi-color +// emits a deterministic array sorted by color. +func TestBalancesJSONCanonicalWrite(t *testing.T) { + t.Parallel() + + b := machine.Balances{ + "alice": machine.AccountBalance{ + "USD/2": machine.ColorBalance{"": big.NewInt(100), "RED": big.NewInt(50)}, + "EUR/2": machine.Uncolored(big.NewInt(-42)), + }, + } + + encoded, err := json.Marshal(b) + require.NoError(t, err) + + const want = `{"alice":{"EUR/2":-42,"USD/2":[{"amount":100},{"color":"RED","amount":50}]}}` + require.JSONEq(t, want, string(encoded)) +} + +// The old strict-nested shape `{"USD/2": {"": 100}}` is no longer valid: +// the value-object must have an "amount" field. This guards against silent +// acceptance of pre-migration fixtures. +func TestBalancesJSONRejectsLegacyNestedShape(t *testing.T) { t.Parallel() var got machine.Balances - err := json.Unmarshal([]byte(`{"alice": {"USD/2": 100}}`), &got) - require.Error(t, err, "flat shorthand must be rejected") + err := json.Unmarshal([]byte(`{"alice": {"USD/2": {"": 100}}}`), &got) + require.Error(t, err, "legacy nested shape must be rejected") +} + +// PrettyPrintPostings must surface Color when any posting carries one — without +// it, two otherwise-identical colored postings would render identically. +func TestPrettyPrintPostingsExposesColor(t *testing.T) { + t.Parallel() + + postings := []machine.Posting{ + {Source: "world", Destination: "alice", Asset: "USD/2", Amount: big.NewInt(100)}, + {Source: "world", Destination: "alice", Asset: "USD/2", Amount: big.NewInt(50), Color: "RED"}, + } + + out := machine.PrettyPrintPostings(postings) + require.Contains(t, out, "Color") + require.Contains(t, out, "RED") +} + +// When no posting is colored, the Color column stays hidden to keep the +// uncolored output unchanged. +func TestPrettyPrintPostingsHidesColorWhenAbsent(t *testing.T) { + t.Parallel() + + postings := []machine.Posting{ + {Source: "world", Destination: "alice", Asset: "USD/2", Amount: big.NewInt(100)}, + } + + out := machine.PrettyPrintPostings(postings) + require.NotContains(t, out, "Color") } diff --git a/internal/interpreter/testdata/numscript-cookbook/experimental/mixed-source-prefer-single-source.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/experimental/mixed-source-prefer-single-source.num.specs.json index 40f3d94e..f8e22097 100644 --- a/internal/interpreter/testdata/numscript-cookbook/experimental/mixed-source-prefer-single-source.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/experimental/mixed-source-prefer-single-source.num.specs.json @@ -11,9 +11,7 @@ "it": "sends from the first source when there is enough balance", "balances": { "s1": { - "USD": { - "": 999 - } + "USD": 999 } }, "expect.postings": [ @@ -29,14 +27,10 @@ "it": "sends from the second one when it has enough balance but the first one doesn't", "balances": { "s1": { - "USD": { - "": 9 - } + "USD": 9 }, "s2": { - "USD": { - "": 999 - } + "USD": 999 } }, "expect.postings": [ @@ -52,14 +46,10 @@ "it": "sends partially from both when none of them has enough balance on its own", "balances": { "s1": { - "USD": { - "": 6 - } + "USD": 6 }, "s2": { - "USD": { - "": 9 - } + "USD": 9 } }, "expect.postings": [ @@ -81,14 +71,10 @@ "it": "fails if there aren't enough funds between all sources", "balances": { "s1": { - "USD": { - "": 5 - } + "USD": 5 }, "s2": { - "USD": { - "": 4 - } + "USD": 4 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/numscript-cookbook/experimental/top-up-many.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/experimental/top-up-many.num.specs.json index 3f2e5963..7bdbcf61 100644 --- a/internal/interpreter/testdata/numscript-cookbook/experimental/top-up-many.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/experimental/top-up-many.num.specs.json @@ -13,14 +13,10 @@ "it": "should be a noop when all balances are >= 0", "balances": { "alice": { - "EUR": { - "": 100 - } + "EUR": 100 }, "bob": { - "EUR": { - "": 200 - } + "EUR": 200 } }, "expect.postings": [] @@ -29,14 +25,10 @@ "it": "should prioritize alice when both have missing funds", "balances": { "alice": { - "EUR": { - "": -120 - } + "EUR": -120 }, "bob": { - "EUR": { - "": -120 - } + "EUR": -120 } }, "expect.postings": [ @@ -52,14 +44,10 @@ "it": "doesn't send funds to alice if there aren't enough funds for the account to be topped-up", "balances": { "alice": { - "EUR": { - "": -80 - } + "EUR": -80 }, "bob": { - "EUR": { - "": -120 - } + "EUR": -120 } }, "expect.postings": [ @@ -75,14 +63,10 @@ "it": "funds are kept if there are spare funds", "balances": { "alice": { - "EUR": { - "": -10 - } + "EUR": -10 }, "bob": { - "EUR": { - "": -20 - } + "EUR": -20 } }, "expect.postings": [] diff --git a/internal/interpreter/testdata/numscript-cookbook/experimental/top-up.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/experimental/top-up.num.specs.json index 535fbc73..70e60b1d 100644 --- a/internal/interpreter/testdata/numscript-cookbook/experimental/top-up.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/experimental/top-up.num.specs.json @@ -8,9 +8,7 @@ "it": "should not emit postings", "balances": { "alice": { - "EUR": { - "": 100 - } + "EUR": 100 } }, "expect.postings": [] @@ -19,9 +17,7 @@ "it": "should send the missing amount to an overdraft account", "balances": { "alice": { - "EUR": { - "": -100 - } + "EUR": -100 } }, "expect.endBalances": { diff --git a/internal/interpreter/testdata/numscript-cookbook/experimental/transfer-example.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/experimental/transfer-example.num.specs.json index 62c4120c..9502a895 100644 --- a/internal/interpreter/testdata/numscript-cookbook/experimental/transfer-example.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/experimental/transfer-example.num.specs.json @@ -14,14 +14,10 @@ "it": "should authorize transfer if both wallet and bank accounts display enough balance", "balances": { "wallet": { - "EUR": { - "": 100 - } + "EUR": 100 }, "bank_account": { - "EUR": { - "": -100 - } + "EUR": -100 } }, "expect.postings": [ @@ -37,14 +33,10 @@ "it": "should not authorize transfer if wallet does not display enough balance and bank account does", "balances": { "wallet": { - "EUR": { - "": 50 - } + "EUR": 50 }, "bank_account": { - "EUR": { - "": -100 - } + "EUR": -100 } }, "expect.error.missingFunds": true @@ -53,14 +45,10 @@ "it": "should not authorize transfer if bank account does not display enough balance and wallet does", "balances": { "wallet": { - "EUR": { - "": 100 - } + "EUR": 100 }, "bank_account": { - "EUR": { - "": -50 - } + "EUR": -50 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/numscript-cookbook/mixed-source-dest-allotment.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/mixed-source-dest-allotment.num.specs.json index 45b05f1e..cd0bc9e9 100644 --- a/internal/interpreter/testdata/numscript-cookbook/mixed-source-dest-allotment.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/mixed-source-dest-allotment.num.specs.json @@ -5,14 +5,10 @@ "it": "matches sources and destinations allotments", "balances": { "src1": { - "USD": { - "": 999 - } + "USD": 999 }, "src2": { - "USD": { - "": 999 - } + "USD": 999 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/numscript-cookbook/send-max.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/send-max.num.specs.json index d6ed98fb..92068884 100644 --- a/internal/interpreter/testdata/numscript-cookbook/send-max.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/send-max.num.specs.json @@ -5,9 +5,7 @@ "it": "is capped to USD100 when balance is higher", "balances": { "src": { - "USD": { - "": 999 - } + "USD": 999 } }, "expect.postings": [ @@ -23,9 +21,7 @@ "it": "allows sending less than the cap when balance is not enough", "balances": { "src": { - "USD": { - "": 42 - } + "USD": 42 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/numscript-cookbook/top-up-max.num.specs.json b/internal/interpreter/testdata/numscript-cookbook/top-up-max.num.specs.json index c0743385..00098115 100644 --- a/internal/interpreter/testdata/numscript-cookbook/top-up-max.num.specs.json +++ b/internal/interpreter/testdata/numscript-cookbook/top-up-max.num.specs.json @@ -6,9 +6,7 @@ }, "balances": { "alice": { - "EUR/2": { - "": 9999 - } + "EUR/2": 9999 } }, "testCases": [ @@ -16,9 +14,7 @@ "it": "should send the amount when destination doesn't reach 150", "balances": { "jon": { - "EUR/2": { - "": 0 - } + "EUR/2": 0 } }, "expect.postings": [ @@ -34,9 +30,7 @@ "it": "should send the amount when destination doesn't reach 150 (2)", "balances": { "jon": { - "EUR/2": { - "": 50 - } + "EUR/2": 50 } }, "expect.postings": [ @@ -52,9 +46,7 @@ "it": "should fail if the end balance would exceed 150", "balances": { "jon": { - "EUR/2": { - "": 51 - } + "EUR/2": 51 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/allocate-dont-take-too-much.num.specs.json b/internal/interpreter/testdata/script-tests/allocate-dont-take-too-much.num.specs.json index ebadeadd..1fc6d9f3 100644 --- a/internal/interpreter/testdata/script-tests/allocate-dont-take-too-much.num.specs.json +++ b/internal/interpreter/testdata/script-tests/allocate-dont-take-too-much.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "users:001": { - "CREDIT": { - "": 100 - } + "CREDIT": 100 }, "users:002": { - "CREDIT": { - "": 110 - } + "CREDIT": 110 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/allocation.num.specs.json b/internal/interpreter/testdata/script-tests/allocation.num.specs.json index 7b1b7f87..02b08a47 100644 --- a/internal/interpreter/testdata/script-tests/allocation.num.specs.json +++ b/internal/interpreter/testdata/script-tests/allocation.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:001": { - "GEM": { - "": 15 - } + "GEM": 15 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/ask-balance-twice.num.specs.json b/internal/interpreter/testdata/script-tests/ask-balance-twice.num.specs.json index aeee81c2..435146cf 100644 --- a/internal/interpreter/testdata/script-tests/ask-balance-twice.num.specs.json +++ b/internal/interpreter/testdata/script-tests/ask-balance-twice.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD/2": { - "": 10 - } + "USD/2": 10 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/balance-simple.num.specs.json b/internal/interpreter/testdata/script-tests/balance-simple.num.specs.json index 5072b86c..53ffac19 100644 --- a/internal/interpreter/testdata/script-tests/balance-simple.num.specs.json +++ b/internal/interpreter/testdata/script-tests/balance-simple.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD/2": { - "": 10 - } + "USD/2": 10 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/balance.num.specs.json b/internal/interpreter/testdata/script-tests/balance.num.specs.json index 4e86ccee..fc6986c0 100644 --- a/internal/interpreter/testdata/script-tests/balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/balance.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "a": { - "EUR/2": { - "": 123 - } + "EUR/2": 123 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/capped-when-less-than-needed.num.specs.json b/internal/interpreter/testdata/script-tests/capped-when-less-than-needed.num.specs.json index ec5c972b..c519e2f0 100644 --- a/internal/interpreter/testdata/script-tests/capped-when-less-than-needed.num.specs.json +++ b/internal/interpreter/testdata/script-tests/capped-when-less-than-needed.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "src1": { - "COIN": { - "": 1000 - } + "COIN": 1000 }, "src2": { - "COIN": { - "": 1000 - } + "COIN": 1000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/capped-when-more-than-balance.num.specs.json b/internal/interpreter/testdata/script-tests/capped-when-more-than-balance.num.specs.json index 05c4e205..2f7cdcf9 100644 --- a/internal/interpreter/testdata/script-tests/capped-when-more-than-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/capped-when-more-than-balance.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 1000 - } + "COIN": 1000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/cascading-sources.num.specs.json b/internal/interpreter/testdata/script-tests/cascading-sources.num.specs.json index f7d8c0bd..7fe60a30 100644 --- a/internal/interpreter/testdata/script-tests/cascading-sources.num.specs.json +++ b/internal/interpreter/testdata/script-tests/cascading-sources.num.specs.json @@ -4,24 +4,16 @@ "it": "-", "balances": { "users:1234:main": { - "USD/2": { - "": 5000 - } + "USD/2": 5000 }, "users:1234:vouchers:2024-01-31": { - "USD/2": { - "": 1000 - } + "USD/2": 1000 }, "users:1234:vouchers:2024-02-17": { - "USD/2": { - "": 3000 - } + "USD/2": 3000 }, "users:1234:vouchers:2024-03-22": { - "USD/2": { - "": 10000 - } + "USD/2": 10000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json index 050ecdef..e9c46c85 100644 --- a/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft-on-send-all.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "s": { - "COIN": { - "": -4 - } + "COIN": -4 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft.num.specs.json b/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft.num.specs.json index 5ca85025..d83e602c 100644 --- a/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft.num.specs.json +++ b/internal/interpreter/testdata/script-tests/do-not-exceed-overdraft.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "s": { - "COIN": { - "": -2 - } + "COIN": -2 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/dynamic-allocation.num.specs.json b/internal/interpreter/testdata/script-tests/dynamic-allocation.num.specs.json index fe0cb7bb..ef002331 100644 --- a/internal/interpreter/testdata/script-tests/dynamic-allocation.num.specs.json +++ b/internal/interpreter/testdata/script-tests/dynamic-allocation.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "a": { - "GEM": { - "": 15 - } + "GEM": 15 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/empty-postings.num.specs.json b/internal/interpreter/testdata/script-tests/empty-postings.num.specs.json index 63a9701c..e1a6dc86 100644 --- a/internal/interpreter/testdata/script-tests/empty-postings.num.specs.json +++ b/internal/interpreter/testdata/script-tests/empty-postings.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "foo": { - "GEM": { - "": 0 - } + "GEM": 0 } }, "expect.postings": [] diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json index 35835d81..318d33ca 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json @@ -7,11 +7,19 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 100, - "BLUE": 30, - "RED": 20 - } + "COIN": [ + { + "amount": 100 + }, + { + "color": "BLUE", + "amount": 30 + }, + { + "color": "RED", + "amount": 20 + } + ] } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json index e95510b6..0e2df535 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json @@ -7,11 +7,19 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 100, - "BLUE": 30, - "RED": 20 - } + "COIN": [ + { + "amount": 100 + }, + { + "color": "BLUE", + "amount": 30 + }, + { + "color": "RED", + "amount": 20 + } + ] } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json index 37e3f88f..b761ac6f 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance-when-missing-funds.num.specs.json @@ -7,10 +7,15 @@ "it": "-", "balances": { "acc": { - "COIN": { - "": 100, - "RED": 1 - } + "COIN": [ + { + "amount": 100 + }, + { + "color": "RED", + "amount": 1 + } + ] } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance.num.specs.json index 93c4e32c..b3c84dc6 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restrict-balance.num.specs.json @@ -7,10 +7,15 @@ "it": "-", "balances": { "acc": { - "COIN": { - "": 1, - "RED": 100 - } + "COIN": [ + { + "amount": 1 + }, + { + "color": "RED", + "amount": 100 + } + ] } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restriction-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restriction-in-send-all.num.specs.json index 00cde547..3bc02a31 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restriction-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-restriction-in-send-all.num.specs.json @@ -8,7 +8,8 @@ "balances": { "src": { "COIN": { - "RED": 42 + "color": "RED", + "amount": 42 } } }, diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/empty-color.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/empty-color.num.specs.json index 0d039cb1..571f280c 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/empty-color.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/empty-color.num.specs.json @@ -7,9 +7,7 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 100 - } + "COIN": 100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json index bf1d2ad1..d9bb50a2 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json @@ -7,10 +7,15 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 100, - "X": 20 - } + "COIN": [ + { + "amount": 100 + }, + { + "color": "X", + "amount": 20 + } + ] } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json index aafdd511..767b3e1b 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json @@ -7,10 +7,15 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 99999, - "X": 20 - } + "COIN": [ + { + "amount": 99999 + }, + { + "color": "X", + "amount": 20 + } + ] } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/no-solution.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/no-solution.num.specs.json index 57f164ab..3ca0b4c9 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/no-solution.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/no-solution.num.specs.json @@ -8,9 +8,7 @@ "it": "allow solution with spare money", "balances": { "src": { - "EUR": { - "": 100 - } + "EUR": 100 } }, "expect.postings": [ @@ -38,12 +36,8 @@ "it": "allow solution with spare money (2)", "balances": { "src": { - "EUR": { - "": 100 - }, - "EUR/1": { - "": 100 - } + "EUR": 100, + "EUR/1": 100 } }, "expect.postings": [ @@ -71,18 +65,10 @@ "it": "no solution", "balances": { "src": { - "EUR": { - "": 0 - }, - "EUR/2": { - "": 1 - }, - "EUR/3": { - "": 1 - }, - "EUR/4": { - "": 10 - } + "EUR": 0, + "EUR/2": 1, + "EUR/3": 1, + "EUR/4": 10 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-all-allotment.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-all-allotment.num.specs.json index 18cc1c86..b0ea06b4 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-all-allotment.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-all-allotment.num.specs.json @@ -5,9 +5,7 @@ ], "balances": { "acc1": { - "EUR/2": { - "": 50 - } + "EUR/2": 50 } }, "testCases": [ @@ -15,9 +13,7 @@ "it": "casts all the avlb", "balances": { "acc2": { - "EUR/3": { - "": 500 - } + "EUR/3": 500 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-allotment.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-allotment.num.specs.json index 4fe9a876..da48ec98 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-allotment.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-allotment.num.specs.json @@ -5,9 +5,7 @@ ], "balances": { "acc1": { - "EUR/2": { - "": 50 - } + "EUR/2": 50 } }, "testCases": [ @@ -15,9 +13,7 @@ "it": "casts all the avlb", "balances": { "acc2": { - "EUR/3": { - "": 500 - } + "EUR/3": 500 } }, "expect.postings": [ @@ -51,9 +47,7 @@ "it": "doesn't send more than needed", "balances": { "acc2": { - "EUR/3": { - "": 999999 - } + "EUR/3": 999999 } }, "expect.postings": [ @@ -87,9 +81,7 @@ "it": "doesn't swap already owned asset", "balances": { "acc2": { - "EUR/2": { - "": 999999 - } + "EUR/2": 999999 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-kept.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-kept.num.specs.json index 9bf3a3cf..fea0689f 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-kept.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-kept.num.specs.json @@ -5,12 +5,8 @@ ], "balances": { "acc": { - "BTC/2": { - "": 2 - }, - "BTC/8": { - "": 1000000 - } + "BTC/2": 2, + "BTC/8": 1000000 } }, "testCases": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-send-all.num.specs.json index db84bef6..694a0645 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-send-all.num.specs.json @@ -8,15 +8,9 @@ "it": "sends all the available assets that can be cast", "balances": { "src": { - "EUR": { - "": 2 - }, - "EUR/2": { - "": 1 - }, - "EUR/3": { - "": 30 - } + "EUR": 2, + "EUR/2": 1, + "EUR/3": 30 } }, "expect.postings": [ @@ -50,9 +44,7 @@ "it": "avoids casting the remainder", "balances": { "src": { - "EUR/3": { - "": 21 - } + "EUR/3": 21 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-with-oneof.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-with-oneof.num.specs.json index fe084267..654b91d0 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-with-oneof.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling-with-oneof.num.specs.json @@ -7,9 +7,7 @@ "balances": { "acc1": {}, "acc2": { - "EUR/2": { - "": 100 - } + "EUR/2": 100 } }, "testCases": [ @@ -17,9 +15,7 @@ "it": "fallbacks to the second branch when there's not enough", "balances": { "acc1": { - "EUR/2": { - "": 1 - } + "EUR/2": 1 } }, "expect.postings": [ @@ -41,9 +37,7 @@ "it": "succeeds in the first branch", "balances": { "acc1": { - "EUR/2": { - "": 100 - } + "EUR/2": 100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling.num.specs.json index 974fda80..ecec370f 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/scaling.num.specs.json @@ -8,12 +8,8 @@ "it": "upscales to an higher currency if possible", "balances": { "src": { - "EUR": { - "": 99 - }, - "EUR/2": { - "": 1 - } + "EUR": 99, + "EUR/2": 1 } }, "expect.postings": [ @@ -41,9 +37,7 @@ "it": "downscales to an smaller currency if possible", "balances": { "src": { - "EUR/3": { - "": 4000 - } + "EUR/3": 4000 } }, "expect.postings": [ @@ -71,12 +65,8 @@ "it": "mix", "balances": { "src": { - "EUR": { - "": 2 - }, - "EUR/2": { - "": 200 - } + "EUR": 2, + "EUR/2": 200 } }, "expect.postings": [ @@ -104,15 +94,9 @@ "it": "fails when there aren't enough funds", "balances": { "src": { - "EUR": { - "": 1 - }, - "EUR/2": { - "": 200 - }, - "EUR/3": { - "": 10 - } + "EUR": 1, + "EUR/2": 200, + "EUR/3": 10 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/update-swap-account-balance.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/update-swap-account-balance.num.specs.json index f96627fa..700f49e5 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-scaling/update-swap-account-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-scaling/update-swap-account-balance.num.specs.json @@ -8,9 +8,7 @@ "it": "updates the swap account balances", "balances": { "src": { - "EUR": { - "": 1 - } + "EUR": 1 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance-after-decrease.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance-after-decrease.num.specs.json index dd090730..03d6d9c3 100644 --- a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance-after-decrease.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance-after-decrease.num.specs.json @@ -7,9 +7,7 @@ "it": "-", "balances": { "acc": { - "USD/2": { - "": 10 - } + "USD/2": 10 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance.num.specs.json index d395a0e4..3436e8ce 100644 --- a/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/mid-script-function-call/midscript-balance.num.specs.json @@ -7,9 +7,7 @@ "it": "-", "balances": { "acc": { - "USD/2": { - "": 42 - } + "USD/2": 42 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-send-all.num.specs.json index 13b44c4e..11f21592 100644 --- a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-in-send-all.num.specs.json @@ -7,9 +7,7 @@ "it": "-", "balances": { "s1": { - "GEM": { - "": 10 - } + "GEM": 10 }, "s2": {}, "s3": {} diff --git a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-singleton.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-singleton.num.specs.json index 7c64f9e5..617d51e0 100644 --- a/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-singleton.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/oneof/oneof-singleton.num.specs.json @@ -7,9 +7,7 @@ "it": "-", "balances": { "a": { - "GEM": { - "": 10 - } + "GEM": 10 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/oneof/update-balances-with-oneof.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/oneof/update-balances-with-oneof.num.specs.json index a8f3b30f..a768236f 100644 --- a/internal/interpreter/testdata/script-tests/experimental/oneof/update-balances-with-oneof.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/oneof/update-balances-with-oneof.num.specs.json @@ -7,9 +7,7 @@ "it": "-", "balances": { "alice": { - "USD": { - "": 100 - } + "USD": 100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-use-case-remove-debt.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-use-case-remove-debt.num.specs.json index 427bdae6..b31e3688 100644 --- a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-use-case-remove-debt.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-use-case-remove-debt.num.specs.json @@ -7,9 +7,7 @@ "it": "-", "balances": { "user:001": { - "USD/2": { - "": -100 - } + "USD/2": -100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-negative.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-negative.num.specs.json index 658fc702..071e38dc 100644 --- a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-negative.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-negative.num.specs.json @@ -7,9 +7,7 @@ "it": "-", "balances": { "acc": { - "EUR/2": { - "": -100 - } + "EUR/2": -100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-positive.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-positive.num.specs.json index dbee73b2..d5a1d0c4 100644 --- a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-positive.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/overdraft-function-when-positive.num.specs.json @@ -7,9 +7,7 @@ "it": "-", "balances": { "acc": { - "EUR/2": { - "": 100 - } + "EUR/2": 100 } }, "expect.postings": [] diff --git a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/reach-zero.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/reach-zero.num.specs.json index c91e5f14..30d34d68 100644 --- a/internal/interpreter/testdata/script-tests/experimental/overdraft-function/reach-zero.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/overdraft-function/reach-zero.num.specs.json @@ -10,9 +10,7 @@ { "balances": { "invoice:001": { - "USD/2": { - "": -999 - } + "USD/2": -999 } }, "it": "only sends to the first one if there's not enough funds to top-up its balance", @@ -28,14 +26,10 @@ { "balances": { "invoice:001": { - "USD/2": { - "": -99 - } + "USD/2": -99 }, "invoice:002": { - "USD/2": { - "": -999 - } + "USD/2": -999 } }, "it": "it sends to the second source after topping-up the first one", @@ -57,14 +51,10 @@ { "balances": { "invoice:001": { - "USD/2": { - "": -2 - } + "USD/2": -2 }, "invoice:002": { - "USD/2": { - "": -3 - } + "USD/2": -3 } }, "it": "it keeps the spare amount", diff --git a/internal/interpreter/testdata/script-tests/feature-flag-syntax.num.specs.json b/internal/interpreter/testdata/script-tests/feature-flag-syntax.num.specs.json index 92df2f90..6b947712 100644 --- a/internal/interpreter/testdata/script-tests/feature-flag-syntax.num.specs.json +++ b/internal/interpreter/testdata/script-tests/feature-flag-syntax.num.specs.json @@ -2,9 +2,7 @@ "$schema": "https://raw.githubusercontent.com/formancehq/numscript/main/specs.schema.json", "balances": { "acc": { - "TK": { - "": -100 - } + "TK": -100 } }, "testCases": [ diff --git a/internal/interpreter/testdata/script-tests/inoder-destination.num.specs.json b/internal/interpreter/testdata/script-tests/inoder-destination.num.specs.json index 7d0632a5..06db229a 100644 --- a/internal/interpreter/testdata/script-tests/inoder-destination.num.specs.json +++ b/internal/interpreter/testdata/script-tests/inoder-destination.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "a": { - "COIN": { - "": 123 - } + "COIN": 123 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/insufficient-funds.num.specs.json b/internal/interpreter/testdata/script-tests/insufficient-funds.num.specs.json index 34a02c8f..3a98515e 100644 --- a/internal/interpreter/testdata/script-tests/insufficient-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/insufficient-funds.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "payments:001": { - "GEM": { - "": 12 - } + "GEM": 12 }, "users:001": { - "GEM": { - "": 3 - } + "GEM": 3 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/kept-in-send-all-inorder.num.specs.json b/internal/interpreter/testdata/script-tests/kept-in-send-all-inorder.num.specs.json index 51897c9c..49e83472 100644 --- a/internal/interpreter/testdata/script-tests/kept-in-send-all-inorder.num.specs.json +++ b/internal/interpreter/testdata/script-tests/kept-in-send-all-inorder.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 10 - } + "COIN": 10 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/kept-with-balance.num.specs.json b/internal/interpreter/testdata/script-tests/kept-with-balance.num.specs.json index c7d1bade..5caac63f 100644 --- a/internal/interpreter/testdata/script-tests/kept-with-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/kept-with-balance.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 1000 - } + "COIN": 1000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/many-kept-dest.num.specs.json b/internal/interpreter/testdata/script-tests/many-kept-dest.num.specs.json index ef0522b6..74715cb9 100644 --- a/internal/interpreter/testdata/script-tests/many-kept-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/many-kept-dest.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "USD/2": { - "": 100 - } + "USD/2": 100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/many-max-dest.num.specs.json b/internal/interpreter/testdata/script-tests/many-max-dest.num.specs.json index 61f7c5ac..d5638cb9 100644 --- a/internal/interpreter/testdata/script-tests/many-max-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/many-max-dest.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "USD/2": { - "": 100 - } + "USD/2": 100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/max-with-unbounded-overdraft.num.specs.json b/internal/interpreter/testdata/script-tests/max-with-unbounded-overdraft.num.specs.json index da238d2c..ab69b837 100644 --- a/internal/interpreter/testdata/script-tests/max-with-unbounded-overdraft.num.specs.json +++ b/internal/interpreter/testdata/script-tests/max-with-unbounded-overdraft.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "account1": { - "COIN": { - "": 10000 - } + "COIN": 10000 }, "account2": { - "COIN": { - "": 10000 - } + "COIN": 10000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/metadata.num.specs.json b/internal/interpreter/testdata/script-tests/metadata.num.specs.json index 2c295c18..3f0788ad 100644 --- a/internal/interpreter/testdata/script-tests/metadata.num.specs.json +++ b/internal/interpreter/testdata/script-tests/metadata.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "sales:042": { - "EUR/2": { - "": 2500 - } + "EUR/2": 2500 }, "users:053": { - "EUR/2": { - "": 500 - } + "EUR/2": 500 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json b/internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json index 9403714c..656e8d3f 100644 --- a/internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/neg-max-dest.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "memo:main": { - "EUR/2": { - "": 99999999 - } + "EUR/2": 99999999 } }, "variables": {}, diff --git a/internal/interpreter/testdata/script-tests/negative-max-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/negative-max-send-all.num.specs.json index 156af908..f50a6f05 100644 --- a/internal/interpreter/testdata/script-tests/negative-max-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/negative-max-send-all.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "USD/2": { - "": 0 - } + "USD/2": 0 } }, "expect.postings": [] diff --git a/internal/interpreter/testdata/script-tests/negative-max.num.specs.json b/internal/interpreter/testdata/script-tests/negative-max.num.specs.json index f368aded..39455ca4 100644 --- a/internal/interpreter/testdata/script-tests/negative-max.num.specs.json +++ b/internal/interpreter/testdata/script-tests/negative-max.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "USD/2": { - "": 0 - } + "USD/2": 0 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/nested-remaining-complex.num.specs.json b/internal/interpreter/testdata/script-tests/nested-remaining-complex.num.specs.json index 967fdeee..22af2c9f 100644 --- a/internal/interpreter/testdata/script-tests/nested-remaining-complex.num.specs.json +++ b/internal/interpreter/testdata/script-tests/nested-remaining-complex.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "orders:1234": { - "EUR/2": { - "": 10000 - } + "EUR/2": 10000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/ovedrafts-playground-example.num.specs.json b/internal/interpreter/testdata/script-tests/ovedrafts-playground-example.num.specs.json index 16b26496..2d281438 100644 --- a/internal/interpreter/testdata/script-tests/ovedrafts-playground-example.num.specs.json +++ b/internal/interpreter/testdata/script-tests/ovedrafts-playground-example.num.specs.json @@ -5,9 +5,7 @@ "balances": { "users:2345:credit": {}, "users:2345:main": { - "USD/2": { - "": 8000 - } + "USD/2": 8000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-in-send-all-when-noop.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-in-send-all-when-noop.num.specs.json index 29a714ca..54c7c930 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-in-send-all-when-noop.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-in-send-all-when-noop.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "USD/2": { - "": 1 - } + "USD/2": 1 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-in-send-all.num.specs.json index af092f35..66ff0af4 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-in-send-all.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "USD/2": { - "": 1000 - } + "USD/2": 1000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-not-enough-funds.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-not-enough-funds.num.specs.json index 8dfa59ca..2889d33d 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-not-enough-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-not-enough-funds.num.specs.json @@ -5,9 +5,7 @@ "balances": { "users:2345:credit": {}, "users:2345:main": { - "USD/2": { - "": 8000 - } + "USD/2": 8000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json index bb574ee8..d8f1b287 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance-in-send-all.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "s": { - "COIN": { - "": -1 - } + "COIN": -1 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance.num.specs.json index 9944527a..8b2e8d48 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-when-negative-balance.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "s": { - "COIN": { - "": 11 - } + "COIN": 11 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-when-negative-ovedraft-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-when-negative-ovedraft-in-send-all.num.specs.json index 24a1304d..8c9c6c6f 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-when-negative-ovedraft-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-when-negative-ovedraft-in-send-all.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "s": { - "COIN": { - "": 1 - } + "COIN": 1 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/overdraft-when-not-enough-funds.num.specs.json b/internal/interpreter/testdata/script-tests/overdraft-when-not-enough-funds.num.specs.json index e5e70bfc..540a5447 100644 --- a/internal/interpreter/testdata/script-tests/overdraft-when-not-enough-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/overdraft-when-not-enough-funds.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:1234": { - "COIN": { - "": 1 - } + "COIN": 1 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/remaining-kept-in-send-all-inorder.num.specs.json b/internal/interpreter/testdata/script-tests/remaining-kept-in-send-all-inorder.num.specs.json index ab39872e..b77df05a 100644 --- a/internal/interpreter/testdata/script-tests/remaining-kept-in-send-all-inorder.num.specs.json +++ b/internal/interpreter/testdata/script-tests/remaining-kept-in-send-all-inorder.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 1000 - } + "COIN": 1000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/remaining-none-in-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/remaining-none-in-send-all.num.specs.json index 176c4bba..4b60aec5 100644 --- a/internal/interpreter/testdata/script-tests/remaining-none-in-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/remaining-none-in-send-all.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 10 - } + "COIN": 10 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__multi-postings.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__multi-postings.num.specs.json index f9fb00d0..1882701e 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__multi-postings.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__multi-postings.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD": { - "": 20 - } + "USD": 20 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-a-different-asset.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-a-different-asset.num.specs.json index 75399d0b..8adbde0f 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-a-different-asset.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-a-different-asset.num.specs.json @@ -4,12 +4,8 @@ "it": "-", "balances": { "alice": { - "COIN": { - "": 100 - }, - "USD": { - "": 20 - } + "COIN": 100, + "USD": 20 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-all-negative-balance.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-all-negative-balance.num.specs.json index b3f04bf3..6d9c6bd9 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-all-negative-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-all-negative-balance.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD": { - "": -100 - } + "USD": -100 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-all.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-all.num.specs.json index cc4678d3..15a5cc24 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-all.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD": { - "": 20 - } + "USD": 20 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-causes-failure.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-causes-failure.num.specs.json index 115ebac7..b714008c 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-causes-failure.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-causes-failure.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD/2": { - "": 30 - } + "USD/2": 30 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__save-more-than-balance.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__save-more-than-balance.num.specs.json index cc4678d3..15a5cc24 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__save-more-than-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__save-more-than-balance.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD": { - "": 20 - } + "USD": 20 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__simple.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__simple.num.specs.json index 5fb02e9d..89dcf05e 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__simple.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__simple.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD": { - "": 20 - } + "USD": 20 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__with-asset-var.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__with-asset-var.num.specs.json index c03edea1..035c7709 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__with-asset-var.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__with-asset-var.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD": { - "": 20 - } + "USD": 20 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/save/save-from-account__with-monetary-var.num.specs.json b/internal/interpreter/testdata/script-tests/save/save-from-account__with-monetary-var.num.specs.json index 1ffab7ec..697b1729 100644 --- a/internal/interpreter/testdata/script-tests/save/save-from-account__with-monetary-var.num.specs.json +++ b/internal/interpreter/testdata/script-tests/save/save-from-account__with-monetary-var.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD": { - "": 20 - } + "USD": 20 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/send-all-destinatio-allot-complex.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-destinatio-allot-complex.num.specs.json index 9ffcf68d..a44dc64e 100644 --- a/internal/interpreter/testdata/script-tests/send-all-destinatio-allot-complex.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-destinatio-allot-complex.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "users:001": { - "USD/2": { - "": 15 - } + "USD/2": 15 }, "users:002": { - "USD/2": { - "": 15 - } + "USD/2": 15 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-destinatio-allot.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-destinatio-allot.num.specs.json index 51afa74d..3db9ef2f 100644 --- a/internal/interpreter/testdata/script-tests/send-all-destinatio-allot.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-destinatio-allot.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:001": { - "USD/2": { - "": 30 - } + "USD/2": 30 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-many-max-in-dest.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-many-max-in-dest.num.specs.json index 1c36b28c..b7be78cb 100644 --- a/internal/interpreter/testdata/script-tests/send-all-many-max-in-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-many-max-in-dest.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "USD/2": { - "": 15 - } + "USD/2": 15 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-multi.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-multi.num.specs.json index d8c1e628..b30bee30 100644 --- a/internal/interpreter/testdata/script-tests/send-all-multi.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-multi.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "users:001:credit": { - "USD/2": { - "": 22 - } + "USD/2": 22 }, "users:001:wallet": { - "USD/2": { - "": 19 - } + "USD/2": 19 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-variable.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-variable.num.specs.json index 856ce239..4ab2bdf6 100644 --- a/internal/interpreter/testdata/script-tests/send-all-variable.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-variable.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:001": { - "USD/2": { - "": 17 - } + "USD/2": 17 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/send-all-when-negative-with-overdraft.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-when-negative-with-overdraft.num.specs.json index a59dfcba..86428b45 100644 --- a/internal/interpreter/testdata/script-tests/send-all-when-negative-with-overdraft.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-when-negative-with-overdraft.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:001": { - "USD/2": { - "": -100 - } + "USD/2": -100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-all-when-negative.num.specs.json b/internal/interpreter/testdata/script-tests/send-all-when-negative.num.specs.json index f2d21c6c..298ea1ac 100644 --- a/internal/interpreter/testdata/script-tests/send-all-when-negative.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all-when-negative.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:001": { - "USD/2": { - "": -100 - } + "USD/2": -100 } }, "expect.postings": [] diff --git a/internal/interpreter/testdata/script-tests/send-all.num.specs.json b/internal/interpreter/testdata/script-tests/send-all.num.specs.json index 62da332c..72b5d1d7 100644 --- a/internal/interpreter/testdata/script-tests/send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-all.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:001": { - "USD/2": { - "": 17 - } + "USD/2": 17 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-allt-max-in-dest.num.specs.json b/internal/interpreter/testdata/script-tests/send-allt-max-in-dest.num.specs.json index eced1e2e..c7ac32a1 100644 --- a/internal/interpreter/testdata/script-tests/send-allt-max-in-dest.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-allt-max-in-dest.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "USD/2": { - "": 100 - } + "USD/2": 100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-allt-max-in-src.num.specs.json b/internal/interpreter/testdata/script-tests/send-allt-max-in-src.num.specs.json index 197986ab..50090e85 100644 --- a/internal/interpreter/testdata/script-tests/send-allt-max-in-src.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-allt-max-in-src.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "src1": { - "USD/2": { - "": 100 - } + "USD/2": 100 }, "src2": { - "USD/2": { - "": 200 - } + "USD/2": 200 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send-allt-max-when-no-amount.num.specs.json b/internal/interpreter/testdata/script-tests/send-allt-max-when-no-amount.num.specs.json index e908dd15..50a034c2 100644 --- a/internal/interpreter/testdata/script-tests/send-allt-max-when-no-amount.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-allt-max-when-no-amount.num.specs.json @@ -5,9 +5,7 @@ "balances": { "src": {}, "src1": { - "USD/2": { - "": 0 - } + "USD/2": 0 } }, "expect.postings": [] diff --git a/internal/interpreter/testdata/script-tests/send-when-negative-balance.num.specs.json b/internal/interpreter/testdata/script-tests/send-when-negative-balance.num.specs.json index a9cc74a9..847f6882 100644 --- a/internal/interpreter/testdata/script-tests/send-when-negative-balance.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send-when-negative-balance.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "s": { - "COIN": { - "": -5 - } + "COIN": -5 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/send.num.specs.json b/internal/interpreter/testdata/script-tests/send.num.specs.json index 38f4eb15..c58a4fc7 100644 --- a/internal/interpreter/testdata/script-tests/send.num.specs.json +++ b/internal/interpreter/testdata/script-tests/send.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "EUR/2": { - "": 100 - } + "EUR/2": 100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/source-allotment-invalid-amt.num.specs.json b/internal/interpreter/testdata/script-tests/source-allotment-invalid-amt.num.specs.json index 2173b37f..15efe3cb 100644 --- a/internal/interpreter/testdata/script-tests/source-allotment-invalid-amt.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source-allotment-invalid-amt.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "a": { - "COIN": { - "": 1 - } + "COIN": 1 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/source-allotment.num.specs.json b/internal/interpreter/testdata/script-tests/source-allotment.num.specs.json index fe26f154..d8c2cd01 100644 --- a/internal/interpreter/testdata/script-tests/source-allotment.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source-allotment.num.specs.json @@ -4,19 +4,13 @@ "it": "-", "balances": { "a": { - "COIN": { - "": 100 - } + "COIN": 100 }, "b": { - "COIN": { - "": 100 - } + "COIN": 100 }, "c": { - "COIN": { - "": 100 - } + "COIN": 100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/source-complex.num.specs.json b/internal/interpreter/testdata/script-tests/source-complex.num.specs.json index c3964d96..02642dca 100644 --- a/internal/interpreter/testdata/script-tests/source-complex.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source-complex.num.specs.json @@ -4,24 +4,16 @@ "it": "-", "balances": { "a": { - "COIN": { - "": 1000 - } + "COIN": 1000 }, "b": { - "COIN": { - "": 40 - } + "COIN": 40 }, "c": { - "COIN": { - "": 1000 - } + "COIN": 1000 }, "d": { - "COIN": { - "": 1000 - } + "COIN": 1000 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/source-overlapping.num.specs.json b/internal/interpreter/testdata/script-tests/source-overlapping.num.specs.json index 858f6e46..6e4830d1 100644 --- a/internal/interpreter/testdata/script-tests/source-overlapping.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source-overlapping.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "a": { - "COIN": { - "": 99 - } + "COIN": 99 }, "b": { - "COIN": { - "": 3 - } + "COIN": 3 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/source.num.specs.json b/internal/interpreter/testdata/script-tests/source.num.specs.json index 20f45613..011726b8 100644 --- a/internal/interpreter/testdata/script-tests/source.num.specs.json +++ b/internal/interpreter/testdata/script-tests/source.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "payments:001": { - "GEM": { - "": 12 - } + "GEM": 12 }, "users:001": { - "GEM": { - "": 3 - } + "GEM": 3 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/track-balances-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/track-balances-send-all.num.specs.json index 6748a374..388bed24 100644 --- a/internal/interpreter/testdata/script-tests/track-balances-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/track-balances-send-all.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 42 - } + "COIN": 42 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/track-balances.num.specs.json b/internal/interpreter/testdata/script-tests/track-balances.num.specs.json index fb3bcb22..46bd9309 100644 --- a/internal/interpreter/testdata/script-tests/track-balances.num.specs.json +++ b/internal/interpreter/testdata/script-tests/track-balances.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "a": { - "COIN": { - "": 50 - } + "COIN": 50 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/track-balances2.num.specs.json b/internal/interpreter/testdata/script-tests/track-balances2.num.specs.json index a9b3cb13..fe4851a6 100644 --- a/internal/interpreter/testdata/script-tests/track-balances2.num.specs.json +++ b/internal/interpreter/testdata/script-tests/track-balances2.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "a": { - "COIN": { - "": 60 - } + "COIN": 60 } }, "expect.error.missingFunds": true diff --git a/internal/interpreter/testdata/script-tests/track-balances3.num.specs.json b/internal/interpreter/testdata/script-tests/track-balances3.num.specs.json index 40c0739a..ec9ec158 100644 --- a/internal/interpreter/testdata/script-tests/track-balances3.num.specs.json +++ b/internal/interpreter/testdata/script-tests/track-balances3.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "foo": { - "COIN": { - "": 2000 - } + "COIN": 2000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json b/internal/interpreter/testdata/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json index 3dce9391..2429a124 100644 --- a/internal/interpreter/testdata/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json +++ b/internal/interpreter/testdata/script-tests/unbounded-overdraft-when-not-enough-funds.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:2345:main": { - "USD/2": { - "": 8000 - } + "USD/2": 8000 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/update-balances.num.specs.json b/internal/interpreter/testdata/script-tests/update-balances.num.specs.json index 5a07dc72..d02b3426 100644 --- a/internal/interpreter/testdata/script-tests/update-balances.num.specs.json +++ b/internal/interpreter/testdata/script-tests/update-balances.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "alice": { - "USD": { - "": 100 - } + "USD": 100 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/use-balance-twice.num.specs.json b/internal/interpreter/testdata/script-tests/use-balance-twice.num.specs.json index e9af3a54..8481ec76 100644 --- a/internal/interpreter/testdata/script-tests/use-balance-twice.num.specs.json +++ b/internal/interpreter/testdata/script-tests/use-balance-twice.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "src": { - "COIN": { - "": 50 - } + "COIN": 50 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/use-different-assets-with-same-source-account.num.specs.json b/internal/interpreter/testdata/script-tests/use-different-assets-with-same-source-account.num.specs.json index 56249440..c14650e6 100644 --- a/internal/interpreter/testdata/script-tests/use-different-assets-with-same-source-account.num.specs.json +++ b/internal/interpreter/testdata/script-tests/use-different-assets-with-same-source-account.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "account1": { - "A": { - "": 100 - } + "A": 100 }, "account2": { - "B": { - "": 100 - } + "B": 100 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/variable-asset.num.specs.json b/internal/interpreter/testdata/script-tests/variable-asset.num.specs.json index d04831a1..36028aa3 100644 --- a/internal/interpreter/testdata/script-tests/variable-asset.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-asset.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "alice": { - "USD": { - "": 10 - } + "USD": 10 }, "bob": { - "USD": { - "": 10 - } + "USD": 10 }, "swap": {} }, diff --git a/internal/interpreter/testdata/script-tests/variable-balance__1.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__1.num.specs.json index fa51de70..835adb53 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__1.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__1.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "A": { - "USD/2": { - "": 40 - } + "USD/2": 40 }, "C": { - "USD/2": { - "": 90 - } + "USD/2": 90 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/variable-balance__2.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__2.num.specs.json index f35d2ff4..99695ecb 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__2.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__2.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "A": { - "USD/2": { - "": 400 - } + "USD/2": 400 }, "C": { - "USD/2": { - "": 90 - } + "USD/2": 90 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/variable-balance__3.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__3.num.specs.json index 26d1bb6e..ec5a6f04 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__3.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__3.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "A": { - "USD/2": { - "": 40 - } + "USD/2": 40 }, "C": { - "USD/2": { - "": 90 - } + "USD/2": 90 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/variable-balance__4.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__4.num.specs.json index 1e35cb05..308be728 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__4.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__4.num.specs.json @@ -4,14 +4,10 @@ "it": "-", "balances": { "A": { - "USD/2": { - "": 400 - } + "USD/2": 400 }, "C": { - "USD/2": { - "": 90 - } + "USD/2": 90 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/variable-balance__5.num.specs.json b/internal/interpreter/testdata/script-tests/variable-balance__5.num.specs.json index a84df651..9170724f 100644 --- a/internal/interpreter/testdata/script-tests/variable-balance__5.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variable-balance__5.num.specs.json @@ -4,29 +4,19 @@ "it": "-", "balances": { "a": { - "COIN": { - "": 1000 - } + "COIN": 1000 }, "b": { - "COIN": { - "": 40 - } + "COIN": 40 }, "c": { - "COIN": { - "": 1000 - } + "COIN": 1000 }, "d": { - "COIN": { - "": 1000 - } + "COIN": 1000 }, "maxAcc": { - "COIN": { - "": 120 - } + "COIN": 120 } }, "expect.postings": [ diff --git a/internal/interpreter/testdata/script-tests/variables-json.num.specs.json b/internal/interpreter/testdata/script-tests/variables-json.num.specs.json index d55ccf83..54900447 100644 --- a/internal/interpreter/testdata/script-tests/variables-json.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variables-json.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:001": { - "EUR/2": { - "": 1000 - } + "EUR/2": 1000 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/variables.num.specs.json b/internal/interpreter/testdata/script-tests/variables.num.specs.json index 7a41c415..6705d59b 100644 --- a/internal/interpreter/testdata/script-tests/variables.num.specs.json +++ b/internal/interpreter/testdata/script-tests/variables.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "users:001": { - "EUR/2": { - "": 1000 - } + "EUR/2": 1000 } }, "variables": { diff --git a/internal/interpreter/testdata/script-tests/world-source.num.specs.json b/internal/interpreter/testdata/script-tests/world-source.num.specs.json index 504195e0..551e59f9 100644 --- a/internal/interpreter/testdata/script-tests/world-source.num.specs.json +++ b/internal/interpreter/testdata/script-tests/world-source.num.specs.json @@ -4,9 +4,7 @@ "it": "-", "balances": { "a": { - "GEM": { - "": 1 - } + "GEM": 1 } }, "expect.postings": [ diff --git a/internal/specs_format/__snapshots__/runner_test.snap b/internal/specs_format/__snapshots__/runner_test.snap index c36e6830..f2ac9815 100755 --- a/internal/specs_format/__snapshots__/runner_test.snap +++ b/internal/specs_format/__snapshots__/runner_test.snap @@ -76,16 +76,12 @@ GOT:  {  "alice": { - "USD/2": { -- "": -100 -+ "": 9899 - } +- "USD/2": -100 ++ "USD/2": 9899  },  "dest": { - "USD/2": { -- "": 1 -+ "": 100 - } +- "USD/2": 1 ++ "USD/2": 100  }  } @@ -123,7 +119,7 @@ Error: example.num.specs.json  Error: example.num.specs.json -json: cannot unmarshal number into Go struct field Specs.balances of type map[string]map[string]*big.Int +json: cannot unmarshal number into Go struct field Specs.balances of type map[string]map[string]json.RawMessage --- diff --git a/internal/specs_format/parse_test.go b/internal/specs_format/parse_test.go index 951ce929..ce7f2e14 100644 --- a/internal/specs_format/parse_test.go +++ b/internal/specs_format/parse_test.go @@ -15,7 +15,7 @@ func TestParseSpecs(t *testing.T) { raw := ` { "balances": { - "alice": { "EUR": { "": 200 } } + "alice": { "EUR": 200 } }, "variables": { "amt": "200" @@ -24,7 +24,7 @@ func TestParseSpecs(t *testing.T) { { "it": "d1", "balances": { - "bob": { "EUR": { "": 42 } } + "bob": { "EUR": 42 } }, "expect.postings": [ { diff --git a/internal/specs_format/run_test.go b/internal/specs_format/run_test.go index 311fc0e7..0bb505e5 100644 --- a/internal/specs_format/run_test.go +++ b/internal/specs_format/run_test.go @@ -29,7 +29,7 @@ func TestRunSpecsSimple(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": { "": 9999 } } }, + "balances": { "src": { "USD": 9999 } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] @@ -81,13 +81,13 @@ func TestRunSpecsSimple(t *testing.T) { func TestRunSpecsMergeOuter(t *testing.T) { j := `{ "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": { "": 10 } } }, + "balances": { "src": { "USD": 10 } }, "testCases": [ { "variables": { "amount": "1" }, "balances": { - "src": { "EUR": { "": 2 } }, - "dest": { "USD": { "": 1 } } + "src": { "EUR": 2 }, + "dest": { "USD": 1 } }, "it": "t1", "expect.postings": [ @@ -147,7 +147,7 @@ func TestRunWithMissingBalance(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": { "": 1 } } }, + "balances": { "src": { "USD": 1 } }, "expect.error.missingFunds": false, "expect.postings": null } @@ -200,7 +200,7 @@ func TestRunWithMissingBalanceWhenExpectedPostings(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": { "": 1 } } }, + "balances": { "src": { "USD": 1 } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 1 } ] @@ -254,7 +254,7 @@ func TestNullPostingsIsNoop(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": { "": 1 } } }, + "balances": { "src": { "USD": 1 } }, "expect.postings": null } ] @@ -378,7 +378,7 @@ func TestFocus(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "10" }, - "balances": { "src": { "USD": { "": 9999 } } }, + "balances": { "src": { "USD": 9999 } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] @@ -387,7 +387,7 @@ func TestFocus(t *testing.T) { "it": "t2", "focus": true, "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": { "": 9999 } } }, + "balances": { "src": { "USD": 9999 } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] diff --git a/internal/specs_format/runner_test.go b/internal/specs_format/runner_test.go index f13d43dc..cb543b02 100644 --- a/internal/specs_format/runner_test.go +++ b/internal/specs_format/runner_test.go @@ -90,11 +90,11 @@ func TestComplexAssertions(t *testing.T) { { "it": "send when there are enough funds", "balances": { - "alice": { "USD/2": { "": 9999 } } + "alice": { "USD/2": 9999 } }, "expect.endBalances": { - "alice": { "USD/2": { "": -100 } }, - "dest": { "USD/2": { "": 1 } } + "alice": { "USD/2": -100 }, + "dest": { "USD/2": 1 } }, "expect.movements": { "alice": { @@ -106,7 +106,7 @@ func TestComplexAssertions(t *testing.T) { { "it": "tpassing", "balances": { - "alice": { "USD/2": { "": 0 } } + "alice": { "USD/2": 0 } }, "expect.error.missingFunds": true } @@ -161,7 +161,7 @@ func TestSchemaErrSpecs(t *testing.T) { SpecsPath: "example.num.specs.json", NumscriptContent: "", SpecsFileContent: []byte(` - { "balances": { "": 42 } } + { "balances": 42 } `), }, }) @@ -219,7 +219,7 @@ func TestFocusUi(t *testing.T) { { "it": "t1", "variables": { "source": "src", "amount": "10" }, - "balances": { "src": { "USD": { "": 9999 } } }, + "balances": { "src": { "USD": 9999 } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] @@ -228,7 +228,7 @@ func TestFocusUi(t *testing.T) { "it": "t2", "focus": true, "variables": { "source": "src", "amount": "42" }, - "balances": { "src": { "USD": { "": 9999 } } }, + "balances": { "src": { "USD": 9999 } }, "expect.postings": [ { "source": "src", "destination": "dest", "asset": "USD", "amount": 42 } ] diff --git a/specs.schema.json b/specs.schema.json index a3cd1680..1a55424b 100644 --- a/specs.schema.json +++ b/specs.schema.json @@ -92,13 +92,31 @@ "additionalProperties": false, "patternProperties": { "^([A-Z]+(/[0-9]+)?)$": { - "type": "number" + "oneOf": [ + { "type": "number", "description": "Uncolored shorthand" }, + { "$ref": "#/definitions/BalanceEntry" }, + { "type": "array", "items": { "$ref": "#/definitions/BalanceEntry" } } + ] } } } } }, + "BalanceEntry": { + "type": "object", + "additionalProperties": false, + "required": ["amount"], + "properties": { + "color": { + "type": "string", + "pattern": "^[A-Z]*$", + "description": "Color bucket. Omit (or set to \"\") for the uncolored bucket." + }, + "amount": { "type": "number" } + } + }, + "VariablesMap": { "type": "object", "description": "Map of variable name to variable stringified value", From 36653bce22871a116bdea091fd52d966232699fd Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 6 Jun 2026 08:53:17 +0200 Subject: [PATCH 08/12] feat: surface Color in PrettyPrintPostings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When color is part of the Posting contract but absent from the pretty printer, two postings that differ only by color render identically — the spec failure output (and `numscript run -o pretty`) hide the only distinguishing field. The Color column is added conditionally: present as soon as any posting in the batch carries a non-empty color; hidden otherwise so the uncolored output is byte-identical to the previous behaviour. --- internal/interpreter/interpreter.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/internal/interpreter/interpreter.go b/internal/interpreter/interpreter.go index 0b7f7c9c..c3f43c82 100644 --- a/internal/interpreter/interpreter.go +++ b/internal/interpreter/interpreter.go @@ -1209,12 +1209,33 @@ func CalculateSafeWithdraw( } func PrettyPrintPostings(postings []Posting) string { + hasColor := false + for _, posting := range postings { + if posting.Color != "" { + hasColor = true + break + } + } + + if !hasColor { + var rows [][]string + for _, posting := range postings { + row := []string{posting.Source, posting.Destination, posting.Asset, posting.Amount.String()} + rows = append(rows, row) + } + return utils.CsvPretty([]string{"Source", "Destination", "Asset", "Amount"}, rows, false) + } + var rows [][]string for _, posting := range postings { - row := []string{posting.Source, posting.Destination, posting.Asset, posting.Amount.String()} + color := posting.Color + if color == "" { + color = "-" + } + row := []string{posting.Source, posting.Destination, posting.Asset, color, posting.Amount.String()} rows = append(rows, row) } - return utils.CsvPretty([]string{"Source", "Destination", "Asset", "Amount"}, rows, false) + return utils.CsvPretty([]string{"Source", "Destination", "Asset", "Color", "Amount"}, rows, false) } func PrettyPrintMeta(meta Metadata) string { From fbac745a4b2a38d9ac2061c55d847973e91c0eaa Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 6 Jun 2026 08:53:23 +0200 Subject: [PATCH 09/12] feat(mcp): align evaluate tool with the new balances shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseBalancesJson now re-marshals the decoded MCP payload through Balances.UnmarshalJSON, so the tool accepts the same three forms documented elsewhere (bare number / value-object / array). The decoded amount path goes via *big.Int directly rather than the previous float64 → big.Int truncation. The tool description and example block are rewritten to match. --- internal/mcp_impl/handlers.go | 72 ++++++++++++++++------------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/internal/mcp_impl/handlers.go b/internal/mcp_impl/handlers.go index 61652644..be5f1c21 100644 --- a/internal/mcp_impl/handlers.go +++ b/internal/mcp_impl/handlers.go @@ -2,8 +2,8 @@ package mcp_impl import ( "context" + "encoding/json" "fmt" - "math/big" "strings" "github.com/formancehq/numscript/internal/analysis" @@ -13,45 +13,28 @@ import ( "github.com/mark3labs/mcp-go/server" ) +// parseBalancesJson routes the caller-supplied balances payload through the +// canonical interpreter.Balances unmarshaller, so the MCP `evaluate` tool +// accepts exactly the same shapes documented elsewhere — bare number for the +// uncolored shorthand, single value-object, or array of value-objects. +// +// The MCP framework hands us an already-decoded `any`; re-marshalling round +// trips through JSON, but big-integer amounts stay safe because the +// re-encoded payload feeds into Balances.UnmarshalJSON which decodes each +// amount directly into a *big.Int. func parseBalancesJson(balancesRaw any) (interpreter.Balances, *mcp.CallToolResult) { - balances, ok := balancesRaw.(map[string]any) - if !ok { - return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected an object as balances, got: <%#v>", balancesRaw)) + if balancesRaw == nil { + return interpreter.Balances{}, nil } - - iBalances := interpreter.Balances{} - for account, assetsRaw := range balances { - if iBalances[account] == nil { - iBalances[account] = interpreter.AccountBalance{} - } - - assets, ok := assetsRaw.(map[string]any) - if !ok { - return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected nested object for account %v", account)) - } - - for asset, perAssetRaw := range assets { - if iBalances[account][asset] == nil { - iBalances[account][asset] = interpreter.ColorBalance{} - } - - // Expected shape: { "USD/2": { "": 100, "RED": 50 } }. - // Color "" is the uncolored bucket. - colorMap, ok := perAssetRaw.(map[string]any) - if !ok { - return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected {color: amount} object for %s/%s, got: <%#v>", account, asset, perAssetRaw)) - } - for color, amountRaw := range colorMap { - amount, ok := amountRaw.(float64) - if !ok { - return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Expected float for amount: %v", amountRaw)) - } - n, _ := big.NewFloat(amount).Int(new(big.Int)) - iBalances[account][asset][color] = n - } - } + encoded, err := json.Marshal(balancesRaw) + if err != nil { + return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Could not re-encode balances: %v", err)) } - return iBalances, nil + var balances interpreter.Balances + if err := json.Unmarshal(encoded, &balances); err != nil { + return interpreter.Balances{}, mcp.NewToolResultError(fmt.Sprintf("Invalid balances payload: %v", err)) + } + return balances, nil } func parseVarsJson(varsRaw any) (map[string]string, *mcp.CallToolResult) { @@ -86,8 +69,19 @@ func addEvalTool(s *server.MCPServer) { ), mcp.WithObject("balances", mcp.Required(), - mcp.Description(`The accounts' balances. A nested map from the account name, to the asset, to its integer amount. - For example: { "alice": { "USD/2": 100, "EUR/2": -42 }, "bob": { "BTC": 1 } } + mcp.Description(`The accounts' balances. A nested map from the account name, to the asset, to the held amount. + + Each per-asset entry accepts three forms: + - a bare integer for the uncolored bucket (shorthand) + - a single value-object: { "amount": N } or { "color": "RED", "amount": N } + - an array of value-objects when several colors coexist on the same asset + + Examples: + { "alice": { "USD/2": 100, "EUR/2": -42 }, "bob": { "BTC": 1 } } + { "alice": { "USD/2": [{ "amount": 100 }, { "color": "RED", "amount": 50 }] } } + + Color is a first-class dimension on the emitted postings (see Posting.color); + the empty/missing color is its own bucket, distinct from any non-empty one. `), ), mcp.WithObject("vars", From f8b9593836cdb0340a2b86569376abbc2e3bba4b Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 6 Jun 2026 08:53:28 +0200 Subject: [PATCH 10/12] chore: add migration script for legacy balances shape Existing downstream specs files using {color: amount} maps can be upgraded in place with `scripts/migrate-specs-to-color.sh path/...`. The jq rules mirror the canonical write form: uncolored-only collapses to a bare number, single color emits one value-object, multi-color emits an array sorted by color. --- scripts/migrate-specs-to-color.sh | 61 +++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100755 scripts/migrate-specs-to-color.sh diff --git a/scripts/migrate-specs-to-color.sh b/scripts/migrate-specs-to-color.sh new file mode 100755 index 00000000..bcc6f40e --- /dev/null +++ b/scripts/migrate-specs-to-color.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Migrate .num.specs.json balance entries from the legacy nested shape +# ({"": N, "COLOR": M}) to the value-object shape introduced when `color` +# became a first-class Posting field. +# +# Output rules per (account, asset) entry: +# • uncolored-only → bare number "USD/2": 100 +# • single colored entry → value-object "USD/2": { "color": "RED", "amount": 50 } +# • mixed / multi-color → array of value-objects, sorted by color +# +# Usage: +# scripts/migrate-specs-to-color.sh path/to/file.num.specs.json [more files...] +# scripts/migrate-specs-to-color.sh $(find . -name '*.num.specs.json') +set -euo pipefail + +if [[ $# -eq 0 ]]; then + echo "usage: $0 file.num.specs.json [...]" >&2 + exit 2 +fi + +read -r -d '' JQ_FILTER <<'JQ' || true +def to_value_object: + if type != "object" then . + elif (keys_unsorted | length) == 0 then . + elif (keys_unsorted | length) == 1 and keys_unsorted[0] == "" then + .[""] + elif (keys_unsorted | length) == 1 then + {color: keys_unsorted[0], amount: .[keys_unsorted[0]]} + else + [ (to_entries | sort_by(.key))[] | + if .key == "" then {amount: .value} + else {color: .key, amount: .value} + end ] + end; + +def transform_balances: + if type != "object" then . + else + with_entries(.value |= ( + if type != "object" then . + else with_entries(.value |= to_value_object) + end + )) + end; + +(if has("balances") then .balances |= transform_balances else . end) | +(if has("testCases") then + .testCases |= map( + (if has("balances") then .balances |= transform_balances else . end) | + (if has("expect.endBalances") then ."expect.endBalances" |= transform_balances else . end) | + (if has("expect.endBalances.include") then ."expect.endBalances.include" |= transform_balances else . end) + ) +else . end) +JQ + +for f in "$@"; do + tmp="$(mktemp)" + jq --indent 2 "$JQ_FILTER" "$f" > "$tmp" + # Preserve trailing newline behaviour of jq output. + mv "$tmp" "$f" +done From 9a568a6aec94e1079cd6c40605a96ce21988708d Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 6 Jun 2026 09:26:13 +0200 Subject: [PATCH 11/12] feat: omit empty Color from Posting JSON output `"color": ""` in every uncolored posting is noise: an absent color field carries the same meaning as the empty-string bucket, so emitting it leaks an internal convention into every JSON payload. With omitempty, uncolored postings (the dominant case) serialize as they did before color became first-class. Round-trip stays correct because Go's zero-value for the missing field is the empty string, which is exactly the uncolored-bucket key on the decode side. --- internal/interpreter/color_semantics_test.go | 17 +++++++++++------ internal/interpreter/interpreter.go | 2 +- .../specs_format/__snapshots__/runner_test.snap | 3 +-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/internal/interpreter/color_semantics_test.go b/internal/interpreter/color_semantics_test.go index 91486160..2c232f1a 100644 --- a/internal/interpreter/color_semantics_test.go +++ b/internal/interpreter/color_semantics_test.go @@ -337,10 +337,10 @@ func TestPostingJSONRoundtripPreservesColor(t *testing.T) { require.Equal(t, postings[0].Color, decoded.Color) } -// Color is always present in JSON output, even when empty, so downstream -// consumers can rely on its presence to distinguish "uncolored" from -// "missing field". -func TestPostingJSONAlwaysIncludesColor(t *testing.T) { +// Uncolored postings omit the color field — an absent color is the +// uncolored bucket, identical in meaning to color == "". Keeping the JSON +// noise-free for the dominant non-color case. +func TestPostingJSONOmitsEmptyColor(t *testing.T) { t.Parallel() p := machine.Posting{ @@ -351,8 +351,13 @@ func TestPostingJSONAlwaysIncludesColor(t *testing.T) { } encoded, err := json.Marshal(p) require.NoError(t, err) - require.Contains(t, string(encoded), `"color":""`, - "uncolored postings must still expose the color field, got: %s", string(encoded)) + require.NotContains(t, string(encoded), `"color"`, + "uncolored postings must not emit a color field, got: %s", string(encoded)) + + // Round-trip: an absent color field decodes back to the uncolored bucket. + var decoded machine.Posting + require.NoError(t, json.Unmarshal(encoded, &decoded)) + require.Equal(t, "", decoded.Color) } // Allocation-style send: one source feeding multiple destinations under a diff --git a/internal/interpreter/interpreter.go b/internal/interpreter/interpreter.go index c3f43c82..567a8074 100644 --- a/internal/interpreter/interpreter.go +++ b/internal/interpreter/interpreter.go @@ -119,7 +119,7 @@ type Posting struct { Destination string `json:"destination"` Amount *big.Int `json:"amount"` Asset string `json:"asset"` - Color string `json:"color"` + Color string `json:"color,omitempty"` } type ExecutionResult struct { diff --git a/internal/specs_format/__snapshots__/runner_test.snap b/internal/specs_format/__snapshots__/runner_test.snap index f2ac9815..2836fed4 100755 --- a/internal/specs_format/__snapshots__/runner_test.snap +++ b/internal/specs_format/__snapshots__/runner_test.snap @@ -33,8 +33,7 @@ GOT: + "source": "world",  "destination": "dest",  "amount": 100, - "asset": "USD/2", - "color": "" + "asset": "USD/2"  }  ] From 649546a97fe3960d31ce0b67452032a662157758 Mon Sep 17 00:00:00 2001 From: Geoffrey Ragot Date: Sat, 6 Jun 2026 09:27:42 +0200 Subject: [PATCH 12/12] chore(fixtures): drop leftover empty color from expect.postings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixtures that exercise the experimental color split kept the `"color": ""` entry on uncolored postings — a leftover from the first migration pass when `COIN_BLUE` was split into `asset: COIN` + `color: `. With Posting.Color now `omitempty`, the runtime no longer emits the empty field, so the assertion fixtures shouldn't either. --- .../asset-colors/color-inorder-send-all.num.specs.json | 3 +-- .../experimental/asset-colors/color-inorder.num.specs.json | 3 +-- .../no-double-spending-in-colored-send-all.num.specs.json | 3 +-- .../no-double-spending-in-colored-send.num.specs.json | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json index 318d33ca..78452622 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder-send-all.num.specs.json @@ -41,8 +41,7 @@ "source": "src", "destination": "dest", "amount": 100, - "asset": "COIN", - "color": "" + "asset": "COIN" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json index 0e2df535..0470c4f2 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/color-inorder.num.specs.json @@ -41,8 +41,7 @@ "source": "src", "destination": "dest", "amount": 50, - "asset": "COIN", - "color": "" + "asset": "COIN" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json index d9bb50a2..af5fb491 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send-all.num.specs.json @@ -30,8 +30,7 @@ "source": "src", "destination": "dest", "amount": 100, - "asset": "COIN", - "color": "" + "asset": "COIN" } ] } diff --git a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json index 767b3e1b..709e60f4 100644 --- a/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json +++ b/internal/interpreter/testdata/script-tests/experimental/asset-colors/no-double-spending-in-colored-send.num.specs.json @@ -30,8 +30,7 @@ "source": "src", "destination": "dest", "amount": 80, - "asset": "COIN", - "color": "" + "asset": "COIN" } ] }