generated from okp4/template-oss
-
Notifications
You must be signed in to change notification settings - Fork 128
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #297 from okp4/feat/logic-bech32
🧠 Logic: 📪 `bech32_address/2` predicate
- Loading branch information
Showing
3 changed files
with
260 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
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" | ||
) | ||
|
||
// Bech32Address is a predicate that convert a bech32 encoded string into base64 bytes and give the address prefix, or | ||
// convert a prefix (HRP) and base64 encoded bytes to bech32 encoded string. The signature is as follows: | ||
// | ||
// bech32_address(-Address, +Bech32) | ||
// bech32_address(+Address, -Bech32) | ||
// bech32_address(+Address, +Bech32) | ||
// | ||
// where: | ||
// - Address is a pair of, HRP (Human-Readable Part) containing the address prefix and the list of integers | ||
// between 0 and 255 (byte) that represent the base64 encoded bech32 address string. Represented like this : -(HRP, Address) | ||
// - Bech32 is an Atom or string representing the bech32 encoded string address | ||
// | ||
// # Example: | ||
// | ||
// - Convert the given bech32 address into base64 encoded byte by unify the prefix of given address (Hrp) and | ||
// the base64 encoded value (Address). | ||
// | ||
// bech32_address(-(Hrp, Address), 'okp415wn30a9z4uc692s0kkx5fp5d4qfr3ac7sj9dqn'). | ||
// | ||
// - Convert the given pair of HRP and base64 encoded address byte by unify the Bech32 string encoded value. | ||
// | ||
// bech32_address(-('okp4', [163,167,23,244,162,175,49,162,170,15,181,141,68,134,141,168,18,56,247,30]), Bech32). | ||
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 := 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: invalid address type: %T, should be Compound (Hrp, Address)", addressPair)) | ||
} | ||
}) | ||
} | ||
|
||
func addressPairToBech32(addressPair engine.Compound, env *engine.Env) (string, error) { | ||
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") | ||
} | ||
|
||
iter := engine.ListIterator{List: a, Env: env} | ||
data, err := ListToBytes(iter, env) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to convert term to bytes list: %w", 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) | ||
if err != nil { | ||
return "", fmt.Errorf("failed to convert base64 encoded address to bech32 string encoded: %w", err) | ||
} | ||
|
||
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)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 to 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"), | ||
wantSuccess: false, | ||
}, | ||
{ | ||
query: `bech32_address(-('okp4', hey(2)), Bech32).`, | ||
wantError: fmt.Errorf("bech32_address/2: address should be a List of bytes"), | ||
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: invalid address type: engine.Variable, should be Compound (Hrp, Address)"), | ||
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) | ||
} | ||
} | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
} | ||
}) | ||
} |