Skip to content

Commit

Permalink
refactor(logic)!: standardize predicates errors
Browse files Browse the repository at this point in the history
  • Loading branch information
ccamel committed Jan 13, 2024
1 parent bf29e51 commit ea7b38d
Show file tree
Hide file tree
Showing 40 changed files with 1,167 additions and 979 deletions.
1 change: 1 addition & 0 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ var registry = map[string]any{
"read_string/3": predicate.ReadString,
"eddsa_verify/4": predicate.EDDSAVerify,
"ecdsa_verify/4": predicate.ECDSAVerify,
"string_bytes/3": predicate.StringBytes,
}

// RegistryNames is the list of the predicate names in the Registry.
Expand Down
74 changes: 28 additions & 46 deletions x/logic/predicate/address.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package predicate

import (
"context"
"fmt"

"github.com/ichiban/prolog/engine"

bech322 "github.com/cosmos/cosmos-sdk/types/bech32"
Expand All @@ -21,7 +18,7 @@ import (
// bech32_address(+Address, +Bech32)
//
// where:
// - Address is a pair of the HRP (Human-Readable Part) which holds the address prefix and a list of integers
// - Address is a pair of the HRP (Human-Readable Part) which holds the address prefix and a list of numbers
// ranging from 0 to 255 that represent the base64 encoded bech32 address string.
// - Bech32 is an Atom or string representing the bech32 encoded string address
//
Expand All @@ -36,56 +33,41 @@ import (
//
// [bech32]: https://docs.cosmos.network/main/build/spec/addresses/bech32#hrp-table
// [base64]: https://fr.wikipedia.org/wiki/Base64
func Bech32Address(vm *engine.VM, address, bech32 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return engine.Delay(func(ctx context.Context) *engine.Promise {
switch b := env.Resolve(bech32).(type) {
case engine.Variable:
case engine.Atom:
h, a, err := bech322.DecodeAndConvert(b.String())
if err != nil {
return engine.Error(fmt.Errorf("bech32_address/2: failed to decode Bech32: %w", err))
}
pair := prolog.AtomPair.Apply(prolog.StringToTerm(h), prolog.BytesToCodepointListTermWithDefault(a, env))
return engine.Unify(vm, address, pair, cont, env)
default:
return engine.Error(fmt.Errorf("bech32_address/2: invalid Bech32 type: %T, should be Atom or Variable", b))
func Bech32Address(_ *engine.VM, address, bech32 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
forwardConverter := func(value []engine.Term, options engine.Term, env *engine.Env) ([]engine.Term, error) {
hrpTerm, dataTerm, err := prolog.AssertPair(env, value[0])
if err != nil {
return nil, err
}
data, err := prolog.ByteListTermToBytes(dataTerm, env)
if err != nil {
return nil, err
}
hrp, err := prolog.AssertAtom(env, hrpTerm)
if err != nil {
return nil, err
}

switch addressPair := env.Resolve(address).(type) {
case engine.Compound:
bech32Decoded, err := addressPairToBech32(addressPair, env)
if err != nil {
return engine.Error(fmt.Errorf("bech32_address/2: %w", err))
}
return engine.Unify(vm, bech32, prolog.StringToTerm(bech32Decoded), cont, env)
default:
return engine.Error(fmt.Errorf("bech32_address/2: invalid address type: %T, should be Compound (Hrp, Address)", addressPair))
b, err := bech322.ConvertAndEncode(hrp.String(), data)
if err != nil {
return nil, prolog.WithError(engine.DomainError(prolog.ValidEncoding("bech32"), value[0], env), err, env)
}
})
}

func addressPairToBech32(addressPair engine.Compound, env *engine.Env) (string, error) {
if addressPair.Functor() != prolog.AtomPair || addressPair.Arity() != 2 {
return "", fmt.Errorf("address should be a Pair '-(Hrp, Address)'")
return []engine.Term{engine.NewAtom(b)}, nil
}

switch a := env.Resolve(addressPair.Arg(1)).(type) {
case engine.Compound:
data, err := prolog.StringTermToBytes(a, prolog.AtomEmpty, env)
backwardConverter := func(value []engine.Term, options engine.Term, env *engine.Env) ([]engine.Term, error) {
b, err := prolog.AssertAtom(env, value[0])
if err != nil {
return "", fmt.Errorf("failed to convert term to bytes list: %w", err)
return nil, err
}
hrp, ok := env.Resolve(addressPair.Arg(0)).(engine.Atom)
if !ok {
return "", fmt.Errorf("HRP should be instantiated")
}
b, err := bech322.ConvertAndEncode(hrp.String(), data)
h, a, err := bech322.DecodeAndConvert(b.String())
if err != nil {
return "", fmt.Errorf("failed to convert base64 encoded address to bech32 string encoded: %w", err)
return nil, prolog.WithError(engine.DomainError(prolog.ValidEncoding("bech32"), value[0], env), err, env)
}

return b, nil
default:
return "", fmt.Errorf("address should be a Pair with a List of bytes in arity 2, given: %T", addressPair.Arg(1))
var r engine.Term = engine.NewAtom(h)
pair := prolog.AtomPair.Apply(r, prolog.BytesToByteListTerm(a))
return []engine.Term{pair}, nil
}
return prolog.UnifyFunctionalPredicate(
[]engine.Term{address}, []engine.Term{bech32}, prolog.AtomEmpty, forwardConverter, backwardConverter, cont, env)
}
39 changes: 23 additions & 16 deletions x/logic/predicate/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package predicate

import (
"fmt"
"strings"
"testing"

"github.com/ichiban/prolog/engine"
Expand Down Expand Up @@ -45,8 +46,8 @@ func TestBech32(t *testing.T) {
wantSuccess: true,
},
{
query: `bech32_address(-('okp4', [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), foo(bar)).`,
wantError: fmt.Errorf("bech32_address/2: invalid Bech32 type: *engine.compound, should be Atom or Variable"),
query: `bech32_address(-('okp4', X), foo(bar)).`,
wantError: fmt.Errorf("error(type_error(atom,foo(bar)),bech32_address/2)"),
wantSuccess: false,
},
{
Expand Down Expand Up @@ -83,33 +84,39 @@ func TestBech32(t *testing.T) {
wantSuccess: true,
},
{
query: `bech32_address(foo(Bar), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: address should be a Pair '-(Hrp, Address)'"),
query: `bech32_address(foo(bar), Bech32).`,
wantError: fmt.Errorf("error(type_error(pair,foo(bar)),bech32_address/2)"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', ['8956',167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: error(domain_error(valid_character_code(8956),[8956,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]),bech32_address/2)"),
query: `bech32_address(-('okp4', ['163',167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`,
wantError: fmt.Errorf("error(type_error(byte,163),bech32_address/2)"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', [163,'x',23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`,
wantError: fmt.Errorf("error(type_error(byte,x),bech32_address/2)"),
wantSuccess: false,
},
{
query: `bech32_address(-(Hrp, [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: HRP should be instantiated"),
wantError: fmt.Errorf("error(instantiation_error,bech32_address/2)"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', hey(2)), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: failed to convert term to bytes list: error(type_error(character_code,hey(2)),bech32_address/2)"),
wantError: fmt.Errorf("error(type_error(list,hey(2)),bech32_address/2)"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', 'foo'), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: address should be a Pair with a List of bytes in arity 2, given: engine.Atom"),
query: `bech32_address(-('okp4', X), foo).`,
wantError: fmt.Errorf("error(domain_error(encoding(bech32),foo),[%s],bech32_address/2)",
strings.Join(strings.Split("decoding bech32 failed: invalid bech32 string length 3", ""), ",")),
wantSuccess: false,
},
{
query: `bech32_address(Address, Bech32).`,
wantError: fmt.Errorf("bech32_address/2: invalid address type: engine.Variable, should be Compound (Hrp, Address)"),
wantError: fmt.Errorf("error(instantiation_error,bech32_address/2)"),
wantSuccess: false,
},
}
Expand All @@ -125,29 +132,29 @@ func TestBech32(t *testing.T) {
interpreter.Register2(engine.NewAtom("bech32_address"), Bech32Address)

err := interpreter.Compile(ctx, tc.program)
So(err, ShouldBeNil)
So(err, ShouldEqual, nil)

Convey("When the predicate is called", func() {
sols, err := interpreter.QueryContext(ctx, tc.query)

Convey("Then the error should be nil", func() {
So(err, ShouldBeNil)
So(err, ShouldEqual, nil)
So(sols, ShouldNotBeNil)

Convey("and the bindings should be as expected", func() {
var got []types.TermResults
for sols.Next() {
m := types.TermResults{}
err := sols.Scan(m)
So(err, ShouldBeNil)
So(err, ShouldEqual, nil)

got = append(got, m)
}
if tc.wantError != nil {
So(sols.Err(), ShouldNotBeNil)
So(sols.Err(), ShouldNotEqual, nil)
So(sols.Err().Error(), ShouldEqual, tc.wantError.Error())
} else {
So(sols.Err(), ShouldBeNil)
So(sols.Err(), ShouldEqual, nil)

if tc.wantSuccess {
So(len(got), ShouldBeGreaterThan, 0)
Expand Down
23 changes: 12 additions & 11 deletions x/logic/predicate/bank.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ package predicate

import (
"context"
"fmt"

"github.com/ichiban/prolog/engine"

sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/okp4/okp4d/x/logic/prolog"
"github.com/okp4/okp4d/x/logic/types"
"github.com/okp4/okp4d/x/logic/util"
)

// BankBalances is a predicate which unifies the given terms with the list of balances (coins) of the given account.
Expand All @@ -34,7 +32,7 @@ import (
// # Query the first balance of the given account by unifying the denomination and amount with the given terms.
// - bank_balances('okp41ffd5wx65l407yvm478cxzlgygw07h79sq0m3fm', [-(D, A), _]).
func BankBalances(vm *engine.VM, account, balances engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return fetchBalances("bank_balances/2",
return fetchBalances(
account,
balances,
vm,
Expand Down Expand Up @@ -69,7 +67,7 @@ func BankBalances(vm *engine.VM, account, balances engine.Term, cont engine.Cont
// # Query the first spendable balances of the given account by unifying the denomination and amount with the given terms.
// - bank_spendable_balances('okp41ffd5wx65l407yvm478cxzlgygw07h79sq0m3fm', [-(D, A), _]).
func BankSpendableBalances(vm *engine.VM, account, balances engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return fetchBalances("bank_spendable_balances/2",
return fetchBalances(
account,
balances,
vm,
Expand Down Expand Up @@ -101,7 +99,7 @@ func BankSpendableBalances(vm *engine.VM, account, balances engine.Term, cont en
// # Query the first locked balances of the given account by unifying the denomination and amount with the given terms.
// - bank_locked_balances('okp41ffd5wx65l407yvm478cxzlgygw07h79sq0m3fm', [-(D, A), _]).
func BankLockedBalances(vm *engine.VM, account, balances engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
return fetchBalances("bank_locked_balances/2",
return fetchBalances(
account,
balances,
vm,
Expand All @@ -116,31 +114,34 @@ func getBech32(env *engine.Env, account engine.Term) (sdk.AccAddress, error) {
switch acc := env.Resolve(account).(type) {
case engine.Variable:
case engine.Atom:
return sdk.AccAddressFromBech32(acc.String())
addr, err := sdk.AccAddressFromBech32(acc.String())
if err != nil {
return nil, prolog.WithError(prolog.ResourceError(prolog.ResourceModule("bank"), env), err, env)
}
return addr, nil
default:
return nil, fmt.Errorf("cannot unify account address with %T", acc)
return nil, engine.TypeError(prolog.AtomTypeAtom, account, env)
}
return sdk.AccAddress(nil), nil
}

func fetchBalances(
predicate string,
account, balances engine.Term,
vm *engine.VM,
env *engine.Env,
cont engine.Cont,
coinsFn func(ctx sdk.Context, bankKeeper types.BankKeeper, coins sdk.Coins, address sdk.AccAddress) sdk.Coins,
) *engine.Promise {
return engine.Delay(func(ctx context.Context) *engine.Promise {
sdkContext, err := util.UnwrapSDKContext(ctx)
sdkContext, err := prolog.UnwrapSDKContext(ctx, env)
if err != nil {
return engine.Error(err)
}
bankKeeper := sdkContext.Value(types.BankKeeperContextKey).(types.BankKeeper)

bech32Addr, err := getBech32(env, account)
if err != nil {
return engine.Error(fmt.Errorf("%s: %w", predicate, err))
return engine.Error(err)
}

if bech32Addr != nil {
Expand All @@ -154,7 +155,7 @@ func fetchBalances(
address := balance.Address
bech32Addr, err = sdk.AccAddressFromBech32(address)
if err != nil {
return engine.Error(fmt.Errorf("%s: %w", predicate, err))
return engine.Error(prolog.WithError(prolog.ResourceError(prolog.ResourceModule("bank"), env), err, env))
}
coins := coinsFn(sdkContext, bankKeeper, balance.Coins, bech32Addr)

Expand Down
20 changes: 12 additions & 8 deletions x/logic/predicate/bank_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package predicate

import (
"fmt"
"strings"
"testing"

"github.com/golang/mock/gomock"
Expand Down Expand Up @@ -144,7 +145,8 @@ func TestBank(t *testing.T) {
balances: []bank.Balance{},
query: `bank_balances('foo', X).`,
wantResult: []types.TermResults{{"X": "[uknow-100]"}},
wantError: fmt.Errorf("bank_balances/2: decoding bech32 failed: invalid bech32 string length 3"),
wantError: fmt.Errorf("error(resource_error(resource_module(bank)),[%s],unknown)",
strings.Join(strings.Split("decoding bech32 failed: invalid bech32 string length 3", ""), ",")),
},
{
balances: []bank.Balance{
Expand Down Expand Up @@ -271,7 +273,8 @@ func TestBank(t *testing.T) {
spendableCoins: []bank.Balance{},
query: `bank_spendable_balances('foo', X).`,
wantResult: []types.TermResults{{"X": "[uknow-100]"}},
wantError: fmt.Errorf("bank_spendable_balances/2: decoding bech32 failed: invalid bech32 string length 3"),
wantError: fmt.Errorf("error(resource_error(resource_module(bank)),[%s],unknown)",
strings.Join(strings.Split("decoding bech32 failed: invalid bech32 string length 3", ""), ",")),
},

{
Expand Down Expand Up @@ -415,7 +418,8 @@ func TestBank(t *testing.T) {
lockedCoins: []bank.Balance{},
query: `bank_locked_balances('foo', X).`,
wantResult: []types.TermResults{{"X": "[uknow-100]"}},
wantError: fmt.Errorf("bank_locked_balances/2: decoding bech32 failed: invalid bech32 string length 3"),
wantError: fmt.Errorf("error(resource_error(resource_module(bank)),[%s],unknown)",
strings.Join(strings.Split("decoding bech32 failed: invalid bech32 string length 3", ""), ",")),
},
}
for nc, tc := range cases {
Expand Down Expand Up @@ -464,29 +468,29 @@ func TestBank(t *testing.T) {
interpreter.Register2(engine.NewAtom("bank_locked_balances"), BankLockedBalances)

err := interpreter.Compile(ctx, tc.program)
So(err, ShouldBeNil)
So(err, ShouldEqual, nil)

Convey("When the predicate is called", func() {
sols, err := interpreter.QueryContext(ctx, tc.query)

Convey("Then the error should be nil", func() {
So(err, ShouldBeNil)
So(err, ShouldEqual, nil)
So(sols, ShouldNotBeNil)

Convey("and the bindings should be as expected", func() {
var got []types.TermResults
for sols.Next() {
m := types.TermResults{}
err := sols.Scan(m)
So(err, ShouldBeNil)
So(err, ShouldEqual, nil)

got = append(got, m)
}
if tc.wantError != nil {
So(sols.Err(), ShouldNotBeNil)
So(sols.Err(), ShouldNotEqual, nil)
So(sols.Err().Error(), ShouldEqual, tc.wantError.Error())
} else {
So(sols.Err(), ShouldBeNil)
So(sols.Err(), ShouldEqual, nil)
So(got, ShouldResemble, tc.wantResult)
}
})
Expand Down
Loading

0 comments on commit ea7b38d

Please sign in to comment.