diff --git a/itest/rfq_test.go b/itest/rfq_test.go index 239e1d401..be26268ee 100644 --- a/itest/rfq_test.go +++ b/itest/rfq_test.go @@ -4,10 +4,13 @@ import ( "context" "fmt" "math" + "math/rand" + "testing" "time" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "github.com/lightningnetwork/lnd/chainreg" @@ -213,6 +216,157 @@ func testRfqAssetBuyHtlcIntercept(t *harnessTest) { require.NoError(t.t, err) } +// testRfqAssetSellHtlcIntercept tests RFQ negotiation, HTLC interception, and +// validation between three peers. The RFQ negotiation is initiated by an asset +// sell request. +func testRfqAssetSellHtlcIntercept(t *harnessTest) { + // Initialize a new test scenario. + ts := newRfqTestScenario(t) + + // Mint an asset with Alice's tapd node. + rpcAssets := MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, ts.AliceTapd, + []*mintrpc.MintAssetRequest{issuableAssets[0]}, + ) + mintedAssetId := rpcAssets[0].AssetGenesis.AssetId + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout) + defer cancel() + + // TODO(ffranr): Add an asset buy offer to Bob's tapd node. This will + // allow Alice to sell the newly minted asset to Bob. + + // Subscribe to Alice's RFQ events stream. + aliceEventNtfns, err := ts.AliceTapd.SubscribeRfqEventNtfns( + ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{}, + ) + require.NoError(t.t, err) + + // Alice sends a sell order to Bob for some amount of the newly minted + // asset. + purchaseAssetAmt := uint64(200) + askAmt := uint64(42000) + sellOrderExpiry := uint64(time.Now().Add(24 * time.Hour).Unix()) + + _, err = ts.AliceTapd.AddAssetSellOrder( + ctxt, &rfqrpc.AddAssetSellOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + MaxAssetAmount: purchaseAssetAmt, + MinAsk: askAmt, + Expiry: sellOrderExpiry, + + // Here we explicitly specify Bob as the destination + // peer for the sell order. This will prompt Alice's + // tapd node to send a request for quote message to + // Bob's node. + PeerPubKey: ts.BobLnd.PubKey[:], + }, + ) + require.NoError(t.t, err, "unable to upsert asset sell order") + + // Wait until Alice receives an incoming sell quote accept message (sent + // from Bob) RFQ event notification. + BeforeTimeout(t.t, func() { + event, err := aliceEventNtfns.Recv() + require.NoError(t.t, err) + + _, ok := event.Event.(*rfqrpc.RfqEvent_PeerAcceptedSellQuote) + require.True(t.t, ok, "unexpected event: %v", event) + }, defaultWaitTimeout) + + // Alice should have received an accepted quote from Bob. This accepted + // quote can be used by Alice to make a payment to Bob. + acceptedQuotes, err := ts.AliceTapd.QueryPeerAcceptedQuotes( + ctxt, &rfqrpc.QueryPeerAcceptedQuotesRequest{}, + ) + require.NoError(t.t, err, "unable to query accepted quotes") + require.Len(t.t, acceptedQuotes.SellQuotes, 1) + + acceptedQuote := acceptedQuotes.SellQuotes[0] + + // Register to receive RFQ events from Bob's tapd node. We'll use this + // to wait for Bob to receive the HTLC with the asset transfer specific + // scid. + bobEventNtfns, err := ts.BobTapd.SubscribeRfqEventNtfns( + ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{}, + ) + require.NoError(t.t, err) + + // Carol generates and invoice for Alice to settle via Bob. + addInvoiceResp := ts.CarolLnd.RPC.AddInvoice(&lnrpc.Invoice{ + ValueMsat: int64(askAmt), + }) + invoice := ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash) + + // Decode the payment request to get the payment address. + payReq := ts.CarolLnd.RPC.DecodePayReq(invoice.PaymentRequest) + + // We now need to construct a route for the payment from Alice to Carol. + // The route will be Alice -> Bob -> Carol. We'll add the accepted quote + // ID as a record to the custom records field of the route's first hop. + // This will allow Bob to validate the payment against the accepted + // quote. + routeBuildRequest := routerrpc.BuildRouteRequest{ + AmtMsat: int64(askAmt), + HopPubkeys: [][]byte{ + ts.BobLnd.PubKey[:], + ts.CarolLnd.PubKey[:], + }, + PaymentAddr: payReq.PaymentAddr, + } + routeBuildResp := ts.AliceLnd.RPC.BuildRoute(&routeBuildRequest) + + // Add the accepted quote ID as a record to the custom records field of + // the route's first hop. + aliceBobHop := routeBuildResp.Route.Hops[0] + if aliceBobHop.CustomRecords == nil { + aliceBobHop.CustomRecords = make(map[uint64][]byte) + } + + aliceBobHop.CustomRecords[rfq.LnCustomRecordType] = + acceptedQuote.Id[:] + + // Update the route with the modified first hop. + routeBuildResp.Route.Hops[0] = aliceBobHop + + // Send the payment to the route. + t.Log("Alice paying invoice") + routeReq := routerrpc.SendToRouteRequest{ + PaymentHash: invoice.RHash, + Route: routeBuildResp.Route, + } + sendAttempt := ts.AliceLnd.RPC.SendToRouteV2(&routeReq) + require.Equal(t.t, lnrpc.HTLCAttempt_SUCCEEDED, sendAttempt.Status) + + // At this point Bob should have received a HTLC with the asset transfer + // specific scid. We'll wait for Bob to publish an accept HTLC event and + // then validate it against the accepted quote. + t.Log("Waiting for Bob to receive HTLC") + BeforeTimeout(t.t, func() { + event, err := bobEventNtfns.Recv() + require.NoError(t.t, err) + + _, ok := event.Event.(*rfqrpc.RfqEvent_AcceptHtlc) + require.True(t.t, ok, "unexpected event: %v", event) + }, defaultWaitTimeout) + + // Confirm that Carol receives the lightning payment from Alice via Bob. + invoice = ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash) + require.Equal(t.t, invoice.State, lnrpc.Invoice_SETTLED) + + // Close event notification streams. + err = aliceEventNtfns.CloseSend() + require.NoError(t.t, err) + + err = bobEventNtfns.CloseSend() + require.NoError(t.t, err) +} + // newLndNode creates a new lnd node with the given name and funds its wallet // with the specified outputs. func newLndNode(name string, outputFunds []btcutil.Amount, @@ -286,10 +440,15 @@ func newRfqTestScenario(t *harnessTest) *rfqTestScenario { outputFunds[i] = fundAmount } + // Generate a unique name for each new node. + aliceName := genRandomNodeName("AliceLnd") + bobName := genRandomNodeName("BobLnd") + carolName := genRandomNodeName("CarolLnd") + // Create three new nodes. - aliceLnd := newLndNode("AliceLnd", outputFunds[:], t.lndHarness) - bobLnd := newLndNode("BobLnd", outputFunds[:], t.lndHarness) - carolLnd := newLndNode("CarolLnd", outputFunds[:], t.lndHarness) + aliceLnd := newLndNode(aliceName, outputFunds[:], t.lndHarness) + bobLnd := newLndNode(bobName, outputFunds[:], t.lndHarness) + carolLnd := newLndNode(carolName, outputFunds[:], t.lndHarness) // Now we want to wait for the nodes to catch up. t.lndHarness.WaitForBlockchainSync(aliceLnd) @@ -347,12 +506,59 @@ func newRfqTestScenario(t *harnessTest) *rfqTestScenario { // Cleanup cleans up the test scenario. func (s *rfqTestScenario) Cleanup() { - // Close the LND channels. - s.testHarness.lndHarness.CloseChannel(s.AliceLnd, s.AliceBobChannel) - s.testHarness.lndHarness.CloseChannel(s.BobLnd, s.BobCarolChannel) + s.testHarness.t.Log("Cleaning up test scenario") // Stop the tapd nodes. require.NoError(s.testHarness.t, s.AliceTapd.stop(!*noDelete)) require.NoError(s.testHarness.t, s.BobTapd.stop(!*noDelete)) require.NoError(s.testHarness.t, s.CarolTapd.stop(!*noDelete)) + + // Kill the LND nodes in the test harness node manager. If we don't + // perform this step the LND test harness node manager will continue to + // run the nodes in the background as "active nodes". + s.testHarness.lndHarness.KillNode(s.AliceLnd) + s.testHarness.lndHarness.KillNode(s.BobLnd) + s.testHarness.lndHarness.KillNode(s.CarolLnd) +} + +// randomString generates a random string of the given length. +func randomString(randStrLen int) string { + const letters = "abcdefghijklmnopqrstuvwxyz0123456789" + b := make([]byte, randStrLen) + for i := range b { + b[i] = letters[rand.Intn(len(letters))] + } + return string(b) +} + +// genRandomNodeName generates a random node name by appending a random string +// to the given base name. +func genRandomNodeName(baseName string) string { + return fmt.Sprintf("%s-%s", baseName, randomString(8)) +} + +// BeforeTimeout executes a function in a goroutine with a timeout. It waits for +// the function to finish or for the timeout to expire, whichever happens first. +// If the function exceeds the timeout, it logs a test error. +func BeforeTimeout(t *testing.T, targetFunc func(), + timeout time.Duration) { + + // Create a channel to signal when the target function has completed. + targetExecComplete := make(chan bool, 1) + + // Execute the target function in a goroutine. + go func() { + targetFunc() + targetExecComplete <- true + }() + + // Wait for the target function to complete or timeout. + select { + case <-targetExecComplete: + return + + case <-time.After(timeout): + t.Errorf("targetFunc did not complete within timeout: %v", + timeout) + } } diff --git a/itest/test_list_on_test.go b/itest/test_list_on_test.go index 61fe80665..69f30050f 100644 --- a/itest/test_list_on_test.go +++ b/itest/test_list_on_test.go @@ -251,6 +251,11 @@ var testCases = []*testCase{ name: "rfq asset buy htlc intercept", test: testRfqAssetBuyHtlcIntercept, }, + { + name: "rfq asset sell htlc intercept", + test: testRfqAssetSellHtlcIntercept, + }, + { name: "multi signature on all levels", test: testMultiSignature, diff --git a/perms/perms.go b/perms/perms.go index c008c281a..bf2544e16 100644 --- a/perms/perms.go +++ b/perms/perms.go @@ -224,6 +224,10 @@ var ( Entity: "rfq", Action: "write", }}, + "/rfqrpc.Rfq/AddAssetSellOrder": {{ + Entity: "rfq", + Action: "write", + }}, "/rfqrpc.Rfq/AddAssetSellOffer": {{ Entity: "rfq", Action: "write", diff --git a/rfq/manager.go b/rfq/manager.go index 0c8f5290f..902008e7f 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -84,6 +84,13 @@ type Manager struct { // represent agreed-upon terms for purchase transactions with our peers. peerAcceptedBuyQuotes lnutils.SyncMap[SerialisedScid, rfqmsg.BuyAccept] + // peerAcceptedSellQuotes holds sell quotes for assets that our node has + // requested and that have been accepted by peer nodes. These quotes are + // exclusively used by our node for the sale of assets, as they + // represent agreed-upon terms for sale transactions with our peers. + peerAcceptedSellQuotes lnutils.SyncMap[SerialisedScid, + rfqmsg.SellAccept] + // subscribers is a map of components that want to be notified on new // events, keyed by their subscription ID. subscribers lnutils.SyncMap[uint64, *fn.EventReceiver[fn.Event]] @@ -107,6 +114,8 @@ func NewManager(cfg ManagerCfg) (*Manager, error) { acceptHtlcEvents: make(chan *AcceptHtlcEvent), peerAcceptedBuyQuotes: lnutils.SyncMap[ SerialisedScid, rfqmsg.BuyAccept]{}, + peerAcceptedSellQuotes: lnutils.SyncMap[ + SerialisedScid, rfqmsg.SellAccept]{}, subscribers: lnutils.SyncMap[ uint64, *fn.EventReceiver[fn.Event]]{}, @@ -281,6 +290,28 @@ func (m *Manager) handleIncomingMessage(incomingMsg rfqmsg.IncomingMsg) error { event := NewPeerAcceptedBuyQuoteEvent(msg) m.publishSubscriberEvent(event) + case *rfqmsg.SellRequest: + err := m.negotiator.HandleIncomingSellRequest(*msg) + if err != nil { + return fmt.Errorf("error handling incoming sell "+ + "request: %w", err) + } + + case *rfqmsg.SellAccept: + // TODO(ffranr): The stream handler should ensure that the + // accept message corresponds to a request. + // + // The quote request has been accepted. Store accepted quote + // so that it can be used to send a payment by our lightning + // node. + scid := SerialisedScid(msg.ShortChannelId()) + m.peerAcceptedSellQuotes.Store(scid, *msg) + + // Notify subscribers of the incoming peer accepted asset sell + // quote. + event := NewPeerAcceptedSellQuoteEvent(msg) + m.publishSubscriberEvent(event) + case *rfqmsg.Reject: // The quote request has been rejected. Notify subscribers of // the rejection. @@ -298,11 +329,22 @@ func (m *Manager) handleIncomingMessage(incomingMsg rfqmsg.IncomingMsg) error { // messages that will be sent to a peer. func (m *Manager) handleOutgoingMessage(outgoingMsg rfqmsg.OutgoingMsg) error { // Perform type specific handling of the outgoing message. - msg, ok := outgoingMsg.(*rfqmsg.BuyAccept) - if ok { - // Before sending an accept message to a peer, inform the HTLC - // order handler that we've accepted the quote request. + switch msg := outgoingMsg.(type) { + case *rfqmsg.BuyAccept: + // A peer sent us an asset buy quote request in an attempt to + // buy an asset from us. Having accepted the request, but before + // we inform our peer of our decision, we inform the order + // handler that we are willing to sell the asset subject to a + // sale policy. m.orderHandler.RegisterAssetSalePolicy(*msg) + + case *rfqmsg.SellAccept: + // A peer sent us an asset sell quote request in an attempt to + // sell an asset to us. Having accepted the request, but before + // we inform our peer of our decision, we inform the order + // handler that we are willing to buy the asset subject to a + // purchase policy. + m.orderHandler.RegisterAssetPurchasePolicy(*msg) } // Send the outgoing message to the peer. @@ -427,13 +469,55 @@ func (m *Manager) UpsertAssetBuyOrder(order BuyOrder) error { return nil } +// SellOrder is a struct that represents an asset sell order. +type SellOrder struct { + // AssetID is the ID of the asset to sell. + AssetID *asset.ID + + // AssetGroupKey is the public key of the asset group to sell. + AssetGroupKey *btcec.PublicKey + + // MaxAssetAmount is the maximum amount of the asset that can be sold as + // part of the order. + MaxAssetAmount uint64 + + // MinAsk is the minimum ask price that the seller is willing to accept. + MinAsk lnwire.MilliSatoshi + + // Expiry is the unix timestamp at which the order expires. + Expiry uint64 + + // Peer is the peer that the buy order is intended for. This field is + // optional. + Peer *route.Vertex +} + +// UpsertAssetSellOrder upserts an asset sell order for management. +func (m *Manager) UpsertAssetSellOrder(order SellOrder) error { + // For now, a peer must be specified. + // + // TODO(ffranr): Add support for peerless sell orders. The negotiator + // should be able to determine the optimal peer. + if order.Peer == nil { + return fmt.Errorf("sell order peer must be specified") + } + + // Pass the asset sell order to the negotiator which will generate sell + // request messages to send to peers. + m.negotiator.HandleOutgoingSellOrder(order) + + return nil +} + // QueryPeerAcceptedQuotes returns quotes that were requested by our node and // have been accepted by our peers. These quotes are exclusively available to // our node for the acquisition/sale of assets. -func (m *Manager) QueryPeerAcceptedQuotes() map[SerialisedScid]rfqmsg.BuyAccept { +func (m *Manager) QueryPeerAcceptedQuotes() (map[SerialisedScid]rfqmsg.BuyAccept, + map[SerialisedScid]rfqmsg.SellAccept) { + // Returning the map directly is not thread safe. We will therefore // create a copy. - quotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept) + buyQuotesCopy := make(map[SerialisedScid]rfqmsg.BuyAccept) m.peerAcceptedBuyQuotes.ForEach( func(scid SerialisedScid, accept rfqmsg.BuyAccept) error { @@ -442,12 +526,28 @@ func (m *Manager) QueryPeerAcceptedQuotes() map[SerialisedScid]rfqmsg.BuyAccept return nil } - quotesCopy[scid] = accept + buyQuotesCopy[scid] = accept return nil }, ) - return quotesCopy + // Returning the map directly is not thread safe. We will therefore + // create a copy. + sellQuotesCopy := make(map[SerialisedScid]rfqmsg.SellAccept) + + m.peerAcceptedSellQuotes.ForEach( + func(scid SerialisedScid, accept rfqmsg.SellAccept) error { + if time.Now().Unix() > int64(accept.Expiry) { + m.peerAcceptedBuyQuotes.Delete(scid) + return nil + } + + sellQuotesCopy[scid] = accept + return nil + }, + ) + + return buyQuotesCopy, sellQuotesCopy } // RegisterSubscriber adds a new subscriber to the set of subscribers that will @@ -516,9 +616,40 @@ func (q *PeerAcceptedBuyQuoteEvent) Timestamp() time.Time { return q.timestamp.UTC() } -// Ensure that the PeerAcceptedBuyQuoteEvent struct implements the Event interface. +// Ensure that the PeerAcceptedBuyQuoteEvent struct implements the Event +// interface. var _ fn.Event = (*PeerAcceptedBuyQuoteEvent)(nil) +// PeerAcceptedSellQuoteEvent is an event that is broadcast when the RFQ manager +// receives an asset sell request accept quote message from a peer. This is a +// quote which was requested by our node and has been accepted by a peer. +type PeerAcceptedSellQuoteEvent struct { + // timestamp is the event creation UTC timestamp. + timestamp time.Time + + // SellAccept is the accepted asset sell quote. + rfqmsg.SellAccept +} + +// NewPeerAcceptedSellQuoteEvent creates a new PeerAcceptedSellQuoteEvent. +func NewPeerAcceptedSellQuoteEvent( + sellAccept *rfqmsg.SellAccept) *PeerAcceptedSellQuoteEvent { + + return &PeerAcceptedSellQuoteEvent{ + timestamp: time.Now().UTC(), + SellAccept: *sellAccept, + } +} + +// Timestamp returns the event creation UTC timestamp. +func (q *PeerAcceptedSellQuoteEvent) Timestamp() time.Time { + return q.timestamp.UTC() +} + +// Ensure that the PeerAcceptedSellQuoteEvent struct implements the Event +// interface. +var _ fn.Event = (*PeerAcceptedSellQuoteEvent)(nil) + // IncomingRejectQuoteEvent is an event that is broadcast when the RFQ manager // receives a reject quote message from a peer. type IncomingRejectQuoteEvent struct { diff --git a/rfq/negotiator.go b/rfq/negotiator.go index befb8684f..e7cbbe62b 100644 --- a/rfq/negotiator.go +++ b/rfq/negotiator.go @@ -76,6 +76,13 @@ func (n *Negotiator) queryBidFromPriceOracle(peer route.Vertex, assetId *asset.ID, assetGroupKey *btcec.PublicKey, assetAmount uint64) (lnwire.MilliSatoshi, uint64, error) { + // TODO(ffranr): Optionally accept a peer's proposed ask price as an + // arg to this func and pass it to the price oracle. The price oracle + // service might be intelligent enough to use the peer's proposed ask + // price as a factor when computing the bid price. This argument must + // be optional because at some call sites we are initiating a request + // and do not have a peer's proposed ask price. + ctx, cancel := n.WithCtxQuitNoTimeout() defer cancel() @@ -293,6 +300,116 @@ func (n *Negotiator) HandleIncomingBuyRequest( return nil } +// HandleIncomingSellRequest handles an incoming asset sell quote request. +// This is a request by our peer to sell an asset to us. +func (n *Negotiator) HandleIncomingSellRequest( + request rfqmsg.SellRequest) error { + + // TODO(ffranr): Ensure that we have a suitable buy offer for the asset + // that our peer is trying to sell to us. + + // Define a thread safe helper function for adding outgoing message to + // the outgoing messages channel. + sendOutgoingMsg := func(msg rfqmsg.OutgoingMsg) { + sendSuccess := fn.SendOrQuit( + n.cfg.OutgoingMessages, msg, n.Quit, + ) + if !sendSuccess { + err := fmt.Errorf("negotiator failed to add message "+ + "to the outgoing messages channel (msg=%v)", + msg) + n.cfg.ErrChan <- err + } + } + + // Query the price oracle asynchronously using a separate goroutine. + // The price oracle might be an external service, responses could be + // delayed. + n.Wg.Add(1) + go func() { + defer n.Wg.Done() + + // Query the price oracle for a bid price. This is the price we + // are willing to pay for the asset that our peer is trying to + // sell to us. + bidPrice, bidExpiry, err := n.queryBidFromPriceOracle( + request.Peer, request.AssetID, request.AssetGroupKey, + request.AssetAmount, + ) + if err != nil { + // Send a reject message to the peer. + msg := rfqmsg.NewReject( + request.Peer, request.ID, + rfqmsg.ErrUnknownReject, + ) + sendOutgoingMsg(msg) + + // Add an error to the error channel and return. + err = fmt.Errorf("failed to query ask price from "+ + "oracle: %w", err) + n.cfg.ErrChan <- err + return + } + + // Construct and send a sell accept message. + msg := rfqmsg.NewSellAcceptFromRequest( + request, bidPrice, bidExpiry, + ) + sendOutgoingMsg(msg) + }() + + return nil +} + +// HandleOutgoingSellOrder handles an outgoing sell order by constructing sell +// requests and passing them to the outgoing messages channel. These requests +// are sent to peers. +func (n *Negotiator) HandleOutgoingSellOrder(order SellOrder) { + // Query the price oracle for a reasonable ask price. We perform this + // query and response handling in a separate goroutine in case it is a + // remote service and takes a long time to respond. + n.Wg.Add(1) + go func() { + defer n.Wg.Done() + + // Query the price oracle for an asking price. + askPrice, _, err := n.queryAskFromPriceOracle( + order.Peer, order.AssetID, order.AssetGroupKey, + order.MaxAssetAmount, nil, + ) + if err != nil { + err := fmt.Errorf("negotiator failed to handle price "+ + "oracle response: %w", err) + n.cfg.ErrChan <- err + return + } + + request, err := rfqmsg.NewSellRequest( + *order.Peer, order.AssetID, order.AssetGroupKey, + order.MaxAssetAmount, askPrice, + ) + if err != nil { + err := fmt.Errorf("unable to create sell request "+ + "message: %w", err) + n.cfg.ErrChan <- err + return + } + + // Send the response message to the outgoing messages channel. + var msg rfqmsg.OutgoingMsg = request + sendSuccess := fn.SendOrQuit( + n.cfg.OutgoingMessages, msg, n.Quit, + ) + if !sendSuccess { + err := fmt.Errorf("negotiator failed to add sell " + + "request message to the outgoing messages " + + "channel") + n.cfg.ErrChan <- err + return + } + }() +} + // SellOffer is a struct that represents an asset sell offer. This // data structure describes the maximum amount of an asset that is available // for sale. diff --git a/rfq/order.go b/rfq/order.go index e9d71be50..47bf7df79 100644 --- a/rfq/order.go +++ b/rfq/order.go @@ -13,6 +13,35 @@ import ( "github.com/lightningnetwork/lnd/lnwire" ) +const ( + // LnCustomRecordType is a taproot-assets specific lightning payment hop + // custom record K-V value. + LnCustomRecordType = 65536 + uint64(rfqmsg.TapMessageTypeBaseOffset) +) + +// parseHtlcCustomRecords parses a HTLC custom record to extract any data which +// is relevant to the RFQ service. If the custom records map is nil or a +// relevant record was not found, false is returned. +func parseHtlcCustomRecords(customRecords map[uint64][]byte) (*rfqmsg.ID, + bool) { + + // If the given custom records map is nil, we return false. + if customRecords == nil { + return nil, false + } + + // Check for the RFQ custom record key in the custom records map. + val, ok := customRecords[LnCustomRecordType] + if !ok { + return nil, false + } + + // TODO(ffranr): val here should be a TLV. + var quoteId rfqmsg.ID + copy(quoteId[:], val) + return "eId, true +} + // SerialisedScid is a serialised short channel id (SCID). type SerialisedScid uint64 @@ -106,6 +135,90 @@ func (c *AssetSalePolicy) Scid() uint64 { // Ensure that AssetSalePolicy implements the Policy interface. var _ Policy = (*AssetSalePolicy)(nil) +// AssetPurchasePolicy is a struct that holds the terms which determine whether +// an asset purchase channel HTLC is accepted or rejected. +type AssetPurchasePolicy struct { + // scid is the serialised short channel ID (SCID) of the channel to + // which the policy applies. + scid SerialisedScid + + // AcceptedQuoteId is the ID of the accepted quote. + AcceptedQuoteId rfqmsg.ID + + // AssetAmount is the amount of the tap asset that is being requested. + AssetAmount uint64 + + // MinimumChannelPayment is the minimum number of millisatoshis that + // must be sent in the HTLC. + MinimumChannelPayment lnwire.MilliSatoshi + + // expiry is the policy's expiry unix timestamp in seconds after which + // the policy is no longer valid. + expiry uint64 +} + +// NewAssetPurchasePolicy creates a new asset purchase policy. +func NewAssetPurchasePolicy(quote rfqmsg.SellAccept) *AssetPurchasePolicy { + // Compute the serialised short channel ID (SCID) for the channel. + scid := SerialisedScid(quote.ShortChannelId()) + + return &AssetPurchasePolicy{ + scid: scid, + AcceptedQuoteId: quote.ID, + AssetAmount: quote.AssetAmount, + MinimumChannelPayment: quote.BidPrice, + expiry: quote.Expiry, + } +} + +// CheckHtlcCompliance returns an error if the given HTLC intercept descriptor +// does not satisfy the subject policy. +func (c *AssetPurchasePolicy) CheckHtlcCompliance( + htlc lndclient.InterceptedHtlc) error { + + // Check that the HTLC contains the accepted quote ID. + quoteId, ok := parseHtlcCustomRecords(htlc.CustomRecords) + if !ok { + return fmt.Errorf("HTLC does not contain a relevant custom "+ + "record (htlc=%v)", htlc) + } + + if *quoteId != c.AcceptedQuoteId { + return fmt.Errorf("HTLC contains a custom record, but it does "+ + "not contain the accepted quote ID (htlc=%v, "+ + "accepted_quote_id=%v)", htlc, c.AcceptedQuoteId) + } + + // Check that the HTLC amount is at least the minimum acceptable amount. + if htlc.AmountOutMsat < c.MinimumChannelPayment { + return fmt.Errorf("htlc out amount is less than the policy "+ + "minimum (htlc_out_msat=%d, policy_min_msat=%d)", + htlc.AmountOutMsat, c.MinimumChannelPayment) + } + + // Lastly, check to ensure that the policy has not expired. + if time.Now().Unix() > int64(c.expiry) { + return fmt.Errorf("policy has expired (expiry_unix_ts=%d)", + c.expiry) + } + + return nil +} + +// Expiry returns the policy's expiry time as a unix timestamp in seconds. +func (c *AssetPurchasePolicy) Expiry() uint64 { + return c.expiry +} + +// Scid returns the serialised short channel ID (SCID) of the channel to which +// the policy applies. +func (c *AssetPurchasePolicy) Scid() uint64 { + return uint64(c.scid) +} + +// Ensure that AssetPurchasePolicy implements the Policy interface. +var _ Policy = (*AssetPurchasePolicy)(nil) + // OrderHandlerCfg is a struct that holds the configuration parameters for the // order handler service. type OrderHandlerCfg struct { @@ -162,14 +275,13 @@ func (h *OrderHandler) handleIncomingHtlc(_ context.Context, log.Debug("Handling incoming HTLC") - scid := SerialisedScid(htlc.OutgoingChannelID.ToUint64()) - policy, ok := h.fetchPolicy(scid) - - // If a policy does not exist for the channel SCID, we resume the HTLC. - // This is because the HTLC may be relevant to another interceptor - // service. We only reject HTLCs that are relevant to the RFQ service - // and do not comply with a known policy. + // Look up a policy for the HTLC. If a policy does not exist, we resume + // the HTLC. This is because the HTLC may be relevant to another + // interceptor service. We only reject HTLCs that are relevant to the + // RFQ service and do not comply with a known policy. + policy, ok := h.fetchPolicy(htlc) if !ok { + log.Debug("Failed to find a policy for the HTLC. Resuming.") return &lndclient.InterceptedHtlcResponse{ Action: lndclient.InterceptorActionResume, }, nil @@ -277,15 +389,72 @@ func (h *OrderHandler) RegisterAssetSalePolicy(buyAccept rfqmsg.BuyAccept) { h.policies.Store(policy.scid, policy) } -// fetchPolicy fetches a policy given a serialised SCID. If a policy is not -// found, false is returned. Expired policies are not returned and are removed -// from the cache. -func (h *OrderHandler) fetchPolicy(scid SerialisedScid) (Policy, bool) { - policy, ok := h.policies.Load(scid) - if !ok { +// RegisterAssetPurchasePolicy generates and registers an asset buy policy with the +// order handler. This function takes an incoming sell accept message as an +// argument. +func (h *OrderHandler) RegisterAssetPurchasePolicy( + sellAccept rfqmsg.SellAccept) { + + log.Debugf("Order handler is registering an asset buy policy given a "+ + "sell accept message: %s", sellAccept.String()) + + policy := NewAssetPurchasePolicy(sellAccept) + h.policies.Store(policy.scid, policy) +} + +// fetchPolicy fetches a policy which is relevant to a given HTLC. If a policy +// is not found, false is returned. Expired policies are not returned and are +// removed from the cache. +func (h *OrderHandler) fetchPolicy(htlc lndclient.InterceptedHtlc) (Policy, + bool) { + + var ( + foundPolicy *Policy + foundScid *SerialisedScid + ) + + // If the HTLC has a custom record, we check if it is relevant to the + // RFQ service. + quoteId, ok := parseHtlcCustomRecords(htlc.CustomRecords) + if ok { + scid := SerialisedScid(quoteId.Scid()) + policy, ok := h.policies.Load(scid) + if ok { + foundPolicy = &policy + foundScid = &scid + } + } + + // If no policy has been found so far, we attempt to look up a policy by + // the outgoing channel SCID. + if foundPolicy == nil { + scid := SerialisedScid(htlc.OutgoingChannelID.ToUint64()) + policy, ok := h.policies.Load(scid) + if ok { + foundPolicy = &policy + foundScid = &scid + } + } + + // If no policy has been found so far, we attempt to look up a policy by + // the incoming channel SCID. + if foundPolicy == nil { + scid := SerialisedScid(htlc.IncomingCircuitKey.ChanID.ToUint64()) + policy, ok := h.policies.Load(scid) + if ok { + foundPolicy = &policy + foundScid = &scid + } + } + + // If no policy has been found, we return false. + if foundPolicy == nil { return nil, false } + policy := *foundPolicy + scid := *foundScid + // If the policy has expired, return false and clear it from the cache. expireTime := time.Unix(int64(policy.Expiry()), 0).UTC() currentTime := time.Now().UTC() diff --git a/rfq/stream.go b/rfq/stream.go index 21d6c6bbd..6d3dd95a1 100644 --- a/rfq/stream.go +++ b/rfq/stream.go @@ -178,8 +178,8 @@ func (h *StreamHandler) mainEventLoop() { } case errSubCustomMessages := <-h.errRecvRawMessages: - // If we receive an error from the peer message - // subscription, we'll terminate the stream handler. + // Report any error that we receive from the peer + // message subscription. log.Warnf("Error received from stream handler wire "+ "message channel: %v", errSubCustomMessages) diff --git a/rfqmsg/messages.go b/rfqmsg/messages.go index 5796d5436..e733ef9af 100644 --- a/rfqmsg/messages.go +++ b/rfqmsg/messages.go @@ -52,9 +52,17 @@ const ( // message. MsgTypeBuyAccept = TapMessageTypeBaseOffset + 1 + // MsgTypeSellRequest is the message type identifier for an asset sell + // quote request message. + MsgTypeSellRequest = TapMessageTypeBaseOffset + 2 + + // MsgTypeSellAccept is the message type identifier for an asset sell + // quote accept message. + MsgTypeSellAccept = TapMessageTypeBaseOffset + 3 + // MsgTypeReject is the message type identifier for a quote // reject message. - MsgTypeReject = TapMessageTypeBaseOffset + 2 + MsgTypeReject = TapMessageTypeBaseOffset + 4 ) var ( @@ -82,6 +90,10 @@ func NewIncomingMsgFromWire(wireMsg WireMessage) (IncomingMsg, error) { return NewBuyRequestMsgFromWire(wireMsg) case MsgTypeBuyAccept: return NewBuyAcceptFromWireMsg(wireMsg) + case MsgTypeSellRequest: + return NewSellRequestMsgFromWire(wireMsg) + case MsgTypeSellAccept: + return NewSellAcceptFromWireMsg(wireMsg) case MsgTypeReject: return NewQuoteRejectFromWireMsg(wireMsg) default: diff --git a/rfqmsg/sell_accept.go b/rfqmsg/sell_accept.go new file mode 100644 index 000000000..66bdb6106 --- /dev/null +++ b/rfqmsg/sell_accept.go @@ -0,0 +1,197 @@ +package rfqmsg + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // Sell accept message type field TLV types. + + TypeSellAcceptID tlv.Type = 0 + TypeSellAcceptBidPrice tlv.Type = 2 + TypeSellAcceptExpiry tlv.Type = 4 + TypeSellAcceptSignature tlv.Type = 6 +) + +func TypeRecordSellAcceptID(id *ID) tlv.Record { + const recordSize = 32 + + return tlv.MakeStaticRecord( + TypeSellAcceptID, id, recordSize, IdEncoder, IdDecoder, + ) +} + +func TypeRecordSellAcceptBidPrice(bidPrice *lnwire.MilliSatoshi) tlv.Record { + return tlv.MakeStaticRecord( + TypeSellAcceptBidPrice, bidPrice, 8, milliSatoshiEncoder, + milliSatoshiDecoder, + ) +} + +func TypeRecordSellAcceptExpiry(expirySeconds *uint64) tlv.Record { + return tlv.MakePrimitiveRecord(TypeSellAcceptExpiry, expirySeconds) +} + +func TypeRecordSellAcceptSig(sig *[64]byte) tlv.Record { + return tlv.MakePrimitiveRecord(TypeSellAcceptSignature, sig) +} + +// sellAcceptMsgData is a struct that represents the data field of an asset sell +// quote request accept message. +type sellAcceptMsgData struct { + // ID represents the unique identifier of the asset sell quote request + // message that this response is associated with. + ID ID + + // BidPrice is the bid price that the message author is willing to pay + // for the asset that is for sale. + BidPrice lnwire.MilliSatoshi + + // Expiry is the bid price expiry lifetime unix timestamp. + Expiry uint64 + + // sig is a signature over the serialized contents of the message. + sig [64]byte +} + +// records provides all TLV records for encoding/decoding. +func (q *sellAcceptMsgData) records() []tlv.Record { + return []tlv.Record{ + TypeRecordSellAcceptID(&q.ID), + TypeRecordSellAcceptBidPrice(&q.BidPrice), + TypeRecordSellAcceptExpiry(&q.Expiry), + TypeRecordSellAcceptSig(&q.sig), + } +} + +// Encode encodes the structure into a TLV stream. +func (q *sellAcceptMsgData) Encode(writer io.Writer) error { + stream, err := tlv.NewStream(q.records()...) + if err != nil { + return err + } + return stream.Encode(writer) +} + +// Decode decodes the structure from a TLV stream. +func (q *sellAcceptMsgData) Decode(r io.Reader) error { + stream, err := tlv.NewStream(q.records()...) + if err != nil { + return err + } + return stream.DecodeP2P(r) +} + +// Bytes encodes the structure into a TLV stream and returns the bytes. +func (q *sellAcceptMsgData) Bytes() ([]byte, error) { + var b bytes.Buffer + err := q.Encode(&b) + if err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +// SellAccept is a struct that represents a sell quote request accept message. +type SellAccept struct { + // Peer is the peer that sent the quote request. + Peer route.Vertex + + // AssetAmount is the amount of the asset that the accept message + // is for. + AssetAmount uint64 + + // sellAcceptMsgData is the message data for the quote accept message. + sellAcceptMsgData +} + +// NewSellAcceptFromRequest creates a new instance of an asset sell quote accept +// message given an asset sell quote request message. +func NewSellAcceptFromRequest(request SellRequest, bidPrice lnwire.MilliSatoshi, + expiry uint64) *SellAccept { + + return &SellAccept{ + Peer: request.Peer, + AssetAmount: request.AssetAmount, + sellAcceptMsgData: sellAcceptMsgData{ + ID: request.ID, + BidPrice: bidPrice, + Expiry: expiry, + }, + } +} + +// NewSellAcceptFromWireMsg instantiates a new instance from a wire message. +func NewSellAcceptFromWireMsg(wireMsg WireMessage) (*SellAccept, error) { + // Ensure that the message type is an accept message. + if wireMsg.MsgType != MsgTypeSellAccept { + return nil, fmt.Errorf("unable to create an asset sell "+ + "accept message from wire message of type %d", + wireMsg.MsgType) + } + + // Decode message data component from TLV bytes. + var msgData sellAcceptMsgData + err := msgData.Decode(bytes.NewReader(wireMsg.Data)) + if err != nil { + return nil, fmt.Errorf("unable to decode sell accept "+ + "message data: %w", err) + } + + return &SellAccept{ + Peer: wireMsg.Peer, + sellAcceptMsgData: msgData, + }, nil +} + +// ShortChannelId returns the short channel ID associated with the asset sale +// event. +func (q *SellAccept) ShortChannelId() SerialisedScid { + // Given valid RFQ message id, we then define a RFQ short chain id + // (SCID) by taking the last 8 bytes of the RFQ message id and + // interpreting them as a 64-bit integer. + scidBytes := q.ID[24:] + + scidInteger := binary.BigEndian.Uint64(scidBytes) + return SerialisedScid(scidInteger) +} + +// ToWire returns a wire message with a serialized data field. +// +// TODO(ffranr): This method should accept a signer so that we can generate a +// signature over the message data. +func (q *SellAccept) ToWire() (WireMessage, error) { + // Encode message data component as TLV bytes. + msgDataBytes, err := q.sellAcceptMsgData.Bytes() + if err != nil { + return WireMessage{}, fmt.Errorf("unable to encode message "+ + "data: %w", err) + } + + return WireMessage{ + Peer: q.Peer, + MsgType: MsgTypeSellAccept, + Data: msgDataBytes, + }, nil +} + +// String returns a human-readable string representation of the message. +func (q *SellAccept) String() string { + return fmt.Sprintf("SellAccept(peer=%x, id=%x, bid_price=%d, "+ + "expiry=%d, scid=%d)", q.Peer[:], q.ID, q.BidPrice, q.Expiry, + q.ShortChannelId()) +} + +// Ensure that the message type implements the OutgoingMsg interface. +var _ OutgoingMsg = (*SellAccept)(nil) + +// Ensure that the message type implements the IncomingMsg interface. +var _ IncomingMsg = (*SellAccept)(nil) diff --git a/rfqmsg/sell_accept_test.go b/rfqmsg/sell_accept_test.go new file mode 100644 index 000000000..cbd63b216 --- /dev/null +++ b/rfqmsg/sell_accept_test.go @@ -0,0 +1,66 @@ +package rfqmsg + +import ( + "bytes" + "testing" + + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// TestSellAcceptMsgDataEncodeDecode tests the encoding and decoding of a sell +// accept message. +func TestSellAcceptMsgDataEncodeDecode(t *testing.T) { + t.Parallel() + + // Create a random ID. + randomIdBytes := test.RandBytes(32) + id := ID(randomIdBytes) + + // Create a random signature. + randomSigBytes := test.RandBytes(64) + var signature [64]byte + copy(signature[:], randomSigBytes[:]) + + testCases := []struct { + testName string + + id ID + bidPrice lnwire.MilliSatoshi + expiry uint64 + sig [64]byte + }{ + { + testName: "all fields populated with basic values", + id: id, + bidPrice: 1000, + expiry: 42000, + sig: signature, + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(tt *testing.T) { + msg := sellAcceptMsgData{ + ID: tc.id, + BidPrice: tc.bidPrice, + Expiry: tc.expiry, + sig: tc.sig, + } + + // Encode the message. + reqBytes, err := msg.Bytes() + require.NoError(tt, err, "unable to encode message") + + // Decode the message. + decodedMsg := sellAcceptMsgData{} + err = decodedMsg.Decode(bytes.NewReader(reqBytes)) + require.NoError(tt, err, "unable to decode message") + + // Assert that the decoded message is equal to the + // original message. + require.Equal(tt, msg, decodedMsg) + }) + } +} diff --git a/rfqmsg/sell_request.go b/rfqmsg/sell_request.go new file mode 100644 index 000000000..c4fbeb82c --- /dev/null +++ b/rfqmsg/sell_request.go @@ -0,0 +1,274 @@ +package rfqmsg + +import ( + "bytes" + "crypto/rand" + "crypto/sha256" + "fmt" + "io" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // SellRequest message type field TLV types. + + TypeSellRequestID tlv.Type = 0 + TypeSellRequestAssetID tlv.Type = 1 + TypeSellRequestAssetGroupKey tlv.Type = 3 + TypeSellRequestAssetAmount tlv.Type = 4 + TypeSellRequestSuggestedAsk tlv.Type = 6 +) + +func TypeRecordSellRequestID(id *ID) tlv.Record { + const recordSize = 32 + + return tlv.MakeStaticRecord( + TypeSellRequestID, id, recordSize, + IdEncoder, IdDecoder, + ) +} + +func TypeRecordSellRequestAssetID(assetID **asset.ID) tlv.Record { + const recordSize = sha256.Size + + return tlv.MakeStaticRecord( + TypeSellRequestAssetID, assetID, recordSize, + AssetIdEncoder, AssetIdDecoder, + ) +} + +func TypeRecordSellRequestAssetGroupKey(groupKey **btcec.PublicKey) tlv.Record { + const recordSize = btcec.PubKeyBytesLenCompressed + + return tlv.MakeStaticRecord( + TypeSellRequestAssetGroupKey, groupKey, recordSize, + asset.CompressedPubKeyEncoder, asset.CompressedPubKeyDecoder, + ) +} + +func TypeRecordSellRequestAssetAmount(assetAmount *uint64) tlv.Record { + return tlv.MakePrimitiveRecord(TypeSellRequestAssetAmount, assetAmount) +} + +func TypeRecordSellRequestAskPrice(ask *lnwire.MilliSatoshi) tlv.Record { + return tlv.MakeStaticRecord( + TypeSellRequestSuggestedAsk, ask, 8, + milliSatoshiEncoder, milliSatoshiDecoder, + ) +} + +// sellRequestMsgData is a struct that represents the message data from an asset +// sell quote request message. +type sellRequestMsgData struct { + // ID is the unique identifier of the quote request. + ID ID + + // AssetID represents the identifier of the asset for which the peer + // is requesting a quote. + AssetID *asset.ID + + // AssetGroupKey is the public group key of the asset for which the peer + // is requesting a quote. + AssetGroupKey *btcec.PublicKey + + // AssetAmount represents the quantity of the specific asset that the + // peer intends to sell. + AssetAmount uint64 + + // AskPrice is the peer's proposed ask price for the asset amount. This + // is not the final price, but a suggested price that the requesting + // peer is willing to accept. + AskPrice lnwire.MilliSatoshi + + // TODO(ffranr): Add expiry time for suggested ask price. +} + +// Validate ensures that the quote request is valid. +func (q *sellRequestMsgData) Validate() error { + if q.AssetID == nil && q.AssetGroupKey == nil { + return fmt.Errorf("asset id and group key cannot both be nil") + } + + if q.AssetID != nil && q.AssetGroupKey != nil { + return fmt.Errorf("asset id and group key cannot both be " + + "non-nil") + } + + return nil +} + +// EncodeRecords determines the non-nil records to include when encoding an +// at runtime. +func (q *sellRequestMsgData) encodeRecords() []tlv.Record { + var records []tlv.Record + + records = append(records, TypeRecordSellRequestID(&q.ID)) + + if q.AssetID != nil { + records = append( + records, TypeRecordSellRequestAssetID(&q.AssetID), + ) + } + + if q.AssetGroupKey != nil { + record := TypeRecordSellRequestAssetGroupKey(&q.AssetGroupKey) + records = append(records, record) + } + + records = append( + records, TypeRecordSellRequestAssetAmount(&q.AssetAmount), + ) + records = append( + records, TypeRecordSellRequestAskPrice(&q.AskPrice), + ) + + return records +} + +// Encode encodes the structure into a TLV stream. +func (q *sellRequestMsgData) Encode(writer io.Writer) error { + stream, err := tlv.NewStream(q.encodeRecords()...) + if err != nil { + return err + } + return stream.Encode(writer) +} + +// Bytes encodes the structure into a TLV stream and returns the bytes. +func (q *sellRequestMsgData) Bytes() ([]byte, error) { + var b bytes.Buffer + err := q.Encode(&b) + if err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +// DecodeRecords provides all TLV records for decoding. +func (q *sellRequestMsgData) decodeRecords() []tlv.Record { + return []tlv.Record{ + TypeRecordSellRequestID(&q.ID), + TypeRecordSellRequestAssetID(&q.AssetID), + TypeRecordSellRequestAssetGroupKey(&q.AssetGroupKey), + TypeRecordSellRequestAssetAmount(&q.AssetAmount), + TypeRecordSellRequestAskPrice(&q.AskPrice), + } +} + +// Decode decodes the structure from a TLV stream. +func (q *sellRequestMsgData) Decode(r io.Reader) error { + stream, err := tlv.NewStream(q.decodeRecords()...) + if err != nil { + return err + } + return stream.Decode(r) +} + +// SellRequest is a struct that represents a asset sell quote request. +type SellRequest struct { + // Peer is the peer that sent the quote request. + Peer route.Vertex + + // sellRequestMsgData is the message data for the quote request + // message. + sellRequestMsgData +} + +// NewSellRequest creates a new asset sell quote request. +func NewSellRequest(peer route.Vertex, assetID *asset.ID, + assetGroupKey *btcec.PublicKey, assetAmount uint64, + askPrice lnwire.MilliSatoshi) (*SellRequest, error) { + + var id [32]byte + _, err := rand.Read(id[:]) + if err != nil { + return nil, fmt.Errorf("unable to generate random id: %w", err) + } + + return &SellRequest{ + Peer: peer, + sellRequestMsgData: sellRequestMsgData{ + ID: id, + AssetID: assetID, + AssetGroupKey: assetGroupKey, + AssetAmount: assetAmount, + AskPrice: askPrice, + }, + }, nil +} + +// NewSellRequestMsgFromWire instantiates a new instance from a wire message. +func NewSellRequestMsgFromWire(wireMsg WireMessage) (*SellRequest, error) { + // Ensure that the message type is a sell request message. + if wireMsg.MsgType != MsgTypeSellRequest { + return nil, fmt.Errorf("unable to create a sell request "+ + "message from wire message of type %d", wireMsg.MsgType) + } + + // Parse the message data from the wire message. + var msgData sellRequestMsgData + err := msgData.Decode(bytes.NewBuffer(wireMsg.Data)) + if err != nil { + return nil, fmt.Errorf("unable to decode incoming sell "+ + "request message data: %w", err) + } + + req := SellRequest{ + Peer: wireMsg.Peer, + sellRequestMsgData: msgData, + } + + // Perform basic sanity checks on the quote request. + err = req.Validate() + if err != nil { + return nil, fmt.Errorf("unable to validate sell request: %w", + err) + } + + return &req, nil +} + +// Validate ensures that the quote request is valid. +func (q *SellRequest) Validate() error { + return q.sellRequestMsgData.Validate() +} + +// ToWire returns a wire message with a serialized data field. +func (q *SellRequest) ToWire() (WireMessage, error) { + // Encode message data component as TLV bytes. + msgDataBytes, err := q.sellRequestMsgData.Bytes() + if err != nil { + return WireMessage{}, fmt.Errorf("unable to encode message "+ + "data: %w", err) + } + + return WireMessage{ + Peer: q.Peer, + MsgType: MsgTypeSellRequest, + Data: msgDataBytes, + }, nil +} + +// String returns a human-readable string representation of the message. +func (q *SellRequest) String() string { + var groupKeyBytes []byte + if q.AssetGroupKey != nil { + groupKeyBytes = q.AssetGroupKey.SerializeCompressed() + } + + return fmt.Sprintf("SellRequest(peer=%s, id=%x, asset_id=%s, "+ + "asset_group_key=%x, asset_amount=%d, ask_price=%d)", q.Peer, + q.ID, q.AssetID, groupKeyBytes, q.AssetAmount, q.AskPrice) +} + +// Ensure that the message type implements the OutgoingMsg interface. +var _ OutgoingMsg = (*SellRequest)(nil) + +// Ensure that the message type implements the IncomingMsg interface. +var _ IncomingMsg = (*SellRequest)(nil) diff --git a/rfqmsg/sell_request_test.go b/rfqmsg/sell_request_test.go new file mode 100644 index 000000000..5363e97ec --- /dev/null +++ b/rfqmsg/sell_request_test.go @@ -0,0 +1,70 @@ +package rfqmsg + +import ( + "bytes" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// TestSellRequestMsgDataEncodeDecode tests the encoding and decoding of a sell +// request message. +func TestSellRequestMsgDataEncodeDecode(t *testing.T) { + t.Parallel() + + // Create a random ID. + randomIdBytes := test.RandBytes(32) + id := ID(randomIdBytes) + + // Create a random asset ID. + randomAssetIdBytes := test.RandBytes(32) + assetId := asset.ID(randomAssetIdBytes) + + testCases := []struct { + testName string + + id ID + assetId *asset.ID + assetGroupKey *btcec.PublicKey + assetAmount uint64 + askPrice lnwire.MilliSatoshi + }{ + { + testName: "all fields populated with basic values", + id: id, + assetId: &assetId, + assetGroupKey: nil, + assetAmount: 1000, + askPrice: lnwire.MilliSatoshi(42000), + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(tt *testing.T) { + msg := sellRequestMsgData{ + ID: tc.id, + AssetID: tc.assetId, + AssetGroupKey: tc.assetGroupKey, + AssetAmount: tc.assetAmount, + AskPrice: tc.askPrice, + } + + // Encode the message. + reqBytes, err := msg.Bytes() + require.NoError(tt, err, "unable to encode message") + + // Decode the message. + decodedMsg := sellRequestMsgData{} + err = decodedMsg.Decode(bytes.NewReader(reqBytes)) + require.NoError(tt, err, "unable to decode message") + + // Assert that the decoded message is equal to the + // original message. + require.Equal(tt, msg, decodedMsg) + }) + } +} diff --git a/rpcserver.go b/rpcserver.go index c3ef9e8f3..864f094eb 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -5519,6 +5519,70 @@ func (r *rpcServer) AddAssetBuyOrder(_ context.Context, return &rfqrpc.AddAssetBuyOrderResponse{}, nil } +// unmarshalAssetSellOrder unmarshals an asset sell order from the RPC form. +func unmarshalAssetSellOrder( + req *rfqrpc.AddAssetSellOrderRequest) (*rfq.SellOrder, error) { + + assetId, assetGroupKey, err := unmarshalAssetSpecifier( + req.AssetSpecifier, + ) + if err != nil { + return nil, fmt.Errorf("error unmarshalling asset specifier: "+ + "%w", err) + } + + // Unmarshal the peer if specified. + var peer *route.Vertex + if len(req.PeerPubKey) > 0 { + pv, err := route.NewVertexFromBytes(req.PeerPubKey) + if err != nil { + return nil, fmt.Errorf("error unmarshalling peer "+ + "route vertex: %w", err) + } + + peer = &pv + } + + return &rfq.SellOrder{ + AssetID: assetId, + AssetGroupKey: assetGroupKey, + MaxAssetAmount: req.MaxAssetAmount, + MinAsk: lnwire.MilliSatoshi(req.MinAsk), + Expiry: req.Expiry, + Peer: peer, + }, nil +} + +// AddAssetSellOrder upserts a new sell order for the given asset into the RFQ +// manager. If the order already exists for the given asset, it will be updated. +func (r *rpcServer) AddAssetSellOrder(_ context.Context, + req *rfqrpc.AddAssetSellOrderRequest) (*rfqrpc.AddAssetSellOrderResponse, + error) { + + // Unmarshal the order from the RPC form. + sellOrder, err := unmarshalAssetSellOrder(req) + if err != nil { + return nil, fmt.Errorf("error unmarshalling sell order: %w", + err) + } + + var peer string + if sellOrder.Peer != nil { + peer = sellOrder.Peer.String() + } + rpcsLog.Debugf("[AddAssetBuyOrder]: upserting sell order "+ + "(dest_peer=%s)", peer) + + // Upsert the order into the RFQ manager. + err = r.cfg.RfqManager.UpsertAssetSellOrder(*sellOrder) + if err != nil { + return nil, fmt.Errorf("error upserting sell order into RFQ "+ + "manager: %w", err) + } + + return &rfqrpc.AddAssetSellOrderResponse{}, nil +} + // AddAssetSellOffer upserts a new sell offer for the given asset into the // RFQ manager. If the offer already exists for the given asset, it will be // updated. @@ -5554,17 +5618,17 @@ func (r *rpcServer) AddAssetSellOffer(_ context.Context, return &rfqrpc.AddAssetSellOfferResponse{}, nil } -// marshalPeerAcceptedQuotes marshals a map of peer accepted quotes into the RPC -// form. These are quotes that were requested by our node and have been accepted -// by our peers. -func marshalPeerAcceptedQuotes( - peerAcceptedQuotes map[rfq.SerialisedScid]rfqmsg.BuyAccept) []*rfqrpc.PeerAcceptedBuyQuote { +// marshalPeerAcceptedBuyQuotes marshals a map of peer accepted asset buy quotes +// into the RPC form. These are quotes that were requested by our node and have +// been accepted by our peers. +func marshalPeerAcceptedBuyQuotes( + quotes map[rfq.SerialisedScid]rfqmsg.BuyAccept) []*rfqrpc.PeerAcceptedBuyQuote { // Marshal the accepted quotes into the RPC form. rpcQuotes := make( - []*rfqrpc.PeerAcceptedBuyQuote, 0, len(peerAcceptedQuotes), + []*rfqrpc.PeerAcceptedBuyQuote, 0, len(quotes), ) - for scid, quote := range peerAcceptedQuotes { + for scid, quote := range quotes { rpcQuote := &rfqrpc.PeerAcceptedBuyQuote{ Peer: quote.Peer.String(), Id: quote.ID[:], @@ -5579,6 +5643,29 @@ func marshalPeerAcceptedQuotes( return rpcQuotes } +// marshalPeerAcceptedSellQuotes marshals a map of peer accepted asset sell +// quotes into the RPC form. These are quotes that were requested by our node +// and have been accepted by our peers. +func marshalPeerAcceptedSellQuotes( + quotes map[rfq.SerialisedScid]rfqmsg.SellAccept) []*rfqrpc.PeerAcceptedSellQuote { + + // Marshal the accepted quotes into the RPC form. + rpcQuotes := make([]*rfqrpc.PeerAcceptedSellQuote, 0, len(quotes)) + for scid, quote := range quotes { + rpcQuote := &rfqrpc.PeerAcceptedSellQuote{ + Peer: quote.Peer.String(), + Id: quote.ID[:], + Scid: uint64(scid), + AssetAmount: quote.AssetAmount, + BidPrice: uint64(quote.BidPrice), + Expiry: quote.Expiry, + } + rpcQuotes = append(rpcQuotes, rpcQuote) + } + + return rpcQuotes +} + // QueryPeerAcceptedQuotes is used to query for quotes that were requested by // our node and have been accepted our peers. func (r *rpcServer) QueryPeerAcceptedQuotes(_ context.Context, @@ -5587,18 +5674,21 @@ func (r *rpcServer) QueryPeerAcceptedQuotes(_ context.Context, // Query the RFQ manager for quotes that were requested by our node and // have been accepted by our peers. - peerAcceptedQuotes := r.cfg.RfqManager.QueryPeerAcceptedQuotes() + peerAcceptedBuyQuotes, peerAcceptedSellQuotes := + r.cfg.RfqManager.QueryPeerAcceptedQuotes() - rpcQuotes := marshalPeerAcceptedQuotes(peerAcceptedQuotes) + rpcBuyQuotes := marshalPeerAcceptedBuyQuotes(peerAcceptedBuyQuotes) + rpcSellQuotes := marshalPeerAcceptedSellQuotes(peerAcceptedSellQuotes) return &rfqrpc.QueryPeerAcceptedQuotesResponse{ - BuyQuotes: rpcQuotes, + BuyQuotes: rpcBuyQuotes, + SellQuotes: rpcSellQuotes, }, nil } // marshallRfqEvent marshals an RFQ event into the RPC form. func marshallRfqEvent(eventInterface fn.Event) (*rfqrpc.RfqEvent, error) { - timestamp := eventInterface.Timestamp().UTC().Unix() + timestamp := eventInterface.Timestamp().UTC().UnixMicro() switch event := eventInterface.(type) { case *rfq.PeerAcceptedBuyQuoteEvent: @@ -5621,6 +5711,26 @@ func marshallRfqEvent(eventInterface fn.Event) (*rfqrpc.RfqEvent, error) { Event: eventRpc, }, nil + case *rfq.PeerAcceptedSellQuoteEvent: + acceptedQuote := &rfqrpc.PeerAcceptedSellQuote{ + Peer: event.Peer.String(), + Id: event.ID[:], + Scid: uint64(event.ShortChannelId()), + AssetAmount: event.AssetAmount, + BidPrice: uint64(event.BidPrice), + Expiry: event.Expiry, + } + + eventRpc := &rfqrpc.RfqEvent_PeerAcceptedSellQuote{ + PeerAcceptedSellQuote: &rfqrpc.PeerAcceptedSellQuoteEvent{ + Timestamp: uint64(timestamp), + PeerAcceptedSellQuote: acceptedQuote, + }, + } + return &rfqrpc.RfqEvent{ + Event: eventRpc, + }, nil + case *rfq.AcceptHtlcEvent: eventRpc := &rfqrpc.RfqEvent_AcceptHtlc{ AcceptHtlc: &rfqrpc.AcceptHtlcEvent{ diff --git a/taprpc/rfqrpc/rfq.pb.go b/taprpc/rfqrpc/rfq.pb.go index 49cef41f9..0000a481e 100644 --- a/taprpc/rfqrpc/rfq.pb.go +++ b/taprpc/rfqrpc/rfq.pb.go @@ -257,6 +257,129 @@ func (*AddAssetBuyOrderResponse) Descriptor() ([]byte, []int) { return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{2} } +type AddAssetSellOrderRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // asset_specifier is the subject asset. + AssetSpecifier *AssetSpecifier `protobuf:"bytes,1,opt,name=asset_specifier,json=assetSpecifier,proto3" json:"asset_specifier,omitempty"` + // The maximum amount of the asset to sell. + MaxAssetAmount uint64 `protobuf:"varint,2,opt,name=max_asset_amount,json=maxAssetAmount,proto3" json:"max_asset_amount,omitempty"` + // The minimum amount of BTC to accept (units: millisats). + MinAsk uint64 `protobuf:"varint,3,opt,name=min_ask,json=minAsk,proto3" json:"min_ask,omitempty"` + // The unix timestamp in seconds after which the order is no longer valid. + Expiry uint64 `protobuf:"varint,4,opt,name=expiry,proto3" json:"expiry,omitempty"` + // peer_pub_key is an optional field for specifying the public key of the + // intended recipient peer for the order. + PeerPubKey []byte `protobuf:"bytes,5,opt,name=peer_pub_key,json=peerPubKey,proto3" json:"peer_pub_key,omitempty"` +} + +func (x *AddAssetSellOrderRequest) Reset() { + *x = AddAssetSellOrderRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddAssetSellOrderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddAssetSellOrderRequest) ProtoMessage() {} + +func (x *AddAssetSellOrderRequest) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddAssetSellOrderRequest.ProtoReflect.Descriptor instead. +func (*AddAssetSellOrderRequest) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{3} +} + +func (x *AddAssetSellOrderRequest) GetAssetSpecifier() *AssetSpecifier { + if x != nil { + return x.AssetSpecifier + } + return nil +} + +func (x *AddAssetSellOrderRequest) GetMaxAssetAmount() uint64 { + if x != nil { + return x.MaxAssetAmount + } + return 0 +} + +func (x *AddAssetSellOrderRequest) GetMinAsk() uint64 { + if x != nil { + return x.MinAsk + } + return 0 +} + +func (x *AddAssetSellOrderRequest) GetExpiry() uint64 { + if x != nil { + return x.Expiry + } + return 0 +} + +func (x *AddAssetSellOrderRequest) GetPeerPubKey() []byte { + if x != nil { + return x.PeerPubKey + } + return nil +} + +type AddAssetSellOrderResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *AddAssetSellOrderResponse) Reset() { + *x = AddAssetSellOrderResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddAssetSellOrderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddAssetSellOrderResponse) ProtoMessage() {} + +func (x *AddAssetSellOrderResponse) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddAssetSellOrderResponse.ProtoReflect.Descriptor instead. +func (*AddAssetSellOrderResponse) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{4} +} + type AddAssetSellOfferRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -271,7 +394,7 @@ type AddAssetSellOfferRequest struct { func (x *AddAssetSellOfferRequest) Reset() { *x = AddAssetSellOfferRequest{} if protoimpl.UnsafeEnabled { - mi := &file_rfqrpc_rfq_proto_msgTypes[3] + mi := &file_rfqrpc_rfq_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -284,7 +407,7 @@ func (x *AddAssetSellOfferRequest) String() string { func (*AddAssetSellOfferRequest) ProtoMessage() {} func (x *AddAssetSellOfferRequest) ProtoReflect() protoreflect.Message { - mi := &file_rfqrpc_rfq_proto_msgTypes[3] + mi := &file_rfqrpc_rfq_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -297,7 +420,7 @@ func (x *AddAssetSellOfferRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAssetSellOfferRequest.ProtoReflect.Descriptor instead. func (*AddAssetSellOfferRequest) Descriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{3} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{5} } func (x *AddAssetSellOfferRequest) GetAssetSpecifier() *AssetSpecifier { @@ -323,7 +446,7 @@ type AddAssetSellOfferResponse struct { func (x *AddAssetSellOfferResponse) Reset() { *x = AddAssetSellOfferResponse{} if protoimpl.UnsafeEnabled { - mi := &file_rfqrpc_rfq_proto_msgTypes[4] + mi := &file_rfqrpc_rfq_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -336,7 +459,7 @@ func (x *AddAssetSellOfferResponse) String() string { func (*AddAssetSellOfferResponse) ProtoMessage() {} func (x *AddAssetSellOfferResponse) ProtoReflect() protoreflect.Message { - mi := &file_rfqrpc_rfq_proto_msgTypes[4] + mi := &file_rfqrpc_rfq_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -349,7 +472,7 @@ func (x *AddAssetSellOfferResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AddAssetSellOfferResponse.ProtoReflect.Descriptor instead. func (*AddAssetSellOfferResponse) Descriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{4} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{6} } type QueryPeerAcceptedQuotesRequest struct { @@ -361,7 +484,7 @@ type QueryPeerAcceptedQuotesRequest struct { func (x *QueryPeerAcceptedQuotesRequest) Reset() { *x = QueryPeerAcceptedQuotesRequest{} if protoimpl.UnsafeEnabled { - mi := &file_rfqrpc_rfq_proto_msgTypes[5] + mi := &file_rfqrpc_rfq_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -374,7 +497,7 @@ func (x *QueryPeerAcceptedQuotesRequest) String() string { func (*QueryPeerAcceptedQuotesRequest) ProtoMessage() {} func (x *QueryPeerAcceptedQuotesRequest) ProtoReflect() protoreflect.Message { - mi := &file_rfqrpc_rfq_proto_msgTypes[5] + mi := &file_rfqrpc_rfq_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -387,7 +510,7 @@ func (x *QueryPeerAcceptedQuotesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryPeerAcceptedQuotesRequest.ProtoReflect.Descriptor instead. func (*QueryPeerAcceptedQuotesRequest) Descriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{5} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{7} } type PeerAcceptedBuyQuote struct { @@ -413,7 +536,7 @@ type PeerAcceptedBuyQuote struct { func (x *PeerAcceptedBuyQuote) Reset() { *x = PeerAcceptedBuyQuote{} if protoimpl.UnsafeEnabled { - mi := &file_rfqrpc_rfq_proto_msgTypes[6] + mi := &file_rfqrpc_rfq_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -426,7 +549,7 @@ func (x *PeerAcceptedBuyQuote) String() string { func (*PeerAcceptedBuyQuote) ProtoMessage() {} func (x *PeerAcceptedBuyQuote) ProtoReflect() protoreflect.Message { - mi := &file_rfqrpc_rfq_proto_msgTypes[6] + mi := &file_rfqrpc_rfq_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -439,7 +562,7 @@ func (x *PeerAcceptedBuyQuote) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerAcceptedBuyQuote.ProtoReflect.Descriptor instead. func (*PeerAcceptedBuyQuote) Descriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{6} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{8} } func (x *PeerAcceptedBuyQuote) GetPeer() string { @@ -484,6 +607,100 @@ func (x *PeerAcceptedBuyQuote) GetExpiry() uint64 { return 0 } +type PeerAcceptedSellQuote struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Quote counterparty peer. + Peer string `protobuf:"bytes,1,opt,name=peer,proto3" json:"peer,omitempty"` + // The unique identifier of the quote request. + Id []byte `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + // scid is the short channel ID of the channel over which the payment for + // the quote should be made. + Scid uint64 `protobuf:"varint,3,opt,name=scid,proto3" json:"scid,omitempty"` + // asset_amount is the amount of the subject asset. + AssetAmount uint64 `protobuf:"varint,4,opt,name=asset_amount,json=assetAmount,proto3" json:"asset_amount,omitempty"` + // bid_price is the price in millisats for the entire asset amount. + BidPrice uint64 `protobuf:"varint,5,opt,name=bid_price,json=bidPrice,proto3" json:"bid_price,omitempty"` + // The unix timestamp in seconds after which the quote is no longer valid. + Expiry uint64 `protobuf:"varint,6,opt,name=expiry,proto3" json:"expiry,omitempty"` +} + +func (x *PeerAcceptedSellQuote) Reset() { + *x = PeerAcceptedSellQuote{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PeerAcceptedSellQuote) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerAcceptedSellQuote) ProtoMessage() {} + +func (x *PeerAcceptedSellQuote) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerAcceptedSellQuote.ProtoReflect.Descriptor instead. +func (*PeerAcceptedSellQuote) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{9} +} + +func (x *PeerAcceptedSellQuote) GetPeer() string { + if x != nil { + return x.Peer + } + return "" +} + +func (x *PeerAcceptedSellQuote) GetId() []byte { + if x != nil { + return x.Id + } + return nil +} + +func (x *PeerAcceptedSellQuote) GetScid() uint64 { + if x != nil { + return x.Scid + } + return 0 +} + +func (x *PeerAcceptedSellQuote) GetAssetAmount() uint64 { + if x != nil { + return x.AssetAmount + } + return 0 +} + +func (x *PeerAcceptedSellQuote) GetBidPrice() uint64 { + if x != nil { + return x.BidPrice + } + return 0 +} + +func (x *PeerAcceptedSellQuote) GetExpiry() uint64 { + if x != nil { + return x.Expiry + } + return 0 +} + type QueryPeerAcceptedQuotesResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -492,12 +709,15 @@ type QueryPeerAcceptedQuotesResponse struct { // buy_quotes is a list of asset buy quotes which were requested by our // node and have been accepted by our peers. BuyQuotes []*PeerAcceptedBuyQuote `protobuf:"bytes,1,rep,name=buy_quotes,json=buyQuotes,proto3" json:"buy_quotes,omitempty"` + // sell_quotes is a list of asset sell quotes which were requested by our + // node and have been accepted by our peers. + SellQuotes []*PeerAcceptedSellQuote `protobuf:"bytes,2,rep,name=sell_quotes,json=sellQuotes,proto3" json:"sell_quotes,omitempty"` } func (x *QueryPeerAcceptedQuotesResponse) Reset() { *x = QueryPeerAcceptedQuotesResponse{} if protoimpl.UnsafeEnabled { - mi := &file_rfqrpc_rfq_proto_msgTypes[7] + mi := &file_rfqrpc_rfq_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -510,7 +730,7 @@ func (x *QueryPeerAcceptedQuotesResponse) String() string { func (*QueryPeerAcceptedQuotesResponse) ProtoMessage() {} func (x *QueryPeerAcceptedQuotesResponse) ProtoReflect() protoreflect.Message { - mi := &file_rfqrpc_rfq_proto_msgTypes[7] + mi := &file_rfqrpc_rfq_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -523,7 +743,7 @@ func (x *QueryPeerAcceptedQuotesResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use QueryPeerAcceptedQuotesResponse.ProtoReflect.Descriptor instead. func (*QueryPeerAcceptedQuotesResponse) Descriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{7} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{10} } func (x *QueryPeerAcceptedQuotesResponse) GetBuyQuotes() []*PeerAcceptedBuyQuote { @@ -533,6 +753,13 @@ func (x *QueryPeerAcceptedQuotesResponse) GetBuyQuotes() []*PeerAcceptedBuyQuote return nil } +func (x *QueryPeerAcceptedQuotesResponse) GetSellQuotes() []*PeerAcceptedSellQuote { + if x != nil { + return x.SellQuotes + } + return nil +} + type SubscribeRfqEventNtfnsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -542,7 +769,7 @@ type SubscribeRfqEventNtfnsRequest struct { func (x *SubscribeRfqEventNtfnsRequest) Reset() { *x = SubscribeRfqEventNtfnsRequest{} if protoimpl.UnsafeEnabled { - mi := &file_rfqrpc_rfq_proto_msgTypes[8] + mi := &file_rfqrpc_rfq_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -555,7 +782,7 @@ func (x *SubscribeRfqEventNtfnsRequest) String() string { func (*SubscribeRfqEventNtfnsRequest) ProtoMessage() {} func (x *SubscribeRfqEventNtfnsRequest) ProtoReflect() protoreflect.Message { - mi := &file_rfqrpc_rfq_proto_msgTypes[8] + mi := &file_rfqrpc_rfq_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -568,7 +795,7 @@ func (x *SubscribeRfqEventNtfnsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SubscribeRfqEventNtfnsRequest.ProtoReflect.Descriptor instead. func (*SubscribeRfqEventNtfnsRequest) Descriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{8} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{11} } type PeerAcceptedBuyQuoteEvent struct { @@ -585,7 +812,7 @@ type PeerAcceptedBuyQuoteEvent struct { func (x *PeerAcceptedBuyQuoteEvent) Reset() { *x = PeerAcceptedBuyQuoteEvent{} if protoimpl.UnsafeEnabled { - mi := &file_rfqrpc_rfq_proto_msgTypes[9] + mi := &file_rfqrpc_rfq_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -598,7 +825,7 @@ func (x *PeerAcceptedBuyQuoteEvent) String() string { func (*PeerAcceptedBuyQuoteEvent) ProtoMessage() {} func (x *PeerAcceptedBuyQuoteEvent) ProtoReflect() protoreflect.Message { - mi := &file_rfqrpc_rfq_proto_msgTypes[9] + mi := &file_rfqrpc_rfq_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -611,7 +838,7 @@ func (x *PeerAcceptedBuyQuoteEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use PeerAcceptedBuyQuoteEvent.ProtoReflect.Descriptor instead. func (*PeerAcceptedBuyQuoteEvent) Descriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{9} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{12} } func (x *PeerAcceptedBuyQuoteEvent) GetTimestamp() uint64 { @@ -628,6 +855,63 @@ func (x *PeerAcceptedBuyQuoteEvent) GetPeerAcceptedBuyQuote() *PeerAcceptedBuyQu return nil } +type PeerAcceptedSellQuoteEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Unix timestamp in microseconds. + Timestamp uint64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // The asset sell quote that was accepted by out peer. + PeerAcceptedSellQuote *PeerAcceptedSellQuote `protobuf:"bytes,2,opt,name=peer_accepted_sell_quote,json=peerAcceptedSellQuote,proto3" json:"peer_accepted_sell_quote,omitempty"` +} + +func (x *PeerAcceptedSellQuoteEvent) Reset() { + *x = PeerAcceptedSellQuoteEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PeerAcceptedSellQuoteEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerAcceptedSellQuoteEvent) ProtoMessage() {} + +func (x *PeerAcceptedSellQuoteEvent) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerAcceptedSellQuoteEvent.ProtoReflect.Descriptor instead. +func (*PeerAcceptedSellQuoteEvent) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{13} +} + +func (x *PeerAcceptedSellQuoteEvent) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *PeerAcceptedSellQuoteEvent) GetPeerAcceptedSellQuote() *PeerAcceptedSellQuote { + if x != nil { + return x.PeerAcceptedSellQuote + } + return nil +} + type AcceptHtlcEvent struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -643,7 +927,7 @@ type AcceptHtlcEvent struct { func (x *AcceptHtlcEvent) Reset() { *x = AcceptHtlcEvent{} if protoimpl.UnsafeEnabled { - mi := &file_rfqrpc_rfq_proto_msgTypes[10] + mi := &file_rfqrpc_rfq_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -656,7 +940,7 @@ func (x *AcceptHtlcEvent) String() string { func (*AcceptHtlcEvent) ProtoMessage() {} func (x *AcceptHtlcEvent) ProtoReflect() protoreflect.Message { - mi := &file_rfqrpc_rfq_proto_msgTypes[10] + mi := &file_rfqrpc_rfq_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -669,7 +953,7 @@ func (x *AcceptHtlcEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use AcceptHtlcEvent.ProtoReflect.Descriptor instead. func (*AcceptHtlcEvent) Descriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{10} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{14} } func (x *AcceptHtlcEvent) GetTimestamp() uint64 { @@ -694,6 +978,7 @@ type RfqEvent struct { // Types that are assignable to Event: // // *RfqEvent_PeerAcceptedBuyQuote + // *RfqEvent_PeerAcceptedSellQuote // *RfqEvent_AcceptHtlc Event isRfqEvent_Event `protobuf_oneof:"event"` } @@ -701,7 +986,7 @@ type RfqEvent struct { func (x *RfqEvent) Reset() { *x = RfqEvent{} if protoimpl.UnsafeEnabled { - mi := &file_rfqrpc_rfq_proto_msgTypes[11] + mi := &file_rfqrpc_rfq_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -714,7 +999,7 @@ func (x *RfqEvent) String() string { func (*RfqEvent) ProtoMessage() {} func (x *RfqEvent) ProtoReflect() protoreflect.Message { - mi := &file_rfqrpc_rfq_proto_msgTypes[11] + mi := &file_rfqrpc_rfq_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -727,7 +1012,7 @@ func (x *RfqEvent) ProtoReflect() protoreflect.Message { // Deprecated: Use RfqEvent.ProtoReflect.Descriptor instead. func (*RfqEvent) Descriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{11} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{15} } func (m *RfqEvent) GetEvent() isRfqEvent_Event { @@ -744,6 +1029,13 @@ func (x *RfqEvent) GetPeerAcceptedBuyQuote() *PeerAcceptedBuyQuoteEvent { return nil } +func (x *RfqEvent) GetPeerAcceptedSellQuote() *PeerAcceptedSellQuoteEvent { + if x, ok := x.GetEvent().(*RfqEvent_PeerAcceptedSellQuote); ok { + return x.PeerAcceptedSellQuote + } + return nil +} + func (x *RfqEvent) GetAcceptHtlc() *AcceptHtlcEvent { if x, ok := x.GetEvent().(*RfqEvent_AcceptHtlc); ok { return x.AcceptHtlc @@ -761,14 +1053,22 @@ type RfqEvent_PeerAcceptedBuyQuote struct { PeerAcceptedBuyQuote *PeerAcceptedBuyQuoteEvent `protobuf:"bytes,1,opt,name=peer_accepted_buy_quote,json=peerAcceptedBuyQuote,proto3,oneof"` } +type RfqEvent_PeerAcceptedSellQuote struct { + // peer_accepted_sell_offer is an event that is emitted when a peer + // accepted (incoming) asset sell quote message is received. + PeerAcceptedSellQuote *PeerAcceptedSellQuoteEvent `protobuf:"bytes,2,opt,name=peer_accepted_sell_quote,json=peerAcceptedSellQuote,proto3,oneof"` +} + type RfqEvent_AcceptHtlc struct { // accept_htlc is an event that is sent when a HTLC is accepted by the // RFQ service. - AcceptHtlc *AcceptHtlcEvent `protobuf:"bytes,2,opt,name=accept_htlc,json=acceptHtlc,proto3,oneof"` + AcceptHtlc *AcceptHtlcEvent `protobuf:"bytes,3,opt,name=accept_htlc,json=acceptHtlc,proto3,oneof"` } func (*RfqEvent_PeerAcceptedBuyQuote) isRfqEvent_Event() {} +func (*RfqEvent_PeerAcceptedSellQuote) isRfqEvent_Event() {} + func (*RfqEvent_AcceptHtlc) isRfqEvent_Event() {} var File_rfqrpc_rfq_proto protoreflect.FileDescriptor @@ -801,89 +1101,140 @@ var file_rfqrpc_rfq_proto_rawDesc = []byte{ 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x1a, 0x0a, 0x18, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x78, 0x0a, 0x18, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, - 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x61, - 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, - 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0e, 0x61, 0x73, - 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, - 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x08, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x69, 0x74, 0x73, 0x22, 0x1b, 0x0a, 0x19, 0x41, 0x64, 0x64, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, 0x0a, 0x1e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, - 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xa6, 0x01, 0x0a, 0x14, 0x50, 0x65, 0x65, - 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x65, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x70, 0x65, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x63, 0x69, 0x64, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x63, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x73, 0x73, - 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, - 0x61, 0x73, 0x6b, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, - 0x08, 0x61, 0x73, 0x6b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, - 0x69, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, - 0x79, 0x22, 0x5e, 0x0a, 0x1f, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, - 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x62, 0x75, 0x79, 0x5f, 0x71, 0x75, 0x6f, 0x74, - 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, - 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, - 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x09, 0x62, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, - 0x73, 0x22, 0x1f, 0x0a, 0x1d, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, - 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x22, 0x8e, 0x01, 0x0a, 0x19, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, - 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x53, - 0x0a, 0x17, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, - 0x62, 0x75, 0x79, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1c, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, - 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x14, 0x70, - 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, - 0x6f, 0x74, 0x65, 0x22, 0x43, 0x0a, 0x0f, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x48, 0x74, 0x6c, - 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x63, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x04, 0x73, 0x63, 0x69, 0x64, 0x22, 0xab, 0x01, 0x0a, 0x08, 0x52, 0x66, 0x71, - 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x5a, 0x0a, 0x17, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x61, 0x63, - 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x75, 0x79, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, - 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, - 0x75, 0x6f, 0x74, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x14, 0x70, 0x65, 0x65, - 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, - 0x65, 0x12, 0x3a, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x5f, 0x68, 0x74, 0x6c, 0x63, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, - 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, - 0x00, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x42, 0x07, 0x0a, - 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x32, 0xf7, 0x02, 0x0a, 0x03, 0x52, 0x66, 0x71, 0x12, 0x55, - 0x0a, 0x10, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, - 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, - 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, - 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, - 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x72, - 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, - 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x6a, 0x0a, 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, - 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x72, 0x66, 0x71, - 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, - 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, - 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, - 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x16, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x53, - 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, - 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x72, - 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, - 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, - 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, - 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, - 0x70, 0x63, 0x2f, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0xd8, 0x01, 0x0a, 0x18, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0f, + 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0e, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x28, 0x0a, + 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x6d, 0x61, 0x78, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x61, + 0x73, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x41, 0x73, 0x6b, + 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0c, 0x70, 0x65, 0x65, 0x72, + 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, + 0x70, 0x65, 0x65, 0x72, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x22, 0x1b, 0x0a, 0x19, 0x41, 0x64, + 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x78, 0x0a, 0x18, 0x41, 0x64, 0x64, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, + 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, + 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x52, 0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x75, 0x6e, 0x69, 0x74, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x55, 0x6e, 0x69, 0x74, + 0x73, 0x22, 0x1b, 0x0a, 0x19, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, + 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20, + 0x0a, 0x1e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, + 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x22, 0xa6, 0x01, 0x0a, 0x14, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, + 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x65, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x65, 0x65, 0x72, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, + 0x04, 0x73, 0x63, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x63, 0x69, + 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x73, 0x6b, 0x5f, 0x70, 0x72, 0x69, 0x63, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x61, 0x73, 0x6b, 0x50, 0x72, 0x69, 0x63, + 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x22, 0xa7, 0x01, 0x0a, 0x15, 0x50, 0x65, + 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x6c, 0x6c, 0x51, 0x75, + 0x6f, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x65, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x65, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x63, 0x69, 0x64, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x63, 0x69, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, + 0x0a, 0x09, 0x62, 0x69, 0x64, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x08, 0x62, 0x69, 0x64, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x65, + 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x79, 0x22, 0x9e, 0x01, 0x0a, 0x1f, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, + 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x62, 0x75, 0x79, 0x5f, 0x71, + 0x75, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x66, + 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, + 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x09, 0x62, 0x75, 0x79, 0x51, 0x75, + 0x6f, 0x74, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0b, 0x73, 0x65, 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x6f, + 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x66, 0x71, 0x72, + 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x53, + 0x65, 0x6c, 0x6c, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x0a, 0x73, 0x65, 0x6c, 0x6c, 0x51, 0x75, + 0x6f, 0x74, 0x65, 0x73, 0x22, 0x1f, 0x0a, 0x1d, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, + 0x65, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x8e, 0x01, 0x0a, 0x19, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x12, 0x53, 0x0a, 0x17, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, + 0x65, 0x64, 0x5f, 0x62, 0x75, 0x79, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, + 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, + 0x52, 0x14, 0x70, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, + 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x22, 0x92, 0x01, 0x0a, 0x1a, 0x50, 0x65, 0x65, 0x72, 0x41, + 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x6c, 0x6c, 0x51, 0x75, 0x6f, 0x74, 0x65, + 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x12, 0x56, 0x0a, 0x18, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x61, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x6c, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, + 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x53, 0x65, 0x6c, 0x6c, 0x51, + 0x75, 0x6f, 0x74, 0x65, 0x52, 0x15, 0x70, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, + 0x65, 0x64, 0x53, 0x65, 0x6c, 0x6c, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x22, 0x43, 0x0a, 0x0f, 0x41, + 0x63, 0x63, 0x65, 0x70, 0x74, 0x48, 0x74, 0x6c, 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1c, + 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x12, 0x0a, 0x04, + 0x73, 0x63, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x63, 0x69, 0x64, + 0x22, 0x8a, 0x02, 0x0a, 0x08, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x5a, 0x0a, + 0x17, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x62, + 0x75, 0x79, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x48, 0x00, 0x52, 0x14, 0x70, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, + 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x5d, 0x0a, 0x18, 0x70, 0x65, 0x65, + 0x72, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x6c, 0x6c, 0x5f, + 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x72, 0x66, + 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, + 0x64, 0x53, 0x65, 0x6c, 0x6c, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, + 0x00, 0x52, 0x15, 0x70, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x53, + 0x65, 0x6c, 0x6c, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x3a, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x48, 0x74, 0x6c, + 0x63, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, + 0x48, 0x74, 0x6c, 0x63, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x32, 0xd1, 0x03, + 0x0a, 0x03, 0x52, 0x66, 0x71, 0x12, 0x55, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x72, 0x66, 0x71, 0x72, + 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, + 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x66, 0x71, + 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, + 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x11, + 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, + 0x72, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x72, 0x66, + 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, + 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, + 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, + 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x6a, 0x0a, 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, + 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x72, 0x66, + 0x71, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, + 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x16, + 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, + 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, + 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, + 0x01, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, + 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, + 0x72, 0x70, 0x63, 0x2f, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -898,41 +1249,51 @@ func file_rfqrpc_rfq_proto_rawDescGZIP() []byte { return file_rfqrpc_rfq_proto_rawDescData } -var file_rfqrpc_rfq_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +var file_rfqrpc_rfq_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_rfqrpc_rfq_proto_goTypes = []interface{}{ (*AssetSpecifier)(nil), // 0: rfqrpc.AssetSpecifier (*AddAssetBuyOrderRequest)(nil), // 1: rfqrpc.AddAssetBuyOrderRequest (*AddAssetBuyOrderResponse)(nil), // 2: rfqrpc.AddAssetBuyOrderResponse - (*AddAssetSellOfferRequest)(nil), // 3: rfqrpc.AddAssetSellOfferRequest - (*AddAssetSellOfferResponse)(nil), // 4: rfqrpc.AddAssetSellOfferResponse - (*QueryPeerAcceptedQuotesRequest)(nil), // 5: rfqrpc.QueryPeerAcceptedQuotesRequest - (*PeerAcceptedBuyQuote)(nil), // 6: rfqrpc.PeerAcceptedBuyQuote - (*QueryPeerAcceptedQuotesResponse)(nil), // 7: rfqrpc.QueryPeerAcceptedQuotesResponse - (*SubscribeRfqEventNtfnsRequest)(nil), // 8: rfqrpc.SubscribeRfqEventNtfnsRequest - (*PeerAcceptedBuyQuoteEvent)(nil), // 9: rfqrpc.PeerAcceptedBuyQuoteEvent - (*AcceptHtlcEvent)(nil), // 10: rfqrpc.AcceptHtlcEvent - (*RfqEvent)(nil), // 11: rfqrpc.RfqEvent + (*AddAssetSellOrderRequest)(nil), // 3: rfqrpc.AddAssetSellOrderRequest + (*AddAssetSellOrderResponse)(nil), // 4: rfqrpc.AddAssetSellOrderResponse + (*AddAssetSellOfferRequest)(nil), // 5: rfqrpc.AddAssetSellOfferRequest + (*AddAssetSellOfferResponse)(nil), // 6: rfqrpc.AddAssetSellOfferResponse + (*QueryPeerAcceptedQuotesRequest)(nil), // 7: rfqrpc.QueryPeerAcceptedQuotesRequest + (*PeerAcceptedBuyQuote)(nil), // 8: rfqrpc.PeerAcceptedBuyQuote + (*PeerAcceptedSellQuote)(nil), // 9: rfqrpc.PeerAcceptedSellQuote + (*QueryPeerAcceptedQuotesResponse)(nil), // 10: rfqrpc.QueryPeerAcceptedQuotesResponse + (*SubscribeRfqEventNtfnsRequest)(nil), // 11: rfqrpc.SubscribeRfqEventNtfnsRequest + (*PeerAcceptedBuyQuoteEvent)(nil), // 12: rfqrpc.PeerAcceptedBuyQuoteEvent + (*PeerAcceptedSellQuoteEvent)(nil), // 13: rfqrpc.PeerAcceptedSellQuoteEvent + (*AcceptHtlcEvent)(nil), // 14: rfqrpc.AcceptHtlcEvent + (*RfqEvent)(nil), // 15: rfqrpc.RfqEvent } var file_rfqrpc_rfq_proto_depIdxs = []int32{ 0, // 0: rfqrpc.AddAssetBuyOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 0, // 1: rfqrpc.AddAssetSellOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 6, // 2: rfqrpc.QueryPeerAcceptedQuotesResponse.buy_quotes:type_name -> rfqrpc.PeerAcceptedBuyQuote - 6, // 3: rfqrpc.PeerAcceptedBuyQuoteEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote - 9, // 4: rfqrpc.RfqEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuoteEvent - 10, // 5: rfqrpc.RfqEvent.accept_htlc:type_name -> rfqrpc.AcceptHtlcEvent - 1, // 6: rfqrpc.Rfq.AddAssetBuyOrder:input_type -> rfqrpc.AddAssetBuyOrderRequest - 3, // 7: rfqrpc.Rfq.AddAssetSellOffer:input_type -> rfqrpc.AddAssetSellOfferRequest - 5, // 8: rfqrpc.Rfq.QueryPeerAcceptedQuotes:input_type -> rfqrpc.QueryPeerAcceptedQuotesRequest - 8, // 9: rfqrpc.Rfq.SubscribeRfqEventNtfns:input_type -> rfqrpc.SubscribeRfqEventNtfnsRequest - 2, // 10: rfqrpc.Rfq.AddAssetBuyOrder:output_type -> rfqrpc.AddAssetBuyOrderResponse - 4, // 11: rfqrpc.Rfq.AddAssetSellOffer:output_type -> rfqrpc.AddAssetSellOfferResponse - 7, // 12: rfqrpc.Rfq.QueryPeerAcceptedQuotes:output_type -> rfqrpc.QueryPeerAcceptedQuotesResponse - 11, // 13: rfqrpc.Rfq.SubscribeRfqEventNtfns:output_type -> rfqrpc.RfqEvent - 10, // [10:14] is the sub-list for method output_type - 6, // [6:10] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 0, // 1: rfqrpc.AddAssetSellOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 0, // 2: rfqrpc.AddAssetSellOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 8, // 3: rfqrpc.QueryPeerAcceptedQuotesResponse.buy_quotes:type_name -> rfqrpc.PeerAcceptedBuyQuote + 9, // 4: rfqrpc.QueryPeerAcceptedQuotesResponse.sell_quotes:type_name -> rfqrpc.PeerAcceptedSellQuote + 8, // 5: rfqrpc.PeerAcceptedBuyQuoteEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote + 9, // 6: rfqrpc.PeerAcceptedSellQuoteEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuote + 12, // 7: rfqrpc.RfqEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuoteEvent + 13, // 8: rfqrpc.RfqEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuoteEvent + 14, // 9: rfqrpc.RfqEvent.accept_htlc:type_name -> rfqrpc.AcceptHtlcEvent + 1, // 10: rfqrpc.Rfq.AddAssetBuyOrder:input_type -> rfqrpc.AddAssetBuyOrderRequest + 3, // 11: rfqrpc.Rfq.AddAssetSellOrder:input_type -> rfqrpc.AddAssetSellOrderRequest + 5, // 12: rfqrpc.Rfq.AddAssetSellOffer:input_type -> rfqrpc.AddAssetSellOfferRequest + 7, // 13: rfqrpc.Rfq.QueryPeerAcceptedQuotes:input_type -> rfqrpc.QueryPeerAcceptedQuotesRequest + 11, // 14: rfqrpc.Rfq.SubscribeRfqEventNtfns:input_type -> rfqrpc.SubscribeRfqEventNtfnsRequest + 2, // 15: rfqrpc.Rfq.AddAssetBuyOrder:output_type -> rfqrpc.AddAssetBuyOrderResponse + 4, // 16: rfqrpc.Rfq.AddAssetSellOrder:output_type -> rfqrpc.AddAssetSellOrderResponse + 6, // 17: rfqrpc.Rfq.AddAssetSellOffer:output_type -> rfqrpc.AddAssetSellOfferResponse + 10, // 18: rfqrpc.Rfq.QueryPeerAcceptedQuotes:output_type -> rfqrpc.QueryPeerAcceptedQuotesResponse + 15, // 19: rfqrpc.Rfq.SubscribeRfqEventNtfns:output_type -> rfqrpc.RfqEvent + 15, // [15:20] is the sub-list for method output_type + 10, // [10:15] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_rfqrpc_rfq_proto_init() } @@ -978,7 +1339,7 @@ func file_rfqrpc_rfq_proto_init() { } } file_rfqrpc_rfq_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AddAssetSellOfferRequest); i { + switch v := v.(*AddAssetSellOrderRequest); i { case 0: return &v.state case 1: @@ -990,7 +1351,7 @@ func file_rfqrpc_rfq_proto_init() { } } file_rfqrpc_rfq_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AddAssetSellOfferResponse); i { + switch v := v.(*AddAssetSellOrderResponse); i { case 0: return &v.state case 1: @@ -1002,7 +1363,7 @@ func file_rfqrpc_rfq_proto_init() { } } file_rfqrpc_rfq_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*QueryPeerAcceptedQuotesRequest); i { + switch v := v.(*AddAssetSellOfferRequest); i { case 0: return &v.state case 1: @@ -1014,7 +1375,7 @@ func file_rfqrpc_rfq_proto_init() { } } file_rfqrpc_rfq_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerAcceptedBuyQuote); i { + switch v := v.(*AddAssetSellOfferResponse); i { case 0: return &v.state case 1: @@ -1026,7 +1387,7 @@ func file_rfqrpc_rfq_proto_init() { } } file_rfqrpc_rfq_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*QueryPeerAcceptedQuotesResponse); i { + switch v := v.(*QueryPeerAcceptedQuotesRequest); i { case 0: return &v.state case 1: @@ -1038,7 +1399,7 @@ func file_rfqrpc_rfq_proto_init() { } } file_rfqrpc_rfq_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*SubscribeRfqEventNtfnsRequest); i { + switch v := v.(*PeerAcceptedBuyQuote); i { case 0: return &v.state case 1: @@ -1050,7 +1411,7 @@ func file_rfqrpc_rfq_proto_init() { } } file_rfqrpc_rfq_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PeerAcceptedBuyQuoteEvent); i { + switch v := v.(*PeerAcceptedSellQuote); i { case 0: return &v.state case 1: @@ -1062,7 +1423,7 @@ func file_rfqrpc_rfq_proto_init() { } } file_rfqrpc_rfq_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AcceptHtlcEvent); i { + switch v := v.(*QueryPeerAcceptedQuotesResponse); i { case 0: return &v.state case 1: @@ -1074,6 +1435,54 @@ func file_rfqrpc_rfq_proto_init() { } } file_rfqrpc_rfq_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SubscribeRfqEventNtfnsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PeerAcceptedBuyQuoteEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PeerAcceptedSellQuoteEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AcceptHtlcEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*RfqEvent); i { case 0: return &v.state @@ -1092,8 +1501,9 @@ func file_rfqrpc_rfq_proto_init() { (*AssetSpecifier_GroupKey)(nil), (*AssetSpecifier_GroupKeyStr)(nil), } - file_rfqrpc_rfq_proto_msgTypes[11].OneofWrappers = []interface{}{ + file_rfqrpc_rfq_proto_msgTypes[15].OneofWrappers = []interface{}{ (*RfqEvent_PeerAcceptedBuyQuote)(nil), + (*RfqEvent_PeerAcceptedSellQuote)(nil), (*RfqEvent_AcceptHtlc)(nil), } type x struct{} @@ -1102,7 +1512,7 @@ func file_rfqrpc_rfq_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_rfqrpc_rfq_proto_rawDesc, NumEnums: 0, - NumMessages: 12, + NumMessages: 16, NumExtensions: 0, NumServices: 1, }, diff --git a/taprpc/rfqrpc/rfq.pb.gw.go b/taprpc/rfqrpc/rfq.pb.gw.go index ff00f4fbb..a94081e7b 100644 --- a/taprpc/rfqrpc/rfq.pb.gw.go +++ b/taprpc/rfqrpc/rfq.pb.gw.go @@ -167,6 +167,142 @@ func local_request_Rfq_AddAssetBuyOrder_1(ctx context.Context, marshaler runtime } +func request_Rfq_AddAssetSellOrder_0(ctx context.Context, marshaler runtime.Marshaler, client RfqClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetSellOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["asset_specifier.asset_id_str"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "asset_specifier.asset_id_str") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "asset_specifier.asset_id_str", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "asset_specifier.asset_id_str", err) + } + + msg, err := client.AddAssetSellOrder(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Rfq_AddAssetSellOrder_0(ctx context.Context, marshaler runtime.Marshaler, server RfqServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetSellOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["asset_specifier.asset_id_str"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "asset_specifier.asset_id_str") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "asset_specifier.asset_id_str", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "asset_specifier.asset_id_str", err) + } + + msg, err := server.AddAssetSellOrder(ctx, &protoReq) + return msg, metadata, err + +} + +func request_Rfq_AddAssetSellOrder_1(ctx context.Context, marshaler runtime.Marshaler, client RfqClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetSellOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["asset_specifier.group_key_str"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "asset_specifier.group_key_str") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "asset_specifier.group_key_str", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "asset_specifier.group_key_str", err) + } + + msg, err := client.AddAssetSellOrder(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Rfq_AddAssetSellOrder_1(ctx context.Context, marshaler runtime.Marshaler, server RfqServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetSellOrderRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["asset_specifier.group_key_str"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "asset_specifier.group_key_str") + } + + err = runtime.PopulateFieldFromPath(&protoReq, "asset_specifier.group_key_str", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "asset_specifier.group_key_str", err) + } + + msg, err := server.AddAssetSellOrder(ctx, &protoReq) + return msg, metadata, err + +} + func request_Rfq_AddAssetSellOffer_0(ctx context.Context, marshaler runtime.Marshaler, client RfqClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq AddAssetSellOfferRequest var metadata runtime.ServerMetadata @@ -402,6 +538,56 @@ func RegisterRfqHandlerServer(ctx context.Context, mux *runtime.ServeMux, server }) + mux.Handle("POST", pattern_Rfq_AddAssetSellOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetSellOrder", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/sellorder/asset-id/{asset_specifier.asset_id_str}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Rfq_AddAssetSellOrder_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetSellOrder_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_Rfq_AddAssetSellOrder_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateIncomingContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetSellOrder", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/sellorder/group-key/{asset_specifier.group_key_str}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Rfq_AddAssetSellOrder_1(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetSellOrder_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_Rfq_AddAssetSellOffer_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -569,6 +755,50 @@ func RegisterRfqHandlerClient(ctx context.Context, mux *runtime.ServeMux, client }) + mux.Handle("POST", pattern_Rfq_AddAssetSellOrder_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetSellOrder", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/sellorder/asset-id/{asset_specifier.asset_id_str}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Rfq_AddAssetSellOrder_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetSellOrder_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_Rfq_AddAssetSellOrder_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + var err error + var annotatedContext context.Context + annotatedContext, err = runtime.AnnotateContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetSellOrder", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/sellorder/group-key/{asset_specifier.group_key_str}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Rfq_AddAssetSellOrder_1(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetSellOrder_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_Rfq_AddAssetSellOffer_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -665,6 +895,10 @@ var ( pattern_Rfq_AddAssetBuyOrder_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"v1", "taproot-assets", "rfq", "buyorder", "group-key", "asset_specifier.group_key_str"}, "")) + pattern_Rfq_AddAssetSellOrder_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"v1", "taproot-assets", "rfq", "sellorder", "asset-id", "asset_specifier.asset_id_str"}, "")) + + pattern_Rfq_AddAssetSellOrder_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"v1", "taproot-assets", "rfq", "sellorder", "group-key", "asset_specifier.group_key_str"}, "")) + pattern_Rfq_AddAssetSellOffer_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"v1", "taproot-assets", "rfq", "selloffer", "asset-id", "asset_specifier.asset_id_str"}, "")) pattern_Rfq_AddAssetSellOffer_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"v1", "taproot-assets", "rfq", "selloffer", "group-key", "asset_specifier.group_key_str"}, "")) @@ -679,6 +913,10 @@ var ( forward_Rfq_AddAssetBuyOrder_1 = runtime.ForwardResponseMessage + forward_Rfq_AddAssetSellOrder_0 = runtime.ForwardResponseMessage + + forward_Rfq_AddAssetSellOrder_1 = runtime.ForwardResponseMessage + forward_Rfq_AddAssetSellOffer_0 = runtime.ForwardResponseMessage forward_Rfq_AddAssetSellOffer_1 = runtime.ForwardResponseMessage diff --git a/taprpc/rfqrpc/rfq.pb.json.go b/taprpc/rfqrpc/rfq.pb.json.go index 110719567..6affb5563 100644 --- a/taprpc/rfqrpc/rfq.pb.json.go +++ b/taprpc/rfqrpc/rfq.pb.json.go @@ -46,6 +46,31 @@ func RegisterRfqJSONCallbacks(registry map[string]func(ctx context.Context, callback(string(respBytes), nil) } + registry["rfqrpc.Rfq.AddAssetSellOrder"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &AddAssetSellOrderRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewRfqClient(conn) + resp, err := client.AddAssetSellOrder(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + registry["rfqrpc.Rfq.AddAssetSellOffer"] = func(ctx context.Context, conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { diff --git a/taprpc/rfqrpc/rfq.proto b/taprpc/rfqrpc/rfq.proto index 74c6044d5..d9c53125d 100644 --- a/taprpc/rfqrpc/rfq.proto +++ b/taprpc/rfqrpc/rfq.proto @@ -12,6 +12,13 @@ service Rfq { rpc AddAssetBuyOrder (AddAssetBuyOrderRequest) returns (AddAssetBuyOrderResponse); + /* tapcli: `rfq sellorder` + AddAssetSellOrder is used to add a sell order for a specific asset. If a + sell order already exists for the asset, it will be updated. + */ + rpc AddAssetSellOrder (AddAssetSellOrderRequest) + returns (AddAssetSellOrderResponse); + /* tapcli: `rfq selloffer` AddAssetSellOffer is used to add a sell offer for a specific asset. If a sell offer already exists for the asset, it will be updated. @@ -71,6 +78,27 @@ message AddAssetBuyOrderRequest { message AddAssetBuyOrderResponse { } +message AddAssetSellOrderRequest { + // asset_specifier is the subject asset. + AssetSpecifier asset_specifier = 1; + + // The maximum amount of the asset to sell. + uint64 max_asset_amount = 2; + + // The minimum amount of BTC to accept (units: millisats). + uint64 min_ask = 3; + + // The unix timestamp in seconds after which the order is no longer valid. + uint64 expiry = 4; + + // peer_pub_key is an optional field for specifying the public key of the + // intended recipient peer for the order. + bytes peer_pub_key = 5; +} + +message AddAssetSellOrderResponse { +} + message AddAssetSellOfferRequest { // asset_specifier is the subject asset. AssetSpecifier asset_specifier = 1; @@ -106,10 +134,35 @@ message PeerAcceptedBuyQuote { uint64 expiry = 6; } +message PeerAcceptedSellQuote { + // Quote counterparty peer. + string peer = 1; + + // The unique identifier of the quote request. + bytes id = 2; + + // scid is the short channel ID of the channel over which the payment for + // the quote should be made. + uint64 scid = 3; + + // asset_amount is the amount of the subject asset. + uint64 asset_amount = 4; + + // bid_price is the price in millisats for the entire asset amount. + uint64 bid_price = 5; + + // The unix timestamp in seconds after which the quote is no longer valid. + uint64 expiry = 6; +} + message QueryPeerAcceptedQuotesResponse { // buy_quotes is a list of asset buy quotes which were requested by our // node and have been accepted by our peers. repeated PeerAcceptedBuyQuote buy_quotes = 1; + + // sell_quotes is a list of asset sell quotes which were requested by our + // node and have been accepted by our peers. + repeated PeerAcceptedSellQuote sell_quotes = 2; } message SubscribeRfqEventNtfnsRequest { @@ -123,6 +176,14 @@ message PeerAcceptedBuyQuoteEvent { PeerAcceptedBuyQuote peer_accepted_buy_quote = 2; } +message PeerAcceptedSellQuoteEvent { + // Unix timestamp in microseconds. + uint64 timestamp = 1; + + // The asset sell quote that was accepted by out peer. + PeerAcceptedSellQuote peer_accepted_sell_quote = 2; +} + message AcceptHtlcEvent { // Unix timestamp. uint64 timestamp = 1; @@ -138,8 +199,12 @@ message RfqEvent { // accepted (incoming) asset buy quote message is received. PeerAcceptedBuyQuoteEvent peer_accepted_buy_quote = 1; + // peer_accepted_sell_offer is an event that is emitted when a peer + // accepted (incoming) asset sell quote message is received. + PeerAcceptedSellQuoteEvent peer_accepted_sell_quote = 2; + // accept_htlc is an event that is sent when a HTLC is accepted by the // RFQ service. - AcceptHtlcEvent accept_htlc = 2; + AcceptHtlcEvent accept_htlc = 3; } } diff --git a/taprpc/rfqrpc/rfq.swagger.json b/taprpc/rfqrpc/rfq.swagger.json index 6fee961bd..d2e759acd 100644 --- a/taprpc/rfqrpc/rfq.swagger.json +++ b/taprpc/rfqrpc/rfq.swagger.json @@ -382,6 +382,172 @@ "Rfq" ] } + }, + "/v1/taproot-assets/rfq/sellorder/asset-id/{asset_specifier.asset_id_str}": { + "post": { + "summary": "tapcli: `rfq sellorder`\nAddAssetSellOrder is used to add a sell order for a specific asset. If a\nsell order already exists for the asset, it will be updated.", + "operationId": "Rfq_AddAssetSellOrder", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/rfqrpcAddAssetSellOrderResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "asset_specifier.asset_id_str", + "description": "The 32-byte asset ID encoded as a hex string (use this for REST).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "asset_specifier": { + "type": "object", + "properties": { + "asset_id": { + "type": "string", + "format": "byte", + "description": "The 32-byte asset ID specified as raw bytes (gRPC only)." + }, + "group_key": { + "type": "string", + "format": "byte", + "description": "The 32-byte asset group key specified as raw bytes (gRPC only)." + }, + "group_key_str": { + "type": "string", + "description": "The 32-byte asset group key encoded as hex string (use this for\nREST)." + } + }, + "description": "asset_specifier is the subject asset.", + "title": "asset_specifier is the subject asset." + }, + "max_asset_amount": { + "type": "string", + "format": "uint64", + "description": "The maximum amount of the asset to sell." + }, + "min_ask": { + "type": "string", + "format": "uint64", + "description": "The minimum amount of BTC to accept (units: millisats)." + }, + "expiry": { + "type": "string", + "format": "uint64", + "description": "The unix timestamp in seconds after which the order is no longer valid." + }, + "peer_pub_key": { + "type": "string", + "format": "byte", + "description": "peer_pub_key is an optional field for specifying the public key of the\nintended recipient peer for the order." + } + } + } + } + ], + "tags": [ + "Rfq" + ] + } + }, + "/v1/taproot-assets/rfq/sellorder/group-key/{asset_specifier.group_key_str}": { + "post": { + "summary": "tapcli: `rfq sellorder`\nAddAssetSellOrder is used to add a sell order for a specific asset. If a\nsell order already exists for the asset, it will be updated.", + "operationId": "Rfq_AddAssetSellOrder2", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/rfqrpcAddAssetSellOrderResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "asset_specifier.group_key_str", + "description": "The 32-byte asset group key encoded as hex string (use this for\nREST).", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "type": "object", + "properties": { + "asset_specifier": { + "type": "object", + "properties": { + "asset_id": { + "type": "string", + "format": "byte", + "description": "The 32-byte asset ID specified as raw bytes (gRPC only)." + }, + "asset_id_str": { + "type": "string", + "description": "The 32-byte asset ID encoded as a hex string (use this for REST)." + }, + "group_key": { + "type": "string", + "format": "byte", + "description": "The 32-byte asset group key specified as raw bytes (gRPC only)." + } + }, + "description": "asset_specifier is the subject asset.", + "title": "asset_specifier is the subject asset." + }, + "max_asset_amount": { + "type": "string", + "format": "uint64", + "description": "The maximum amount of the asset to sell." + }, + "min_ask": { + "type": "string", + "format": "uint64", + "description": "The minimum amount of BTC to accept (units: millisats)." + }, + "expiry": { + "type": "string", + "format": "uint64", + "description": "The unix timestamp in seconds after which the order is no longer valid." + }, + "peer_pub_key": { + "type": "string", + "format": "byte", + "description": "peer_pub_key is an optional field for specifying the public key of the\nintended recipient peer for the order." + } + } + } + } + ], + "tags": [ + "Rfq" + ] + } } }, "definitions": { @@ -415,6 +581,9 @@ "rfqrpcAddAssetSellOfferResponse": { "type": "object" }, + "rfqrpcAddAssetSellOrderResponse": { + "type": "object" + }, "rfqrpcAssetSpecifier": { "type": "object", "properties": { @@ -486,6 +655,54 @@ } } }, + "rfqrpcPeerAcceptedSellQuote": { + "type": "object", + "properties": { + "peer": { + "type": "string", + "description": "Quote counterparty peer." + }, + "id": { + "type": "string", + "format": "byte", + "description": "The unique identifier of the quote request." + }, + "scid": { + "type": "string", + "format": "uint64", + "description": "scid is the short channel ID of the channel over which the payment for\nthe quote should be made." + }, + "asset_amount": { + "type": "string", + "format": "uint64", + "description": "asset_amount is the amount of the subject asset." + }, + "bid_price": { + "type": "string", + "format": "uint64", + "description": "bid_price is the price in millisats for the entire asset amount." + }, + "expiry": { + "type": "string", + "format": "uint64", + "description": "The unix timestamp in seconds after which the quote is no longer valid." + } + } + }, + "rfqrpcPeerAcceptedSellQuoteEvent": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "uint64", + "description": "Unix timestamp in microseconds." + }, + "peer_accepted_sell_quote": { + "$ref": "#/definitions/rfqrpcPeerAcceptedSellQuote", + "description": "The asset sell quote that was accepted by out peer." + } + } + }, "rfqrpcQueryPeerAcceptedQuotesResponse": { "type": "object", "properties": { @@ -496,6 +713,14 @@ "$ref": "#/definitions/rfqrpcPeerAcceptedBuyQuote" }, "description": "buy_quotes is a list of asset buy quotes which were requested by our\nnode and have been accepted by our peers." + }, + "sell_quotes": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/rfqrpcPeerAcceptedSellQuote" + }, + "description": "sell_quotes is a list of asset sell quotes which were requested by our\nnode and have been accepted by our peers." } } }, @@ -506,6 +731,10 @@ "$ref": "#/definitions/rfqrpcPeerAcceptedBuyQuoteEvent", "description": "peer_accepted_buy_quote is an event that is emitted when a peer\naccepted (incoming) asset buy quote message is received." }, + "peer_accepted_sell_quote": { + "$ref": "#/definitions/rfqrpcPeerAcceptedSellQuoteEvent", + "description": "peer_accepted_sell_offer is an event that is emitted when a peer\naccepted (incoming) asset sell quote message is received." + }, "accept_htlc": { "$ref": "#/definitions/rfqrpcAcceptHtlcEvent", "description": "accept_htlc is an event that is sent when a HTLC is accepted by the\nRFQ service." diff --git a/taprpc/rfqrpc/rfq.yaml b/taprpc/rfqrpc/rfq.yaml index 26ea695b8..a3257e49c 100644 --- a/taprpc/rfqrpc/rfq.yaml +++ b/taprpc/rfqrpc/rfq.yaml @@ -10,6 +10,13 @@ http: - post: "/v1/taproot-assets/rfq/buyorder/group-key/{asset_specifier.group_key_str}" body: "*" + - selector: rfqrpc.Rfq.AddAssetSellOrder + post: "/v1/taproot-assets/rfq/sellorder/asset-id/{asset_specifier.asset_id_str}" + body: "*" + additional_bindings: + - post: "/v1/taproot-assets/rfq/sellorder/group-key/{asset_specifier.group_key_str}" + body: "*" + - selector: rfqrpc.Rfq.AddAssetSellOffer post: "/v1/taproot-assets/rfq/selloffer/asset-id/{asset_specifier.asset_id_str}" body: "*" diff --git a/taprpc/rfqrpc/rfq_grpc.pb.go b/taprpc/rfqrpc/rfq_grpc.pb.go index 2b62b074c..b029379ad 100644 --- a/taprpc/rfqrpc/rfq_grpc.pb.go +++ b/taprpc/rfqrpc/rfq_grpc.pb.go @@ -22,6 +22,10 @@ type RfqClient interface { // AddAssetBuyOrder is used to add a buy order for a specific asset. If a buy // order already exists for the asset, it will be updated. AddAssetBuyOrder(ctx context.Context, in *AddAssetBuyOrderRequest, opts ...grpc.CallOption) (*AddAssetBuyOrderResponse, error) + // tapcli: `rfq sellorder` + // AddAssetSellOrder is used to add a sell order for a specific asset. If a + // sell order already exists for the asset, it will be updated. + AddAssetSellOrder(ctx context.Context, in *AddAssetSellOrderRequest, opts ...grpc.CallOption) (*AddAssetSellOrderResponse, error) // tapcli: `rfq selloffer` // AddAssetSellOffer is used to add a sell offer for a specific asset. If a // sell offer already exists for the asset, it will be updated. @@ -51,6 +55,15 @@ func (c *rfqClient) AddAssetBuyOrder(ctx context.Context, in *AddAssetBuyOrderRe return out, nil } +func (c *rfqClient) AddAssetSellOrder(ctx context.Context, in *AddAssetSellOrderRequest, opts ...grpc.CallOption) (*AddAssetSellOrderResponse, error) { + out := new(AddAssetSellOrderResponse) + err := c.cc.Invoke(ctx, "/rfqrpc.Rfq/AddAssetSellOrder", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *rfqClient) AddAssetSellOffer(ctx context.Context, in *AddAssetSellOfferRequest, opts ...grpc.CallOption) (*AddAssetSellOfferResponse, error) { out := new(AddAssetSellOfferResponse) err := c.cc.Invoke(ctx, "/rfqrpc.Rfq/AddAssetSellOffer", in, out, opts...) @@ -109,6 +122,10 @@ type RfqServer interface { // AddAssetBuyOrder is used to add a buy order for a specific asset. If a buy // order already exists for the asset, it will be updated. AddAssetBuyOrder(context.Context, *AddAssetBuyOrderRequest) (*AddAssetBuyOrderResponse, error) + // tapcli: `rfq sellorder` + // AddAssetSellOrder is used to add a sell order for a specific asset. If a + // sell order already exists for the asset, it will be updated. + AddAssetSellOrder(context.Context, *AddAssetSellOrderRequest) (*AddAssetSellOrderResponse, error) // tapcli: `rfq selloffer` // AddAssetSellOffer is used to add a sell offer for a specific asset. If a // sell offer already exists for the asset, it will be updated. @@ -129,6 +146,9 @@ type UnimplementedRfqServer struct { func (UnimplementedRfqServer) AddAssetBuyOrder(context.Context, *AddAssetBuyOrderRequest) (*AddAssetBuyOrderResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AddAssetBuyOrder not implemented") } +func (UnimplementedRfqServer) AddAssetSellOrder(context.Context, *AddAssetSellOrderRequest) (*AddAssetSellOrderResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddAssetSellOrder not implemented") +} func (UnimplementedRfqServer) AddAssetSellOffer(context.Context, *AddAssetSellOfferRequest) (*AddAssetSellOfferResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method AddAssetSellOffer not implemented") } @@ -169,6 +189,24 @@ func _Rfq_AddAssetBuyOrder_Handler(srv interface{}, ctx context.Context, dec fun return interceptor(ctx, in, info, handler) } +func _Rfq_AddAssetSellOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddAssetSellOrderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RfqServer).AddAssetSellOrder(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rfqrpc.Rfq/AddAssetSellOrder", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RfqServer).AddAssetSellOrder(ctx, req.(*AddAssetSellOrderRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Rfq_AddAssetSellOffer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(AddAssetSellOfferRequest) if err := dec(in); err != nil { @@ -237,6 +275,10 @@ var Rfq_ServiceDesc = grpc.ServiceDesc{ MethodName: "AddAssetBuyOrder", Handler: _Rfq_AddAssetBuyOrder_Handler, }, + { + MethodName: "AddAssetSellOrder", + Handler: _Rfq_AddAssetSellOrder_Handler, + }, { MethodName: "AddAssetSellOffer", Handler: _Rfq_AddAssetSellOffer_Handler,