package market import ( "context" "errors" "math/rand" "testing" "time" "github.com/stretchr/testify/require" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/crypto" tutils "github.com/filecoin-project/specs-actors/v2/support/testing" "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/actors" "github.com/filecoin-project/lotus/chain/actors/builtin/market" "github.com/filecoin-project/lotus/chain/types" ) type fakeAPI struct { returnedBalance api.MarketBalance returnedBalanceErr error signature crypto.Signature receivedMessage *types.Message pushMessageErr error lookupIDErr error } func (fapi *fakeAPI) StateLookupID(_ context.Context, addr address.Address, _ types.TipSetKey) (address.Address, error) { return addr, fapi.lookupIDErr } func (fapi *fakeAPI) StateMarketBalance(context.Context, address.Address, types.TipSetKey) (api.MarketBalance, error) { return fapi.returnedBalance, fapi.returnedBalanceErr } func (fapi *fakeAPI) MpoolPushMessage(ctx context.Context, msg *types.Message, spec *api.MessageSendSpec) (*types.SignedMessage, error) { fapi.receivedMessage = msg return &types.SignedMessage{ Message: *msg, Signature: fapi.signature, }, fapi.pushMessageErr } func addFundsMsg(toAdd abi.TokenAmount, addr address.Address, wallet address.Address) *types.Message { params, _ := actors.SerializeParams(&addr) return &types.Message{ To: market.Address, From: wallet, Value: toAdd, Method: market.Methods.AddBalance, Params: params, } } type expectedResult struct { addAmt abi.TokenAmount shouldAdd bool err error cachedAvailable abi.TokenAmount } func TestAddFunds(t *testing.T) { ctx := context.Background() testCases := map[string]struct { returnedBalanceErr error returnedBalance api.MarketBalance addAmounts []abi.TokenAmount pushMessageErr error expectedResults []expectedResult lookupIDErr error }{ "succeeds, trivial case": { returnedBalance: api.MarketBalance{Escrow: abi.NewTokenAmount(0), Locked: abi.NewTokenAmount(0)}, addAmounts: []abi.TokenAmount{abi.NewTokenAmount(100)}, expectedResults: []expectedResult{ { addAmt: abi.NewTokenAmount(100), shouldAdd: true, err: nil, }, }, }, "succeeds, money already present": { returnedBalance: api.MarketBalance{Escrow: abi.NewTokenAmount(150), Locked: abi.NewTokenAmount(50)}, addAmounts: []abi.TokenAmount{abi.NewTokenAmount(100)}, expectedResults: []expectedResult{ { shouldAdd: false, err: nil, cachedAvailable: abi.NewTokenAmount(100), }, }, }, "succeeds, multiple adds": { returnedBalance: api.MarketBalance{Escrow: abi.NewTokenAmount(150), Locked: abi.NewTokenAmount(50)}, addAmounts: []abi.TokenAmount{abi.NewTokenAmount(100), abi.NewTokenAmount(200), abi.NewTokenAmount(250), abi.NewTokenAmount(250)}, expectedResults: []expectedResult{ { shouldAdd: false, err: nil, }, { addAmt: abi.NewTokenAmount(100), shouldAdd: true, err: nil, cachedAvailable: abi.NewTokenAmount(200), }, { addAmt: abi.NewTokenAmount(50), shouldAdd: true, err: nil, cachedAvailable: abi.NewTokenAmount(250), }, { shouldAdd: false, err: nil, cachedAvailable: abi.NewTokenAmount(250), }, }, }, "error on market balance": { returnedBalanceErr: errors.New("something went wrong"), addAmounts: []abi.TokenAmount{abi.NewTokenAmount(100)}, expectedResults: []expectedResult{ { err: errors.New("something went wrong"), }, }, }, "error on push message": { returnedBalance: api.MarketBalance{Escrow: abi.NewTokenAmount(0), Locked: abi.NewTokenAmount(0)}, pushMessageErr: errors.New("something went wrong"), addAmounts: []abi.TokenAmount{abi.NewTokenAmount(100)}, expectedResults: []expectedResult{ { err: errors.New("something went wrong"), cachedAvailable: abi.NewTokenAmount(0), }, }, }, "error looking up address": { lookupIDErr: errors.New("something went wrong"), addAmounts: []abi.TokenAmount{abi.NewTokenAmount(100)}, expectedResults: []expectedResult{ { err: errors.New("something went wrong"), }, }, }, } for testCase, data := range testCases { //nolint:scopelint t.Run(testCase, func(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() sig := make([]byte, 100) _, err := rand.Read(sig) require.NoError(t, err) fapi := &fakeAPI{ returnedBalance: data.returnedBalance, returnedBalanceErr: data.returnedBalanceErr, signature: crypto.Signature{ Type: crypto.SigTypeUnknown, Data: sig, }, pushMessageErr: data.pushMessageErr, lookupIDErr: data.lookupIDErr, } fundMgr := newFundMgr(fapi) addr := tutils.NewIDAddr(t, uint64(rand.Uint32())) wallet := tutils.NewIDAddr(t, uint64(rand.Uint32())) for i, amount := range data.addAmounts { fapi.receivedMessage = nil _, err := fundMgr.EnsureAvailable(ctx, addr, wallet, amount) expected := data.expectedResults[i] if expected.err == nil { require.NoError(t, err) if expected.shouldAdd { expectedMessage := addFundsMsg(expected.addAmt, addr, wallet) require.Equal(t, expectedMessage, fapi.receivedMessage) } else { require.Nil(t, fapi.receivedMessage) } } else { require.EqualError(t, err, expected.err.Error()) } if !expected.cachedAvailable.Nil() { require.Equal(t, expected.cachedAvailable, fundMgr.available[addr]) } } }) } }