2020-02-13 00:28:23 +00:00
|
|
|
package paychmgr
|
2019-08-09 21:42:56 +00:00
|
|
|
|
|
|
|
import (
|
2019-09-25 12:56:04 +00:00
|
|
|
"bytes"
|
2019-08-09 21:42:56 +00:00
|
|
|
"context"
|
|
|
|
"fmt"
|
2019-09-10 13:43:01 +00:00
|
|
|
"math"
|
2019-08-09 21:42:56 +00:00
|
|
|
|
2020-07-09 20:35:43 +00:00
|
|
|
"github.com/filecoin-project/lotus/api"
|
|
|
|
|
2020-02-12 23:52:19 +00:00
|
|
|
cborutil "github.com/filecoin-project/go-cbor-util"
|
|
|
|
"github.com/filecoin-project/specs-actors/actors/builtin"
|
2020-03-03 02:12:36 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/builtin/account"
|
2020-02-12 23:52:19 +00:00
|
|
|
"github.com/filecoin-project/specs-actors/actors/builtin/paych"
|
2019-10-08 05:51:34 +00:00
|
|
|
"golang.org/x/xerrors"
|
|
|
|
|
2020-01-08 19:10:57 +00:00
|
|
|
logging "github.com/ipfs/go-log/v2"
|
2019-09-16 16:40:26 +00:00
|
|
|
"go.uber.org/fx"
|
2019-09-09 13:59:07 +00:00
|
|
|
|
2019-12-19 20:13:17 +00:00
|
|
|
"github.com/filecoin-project/go-address"
|
2020-02-12 23:52:19 +00:00
|
|
|
|
2019-10-18 04:47:41 +00:00
|
|
|
"github.com/filecoin-project/lotus/chain/actors"
|
|
|
|
"github.com/filecoin-project/lotus/chain/stmgr"
|
|
|
|
"github.com/filecoin-project/lotus/chain/types"
|
2020-01-30 23:48:25 +00:00
|
|
|
"github.com/filecoin-project/lotus/lib/sigs"
|
2019-10-18 04:47:41 +00:00
|
|
|
"github.com/filecoin-project/lotus/node/impl/full"
|
2019-08-09 21:42:56 +00:00
|
|
|
)
|
|
|
|
|
2019-09-09 13:59:07 +00:00
|
|
|
var log = logging.Logger("paych")
|
|
|
|
|
2019-09-16 16:40:26 +00:00
|
|
|
type ManagerApi struct {
|
|
|
|
fx.In
|
|
|
|
|
|
|
|
full.MpoolAPI
|
|
|
|
full.WalletAPI
|
2019-10-08 05:51:34 +00:00
|
|
|
full.StateAPI
|
2019-09-16 16:40:26 +00:00
|
|
|
}
|
|
|
|
|
2020-07-09 20:35:43 +00:00
|
|
|
type StateManagerApi interface {
|
|
|
|
LoadActorState(ctx context.Context, a address.Address, out interface{}, ts *types.TipSet) (*types.Actor, error)
|
|
|
|
Call(ctx context.Context, msg *types.Message, ts *types.TipSet) (*api.InvocResult, error)
|
|
|
|
}
|
|
|
|
|
|
|
|
//type StateApi interface {
|
|
|
|
// StateWaitMsg(ctx context.Context, msg cid.Cid, confidence uint64) (*api.MsgLookup, error)
|
|
|
|
//}
|
|
|
|
//
|
|
|
|
//type MpoolApi interface {
|
|
|
|
// MpoolPushMessage(ctx context.Context, msg *types.Message) (*types.SignedMessage, error)
|
|
|
|
//}
|
|
|
|
|
2019-08-12 17:09:56 +00:00
|
|
|
type Manager struct {
|
|
|
|
store *Store
|
2020-07-09 20:35:43 +00:00
|
|
|
//sm *stmgr.StateManager
|
|
|
|
sm StateManagerApi
|
2019-09-16 13:46:05 +00:00
|
|
|
|
2019-09-16 16:40:26 +00:00
|
|
|
mpool full.MpoolAPI
|
|
|
|
wallet full.WalletAPI
|
2019-10-08 05:51:34 +00:00
|
|
|
state full.StateAPI
|
2019-08-09 21:42:56 +00:00
|
|
|
}
|
|
|
|
|
2019-09-16 16:40:26 +00:00
|
|
|
func NewManager(sm *stmgr.StateManager, pchstore *Store, api ManagerApi) *Manager {
|
2019-08-12 17:09:56 +00:00
|
|
|
return &Manager{
|
2019-08-09 21:42:56 +00:00
|
|
|
store: pchstore,
|
2019-09-06 06:26:02 +00:00
|
|
|
sm: sm,
|
2019-09-16 16:40:26 +00:00
|
|
|
|
2019-09-16 21:26:19 +00:00
|
|
|
mpool: api.MpoolAPI,
|
2019-09-16 16:40:26 +00:00
|
|
|
wallet: api.WalletAPI,
|
2019-10-08 05:51:34 +00:00
|
|
|
state: api.StateAPI,
|
2019-08-09 21:42:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-09 20:35:43 +00:00
|
|
|
// Used by the tests to supply mocks
|
|
|
|
func newManager(sm StateManagerApi, pchstore *Store) *Manager {
|
|
|
|
return &Manager{
|
|
|
|
store: pchstore,
|
|
|
|
sm: sm,
|
|
|
|
|
|
|
|
//mpool: api.MpoolAPI,
|
|
|
|
//wallet: api.WalletAPI,
|
|
|
|
//state: api.StateAPI,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-25 20:35:15 +00:00
|
|
|
func maxLaneFromState(st *paych.State) (uint64, error) {
|
2020-02-21 17:26:44 +00:00
|
|
|
maxLane := uint64(math.MaxInt64)
|
2020-02-12 23:52:19 +00:00
|
|
|
for _, state := range st.LaneStates {
|
2020-02-21 17:26:44 +00:00
|
|
|
if (state.ID)+1 > maxLane+1 {
|
|
|
|
maxLane = state.ID
|
2019-09-10 13:43:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return maxLane, nil
|
|
|
|
}
|
|
|
|
|
2019-08-12 17:09:56 +00:00
|
|
|
func (pm *Manager) TrackInboundChannel(ctx context.Context, ch address.Address) error {
|
|
|
|
_, st, err := pm.loadPaychState(ctx, ch)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-03 02:12:36 +00:00
|
|
|
var account account.State
|
|
|
|
_, err = pm.sm.LoadActorState(ctx, st.From, &account, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
from := account.Address
|
2020-06-26 06:06:01 +00:00
|
|
|
_, err = pm.sm.LoadActorState(ctx, st.To, &account, nil)
|
2020-03-03 02:12:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
to := account.Address
|
|
|
|
|
2019-09-10 13:43:01 +00:00
|
|
|
maxLane, err := maxLaneFromState(st)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-08-12 17:09:56 +00:00
|
|
|
return pm.store.TrackChannel(&ChannelInfo{
|
2019-09-10 13:43:01 +00:00
|
|
|
Channel: ch,
|
2020-03-03 02:12:36 +00:00
|
|
|
Control: to,
|
|
|
|
Target: from,
|
2019-09-10 13:43:01 +00:00
|
|
|
|
|
|
|
Direction: DirInbound,
|
|
|
|
NextLane: maxLane + 1,
|
2019-08-12 17:09:56 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-09-16 17:23:48 +00:00
|
|
|
func (pm *Manager) loadOutboundChannelInfo(ctx context.Context, ch address.Address) (*ChannelInfo, error) {
|
2019-08-12 17:09:56 +00:00
|
|
|
_, st, err := pm.loadPaychState(ctx, ch)
|
|
|
|
if err != nil {
|
2019-09-16 17:23:48 +00:00
|
|
|
return nil, err
|
2019-08-12 17:09:56 +00:00
|
|
|
}
|
|
|
|
|
2019-09-10 13:43:01 +00:00
|
|
|
maxLane, err := maxLaneFromState(st)
|
|
|
|
if err != nil {
|
2019-09-16 17:23:48 +00:00
|
|
|
return nil, err
|
2019-09-10 13:43:01 +00:00
|
|
|
}
|
|
|
|
|
2020-03-03 02:12:36 +00:00
|
|
|
var account account.State
|
|
|
|
_, err = pm.sm.LoadActorState(ctx, st.From, &account, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
from := account.Address
|
2020-06-26 06:06:01 +00:00
|
|
|
_, err = pm.sm.LoadActorState(ctx, st.To, &account, nil)
|
2020-03-03 02:12:36 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
to := account.Address
|
|
|
|
|
2019-09-16 17:23:48 +00:00
|
|
|
return &ChannelInfo{
|
2019-09-10 13:43:01 +00:00
|
|
|
Channel: ch,
|
2020-03-03 02:12:36 +00:00
|
|
|
Control: from,
|
|
|
|
Target: to,
|
2019-09-10 13:43:01 +00:00
|
|
|
|
|
|
|
Direction: DirOutbound,
|
|
|
|
NextLane: maxLane + 1,
|
2019-09-16 17:23:48 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pm *Manager) TrackOutboundChannel(ctx context.Context, ch address.Address) error {
|
|
|
|
ci, err := pm.loadOutboundChannelInfo(ctx, ch)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return pm.store.TrackChannel(ci)
|
2019-08-12 17:09:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (pm *Manager) ListChannels() ([]address.Address, error) {
|
|
|
|
return pm.store.ListChannels()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pm *Manager) GetChannelInfo(addr address.Address) (*ChannelInfo, error) {
|
|
|
|
return pm.store.getChannelInfo(addr)
|
2019-08-09 21:42:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// checks if the given voucher is valid (is or could become spendable at some point)
|
2020-02-25 21:09:22 +00:00
|
|
|
func (pm *Manager) CheckVoucherValid(ctx context.Context, ch address.Address, sv *paych.SignedVoucher) error {
|
2019-08-09 21:42:56 +00:00
|
|
|
act, pca, err := pm.loadPaychState(ctx, ch)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-03-03 02:12:36 +00:00
|
|
|
var account account.State
|
|
|
|
_, err = pm.sm.LoadActorState(ctx, pca.From, &account, nil)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
from := account.Address
|
|
|
|
|
2019-08-09 21:42:56 +00:00
|
|
|
// verify signature
|
|
|
|
vb, err := sv.SigningBytes()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-08-13 18:47:40 +00:00
|
|
|
|
|
|
|
// TODO: technically, either party may create and sign a voucher.
|
|
|
|
// However, for now, we only accept them from the channel creator.
|
|
|
|
// More complex handling logic can be added later
|
2020-03-03 02:12:36 +00:00
|
|
|
if err := sigs.Verify(sv.Signature, from, vb); err != nil {
|
2019-08-09 21:42:56 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
sendAmount := sv.Amount
|
|
|
|
|
|
|
|
// now check the lane state
|
2019-08-13 18:47:40 +00:00
|
|
|
// TODO: should check against vouchers in our local store too
|
|
|
|
// there might be something conflicting
|
2020-02-12 23:52:19 +00:00
|
|
|
ls := findLane(pca.LaneStates, uint64(sv.Lane))
|
|
|
|
if ls == nil {
|
2019-08-09 21:42:56 +00:00
|
|
|
} else {
|
2020-02-12 23:52:19 +00:00
|
|
|
if (ls.Nonce) >= sv.Nonce {
|
2019-08-09 21:42:56 +00:00
|
|
|
return fmt.Errorf("nonce too low")
|
|
|
|
}
|
|
|
|
|
2020-07-09 20:35:43 +00:00
|
|
|
// TODO: return error if ls.Redeemed > vs.Amount
|
2019-08-09 21:42:56 +00:00
|
|
|
sendAmount = types.BigSub(sv.Amount, ls.Redeemed)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: also account for vouchers on other lanes we've received
|
|
|
|
newTotal := types.BigAdd(sendAmount, pca.ToSend)
|
2019-09-23 17:11:44 +00:00
|
|
|
if act.Balance.LessThan(newTotal) {
|
2019-08-09 21:42:56 +00:00
|
|
|
return fmt.Errorf("not enough funds in channel to cover voucher")
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(sv.Merges) != 0 {
|
|
|
|
return fmt.Errorf("dont currently support paych lane merges")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// checks if the given voucher is currently spendable
|
2020-02-25 21:09:22 +00:00
|
|
|
func (pm *Manager) CheckVoucherSpendable(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, secret []byte, proof []byte) (bool, error) {
|
2019-08-09 21:42:56 +00:00
|
|
|
owner, err := pm.getPaychOwner(ctx, ch)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2019-09-09 13:59:07 +00:00
|
|
|
if sv.Extra != nil && proof == nil {
|
|
|
|
known, err := pm.ListVouchers(ctx, ch)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, v := range known {
|
2020-02-12 23:52:19 +00:00
|
|
|
eq, err := cborutil.Equals(v.Voucher, sv)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
if v.Proof != nil && eq {
|
2019-09-09 13:59:07 +00:00
|
|
|
log.Info("CheckVoucherSpendable: using stored proof")
|
|
|
|
proof = v.Proof
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if proof == nil {
|
|
|
|
log.Warn("CheckVoucherSpendable: nil proof for voucher with validation")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-12 23:52:19 +00:00
|
|
|
enc, err := actors.SerializeParams(&paych.UpdateChannelStateParams{
|
2019-08-09 21:42:56 +00:00
|
|
|
Sv: *sv,
|
|
|
|
Secret: secret,
|
|
|
|
Proof: proof,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2019-09-09 20:03:10 +00:00
|
|
|
ret, err := pm.sm.Call(ctx, &types.Message{
|
2019-08-09 21:42:56 +00:00
|
|
|
From: owner,
|
|
|
|
To: ch,
|
2020-02-12 23:52:19 +00:00
|
|
|
Method: builtin.MethodsPaych.UpdateChannelState,
|
2019-08-09 21:42:56 +00:00
|
|
|
Params: enc,
|
|
|
|
}, nil)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2020-03-03 23:32:17 +00:00
|
|
|
if ret.MsgRct.ExitCode != 0 {
|
2019-08-09 21:42:56 +00:00
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2019-08-12 17:09:56 +00:00
|
|
|
func (pm *Manager) getPaychOwner(ctx context.Context, ch address.Address) (address.Address, error) {
|
2020-02-12 23:52:19 +00:00
|
|
|
var state paych.State
|
|
|
|
if _, err := pm.sm.LoadActorState(ctx, ch, &state, nil); err != nil {
|
|
|
|
return address.Address{}, err
|
2019-08-09 21:42:56 +00:00
|
|
|
}
|
|
|
|
|
2020-02-12 23:52:19 +00:00
|
|
|
return state.From, nil
|
2019-08-09 21:42:56 +00:00
|
|
|
}
|
2019-08-12 17:09:56 +00:00
|
|
|
|
2020-02-25 21:09:22 +00:00
|
|
|
func (pm *Manager) AddVoucher(ctx context.Context, ch address.Address, sv *paych.SignedVoucher, proof []byte, minDelta types.BigInt) (types.BigInt, error) {
|
2019-08-12 17:09:56 +00:00
|
|
|
if err := pm.CheckVoucherValid(ctx, ch, sv); err != nil {
|
2019-09-16 21:25:23 +00:00
|
|
|
return types.NewInt(0), err
|
2019-08-12 17:09:56 +00:00
|
|
|
}
|
|
|
|
|
2019-09-25 12:56:04 +00:00
|
|
|
pm.store.lk.Lock()
|
|
|
|
defer pm.store.lk.Unlock()
|
|
|
|
|
|
|
|
ci, err := pm.store.getChannelInfo(ch)
|
|
|
|
if err != nil {
|
|
|
|
return types.NewInt(0), err
|
|
|
|
}
|
|
|
|
|
2020-02-12 23:52:19 +00:00
|
|
|
laneState, err := pm.laneState(ctx, ch, uint64(sv.Lane))
|
2019-09-25 12:56:04 +00:00
|
|
|
if err != nil {
|
|
|
|
return types.NewInt(0), err
|
|
|
|
}
|
|
|
|
|
2020-07-09 20:35:43 +00:00
|
|
|
// TODO: I believe this check is redundant because
|
|
|
|
// CheckVoucherValid() already returns an error if laneState.Nonce >= sv.Nonce
|
2019-09-25 13:25:54 +00:00
|
|
|
if minDelta.GreaterThan(types.NewInt(0)) && laneState.Nonce > sv.Nonce {
|
2019-09-25 12:56:04 +00:00
|
|
|
return types.NewInt(0), xerrors.Errorf("already storing voucher with higher nonce; %d > %d", laneState.Nonce, sv.Nonce)
|
|
|
|
}
|
|
|
|
|
2020-07-09 20:35:43 +00:00
|
|
|
// TODO:
|
|
|
|
// It's possible to repeatedly add a voucher with the same proof:
|
|
|
|
// 1. add a voucher with proof P1
|
|
|
|
// 2. add a voucher with proof P2
|
|
|
|
// 3. add a voucher with proof P2 (again)
|
|
|
|
// Voucher with proof P2 has been added twice
|
|
|
|
//
|
2019-09-25 12:56:04 +00:00
|
|
|
// look for duplicates
|
|
|
|
for i, v := range ci.Vouchers {
|
2020-03-03 02:12:36 +00:00
|
|
|
eq, err := cborutil.Equals(sv, v.Voucher)
|
2020-02-12 23:52:19 +00:00
|
|
|
if err != nil {
|
|
|
|
return types.BigInt{}, err
|
|
|
|
}
|
|
|
|
if !eq {
|
2019-09-25 12:56:04 +00:00
|
|
|
continue
|
|
|
|
}
|
2020-07-09 20:35:43 +00:00
|
|
|
// TODO: CBOR encoding / decoding changes nil into []byte{}, so instead of
|
|
|
|
// checking v.Proof against nil we should check len(v.Proof) == 0
|
2019-09-25 12:56:04 +00:00
|
|
|
if v.Proof != nil {
|
|
|
|
if !bytes.Equal(v.Proof, proof) {
|
|
|
|
log.Warnf("AddVoucher: multiple proofs for single voucher, storing both")
|
|
|
|
break
|
|
|
|
}
|
|
|
|
log.Warnf("AddVoucher: voucher re-added with matching proof")
|
|
|
|
return types.NewInt(0), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Warnf("AddVoucher: adding proof to stored voucher")
|
|
|
|
ci.Vouchers[i] = &VoucherInfo{
|
|
|
|
Voucher: v.Voucher,
|
|
|
|
Proof: proof,
|
|
|
|
}
|
|
|
|
|
|
|
|
return types.NewInt(0), pm.store.putChannelInfo(ci)
|
|
|
|
}
|
|
|
|
|
|
|
|
delta := types.BigSub(sv.Amount, laneState.Redeemed)
|
|
|
|
if minDelta.GreaterThan(delta) {
|
|
|
|
return delta, xerrors.Errorf("addVoucher: supplied token amount too low; minD=%s, D=%s; laneAmt=%s; v.Amt=%s", minDelta, delta, laneState.Redeemed, sv.Amount)
|
|
|
|
}
|
|
|
|
|
|
|
|
ci.Vouchers = append(ci.Vouchers, &VoucherInfo{
|
|
|
|
Voucher: sv,
|
|
|
|
Proof: proof,
|
|
|
|
})
|
|
|
|
|
2020-02-21 17:26:44 +00:00
|
|
|
if ci.NextLane <= (sv.Lane) {
|
|
|
|
ci.NextLane = sv.Lane + 1
|
2019-09-25 12:56:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return delta, pm.store.putChannelInfo(ci)
|
2019-08-12 17:09:56 +00:00
|
|
|
}
|
2019-08-12 19:51:01 +00:00
|
|
|
|
2020-02-21 17:26:44 +00:00
|
|
|
func (pm *Manager) AllocateLane(ch address.Address) (uint64, error) {
|
2020-07-09 20:35:43 +00:00
|
|
|
// TODO: should this take into account lane state?
|
2019-09-10 13:43:01 +00:00
|
|
|
return pm.store.AllocateLane(ch)
|
|
|
|
}
|
|
|
|
|
2019-09-09 13:59:07 +00:00
|
|
|
func (pm *Manager) ListVouchers(ctx context.Context, ch address.Address) ([]*VoucherInfo, error) {
|
2019-08-12 19:51:01 +00:00
|
|
|
// TODO: just having a passthrough method like this feels odd. Seems like
|
|
|
|
// there should be some filtering we're doing here
|
|
|
|
return pm.store.VouchersForPaych(ch)
|
|
|
|
}
|
|
|
|
|
2019-09-10 13:43:01 +00:00
|
|
|
func (pm *Manager) OutboundChanTo(from, to address.Address) (address.Address, error) {
|
2019-09-16 13:46:05 +00:00
|
|
|
pm.store.lk.Lock()
|
|
|
|
defer pm.store.lk.Unlock()
|
|
|
|
|
2019-09-10 13:43:01 +00:00
|
|
|
return pm.store.findChan(func(ci *ChannelInfo) bool {
|
|
|
|
if ci.Direction != DirOutbound {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return ci.Control == from && ci.Target == to
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-08-12 19:51:01 +00:00
|
|
|
func (pm *Manager) NextNonceForLane(ctx context.Context, ch address.Address, lane uint64) (uint64, error) {
|
2020-07-09 20:35:43 +00:00
|
|
|
// TODO: should this take into account lane state?
|
2019-08-12 19:51:01 +00:00
|
|
|
vouchers, err := pm.store.VouchersForPaych(ch)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var maxnonce uint64
|
|
|
|
for _, v := range vouchers {
|
2020-02-12 23:52:19 +00:00
|
|
|
if uint64(v.Voucher.Lane) == lane {
|
|
|
|
if uint64(v.Voucher.Nonce) > maxnonce {
|
|
|
|
maxnonce = uint64(v.Voucher.Nonce)
|
2019-08-12 19:51:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-13 04:27:54 +00:00
|
|
|
return maxnonce + 1, nil
|
2019-08-12 19:51:01 +00:00
|
|
|
}
|