Skip to content

Commit

Permalink
feat(eibc): fulfill demand orders with authorization from granter acc…
Browse files Browse the repository at this point in the history
…ount (#1326)
  • Loading branch information
zale144 authored Oct 21, 2024
1 parent 6793f65 commit d9b6cb8
Show file tree
Hide file tree
Showing 17 changed files with 2,488 additions and 59 deletions.
3 changes: 2 additions & 1 deletion app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,8 @@ func (a *AppKeepers) InitKeepers(
a.GetSubspace(eibcmoduletypes.ModuleName),
a.AccountKeeper,
a.BankKeeper,
nil,
a.DelayedAckKeeper,
a.RollappKeeper,
)

a.DymNSKeeper = dymnskeeper.NewKeeper(
Expand Down
47 changes: 47 additions & 0 deletions proto/dymensionxyz/dymension/eibc/authz.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
syntax = "proto3";
package dymensionxyz.dymension.eibc;

import "gogoproto/gogo.proto";
import "cosmos/msg/v1/msg.proto";
import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";

option go_package = "github.com/dymensionxyz/dymension/v3/x/eibc/types";

// FulfillOrderAuthorization allows the grantee to fulfill eIBC demand orders from the granter's account.
message FulfillOrderAuthorization {
option (cosmos_proto.implements_interface) = "cosmos.authz.v1beta1.Authorization";

// rollapps is an optional list of rollapp IDs that the grantee can fulfill demand orders from
repeated string rollapps = 1;

// denoms is an optional list of denoms that the grantee can fulfill demand orders for
repeated string denoms = 2;

// min_lp_fee_percentage is the minimum fee earning percentage the LP is willing to get from a demand order
cosmos.base.v1beta1.DecProto min_lp_fee_percentage = 3 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.DecProto"
];

// max_price is the optional maximum order price acceptable to the granter
repeated cosmos.base.v1beta1.Coin max_price = 4 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];

// operator_fee_share is the share of the fee earnings willing to give to the operator
cosmos.base.v1beta1.DecProto operator_fee_share = 5 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.DecProto"
];

// settlement_validated is the flag to only fulfill demand orders that have been settlement validated
bool settlement_validated = 6;

// spend_limit is the optional maximum amount of coins that can be spent by the grantee
repeated cosmos.base.v1beta1.Coin spend_limit = 7 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
}
38 changes: 38 additions & 0 deletions proto/dymensionxyz/dymension/eibc/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ syntax = "proto3";
package dymensionxyz.dymension.eibc;

import "cosmos/msg/v1/msg.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "cosmos/base/v1beta1/coin.proto";

option go_package = "github.com/dymensionxyz/dymension/v3/x/eibc/types";

// Msg defines the Msg service.
service Msg {
rpc FulfillOrder(MsgFulfillOrder) returns (MsgFulfillOrderResponse) {}
rpc FulfillOrderAuthorized(MsgFulfillOrderAuthorized) returns (MsgFulfillOrderAuthorizedResponse) {}
rpc UpdateDemandOrder(MsgUpdateDemandOrder) returns (MsgUpdateDemandOrderResponse) {}
}

Expand All @@ -25,6 +29,40 @@ message MsgFulfillOrder {
// MsgFulfillOrderResponse defines the FulfillOrder response type.
message MsgFulfillOrderResponse {}

// MsgFulfillOrderAuthorized defines the FulfillOrderAuthorized request type.
message MsgFulfillOrderAuthorized {
option (cosmos.msg.v1.signer) = "lp_address";
// order_id is the unique identifier of the order to be fulfilled.
string order_id = 1;
// rollapp_id is the unique identifier of the rollapp that the order is associated with.
string rollapp_id = 2;
// price is the price of the demand order
repeated cosmos.base.v1beta1.Coin price = 3 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"
];
// operator_address is the bech32-encoded address of the account which is authorized to fulfill the demand order.
string operator_address = 4;
// lp_address is the bech32-encoded address of the account which the authorization was granted from.
// This account will receive the price amount at the finalization phase.
string lp_address = 5;
// operator_fee_address is an optional bech32-encoded address of an account that would collect the operator_fee_part
// if it's empty, the operator_fee_part will go to the operator_address
string operator_fee_address = 6;
// expected_fee is the nominal fee set in the order.
string expected_fee = 7;
// operator_fee_share is the share of the fee earnings that goes to the operator
// it will be deduced from the fee of the demand order and paid out immediately
cosmos.base.v1beta1.DecProto operator_fee_share = 8 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.DecProto"
];
// settlement_validated signals if the block behind the demand order needs to be "settlement validated" or not
bool settlement_validated = 9;
}

message MsgFulfillOrderAuthorizedResponse {}

message MsgUpdateDemandOrder {
option (cosmos.msg.v1.signer) = "owner_address";
// owner_address is the bech32-encoded address of the account owns the order.
Expand Down
4 changes: 3 additions & 1 deletion testutil/keeper/eibc.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import (
storetypes "github.com/cosmos/cosmos-sdk/store/types"
sdk "github.com/cosmos/cosmos-sdk/types"
typesparams "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/stretchr/testify/require"

"github.com/dymensionxyz/dymension/v3/x/eibc/keeper"
"github.com/dymensionxyz/dymension/v3/x/eibc/types"
"github.com/stretchr/testify/require"
)

func EibcKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
Expand Down Expand Up @@ -44,6 +45,7 @@ func EibcKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
nil,
nil,
nil,
nil,
)

ctx := sdk.NewContext(stateStore, cometbftproto.Header{}, false, log.NewNopLogger())
Expand Down
162 changes: 162 additions & 0 deletions x/eibc/client/cli/authz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package cli

import (
"fmt"
"strings"
"time"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/authz"
"github.com/spf13/cobra"

"github.com/dymensionxyz/dymension/v3/x/eibc/types"
)

const (
FlagSpendLimit = "spend-limit"
FlagExpiration = "expiration"
FlagRollapps = "rollapps"
FlagDenoms = "denoms"
FlagMinLPFeePercentage = "min-lp-fee-percentage"
FlagMaxPrice = "max-price"
FlagOperatorFeePart = "operator-fee-part"
FlagSettlementValidated = "settlement-validated"
)

// NewCmdGrantAuthorization returns a CLI command handler for creating a MsgGrant transaction.
func NewCmdGrantAuthorization() *cobra.Command {
cmd := &cobra.Command{
Use: "grant <grantee> --from <granter>",
Short: "Grant authorization to an address",
Long: strings.TrimSpace(
fmt.Sprintf(`create a new grant authorization to an address to execute a transaction on your behalf:
Examples:
$ %s tx %s grant dym1skjw.. --spend-limit=1000stake... --from=dym1skl..`, version.AppName, authz.ModuleName),
),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return fmt.Errorf("failed to get client context: %w", err)
}

grantee, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return fmt.Errorf("failed to parse grantee address: %w", err)
}

rollapps, err := cmd.Flags().GetStringSlice(FlagRollapps)
if err != nil {
return fmt.Errorf("failed to get rollapps: %w", err)
}
denoms, err := cmd.Flags().GetStringSlice(FlagDenoms)
if err != nil {
return fmt.Errorf("failed to get denoms: %w", err)
}

minFeeStr, err := cmd.Flags().GetString(FlagMinLPFeePercentage)
if err != nil {
return fmt.Errorf("failed to get min fee: %w", err)
}

minFeePercDec, err := sdk.NewDecFromStr(minFeeStr)
if err != nil {
return fmt.Errorf("invalid min lp fee percentage: %w", err)
}
minLPFeePercent := sdk.DecProto{Dec: minFeePercDec}

maxPriceStr, err := cmd.Flags().GetString(FlagMaxPrice)
if err != nil {
return fmt.Errorf("failed to get max price: %w", err)
}

maxPrice, err := sdk.ParseCoinsNormalized(maxPriceStr)
if err != nil {
return fmt.Errorf("failed to parse max price: %w", err)
}

fulfillerFeePartStr, err := cmd.Flags().GetString(FlagOperatorFeePart)
if err != nil {
return fmt.Errorf("failed to get fulfiller fee part: %w", err)
}

fulfillerFeePartDec, err := sdk.NewDecFromStr(fulfillerFeePartStr)
if err != nil {
return fmt.Errorf("failed to parse fulfiller fee part: %w", err)
}
fulfillerFeePart := sdk.DecProto{Dec: fulfillerFeePartDec}

settlementValidated, err := cmd.Flags().GetBool(FlagSettlementValidated)
if err != nil {
return fmt.Errorf("failed to get settlement validated: %w", err)
}

limit, err := cmd.Flags().GetString(FlagSpendLimit)
if err != nil {
return fmt.Errorf("failed to get spend limit: %w", err)
}

var spendLimit sdk.Coins
if limit != "" {
spendLimit, err = sdk.ParseCoinsNormalized(limit)
if err != nil {
return fmt.Errorf("failed to parse spend limit: %w", err)
}

if !spendLimit.IsAllPositive() {
return fmt.Errorf("spend-limit should be greater than zero")
}
}

authorization := types.NewFulfillOrderAuthorization(
rollapps,
denoms,
minLPFeePercent,
maxPrice,
fulfillerFeePart,
settlementValidated,
spendLimit,
)

expire, err := getExpireTime(cmd)
if err != nil {
return fmt.Errorf("failed to get expiration time: %w", err)
}

msg, err := authz.NewMsgGrant(clientCtx.GetFromAddress(), grantee, authorization, expire)
if err != nil {
return fmt.Errorf("failed to create MsgGrant: %w", err)
}

return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}

flags.AddTxFlagsToCmd(cmd)
cmd.Flags().StringSlice(FlagRollapps, []string{}, "An array of Rollapp IDs allowed")
cmd.Flags().StringSlice(FlagDenoms, []string{}, "An array of denoms allowed to use")
cmd.Flags().String(FlagSpendLimit, "", "An array of Coins allowed to spend")
cmd.Flags().Bool(FlagSettlementValidated, false, "Settlement validated flag")
cmd.Flags().String(FlagMinLPFeePercentage, "", "Minimum fee")
cmd.Flags().String(FlagMaxPrice, "", "Maximum price")
cmd.Flags().String(FlagOperatorFeePart, "", "Fulfiller fee part")
cmd.Flags().Int64(FlagExpiration, 0, "Expire time as Unix timestamp. Set zero (0) for no expiry. Default is 0.")
return cmd
}

func getExpireTime(cmd *cobra.Command) (*time.Time, error) {
exp, err := cmd.Flags().GetInt64(FlagExpiration)
if err != nil {
return nil, err
}
if exp == 0 {
return nil, nil
}
e := time.Unix(exp, 0)
return &e, nil
}
Loading

0 comments on commit d9b6cb8

Please sign in to comment.