Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🧠 Logic: 📪 bech32_address/2 predicate #297

Merged
merged 9 commits into from
Feb 28, 2023
1 change: 1 addition & 0 deletions x/logic/interpreter/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ var Registry = map[string]RegistryEntry{
"did_components/2": {predicate.DIDComponents, 1},
"sha_hash/2": {predicate.SHAHash, 1},
"hex_bytes/2": {predicate.HexBytes, 1},
"bech32_address/2": {predicate.Bech32Address, 1},
}

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

import (
"context"
"fmt"

bech322 "github.com/cosmos/cosmos-sdk/types/bech32"
"github.com/ichiban/prolog/engine"
"github.com/okp4/okp4d/x/logic/util"
)

func Bech32Address(vm *engine.VM, address, bech32 engine.Term, cont engine.Cont, env *engine.Env) *engine.Promise {
bdeneux marked this conversation as resolved.
Show resolved Hide resolved
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 convert bech32 encoded string to base64: %w", err))
bdeneux marked this conversation as resolved.
Show resolved Hide resolved
}
pair := AtomPair.Apply(util.StringToTerm(h), BytesToList(a))
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))
}

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, util.StringToTerm(bech32Decoded), cont, env)
default:
return engine.Error(fmt.Errorf("bech32_address/2: you should give at least on instantiated value (Address or Bech32)"))
bdeneux marked this conversation as resolved.
Show resolved Hide resolved
}
})
}

func AddressPairToBech32(addressPair engine.Compound, env *engine.Env) (string, error) {
bdeneux marked this conversation as resolved.
Show resolved Hide resolved
if addressPair.Functor() != AtomPair || addressPair.Arity() != 2 {
return "", fmt.Errorf("address should be a Pair '-(Hrp, Address)'")
}

switch a := env.Resolve(addressPair.Arg(1)).(type) {
case engine.Compound:
if a.Arity() != 2 || a.Functor().String() != "." {
return "", fmt.Errorf("address should be a List of bytes, give %s/%d", a.Functor().String(), a.Arity())
bdeneux marked this conversation as resolved.
Show resolved Hide resolved
}

iter := engine.ListIterator{List: a, Env: env}
data, err := ListToBytes(iter, env)
if err != nil {
return "", fmt.Errorf("failed convert term to bytes list: %w", err)
bdeneux marked this conversation as resolved.
Show resolved Hide resolved
}
hrp, ok := env.Resolve(addressPair.Arg(0)).(engine.Atom)
if !ok {
return "", fmt.Errorf("HRP should be instantiated when trying convert bytes to bech32")
bdeneux marked this conversation as resolved.
Show resolved Hide resolved
}
b, err := bech322.ConvertAndEncode(hrp.String(), data)
if err != nil {
return "", fmt.Errorf("failed convert base64 encoded address to bech32 string encoded: %w", err)
bdeneux marked this conversation as resolved.
Show resolved Hide resolved
}

return b, nil
default:
return "", fmt.Errorf("address should be a Pair with a List of bytes in arity 2, give %T", addressPair.Arg(1))
}
}
168 changes: 168 additions & 0 deletions x/logic/predicate/address_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//nolint:gocognit,lll
package predicate

import (
"fmt"
"testing"

"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ichiban/prolog/engine"
"github.com/okp4/okp4d/x/logic/testutil"
"github.com/okp4/okp4d/x/logic/types"
. "github.com/smartystreets/goconvey/convey"
"github.com/tendermint/tendermint/libs/log"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmdb "github.com/tendermint/tm-db"
)

func TestBech32(t *testing.T) {
Convey("Given a test cases", t, func() {
cases := []struct {
program string
query string
wantResult []types.TermResults
wantError error
wantSuccess bool
}{
{
query: `bech32_address(-(Hrp, Address), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{
"Hrp": "okp4",
"Address": "[163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]",
}},
wantSuccess: true,
},
{
query: `bech32_address(Address, 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{
"Address": "okp4-[163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]",
}},
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"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', Address), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{
"Address": "[163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]",
}},
wantSuccess: true,
},
{
query: `bech32_address(-('okp5', Address), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantSuccess: false,
},
{
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).`,
wantResult: []types.TermResults{{
"Bech32": "okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn",
}},
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]), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{}},
wantSuccess: true,
},
{
query: `bech32_address(-(Hrp, [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{"Hrp": "okp4"}},
wantSuccess: true,
},
{
query: `bech32_address(-(Hrp, [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn').`,
wantResult: []types.TermResults{{"Hrp": "okp4"}},
wantSuccess: true,
},
{
query: `bech32_address(foo(Bar), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: address should be a Pair '-(Hrp, Address)'"),
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 convert term to bytes list: invalid term type in list engine.Atom, only integer allowed"),
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 when trying convert bytes to bech32"),
wantSuccess: false,
},
{
query: `bech32_address(-('okp4', hey(2)), Bech32).`,
wantError: fmt.Errorf("bech32_address/2: address should be a List of bytes, give hey/1"),
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, give engine.Variable"),
wantSuccess: false,
},
{
query: `bech32_address(Address, Bech32).`,
wantError: fmt.Errorf("bech32_address/2: you should give at least on instantiated value (Address or Bech32)"),
wantSuccess: false,
},
}
for nc, tc := range cases {
Convey(fmt.Sprintf("Given the query #%d: %s", nc, tc.query), func() {
Convey("and a context", func() {
db := tmdb.NewMemDB()
stateStore := store.NewCommitMultiStore(db)
ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger())

Convey("and a vm", func() {
interpreter := testutil.NewInterpreterMust(ctx)
interpreter.Register2(engine.NewAtom("bech32_address"), Bech32Address)

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

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(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)

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

if tc.wantSuccess {
So(len(got), ShouldBeGreaterThan, 0)
So(len(got), ShouldEqual, len(tc.wantResult))
for iGot, resultGot := range got {
for varGot, termGot := range resultGot {
So(testutil.ReindexUnknownVariables(termGot), ShouldEqual, tc.wantResult[iGot][varGot])
}
}
} else {
So(len(got), ShouldEqual, 0)
}
}
})
})
})
})
})
})
}
})
}