2017-05-01 19:58:08 +03:00
|
|
|
package htlcswitch
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/sha256"
|
2017-05-02 02:29:30 +03:00
|
|
|
"encoding/binary"
|
2017-05-01 19:58:08 +03:00
|
|
|
"sync"
|
|
|
|
"testing"
|
|
|
|
|
2017-05-02 02:29:30 +03:00
|
|
|
"io"
|
2017-05-01 19:58:08 +03:00
|
|
|
"sync/atomic"
|
|
|
|
|
2017-06-29 16:40:45 +03:00
|
|
|
"bytes"
|
|
|
|
|
2017-05-02 02:29:30 +03:00
|
|
|
"github.com/btcsuite/fastsha256"
|
2017-05-01 19:58:08 +03:00
|
|
|
"github.com/go-errors/errors"
|
2017-05-02 02:29:30 +03:00
|
|
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
|
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
2017-05-01 19:58:08 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
2017-05-02 02:29:30 +03:00
|
|
|
"github.com/roasbeef/btcd/btcec"
|
|
|
|
"github.com/roasbeef/btcd/chaincfg/chainhash"
|
|
|
|
"github.com/roasbeef/btcd/txscript"
|
|
|
|
"github.com/roasbeef/btcd/wire"
|
2017-05-01 19:58:08 +03:00
|
|
|
"github.com/roasbeef/btcutil"
|
|
|
|
)
|
|
|
|
|
|
|
|
type mockServer struct {
|
|
|
|
sync.Mutex
|
|
|
|
|
|
|
|
started int32
|
|
|
|
shutdown int32
|
|
|
|
wg sync.WaitGroup
|
|
|
|
quit chan bool
|
|
|
|
|
|
|
|
t *testing.T
|
|
|
|
name string
|
|
|
|
messages chan lnwire.Message
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
id [33]byte
|
2017-05-01 19:58:08 +03:00
|
|
|
htlcSwitch *Switch
|
|
|
|
|
2017-05-02 02:29:30 +03:00
|
|
|
registry *mockInvoiceRegistry
|
2017-05-01 19:58:08 +03:00
|
|
|
recordFuncs []func(lnwire.Message)
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Peer = (*mockServer)(nil)
|
|
|
|
|
|
|
|
func newMockServer(t *testing.T, name string) *mockServer {
|
2017-06-17 01:08:19 +03:00
|
|
|
var id [33]byte
|
|
|
|
h := sha256.Sum256([]byte(name))
|
|
|
|
copy(id[:], h[:])
|
|
|
|
|
2017-05-01 19:58:08 +03:00
|
|
|
return &mockServer{
|
2017-06-29 17:03:42 +03:00
|
|
|
t: t,
|
|
|
|
id: id,
|
|
|
|
name: name,
|
|
|
|
messages: make(chan lnwire.Message, 3000),
|
|
|
|
quit: make(chan bool),
|
|
|
|
registry: newMockRegistry(),
|
|
|
|
htlcSwitch: New(Config{
|
|
|
|
UpdateTopology: func(msg *lnwire.ChannelUpdate) error {
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}),
|
2017-05-01 19:58:08 +03:00
|
|
|
recordFuncs: make([]func(lnwire.Message), 0),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *mockServer) Start() error {
|
|
|
|
if !atomic.CompareAndSwapInt32(&s.started, 0, 1) {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
s.htlcSwitch.Start()
|
|
|
|
|
|
|
|
s.wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer s.wg.Done()
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case msg := <-s.messages:
|
|
|
|
for _, f := range s.recordFuncs {
|
|
|
|
f(msg)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := s.readHandler(msg); err != nil {
|
|
|
|
s.Lock()
|
|
|
|
defer s.Unlock()
|
|
|
|
s.t.Fatalf("%v server error: %v", s.name, err)
|
|
|
|
}
|
|
|
|
case <-s.quit:
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-05-02 02:29:30 +03:00
|
|
|
// mockHopIterator represents the test version of hop iterator which instead
|
|
|
|
// of encrypting the path in onion blob just stores the path as a list of hops.
|
|
|
|
type mockHopIterator struct {
|
2017-06-17 01:08:19 +03:00
|
|
|
hops []ForwardingInfo
|
2017-05-02 02:29:30 +03:00
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
func newMockHopIterator(hops ...ForwardingInfo) HopIterator {
|
2017-05-02 02:29:30 +03:00
|
|
|
return &mockHopIterator{hops: hops}
|
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
func (r *mockHopIterator) ForwardingInstructions() ForwardingInfo {
|
|
|
|
h := r.hops[0]
|
|
|
|
r.hops = r.hops[1:]
|
|
|
|
return h
|
2017-05-02 02:29:30 +03:00
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
func (r *mockHopIterator) EncodeNextHop(w io.Writer) error {
|
2017-05-02 02:29:30 +03:00
|
|
|
var hopLength [4]byte
|
|
|
|
binary.BigEndian.PutUint32(hopLength[:], uint32(len(r.hops)))
|
|
|
|
|
|
|
|
if _, err := w.Write(hopLength[:]); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, hop := range r.hops {
|
2017-06-17 01:08:19 +03:00
|
|
|
if err := hop.encode(w); err != nil {
|
2017-05-02 02:29:30 +03:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
func (f *ForwardingInfo) encode(w io.Writer) error {
|
|
|
|
if _, err := w.Write([]byte{byte(f.Network)}); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := binary.Write(w, binary.BigEndian, f.NextHop); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := binary.Write(w, binary.BigEndian, f.AmountToForward); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := binary.Write(w, binary.BigEndian, f.OutgoingCTLV); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-05-02 02:29:30 +03:00
|
|
|
var _ HopIterator = (*mockHopIterator)(nil)
|
|
|
|
|
2017-06-29 16:40:45 +03:00
|
|
|
// mockObfuscator mock implementation of the failure obfuscator which only
|
|
|
|
// encodes the failure and do not makes any onion obfuscation.
|
|
|
|
type mockObfuscator struct{}
|
|
|
|
|
|
|
|
func newMockObfuscator() Obfuscator {
|
|
|
|
return &mockObfuscator{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *mockObfuscator) InitialObfuscate(failure lnwire.FailureMessage) (
|
|
|
|
lnwire.OpaqueReason, error) {
|
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
if err := lnwire.EncodeFailure(&b, failure, 0); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return b.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *mockObfuscator) BackwardObfuscate(reason lnwire.OpaqueReason) lnwire.OpaqueReason {
|
|
|
|
return reason
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// mockDeobfuscator mock implementation of the failure deobfuscator which
|
|
|
|
// only decodes the failure do not makes any onion obfuscation.
|
|
|
|
type mockDeobfuscator struct{}
|
|
|
|
|
|
|
|
func newMockDeobfuscator() Deobfuscator {
|
|
|
|
return &mockDeobfuscator{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (o *mockDeobfuscator) Deobfuscate(reason lnwire.OpaqueReason) (lnwire.FailureMessage,
|
|
|
|
error) {
|
|
|
|
r := bytes.NewReader(reason)
|
|
|
|
failure, err := lnwire.DecodeFailure(r, 0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return failure, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Deobfuscator = (*mockDeobfuscator)(nil)
|
|
|
|
|
2017-05-02 02:29:30 +03:00
|
|
|
// mockIteratorDecoder test version of hop iterator decoder which decodes the
|
|
|
|
// encoded array of hops.
|
|
|
|
type mockIteratorDecoder struct{}
|
|
|
|
|
2017-06-29 16:40:45 +03:00
|
|
|
func (p *mockIteratorDecoder) DecodeHopIterator(r io.Reader, meta []byte) (
|
|
|
|
HopIterator, lnwire.FailCode) {
|
|
|
|
|
2017-05-02 02:29:30 +03:00
|
|
|
var b [4]byte
|
|
|
|
_, err := r.Read(b[:])
|
|
|
|
if err != nil {
|
2017-06-29 16:40:45 +03:00
|
|
|
return nil, lnwire.CodeTemporaryChannelFailure
|
2017-05-02 02:29:30 +03:00
|
|
|
}
|
|
|
|
hopLength := binary.BigEndian.Uint32(b[:])
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
hops := make([]ForwardingInfo, hopLength)
|
2017-05-02 02:29:30 +03:00
|
|
|
for i := uint32(0); i < hopLength; i++ {
|
2017-06-17 01:08:19 +03:00
|
|
|
f := &ForwardingInfo{}
|
|
|
|
if err := f.decode(r); err != nil {
|
2017-06-29 16:40:45 +03:00
|
|
|
return nil, lnwire.CodeTemporaryChannelFailure
|
2017-05-02 02:29:30 +03:00
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
hops[i] = *f
|
2017-05-02 02:29:30 +03:00
|
|
|
}
|
|
|
|
|
2017-06-29 16:40:45 +03:00
|
|
|
return newMockHopIterator(hops...), lnwire.CodeNone
|
2017-05-02 02:29:30 +03:00
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
func (f *ForwardingInfo) decode(r io.Reader) error {
|
|
|
|
var net [1]byte
|
|
|
|
if _, err := r.Read(net[:]); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
f.Network = NetworkHop(net[0])
|
|
|
|
|
|
|
|
if err := binary.Read(r, binary.BigEndian, &f.NextHop); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := binary.Read(r, binary.BigEndian, &f.AmountToForward); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := binary.Read(r, binary.BigEndian, &f.OutgoingCTLV); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-05-01 19:58:08 +03:00
|
|
|
// messageInterceptor is function that handles the incoming peer messages and
|
|
|
|
// may decide should we handle it or not.
|
|
|
|
type messageInterceptor func(m lnwire.Message)
|
|
|
|
|
|
|
|
// Record is used to set the function which will be triggered when new
|
|
|
|
// lnwire message was received.
|
|
|
|
func (s *mockServer) record(f messageInterceptor) {
|
|
|
|
s.recordFuncs = append(s.recordFuncs, f)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *mockServer) SendMessage(message lnwire.Message) error {
|
|
|
|
select {
|
|
|
|
case s.messages <- message:
|
|
|
|
case <-s.quit:
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *mockServer) readHandler(message lnwire.Message) error {
|
|
|
|
var targetChan lnwire.ChannelID
|
|
|
|
|
|
|
|
switch msg := message.(type) {
|
|
|
|
case *lnwire.UpdateAddHTLC:
|
|
|
|
targetChan = msg.ChanID
|
|
|
|
case *lnwire.UpdateFufillHTLC:
|
|
|
|
targetChan = msg.ChanID
|
|
|
|
case *lnwire.UpdateFailHTLC:
|
|
|
|
targetChan = msg.ChanID
|
2017-06-29 16:40:45 +03:00
|
|
|
case *lnwire.UpdateFailMalformedHTLC:
|
|
|
|
targetChan = msg.ChanID
|
2017-05-01 19:58:08 +03:00
|
|
|
case *lnwire.RevokeAndAck:
|
|
|
|
targetChan = msg.ChanID
|
|
|
|
case *lnwire.CommitSig:
|
|
|
|
targetChan = msg.ChanID
|
|
|
|
default:
|
|
|
|
return errors.New("unknown message type")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Dispatch the commitment update message to the proper
|
|
|
|
// channel link dedicated to this channel.
|
|
|
|
link, err := s.htlcSwitch.GetLink(targetChan)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create goroutine for this, in order to be able to properly stop
|
|
|
|
// the server when handler stacked (server unavailable)
|
|
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
|
|
defer func() {
|
|
|
|
done <- struct{}{}
|
|
|
|
}()
|
|
|
|
|
|
|
|
link.HandleChannelUpdate(message)
|
|
|
|
}()
|
|
|
|
select {
|
|
|
|
case <-done:
|
|
|
|
case <-s.quit:
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
func (s *mockServer) PubKey() [33]byte {
|
2017-05-01 19:58:08 +03:00
|
|
|
return s.id
|
|
|
|
}
|
|
|
|
|
2017-07-12 16:44:17 +03:00
|
|
|
func (s *mockServer) Disconnect(reason error) {
|
2017-05-01 19:58:08 +03:00
|
|
|
s.Stop()
|
|
|
|
s.t.Fatalf("server %v was disconnected", s.name)
|
|
|
|
}
|
|
|
|
|
2017-05-03 17:02:22 +03:00
|
|
|
func (s *mockServer) WipeChannel(*lnwallet.LightningChannel) error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-05-01 19:58:08 +03:00
|
|
|
func (s *mockServer) Stop() {
|
|
|
|
if !atomic.CompareAndSwapInt32(&s.shutdown, 0, 1) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
go s.htlcSwitch.Stop()
|
|
|
|
|
|
|
|
close(s.quit)
|
|
|
|
s.wg.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *mockServer) String() string {
|
2017-06-17 01:08:19 +03:00
|
|
|
return s.name
|
2017-05-01 19:58:08 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
type mockChannelLink struct {
|
2017-06-17 01:08:19 +03:00
|
|
|
shortChanID lnwire.ShortChannelID
|
|
|
|
|
|
|
|
chanID lnwire.ChannelID
|
|
|
|
|
|
|
|
peer Peer
|
|
|
|
|
2017-05-01 19:58:08 +03:00
|
|
|
packets chan *htlcPacket
|
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
func newMockChannelLink(chanID lnwire.ChannelID, shortChanID lnwire.ShortChannelID,
|
2017-05-01 19:58:08 +03:00
|
|
|
peer Peer) *mockChannelLink {
|
2017-06-17 01:08:19 +03:00
|
|
|
|
2017-05-01 19:58:08 +03:00
|
|
|
return &mockChannelLink{
|
2017-06-17 01:08:19 +03:00
|
|
|
chanID: chanID,
|
|
|
|
shortChanID: shortChanID,
|
|
|
|
packets: make(chan *htlcPacket, 1),
|
|
|
|
peer: peer,
|
2017-05-01 19:58:08 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *mockChannelLink) HandleSwitchPacket(packet *htlcPacket) {
|
|
|
|
f.packets <- packet
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *mockChannelLink) HandleChannelUpdate(lnwire.Message) {
|
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
func (f *mockChannelLink) UpdateForwardingPolicy(_ ForwardingPolicy) {
|
|
|
|
}
|
|
|
|
|
2017-05-01 19:58:08 +03:00
|
|
|
func (f *mockChannelLink) Stats() (uint64, btcutil.Amount, btcutil.Amount) {
|
|
|
|
return 0, 0, 0
|
|
|
|
}
|
|
|
|
|
2017-06-17 01:08:19 +03:00
|
|
|
func (f *mockChannelLink) ChanID() lnwire.ChannelID { return f.chanID }
|
|
|
|
func (f *mockChannelLink) ShortChanID() lnwire.ShortChannelID { return f.shortChanID }
|
|
|
|
func (f *mockChannelLink) Bandwidth() btcutil.Amount { return 99999999 }
|
|
|
|
func (f *mockChannelLink) Peer() Peer { return f.peer }
|
|
|
|
func (f *mockChannelLink) Start() error { return nil }
|
|
|
|
func (f *mockChannelLink) Stop() {}
|
2017-05-01 19:58:08 +03:00
|
|
|
|
|
|
|
var _ ChannelLink = (*mockChannelLink)(nil)
|
2017-05-02 02:29:30 +03:00
|
|
|
|
|
|
|
type mockInvoiceRegistry struct {
|
|
|
|
sync.Mutex
|
|
|
|
invoices map[chainhash.Hash]*channeldb.Invoice
|
|
|
|
}
|
|
|
|
|
|
|
|
func newMockRegistry() *mockInvoiceRegistry {
|
|
|
|
return &mockInvoiceRegistry{
|
|
|
|
invoices: make(map[chainhash.Hash]*channeldb.Invoice),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *mockInvoiceRegistry) LookupInvoice(rHash chainhash.Hash) (*channeldb.Invoice, error) {
|
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
invoice, ok := i.invoices[rHash]
|
|
|
|
if !ok {
|
|
|
|
return nil, errors.New("can't find mock invoice")
|
|
|
|
}
|
|
|
|
|
|
|
|
return invoice, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *mockInvoiceRegistry) SettleInvoice(rhash chainhash.Hash) error {
|
|
|
|
|
|
|
|
invoice, err := i.LookupInvoice(rhash)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
i.Lock()
|
|
|
|
invoice.Terms.Settled = true
|
|
|
|
i.Unlock()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (i *mockInvoiceRegistry) AddInvoice(invoice *channeldb.Invoice) error {
|
|
|
|
i.Lock()
|
|
|
|
defer i.Unlock()
|
|
|
|
|
|
|
|
rhash := fastsha256.Sum256(invoice.Terms.PaymentPreimage[:])
|
|
|
|
i.invoices[chainhash.Hash(rhash)] = invoice
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ InvoiceDatabase = (*mockInvoiceRegistry)(nil)
|
|
|
|
|
|
|
|
type mockSigner struct {
|
|
|
|
key *btcec.PrivateKey
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockSigner) SignOutputRaw(tx *wire.MsgTx, signDesc *lnwallet.SignDescriptor) ([]byte, error) {
|
|
|
|
amt := signDesc.Output.Value
|
|
|
|
witnessScript := signDesc.WitnessScript
|
|
|
|
privKey := m.key
|
|
|
|
|
|
|
|
sig, err := txscript.RawTxInWitnessSignature(tx, signDesc.SigHashes,
|
|
|
|
signDesc.InputIndex, amt, witnessScript, txscript.SigHashAll, privKey)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return sig[:len(sig)-1], nil
|
|
|
|
}
|
|
|
|
func (m *mockSigner) ComputeInputScript(tx *wire.MsgTx, signDesc *lnwallet.SignDescriptor) (*lnwallet.InputScript, error) {
|
|
|
|
|
|
|
|
witnessScript, err := txscript.WitnessScript(tx, signDesc.SigHashes,
|
|
|
|
signDesc.InputIndex, signDesc.Output.Value, signDesc.Output.PkScript,
|
|
|
|
txscript.SigHashAll, m.key, true)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &lnwallet.InputScript{
|
|
|
|
Witness: witnessScript,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type mockNotifier struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, numConfs uint32) (*chainntnfs.ConfirmationEvent, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
func (m *mockNotifier) RegisterBlockEpochNtfn() (*chainntnfs.BlockEpochEvent, error) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockNotifier) Start() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *mockNotifier) Stop() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (m *mockNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint) (*chainntnfs.SpendEvent, error) {
|
|
|
|
return &chainntnfs.SpendEvent{
|
|
|
|
Spend: make(chan *chainntnfs.SpendDetail),
|
|
|
|
}, nil
|
|
|
|
}
|