Files

1279 lines
27 KiB
Go

package interpreter_test
import (
"bytes"
"context"
"encoding/json"
"math/big"
"github.com/hanzo-fi/numscript/internal/flags"
"github.com/hanzo-fi/numscript/internal/interpreter"
"github.com/hanzo-fi/numscript/internal/specs_format"
"testing"
"github.com/hanzo-fi/numscript/internal/parser"
"github.com/stretchr/testify/require"
)
const scriptsFolder = "testdata/script-tests"
func TestScripts(t *testing.T) {
t.Parallel()
rawSpecs, err := specs_format.ReadSpecsFiles([]string{scriptsFolder})
require.Nil(t, err)
var buf bytes.Buffer
buf.WriteByte('\n')
ok := specs_format.RunSpecs(&buf, &buf, rawSpecs)
if !ok {
t.Log(buf.String())
t.Fail()
}
}
type TestCase struct {
source string
program *parser.Program
vars map[string]string
meta interpreter.AccountsMetadata
balances interpreter.Balances
expected CaseResult
}
func NewTestCase() TestCase {
return TestCase{
vars: make(map[string]string),
meta: interpreter.AccountsMetadata{},
balances: nil,
expected: CaseResult{
Error: nil,
},
}
}
// returns a version of the error in which the range is normalized
// to golang's default value
func removeRange(e interpreter.InterpreterError) interpreter.InterpreterError {
switch e := e.(type) {
case interpreter.MissingFundsErr:
e.Range = parser.Range{}
return e
case interpreter.InvalidUnboundedAddressInScalingAddress:
e.Range = parser.Range{}
return e
case interpreter.TypeError:
e.Range = parser.Range{}
return e
case interpreter.InvalidTypeErr:
e.Range = parser.Range{}
return e
case interpreter.NegativeAmountErr:
e.Range = parser.Range{}
return e
default:
return e
}
}
func (c *TestCase) setVarsFromJSON(t *testing.T, str string) {
var jsonVars map[string]string
err := json.Unmarshal([]byte(str), &jsonVars)
require.NoError(t, err)
c.vars = jsonVars
}
func (tc *TestCase) compile(t *testing.T, src string) string {
t.Parallel()
tc.source = src
parsed := parser.Parse(src)
if len(parsed.Errors) != 0 {
t.Errorf("Got parsing errors: %v\n", parsed.Errors)
}
tc.program = &parsed.Value
return src
}
func (c *TestCase) setBalance(account string, asset string, amount int64) {
c.balances = append(c.balances, interpreter.BalanceRow{
Account: account,
Asset: asset,
Amount: big.NewInt(amount),
})
}
func (c *TestCase) setColoredBalance(account string, asset string, color string, amount int64) {
c.balances = append(c.balances, interpreter.BalanceRow{
Account: account,
Asset: asset,
Color: color,
Amount: big.NewInt(amount),
})
}
func test(t *testing.T, testCase TestCase) {
testWithFeatureFlag(t, testCase, "")
}
// A version of test() which tests code under a feature flag
// if the feature flag is the empty string, it behaves as test()
// otherwise, it tests the program under that feature flag and also tests that
// the same script, without the flag, yields the ExperimentalFeature{} error
func testWithFeatureFlag(t *testing.T, testCase TestCase, flagName string) {
prog := testCase.program
require.NotNil(t, prog)
featureFlags := map[string]struct{}{}
if flagName != "" {
featureFlags[flagName] = struct{}{}
_, err := interpreter.RunProgram(
context.Background(),
*prog,
testCase.vars,
interpreter.StaticStore{
testCase.balances,
testCase.meta,
},
nil,
)
require.Equal(t, interpreter.ExperimentalFeature{
FlagName: flagName,
}, removeRange(err))
}
_, err := interpreter.RunProgram(
context.Background(),
*prog,
testCase.vars,
interpreter.StaticStore{
testCase.balances,
testCase.meta,
},
featureFlags,
)
expected := testCase.expected
if expected.Error != nil {
require.Equal(t, removeRange(expected.Error), removeRange(err))
} else {
require.NoError(t, err)
}
}
func TestStaticStore(t *testing.T) {
t.Run("request currencies", func(t *testing.T) {
store := interpreter.StaticStore{
Balances: interpreter.Balances{
{Account: "a", Asset: "USD/2", Amount: big.NewInt(10)},
{Account: "a", Asset: "EUR/2", Amount: big.NewInt(1)},
{Account: "b", Asset: "USD/2", Amount: big.NewInt(10)},
{Account: "b", Asset: "COIN", Amount: big.NewInt(11)},
},
}
q1, _ := store.GetBalances(context.TODO(), interpreter.BalanceQuery{
{Account: "a", Asset: "USD/2"},
})
require.Equal(t, interpreter.Balances{
{Account: "a", Asset: "USD/2", Amount: big.NewInt(10)},
}, q1)
q2, _ := store.GetBalances(context.TODO(), interpreter.BalanceQuery{
{Account: "b", Asset: "USD/2"},
{Account: "b", Asset: "COIN"},
})
require.Equal(t, interpreter.Balances{
{Account: "b", Asset: "USD/2", Amount: big.NewInt(10)},
{Account: "b", Asset: "COIN", Amount: big.NewInt(11)},
}, q2)
})
t.Run("assets catchall", func(t *testing.T) {
store := interpreter.StaticStore{
Balances: interpreter.Balances{
{Account: "a", Asset: "USD", Amount: big.NewInt(1)},
{Account: "a", Asset: "USD/2", Amount: big.NewInt(2)},
{Account: "a", Asset: "USD/3", Amount: big.NewInt(3)},
},
}
balances, err := store.GetBalances(context.Background(), interpreter.BalanceQuery{
{Account: "a", Asset: "USD/*"},
})
require.Nil(t, err)
require.Equal(t, interpreter.Balances{
{Account: "a", Asset: "USD", Amount: big.NewInt(1)},
{Account: "a", Asset: "USD/2", Amount: big.NewInt(2)},
{Account: "a", Asset: "USD/3", Amount: big.NewInt(3)},
}, balances)
})
}
type CaseResult struct {
Postings []interpreter.Posting
TxMetadata map[string]interpreter.Value
AccountMetadata interpreter.AccountsMetadata
Error interpreter.InterpreterError
}
type Posting = interpreter.Posting
func TestBadPortionSyntax(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `vars {
portion $por
}
send [COIN 3] (
source = @world
destination = {
$por to @a
remaining kept
}
)
`)
tc.setVarsFromJSON(t, `{
"por": "not a portion"
}`)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.BadPortionParsingErr{
Source: "not a portion",
Reason: "invalid format",
},
}
test(t, tc)
}
func TestBadAssetInMeta(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `vars {
asset $asset = meta(@acc, "my-asset")
}
send [$asset 42] (
source = @world
destination = @dest
)
`)
tc.meta = interpreter.AccountsMetadata{
{Account: "acc", Key: "my-asset", Value: "Aa"},
}
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.InvalidAsset{
Name: "Aa",
},
}
test(t, tc)
}
// runScopedSend runs a scoped send once, with the scope passed in as a string
// variable, and returns the (possibly nil) error.
func runScopedSend(t *testing.T, scope string) interpreter.InterpreterError {
t.Helper()
parsed := parser.Parse(`
vars { string $scope }
send [COIN 1] (
source = scoped(@a, $scope) allowing unbounded overdraft
destination = @dest
)
`)
require.Empty(t, parsed.Errors)
_, err := interpreter.RunProgram(
context.Background(),
parsed.Value,
map[string]string{"scope": scope},
interpreter.StaticStore{},
map[string]struct{}{
flags.ExperimentalScopedFunction: {},
flags.ExperimentalMidScriptFunctionCall: {},
},
)
return err
}
func TestScopedRejectsInvalidScope(t *testing.T) {
// scopes must match ^[a-z0-9_]*$
invalid := []string{"UPPER", "Mixed", "with-dash", "with:colon", "with space", "with/slash", "with!bang", "with.dot"}
for _, scope := range invalid {
t.Run(scope, func(t *testing.T) {
require.Equal(t, interpreter.InvalidScope{Scope: scope}, runScopedSend(t, scope))
})
}
}
func TestScopedAcceptsValidScope(t *testing.T) {
// lowercase, digits, underscores — and the empty string (means "no scope")
valid := []string{"reserve", "x", "a1", "with_underscore", "123", ""}
for _, scope := range valid {
t.Run("scope="+scope, func(t *testing.T) {
require.Nil(t, runScopedSend(t, scope))
})
}
}
func TestCannotInterpolateScopedAccount(t *testing.T) {
parsed := parser.Parse(`
vars {
account $scoped = scoped(@a, "s")
}
send [COIN 1] (
source = @foo:$scoped allowing unbounded overdraft
destination = @dest
)
`)
require.Empty(t, parsed.Errors)
_, err := interpreter.RunProgram(
context.Background(),
parsed.Value,
nil,
interpreter.StaticStore{},
map[string]struct{}{
flags.ExperimentalScopedFunction: {},
flags.ExperimentalAccountInterpolationFlag: {},
},
)
var scopedErr interpreter.CannotCastScopedAccountToString
require.ErrorAs(t, err, &scopedErr)
require.Equal(t, "a", scopedErr.Account)
require.Equal(t, "s", scopedErr.Scope)
}
func TestCannotStoreScopedAccountInAccountMeta(t *testing.T) {
parsed := parser.Parse(`
set_account_meta(@acc, "beneficiary", scoped(@clients, "premium"))
`)
require.Empty(t, parsed.Errors)
_, err := interpreter.RunProgram(
context.Background(),
parsed.Value,
nil,
interpreter.StaticStore{},
map[string]struct{}{
flags.ExperimentalScopedFunction: {},
flags.ExperimentalMidScriptFunctionCall: {},
},
)
var scopedErr interpreter.CannotStoreScopedAccountInMeta
require.ErrorAs(t, err, &scopedErr)
require.Equal(t, "clients", scopedErr.Account)
require.Equal(t, "premium", scopedErr.Scope)
}
func TestCannotStoreScopedAccountInTxMeta(t *testing.T) {
parsed := parser.Parse(`
set_tx_meta("beneficiary", scoped(@clients, "premium"))
`)
require.Empty(t, parsed.Errors)
_, err := interpreter.RunProgram(
context.Background(),
parsed.Value,
nil,
interpreter.StaticStore{},
map[string]struct{}{
flags.ExperimentalScopedFunction: {},
flags.ExperimentalMidScriptFunctionCall: {},
},
)
var scopedErr interpreter.CannotStoreScopedAccountInMeta
require.ErrorAs(t, err, &scopedErr)
require.Equal(t, "clients", scopedErr.Account)
require.Equal(t, "premium", scopedErr.Scope)
}
func TestInvalidAllotInSendAll(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `send [USD/2 *] (
source = {
1/2 from @a
2/3 from @b
}
destination = @dest
)`)
tc.expected = CaseResult{
Error: interpreter.InvalidAllotmentInSendAll{},
}
test(t, tc)
}
func TestDivByZero(t *testing.T) {
tc := NewTestCase()
src := tc.compile(t, `set_tx_meta("k", 3/0)`)
tc.expected = CaseResult{
Error: interpreter.DivideByZero{
Numerator: big.NewInt(3),
Range: parser.RangeOfIndexed(src, "3/0", 0),
},
}
test(t, tc)
}
func TestInvalidUnboundedWorldInSendAll(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `send [USD/2 *] (
source = @world
destination = @dest
)`)
tc.expected = CaseResult{
Error: interpreter.InvalidUnboundedInSendAll{Name: "world"},
}
test(t, tc)
}
func TestInvalidUnboundedInSendAll(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `send [USD/2 *] (
source = @a allowing unbounded overdraft
destination = @dest
)`)
tc.expected = CaseResult{
Error: interpreter.InvalidUnboundedInSendAll{Name: "a"},
}
test(t, tc)
}
func TestInsufficientFunds(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `vars {
account $balance
account $payment
account $seller
}
send [GEM 16] (
source = {
$balance
$payment
}
destination = $seller
)`)
tc.setVarsFromJSON(t, `{
"balance": "users:001",
"payment": "payments:001",
"seller": "users:002"
}`)
tc.setBalance("users:001", "GEM", 3)
tc.setBalance("payments:001", "GEM", 12)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.MissingFundsErr{
Asset: "GEM",
Needed: *big.NewInt(16),
Available: *big.NewInt(15),
},
}
test(t, tc)
}
func TestTrackBalances2(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
send [COIN 50] (
source = @a
destination = @z
)
send [COIN 50] (
source = @a
destination = @z
)`)
tc.setBalance("a", "COIN", 60)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.MissingFundsErr{
Asset: "COIN",
Needed: *big.NewInt(50),
Available: *big.NewInt(10),
},
}
test(t, tc)
}
func TestInvalidSourceAllotmentSum(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `send [COIN 100] (
source = {
42% from @world
}
destination = @dest
)`)
tc.expected = CaseResult{
Error: interpreter.InvalidAllotmentSum{
ActualSum: *big.NewRat(42, 100),
},
}
test(t, tc)
}
func TestInvalidDestinationAllotmentSum(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `send [COIN 100] (
source = @world
destination = {
1/4 to @x
}
)`)
tc.expected = CaseResult{
Error: interpreter.InvalidAllotmentSum{
ActualSum: *big.NewRat(1, 4),
},
}
test(t, tc)
}
func TestSourceAllotmentInvalidAmt(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `send [COIN 100] (
source = {
// a doesn't have enough amount
10% from @a
// world has, but the computation has already failed
remaining from @world
}
destination = @d
)`)
tc.setBalance("a", "COIN", 1)
tc.expected = CaseResult{
Error: interpreter.MissingFundsErr{
Asset: "COIN",
Needed: *big.NewInt(10),
Available: *big.NewInt(1),
},
}
test(t, tc)
}
func TestNegativeBalance(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
vars {
monetary $balance = balance(@a, EUR/2)
}
send $balance (
source = @world
destination = @dest
)`)
tc.setBalance("a", "EUR/2", -100)
tc.expected = CaseResult{
Error: interpreter.NegativeBalanceError{
Account: "a",
Amount: *big.NewInt(-100),
},
}
test(t, tc)
}
func TestNegativeBalanceLiteral(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
send [EUR/2 -100] (
source = @world
destination = @dest
)`)
tc.expected = CaseResult{
Error: interpreter.NegativeAmountErr{
Amount: interpreter.MonetaryInt(*big.NewInt(-100)),
},
}
test(t, tc)
}
func TestOverdraftBadCurrency(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
send [COIN 100] (
source = @users:1234 allowing overdraft up to [WRONGCURR 100]
destination = @dest
)
`)
tc.expected = CaseResult{
Error: interpreter.MismatchedCurrencyError{
Expected: "COIN",
Got: "WRONGCURR",
},
}
test(t, tc)
}
func TestOverdraftWhenNotEnoughFunds(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
send [COIN 100] (
source = @users:1234 allowing overdraft up to [COIN 10]
destination = @dest
)
`)
tc.setBalance("users:1234", "COIN", 1)
tc.expected = CaseResult{
Error: interpreter.MissingFundsErr{
Asset: "COIN",
Needed: *big.NewInt(100),
Available: *big.NewInt(11),
},
}
test(t, tc)
}
func TestErrors(t *testing.T) {
t.Run("wrong type for send literal", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
send @bad:type (
source = @a
destination = @b
)`)
tc.expected = CaseResult{
Error: interpreter.TypeError{
Expected: "monetary",
Value: interpreter.AccountAddress{Name: "bad:type"},
},
}
test(t, tc)
})
t.Run("wrong type for account literal", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
vars {
number $var_src
}
send [COIN 10] (
source = {
1/2 from @world
remaining from {
@empty
max [COIN 100] from $var_src
}
}
destination = @b
)`)
tc.setVarsFromJSON(t, `{"var_src": "42"}`)
tc.expected = CaseResult{
Error: interpreter.TypeError{
Expected: "account",
Value: interpreter.NewMonetaryInt(42),
},
}
test(t, tc)
})
t.Run("wrong type for account cap", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
vars {
string $v
}
send [COIN 10] (
source = max $v from @src
destination = @b
)`)
tc.setVarsFromJSON(t, `{"v": "abc"}`)
tc.expected = CaseResult{
Error: interpreter.TypeError{
Expected: "monetary",
Value: interpreter.String("abc"),
},
}
test(t, tc)
})
t.Run("unbound variable", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
send $unbound_var (
source = @a
destination = @b
)`)
tc.expected = CaseResult{
Error: interpreter.UnboundVariableErr{
Name: "unbound_var",
Range: parser.RangeOfIndexed(tc.source, "$unbound_var", 0),
},
}
test(t, tc)
})
t.Run("missing variable from json", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
vars {
monetary $x
}
send $x (
source = @a
destination = @b
)`)
tc.expected = CaseResult{
Error: interpreter.MissingVariableErr{
Name: "x",
},
}
test(t, tc)
})
t.Run("unbound fn", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `unbound_fn(1, 2)`)
tc.expected = CaseResult{
Error: interpreter.UnboundFunctionErr{
Name: "unbound_fn",
},
}
test(t, tc)
})
t.Run("unbound fn (origin)", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
vars {
number $x = unbound_fn(1, 2)
}
`)
tc.expected = CaseResult{
Error: interpreter.UnboundFunctionErr{
Name: "unbound_fn",
},
}
test(t, tc)
})
t.Run("wrong fn arity", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `set_tx_meta()`)
tc.expected = CaseResult{
Error: interpreter.BadArityErr{
ExpectedArity: 2,
GivenArguments: 0,
},
}
test(t, tc)
})
t.Run("wrong fn type", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `set_tx_meta(@key_wrong_type, "value")`)
tc.expected = CaseResult{
Error: interpreter.TypeError{
Expected: "string",
Value: interpreter.AccountAddress{Name: "key_wrong_type"},
},
}
test(t, tc)
})
t.Run("invalid variable type", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
vars {
invalidt $x
}
`)
tc.setVarsFromJSON(t, `{"x": "42"}`)
tc.expected = CaseResult{
Error: interpreter.InvalidTypeErr{
Name: "invalidt",
},
}
test(t, tc)
})
t.Run("bad currency type in max (source)", func(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
send [EUR/2 1] (
source = max [USD/2 10] from @world
destination = @b
)
`)
tc.expected = CaseResult{
Error: interpreter.MismatchedCurrencyError{
Expected: "EUR/2",
Got: "USD/2",
},
}
test(t, tc)
})
}
func TestTrackBalancesTricky(t *testing.T) {
t.Skip()
tc := NewTestCase()
tc.setBalance("src", "COIN", 5)
tc.compile(t, `
send [COIN 25] ( // send 10 + 15
source= {
max [COIN 10] from @world
@src // src only has 5 before the program starts
}
destination = {
max [COIN 10] to @src
remaining to @dest // but @src needs to send 15 here
}
)
`)
tc.expected = CaseResult{
Postings: []interpreter.Posting{
{
Source: "world",
Destination: "src",
Amount: big.NewInt(10),
Asset: "GEM",
},
{
Source: "src",
Destination: "dest",
Amount: big.NewInt(15),
Asset: "GEM",
},
},
}
test(t, tc)
}
func TestInvalidNumberLiteral(t *testing.T) {
script := `
vars { number $amt }
send [$amt USD/2] (
source = @world
destination = @dest
)
`
tc := NewTestCase()
tc.compile(t, script)
tc.vars = map[string]string{
"amt": "not a number",
}
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.InvalidNumberLiteral{Range: parser.Range{}, Source: "not a number"},
}
test(t, tc)
}
func TestSaveFromAccount(t *testing.T) {
t.Run("save causes failure", func(t *testing.T) {
script := `
save [USD/2 1] from @alice
send [USD/2 30] (
source = @alice
destination = @bob
)`
tc := NewTestCase()
tc.compile(t, script)
tc.setBalance("alice", "USD/2", 30)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.MissingFundsErr{
Asset: "USD/2",
Needed: *big.NewInt(30),
Available: *big.NewInt(29),
},
}
test(t, tc)
})
t.Run("negative amount", func(t *testing.T) {
script := `
save [USD -100] from @A`
tc := NewTestCase()
tc.compile(t, script)
tc.setBalance("A", "USD", -100)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.NegativeAmountErr{
Amount: interpreter.NewMonetaryInt(-100),
},
}
test(t, tc)
})
}
func TestAddNumbersInvalidRightType(t *testing.T) {
script := `
set_tx_meta("k", 1 + "not a number")
`
tc := NewTestCase()
tc.compile(t, script)
tc.expected = CaseResult{
Error: interpreter.TypeError{
Expected: "number",
Value: interpreter.String("not a number"),
},
}
test(t, tc)
}
func TestAddMonetariesDifferentCurrencies(t *testing.T) {
script := `
send [USD/2 1] + [EUR/2 2] (
source = @world
destination = @dest
)
`
tc := NewTestCase()
tc.compile(t, script)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.MismatchedCurrencyError{
Expected: "USD/2",
Got: "EUR/2",
},
}
test(t, tc)
}
func TestAddInvalidLeftType(t *testing.T) {
script := `
set_tx_meta("k", EUR/2 + EUR/3)
`
tc := NewTestCase()
tc.compile(t, script)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.TypeError{
Expected: "monetary|number",
Value: interpreter.Asset("EUR/2"),
},
}
test(t, tc)
}
func TestOneofAllFailing(t *testing.T) {
tc := NewTestCase()
tc.compile(t, `
send [GEM 1] (
source = oneof {
@empty1
@empty2
@empty3
}
destination = @dest
)
`)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.MissingFundsErr{
Asset: "GEM",
Needed: *big.NewInt(1),
Available: *big.NewInt(0),
},
}
testWithFeatureFlag(t, tc, flags.ExperimentalOneofFeatureFlag)
}
func TestInvalidAccount(t *testing.T) {
script := `
vars {
account $acc
}
set_tx_meta("k", $acc)
`
tc := NewTestCase()
tc.setVarsFromJSON(t, `
{
"acc": "!invalid acc.."
}
`)
tc.compile(t, script)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.InvalidAccountName{
Name: "!invalid acc..",
},
}
test(t, tc)
}
func TestInvalidInterpAccount(t *testing.T) {
script := `
vars {
string $status
}
set_tx_meta("k", @user:$status)
`
tc := NewTestCase()
tc.setVarsFromJSON(t, `
{
"status": "!invalid acc.."
}
`)
tc.compile(t, script)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.InvalidAccountName{
Name: "user:!invalid acc..",
},
}
testWithFeatureFlag(t, tc, flags.ExperimentalAccountInterpolationFlag)
}
func TestAccountInvalidString(t *testing.T) {
script := `
vars {
monetary $m
}
set_tx_meta("k", @acc:$m)
`
tc := NewTestCase()
tc.setVarsFromJSON(t, `
{
"m": "USD/2 10"
}
`)
tc.compile(t, script)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.CannotCastToString{
Range: parser.RangeOfIndexed(script, "@acc:$m", 0),
Value: interpreter.Monetary{
Amount: interpreter.NewMonetaryInt(10),
Asset: interpreter.Asset("USD/2"),
},
},
}
testWithFeatureFlag(t, tc, flags.ExperimentalAccountInterpolationFlag)
}
func TestInvalidNestedMetaCall(t *testing.T) {
script := `
vars {
number $x = 1 + meta(@acc, "k")
}
`
tc := NewTestCase()
tc.meta = interpreter.AccountsMetadata{
{Account: "acc", Key: "k", Value: "42"},
}
tc.compile(t, script)
tc.expected = CaseResult{
Error: interpreter.InvalidNestedMeta{},
}
testWithFeatureFlag(t, tc, flags.ExperimentalMidScriptFunctionCall)
}
func TestColorRestrictBalanceWhenMissingFunds(t *testing.T) {
script := `
send [COIN 20] (
source = @acc \ "RED"
destination = @dest
)
`
tc := NewTestCase()
tc.setBalance("acc", "COIN", 100)
tc.setColoredBalance("acc", "COIN", "RED", 1)
tc.compile(t, script)
tc.expected = CaseResult{
Postings: []Posting{},
Error: interpreter.MissingFundsErr{
Needed: *big.NewInt(20),
Available: *big.NewInt(1),
Asset: "COIN",
},
}
testWithFeatureFlag(t, tc, flags.ExperimentalAssetColors)
}
func TestSafeMaxWithdraft(t *testing.T) {
require.Equal(t, big.NewInt(0), interpreter.CalculateMaxSafeWithdraw(
big.NewInt(0),
big.NewInt(0),
))
require.Equal(t, big.NewInt(200), interpreter.CalculateMaxSafeWithdraw(
big.NewInt(100),
big.NewInt(100),
))
require.Equal(t, big.NewInt(105), interpreter.CalculateMaxSafeWithdraw(
big.NewInt(100),
big.NewInt(5),
))
require.Equal(t, big.NewInt(0), interpreter.CalculateMaxSafeWithdraw(
big.NewInt(-10),
big.NewInt(0),
))
require.Equal(t, big.NewInt(0), interpreter.CalculateMaxSafeWithdraw(
big.NewInt(-10),
big.NewInt(5),
))
require.Equal(t, big.NewInt(0), interpreter.CalculateMaxSafeWithdraw(
big.NewInt(-10),
big.NewInt(10),
))
require.Equal(t, big.NewInt(1), interpreter.CalculateMaxSafeWithdraw(
big.NewInt(-10),
big.NewInt(11),
))
}
// TODO this should be a fuzz test instead
func TestSafeWithdraft(t *testing.T) {
t.Run("with zero overdraft, only take what's available", func(t *testing.T) {
t.Run("balance > 0 allows you to take what's available", func(t *testing.T) {
require.Equal(t, big.NewInt(10), interpreter.CalculateSafeWithdraw(
big.NewInt(100),
big.NewInt(0),
big.NewInt(10),
))
require.Equal(t, big.NewInt(10), interpreter.CalculateSafeWithdraw(
big.NewInt(10),
big.NewInt(0),
big.NewInt(10),
))
require.Equal(t, big.NewInt(1), interpreter.CalculateSafeWithdraw(
big.NewInt(1),
big.NewInt(0),
big.NewInt(10),
))
require.Equal(t, big.NewInt(0), interpreter.CalculateSafeWithdraw(
big.NewInt(10),
big.NewInt(0),
big.NewInt(0),
))
// not enough balance:
require.Equal(t, big.NewInt(10), interpreter.CalculateSafeWithdraw(
big.NewInt(10),
big.NewInt(0),
big.NewInt(100),
))
})
t.Run("balance == 0 doesn't let you take anything", func(t *testing.T) {
require.Equal(t, big.NewInt(0), interpreter.CalculateSafeWithdraw(
big.NewInt(0),
big.NewInt(0),
big.NewInt(0),
))
})
t.Run("balance < 0 doesn't let you take anything if there's no overdraft", func(t *testing.T) {
require.Equal(t, big.NewInt(0), interpreter.CalculateSafeWithdraw(
big.NewInt(-100),
big.NewInt(0),
big.NewInt(10),
))
require.Equal(t, big.NewInt(0), interpreter.CalculateSafeWithdraw(
big.NewInt(0),
big.NewInt(0),
big.NewInt(10),
))
require.Equal(t, big.NewInt(0), interpreter.CalculateSafeWithdraw(
big.NewInt(-1),
big.NewInt(0),
big.NewInt(10),
))
require.Equal(t, big.NewInt(0), interpreter.CalculateSafeWithdraw(
big.NewInt(-10),
big.NewInt(0),
big.NewInt(0),
))
})
})
t.Run("when overdraft is not zero, you can go over your balance", func(t *testing.T) {
t.Run("if we have enough balance>=requestedAmount, overdraft is ignored matter", func(t *testing.T) {
require.Equal(t, big.NewInt(10), interpreter.CalculateSafeWithdraw(
big.NewInt(100),
big.NewInt(100),
big.NewInt(10),
))
require.Equal(t, big.NewInt(100), interpreter.CalculateSafeWithdraw(
big.NewInt(100),
big.NewInt(42),
big.NewInt(100),
))
})
t.Run("if we have zero balance, overdraft allows us to withdraw", func(t *testing.T) {
require.Equal(t, big.NewInt(10), interpreter.CalculateSafeWithdraw(
big.NewInt(0),
big.NewInt(100),
big.NewInt(10),
))
})
})
}
func TestInvalidScalingWorld(t *testing.T) {
script := `
send [EUR/2 *] (
source = @world with scaling through @swap
destination = @dest
)
`
tc := NewTestCase()
tc.compile(t, script)
tc.expected = CaseResult{
Error: interpreter.InvalidUnboundedAddressInScalingAddress{},
}
testWithFeatureFlag(t, tc, flags.AssetScaling)
}