diff --git a/x/tokenfactory/ante/before_send_hook.go b/x/tokenfactory/ante/before_send_hook.go new file mode 100644 index 0000000..e710d33 --- /dev/null +++ b/x/tokenfactory/ante/before_send_hook.go @@ -0,0 +1,79 @@ +package decorators + +import ( + "fmt" + + authz "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/keeper" + tokenfactorytypes "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type MsgBeforeSendHook struct { + TokenFactoryKeeper keeper.Keeper +} + +func NewMsgBeforeSendHook(k keeper.Keeper) MsgBeforeSendHook { + return MsgBeforeSendHook{ + TokenFactoryKeeper: k, + } +} + +func (mfd MsgBeforeSendHook) AnteHandle(ctx sdk.Context, tx sdk.Tx, simulate bool, next sdk.AnteHandler) (newCtx sdk.Context, err error) { + if err := mfd.applyDenomHookIfApplicable(tx.GetMsgs()); err != nil { + return ctx, err + } + + return next(ctx, tx, simulate) +} + +func (mfd MsgBeforeSendHook) applyDenomHookIfApplicable(msgs []sdk.Msg) error { + for _, msg := range msgs { + // Check if an authz message, loop through all inner messages, and recursively call this function + if execMsg, ok := msg.(*authz.MsgExec); ok { + msgs, err := execMsg.GetMessages() + if err != nil { + return err + } + + // Recursively call this function with the inner messages + if err = mfd.applyDenomHookIfApplicable(msgs); err != nil { + return err + } + } + + // if it is a bank message, perform the action if it's a tokenfactory denom + if m, ok := msg.(*banktypes.MsgSend); ok { + // mfd.performAction(m) + + sender := m.FromAddress + recipient := m.ToAddress + coins := m.Amount + + for _, coin := range coins { + coin := coin + + // validate is a tokenfactory denom + _, _, err := tokenfactorytypes.DeconstructDenom(coin.Denom) + if err != nil { + continue + } + + // TODO: see if hooks is registered + + // This is a validate tokenfactory denom being sent, check if it is registered for hooks + fireEvent(sender, recipient, coin) + } + } + } + + return nil +} + +func fireEvent(sender, recipient string, coin sdk.Coin) error { + // Perform the SudoMsg execute if it is registered. + fmt.Printf("Fire event for %s -> %s: %s\n", sender, recipient, coin) + return nil +} diff --git a/x/tokenfactory/ante/before_send_test.go b/x/tokenfactory/ante/before_send_test.go new file mode 100644 index 0000000..f6ed638 --- /dev/null +++ b/x/tokenfactory/ante/before_send_test.go @@ -0,0 +1,96 @@ +package decorators_test + +import ( + "fmt" + "os" + "testing" + + sdkmath "cosmossdk.io/math" + + "github.com/CosmWasm/wasmd/x/wasm/keeper" + "github.com/cometbft/cometbft/crypto/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/strangelove-ventures/tokenfactory/app" + "github.com/stretchr/testify/require" + + ante "github.com/strangelove-ventures/tokenfactory/x/tokenfactory/ante" +) + +var ( + EmptyAnte = func(ctx sdk.Context, _ sdk.Tx, _ bool) (sdk.Context, error) { + return ctx, nil + } +) + +func TestBeforeSend(t *testing.T) { + fromAddr := createAccount() + toAddr := createAccount() + + // Setup chain and single ante + ctx, chain := app.Setup(t) + anteHandler := ante.NewMsgBeforeSendHook(chain.TokenFactoryKeeper) + + // Create new token + token, err := chain.TokenFactoryKeeper.CreateDenom(ctx, fromAddr.String(), "bitcoin") + require.NoError(t, err) + require.NotEmpty(t, token) + fmt.Println("token", token) + + // Mint new coins to the fromAddr + initCoins := newCoins(token, 100_000) + err = chain.BankKeeper.MintCoins(ctx, "mint", initCoins) + require.NoError(t, err) + err = chain.BankKeeper.SendCoinsFromModuleToAccount(ctx, "mint", fromAddr, initCoins) + require.NoError(t, err) + + // Setup the hooks contract + // TODO: change me (and the readme) to proper contract sudo bindings + codeID := storeHooksContract(t, ctx, chain, fromAddr) + cAddr := instantiateBeforeSendContract(t, ctx, chain, fromAddr, codeID) + fmt.Println(cAddr.String()) + + // TODO: register contract -> token (match osmosis spec) + + // TODO: validate execution + bankSendMsg := banktypes.MsgSend{ + FromAddress: fromAddr.String(), + ToAddress: toAddr.String(), + Amount: newCoins(token, 117), + } + + if _, err := anteHandler.AnteHandle(ctx, NewMockTx(&bankSendMsg), false, EmptyAnte); err != nil { + t.Fatalf("unexpected error: %v", err) + } + +} + +func storeHooksContract(t *testing.T, ctx sdk.Context, app *app.TokenFactoryApp, addr sdk.AccAddress) uint64 { + wasmCode, err := os.ReadFile("../bindings/testdata/hooks.wasm") + require.NoError(t, err) + + contractKeeper := keeper.NewDefaultPermissionKeeper(app.WasmKeeper) + codeID, _, err := contractKeeper.Create(ctx, addr, wasmCode, nil) + require.NoError(t, err) + + return codeID +} + +func instantiateBeforeSendContract(t *testing.T, ctx sdk.Context, app *app.TokenFactoryApp, funder sdk.AccAddress, codeID uint64) sdk.AccAddress { + initMsgBz := []byte("{}") + contractKeeper := keeper.NewDefaultPermissionKeeper(app.WasmKeeper) + + addr, _, err := contractKeeper.Instantiate(ctx, codeID, funder, funder, initMsgBz, "hooks contract", nil) + require.NoError(t, err) + + return addr +} + +func createAccount() sdk.AccAddress { + pk1 := ed25519.GenPrivKey().PubKey() + return sdk.AccAddress(pk1.Address()) +} + +func newCoins(token string, amt int64) sdk.Coins { + return sdk.NewCoins(sdk.NewCoin(token, sdkmath.NewInt(amt))) +} diff --git a/x/tokenfactory/ante/mocks_test.go b/x/tokenfactory/ante/mocks_test.go new file mode 100644 index 0000000..7c6cf30 --- /dev/null +++ b/x/tokenfactory/ante/mocks_test.go @@ -0,0 +1,28 @@ +package decorators_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + protov2 "google.golang.org/protobuf/proto" +) + +type MockTx struct { + msgs []sdk.Msg +} + +func NewMockTx(msgs ...sdk.Msg) MockTx { + return MockTx{ + msgs: msgs, + } +} + +func (tx MockTx) GetMsgs() []sdk.Msg { + return tx.msgs +} + +func (tx MockTx) GetMsgsV2() ([]protov2.Message, error) { + return nil, nil +} + +func (tx MockTx) ValidateBasic() error { + return nil +} diff --git a/x/tokenfactory/bindings/testdata/README.md b/x/tokenfactory/bindings/testdata/README.md index 221c651..6cefc5a 100644 --- a/x/tokenfactory/bindings/testdata/README.md +++ b/x/tokenfactory/bindings/testdata/README.md @@ -1,5 +1,14 @@ -# token-reflect-contract +# Contracts + +## token-reflect-contract Commit: 834bb36573fb21c74f8e78207308d9001df127a2 + + +## BeforeSendHook + + + +Commit: d0d2ac541975f053b448c806e184478d920ac569 \ No newline at end of file diff --git a/x/tokenfactory/bindings/testdata/hooks.wasm b/x/tokenfactory/bindings/testdata/hooks.wasm new file mode 100644 index 0000000..e2d99f7 Binary files /dev/null and b/x/tokenfactory/bindings/testdata/hooks.wasm differ