Skip to content

Commit

Permalink
refactor: machine resources resolution (#162)
Browse files Browse the repository at this point in the history
  • Loading branch information
gfyrag authored and flemzord committed May 12, 2023
1 parent a3d2c72 commit 9867c88
Show file tree
Hide file tree
Showing 13 changed files with 1,288 additions and 607 deletions.
12 changes: 6 additions & 6 deletions components/ledger/pkg/api/apierrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"strings"

"github.com/formancehq/ledger/pkg/ledger/runner"
"github.com/formancehq/ledger/pkg/machine"
"github.com/formancehq/ledger/pkg/machine/vm"
"github.com/formancehq/ledger/pkg/storage"
"github.com/formancehq/stack/libs/go-libs/api"
"github.com/formancehq/stack/libs/go-libs/logging"
Expand Down Expand Up @@ -56,11 +56,11 @@ func coreErrorToErrorCode(err error) (int, string, string) {
return http.StatusBadRequest, ErrValidation, ""
case runner.IsNotFoundError(err):
return http.StatusNotFound, ErrNotFound, ""
case machine.IsScriptErrorWithCode(err, ErrScriptNoScript),
machine.IsScriptErrorWithCode(err, ErrInsufficientFund),
machine.IsScriptErrorWithCode(err, ErrScriptCompilationFailed),
machine.IsScriptErrorWithCode(err, ErrScriptMetadataOverride):
scriptErr := &machine.ScriptError{}
case vm.IsScriptErrorWithCode(err, ErrScriptNoScript),
vm.IsScriptErrorWithCode(err, ErrInsufficientFund),
vm.IsScriptErrorWithCode(err, ErrScriptCompilationFailed),
vm.IsScriptErrorWithCode(err, ErrScriptMetadataOverride):
scriptErr := &vm.ScriptError{}
_ = errors.As(err, &scriptErr)
return http.StatusBadRequest, scriptErr.Code, EncodeLink(scriptErr.Message)
case errors.Is(err, context.Canceled):
Expand Down
26 changes: 23 additions & 3 deletions components/ledger/pkg/core/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TypeFromName(name string) (Type, bool) {
}
}

func NewValueFromTypedJSON(rawInput json.RawMessage) (*Value, error) {
func NewValueFromTypedJSON(rawInput json.RawMessage) (Value, error) {
var input ValueJSON
if err := json.Unmarshal(rawInput, &input); err != nil {
return nil, err
Expand All @@ -43,7 +43,7 @@ func NewValueFromTypedJSON(rawInput json.RawMessage) (*Value, error) {
return NewValueFromJSON(typ, input.Value)
}

func NewValueFromJSON(typ Type, data json.RawMessage) (*Value, error) {
func NewValueFromJSON(typ Type, data json.RawMessage) (Value, error) {
var value Value
switch typ {
case TypeAccount:
Expand Down Expand Up @@ -106,5 +106,25 @@ func NewValueFromJSON(typ Type, data json.RawMessage) (*Value, error) {
return nil, fmt.Errorf("invalid type '%v'", typ)
}

return &value, nil
return value, nil
}

func NewJSONFromValue(value Value) (any, error) {
switch value.GetType() {
case TypeAccount:
return string(value.(AccountAddress)), nil
case TypeAsset:
return string(value.(Asset)), nil
case TypeString:
return string(value.(String)), nil
case TypeNumber:
return value.(*MonetaryInt).String(), nil
case TypeMonetary:
return value, nil

case TypePortion:
return value.(Portion).String(), nil
default:
return nil, fmt.Errorf("invalid type '%v'", value.GetType())
}
}
20 changes: 10 additions & 10 deletions components/ledger/pkg/core/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ func TestAccountTypedJSON(t *testing.T) {
value, err := NewValueFromTypedJSON(j)
require.NoError(t, err)

if !ValueEquals(*value, AccountAddress("users:001")) {
t.Fatalf("unexpected value: %v", *value)
if !ValueEquals(value, AccountAddress("users:001")) {
t.Fatalf("unexpected value: %v", value)
}
}

Expand All @@ -30,8 +30,8 @@ func TestAssetTypedJSON(t *testing.T) {
value, err := NewValueFromTypedJSON(j)
require.NoError(t, err)

if !ValueEquals(*value, Asset("EUR/2")) {
t.Fatalf("unexpected value: %v", *value)
if !ValueEquals(value, Asset("EUR/2")) {
t.Fatalf("unexpected value: %v", value)
}
}

Expand All @@ -46,8 +46,8 @@ func TestNumberTypedJSON(t *testing.T) {
num, err := ParseNumber("89849865111111111111111111111111111555555555555555555555555555555555555555555555555999999999999999999999")
require.NoError(t, err)

if !ValueEquals(*value, num) {
t.Fatalf("unexpected value: %v", *value)
if !ValueEquals(value, num) {
t.Fatalf("unexpected value: %v", value)
}
}

Expand All @@ -62,11 +62,11 @@ func TestMonetaryTypedJSON(t *testing.T) {
value, err := NewValueFromTypedJSON(j)
require.NoError(t, err)

if !ValueEquals(*value, Monetary{
if !ValueEquals(value, Monetary{
Asset: "EUR/2",
Amount: NewMonetaryInt(123456),
}) {
t.Fatalf("unexpected value: %v", *value)
t.Fatalf("unexpected value: %v", value)
}
}

Expand All @@ -81,8 +81,8 @@ func TestPortionTypedJSON(t *testing.T) {
portion, err := NewPortionSpecific(*big.NewRat(90, 100))
require.NoError(t, err)

if !ValueEquals(*value, *portion) {
t.Fatalf("unexpected value: %v", *value)
if !ValueEquals(value, *portion) {
t.Fatalf("unexpected value: %v", value)
}
}

Expand Down
7 changes: 4 additions & 3 deletions components/ledger/pkg/ledger/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/formancehq/ledger/pkg/ledger/lock"
"github.com/formancehq/ledger/pkg/machine"
"github.com/formancehq/ledger/pkg/machine/script/compiler"
"github.com/formancehq/ledger/pkg/machine/vm"
"github.com/formancehq/ledger/pkg/storage"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -115,7 +116,7 @@ func (r *Runner) acquireInflight(ctx context.Context, script core.RunScript) (*i

script.WithDefaultValues()
if script.Plain == "" {
return nil, machine.NewScriptError(machine.ScriptErrorNoScript, "no script to execute")
return nil, vm.NewScriptError(vm.ScriptErrorNoScript, "no script to execute")
}

if err := r.checkConstraints(ctx, script); err != nil {
Expand All @@ -135,12 +136,12 @@ func (r *Runner) execute(ctx context.Context, script core.RunScript, dryRun bool

prog, err := compiler.Compile(script.Plain)
if err != nil {
return nil, nil, machine.NewScriptError(machine.ScriptErrorCompilationFailed, errors.Wrap(err, "compiling numscript").Error())
return nil, nil, vm.NewScriptError(vm.ScriptErrorCompilationFailed, errors.Wrap(err, "compiling numscript").Error())
}

involvedAccounts, err := prog.GetInvolvedAccounts(script.Vars)
if err != nil {
return nil, nil, machine.NewScriptError(machine.ScriptErrorCompilationFailed, err.Error())
return nil, nil, vm.NewScriptError(vm.ScriptErrorCompilationFailed, err.Error())
}

unlock, err := r.locker.Lock(ctx, r.store.Name(), involvedAccounts...)
Expand Down
6 changes: 3 additions & 3 deletions components/ledger/pkg/ledger/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/formancehq/ledger/pkg/ledger/cache"
"github.com/formancehq/ledger/pkg/ledger/lock"
"github.com/formancehq/ledger/pkg/ledgertesting"
"github.com/formancehq/ledger/pkg/machine"
"github.com/formancehq/ledger/pkg/machine/vm"
"github.com/formancehq/stack/libs/go-libs/pgtesting"
"github.com/google/uuid"
"github.com/pkg/errors"
Expand Down Expand Up @@ -74,12 +74,12 @@ var testCases = []testCase{
{
name: "no script",
script: ``,
expectedError: machine.NewScriptError(machine.ScriptErrorNoScript, ""),
expectedError: vm.NewScriptError(vm.ScriptErrorNoScript, ""),
},
{
name: "invalid script",
script: `XXX`,
expectedError: machine.NewScriptError(machine.ScriptErrorCompilationFailed, ""),
expectedError: vm.NewScriptError(vm.ScriptErrorCompilationFailed, ""),
},
{
name: "set reference conflict",
Expand Down
47 changes: 22 additions & 25 deletions components/ledger/pkg/machine/examples/basic.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"fmt"

"github.com/formancehq/ledger/pkg/core"
Expand Down Expand Up @@ -38,35 +39,31 @@ func main() {
panic(err)
}

initialBalances := map[string]map[string]*core.MonetaryInt{
"alice": {"COIN": core.NewMonetaryInt(10)},
"bob": {"COIN": core.NewMonetaryInt(100)},
initialVolumes := map[string]map[string]core.Volumes{
"alice": {
"COIN": core.NewEmptyVolumes().WithInput(core.NewMonetaryInt(10)),
},
"bob": {
"COIN": core.NewEmptyVolumes().WithInput(core.NewMonetaryInt(100)),
},
}

{
ch, err := m.ResolveResources()
if err != nil {
panic(err)
}
for req := range ch {
if req.Error != nil {
panic(req.Error)
}
}
err = m.ResolveResources(context.Background(), vm.EmptyStore)
if err != nil {
panic(err)
}

{
ch, err := m.ResolveBalances()
if err != nil {
panic(err)
}
for req := range ch {
val := initialBalances[req.Account][req.Asset]
if req.Error != nil {
panic(req.Error)
}
req.Response <- val
}
err = m.ResolveBalances(context.Background(), vm.StoreFn(func(ctx context.Context, address string) (*core.AccountWithVolumes, error) {
return &core.AccountWithVolumes{
Account: core.Account{
Address: address,
Metadata: core.Metadata{},
},
Volumes: initialVolumes[address],
}, nil
}))
if err != nil {
panic(err)
}

exitCode, err := m.Execute()
Expand Down
66 changes: 9 additions & 57 deletions components/ledger/pkg/machine/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package machine
import (
"context"
"encoding/json"
"fmt"

"github.com/formancehq/ledger/pkg/core"
"github.com/formancehq/ledger/pkg/machine/vm"
Expand All @@ -26,67 +25,20 @@ func Run(ctx context.Context, store Store, prog *program.Program, script core.Ru
m := vm.NewMachine(*prog)

if err := m.SetVarsFromJSON(script.Vars); err != nil {
return nil, NewScriptError(ScriptErrorCompilationFailed,
return nil, vm.NewScriptError(vm.ScriptErrorCompilationFailed,
errors.Wrap(err, "could not set variables").Error())
}

resourcesChan, err := m.ResolveResources()
err := m.ResolveResources(ctx, store)
if err != nil {
return nil, errors.Wrap(err, "could not resolve program resources")
}
for req := range resourcesChan {
if req.Error != nil {
return nil, NewScriptError(ScriptErrorCompilationFailed,
errors.Wrap(req.Error, "could not resolve program resources").Error())
}
account, err := store.GetAccountWithVolumes(ctx, req.Account)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("could not get account %q", req.Account))
}
if req.Key != "" {
entry, ok := account.Metadata[req.Key]
if !ok {
return nil, NewScriptError(ScriptErrorCompilationFailed,
fmt.Sprintf("missing key %v in metadata for account %v", req.Key, req.Account))
}
data, err := json.Marshal(entry)
if err != nil {
return nil, errors.Wrap(err, "marshaling metadata")
}
value, err := core.NewValueFromTypedJSON(data)
if err != nil {
return nil, NewScriptError(ScriptErrorCompilationFailed,
errors.Wrap(err, fmt.Sprintf("invalid format for metadata at key %v for account %v",
req.Key, req.Account)).Error())
}
req.Response <- *value
} else if req.Asset != "" {
amt := account.Volumes[req.Asset].Balance().OrZero()
resp := *amt
req.Response <- &resp
} else {
return nil, NewScriptError(ScriptErrorCompilationFailed,
errors.Wrap(err, fmt.Sprintf("invalid ResourceRequest: %+v", req)).Error())
}
return nil, vm.NewScriptError(vm.ScriptErrorCompilationFailed,
errors.Wrap(err, "could not resolve program resources").Error())
}

balanceCh, err := m.ResolveBalances()
err = m.ResolveBalances(ctx, store)
if err != nil {
return nil, errors.Wrap(err, "could not resolve balances")
}
for req := range balanceCh {
if req.Error != nil {
return nil, NewScriptError(ScriptErrorCompilationFailed,
errors.Wrap(req.Error, "could not resolve program balances").Error())
}
var amt *core.MonetaryInt
account, err := store.GetAccountWithVolumes(ctx, req.Account)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("could not get account %q", req.Account))
}
amt = account.Volumes[req.Asset].Balance().OrZero()
resp := *amt
req.Response <- &resp
return nil, vm.NewScriptError(vm.ScriptErrorCompilationFailed,
errors.Wrap(err, "could not resolve balances").Error())
}

exitCode, err := m.Execute()
Expand All @@ -103,7 +55,7 @@ func Run(ctx context.Context, store Store, prog *program.Program, script core.Ru
case vm.EXIT_FAIL_INSUFFICIENT_FUNDS:
// TODO: If the machine can provide the asset which is failing
// we should be able to use InsufficientFundError{} instead of error code
return nil, NewScriptError(ScriptErrorInsufficientFund,
return nil, vm.NewScriptError(vm.ScriptErrorInsufficientFund,
"account had insufficient funds")
default:
return nil, errors.New("script execution failed")
Expand Down Expand Up @@ -136,7 +88,7 @@ func Run(ctx context.Context, store Store, prog *program.Program, script core.Ru
for k, v := range script.Metadata {
_, ok := result.Metadata[k]
if ok {
return nil, NewScriptError(ScriptErrorMetadataOverride, "cannot override metadata from script")
return nil, vm.NewScriptError(vm.ScriptErrorMetadataOverride, "cannot override metadata from script")
}
result.Metadata[k] = v
}
Expand Down
Loading

0 comments on commit 9867c88

Please sign in to comment.