github.com/mit-dci/lit@v0.0.0-20221102210550-8c3d3b49f2ce/litrpc/chancmds.go (about)

     1  package litrpc
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/mit-dci/lit/logging"
     7  
     8  	"github.com/mit-dci/lit/btcutil"
     9  	"github.com/mit-dci/lit/consts"
    10  	"github.com/mit-dci/lit/portxo"
    11  	"github.com/mit-dci/lit/qln"
    12  )
    13  
    14  type ChannelInfo struct {
    15  	OutPoint      string
    16  	CoinType      uint32
    17  	Closed        bool
    18  	Failed        bool
    19  	Capacity      int64
    20  	MyBalance     int64
    21  	Height        int32  // block height of channel fund confirmation
    22  	StateNum      uint64 // Most recent commit number
    23  	PeerIdx, CIdx uint32
    24  	PeerID        string
    25  	Data          [32]byte
    26  	Pkh           [20]byte
    27  	HTLCs         []HTLCInfo
    28  	LastUpdate    uint64
    29  }
    30  type ChannelListReply struct {
    31  	Channels []ChannelInfo
    32  }
    33  type HTLCInfo struct {
    34  	Idx            uint32
    35  	Incoming       bool
    36  	Amt            int64
    37  	RHash          [32]byte
    38  	Locktime       uint32
    39  	R              [16]byte
    40  	Cleared        bool
    41  	Clearing       bool
    42  	ClearedOnChain bool
    43  	InProg         bool
    44  }
    45  
    46  // ChannelList sends back a list of every (open?) channel with some
    47  // info for each.
    48  func (r *LitRPC) ChannelList(args ChanArgs, reply *ChannelListReply) error {
    49  	var err error
    50  	var qcs []*qln.Qchan
    51  
    52  	if args.ChanIdx == 0 {
    53  		qcs, err = r.Node.GetAllQchans()
    54  		if err != nil {
    55  			return err
    56  		}
    57  	} else {
    58  		qc, err := r.Node.GetQchanByIdx(args.ChanIdx)
    59  		if err != nil {
    60  			return err
    61  		}
    62  		qcs = append(qcs, qc)
    63  	}
    64  
    65  	reply.Channels = make([]ChannelInfo, len(qcs))
    66  
    67  	for i, q := range qcs {
    68  		reply.Channels[i].OutPoint = q.Op.String()
    69  		reply.Channels[i].CoinType = q.Coin()
    70  		reply.Channels[i].Closed = q.CloseData.Closed
    71  		reply.Channels[i].Failed = q.State.Failed
    72  		reply.Channels[i].Capacity = q.Value
    73  		reply.Channels[i].MyBalance = q.State.MyAmt
    74  		reply.Channels[i].Height = q.Height
    75  		reply.Channels[i].StateNum = q.State.StateIdx
    76  		reply.Channels[i].PeerIdx = q.KeyGen.Step[3] & 0x7fffffff
    77  		reply.Channels[i].CIdx = q.KeyGen.Step[4] & 0x7fffffff
    78  		reply.Channels[i].Data = q.State.Data
    79  		reply.Channels[i].Pkh = q.WatchRefundAdr
    80  		for _, h := range q.State.HTLCs {
    81  			hi := HTLCInfo{
    82  				h.Idx,
    83  				h.Incoming,
    84  				h.Amt,
    85  				h.RHash,
    86  				h.Locktime,
    87  				h.R,
    88  				h.Cleared,
    89  				h.Clearing,
    90  				h.ClearedOnChain,
    91  				false,
    92  			}
    93  
    94  			reply.Channels[i].HTLCs = append(reply.Channels[i].HTLCs, hi)
    95  		}
    96  
    97  		if q.State.InProgHTLC != nil {
    98  			h := q.State.InProgHTLC
    99  			hi := HTLCInfo{
   100  				h.Idx,
   101  				h.Incoming,
   102  				h.Amt,
   103  				h.RHash,
   104  				h.Locktime,
   105  				h.R,
   106  				h.Cleared,
   107  				h.Clearing,
   108  				h.ClearedOnChain,
   109  				true,
   110  			}
   111  			reply.Channels[i].HTLCs = append(reply.Channels[i].HTLCs, hi)
   112  		}
   113  
   114  		if q.State.CollidingHTLC != nil {
   115  			h := q.State.CollidingHTLC
   116  			hi := HTLCInfo{
   117  				h.Idx,
   118  				h.Incoming,
   119  				h.Amt,
   120  				h.RHash,
   121  				h.Locktime,
   122  				h.R,
   123  				h.Cleared,
   124  				h.Clearing,
   125  				h.ClearedOnChain,
   126  				true,
   127  			}
   128  			reply.Channels[i].HTLCs = append(reply.Channels[i].HTLCs, hi)
   129  		}
   130  		reply.Channels[i].LastUpdate = q.LastUpdate
   131  	}
   132  	return nil
   133  }
   134  
   135  // ------------------------- fund
   136  type FundArgs struct {
   137  	Peer        uint32 // who to make the channel with
   138  	CoinType    uint32 // what coin to use
   139  	Capacity    int64  // later can be minimum capacity
   140  	Roundup     int64  // ignore for now; can be used to round-up capacity
   141  	InitialSend int64  // Initial send of -1 means "ALL"
   142  	Data        [32]byte
   143  }
   144  
   145  type FundReply struct {
   146  	Status     string
   147  	ChanIdx    uint32
   148  	FundHeight int32
   149  }
   150  
   151  func (r *LitRPC) FundChannel(args FundArgs, reply *FundReply) error {
   152  	var err error
   153  	if r.Node.InProg != nil && r.Node.InProg.PeerIdx != 0 {
   154  		return fmt.Errorf("channel with peer %d not done yet", r.Node.InProg.PeerIdx)
   155  	}
   156  
   157  	if args.InitialSend < 0 || args.Capacity < 0 {
   158  		return fmt.Errorf("Can't have negative send or capacity")
   159  	}
   160  	if args.Capacity < consts.MinChanCapacity { // limit for now
   161  		return fmt.Errorf("Min channel capacity 1M sat")
   162  	}
   163  	if args.InitialSend > args.Capacity {
   164  		return fmt.Errorf("Can't send %d in %d capacity channel",
   165  			args.InitialSend, args.Capacity)
   166  	}
   167  
   168  	wal := r.Node.SubWallet[args.CoinType]
   169  	if wal == nil {
   170  		return fmt.Errorf("No wallet of cointype %d linked", args.CoinType)
   171  	}
   172  
   173  	nowHeight := wal.CurrentHeight()
   174  
   175  	// see if we have enough money before calling the funding function.  Not
   176  	// strictly required but it's better to fail here instead of after net traffic.
   177  	// also assume a fee of like 50K sat just to be safe
   178  	var allPorTxos portxo.TxoSliceByAmt
   179  	allPorTxos, err = wal.UtxoDump()
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	spendable := allPorTxos.SumWitness(nowHeight)
   185  
   186  	if args.Capacity > spendable-wal.Fee()*consts.JusticeTxBump {
   187  		return fmt.Errorf("Wanted %d but %d available for channel creation",
   188  			args.Capacity, spendable-wal.Fee()*consts.JusticeTxBump)
   189  	}
   190  
   191  	idx, err := r.Node.FundChannel(
   192  		args.Peer, args.CoinType, args.Capacity, args.InitialSend, args.Data)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	reply.Status = fmt.Sprintf("funded channel %d", idx)
   198  	reply.ChanIdx = idx
   199  	reply.FundHeight = nowHeight
   200  
   201  	return nil
   202  }
   203  
   204  // ------------------------- dual fund
   205  type DualFundArgs struct {
   206  	Peer        uint32 // who to make the channel with
   207  	CoinType    uint32 // what coin to use
   208  	OurAmount   int64  // what amount we will fund
   209  	TheirAmount int64  // what amount we request them to fund
   210  }
   211  
   212  func (r *LitRPC) DualFundChannel(args DualFundArgs, reply *StatusReply) error {
   213  	var err error
   214  	if r.Node.InProgDual != nil && r.Node.InProgDual.PeerIdx != 0 {
   215  		return fmt.Errorf("channel with peer %d not done yet", r.Node.InProgDual.PeerIdx)
   216  	}
   217  
   218  	if args.OurAmount <= 0 || args.TheirAmount <= 0 {
   219  		return fmt.Errorf("Need both our and their amount to be more than zero")
   220  	}
   221  	if args.OurAmount+args.TheirAmount < 1000000 { // limit for now
   222  		return fmt.Errorf("Min channel capacity 1M sat")
   223  	}
   224  
   225  	wal := r.Node.SubWallet[args.CoinType]
   226  	if wal == nil {
   227  		return fmt.Errorf("No wallet of cointype %d linked", args.CoinType)
   228  	}
   229  
   230  	nowHeight := wal.CurrentHeight()
   231  
   232  	// see if we have enough money before calling the funding function.  Not
   233  	// strictly required but it's better to fail here instead of after net traffic.
   234  	// also assume a fee of like 50K sat just to be safe
   235  	var allPorTxos portxo.TxoSliceByAmt
   236  	allPorTxos, err = wal.UtxoDump()
   237  	if err != nil {
   238  		return err
   239  	}
   240  
   241  	spendable := allPorTxos.SumWitness(nowHeight)
   242  
   243  	if args.OurAmount > spendable-50000 {
   244  		return fmt.Errorf("Our amount to fund is %d but only %d available for channel creation",
   245  			args.OurAmount, spendable-50000)
   246  	}
   247  
   248  	result, err := r.Node.DualFundChannel(
   249  		args.Peer, args.CoinType, args.OurAmount, args.TheirAmount)
   250  	if err != nil {
   251  
   252  		return err
   253  	}
   254  
   255  	if !result.Accepted {
   256  		return fmt.Errorf("Peer declined the funding request for reason %d", result.DeclineReason)
   257  	}
   258  
   259  	reply.Status = fmt.Sprintf("funded channel %d", result.ChannelId)
   260  
   261  	return nil
   262  }
   263  
   264  type DualFundRespondArgs struct {
   265  	// True for accept, false for decline
   266  	AcceptOrDecline bool
   267  }
   268  
   269  func (r *LitRPC) DualFundRespond(args DualFundRespondArgs, reply *StatusReply) error {
   270  	peerIdx := r.Node.InProgDual.PeerIdx
   271  
   272  	if peerIdx == 0 || r.Node.InProgDual.InitiatedByUs {
   273  		return fmt.Errorf("There is no pending request to reject")
   274  	}
   275  
   276  	if args.AcceptOrDecline {
   277  		r.Node.DualFundAccept()
   278  		reply.Status = fmt.Sprintf("Successfully accepted funding request from peer %d", peerIdx)
   279  	} else {
   280  		r.Node.DualFundDecline(0x01)
   281  		reply.Status = fmt.Sprintf("Successfully declined funding request from peer %d", peerIdx)
   282  	}
   283  
   284  	return nil
   285  }
   286  
   287  type PendingDualFundRequestsArgs struct {
   288  	// none
   289  }
   290  
   291  type PendingDualFundReply struct {
   292  	Pending         bool
   293  	PeerIdx         uint32
   294  	CoinType        uint32
   295  	TheirAmount     int64
   296  	RequestedAmount int64
   297  }
   298  
   299  func (r *LitRPC) PendingDualFund(args PendingDualFundRequestsArgs, reply *PendingDualFundReply) error {
   300  
   301  	if r.Node.InProgDual.PeerIdx != 0 && !r.Node.InProgDual.InitiatedByUs {
   302  		reply.Pending = true
   303  		reply.TheirAmount = r.Node.InProgDual.TheirAmount
   304  		reply.RequestedAmount = r.Node.InProgDual.OurAmount
   305  		reply.PeerIdx = r.Node.InProgDual.PeerIdx
   306  		reply.CoinType = r.Node.InProgDual.CoinType
   307  	}
   308  
   309  	return nil
   310  }
   311  
   312  // ------------------------- statedump
   313  type StateDumpArgs struct {
   314  	// none
   315  }
   316  
   317  type StateDumpReply struct {
   318  	Txs []qln.JusticeTx
   319  }
   320  
   321  // StateDump dumps all of the meta data for the state commitments of a channel
   322  func (r *LitRPC) StateDump(args StateDumpArgs, reply *StateDumpReply) error {
   323  	var err error
   324  	reply.Txs, err = r.Node.DumpJusticeDB()
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	return nil
   330  }
   331  
   332  // ------------------------- push
   333  type PushArgs struct {
   334  	ChanIdx uint32
   335  	Amt     int64
   336  	Data    [32]byte
   337  }
   338  type PushReply struct {
   339  	StateIndex uint64
   340  }
   341  
   342  // Push is the command to push money to the other side of the channel.
   343  // Currently waits for the process to complete before returning.
   344  // Will change to .. tries to send, but may not complete.
   345  
   346  func (r *LitRPC) Push(args PushArgs, reply *PushReply) error {
   347  	if args.Amt > consts.MaxChanCapacity || args.Amt < 1 {
   348  		return fmt.Errorf(
   349  			"can't push %d max is 1 coin (100000000), min is 1", args.Amt)
   350  	}
   351  
   352  	logging.Infof("push %d to chan %d with data %x\n", args.Amt, args.ChanIdx, args.Data)
   353  
   354  	// load the whole channel from disk just to see who the peer is
   355  	// (pretty inefficient)
   356  	dummyqc, err := r.Node.GetQchanByIdx(args.ChanIdx)
   357  	if err != nil {
   358  		return err
   359  	}
   360  	// see if channel is closed and error early
   361  	if dummyqc.CloseData.Closed {
   362  		return fmt.Errorf("Can't push; channel %d closed", args.ChanIdx)
   363  	}
   364  
   365  	// but we want to reference the qc that's already in ram
   366  	// first see if we're connected to that peer
   367  
   368  	// map read, need mutex...?
   369  	r.Node.RemoteMtx.Lock()
   370  	peer, ok := r.Node.RemoteCons[dummyqc.Peer()]
   371  	r.Node.RemoteMtx.Unlock()
   372  	if !ok {
   373  		return fmt.Errorf("not connected to peer %d for channel %d",
   374  			dummyqc.Peer(), dummyqc.Idx())
   375  	}
   376  	qc, ok := peer.QCs[dummyqc.Idx()]
   377  	if !ok {
   378  		return fmt.Errorf("peer %d doesn't have channel %d",
   379  			dummyqc.Peer(), dummyqc.Idx())
   380  	}
   381  
   382  	logging.Infof("channel %s\n", qc.Op.String())
   383  
   384  	if qc.CloseData.Closed {
   385  		return fmt.Errorf("Channel %d already closed by tx %s",
   386  			args.ChanIdx, qc.CloseData.CloseTxid.String())
   387  	}
   388  
   389  	// TODO this is a bad place to put it -- litRPC should be a thin layer
   390  	// to the Node.Func() calls.  For now though, set the height here...
   391  	qc.Height = dummyqc.Height
   392  
   393  	err = r.Node.PushChannel(qc, uint32(args.Amt), args.Data)
   394  	if err != nil {
   395  		logging.Errorf("Push error: %s\n", err.Error())
   396  		return err
   397  	}
   398  
   399  	reply.StateIndex = qc.State.StateIdx
   400  	return nil
   401  }
   402  
   403  // ------------------------- cclose
   404  type ChanArgs struct {
   405  	ChanIdx uint32
   406  }
   407  
   408  // reply with status string
   409  // CloseChannel is a cooperative closing of a channel to a specified address.
   410  func (r *LitRPC) CloseChannel(args ChanArgs, reply *StatusReply) error {
   411  
   412  	qc, err := r.Node.GetQchanByIdx(args.ChanIdx)
   413  	if err != nil {
   414  		return err
   415  	}
   416  
   417  	err = r.Node.CoopClose(qc)
   418  	if err != nil {
   419  		return err
   420  	}
   421  	reply.Status = "OK closed"
   422  
   423  	return nil
   424  }
   425  
   426  // ------------------------- break
   427  func (r *LitRPC) BreakChannel(args ChanArgs, reply *StatusReply) error {
   428  
   429  	qc, err := r.Node.GetQchanByIdx(args.ChanIdx)
   430  	if err != nil {
   431  		return err
   432  	}
   433  	return r.Node.BreakChannel(qc)
   434  }
   435  
   436  // ------------------------- dumpPriv
   437  type PrivInfo struct {
   438  	OutPoint string
   439  	Amt      int64
   440  	Height   int32
   441  	Delay    int32
   442  	CoinType string
   443  	Witty    bool
   444  	PairKey  string
   445  
   446  	WIF string
   447  }
   448  
   449  type DumpReply struct {
   450  	Privs []PrivInfo
   451  }
   452  
   453  // DumpPrivs returns WIF private keys for every utxo and channel
   454  func (r *LitRPC) DumpPrivs(args NoArgs, reply *DumpReply) error {
   455  	// get wifs for all channels
   456  	qcs, err := r.Node.GetAllQchans()
   457  	if err != nil {
   458  		return err
   459  	}
   460  
   461  	for _, qc := range qcs {
   462  		wal, ok := r.Node.SubWallet[qc.Coin()]
   463  		if !ok {
   464  			logging.Errorf(
   465  				"Channel %s error - coin %d not connected; can't show keys",
   466  				qc.Op.String(), qc.Coin())
   467  			continue
   468  		}
   469  
   470  		var thisTxo PrivInfo
   471  		thisTxo.OutPoint = qc.Op.String()
   472  		thisTxo.Amt = qc.Value
   473  		thisTxo.Height = qc.Height
   474  		thisTxo.CoinType = wal.Params().Name
   475  		thisTxo.Witty = true
   476  		thisTxo.PairKey = fmt.Sprintf("%x", qc.TheirPub)
   477  
   478  		priv, err := wal.GetPriv(qc.KeyGen)
   479  		if err != nil {
   480  			return err
   481  		}
   482  		wif := btcutil.WIF{PrivKey: priv, CompressPubKey: true, NetID: wal.Params().PrivateKeyID}
   483  		thisTxo.WIF = wif.String()
   484  
   485  		reply.Privs = append(reply.Privs, thisTxo)
   486  	}
   487  
   488  	// get WIFs for all utxos in the wallets
   489  	for _, wal := range r.Node.SubWallet {
   490  		walTxos, err := wal.UtxoDump()
   491  		if err != nil {
   492  			return err
   493  		}
   494  
   495  		syncHeight := wal.CurrentHeight()
   496  
   497  		theseTxos := make([]PrivInfo, len(walTxos))
   498  		for i, u := range walTxos {
   499  			theseTxos[i].OutPoint = u.Op.String()
   500  			theseTxos[i].Amt = u.Value
   501  			theseTxos[i].Height = u.Height
   502  			theseTxos[i].CoinType = wal.Params().Name
   503  			// show delay before utxo can be spent
   504  			if u.Seq != 0 {
   505  				theseTxos[i].Delay = u.Height + int32(u.Seq) - syncHeight
   506  			}
   507  			theseTxos[i].Witty = u.Mode&portxo.FlagTxoWitness != 0
   508  			priv, err := wal.GetPriv(u.KeyGen)
   509  			if err != nil {
   510  				return err
   511  			}
   512  			wif := btcutil.WIF{PrivKey: priv, CompressPubKey: true, NetID: wal.Params().PrivateKeyID}
   513  
   514  			theseTxos[i].WIF = wif.String()
   515  		}
   516  
   517  		reply.Privs = append(reply.Privs, theseTxos...)
   518  	}
   519  
   520  	return nil
   521  }
   522  
   523  // ------------------------- HTLCs
   524  type AddHTLCArgs struct {
   525  	ChanIdx  uint32
   526  	Amt      int64
   527  	LockTime uint32
   528  	RHash    [32]byte
   529  	Data     [32]byte
   530  }
   531  type AddHTLCReply struct {
   532  	StateIndex uint64
   533  	HTLCIndex  uint32
   534  }
   535  
   536  func (r *LitRPC) AddHTLC(args AddHTLCArgs, reply *AddHTLCReply) error {
   537  	if args.Amt > consts.MaxChanCapacity || args.Amt < consts.MinOutput {
   538  		return fmt.Errorf(
   539  			"can't add HTLC %d max is 1 coin (100000000), min is %d", args.Amt, consts.MinOutput)
   540  	}
   541  
   542  	logging.Infof("add HTLC %d to chan %d with data %x and RHash %x\n", args.Amt, args.ChanIdx, args.Data, args.RHash)
   543  
   544  	// load the whole channel from disk just to see who the peer is
   545  	// (pretty inefficient)
   546  	dummyqc, err := r.Node.GetQchanByIdx(args.ChanIdx)
   547  	if err != nil {
   548  		return err
   549  	}
   550  	// see if channel is closed and error early
   551  	if dummyqc.CloseData.Closed {
   552  		return fmt.Errorf("Can't push; channel %d closed", args.ChanIdx)
   553  	}
   554  
   555  	// but we want to reference the qc that's already in ram
   556  	// first see if we're connected to that peer
   557  
   558  	// map read, need mutex...?
   559  	r.Node.RemoteMtx.Lock()
   560  	peer, ok := r.Node.RemoteCons[dummyqc.Peer()]
   561  	r.Node.RemoteMtx.Unlock()
   562  	if !ok {
   563  		return fmt.Errorf("not connected to peer %d for channel %d",
   564  			dummyqc.Peer(), dummyqc.Idx())
   565  	}
   566  	qc, ok := peer.QCs[dummyqc.Idx()]
   567  	if !ok {
   568  		return fmt.Errorf("peer %d doesn't have channel %d",
   569  			dummyqc.Peer(), dummyqc.Idx())
   570  	}
   571  
   572  	logging.Infof("channel %s\n", qc.Op.String())
   573  
   574  	if qc.CloseData.Closed {
   575  		return fmt.Errorf("Channel %d already closed by tx %s",
   576  			args.ChanIdx, qc.CloseData.CloseTxid.String())
   577  	}
   578  
   579  	// TODO this is a bad place to put it -- litRPC should be a thin layer
   580  	// to the Node.Func() calls.  For now though, set the height here...
   581  	qc.Height = dummyqc.Height
   582  	curHeight := uint32(r.Node.SubWallet[qc.Coin()].CurrentHeight())
   583  	curHeight += args.LockTime
   584  
   585  	err = r.Node.OfferHTLC(qc, uint32(args.Amt), args.RHash, curHeight, args.Data)
   586  	if err != nil {
   587  		return err
   588  	}
   589  
   590  	reply.StateIndex = qc.State.StateIdx
   591  	reply.HTLCIndex = qc.State.HTLCIdx - 1
   592  	return nil
   593  }
   594  
   595  type ClearHTLCArgs struct {
   596  	ChanIdx uint32
   597  	HTLCIdx uint32
   598  	R       [16]byte
   599  	Data    [32]byte
   600  }
   601  type ClearHTLCReply struct {
   602  	StateIndex uint64
   603  }
   604  
   605  func (r *LitRPC) ClearHTLC(args ClearHTLCArgs, reply *ClearHTLCReply) error {
   606  	logging.Infof("clear HTLC %d from chan %d with data %x and preimage %x\n", args.HTLCIdx, args.ChanIdx, args.Data, args.R)
   607  
   608  	// load the whole channel from disk just to see who the peer is
   609  	// (pretty inefficient)
   610  	dummyqc, err := r.Node.GetQchanByIdx(args.ChanIdx)
   611  	if err != nil {
   612  		return err
   613  	}
   614  	// see if channel is closed and error early
   615  	if dummyqc.CloseData.Closed {
   616  		return fmt.Errorf("Can't clear; channel %d closed", args.ChanIdx)
   617  	}
   618  
   619  	// but we want to reference the qc that's already in ram
   620  	// first see if we're connected to that peer
   621  
   622  	// map read, need mutex...?
   623  	r.Node.RemoteMtx.Lock()
   624  	peer, ok := r.Node.RemoteCons[dummyqc.Peer()]
   625  	r.Node.RemoteMtx.Unlock()
   626  	if !ok {
   627  		return fmt.Errorf("not connected to peer %d for channel %d",
   628  			dummyqc.Peer(), dummyqc.Idx())
   629  	}
   630  	qc, ok := peer.QCs[dummyqc.Idx()]
   631  	if !ok {
   632  		return fmt.Errorf("peer %d doesn't have channel %d",
   633  			dummyqc.Peer(), dummyqc.Idx())
   634  	}
   635  
   636  	logging.Infof("channel %s\n", qc.Op.String())
   637  
   638  	if qc.CloseData.Closed {
   639  		return fmt.Errorf("Channel %d already closed by tx %s",
   640  			args.ChanIdx, qc.CloseData.CloseTxid.String())
   641  	}
   642  
   643  	// TODO this is a bad place to put it -- litRPC should be a thin layer
   644  	// to the Node.Func() calls.  For now though, set the height here...
   645  	qc.Height = dummyqc.Height
   646  
   647  	err = r.Node.ClearHTLC(qc, args.R, args.HTLCIdx, args.Data)
   648  	if err != nil {
   649  		return err
   650  	}
   651  
   652  	reply.StateIndex = qc.State.StateIdx
   653  	return nil
   654  }
   655  
   656  type PayMultihopArgs struct {
   657  	DestLNAdr      string
   658  	DestCoinType   uint32
   659  	OriginCoinType uint32
   660  	Amt            int64
   661  }
   662  
   663  // PayMultihop tries to find a multi-hop path to send the payment along
   664  func (r *LitRPC) PayMultihop(args PayMultihopArgs, reply *StatusReply) error {
   665  	_, err := r.Node.PayMultihop(args.DestLNAdr, args.OriginCoinType, args.DestCoinType, args.Amt)
   666  	return err
   667  }