Skip to content

Commit 4835b76

Browse files
authored
fix: save & overdraft interaction + world var error (#1177)
* fix save & overdraft interaction + properly handle world var error * test fmt tweak * improve error message & add tests for world var behavior
1 parent 1780eb8 commit 4835b76

File tree

2 files changed

+87
-8
lines changed

2 files changed

+87
-8
lines changed

internal/machine/vm/machine.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -434,11 +434,23 @@ func (m *Machine) tick() (bool, error) {
434434
case program.OP_SAVE:
435435
a := pop[machine.AccountAddress](m)
436436
v := m.popValue()
437+
// note: if the balance is not present, it means it's never used as a source so we don't need to save anything
437438
switch v := v.(type) {
438439
case machine.Asset:
439-
m.Balances[a][v] = machine.Zero
440+
if balances, ok := m.Balances[a]; ok {
441+
if balance, ok := balances[v]; ok {
442+
if balance.ToBigInt().Sign() > 0 {
443+
balances[v] = machine.Zero
444+
}
445+
}
446+
}
447+
440448
case machine.Monetary:
441-
m.Balances[a][v.Asset] = m.Balances[a][v.Asset].Sub(v.Amount)
449+
if balances, ok := m.Balances[a]; ok {
450+
if balance, ok := balances[v.Asset]; ok {
451+
balances[v.Asset] = balance.Sub(v.Amount)
452+
}
453+
}
442454
default:
443455
panic(fmt.Errorf("invalid value type: %T", v))
444456
}
@@ -494,13 +506,18 @@ func (m *Machine) ResolveBalances(ctx context.Context, store Store) error {
494506
assignBalanceAsResource[address][string(monetary.Asset)] = resourceIndex
495507
}
496508

509+
m.Balances = make(map[machine.AccountAddress]map[machine.Asset]*machine.MonetaryInt)
510+
497511
// for every account that we need balances of, check if it's there
498512
for addr, neededAssets := range m.Program.NeededBalances {
499513
account, ok := m.getResource(addr)
500514
if !ok {
501515
return errors.New("invalid program (resolve balances: invalid address of account)")
502516
}
503517
accountAddress := (*account).(machine.AccountAddress)
518+
if string(accountAddress) == "world" {
519+
return machine.NewErrInvalidVars("`@world` can only be used as a variable in the experimental interpreter, or if it is never used as a source")
520+
}
504521

505522
// for every asset, register the query
506523
for addr := range neededAssets {
@@ -510,16 +527,11 @@ func (m *Machine) ResolveBalances(ctx context.Context, store Store) error {
510527
}
511528

512529
asset := (*mon).(machine.HasAsset).GetAsset()
513-
if string(accountAddress) == "world" {
514-
m.Balances[accountAddress][asset] = machine.Zero
515-
continue
516-
}
517530

518531
balancesQuery[string(accountAddress)] = append(balancesQuery[string(accountAddress)], string(asset))
519532
}
520533
}
521534

522-
m.Balances = make(map[machine.AccountAddress]map[machine.Asset]*machine.MonetaryInt)
523535
if len(balancesQuery) > 0 {
524536
balances, err := store.GetBalances(ctx, balancesQuery)
525537
if err != nil {

internal/machine/vm/machine_test.go

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func testImpl(t *testing.T, prog *program.Program, expected CaseResult, exec fun
134134

135135
err := exec(m)
136136
if expected.Error != nil {
137-
require.True(t, errors.Is(err, expected.Error), "got wrong error, want: %v, got: %v", expected.Error, err)
137+
require.True(t, errors.Is(err, expected.Error), "got wrong error, want: %[1]v (%[1]T), got: %v", expected.Error, err)
138138
if expected.ErrorContains != "" {
139139
require.ErrorContains(t, err, expected.ErrorContains)
140140
}
@@ -1699,6 +1699,54 @@ func TestVariablesErrors(t *testing.T) {
16991699
test(t, tc)
17001700
}
17011701

1702+
func TestWorldSourceVariable(t *testing.T) {
1703+
tc := NewTestCase()
1704+
tc.compile(t, `vars {
1705+
account $foo
1706+
}
1707+
send [COIN 1] (
1708+
source = $foo
1709+
destination = @bob
1710+
)`)
1711+
tc.vars = map[string]string{
1712+
"foo": "world",
1713+
}
1714+
tc.expected = CaseResult{
1715+
Printed: []machine.Value{},
1716+
Postings: []Posting{},
1717+
Error: &machine.ErrInvalidVars{},
1718+
ErrorContains: "`@world` can only be used as a variable in the experimental interpreter, or if it is never used as a source",
1719+
}
1720+
test(t, tc)
1721+
}
1722+
1723+
func TestWorldNonSourceVariable(t *testing.T) {
1724+
tc := NewTestCase()
1725+
tc.compile(t, `vars {
1726+
account $foo
1727+
}
1728+
send [COIN 1] (
1729+
source = @alice
1730+
destination = $foo
1731+
)`)
1732+
tc.setBalance("alice", "COIN", 1)
1733+
tc.vars = map[string]string{
1734+
"foo": "world",
1735+
}
1736+
tc.expected = CaseResult{
1737+
Printed: []machine.Value{},
1738+
Postings: []Posting{
1739+
{
1740+
Source: "alice",
1741+
Destination: "world",
1742+
Asset: "COIN",
1743+
Amount: machine.NewMonetaryInt(1),
1744+
},
1745+
},
1746+
}
1747+
test(t, tc)
1748+
}
1749+
17021750
func TestSetVarsFromJSON(t *testing.T) {
17031751

17041752
type testCase struct {
@@ -2267,6 +2315,25 @@ func TestSaveFromAccount(t *testing.T) {
22672315
}
22682316
test(t, tc)
22692317
})
2318+
2319+
t.Run("save all and overdraft", func(t *testing.T) {
2320+
script := `
2321+
save [USD *] from @alice
2322+
2323+
send [USD 10] (
2324+
source = @alice allowing overdraft up to [USD 10]
2325+
destination = @world
2326+
)`
2327+
tc := NewTestCase()
2328+
tc.compile(t, script)
2329+
tc.setBalance("alice", "USD", -10)
2330+
tc.expected = CaseResult{
2331+
Printed: []machine.Value{},
2332+
ErrorContains: "insufficient funds",
2333+
Error: &machine.ErrInsufficientFund{},
2334+
}
2335+
test(t, tc)
2336+
})
22702337
}
22712338

22722339
func TestUseDifferentAssetsWithSameSourceAccount(t *testing.T) {

0 commit comments

Comments
 (0)