Merge pull request #5432 from filecoin-project/refac/send
Refactor send command for better testability
This commit is contained in:
commit
7721ea20ba
@ -32,6 +32,8 @@ import (
|
|||||||
"github.com/filecoin-project/lotus/node/modules/dtypes"
|
"github.com/filecoin-project/lotus/node/modules/dtypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:generate go run github.com/golang/mock/mockgen -destination=mocks/mock_full.go -package=mocks . FullNode
|
||||||
|
|
||||||
// FullNode API is a low-level interface to the Filecoin network full node
|
// FullNode API is a low-level interface to the Filecoin network full node
|
||||||
type FullNode interface {
|
type FullNode interface {
|
||||||
Common
|
Common
|
||||||
|
2972
api/mocks/mock_full.go
Normal file
2972
api/mocks/mock_full.go
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,5 +3,6 @@
|
|||||||
package build
|
package build
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "github.com/golang/mock/mockgen"
|
||||||
_ "github.com/whyrusleeping/bencher"
|
_ "github.com/whyrusleeping/bencher"
|
||||||
)
|
)
|
||||||
|
13
cli/cmd.go
13
cli/cmd.go
@ -207,6 +207,19 @@ func GetFullNodeAPI(ctx *cli.Context) (api.FullNode, jsonrpc.ClientCloser, error
|
|||||||
return client.NewFullNodeRPC(ctx.Context, addr, headers)
|
return client.NewFullNodeRPC(ctx.Context, addr, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetFullNodeServices(ctx *cli.Context) (ServicesAPI, error) {
|
||||||
|
if tn, ok := ctx.App.Metadata["test-services"]; ok {
|
||||||
|
return tn.(ServicesAPI), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
api, c, err := GetFullNodeAPI(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ServicesImpl{api: api, closer: c}, nil
|
||||||
|
}
|
||||||
|
|
||||||
type GetStorageMinerOptions struct {
|
type GetStorageMinerOptions struct {
|
||||||
PreferHttp bool
|
PreferHttp bool
|
||||||
}
|
}
|
||||||
|
144
cli/send.go
144
cli/send.go
@ -1,22 +1,17 @@
|
|||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
cbg "github.com/whyrusleeping/cbor-gen"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
"github.com/filecoin-project/go-state-types/abi"
|
"github.com/filecoin-project/go-state-types/abi"
|
||||||
|
|
||||||
"github.com/filecoin-project/lotus/api"
|
|
||||||
"github.com/filecoin-project/lotus/chain/actors/builtin"
|
"github.com/filecoin-project/lotus/chain/actors/builtin"
|
||||||
"github.com/filecoin-project/lotus/chain/stmgr"
|
|
||||||
"github.com/filecoin-project/lotus/chain/types"
|
"github.com/filecoin-project/lotus/chain/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -72,15 +67,16 @@ var sendCmd = &cli.Command{
|
|||||||
return ShowHelp(cctx, fmt.Errorf("'send' expects two arguments, target and amount"))
|
return ShowHelp(cctx, fmt.Errorf("'send' expects two arguments, target and amount"))
|
||||||
}
|
}
|
||||||
|
|
||||||
api, closer, err := GetFullNodeAPI(cctx)
|
srv, err := GetFullNodeServices(cctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer closer()
|
defer srv.Close() //nolint:errcheck
|
||||||
|
|
||||||
ctx := ReqContext(cctx)
|
ctx := ReqContext(cctx)
|
||||||
|
var params SendParams
|
||||||
|
|
||||||
toAddr, err := address.NewFromString(cctx.Args().Get(0))
|
params.To, err = address.NewFromString(cctx.Args().Get(0))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ShowHelp(cctx, fmt.Errorf("failed to parse target address: %w", err))
|
return ShowHelp(cctx, fmt.Errorf("failed to parse target address: %w", err))
|
||||||
}
|
}
|
||||||
@ -89,123 +85,75 @@ var sendCmd = &cli.Command{
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ShowHelp(cctx, fmt.Errorf("failed to parse amount: %w", err))
|
return ShowHelp(cctx, fmt.Errorf("failed to parse amount: %w", err))
|
||||||
}
|
}
|
||||||
|
params.Val = abi.TokenAmount(val)
|
||||||
|
|
||||||
var fromAddr address.Address
|
if from := cctx.String("from"); from != "" {
|
||||||
if from := cctx.String("from"); from == "" {
|
|
||||||
defaddr, err := api.WalletDefaultAddress(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fromAddr = defaddr
|
|
||||||
} else {
|
|
||||||
addr, err := address.NewFromString(from)
|
addr, err := address.NewFromString(from)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fromAddr = addr
|
params.From = addr
|
||||||
}
|
}
|
||||||
|
|
||||||
gp, err := types.BigFromString(cctx.String("gas-premium"))
|
if cctx.IsSet("gas-premium") {
|
||||||
if err != nil {
|
gp, err := types.BigFromString(cctx.String("gas-premium"))
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
gfc, err := types.BigFromString(cctx.String("gas-feecap"))
|
}
|
||||||
if err != nil {
|
params.GasPremium = &gp
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
method := abi.MethodNum(cctx.Uint64("method"))
|
if cctx.IsSet("gas-feecap") {
|
||||||
|
gfc, err := types.BigFromString(cctx.String("gas-feecap"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
params.GasFeeCap = &gfc
|
||||||
|
}
|
||||||
|
|
||||||
|
if cctx.IsSet("gas-limit") {
|
||||||
|
limit := cctx.Int64("gas-limit")
|
||||||
|
params.GasLimit = &limit
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Method = abi.MethodNum(cctx.Uint64("method"))
|
||||||
|
|
||||||
var params []byte
|
|
||||||
if cctx.IsSet("params-json") {
|
if cctx.IsSet("params-json") {
|
||||||
decparams, err := decodeTypedParams(ctx, api, toAddr, method, cctx.String("params-json"))
|
decparams, err := srv.DecodeTypedParamsFromJSON(ctx, params.To, params.Method, cctx.String("params-json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decode json params: %w", err)
|
return fmt.Errorf("failed to decode json params: %w", err)
|
||||||
}
|
}
|
||||||
params = decparams
|
params.Params = decparams
|
||||||
}
|
}
|
||||||
if cctx.IsSet("params-hex") {
|
if cctx.IsSet("params-hex") {
|
||||||
if params != nil {
|
if params.Params != nil {
|
||||||
return fmt.Errorf("can only specify one of 'params-json' and 'params-hex'")
|
return fmt.Errorf("can only specify one of 'params-json' and 'params-hex'")
|
||||||
}
|
}
|
||||||
decparams, err := hex.DecodeString(cctx.String("params-hex"))
|
decparams, err := hex.DecodeString(cctx.String("params-hex"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to decode hex params: %w", err)
|
return fmt.Errorf("failed to decode hex params: %w", err)
|
||||||
}
|
}
|
||||||
params = decparams
|
params.Params = decparams
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := &types.Message{
|
params.Force = cctx.Bool("force")
|
||||||
From: fromAddr,
|
|
||||||
To: toAddr,
|
|
||||||
Value: types.BigInt(val),
|
|
||||||
GasPremium: gp,
|
|
||||||
GasFeeCap: gfc,
|
|
||||||
GasLimit: cctx.Int64("gas-limit"),
|
|
||||||
Method: method,
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cctx.Bool("force") {
|
|
||||||
// Funds insufficient check
|
|
||||||
fromBalance, err := api.WalletBalance(ctx, msg.From)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
totalCost := types.BigAdd(types.BigMul(msg.GasFeeCap, types.NewInt(uint64(msg.GasLimit))), msg.Value)
|
|
||||||
|
|
||||||
if fromBalance.LessThan(totalCost) {
|
|
||||||
fmt.Printf("WARNING: From balance %s less than total cost %s\n", types.FIL(fromBalance), types.FIL(totalCost))
|
|
||||||
return fmt.Errorf("--force must be specified for this action to have an effect; you have been warned")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cctx.IsSet("nonce") {
|
if cctx.IsSet("nonce") {
|
||||||
msg.Nonce = cctx.Uint64("nonce")
|
n := cctx.Uint64("nonce")
|
||||||
sm, err := api.WalletSignMessage(ctx, fromAddr, msg)
|
params.Nonce = &n
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = api.MpoolPush(ctx, sm)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println(sm.Cid())
|
|
||||||
} else {
|
|
||||||
sm, err := api.MpoolPushMessage(ctx, msg, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println(sm.Cid())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
msgCid, err := srv.Send(ctx, params)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrSendBalanceTooLow) {
|
||||||
|
return fmt.Errorf("--force must be specified for this action to have an effect; you have been warned: %w", err)
|
||||||
|
}
|
||||||
|
return xerrors.Errorf("executing send: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(cctx.App.Writer, "%s\n", msgCid)
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeTypedParams(ctx context.Context, fapi api.FullNode, to address.Address, method abi.MethodNum, paramstr string) ([]byte, error) {
|
|
||||||
act, err := fapi.StateGetActor(ctx, to, types.EmptyTSK)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
methodMeta, found := stmgr.MethodsMap[act.Code][method]
|
|
||||||
if !found {
|
|
||||||
return nil, fmt.Errorf("method %d not found on actor %s", method, act.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
p := reflect.New(methodMeta.Params.Elem()).Interface().(cbg.CBORMarshaler)
|
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(paramstr), p); err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshaling input into params type: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := new(bytes.Buffer)
|
|
||||||
if err := p.MarshalCBOR(buf); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
128
cli/send_test.go
Normal file
128
cli/send_test.go
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/go-address"
|
||||||
|
"github.com/filecoin-project/go-state-types/abi"
|
||||||
|
types "github.com/filecoin-project/lotus/chain/types"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
cid "github.com/ipfs/go-cid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
ucli "github.com/urfave/cli/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var arbtCid = (&types.Message{
|
||||||
|
From: mustAddr(address.NewIDAddress(2)),
|
||||||
|
To: mustAddr(address.NewIDAddress(1)),
|
||||||
|
Value: types.NewInt(1000),
|
||||||
|
}).Cid()
|
||||||
|
|
||||||
|
func mustAddr(a address.Address, err error) address.Address {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockApp(t *testing.T, cmd *ucli.Command) (*ucli.App, *MockServicesAPI, *bytes.Buffer, func()) {
|
||||||
|
app := ucli.NewApp()
|
||||||
|
app.Commands = ucli.Commands{cmd}
|
||||||
|
app.Setup()
|
||||||
|
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
mockSrvcs := NewMockServicesAPI(mockCtrl)
|
||||||
|
app.Metadata["test-services"] = mockSrvcs
|
||||||
|
|
||||||
|
buf := &bytes.Buffer{}
|
||||||
|
app.Writer = buf
|
||||||
|
|
||||||
|
return app, mockSrvcs, buf, mockCtrl.Finish
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendCLI(t *testing.T) {
|
||||||
|
oneFil := abi.TokenAmount(types.MustParseFIL("1"))
|
||||||
|
|
||||||
|
t.Run("simple", func(t *testing.T) {
|
||||||
|
app, mockSrvcs, buf, done := newMockApp(t, sendCmd)
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
gomock.InOrder(
|
||||||
|
mockSrvcs.EXPECT().Send(gomock.Any(), SendParams{
|
||||||
|
To: mustAddr(address.NewIDAddress(1)),
|
||||||
|
Val: oneFil,
|
||||||
|
}).Return(arbtCid, nil),
|
||||||
|
mockSrvcs.EXPECT().Close(),
|
||||||
|
)
|
||||||
|
err := app.Run([]string{"lotus", "send", "t01", "1"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, arbtCid.String()+"\n", buf.String())
|
||||||
|
})
|
||||||
|
t.Run("ErrSendBalanceTooLow", func(t *testing.T) {
|
||||||
|
app, mockSrvcs, _, done := newMockApp(t, sendCmd)
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
gomock.InOrder(
|
||||||
|
mockSrvcs.EXPECT().Send(gomock.Any(), SendParams{
|
||||||
|
To: mustAddr(address.NewIDAddress(1)),
|
||||||
|
Val: oneFil,
|
||||||
|
}).Return(cid.Undef, ErrSendBalanceTooLow),
|
||||||
|
mockSrvcs.EXPECT().Close(),
|
||||||
|
)
|
||||||
|
err := app.Run([]string{"lotus", "send", "t01", "1"})
|
||||||
|
assert.ErrorIs(t, err, ErrSendBalanceTooLow)
|
||||||
|
})
|
||||||
|
t.Run("generic-err-is-forwarded", func(t *testing.T) {
|
||||||
|
app, mockSrvcs, _, done := newMockApp(t, sendCmd)
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
errMark := errors.New("something")
|
||||||
|
gomock.InOrder(
|
||||||
|
mockSrvcs.EXPECT().Send(gomock.Any(), SendParams{
|
||||||
|
To: mustAddr(address.NewIDAddress(1)),
|
||||||
|
Val: oneFil,
|
||||||
|
}).Return(cid.Undef, errMark),
|
||||||
|
mockSrvcs.EXPECT().Close(),
|
||||||
|
)
|
||||||
|
err := app.Run([]string{"lotus", "send", "t01", "1"})
|
||||||
|
assert.ErrorIs(t, err, errMark)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("from-specific", func(t *testing.T) {
|
||||||
|
app, mockSrvcs, buf, done := newMockApp(t, sendCmd)
|
||||||
|
defer done()
|
||||||
|
|
||||||
|
gomock.InOrder(
|
||||||
|
mockSrvcs.EXPECT().Send(gomock.Any(), SendParams{
|
||||||
|
To: mustAddr(address.NewIDAddress(1)),
|
||||||
|
From: mustAddr(address.NewIDAddress(2)),
|
||||||
|
Val: oneFil,
|
||||||
|
}).Return(arbtCid, nil),
|
||||||
|
mockSrvcs.EXPECT().Close(),
|
||||||
|
)
|
||||||
|
err := app.Run([]string{"lotus", "send", "--from=t02", "t01", "1"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, arbtCid.String()+"\n", buf.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nonce-specific", func(t *testing.T) {
|
||||||
|
app, mockSrvcs, buf, done := newMockApp(t, sendCmd)
|
||||||
|
defer done()
|
||||||
|
zero := uint64(0)
|
||||||
|
|
||||||
|
gomock.InOrder(
|
||||||
|
mockSrvcs.EXPECT().Send(gomock.Any(), SendParams{
|
||||||
|
To: mustAddr(address.NewIDAddress(1)),
|
||||||
|
Nonce: &zero,
|
||||||
|
Val: oneFil,
|
||||||
|
}).Return(arbtCid, nil),
|
||||||
|
mockSrvcs.EXPECT().Close(),
|
||||||
|
)
|
||||||
|
err := app.Run([]string{"lotus", "send", "--nonce=0", "t01", "1"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, arbtCid.String()+"\n", buf.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
166
cli/services.go
Normal file
166
cli/services.go
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/go-address"
|
||||||
|
"github.com/filecoin-project/go-jsonrpc"
|
||||||
|
"github.com/filecoin-project/go-state-types/abi"
|
||||||
|
"github.com/filecoin-project/lotus/api"
|
||||||
|
"github.com/filecoin-project/lotus/chain/stmgr"
|
||||||
|
types "github.com/filecoin-project/lotus/chain/types"
|
||||||
|
cid "github.com/ipfs/go-cid"
|
||||||
|
cbg "github.com/whyrusleeping/cbor-gen"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go run github.com/golang/mock/mockgen -destination=servicesmock_test.go -package=cli -self_package github.com/filecoin-project/lotus/cli . ServicesAPI
|
||||||
|
|
||||||
|
type ServicesAPI interface {
|
||||||
|
// Sends executes a send given SendParams
|
||||||
|
Send(ctx context.Context, params SendParams) (cid.Cid, error)
|
||||||
|
// DecodeTypedParamsFromJSON takes in information needed to identify a method and converts JSON
|
||||||
|
// parameters to bytes of their CBOR encoding
|
||||||
|
DecodeTypedParamsFromJSON(ctx context.Context, to address.Address, method abi.MethodNum, paramstr string) ([]byte, error)
|
||||||
|
|
||||||
|
// Close ends the session of services and disconnects from RPC, using Services after Close is called
|
||||||
|
// most likely will result in an error
|
||||||
|
// Should not be called concurrently
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServicesImpl struct {
|
||||||
|
api api.FullNode
|
||||||
|
closer jsonrpc.ClientCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServicesImpl) Close() error {
|
||||||
|
if s.closer == nil {
|
||||||
|
return xerrors.Errorf("Services already closed")
|
||||||
|
}
|
||||||
|
s.closer()
|
||||||
|
s.closer = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServicesImpl) DecodeTypedParamsFromJSON(ctx context.Context, to address.Address, method abi.MethodNum, paramstr string) ([]byte, error) {
|
||||||
|
act, err := s.api.StateGetActor(ctx, to, types.EmptyTSK)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
methodMeta, found := stmgr.MethodsMap[act.Code][method]
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("method %d not found on actor %s", method, act.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := reflect.New(methodMeta.Params.Elem()).Interface().(cbg.CBORMarshaler)
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(paramstr), p); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshaling input into params type: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := p.MarshalCBOR(buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendParams struct {
|
||||||
|
To address.Address
|
||||||
|
From address.Address
|
||||||
|
Val abi.TokenAmount
|
||||||
|
|
||||||
|
GasPremium *abi.TokenAmount
|
||||||
|
GasFeeCap *abi.TokenAmount
|
||||||
|
GasLimit *int64
|
||||||
|
|
||||||
|
Nonce *uint64
|
||||||
|
Method abi.MethodNum
|
||||||
|
Params []byte
|
||||||
|
|
||||||
|
Force bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is specialised Send for Send command
|
||||||
|
// There might be room for generic Send that other commands can use to send their messages
|
||||||
|
// We will see
|
||||||
|
|
||||||
|
var ErrSendBalanceTooLow = errors.New("balance too low")
|
||||||
|
|
||||||
|
func (s *ServicesImpl) Send(ctx context.Context, params SendParams) (cid.Cid, error) {
|
||||||
|
if params.From == address.Undef {
|
||||||
|
defaddr, err := s.api.WalletDefaultAddress(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
params.From = defaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := &types.Message{
|
||||||
|
From: params.From,
|
||||||
|
To: params.To,
|
||||||
|
Value: params.Val,
|
||||||
|
|
||||||
|
Method: params.Method,
|
||||||
|
Params: params.Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.GasPremium != nil {
|
||||||
|
msg.GasPremium = *params.GasPremium
|
||||||
|
} else {
|
||||||
|
msg.GasPremium = types.NewInt(0)
|
||||||
|
}
|
||||||
|
if params.GasFeeCap != nil {
|
||||||
|
msg.GasFeeCap = *params.GasFeeCap
|
||||||
|
} else {
|
||||||
|
msg.GasFeeCap = types.NewInt(0)
|
||||||
|
}
|
||||||
|
if params.GasLimit != nil {
|
||||||
|
msg.GasLimit = *params.GasLimit
|
||||||
|
} else {
|
||||||
|
msg.GasLimit = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if !params.Force {
|
||||||
|
// Funds insufficient check
|
||||||
|
fromBalance, err := s.api.WalletBalance(ctx, msg.From)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
totalCost := types.BigAdd(types.BigMul(msg.GasFeeCap, types.NewInt(uint64(msg.GasLimit))), msg.Value)
|
||||||
|
|
||||||
|
if fromBalance.LessThan(totalCost) {
|
||||||
|
return cid.Undef, xerrors.Errorf("From balance %s less than total cost %s: %w", types.FIL(fromBalance), types.FIL(totalCost), ErrSendBalanceTooLow)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Nonce != nil {
|
||||||
|
msg.Nonce = *params.Nonce
|
||||||
|
sm, err := s.api.WalletSignMessage(ctx, params.From, msg)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.api.MpoolPush(ctx, sm)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sm.Cid(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sm, err := s.api.MpoolPushMessage(ctx, msg, nil)
|
||||||
|
if err != nil {
|
||||||
|
return cid.Undef, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sm.Cid(), nil
|
||||||
|
}
|
266
cli/services_send_test.go
Normal file
266
cli/services_send_test.go
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/go-address"
|
||||||
|
"github.com/filecoin-project/go-state-types/big"
|
||||||
|
"github.com/filecoin-project/go-state-types/crypto"
|
||||||
|
"github.com/filecoin-project/lotus/api"
|
||||||
|
"github.com/filecoin-project/lotus/api/mocks"
|
||||||
|
types "github.com/filecoin-project/lotus/chain/types"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
cid "github.com/ipfs/go-cid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type markerKeyType struct{}
|
||||||
|
|
||||||
|
var markerKey = markerKeyType{}
|
||||||
|
|
||||||
|
type contextMatcher struct {
|
||||||
|
marker *int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches returns whether x is a match.
|
||||||
|
func (cm contextMatcher) Matches(x interface{}) bool {
|
||||||
|
ctx, ok := x.(context.Context)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
maybeMarker, ok := ctx.Value(markerKey).(*int)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return cm.marker == maybeMarker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cm contextMatcher) String() string {
|
||||||
|
return fmt.Sprintf("Context with Value(%v/%T, %p)", markerKey, markerKey, cm.marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContextWithMarker(ctx context.Context) (context.Context, gomock.Matcher) {
|
||||||
|
marker := new(int)
|
||||||
|
outCtx := context.WithValue(ctx, markerKey, marker)
|
||||||
|
return outCtx, contextMatcher{marker: marker}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupMockSrvcs(t *testing.T) (*ServicesImpl, *mocks.MockFullNode) {
|
||||||
|
mockCtrl := gomock.NewController(t)
|
||||||
|
|
||||||
|
mockApi := mocks.NewMockFullNode(mockCtrl)
|
||||||
|
|
||||||
|
srvcs := &ServicesImpl{
|
||||||
|
api: mockApi,
|
||||||
|
closer: mockCtrl.Finish,
|
||||||
|
}
|
||||||
|
return srvcs, mockApi
|
||||||
|
}
|
||||||
|
|
||||||
|
func fakeSign(msg *types.Message) *types.SignedMessage {
|
||||||
|
return &types.SignedMessage{
|
||||||
|
Message: *msg,
|
||||||
|
Signature: crypto.Signature{Type: crypto.SigTypeSecp256k1, Data: make([]byte, 32)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeMessageSigner() (*cid.Cid, interface{}) {
|
||||||
|
smCid := cid.Undef
|
||||||
|
return &smCid,
|
||||||
|
func(_ context.Context, msg *types.Message, _ *api.MessageSendSpec) (*types.SignedMessage, error) {
|
||||||
|
sm := fakeSign(msg)
|
||||||
|
smCid = sm.Cid()
|
||||||
|
return sm, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessageMatcher SendParams
|
||||||
|
|
||||||
|
var _ gomock.Matcher = MessageMatcher{}
|
||||||
|
|
||||||
|
// Matches returns whether x is a match.
|
||||||
|
func (mm MessageMatcher) Matches(x interface{}) bool {
|
||||||
|
m, ok := x.(*types.Message)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mm.From != address.Undef && mm.From != m.From {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if mm.To != address.Undef && mm.To != m.To {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if types.BigCmp(mm.Val, m.Value) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mm.Nonce != nil && *mm.Nonce != m.Nonce {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mm.GasPremium != nil && big.Cmp(*mm.GasPremium, m.GasPremium) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if mm.GasPremium == nil && m.GasPremium.Sign() != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mm.GasFeeCap != nil && big.Cmp(*mm.GasFeeCap, m.GasFeeCap) != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if mm.GasFeeCap == nil && m.GasFeeCap.Sign() != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mm.GasLimit != nil && *mm.GasLimit != m.GasLimit {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if mm.GasLimit == nil && m.GasLimit != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// handle rest of options
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// String describes what the matcher matches.
|
||||||
|
func (mm MessageMatcher) String() string {
|
||||||
|
return fmt.Sprintf("%#v", SendParams(mm))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSendService(t *testing.T) {
|
||||||
|
addrGen := address.NewForTestGetter()
|
||||||
|
a1 := addrGen()
|
||||||
|
a2 := addrGen()
|
||||||
|
|
||||||
|
const balance = 10000
|
||||||
|
|
||||||
|
params := SendParams{
|
||||||
|
From: a1,
|
||||||
|
To: a2,
|
||||||
|
Val: types.NewInt(balance - 100),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, ctxM := ContextWithMarker(context.Background())
|
||||||
|
|
||||||
|
t.Run("happy", func(t *testing.T) {
|
||||||
|
params := params
|
||||||
|
srvcs, mockApi := setupMockSrvcs(t)
|
||||||
|
defer srvcs.Close() //nolint:errcheck
|
||||||
|
msgCid, sign := makeMessageSigner()
|
||||||
|
gomock.InOrder(
|
||||||
|
mockApi.EXPECT().WalletBalance(ctxM, params.From).Return(types.NewInt(balance), nil),
|
||||||
|
mockApi.EXPECT().MpoolPushMessage(ctxM, MessageMatcher(params), nil).DoAndReturn(sign),
|
||||||
|
)
|
||||||
|
|
||||||
|
c, err := srvcs.Send(ctx, params)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, *msgCid, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("balance-too-low", func(t *testing.T) {
|
||||||
|
params := params
|
||||||
|
srvcs, mockApi := setupMockSrvcs(t)
|
||||||
|
defer srvcs.Close() //nolint:errcheck
|
||||||
|
gomock.InOrder(
|
||||||
|
mockApi.EXPECT().WalletBalance(ctxM, a1).Return(types.NewInt(balance-200), nil),
|
||||||
|
// no MpoolPushMessage
|
||||||
|
)
|
||||||
|
|
||||||
|
c, err := srvcs.Send(ctx, params)
|
||||||
|
assert.Equal(t, c, cid.Undef)
|
||||||
|
assert.ErrorIs(t, err, ErrSendBalanceTooLow)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("force", func(t *testing.T) {
|
||||||
|
params := params
|
||||||
|
params.Force = true
|
||||||
|
srvcs, mockApi := setupMockSrvcs(t)
|
||||||
|
defer srvcs.Close() //nolint:errcheck
|
||||||
|
msgCid, sign := makeMessageSigner()
|
||||||
|
gomock.InOrder(
|
||||||
|
mockApi.EXPECT().WalletBalance(ctxM, a1).Return(types.NewInt(balance-200), nil).AnyTimes(),
|
||||||
|
mockApi.EXPECT().MpoolPushMessage(ctxM, MessageMatcher(params), nil).DoAndReturn(sign),
|
||||||
|
)
|
||||||
|
|
||||||
|
c, err := srvcs.Send(ctx, params)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, *msgCid, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("default-from", func(t *testing.T) {
|
||||||
|
params := params
|
||||||
|
params.From = address.Undef
|
||||||
|
mm := MessageMatcher(params)
|
||||||
|
mm.From = a1
|
||||||
|
|
||||||
|
srvcs, mockApi := setupMockSrvcs(t)
|
||||||
|
defer srvcs.Close() //nolint:errcheck
|
||||||
|
msgCid, sign := makeMessageSigner()
|
||||||
|
gomock.InOrder(
|
||||||
|
mockApi.EXPECT().WalletDefaultAddress(ctxM).Return(a1, nil),
|
||||||
|
mockApi.EXPECT().WalletBalance(ctxM, a1).Return(types.NewInt(balance), nil),
|
||||||
|
mockApi.EXPECT().MpoolPushMessage(ctxM, mm, nil).DoAndReturn(sign),
|
||||||
|
)
|
||||||
|
|
||||||
|
c, err := srvcs.Send(ctx, params)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, *msgCid, c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("set-nonce", func(t *testing.T) {
|
||||||
|
params := params
|
||||||
|
n := uint64(5)
|
||||||
|
params.Nonce = &n
|
||||||
|
mm := MessageMatcher(params)
|
||||||
|
|
||||||
|
srvcs, mockApi := setupMockSrvcs(t)
|
||||||
|
defer srvcs.Close() //nolint:errcheck
|
||||||
|
_, _ = mm, mockApi
|
||||||
|
|
||||||
|
var sm *types.SignedMessage
|
||||||
|
gomock.InOrder(
|
||||||
|
mockApi.EXPECT().WalletBalance(ctxM, a1).Return(types.NewInt(balance), nil),
|
||||||
|
mockApi.EXPECT().WalletSignMessage(ctxM, a1, mm).DoAndReturn(
|
||||||
|
func(_ context.Context, _ address.Address, msg *types.Message) (*types.SignedMessage, error) {
|
||||||
|
sm = fakeSign(msg)
|
||||||
|
|
||||||
|
// now we expect MpoolPush with that SignedMessage
|
||||||
|
mockApi.EXPECT().MpoolPush(ctxM, sm).Return(sm.Cid(), nil)
|
||||||
|
return sm, nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
c, err := srvcs.Send(ctx, params)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, sm.Cid(), c)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gas-params", func(t *testing.T) {
|
||||||
|
params := params
|
||||||
|
limit := int64(1)
|
||||||
|
params.GasLimit = &limit
|
||||||
|
gfc := big.NewInt(100)
|
||||||
|
params.GasFeeCap = &gfc
|
||||||
|
gp := big.NewInt(10)
|
||||||
|
params.GasPremium = &gp
|
||||||
|
|
||||||
|
srvcs, mockApi := setupMockSrvcs(t)
|
||||||
|
defer srvcs.Close() //nolint:errcheck
|
||||||
|
msgCid, sign := makeMessageSigner()
|
||||||
|
gomock.InOrder(
|
||||||
|
mockApi.EXPECT().WalletBalance(ctxM, params.From).Return(types.NewInt(balance), nil),
|
||||||
|
mockApi.EXPECT().MpoolPushMessage(ctxM, MessageMatcher(params), nil).DoAndReturn(sign),
|
||||||
|
)
|
||||||
|
|
||||||
|
c, err := srvcs.Send(ctx, params)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, *msgCid, c)
|
||||||
|
})
|
||||||
|
}
|
81
cli/servicesmock_test.go
Normal file
81
cli/servicesmock_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/filecoin-project/lotus/cli (interfaces: ServicesAPI)
|
||||||
|
|
||||||
|
// Package cli is a generated GoMock package.
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
go_address "github.com/filecoin-project/go-address"
|
||||||
|
abi "github.com/filecoin-project/go-state-types/abi"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
go_cid "github.com/ipfs/go-cid"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockServicesAPI is a mock of ServicesAPI interface
|
||||||
|
type MockServicesAPI struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockServicesAPIMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockServicesAPIMockRecorder is the mock recorder for MockServicesAPI
|
||||||
|
type MockServicesAPIMockRecorder struct {
|
||||||
|
mock *MockServicesAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockServicesAPI creates a new mock instance
|
||||||
|
func NewMockServicesAPI(ctrl *gomock.Controller) *MockServicesAPI {
|
||||||
|
mock := &MockServicesAPI{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockServicesAPIMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockServicesAPI) EXPECT() *MockServicesAPIMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close mocks base method
|
||||||
|
func (m *MockServicesAPI) Close() error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Close")
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close indicates an expected call of Close
|
||||||
|
func (mr *MockServicesAPIMockRecorder) Close() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockServicesAPI)(nil).Close))
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeTypedParamsFromJSON mocks base method
|
||||||
|
func (m *MockServicesAPI) DecodeTypedParamsFromJSON(arg0 context.Context, arg1 go_address.Address, arg2 abi.MethodNum, arg3 string) ([]byte, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "DecodeTypedParamsFromJSON", arg0, arg1, arg2, arg3)
|
||||||
|
ret0, _ := ret[0].([]byte)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeTypedParamsFromJSON indicates an expected call of DecodeTypedParamsFromJSON
|
||||||
|
func (mr *MockServicesAPIMockRecorder) DecodeTypedParamsFromJSON(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecodeTypedParamsFromJSON", reflect.TypeOf((*MockServicesAPI)(nil).DecodeTypedParamsFromJSON), arg0, arg1, arg2, arg3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send mocks base method
|
||||||
|
func (m *MockServicesAPI) Send(arg0 context.Context, arg1 SendParams) (go_cid.Cid, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Send", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(go_cid.Cid)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send indicates an expected call of Send
|
||||||
|
func (mr *MockServicesAPIMockRecorder) Send(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockServicesAPI)(nil).Send), arg0, arg1)
|
||||||
|
}
|
1
go.mod
1
go.mod
@ -50,6 +50,7 @@ require (
|
|||||||
github.com/gbrlsnchs/jwt/v3 v3.0.0-beta.1
|
github.com/gbrlsnchs/jwt/v3 v3.0.0-beta.1
|
||||||
github.com/go-kit/kit v0.10.0
|
github.com/go-kit/kit v0.10.0
|
||||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||||
|
github.com/golang/mock v1.4.4
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/gorilla/mux v1.7.4
|
github.com/gorilla/mux v1.7.4
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
|
Loading…
Reference in New Issue
Block a user