@@ -6946,6 +6946,140 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest,
69466946 }
69476947}
69486948
6949+ // AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset
6950+ // specific parameters. It allows RPC users to create invoices that correspond
6951+ // to the specified asset amount.
6952+ func (r * rpcServer ) AddInvoice (ctx context.Context ,
6953+ req * tchrpc.AddInvoiceRequest ) (* tchrpc.AddInvoiceResponse , error ) {
6954+
6955+ if req .InvoiceRequest == nil {
6956+ return nil , fmt .Errorf ("invoice request must be specified" )
6957+ }
6958+ iReq := req .InvoiceRequest
6959+
6960+ // Do some preliminary checks on the asset ID and make sure we have any
6961+ // balance for that asset.
6962+ if len (req .AssetId ) != sha256 .Size {
6963+ return nil , fmt .Errorf ("asset ID must be 32 bytes" )
6964+ }
6965+ var assetID asset.ID
6966+ copy (assetID [:], req .AssetId )
6967+
6968+ // The peer public key is optional if there is only a single asset
6969+ // channel.
6970+ var peerPubKey * route.Vertex
6971+ if len (req .PeerPubkey ) > 0 {
6972+ parsedKey , err := route .NewVertexFromBytes (req .PeerPubkey )
6973+ if err != nil {
6974+ return nil , fmt .Errorf ("error parsing peer pubkey: %w" ,
6975+ err )
6976+ }
6977+
6978+ peerPubKey = & parsedKey
6979+ }
6980+
6981+ // We can now query the asset channels we have.
6982+ assetChan , err := r .rfqChannel (ctx , assetID , peerPubKey )
6983+ if err != nil {
6984+ return nil , fmt .Errorf ("error finding asset channel to use: %w" ,
6985+ err )
6986+ }
6987+
6988+ // Even if the user didn't specify the peer public key before, we
6989+ // definitely know it now. So let's make sure it's always set.
6990+ peerPubKey = & assetChan .channelInfo .PubKeyBytes
6991+
6992+ expirySeconds := iReq .Expiry
6993+ if expirySeconds == 0 {
6994+ expirySeconds = int64 (rfq .DefaultInvoiceExpiry .Seconds ())
6995+ }
6996+ expiryTimestamp := time .Now ().Add (
6997+ time .Duration (expirySeconds ) * time .Second ,
6998+ )
6999+
7000+ resp , err := r .AddAssetBuyOrder (ctx , & rfqrpc.AddAssetBuyOrderRequest {
7001+ AssetSpecifier : & rfqrpc.AssetSpecifier {
7002+ Id : & rfqrpc.AssetSpecifier_AssetId {
7003+ AssetId : assetID [:],
7004+ },
7005+ },
7006+ MinAssetAmount : req .AssetAmount ,
7007+ Expiry : uint64 (expiryTimestamp .Unix ()),
7008+ PeerPubKey : peerPubKey [:],
7009+ TimeoutSeconds : uint32 (
7010+ rfq .DefaultTimeout .Seconds (),
7011+ ),
7012+ })
7013+ if err != nil {
7014+ return nil , fmt .Errorf ("error adding buy order: %w" , err )
7015+ }
7016+
7017+ var acceptedQuote * rfqrpc.PeerAcceptedBuyQuote
7018+ switch r := resp .Response .(type ) {
7019+ case * rfqrpc.AddAssetBuyOrderResponse_AcceptedQuote :
7020+ acceptedQuote = r .AcceptedQuote
7021+
7022+ case * rfqrpc.AddAssetBuyOrderResponse_InvalidQuote :
7023+ return nil , fmt .Errorf ("peer %v sent back an invalid quote, " +
7024+ "status: %v" , r .InvalidQuote .Peer ,
7025+ r .InvalidQuote .Status .String ())
7026+
7027+ case * rfqrpc.AddAssetBuyOrderResponse_RejectedQuote :
7028+ return nil , fmt .Errorf ("peer %v rejected the quote, code: %v, " +
7029+ "error message: %v" , r .RejectedQuote .Peer ,
7030+ r .RejectedQuote .ErrorCode , r .RejectedQuote .ErrorMessage )
7031+
7032+ default :
7033+ return nil , fmt .Errorf ("unexpected response type: %T" , r )
7034+ }
7035+
7036+ // Now that we have the accepted quote, we know the amount in Satoshi
7037+ // that we need to pay. We can now update the invoice with this amount.
7038+ mSatPerUnit := acceptedQuote .AskPrice
7039+ iReq .ValueMsat = int64 (req .AssetAmount * mSatPerUnit )
7040+
7041+ // The last step is to create a hop hint that includes the fake SCID of
7042+ // the quote, alongside the channel's routing policy. We need to choose
7043+ // the policy that points towards us, as the payment will be flowing in.
7044+ // So we get the policy that's being set by the remote peer.
7045+ channelID := assetChan .channelInfo .ChannelID
7046+ inboundPolicy , err := r .getInboundPolicy (
7047+ ctx , channelID , peerPubKey .String (),
7048+ )
7049+ if err != nil {
7050+ return nil , fmt .Errorf ("unable to get inbound channel policy " +
7051+ "for channel with ID %d: %w" , channelID , err )
7052+ }
7053+
7054+ hopHint := & lnrpc.HopHint {
7055+ NodeId : peerPubKey .String (),
7056+ ChanId : acceptedQuote .Scid ,
7057+ FeeBaseMsat : uint32 (inboundPolicy .FeeBaseMsat ),
7058+ FeeProportionalMillionths : uint32 (
7059+ inboundPolicy .FeeRateMilliMsat ,
7060+ ),
7061+ CltvExpiryDelta : inboundPolicy .TimeLockDelta ,
7062+ }
7063+ iReq .RouteHints = []* lnrpc.RouteHint {
7064+ {
7065+ HopHints : []* lnrpc.HopHint {
7066+ hopHint ,
7067+ },
7068+ },
7069+ }
7070+
7071+ rpcCtx , _ , rawClient := r .cfg .Lnd .Client .RawClientWithMacAuth (ctx )
7072+ invoiceResp , err := rawClient .AddInvoice (rpcCtx , iReq )
7073+ if err != nil {
7074+ return nil , fmt .Errorf ("error creating invoice: %w" , err )
7075+ }
7076+
7077+ return & tchrpc.AddInvoiceResponse {
7078+ AcceptedBuyQuote : acceptedQuote ,
7079+ InvoiceResult : invoiceResp ,
7080+ }, nil
7081+ }
7082+
69497083// DeclareScriptKey declares a new script key to the wallet. This is useful
69507084// when the script key contains scripts, which would mean it wouldn't be
69517085// recognized by the wallet automatically. Declaring a script key will make any
@@ -7166,3 +7300,24 @@ func (r *rpcServer) computeChannelAssetBalance(
71667300
71677301 return channelsByID , nil
71687302}
7303+
7304+ // getInboundPolicy returns the policy of the given channel that points towards
7305+ // our node, so it's the policy set by the remote peer.
7306+ func (r * rpcServer ) getInboundPolicy (ctx context.Context , chanID uint64 ,
7307+ remotePubStr string ) (* lnrpc.RoutingPolicy , error ) {
7308+
7309+ rpcCtx , _ , rawClient := r .cfg .Lnd .Client .RawClientWithMacAuth (ctx )
7310+ edge , err := rawClient .GetChanInfo (rpcCtx , & lnrpc.ChanInfoRequest {
7311+ ChanId : chanID ,
7312+ })
7313+ if err != nil {
7314+ return nil , fmt .Errorf ("unable to fetch channel: %w" , err )
7315+ }
7316+
7317+ policy := edge .Node2Policy
7318+ if edge .Node2Pub == remotePubStr {
7319+ policy = edge .Node1Policy
7320+ }
7321+
7322+ return policy , nil
7323+ }
0 commit comments