From 3bd1090b95f4fc9e301f32c03e14e7459e43b079 Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 19 Jan 2024 17:32:56 +0000 Subject: [PATCH 1/8] asset: improve code formatting --- asset/encoding.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/asset/encoding.go b/asset/encoding.go index 281f8bce4..9978af78c 100644 --- a/asset/encoding.go +++ b/asset/encoding.go @@ -140,13 +140,16 @@ func CompressedPubKeyEncoder(w io.Writer, val any, buf *[8]byte) error { return tlv.NewTypeForEncodingErr(val, "*btcec.PublicKey") } -func CompressedPubKeyDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error { +func CompressedPubKeyDecoder(r io.Reader, val any, buf *[8]byte, + l uint64) error { + if typ, ok := val.(**btcec.PublicKey); ok { var keyBytes [btcec.PubKeyBytesLenCompressed]byte err := tlv.DBytes33(r, &keyBytes, buf, btcec.PubKeyBytesLenCompressed) if err != nil { return err } + var key *btcec.PublicKey // Handle empty key, which is not on the curve. if keyBytes == [btcec.PubKeyBytesLenCompressed]byte{} { @@ -160,6 +163,7 @@ func CompressedPubKeyDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error *typ = key return nil } + return tlv.NewTypeForDecodingErr( val, "*btcec.PublicKey", l, btcec.PubKeyBytesLenCompressed, ) From 1c67efd24d3803962c1475a87a67830c5c52a30b Mon Sep 17 00:00:00 2001 From: ffranr Date: Wed, 14 Feb 2024 11:46:14 +0000 Subject: [PATCH 2/8] itest: fix comment --- itest/test_harness.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/itest/test_harness.go b/itest/test_harness.go index 654fff447..2ed3ef2b8 100644 --- a/itest/test_harness.go +++ b/itest/test_harness.go @@ -322,7 +322,7 @@ func setupHarnesses(t *testing.T, ht *harnessTest, proofCourier = universeServer } - // Create a tapd that uses Bob and connect it to the universe server. + // Create a tapd that uses Alice and connect it to the universe server. tapdHarness := setupTapdHarness( t, ht, lndHarness.Alice, universeServer, func(params *tapdHarnessParams) { From d6c994f357a087cac0bff2d1899125796f9bfd93 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 19 Feb 2024 17:38:04 +0000 Subject: [PATCH 3/8] rpcserver: fix log message --- rpcserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rpcserver.go b/rpcserver.go index 92ba5ee7c..baad718cb 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -2558,7 +2558,7 @@ func (r *rpcServer) SubscribeSendAssetEventNtfns( eventSubscriber, false, false, ) if err != nil { - return fmt.Errorf("failed to register chain porter event"+ + return fmt.Errorf("failed to register chain porter event "+ "notifications subscription: %w", err) } From 596805dcc7ac14faa439deb7c92691000b761ac0 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 19 Feb 2024 17:24:53 +0000 Subject: [PATCH 4/8] rfqmsg: add new RFQ message types package This commit adds a new package called rfqmsg which contains RFQ message types. Message types are: request, accept, and reject. --- rfqmsg/accept.go | 247 +++++++++++++++++++++++++++++++ rfqmsg/accept_test.go | 42 ++++++ rfqmsg/messages.go | 95 ++++++++++++ rfqmsg/reject.go | 271 ++++++++++++++++++++++++++++++++++ rfqmsg/request.go | 323 +++++++++++++++++++++++++++++++++++++++++ rfqmsg/request_test.go | 70 +++++++++ 6 files changed, 1048 insertions(+) create mode 100644 rfqmsg/accept.go create mode 100644 rfqmsg/accept_test.go create mode 100644 rfqmsg/messages.go create mode 100644 rfqmsg/reject.go create mode 100644 rfqmsg/request.go create mode 100644 rfqmsg/request_test.go diff --git a/rfqmsg/accept.go b/rfqmsg/accept.go new file mode 100644 index 000000000..187bf6b0a --- /dev/null +++ b/rfqmsg/accept.go @@ -0,0 +1,247 @@ +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 ( + // Accept message type field TLV types. + + TypeAcceptID tlv.Type = 0 + TypeAcceptAskPrice tlv.Type = 2 + TypeAcceptExpiry tlv.Type = 4 + TypeAcceptSignature tlv.Type = 6 +) + +func TypeRecordAcceptID(id *ID) tlv.Record { + const recordSize = 32 + + return tlv.MakeStaticRecord( + TypeAcceptID, id, recordSize, IdEncoder, IdDecoder, + ) +} + +func TypeRecordAcceptAskPrice(askPrice *lnwire.MilliSatoshi) tlv.Record { + return tlv.MakeStaticRecord( + TypeAcceptAskPrice, askPrice, 8, milliSatoshiEncoder, + milliSatoshiDecoder, + ) +} + +func milliSatoshiEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + if ms, ok := val.(*lnwire.MilliSatoshi); ok { + msUint64 := uint64(*ms) + return tlv.EUint64(w, &msUint64, buf) + } + + return tlv.NewTypeForEncodingErr(val, "MilliSatoshi") +} + +func milliSatoshiDecoder(r io.Reader, val interface{}, buf *[8]byte, + l uint64) error { + + if ms, ok := val.(*lnwire.MilliSatoshi); ok { + var msInt uint64 + err := tlv.DUint64(r, &msInt, buf, l) + if err != nil { + return err + } + + *ms = lnwire.MilliSatoshi(msInt) + return nil + } + + return tlv.NewTypeForDecodingErr(val, "MilliSatoshi", l, 8) +} + +func TypeRecordAcceptExpiry(expirySeconds *uint64) tlv.Record { + return tlv.MakePrimitiveRecord( + TypeAcceptExpiry, expirySeconds, + ) +} + +func TypeRecordAcceptSig(sig *[64]byte) tlv.Record { + return tlv.MakePrimitiveRecord( + TypeAcceptSignature, sig, + ) +} + +// acceptMsgData is a struct that represents the data field of a quote +// accept message. +type acceptMsgData struct { + // ID represents the unique identifier of the quote request message that + // this response is associated with. + ID ID + + // AskPrice is the asking price of the quote. + AskPrice lnwire.MilliSatoshi + + // Expiry is the asking price expiry lifetime unix timestamp. + Expiry uint64 + + // sig is a signature over the serialized contents of the message. + sig [64]byte +} + +// encodeRecords determines the non-nil records to include when encoding at +// runtime. +func (q *acceptMsgData) encodeRecords() []tlv.Record { + var records []tlv.Record + + // Add id record. + records = append(records, TypeRecordAcceptID(&q.ID)) + + // Add ask price record. + records = append(records, TypeRecordAcceptAskPrice(&q.AskPrice)) + + // Add expiry record. + records = append( + records, TypeRecordAcceptExpiry(&q.Expiry), + ) + + // Add signature record. + records = append( + records, TypeRecordAcceptSig(&q.sig), + ) + + return records +} + +// Encode encodes the structure into a TLV stream. +func (q *acceptMsgData) Encode(writer io.Writer) error { + stream, err := tlv.NewStream(q.encodeRecords()...) + if err != nil { + return err + } + return stream.Encode(writer) +} + +// DecodeRecords provides all TLV records for decoding. +func (q *acceptMsgData) decodeRecords() []tlv.Record { + return []tlv.Record{ + TypeRecordAcceptID(&q.ID), + TypeRecordAcceptAskPrice(&q.AskPrice), + TypeRecordAcceptExpiry(&q.Expiry), + TypeRecordAcceptSig(&q.sig), + } +} + +// Decode decodes the structure from a TLV stream. +func (q *acceptMsgData) Decode(r io.Reader) error { + stream, err := tlv.NewStream(q.decodeRecords()...) + if err != nil { + return err + } + return stream.DecodeP2P(r) +} + +// Bytes encodes the structure into a TLV stream and returns the bytes. +func (q *acceptMsgData) Bytes() ([]byte, error) { + var b bytes.Buffer + err := q.Encode(&b) + if err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +// Accept is a struct that represents an accepted quote message. +type Accept 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 + + // acceptMsgData is the message data for the quote accept message. + acceptMsgData +} + +// NewAcceptFromRequest creates a new instance of a quote accept message given +// a quote request message. +func NewAcceptFromRequest(request Request, askPrice lnwire.MilliSatoshi, + expiry uint64) *Accept { + + return &Accept{ + Peer: request.Peer, + AssetAmount: request.AssetAmount, + acceptMsgData: acceptMsgData{ + ID: request.ID, + AskPrice: askPrice, + Expiry: expiry, + }, + } +} + +// NewAcceptFromWireMsg instantiates a new instance from a wire message. +func NewAcceptFromWireMsg(wireMsg WireMessage) (*Accept, error) { + // Ensure that the message type is an accept message. + if wireMsg.MsgType != MsgTypeAccept { + return nil, fmt.Errorf("unable to create an accept message "+ + "from wire message of type %d", wireMsg.MsgType) + } + + // Decode message data component from TLV bytes. + var msgData acceptMsgData + err := msgData.Decode(bytes.NewReader(wireMsg.Data)) + if err != nil { + return nil, fmt.Errorf("unable to decode quote accept "+ + "message data: %w", err) + } + + return &Accept{ + Peer: wireMsg.Peer, + acceptMsgData: msgData, + }, nil +} + +// ShortChannelId returns the short channel ID of the quote accept. +func (q *Accept) 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 *Accept) ToWire() (WireMessage, error) { + // Encode message data component as TLV bytes. + msgDataBytes, err := q.acceptMsgData.Bytes() + if err != nil { + return WireMessage{}, fmt.Errorf("unable to encode message "+ + "data: %w", err) + } + + return WireMessage{ + Peer: q.Peer, + MsgType: MsgTypeAccept, + Data: msgDataBytes, + }, nil +} + +// String returns a human-readable string representation of the message. +func (q *Accept) String() string { + return fmt.Sprintf("Accept(peer=%x, id=%x, ask_price=%d, expiry=%d)", + q.Peer[:], q.ID, q.AskPrice, q.Expiry) +} + +// Ensure that the message type implements the OutgoingMsg interface. +var _ OutgoingMsg = (*Accept)(nil) + +// Ensure that the message type implements the IncomingMsg interface. +var _ IncomingMsg = (*Accept)(nil) diff --git a/rfqmsg/accept_test.go b/rfqmsg/accept_test.go new file mode 100644 index 000000000..8d52883b1 --- /dev/null +++ b/rfqmsg/accept_test.go @@ -0,0 +1,42 @@ +package rfqmsg + +import ( + "encoding/binary" + "math/rand" + "testing" + + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// TestAcceptShortChannelId tests the ShortChannelId method of a quote accept +// message. +func TestAcceptShortChannelId(t *testing.T) { + t.Parallel() + + // Generate a random short channel ID. + scidInt := rand.Uint64() + scid := lnwire.NewShortChanIDFromInt(scidInt) + + // Create a random ID. + randomIdBytes := test.RandBytes(32) + id := ID(randomIdBytes) + + // Set the last 8 bytes of the ID to the short channel ID. + binary.BigEndian.PutUint64(id[24:], scid.ToUint64()) + + // Create an accept message. + acceptMsg := Accept{ + acceptMsgData: acceptMsgData{ + ID: id, + }, + } + + // Derive the short channel ID from the accept message. + actualScidInt := acceptMsg.ShortChannelId() + + // Assert that the derived short channel ID is equal to the expected + // short channel ID. + require.Equal(t, scidInt, uint64(actualScidInt)) +} diff --git a/rfqmsg/messages.go b/rfqmsg/messages.go new file mode 100644 index 000000000..155540adf --- /dev/null +++ b/rfqmsg/messages.go @@ -0,0 +1,95 @@ +package rfqmsg + +import ( + "encoding/hex" + "errors" + "math" + + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +// ID is the identifier for a RFQ message. +type ID [32]byte + +// String returns the string representation of the ID. +func (id ID) String() string { + return hex.EncodeToString(id[:]) +} + +// SerialisedScid is a serialised short channel id (SCID). +type SerialisedScid uint64 + +// MaxMessageType is the maximum supported message type value. +const MaxMessageType = lnwire.MessageType(math.MaxUint16) + +// TapMessageTypeBaseOffset is the taproot-assets specific message type +// identifier base offset. All tap messages will have a type identifier that is +// greater than this value. +// +// This offset was chosen as the concatenation of the alphabetical index +// positions of the letters "t" (20), "a" (1), and "p" (16). +const TapMessageTypeBaseOffset = 20116 + lnwire.CustomTypeStart + +const ( + // MsgTypeRequest is the message type identifier for a quote request + // message. + MsgTypeRequest = TapMessageTypeBaseOffset + 0 + + // MsgTypeAccept is the message type identifier for a quote accept + // message. + MsgTypeAccept = TapMessageTypeBaseOffset + 1 + + // MsgTypeReject is the message type identifier for a quote + // reject message. + MsgTypeReject = TapMessageTypeBaseOffset + 2 +) + +var ( + // ErrUnknownMessageType is an error that is returned when an unknown + // message type is encountered. + ErrUnknownMessageType = errors.New("unknown message type") +) + +// WireMessage is a struct that represents a general wire message. +type WireMessage struct { + // Peer is the origin/destination peer for this message. + Peer route.Vertex + + // MsgType is the protocol message type number. + MsgType lnwire.MessageType + + // Data is the data exchanged. + Data []byte +} + +// NewIncomingMsgFromWire creates a new RFQ message from a wire message. +func NewIncomingMsgFromWire(wireMsg WireMessage) (IncomingMsg, error) { + switch wireMsg.MsgType { + case MsgTypeRequest: + return NewRequestMsgFromWire(wireMsg) + case MsgTypeAccept: + return NewAcceptFromWireMsg(wireMsg) + case MsgTypeReject: + return NewQuoteRejectFromWireMsg(wireMsg) + default: + return nil, ErrUnknownMessageType + } +} + +// IncomingMsg is an interface that represents an inbound wire message +// that has been received from a peer. +type IncomingMsg interface { + // String returns a human-readable string representation of the message. + String() string +} + +// OutgoingMsg is an interface that represents an outbound wire message +// that can be sent to a peer. +type OutgoingMsg interface { + // String returns a human-readable string representation of the message. + String() string + + // ToWire returns a wire message with a serialized data field. + ToWire() (WireMessage, error) +} diff --git a/rfqmsg/reject.go b/rfqmsg/reject.go new file mode 100644 index 000000000..e45c6754c --- /dev/null +++ b/rfqmsg/reject.go @@ -0,0 +1,271 @@ +package rfqmsg + +import ( + "bytes" + "fmt" + "io" + + "github.com/lightningnetwork/lnd/routing/route" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // Reject message type field TLV types. + + TypeRejectID tlv.Type = 0 + TypeRejectErrCode tlv.Type = 1 + TypeRejectErrMsg tlv.Type = 3 +) + +func TypeRecordRejectID(id *ID) tlv.Record { + const recordSize = 32 + + return tlv.MakeStaticRecord( + TypeRejectID, id, recordSize, IdEncoder, IdDecoder, + ) +} + +func TypeRecordRejectErrCode(errCode *uint8) tlv.Record { + return tlv.MakePrimitiveRecord(TypeRejectErrCode, errCode) +} + +func TypeRecordRejectErrMsg(errMsg *string) tlv.Record { + sizeFunc := func() uint64 { + return uint64(len(*errMsg)) + } + return tlv.MakeDynamicRecord( + TypeRejectErrMsg, errMsg, sizeFunc, + rejectErrMsgEncoder, rejectErrMsgDecoder, + ) +} + +func rejectErrMsgEncoder(w io.Writer, val any, buf *[8]byte) error { + if typ, ok := val.(**string); ok { + v := *typ + + msgBytes := []byte(*v) + err := tlv.EVarBytes(w, msgBytes, buf) + if err != nil { + return err + } + + return nil + } + + return tlv.NewTypeForEncodingErr(val, "RejectErrMsg") +} + +func rejectErrMsgDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error { + if typ, ok := val.(**string); ok { + var errMsgBytes []byte + err := tlv.DVarBytes(r, &errMsgBytes, buf, l) + if err != nil { + return err + } + + msgStr := string(errMsgBytes) + *typ = &msgStr + + return nil + } + + return tlv.NewTypeForDecodingErr(val, "RejectErrMsg", l, l) +} + +// RejectErr is a struct that represents the error code and message of a quote +// reject message. +type RejectErr struct { + // Code is the error code that provides the reason for the rejection. + Code uint8 + + // Msg is the error message that provides the reason for the rejection. + Msg string +} + +var ( + // ErrUnknownReject is the error code for when the quote is rejected + // for an unspecified reason. + ErrUnknownReject = RejectErr{ + Code: 0, + Msg: "unknown reject error", + } + + // ErrNoSuitableSellOffer is the error code for when there is no + // suitable sell offer available. + ErrNoSuitableSellOffer = RejectErr{ + Code: 1, + Msg: "no suitable sell offer available", + } + + // ErrPriceOracleUnavailable is the error code for when the price + // oracle is unavailable. + ErrPriceOracleUnavailable = RejectErr{ + Code: 2, + Msg: "price oracle service unavailable", + } + + // ErrPriceOracleUnspecifiedError is the error code for when the price + // oracle returns an unspecified error. + ErrPriceOracleUnspecifiedError = RejectErr{ + Code: 3, + Msg: "price oracle service returned an unspecified error", + } + + // ErrPriceOracleError is the error code for when the price oracle + // returns a specified error. + ErrPriceOracleError = RejectErr{ + Code: 4, + Msg: "price oracle service error: (err_code=%d, err_msg=%s)", + } +) + +// NewErrPriceOracleError creates a new instance of a reject message price +// oracle error. +func NewErrPriceOracleError(oracleErrCode uint8, + oracleErrMsg string) RejectErr { + + // Sanitise price oracle error message by truncating to 255 characters. + // The price oracle service might be a third-party service and could + // return an error message that is too long. + if len(oracleErrMsg) > 255 { + oracleErrMsg = oracleErrMsg[:255] + } + + errMsg := fmt.Sprintf( + ErrPriceOracleError.Msg, oracleErrCode, oracleErrMsg, + ) + return RejectErr{ + Code: ErrPriceOracleError.Code, + Msg: errMsg, + } +} + +// rejectMsgData is a struct that represents the data field of a quote +// reject message. +type rejectMsgData struct { + // ID represents the unique identifier of the quote request message that + // this response is associated with. + ID ID + + // Err is the error code and message that provides the reason for the + // rejection. + Err RejectErr +} + +// EncodeRecords determines the non-nil records to include when encoding at +// runtime. +func (q *rejectMsgData) encodeRecords() []tlv.Record { + return []tlv.Record{ + TypeRecordRejectID(&q.ID), + TypeRecordRejectErrCode(&q.Err.Code), + TypeRecordRejectErrMsg(&q.Err.Msg), + } +} + +// Encode encodes the structure into a TLV stream. +func (q *rejectMsgData) Encode(writer io.Writer) error { + stream, err := tlv.NewStream(q.encodeRecords()...) + if err != nil { + return err + } + return stream.Encode(writer) +} + +// DecodeRecords provides all TLV records for decoding. +func (q *rejectMsgData) decodeRecords() []tlv.Record { + return []tlv.Record{ + TypeRecordRejectID(&q.ID), + TypeRecordRejectErrCode(&q.Err.Code), + TypeRecordRejectErrMsg(&q.Err.Msg), + } +} + +// Decode decodes the structure from a TLV stream. +func (q *rejectMsgData) Decode(r io.Reader) error { + stream, err := tlv.NewStream(q.decodeRecords()...) + if err != nil { + return err + } + return stream.DecodeP2P(r) +} + +// Bytes encodes the structure into a TLV stream and returns the bytes. +func (q *rejectMsgData) Bytes() ([]byte, error) { + var b bytes.Buffer + err := q.Encode(&b) + if err != nil { + return nil, err + } + + return b.Bytes(), nil +} + +// Reject is a struct that represents a quote reject message. +type Reject struct { + // Peer is the peer that sent the quote request. + Peer route.Vertex + + // rejectMsgData is the message data for the quote reject message. + rejectMsgData +} + +// NewReject creates a new instance of a quote reject message. +func NewReject(request Request, rejectErr RejectErr) *Reject { + return &Reject{ + Peer: request.Peer, + rejectMsgData: rejectMsgData{ + ID: request.ID, + Err: rejectErr, + }, + } +} + +// NewQuoteRejectFromWireMsg instantiates a new instance from a wire message. +func NewQuoteRejectFromWireMsg(wireMsg WireMessage) (*Reject, error) { + // Ensure that the message type is a reject message. + if wireMsg.MsgType != MsgTypeReject { + return nil, fmt.Errorf("unable to create a reject message "+ + "from wire message of type %d", wireMsg.MsgType) + } + + // Decode message data component from TLV bytes. + var msgData rejectMsgData + err := msgData.Decode(bytes.NewReader(wireMsg.Data)) + if err != nil { + return nil, fmt.Errorf("unable to decode quote reject "+ + "message data: %w", err) + } + + return &Reject{ + Peer: wireMsg.Peer, + rejectMsgData: msgData, + }, nil +} + +// ToWire returns a wire message with a serialized data field. +func (q *Reject) ToWire() (WireMessage, error) { + // Encode message data component as TLV bytes. + msgDataBytes, err := q.rejectMsgData.Bytes() + if err != nil { + return WireMessage{}, fmt.Errorf("unable to encode message "+ + "data: %w", err) + } + + return WireMessage{ + Peer: q.Peer, + MsgType: MsgTypeReject, + Data: msgDataBytes, + }, nil +} + +// String returns a human-readable string representation of the message. +func (q *Reject) String() string { + return fmt.Sprintf("Reject(id=%x, err_code=%d, err_msg=%s)", + q.ID[:], q.Err.Code, q.Err.Msg) +} + +// Ensure that the message type implements the OutgoingMsg interface. +var _ OutgoingMsg = (*Reject)(nil) + +// Ensure that the message type implements the IncomingMsg interface. +var _ IncomingMsg = (*Reject)(nil) diff --git a/rfqmsg/request.go b/rfqmsg/request.go new file mode 100644 index 000000000..db84485c4 --- /dev/null +++ b/rfqmsg/request.go @@ -0,0 +1,323 @@ +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 ( + // Request message type field TLV types. + + TypeRequestID tlv.Type = 0 + TypeRequestAssetID tlv.Type = 1 + TypeRequestAssetGroupKey tlv.Type = 3 + TypeRequestAssetAmount tlv.Type = 4 + TypeRequestBidPrice tlv.Type = 6 +) + +func TypeRecordRequestID(id *ID) tlv.Record { + const recordSize = 32 + + return tlv.MakeStaticRecord( + TypeRequestID, id, recordSize, + IdEncoder, IdDecoder, + ) +} + +func IdEncoder(w io.Writer, val any, buf *[8]byte) error { + if t, ok := val.(*ID); ok { + id := [32]byte(*t) + return tlv.EBytes32(w, &id, buf) + } + + return tlv.NewTypeForEncodingErr(val, "MessageID") +} + +func IdDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error { + const idBytesLen = 32 + + if typ, ok := val.(*ID); ok { + var idBytes [idBytesLen]byte + + err := tlv.DBytes32(r, &idBytes, buf, idBytesLen) + if err != nil { + return err + } + + id := ID(idBytes) + + *typ = id + return nil + } + + return tlv.NewTypeForDecodingErr(val, "MessageID", l, idBytesLen) +} + +func TypeRecordRequestAssetID(assetID **asset.ID) tlv.Record { + const recordSize = sha256.Size + + return tlv.MakeStaticRecord( + TypeRequestAssetID, assetID, recordSize, + AssetIdEncoder, AssetIdDecoder, + ) +} + +func AssetIdEncoder(w io.Writer, val any, buf *[8]byte) error { + if t, ok := val.(**asset.ID); ok { + id := [sha256.Size]byte(**t) + return tlv.EBytes32(w, &id, buf) + } + + return tlv.NewTypeForEncodingErr(val, "assetId") +} + +func AssetIdDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error { + const assetIDBytesLen = sha256.Size + + if typ, ok := val.(**asset.ID); ok { + var idBytes [assetIDBytesLen]byte + + err := tlv.DBytes32(r, &idBytes, buf, assetIDBytesLen) + if err != nil { + return err + } + + id := asset.ID(idBytes) + assetId := &id + + *typ = assetId + return nil + } + + return tlv.NewTypeForDecodingErr(val, "assetId", l, sha256.Size) +} + +func TypeRecordRequestAssetGroupKey(groupKey **btcec.PublicKey) tlv.Record { + const recordSize = btcec.PubKeyBytesLenCompressed + + return tlv.MakeStaticRecord( + TypeRequestAssetGroupKey, groupKey, recordSize, + asset.CompressedPubKeyEncoder, asset.CompressedPubKeyDecoder, + ) +} + +func TypeRecordRequestAssetAmount(assetAmount *uint64) tlv.Record { + return tlv.MakePrimitiveRecord(TypeRequestAssetAmount, assetAmount) +} + +func TypeRecordRequestBidPrice(bid *lnwire.MilliSatoshi) tlv.Record { + return tlv.MakeStaticRecord( + TypeRequestBidPrice, bid, 8, + milliSatoshiEncoder, milliSatoshiDecoder, + ) +} + +// requestMsgData is a struct that represents the message data from a quote +// request message. +type requestMsgData 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 is the amount of the asset for which the peer is + // requesting a quote. + AssetAmount uint64 + + // BidPrice is the peer's proposed bid price for the asset amount. + BidPrice lnwire.MilliSatoshi +} + +// Validate ensures that the quote request is valid. +func (q *requestMsgData) 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 *requestMsgData) encodeRecords() []tlv.Record { + var records []tlv.Record + + records = append(records, TypeRecordRequestID(&q.ID)) + + if q.AssetID != nil { + records = append(records, TypeRecordRequestAssetID(&q.AssetID)) + } + + if q.AssetGroupKey != nil { + record := TypeRecordRequestAssetGroupKey(&q.AssetGroupKey) + records = append(records, record) + } + + records = append(records, TypeRecordRequestAssetAmount(&q.AssetAmount)) + records = append(records, TypeRecordRequestBidPrice(&q.BidPrice)) + + return records +} + +// Encode encodes the structure into a TLV stream. +func (q *requestMsgData) 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 *requestMsgData) 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 *requestMsgData) decodeRecords() []tlv.Record { + return []tlv.Record{ + TypeRecordRequestID(&q.ID), + TypeRecordRequestAssetID(&q.AssetID), + TypeRecordRequestAssetGroupKey(&q.AssetGroupKey), + TypeRecordRequestAssetAmount(&q.AssetAmount), + TypeRecordRequestBidPrice(&q.BidPrice), + } +} + +// Decode decodes the structure from a TLV stream. +func (q *requestMsgData) Decode(r io.Reader) error { + stream, err := tlv.NewStream(q.decodeRecords()...) + if err != nil { + return err + } + return stream.DecodeP2P(r) +} + +// Request is a struct that represents a request for a quote (RFQ). +type Request struct { + // Peer is the peer that sent the quote request. + Peer route.Vertex + + // requestMsgData is the message data for the quote request + // message. + requestMsgData +} + +// NewRequest creates a new quote request. +func NewRequest(peer route.Vertex, assetID *asset.ID, + assetGroupKey *btcec.PublicKey, assetAmount uint64, + bidPrice lnwire.MilliSatoshi) (*Request, error) { + + var id [32]byte + _, err := rand.Read(id[:]) + if err != nil { + return nil, fmt.Errorf("unable to generate random "+ + "quote request id: %w", err) + } + + return &Request{ + Peer: peer, + requestMsgData: requestMsgData{ + ID: id, + AssetID: assetID, + AssetGroupKey: assetGroupKey, + AssetAmount: assetAmount, + BidPrice: bidPrice, + }, + }, nil +} + +// NewRequestMsgFromWire instantiates a new instance from a wire message. +func NewRequestMsgFromWire(wireMsg WireMessage) (*Request, error) { + // Ensure that the message type is a quote request message. + if wireMsg.MsgType != MsgTypeRequest { + return nil, fmt.Errorf("unable to create a quote request "+ + "message from wire message of type %d", wireMsg.MsgType) + } + + var msgData requestMsgData + err := msgData.Decode(bytes.NewBuffer(wireMsg.Data)) + if err != nil { + return nil, fmt.Errorf("unable to decode incoming quote "+ + "request message data: %w", err) + } + + quoteRequest := Request{ + Peer: wireMsg.Peer, + requestMsgData: msgData, + } + + // Perform basic sanity checks on the quote request. + err = quoteRequest.Validate() + if err != nil { + return nil, fmt.Errorf("unable to validate quote request: %w", + err) + } + + return "eRequest, nil +} + +// Validate ensures that the quote request is valid. +func (q *Request) Validate() error { + return q.requestMsgData.Validate() +} + +// ToWire returns a wire message with a serialized data field. +func (q *Request) ToWire() (WireMessage, error) { + // Encode message data component as TLV bytes. + msgDataBytes, err := q.requestMsgData.Bytes() + if err != nil { + return WireMessage{}, fmt.Errorf("unable to encode message "+ + "data: %w", err) + } + + return WireMessage{ + Peer: q.Peer, + MsgType: MsgTypeRequest, + Data: msgDataBytes, + }, nil +} + +// String returns a human-readable string representation of the message. +func (q *Request) String() string { + var groupKeyBytes []byte + if q.AssetGroupKey != nil { + groupKeyBytes = q.AssetGroupKey.SerializeCompressed() + } + + return fmt.Sprintf("Request(peer=%s, id=%x, asset_id=%s, "+ + "asset_group_key=%x, asset_amount=%d, bid_price=%d)", q.Peer, + q.ID, q.AssetID, groupKeyBytes, q.AssetAmount, q.BidPrice) +} + +// Ensure that the message type implements the OutgoingMsg interface. +var _ OutgoingMsg = (*Request)(nil) + +// Ensure that the message type implements the IncomingMsg interface. +var _ IncomingMsg = (*Request)(nil) diff --git a/rfqmsg/request_test.go b/rfqmsg/request_test.go new file mode 100644 index 000000000..acfbe1f25 --- /dev/null +++ b/rfqmsg/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" +) + +// TestRequestMsgDataEncodeDecode tests the encoding and decoding of a request +// message. +func TestRequestMsgDataEncodeDecode(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 + bidPrice lnwire.MilliSatoshi + }{ + { + testName: "asset group key nil", + id: id, + assetId: &assetId, + assetGroupKey: nil, + assetAmount: 1000, + bidPrice: lnwire.MilliSatoshi(42000), + }, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(tt *testing.T) { + req := requestMsgData{ + ID: tc.id, + AssetID: tc.assetId, + AssetGroupKey: tc.assetGroupKey, + AssetAmount: tc.assetAmount, + BidPrice: tc.bidPrice, + } + + // Encode the request message. + reqBytes, err := req.Bytes() + require.NoError(tt, err, "unable to encode request") + + // Decode the request message. + decodedReq := requestMsgData{} + err = decodedReq.Decode(bytes.NewReader(reqBytes)) + require.NoError(tt, err, "unable to decode request") + + // Assert that the decoded request message is equal to + // the original request message. + require.Equal(tt, req, decodedReq) + }) + } +} From 47d74e578669a31701b2d15835c5230432627272 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 19 Feb 2024 17:37:09 +0000 Subject: [PATCH 5/8] rfq: add new RFQ service package This commit adds a new package called rfq which contains the RFQ service. Service sub-systems include: 1. Central service manager. 2. Quote price negotiator (and price oracle). 3. Peer message manager (stream handler). 4. HTLC order manager. --- rfq/log.go | 26 +++ rfq/manager.go | 578 ++++++++++++++++++++++++++++++++++++++++++++++ rfq/negotiator.go | 422 +++++++++++++++++++++++++++++++++ rfq/oracle.go | 156 +++++++++++++ rfq/order.go | 317 +++++++++++++++++++++++++ rfq/stream.go | 232 +++++++++++++++++++ 6 files changed, 1731 insertions(+) create mode 100644 rfq/log.go create mode 100644 rfq/manager.go create mode 100644 rfq/negotiator.go create mode 100644 rfq/oracle.go create mode 100644 rfq/order.go create mode 100644 rfq/stream.go diff --git a/rfq/log.go b/rfq/log.go new file mode 100644 index 000000000..eeceea8b4 --- /dev/null +++ b/rfq/log.go @@ -0,0 +1,26 @@ +package rfq + +import ( + "github.com/btcsuite/btclog" +) + +// Subsystem defines the logging code for this subsystem. +const Subsystem = "RFQS" + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log = btclog.Disabled + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/rfq/manager.go b/rfq/manager.go new file mode 100644 index 000000000..a558d9529 --- /dev/null +++ b/rfq/manager.go @@ -0,0 +1,578 @@ +package rfq + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightningnetwork/lnd/lnutils" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +const ( + // DefaultTimeout is the default timeout used for context operations. + DefaultTimeout = 30 * time.Second + + // CacheCleanupInterval is the interval at which local runtime caches + // are cleaned up. + CacheCleanupInterval = 30 * time.Second +) + +// ManagerCfg is a struct that holds the configuration parameters for the RFQ +// manager. +type ManagerCfg struct { + // PeerMessenger is the peer messenger. This component provides the RFQ + // manager with the ability to send and receive raw peer messages. + PeerMessenger PeerMessenger + + // HtlcInterceptor is the HTLC interceptor. This component is used to + // intercept and accept/reject HTLCs. + HtlcInterceptor HtlcInterceptor + + // PriceOracle is the price oracle that the RFQ manager will use to + // determine whether a quote is accepted or rejected. + PriceOracle PriceOracle + + // ErrChan is the main error channel which will be used to report back + // critical errors to the main server. + ErrChan chan<- error +} + +// Manager is a struct that manages the request for quote (RFQ) system. +type Manager struct { + startOnce sync.Once + stopOnce sync.Once + + // cfg holds the configuration parameters for the RFQ manager. + cfg ManagerCfg + + // orderHandler is the RFQ order handler. This subsystem monitors HTLCs + // (Hash Time Locked Contracts), determining acceptance or rejection + // based on compliance with the terms of any associated quote. + orderHandler *OrderHandler + + // streamHandler is the RFQ stream handler. This subsystem handles + // incoming and outgoing peer RFQ stream messages. + streamHandler *StreamHandler + + // negotiator is the RFQ quote negotiator. This subsystem determines + // whether a quote is accepted or rejected. + negotiator *Negotiator + + // incomingMessages is a channel which is populated with incoming + // messages. + incomingMessages chan rfqmsg.IncomingMsg + + // outgoingMessages is a channel which is populated with outgoing + // messages. These are messages which are destined to be sent to peers. + outgoingMessages chan rfqmsg.OutgoingMsg + + // acceptHtlcEvents is a channel which is populated with accept HTLCs + // events. + acceptHtlcEvents chan *AcceptHtlcEvent + + // peerAcceptedQuotes is a map of serialised short channel IDs (SCIDs) + // to associated accepted quotes. These quotes have been accepted by + // peer nodes and are therefore available for use in buying assets. + peerAcceptedQuotes lnutils.SyncMap[SerialisedScid, rfqmsg.Accept] + + // 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]] + + // subsystemErrChan is the error channel populated by subsystems. + subsystemErrChan chan error + + // ContextGuard provides a wait group and main quit channel that can be + // used to create guarded contexts. + *fn.ContextGuard +} + +// NewManager creates a new RFQ manager. +func NewManager(cfg ManagerCfg) (*Manager, error) { + return &Manager{ + cfg: cfg, + + incomingMessages: make(chan rfqmsg.IncomingMsg), + outgoingMessages: make(chan rfqmsg.OutgoingMsg), + + acceptHtlcEvents: make(chan *AcceptHtlcEvent), + peerAcceptedQuotes: lnutils.SyncMap[ + SerialisedScid, rfqmsg.Accept]{}, + + subscribers: lnutils.SyncMap[ + uint64, *fn.EventReceiver[fn.Event]]{}, + + subsystemErrChan: make(chan error, 10), + + ContextGuard: &fn.ContextGuard{ + DefaultTimeout: DefaultTimeout, + Quit: make(chan struct{}), + }, + }, nil +} + +// startSubsystems starts the RFQ subsystems. +func (m *Manager) startSubsystems(ctx context.Context) error { + var err error + + // Initialise and start the order handler. + m.orderHandler, err = NewOrderHandler(OrderHandlerCfg{ + CleanupInterval: CacheCleanupInterval, + HtlcInterceptor: m.cfg.HtlcInterceptor, + AcceptHtlcEvents: m.acceptHtlcEvents, + }) + if err != nil { + return fmt.Errorf("error initializing RFQ order handler: %w", + err) + } + + if err := m.orderHandler.Start(); err != nil { + return fmt.Errorf("unable to start RFQ order handler: %w", err) + } + + // Initialise and start the peer message stream handler. + m.streamHandler, err = NewStreamHandler( + ctx, StreamHandlerCfg{ + PeerMessenger: m.cfg.PeerMessenger, + IncomingMessages: m.incomingMessages, + }, + ) + if err != nil { + return fmt.Errorf("error initializing RFQ subsystem service: "+ + "peer message stream handler: %w", err) + } + + if err := m.streamHandler.Start(); err != nil { + return fmt.Errorf("unable to start RFQ subsystem service: "+ + "peer message stream handler: %w", err) + } + + // Initialise and start the quote negotiator. + m.negotiator, err = NewNegotiator( + NegotiatorCfg{ + PriceOracle: m.cfg.PriceOracle, + OutgoingMessages: m.outgoingMessages, + ErrChan: m.subsystemErrChan, + }, + ) + if err != nil { + return fmt.Errorf("error initializing RFQ negotiator: %w", + err) + } + + if err := m.negotiator.Start(); err != nil { + return fmt.Errorf("unable to start RFQ negotiator: %w", err) + } + + return err +} + +// Start attempts to start a new RFQ manager. +func (m *Manager) Start() error { + var startErr error + m.startOnce.Do(func() { + ctx, cancel := m.WithCtxQuitNoTimeout() + + log.Info("Initializing RFQ subsystems") + err := m.startSubsystems(ctx) + if err != nil { + startErr = err + return + } + + // Start the manager's main event loop in a separate goroutine. + m.Wg.Add(1) + go func() { + defer func() { + m.Wg.Done() + + // Attempt to stop all subsystems if the main + // event loop exits. + err = m.stopSubsystems() + if err != nil { + log.Errorf("Error stopping RFQ "+ + "subsystems: %v", err) + } + + // The context can now be cancelled as all + // dependant components have been stopped. + cancel() + }() + + log.Info("Starting RFQ manager main event loop") + m.mainEventLoop() + }() + }) + return startErr +} + +// Stop attempts to stop the RFQ manager. +func (m *Manager) Stop() error { + var stopErr error + + m.stopOnce.Do(func() { + log.Info("Stopping RFQ system") + stopErr = m.stopSubsystems() + + // Stop the main event loop. + close(m.Quit) + }) + + return stopErr +} + +// stopSubsystems stops the RFQ subsystems. +func (m *Manager) stopSubsystems() error { + // Stop the RFQ order handler. + err := m.orderHandler.Stop() + if err != nil { + return fmt.Errorf("error stopping RFQ order handler: %w", err) + } + + // Stop the RFQ stream handler. + err = m.streamHandler.Stop() + if err != nil { + return fmt.Errorf("error stopping RFQ stream handler: %w", err) + } + + // Stop the RFQ quote negotiator. + err = m.negotiator.Stop() + if err != nil { + return fmt.Errorf("error stopping RFQ quote negotiator: %w", + err) + } + + return nil +} + +// handleIncomingMessage handles an incoming message. These are messages that +// have been received from a peer. +func (m *Manager) handleIncomingMessage(incomingMsg rfqmsg.IncomingMsg) error { + // Perform type specific handling of the incoming message. + switch msg := incomingMsg.(type) { + case *rfqmsg.Request: + err := m.negotiator.HandleIncomingQuoteRequest(*msg) + if err != nil { + return fmt.Errorf("error handling incoming quote "+ + "request: %w", err) + } + + case *rfqmsg.Accept: + // 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.peerAcceptedQuotes.Store(scid, *msg) + + // Notify subscribers of the incoming quote accept. + event := NewIncomingAcceptQuoteEvent(msg) + m.publishSubscriberEvent(event) + + case *rfqmsg.Reject: + // The quote request has been rejected. Notify subscribers of + // the rejection. + event := NewIncomingRejectQuoteEvent(msg) + m.publishSubscriberEvent(event) + + default: + return fmt.Errorf("unhandled incoming message type: %T", msg) + } + + return nil +} + +// handleOutgoingMessage handles an outgoing message. Outgoing messages are +// messages that will be sent to a peer. +func (m *Manager) handleOutgoingMessage(outgoingMsg rfqmsg.OutgoingMsg) error { + // Perform type specific handling of the outgoing message. + switch msg := outgoingMsg.(type) { + case *rfqmsg.Accept: + // Before sending an accept message to a peer, inform the HTLC + // order handler that we've accepted the quote request. + m.orderHandler.RegisterChannelRemit(*msg) + } + + // Send the outgoing message to the peer. + err := m.streamHandler.HandleOutgoingMessage(outgoingMsg) + if err != nil { + return fmt.Errorf("error sending outgoing message to stream "+ + "handler: %w", err) + } + + return nil +} + +// mainEventLoop is the main event loop of the RFQ manager. +func (m *Manager) mainEventLoop() { + for { + select { + // Handle incoming message. + case incomingMsg := <-m.incomingMessages: + log.Debugf("Manager handling incoming message: %s", + incomingMsg) + + err := m.handleIncomingMessage(incomingMsg) + if err != nil { + m.cfg.ErrChan <- fmt.Errorf("failed to "+ + "handle incoming message: %w", err) + } + + // Handle outgoing message. + case outgoingMsg := <-m.outgoingMessages: + log.Debugf("Manager handling outgoing message: %s", + outgoingMsg) + + err := m.handleOutgoingMessage(outgoingMsg) + if err != nil { + m.cfg.ErrChan <- fmt.Errorf("failed to "+ + "handle outgoing message: %w", err) + } + + case acceptHtlcEvent := <-m.acceptHtlcEvents: + // Handle a HTLC accept event. Notify any subscribers. + m.publishSubscriberEvent(acceptHtlcEvent) + + // Handle subsystem errors. + case err := <-m.subsystemErrChan: + // Report the subsystem error to the main server. + m.cfg.ErrChan <- fmt.Errorf("encountered RFQ "+ + "subsystem error: %w", err) + + case <-m.Quit: + log.Debug("Manager main event loop has received the " + + "shutdown signal") + return + } + } +} + +// UpsertAssetSellOffer upserts an asset sell offer for management by the RFQ +// system. If the offer already exists for the given asset, it will be updated. +func (m *Manager) UpsertAssetSellOffer(offer SellOffer) error { + // Store the asset sell offer in the negotiator. + err := m.negotiator.UpsertAssetSellOffer(offer) + if err != nil { + return fmt.Errorf("error registering asset sell offer: %w", err) + } + + return nil +} + +// RemoveAssetSellOffer removes an asset sell offer from the RFQ manager. +func (m *Manager) RemoveAssetSellOffer(assetID *asset.ID, + assetGroupKey *btcec.PublicKey) error { + + // Remove the asset sell offer from the negotiator. + err := m.negotiator.RemoveAssetSellOffer(assetID, assetGroupKey) + if err != nil { + return fmt.Errorf("error removing asset sell offer: %w", err) + } + + return nil +} + +// BuyOrder is a struct that represents a buy order. +type BuyOrder struct { + // AssetID is the ID of the asset that the buyer is interested in. + AssetID *asset.ID + + // AssetGroupKey is the public key of the asset group that the buyer is + // interested in. + AssetGroupKey *btcec.PublicKey + + // MinAssetAmount is the minimum amount of the asset that the buyer is + // willing to accept. + MinAssetAmount uint64 + + // MaxBid is the maximum bid price that the buyer is willing to pay. + MaxBid lnwire.MilliSatoshi + + // Expiry is the unix timestamp at which the buy order expires. + Expiry uint64 + + // Peer is the peer that the buy order is intended for. This field is + // optional. + Peer *route.Vertex +} + +// UpsertAssetBuyOrder upserts an asset buy order for management. +func (m *Manager) UpsertAssetBuyOrder(order BuyOrder) error { + // For now, a peer must be specified. + // + // TODO(ffranr): Add support for peerless buy orders. The negotiator + // should be able to determine the optimal peer. + if order.Peer == nil { + return fmt.Errorf("buy order peer must be specified") + } + + // Request a quote from a peer via the negotiator. + err := m.negotiator.RequestQuote(order) + if err != nil { + return fmt.Errorf("error registering asset buy order: %w", err) + } + + return nil +} + +// QueryAcceptedQuotes returns a map of accepted quotes that have been +// registered with the RFQ manager. +func (m *Manager) QueryAcceptedQuotes() map[SerialisedScid]rfqmsg.Accept { + // Returning the map directly is not thread safe. We will therefore + // create a copy. + quotesCopy := make(map[SerialisedScid]rfqmsg.Accept) + + m.peerAcceptedQuotes.ForEach( + func(scid SerialisedScid, accept rfqmsg.Accept) error { + if time.Now().Unix() > int64(accept.Expiry) { + m.peerAcceptedQuotes.Delete(scid) + return nil + } + + quotesCopy[scid] = accept + return nil + }, + ) + + return quotesCopy +} + +// RegisterSubscriber adds a new subscriber to the set of subscribers that will +// be notified of any new events that are broadcast. +// +// TODO(ffranr): Add support for delivering existing events to new subscribers. +func (m *Manager) RegisterSubscriber( + receiver *fn.EventReceiver[fn.Event], + deliverExisting bool, deliverFrom uint64) error { + + m.subscribers.Store(receiver.ID(), receiver) + return nil +} + +// RemoveSubscriber removes a subscriber from the set of subscribers that will +// be notified of any new events that are broadcast. +func (m *Manager) RemoveSubscriber( + subscriber *fn.EventReceiver[fn.Event]) error { + + _, ok := m.subscribers.Load(subscriber.ID()) + if !ok { + return fmt.Errorf("subscriber with ID %d not found", + subscriber.ID()) + } + + subscriber.Stop() + m.subscribers.Delete(subscriber.ID()) + + return nil +} + +// publishSubscriberEvent publishes an event to all subscribers. +func (m *Manager) publishSubscriberEvent(event fn.Event) { + // Iterate over the subscribers and deliver the event to each one. + m.subscribers.Range( + func(id uint64, sub *fn.EventReceiver[fn.Event]) bool { + sub.NewItemCreated.ChanIn() <- event + return true + }, + ) +} + +// IncomingAcceptQuoteEvent is an event that is broadcast when the RFQ manager +// receives an accept quote message from a peer. +type IncomingAcceptQuoteEvent struct { + // timestamp is the event creation UTC timestamp. + timestamp time.Time + + // Accept is the accepted quote. + rfqmsg.Accept +} + +// NewIncomingAcceptQuoteEvent creates a new IncomingAcceptQuoteEvent. +func NewIncomingAcceptQuoteEvent( + accept *rfqmsg.Accept) *IncomingAcceptQuoteEvent { + + return &IncomingAcceptQuoteEvent{ + timestamp: time.Now().UTC(), + Accept: *accept, + } +} + +// Timestamp returns the event creation UTC timestamp. +func (q *IncomingAcceptQuoteEvent) Timestamp() time.Time { + return q.timestamp.UTC() +} + +// Ensure that the IncomingAcceptQuoteEvent struct implements the Event +// interface. +var _ fn.Event = (*IncomingAcceptQuoteEvent)(nil) + +// IncomingRejectQuoteEvent is an event that is broadcast when the RFQ manager +// receives a reject quote message from a peer. +type IncomingRejectQuoteEvent struct { + // timestamp is the event creation UTC timestamp. + timestamp time.Time + + // Reject is the rejected quote. + rfqmsg.Reject +} + +// NewIncomingRejectQuoteEvent creates a new IncomingRejectQuoteEvent. +func NewIncomingRejectQuoteEvent( + reject *rfqmsg.Reject) *IncomingRejectQuoteEvent { + + return &IncomingRejectQuoteEvent{ + timestamp: time.Now().UTC(), + Reject: *reject, + } +} + +// Timestamp returns the event creation UTC timestamp. +func (q *IncomingRejectQuoteEvent) Timestamp() time.Time { + return q.timestamp.UTC() +} + +// Ensure that the IncomingRejectQuoteEvent struct implements the Event +// interface. +var _ fn.Event = (*IncomingRejectQuoteEvent)(nil) + +// AcceptHtlcEvent is an event that is sent to the accept HTLCs channel when +// an HTLC is accepted. +type AcceptHtlcEvent struct { + // Timestamp is the unix timestamp at which the HTLC was accepted. + timestamp uint64 + + // Htlc is the intercepted HTLC. + Htlc lndclient.InterceptedHtlc + + // ChannelRemit is the channel remit that the HTLC complies with. + ChannelRemit ChannelRemit +} + +// NewAcceptHtlcEvent creates a new AcceptedHtlcEvent. +func NewAcceptHtlcEvent(htlc lndclient.InterceptedHtlc, + channelRemit ChannelRemit) *AcceptHtlcEvent { + + return &AcceptHtlcEvent{ + timestamp: uint64(time.Now().UTC().Unix()), + Htlc: htlc, + ChannelRemit: channelRemit, + } +} + +// Timestamp returns the event creation UTC timestamp. +func (q *AcceptHtlcEvent) Timestamp() time.Time { + return time.Unix(int64(q.timestamp), 0).UTC() +} + +// Ensure that the AcceptedHtlcEvent struct implements the Event interface. +var _ fn.Event = (*AcceptHtlcEvent)(nil) diff --git a/rfq/negotiator.go b/rfq/negotiator.go new file mode 100644 index 000000000..4232f6cb7 --- /dev/null +++ b/rfq/negotiator.go @@ -0,0 +1,422 @@ +package rfq + +import ( + "fmt" + "sync" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightningnetwork/lnd/lnutils" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +// NegotiatorCfg holds the configuration for the negotiator. +type NegotiatorCfg struct { + // PriceOracle is the price oracle that the negotiator will use to + // determine whether a quote is accepted or rejected. + PriceOracle PriceOracle + + // OutgoingMessages is a channel which is populated with outgoing peer + // messages. These are messages which are destined to be sent to peers. + OutgoingMessages chan<- rfqmsg.OutgoingMsg + + // ErrChan is a channel that is populated with errors by this subsystem. + ErrChan chan<- error +} + +// Negotiator is a struct that handles the negotiation of quotes. It is a RFQ +// subsystem. It determines whether a quote request is accepted or rejected. +type Negotiator struct { + startOnce sync.Once + stopOnce sync.Once + + // cfg holds the configuration parameters for the negotiator. + cfg NegotiatorCfg + + // assetSellOffers is a map (keyed on asset ID) that holds asset sell + // offers. + assetSellOffers lnutils.SyncMap[asset.ID, SellOffer] + + // assetGroupSellOffers is a map (keyed on asset group key) that holds + // asset sell offers. + assetGroupSellOffers lnutils.SyncMap[btcec.PublicKey, SellOffer] + + // ContextGuard provides a wait group and main quit channel that can be + // used to create guarded contexts. + *fn.ContextGuard +} + +// NewNegotiator creates a new quote negotiator. +func NewNegotiator(cfg NegotiatorCfg) (*Negotiator, error) { + // If the price oracle is nil, then we will return an error. + if cfg.PriceOracle == nil { + return nil, fmt.Errorf("price oracle is nil") + } + + return &Negotiator{ + cfg: cfg, + + assetSellOffers: lnutils.SyncMap[asset.ID, SellOffer]{}, + assetGroupSellOffers: lnutils.SyncMap[ + btcec.PublicKey, SellOffer]{}, + + ContextGuard: &fn.ContextGuard{ + DefaultTimeout: DefaultTimeout, + Quit: make(chan struct{}), + }, + }, nil +} + +// queryBidFromPriceOracle queries the price oracle for a bid price. It returns +// an appropriate outgoing response message which should be sent to the peer. +func (n *Negotiator) queryBidFromPriceOracle(peer route.Vertex, + assetId *asset.ID, assetGroupKey *btcec.PublicKey, + assetAmount uint64) (rfqmsg.OutgoingMsg, error) { + + ctx, cancel := n.WithCtxQuitNoTimeout() + defer cancel() + + oracleResponse, err := n.cfg.PriceOracle.QueryBidPrice( + ctx, assetId, assetGroupKey, assetAmount, + ) + if err != nil { + return nil, fmt.Errorf("failed to query price oracle for "+ + "bid: %w", err) + } + + // TODO(ffranr): Check if the price oracle returned an error. + + // If the bid price is nil, then we will return the error message + // supplied by the price oracle. + if oracleResponse.BidPrice == nil { + return nil, fmt.Errorf("price oracle returned error: %v", + *oracleResponse.Err) + } + + // TODO(ffranr): Ensure that the expiryDelay time is valid and + // sufficient. + + request, err := rfqmsg.NewRequest( + peer, assetId, assetGroupKey, assetAmount, + *oracleResponse.BidPrice, + ) + if err != nil { + return nil, fmt.Errorf("unable to create quote request "+ + "message: %w", err) + } + + return request, nil +} + +// RequestQuote requests a bid quote (buying an asset) from a peer. +func (n *Negotiator) RequestQuote(buyOrder BuyOrder) error { + // Query the price oracle for a reasonable bid 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 a bid price. + outgoingMsg, err := n.queryBidFromPriceOracle( + *buyOrder.Peer, buyOrder.AssetID, + buyOrder.AssetGroupKey, buyOrder.MinAssetAmount, + ) + if err != nil { + err := fmt.Errorf("negotiator failed to handle price "+ + "oracle response: %w", err) + n.cfg.ErrChan <- err + return + } + + // Send the response message to the outgoing messages channel. + sendSuccess := fn.SendOrQuit( + n.cfg.OutgoingMessages, outgoingMsg, n.Quit, + ) + if !sendSuccess { + err := fmt.Errorf("negotiator failed to add quote " + + "request message to the outgoing messages " + + "channel") + n.cfg.ErrChan <- err + return + } + }() + + return nil +} + +// queryAskFromPriceOracle queries the price oracle for an asking price. It +// returns an appropriate outgoing response message which should be sent to the +// peer. +func (n *Negotiator) queryAskFromPriceOracle( + request rfqmsg.Request) (rfqmsg.OutgoingMsg, error) { + + // Query the price oracle for an asking price. + ctx, cancel := n.WithCtxQuitNoTimeout() + defer cancel() + + oracleResponse, err := n.cfg.PriceOracle.QueryAskPrice( + ctx, request.AssetID, request.AssetGroupKey, + request.AssetAmount, &request.BidPrice, + ) + if err != nil { + return nil, fmt.Errorf("failed to query price oracle for ask "+ + "price: %w", err) + } + + // If the price oracle returned an error, then we will return a quote + // reject message which contains the error message supplied by the + // price oracle. + if oracleResponse.Err != nil { + rejectErr := rfqmsg.NewErrPriceOracleError( + oracleResponse.Err.Code, oracleResponse.Err.Msg, + ) + + reject := rfqmsg.NewReject(request, rejectErr) + return reject, nil + } + + // By this point the price oracle, did not return an error. However, if + // the asking price is nil, then we will return a quote reject + // message indicating that the price oracle did not specify an error. + if oracleResponse.AskPrice == nil { + rejectErr := rfqmsg.ErrPriceOracleUnspecifiedError + + reject := rfqmsg.NewReject(request, rejectErr) + return reject, nil + } + + // TODO(ffranr): Ensure that the expiryDelay time is valid and + // sufficient. + + // If the asking price is not nil, then we can proceed to compute a + // final asking price. + // + // If the bid price (bid price suggested in the quote request) is + // greater than the asking price, then we will use the bid price as the + // final asking price. Otherwise, we will use the asking price provided + // by the price oracle as the final asking price. + var finalAskPrice lnwire.MilliSatoshi + + if request.BidPrice > *oracleResponse.AskPrice { + finalAskPrice = request.BidPrice + } else { + finalAskPrice = *oracleResponse.AskPrice + } + + accept := rfqmsg.NewAcceptFromRequest( + request, finalAskPrice, oracleResponse.Expiry, + ) + return accept, nil +} + +// HandleIncomingQuoteRequest handles an incoming quote request. +func (n *Negotiator) HandleIncomingQuoteRequest(request rfqmsg.Request) error { + // Ensure that we have a suitable sell offer for the asset that is being + // requested. Here we can handle the case where this node does not wish + // to sell a particular asset. + offerAvailable := n.HasAssetSellOffer( + request.AssetID, request.AssetGroupKey, request.AssetAmount, + ) + if !offerAvailable { + // If we do not have a suitable sell offer, then we will reject + // the quote request with an error. + reject := rfqmsg.NewReject( + request, rfqmsg.ErrNoSuitableSellOffer, + ) + var msg rfqmsg.OutgoingMsg = reject + + sendSuccess := fn.SendOrQuit( + n.cfg.OutgoingMessages, msg, n.Quit, + ) + if !sendSuccess { + return fmt.Errorf("negotiator failed to send reject " + + "message") + } + + return nil + } + + // Initiate a query to the price oracle asynchronously using a separate + // goroutine. Since 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 an asking price. + outgoingMsgResponse, err := n.queryAskFromPriceOracle(request) + if err != nil { + err = fmt.Errorf("negotiator failed to handle price "+ + "oracle ask price response: %w", err) + n.cfg.ErrChan <- err + } + + // Send the response message to the outgoing messages channel. + sendSuccess := fn.SendOrQuit( + n.cfg.OutgoingMessages, outgoingMsgResponse, n.Quit, + ) + if !sendSuccess { + err = fmt.Errorf("negotiator failed to add message "+ + "to the outgoing messages channel (msg=%v)", + outgoingMsgResponse) + n.cfg.ErrChan <- err + } + }() + + return nil +} + +// 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. +// +// A sell offer is passive (unlike a buy order), meaning that it does not +// actively lead to a bid request from a peer. Instead, it is used by the node +// to selectively accept or reject incoming quote requests early before price +// considerations. +type SellOffer struct { + // AssetID represents the identifier of the subject asset. + AssetID *asset.ID + + // AssetGroupKey is the public group key of the subject asset. + AssetGroupKey *btcec.PublicKey + + // MaxUnits is the maximum amount of the asset under offer. + MaxUnits uint64 +} + +// Validate validates the asset sell offer. +func (a *SellOffer) Validate() error { + if a.AssetID == nil && a.AssetGroupKey == nil { + return fmt.Errorf("asset ID is nil and asset group key is nil") + } + + if a.AssetID != nil && a.AssetGroupKey != nil { + return fmt.Errorf("asset ID and asset group key are both set") + } + + if a.MaxUnits == 0 { + return fmt.Errorf("max asset amount is zero") + } + + return nil +} + +// UpsertAssetSellOffer upserts an asset sell offer. If the offer already exists +// for the given asset, it will be updated. +func (n *Negotiator) UpsertAssetSellOffer(offer SellOffer) error { + // Validate the offer. + err := offer.Validate() + if err != nil { + return fmt.Errorf("invalid asset sell offer: %w", err) + } + + // Store the offer in the appropriate map. + // + // If the asset group key is not nil, then we will use it as the key for + // the offer. Otherwise, we will use the asset ID as the key. + switch { + case offer.AssetGroupKey != nil: + n.assetGroupSellOffers.Store(*offer.AssetGroupKey, offer) + + case offer.AssetID != nil: + n.assetSellOffers.Store(*offer.AssetID, offer) + } + + return nil +} + +// RemoveAssetSellOffer removes an asset sell offer from the negotiator. +func (n *Negotiator) RemoveAssetSellOffer(assetID *asset.ID, + assetGroupKey *btcec.PublicKey) error { + + // Remove the offer from the appropriate map. + // + // If the asset group key is not nil, then we will use it as the key for + // the offer. Otherwise, we will use the asset ID as the key. + switch { + case assetGroupKey != nil: + n.assetGroupSellOffers.Delete(*assetGroupKey) + + case assetID != nil: + n.assetSellOffers.Delete(*assetID) + + default: + return fmt.Errorf("asset ID and asset group key are both nil") + } + + return nil +} + +// HasAssetSellOffer returns true if the negotiator has an asset sell offer +// which matches the given asset ID/group and asset amount. +// +// TODO(ffranr): This method should return errors which can be used to +// differentiate between a missing offer and an invalid offer. +func (n *Negotiator) HasAssetSellOffer(assetID *asset.ID, + assetGroupKey *btcec.PublicKey, assetAmt uint64) bool { + + // If the asset group key is not nil, then we will use it as the key for + // the offer. Otherwise, we will use the asset ID as the key. + var sellOffer *SellOffer + switch { + case assetGroupKey != nil: + offer, ok := n.assetGroupSellOffers.Load(*assetGroupKey) + if !ok { + // Corresponding offer not found. + return false + } + + sellOffer = &offer + + case assetID != nil: + offer, ok := n.assetSellOffers.Load(*assetID) + if !ok { + // Corresponding offer not found. + return false + } + + sellOffer = &offer + } + + // We should never have a nil sell offer at this point. Check added here + // for robustness. + if sellOffer == nil { + return false + } + + // If the asset amount is greater than the maximum asset amount under + // offer, then we will return false (we do not have a suitable offer). + if assetAmt > sellOffer.MaxUnits { + log.Warnf("asset amount is greater than sell offer max units "+ + "(asset_amt=%d, sell_offer_max_units=%d)", assetAmt, + sellOffer.MaxUnits) + return false + } + + return true +} + +// Start starts the service. +func (n *Negotiator) Start() error { + var startErr error + n.startOnce.Do(func() { + log.Info("Starting subsystem: negotiator") + }) + return startErr +} + +// Stop stops the handler. +func (n *Negotiator) Stop() error { + n.stopOnce.Do(func() { + log.Info("Stopping subsystem: quote negotiator") + + // Stop any active context. + close(n.Quit) + }) + return nil +} diff --git a/rfq/oracle.go b/rfq/oracle.go new file mode 100644 index 000000000..b3d566a34 --- /dev/null +++ b/rfq/oracle.go @@ -0,0 +1,156 @@ +package rfq + +import ( + "context" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightningnetwork/lnd/lnwire" +) + +// OracleError is a struct that holds an error returned by the price oracle +// service. +type OracleError struct { + // Code is a code which uniquely identifies the error type. + Code uint8 + + // Msg is a human-readable error message. + Msg string +} + +// OracleAskResponse is a struct that holds the price oracle's suggested ask +// price for an asset. +type OracleAskResponse struct { + // AskPrice is the asking price of the quote. + AskPrice *lnwire.MilliSatoshi + + // Expiry is the price expiryDelay lifetime unix timestamp. + Expiry uint64 + + // Err is an optional error returned by the price oracle service. + Err *OracleError +} + +// OracleBidResponse is a struct that holds the price oracle's suggested bid +// price for an asset. +type OracleBidResponse struct { + // BidPrice is the suggested bid price for the asset amount. + BidPrice *lnwire.MilliSatoshi + + // Expiry is the price expiryDelay lifetime unix timestamp. + Expiry uint64 + + // Err is an optional error returned by the price oracle service. + Err *OracleError +} + +// PriceOracle is an interface that provides exchange rate information for +// assets. +type PriceOracle interface { + // QueryAskPrice returns an asking price for the given asset amount. + QueryAskPrice(ctx context.Context, assetId *asset.ID, + assetGroupKey *btcec.PublicKey, assetAmount uint64, + suggestedBidPrice *lnwire.MilliSatoshi) (*OracleAskResponse, + error) + + // QueryBidPrice returns a bid price for the given asset amount. + QueryBidPrice(ctx context.Context, assetId *asset.ID, + assetGroupKey *btcec.PublicKey, + assetAmount uint64) (*OracleBidResponse, error) +} + +//// RpcPriceOracle is a price oracle that uses an external RPC server to get +//// exchange rate information. +//type RpcPriceOracle struct { +//} +// +//// serverDialOpts returns the set of server options needed to connect to the +//// price oracle RPC server using a TLS connection. +//func serverDialOpts() ([]grpc.DialOption, error) { +// var opts []grpc.DialOption +// +// // Skip TLS certificate verification. +// tlsConfig := tls.Config{InsecureSkipVerify: true} +// transportCredentials := credentials.NewTLS(&tlsConfig) +// opts = append(opts, grpc.WithTransportCredentials(transportCredentials)) +// +// return opts, nil +//} +// +//// NewRpcPriceOracle creates a new RPC price oracle handle given the address +//// of the price oracle RPC server. +//func NewRpcPriceOracle(addr url.URL) (*RpcPriceOracle, error) { +// //// Connect to the RPC server. +// //dialOpts, err := serverDialOpts() +// //if err != nil { +// // return nil, err +// //} +// // +// //serverAddr := fmt.Sprintf("%s:%s", addr.Hostname(), addr.Port()) +// //conn, err := grpc.Dial(serverAddr, dialOpts...) +// //if err != nil { +// // return nil, err +// //} +// +// return &RpcPriceOracle{}, nil +//} +// +//// QueryAskingPrice returns the asking price for the given asset amount. +//func (r *RpcPriceOracle) QueryAskingPrice(ctx context.Context, +// assetId *asset.ID, assetGroupKey *btcec.PublicKey, assetAmount uint64, +// bidPrice *lnwire.MilliSatoshi) (*OracleAskResponse, error) { +// +// //// Call the external oracle service to get the exchange rate. +// //conn := getClientConn(ctx, false) +// +// return nil, nil +//} +// +//// Ensure that RpcPriceOracle implements the PriceOracle interface. +//var _ PriceOracle = (*RpcPriceOracle)(nil) + +// MockPriceOracle is a mock implementation of the PriceOracle interface. +// It returns the suggested rate as the exchange rate. +type MockPriceOracle struct { + expiryDelay uint64 +} + +// NewMockPriceOracle creates a new mock price oracle. +func NewMockPriceOracle(expiryDelay uint64) *MockPriceOracle { + return &MockPriceOracle{ + expiryDelay: expiryDelay, + } +} + +// QueryAskPrice returns the ask price for the given asset amount. +func (m *MockPriceOracle) QueryAskPrice(_ context.Context, + _ *asset.ID, _ *btcec.PublicKey, _ uint64, + suggestedBidPrice *lnwire.MilliSatoshi) (*OracleAskResponse, error) { + + // Calculate the rate expiryDelay lifetime. + expiry := uint64(time.Now().Unix()) + m.expiryDelay + + return &OracleAskResponse{ + AskPrice: suggestedBidPrice, + Expiry: expiry, + }, nil +} + +// QueryBidPrice returns a bid price for the given asset amount. +func (m *MockPriceOracle) QueryBidPrice(_ context.Context, _ *asset.ID, + _ *btcec.PublicKey, _ uint64) (*OracleBidResponse, error) { + + // Calculate the rate expiryDelay lifetime. + expiry := uint64(time.Now().Unix()) + m.expiryDelay + + bidPrice := lnwire.MilliSatoshi(42000) + + return &OracleBidResponse{ + BidPrice: &bidPrice, + Expiry: expiry, + }, nil +} + +// Ensure that MockPriceOracle implements the PriceOracle interface. +var _ PriceOracle = (*MockPriceOracle)(nil) diff --git a/rfq/order.go b/rfq/order.go new file mode 100644 index 000000000..3b2ad0c8d --- /dev/null +++ b/rfq/order.go @@ -0,0 +1,317 @@ +package rfq + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightningnetwork/lnd/lnutils" + "github.com/lightningnetwork/lnd/lnwire" +) + +// SerialisedScid is a serialised short channel id (SCID). +type SerialisedScid uint64 + +// ChannelRemit is a struct that holds the terms which determine whether a +// channel HTLC is accepted or rejected. +type ChannelRemit struct { + // Scid is the serialised short channel ID (SCID) of the channel to + // which the remit applies. + Scid SerialisedScid + + // 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 asking price expiryDelay lifetime unix timestamp. + Expiry uint64 +} + +// NewChannelRemit creates a new channel remit. +func NewChannelRemit(quoteAccept rfqmsg.Accept) *ChannelRemit { + // Compute the serialised short channel ID (SCID) for the channel. + scid := SerialisedScid(quoteAccept.ShortChannelId()) + + return &ChannelRemit{ + Scid: scid, + AssetAmount: quoteAccept.AssetAmount, + MinimumChannelPayment: quoteAccept.AskPrice, + Expiry: quoteAccept.Expiry, + } +} + +// CheckHtlcCompliance returns an error if the given HTLC intercept descriptor +// does not satisfy the subject channel remit. +func (c *ChannelRemit) CheckHtlcCompliance( + htlc lndclient.InterceptedHtlc) error { + + // Check that the channel SCID is as expected. + htlcScid := SerialisedScid(htlc.OutgoingChannelID.ToUint64()) + if htlcScid != c.Scid { + return fmt.Errorf("htlc outgoing channel ID does not match "+ + "remit's SCID (htlc_scid=%d, remit_scid=%d)", htlcScid, + c.Scid) + } + + // 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 remit's "+ + "minimum (htlc_out_msat=%d, remit_min_msat=%d)", + htlc.AmountOutMsat, c.MinimumChannelPayment) + } + + // Lastly, check to ensure that the channel remit has not expired. + if time.Now().Unix() > int64(c.Expiry) { + return fmt.Errorf("channel remit has expired "+ + "(expiry_unix_ts=%d)", c.Expiry) + } + + return nil +} + +// OrderHandlerCfg is a struct that holds the configuration parameters for the +// order handler service. +type OrderHandlerCfg struct { + // CleanupInterval is the interval at which the order handler cleans up + // expired accepted quotes from its local cache. + CleanupInterval time.Duration + + // HtlcInterceptor is the HTLC interceptor. This component is used to + // intercept and accept/reject HTLCs. + HtlcInterceptor HtlcInterceptor + + // AcceptHtlcEvents is a channel that receives accepted HTLCs. + AcceptHtlcEvents chan<- *AcceptHtlcEvent +} + +// OrderHandler orchestrates management of accepted quote bundles. It monitors +// HTLCs (Hash Time Locked Contracts), and determines acceptance/rejection based +// on the terms of the associated accepted quote. +type OrderHandler struct { + startOnce sync.Once + stopOnce sync.Once + + // cfg holds the configuration parameters for the RFQ order handler. + cfg OrderHandlerCfg + + // channelRemits is a map of serialised short channel IDs (SCIDs) to + // associated active channel remits. + channelRemits lnutils.SyncMap[SerialisedScid, *ChannelRemit] + + // ContextGuard provides a wait group and main quit channel that can be + // used to create guarded contexts. + *fn.ContextGuard +} + +// NewOrderHandler creates a new struct instance. +func NewOrderHandler(cfg OrderHandlerCfg) (*OrderHandler, error) { + return &OrderHandler{ + cfg: cfg, + channelRemits: lnutils.SyncMap[SerialisedScid, *ChannelRemit]{}, + ContextGuard: &fn.ContextGuard{ + DefaultTimeout: DefaultTimeout, + Quit: make(chan struct{}), + }, + }, nil +} + +// handleIncomingHtlc handles an incoming HTLC. +// +// NOTE: This function must be thread safe. It is used by an external +// interceptor service. +func (h *OrderHandler) handleIncomingHtlc(_ context.Context, + htlc lndclient.InterceptedHtlc) (*lndclient.InterceptedHtlcResponse, + error) { + + log.Debug("Handling incoming HTLC") + + scid := SerialisedScid(htlc.OutgoingChannelID.ToUint64()) + channelRemit, ok := h.FetchChannelRemit(scid) + + // If a channel remit 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 channel remit. + if !ok { + return &lndclient.InterceptedHtlcResponse{ + Action: lndclient.InterceptorActionResume, + }, nil + } + + // At this point, we know that the channel remit exists and has not + // expired whilst sitting in the local cache. We can now check that the + // HTLC complies with the channel remit. + err := channelRemit.CheckHtlcCompliance(htlc) + if err != nil { + log.Warnf("HTLC does not comply with channel remit: %v "+ + "(htlc=%v, channel_remit=%v)", err, htlc, channelRemit) + + return &lndclient.InterceptedHtlcResponse{ + Action: lndclient.InterceptorActionFail, + }, nil + } + + log.Debug("HTLC complies with channel remit. Broadcasting accept " + + "event.") + acceptHtlcEvent := NewAcceptHtlcEvent(htlc, *channelRemit) + h.cfg.AcceptHtlcEvents <- acceptHtlcEvent + + return &lndclient.InterceptedHtlcResponse{ + Action: lndclient.InterceptorActionResume, + }, nil +} + +// setupHtlcIntercept sets up HTLC interception. +func (h *OrderHandler) setupHtlcIntercept(ctx context.Context) error { + // Intercept incoming HTLCs. This call passes the handleIncomingHtlc + // function to the interceptor. The interceptor will call this function + // in a separate goroutine. + err := h.cfg.HtlcInterceptor.InterceptHtlcs(ctx, h.handleIncomingHtlc) + if err != nil { + if fn.IsCanceled(err) { + return nil + } + + return fmt.Errorf("unable to setup incoming HTLC "+ + "interceptor: %w", err) + } + + return nil +} + +// mainEventLoop executes the main event handling loop. +func (h *OrderHandler) mainEventLoop() { + log.Debug("Starting main event loop for order handler") + + cleanupTicker := time.NewTicker(h.cfg.CleanupInterval) + defer cleanupTicker.Stop() + + for { + select { + // Periodically clean up expired channel remits from our local + // cache. + case <-cleanupTicker.C: + log.Debug("Cleaning up any stale channel remits from " + + "the order handler") + h.cleanupStaleChannelRemits() + + case <-h.Quit: + log.Debug("Received quit signal. Stopping negotiator " + + "event loop") + return + } + } +} + +// Start starts the service. +func (h *OrderHandler) Start() error { + var startErr error + h.startOnce.Do(func() { + log.Info("Starting subsystem: order handler") + + // Start the main event loop in a separate goroutine. + h.Wg.Add(1) + go func() { + defer h.Wg.Done() + + ctx, cancel := h.WithCtxQuitNoTimeout() + defer cancel() + + startErr = h.setupHtlcIntercept(ctx) + if startErr != nil { + log.Errorf("Error setting up HTLC "+ + "interception: %v", startErr) + return + } + + h.mainEventLoop() + }() + }) + + return startErr +} + +// RegisterChannelRemit registers a channel management remit. If a remit exists +// for the channel SCID, it is overwritten. +func (h *OrderHandler) RegisterChannelRemit(quoteAccept rfqmsg.Accept) { + log.Debugf("Registering channel remit for SCID: %d", + quoteAccept.ShortChannelId()) + + channelRemit := NewChannelRemit(quoteAccept) + h.channelRemits.Store(channelRemit.Scid, channelRemit) +} + +// FetchChannelRemit fetches a channel remit given a serialised SCID. If a +// channel remit is not found, false is returned. Expired channel remits are +// not returned and are removed from the cache. +func (h *OrderHandler) FetchChannelRemit(scid SerialisedScid) (*ChannelRemit, + bool) { + + remit, ok := h.channelRemits.Load(scid) + if !ok { + return nil, false + } + + // If the remit has expired, return false and clear it from the cache. + expireTime := time.Unix(int64(remit.Expiry), 0).UTC() + currentTime := time.Now().UTC() + + if currentTime.After(expireTime) { + h.channelRemits.Delete(scid) + return nil, false + } + + return remit, true +} + +// cleanupStaleChannelRemits removes expired channel remits from the local +// cache. +func (h *OrderHandler) cleanupStaleChannelRemits() { + // Iterate over the channel remits and remove any that have expired. + staleCounter := 0 + + h.channelRemits.ForEach( + func(scid SerialisedScid, remit *ChannelRemit) error { + expireTime := time.Unix(int64(remit.Expiry), 0).UTC() + currentTime := time.Now().UTC() + + if currentTime.After(expireTime) { + staleCounter++ + h.channelRemits.Delete(scid) + } + + return nil + }, + ) + + if staleCounter > 0 { + log.Tracef("Removed stale channel remits from the order "+ + "handler: (count=%d)", staleCounter) + } +} + +// Stop stops the handler. +func (h *OrderHandler) Stop() error { + h.stopOnce.Do(func() { + log.Info("Stopping subsystem: order handler") + + // Stop the main event loop. + close(h.Quit) + }) + return nil +} + +// HtlcInterceptor is an interface that abstracts the hash time locked contract +// (HTLC) intercept functionality. +type HtlcInterceptor interface { + // InterceptHtlcs intercepts HTLCs, using the handling function provided + // to respond to HTLCs. + InterceptHtlcs(context.Context, lndclient.HtlcInterceptHandler) error +} diff --git a/rfq/stream.go b/rfq/stream.go new file mode 100644 index 000000000..21d6c6bbd --- /dev/null +++ b/rfq/stream.go @@ -0,0 +1,232 @@ +package rfq + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightningnetwork/lnd/lnwire" +) + +// StreamHandlerCfg is a struct that holds the configuration parameters for the +// RFQ peer message stream handler. +type StreamHandlerCfg struct { + // PeerMessenger is the peer messenger. This component provides the RFQ + // manager with the ability to send and receive raw peer messages. + PeerMessenger PeerMessenger + + // IncomingMessages is a channel which is populated with incoming + // (received) RFQ messages. These messages have been extracted from the + // raw peer wire messages by the stream handler service. + IncomingMessages chan<- rfqmsg.IncomingMsg +} + +// StreamHandler is a struct that handles incoming and outgoing peer RFQ stream +// messages. +// +// This component subscribes to incoming raw peer messages (custom messages). It +// processes those messages with the aim of extracting relevant request for +// quotes (RFQs). +type StreamHandler struct { + startOnce sync.Once + stopOnce sync.Once + + // cfg holds the configuration parameters for the RFQ peer message + // stream handler. + cfg StreamHandlerCfg + + // recvRawMessages is a channel that receives incoming raw peer + // messages. + recvRawMessages <-chan lndclient.CustomMessage + + // errRecvRawMessages is a channel that receives errors emanating from + // the peer raw messages subscription. + errRecvRawMessages <-chan error + + // ContextGuard provides a wait group and main quit channel that can be + // used to create guarded contexts. + *fn.ContextGuard +} + +// NewStreamHandler creates and starts a new RFQ stream handler. +// +// TODO(ffranr): Pass in a signer so that we can create a signature over output +// message fields. +func NewStreamHandler(ctx context.Context, + cfg StreamHandlerCfg) (*StreamHandler, error) { + + pPorter := cfg.PeerMessenger + msgChan, peerMsgErrChan, err := pPorter.SubscribeCustomMessages(ctx) + if err != nil { + return nil, fmt.Errorf("failed to subscribe to wire "+ + "messages via peer message porter: %w", err) + } + + return &StreamHandler{ + cfg: cfg, + + recvRawMessages: msgChan, + errRecvRawMessages: peerMsgErrChan, + + ContextGuard: &fn.ContextGuard{ + DefaultTimeout: DefaultTimeout, + Quit: make(chan struct{}), + }, + }, nil +} + +// handleIncomingWireMessage handles an incoming wire message. +func (h *StreamHandler) handleIncomingWireMessage( + wireMsg rfqmsg.WireMessage) error { + + // Parse the wire message as an RFQ message. + msg, err := rfqmsg.NewIncomingMsgFromWire(wireMsg) + if err != nil { + if errors.Is(err, rfqmsg.ErrUnknownMessageType) { + // Silently disregard the message if we don't recognise + // the message type. + log.Tracef("Silently disregarding incoming message of "+ + "unknown type (msg_type=%d)", wireMsg.MsgType) + return nil + } + + return fmt.Errorf("unable to create incoming message from "+ + "wire message: %w", err) + } + + log.Debugf("Stream handling incoming message: %s", msg) + + // Send the incoming message to the RFQ manager. + sendSuccess := fn.SendOrQuit(h.cfg.IncomingMessages, msg, h.Quit) + if !sendSuccess { + return fmt.Errorf("RFQ stream handler shutting down") + } + + return nil +} + +// HandleOutgoingMessage handles an outgoing RFQ message. +func (h *StreamHandler) HandleOutgoingMessage( + outgoingMsg rfqmsg.OutgoingMsg) error { + + log.Debugf("Stream handling outgoing message: %s", outgoingMsg) + + // Convert the outgoing message to a lndclient custom message. + wireMsg, err := outgoingMsg.ToWire() + if err != nil { + return fmt.Errorf("unable to create lndclient custom "+ + "message: %w", err) + } + lndClientCustomMsg := lndclient.CustomMessage{ + Peer: wireMsg.Peer, + MsgType: uint32(wireMsg.MsgType), + Data: wireMsg.Data, + } + + // Send the message to the peer. + ctx, cancel := h.WithCtxQuitNoTimeout() + defer cancel() + + err = h.cfg.PeerMessenger.SendCustomMessage(ctx, lndClientCustomMsg) + if err != nil { + return fmt.Errorf("unable to send message to peer: %w", + err) + } + + return nil +} + +// mainEventLoop executes the main event handling loop. +func (h *StreamHandler) mainEventLoop() { + log.Debug("Starting stream handler event loop") + + for { + select { + case rawMsg, ok := <-h.recvRawMessages: + if !ok { + log.Warnf("Raw peer messages channel closed " + + "unexpectedly") + return + } + + // Convert custom message type to wire message type, + // taking care not to overflow in the down conversion. + if rawMsg.MsgType > uint32(rfqmsg.MaxMessageType) { + log.Warnf("Received message with invalid "+ + "type: msg_type=%d", rawMsg.MsgType) + continue + } + msgType := lnwire.MessageType(rawMsg.MsgType) + + // Convert the raw peer message into a wire message. + // Wire message is a RFQ package type that is used by + // interfaces throughout the package. + wireMsg := rfqmsg.WireMessage{ + Peer: rawMsg.Peer, + MsgType: msgType, + Data: rawMsg.Data, + } + + err := h.handleIncomingWireMessage(wireMsg) + if err != nil { + log.Warnf("Error handling incoming wire "+ + "message: %v", err) + } + + case errSubCustomMessages := <-h.errRecvRawMessages: + // If we receive an error from the peer message + // subscription, we'll terminate the stream handler. + log.Warnf("Error received from stream handler wire "+ + "message channel: %v", errSubCustomMessages) + + case <-h.Quit: + log.Debug("Received quit signal. Stopping stream " + + "handler event loop") + return + } + } +} + +// Start starts the service. +func (h *StreamHandler) Start() error { + var startErr error + h.startOnce.Do(func() { + log.Info("Starting subsystem: peer message stream handler") + + // Start the main event loop in a separate goroutine. + h.Wg.Add(1) + go func() { + defer h.Wg.Done() + h.mainEventLoop() + }() + }) + return startErr +} + +// Stop stops the handler. +func (h *StreamHandler) Stop() error { + h.stopOnce.Do(func() { + log.Info("Stopping subsystem: stream handler") + + // Stop the main event loop. + close(h.Quit) + }) + return nil +} + +// PeerMessenger is an interface that abstracts the peer message transport +// layer. +type PeerMessenger interface { + // SubscribeCustomMessages creates a subscription to raw messages + // received from our peers. + SubscribeCustomMessages( + ctx context.Context) (<-chan lndclient.CustomMessage, + <-chan error, error) + + // SendCustomMessage sends a raw message to a peer. + SendCustomMessage(context.Context, lndclient.CustomMessage) error +} From ff71f2bec4f8121b479d51b2de9bcc329c0df4f1 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 19 Feb 2024 17:41:11 +0000 Subject: [PATCH 6/8] multi: start the RFQ service on startup This commit plugs the new RFQ service into the greater tapd service. --- chain_bridge.go | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ config.go | 3 +++ log.go | 2 ++ server.go | 9 ++++++++ tapcfg/server.go | 20 ++++++++++++++++ 5 files changed, 93 insertions(+) diff --git a/chain_bridge.go b/chain_bridge.go index 244ee04c1..5f95a41c3 100644 --- a/chain_bridge.go +++ b/chain_bridge.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/tapgarden" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/lnrpc/verrpc" @@ -203,3 +204,61 @@ func (l *LndRpcChainBridge) EstimateFee(ctx context.Context, // A compile time assertion to ensure LndRpcChainBridge meets the // tapgarden.ChainBridge interface. var _ tapgarden.ChainBridge = (*LndRpcChainBridge)(nil) + +// LndMsgTransportClient is an LND RPC message transport client. +type LndMsgTransportClient struct { + lnd *lndclient.LndServices +} + +// NewLndMsgTransportClient creates a new message transport RPC client for a +// given LND service. +func NewLndMsgTransportClient( + lnd *lndclient.LndServices) *LndMsgTransportClient { + + return &LndMsgTransportClient{ + lnd: lnd, + } +} + +// SubscribeCustomMessages creates a subscription to custom messages received +// from our peers. +func (l *LndMsgTransportClient) SubscribeCustomMessages( + ctx context.Context) (<-chan lndclient.CustomMessage, + <-chan error, error) { + + return l.lnd.Client.SubscribeCustomMessages(ctx) +} + +// SendCustomMessage sends a custom message to a peer. +func (l *LndMsgTransportClient) SendCustomMessage(ctx context.Context, + msg lndclient.CustomMessage) error { + + return l.lnd.Client.SendCustomMessage(ctx, msg) +} + +// Ensure LndMsgTransportClient implements the rfq.PeerMessenger interface. +var _ rfq.PeerMessenger = (*LndMsgTransportClient)(nil) + +// LndRouterClient is an LND router RPC client. +type LndRouterClient struct { + lnd *lndclient.LndServices +} + +// NewLndRouterClient creates a new LND router client for a given LND service. +func NewLndRouterClient(lnd *lndclient.LndServices) *LndRouterClient { + return &LndRouterClient{ + lnd: lnd, + } +} + +// InterceptHtlcs intercepts all incoming HTLCs and calls the given handler +// function with the HTLC details. The handler function can then decide whether +// to accept or reject the HTLC. +func (l *LndRouterClient) InterceptHtlcs( + ctx context.Context, handler lndclient.HtlcInterceptHandler) error { + + return l.lnd.Router.InterceptHtlcs(ctx, handler) +} + +// Ensure LndRouterClient implements the rfq.HtlcInterceptor interface. +var _ rfq.HtlcInterceptor = (*LndRouterClient)(nil) diff --git a/config.go b/config.go index bb5e233a9..be58e48e6 100644 --- a/config.go +++ b/config.go @@ -10,6 +10,7 @@ import ( "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/monitoring" "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/tapdb" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" @@ -122,6 +123,8 @@ type Config struct { UniverseFederation *universe.FederationEnvoy + RfqManager *rfq.Manager + UniverseStats universe.Telemetry // UniversePublicAccess is flag which, If true, and the Universe server diff --git a/log.go b/log.go index 63be50d29..787ab1963 100644 --- a/log.go +++ b/log.go @@ -6,6 +6,7 @@ import ( "github.com/lightninglabs/taproot-assets/commitment" "github.com/lightninglabs/taproot-assets/monitoring" "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/tapdb" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" @@ -107,6 +108,7 @@ func SetupLoggers(root *build.RotatingLogWriter, interceptor signal.Interceptor) AddSubLogger( root, monitoring.Subsystem, interceptor, monitoring.UseLogger, ) + AddSubLogger(root, rfq.Subsystem, interceptor, rfq.UseLogger) } // AddSubLogger is a helper method to conveniently create and register the diff --git a/server.go b/server.go index 341d3afe3..533348f10 100644 --- a/server.go +++ b/server.go @@ -158,6 +158,11 @@ func (s *Server) initialize(interceptorChain *rpcperms.InterceptorChain) error { "federation: %w", err) } + // Start the request for quote (RFQ) manager. + if err := s.cfg.RfqManager.Start(); err != nil { + return fmt.Errorf("unable to start RFQ manager: %w", err) + } + if s.cfg.UniversePublicAccess { err := s.cfg.UniverseFederation.SetAllowPublicAccess() if err != nil { @@ -586,6 +591,10 @@ func (s *Server) Stop() error { return err } + if err := s.cfg.RfqManager.Stop(); err != nil { + return err + } + if s.macaroonService != nil { err := s.macaroonService.Stop() if err != nil { diff --git a/tapcfg/server.go b/tapcfg/server.go index 25d1acc53..c43ff14d6 100644 --- a/tapcfg/server.go +++ b/tapcfg/server.go @@ -13,6 +13,7 @@ import ( "github.com/lightninglabs/taproot-assets/address" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/rfq" "github.com/lightninglabs/taproot-assets/tapdb" "github.com/lightninglabs/taproot-assets/tapdb/sqlc" "github.com/lightninglabs/taproot-assets/tapfreighter" @@ -96,6 +97,8 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, keyRing := tap.NewLndRpcKeyRing(lndServices) walletAnchor := tap.NewLndRpcWalletAnchor(lndServices) chainBridge := tap.NewLndRpcChainBridge(lndServices) + msgTransportClient := tap.NewLndMsgTransportClient(lndServices) + lndRouterClient := tap.NewLndRouterClient(lndServices) assetStore := tapdb.NewAssetStore(assetDB, defaultClock) @@ -321,6 +324,22 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, multiNotifier := proof.NewMultiArchiveNotifier(assetStore, multiverse) + // TODO(ffranr): Replace the mock price oracle with a real one. + priceOracle := rfq.NewMockPriceOracle(3600) + + // Construct the RFQ manager. + rfqManager, err := rfq.NewManager( + rfq.ManagerCfg{ + PeerMessenger: msgTransportClient, + HtlcInterceptor: lndRouterClient, + PriceOracle: priceOracle, + ErrChan: mainErrChan, + }, + ) + if err != nil { + return nil, err + } + return &tap.Config{ DebugLevel: cfg.DebugLevel, RuntimeID: runtimeID, @@ -394,6 +413,7 @@ func genServerConfig(cfg *Config, cfgLogger btclog.Logger, UniversePublicAccess: cfg.Universe.PublicAccess, UniverseQueriesPerSecond: cfg.Universe.UniverseQueriesPerSecond, UniverseQueriesBurst: cfg.Universe.UniverseQueriesBurst, + RfqManager: rfqManager, LogWriter: cfg.LogWriter, DatabaseConfig: &tap.DatabaseConfig{ RootKeyStore: tapdb.NewRootKeyStore(rksDB), From a73aac8375032cca38f1b3ab7eec6587f311957f Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 19 Feb 2024 17:42:49 +0000 Subject: [PATCH 7/8] rpc: add RPC endpoints for the RFQ system This commit adds the RFQ RPC server. It also adds four RPC endpoints: 1. Upsert buy order. 2. Upsert sell offer. 3. List accepted quotes. 4. Subscribe to RFQ events. --- perms/perms.go | 16 + rpcserver.go | 318 +++++++++ taprpc/gen_protos.sh | 4 +- taprpc/rfqrpc/rfq.pb.go | 1112 ++++++++++++++++++++++++++++++++ taprpc/rfqrpc/rfq.pb.gw.go | 667 +++++++++++++++++++ taprpc/rfqrpc/rfq.pb.json.go | 140 ++++ taprpc/rfqrpc/rfq.proto | 143 ++++ taprpc/rfqrpc/rfq.swagger.json | 439 +++++++++++++ taprpc/rfqrpc/rfq.yaml | 25 + taprpc/rfqrpc/rfq_grpc.pb.go | 257 ++++++++ 10 files changed, 3119 insertions(+), 2 deletions(-) create mode 100644 taprpc/rfqrpc/rfq.pb.go create mode 100644 taprpc/rfqrpc/rfq.pb.gw.go create mode 100644 taprpc/rfqrpc/rfq.pb.json.go create mode 100644 taprpc/rfqrpc/rfq.proto create mode 100644 taprpc/rfqrpc/rfq.swagger.json create mode 100644 taprpc/rfqrpc/rfq.yaml create mode 100644 taprpc/rfqrpc/rfq_grpc.pb.go diff --git a/perms/perms.go b/perms/perms.go index ea718f967..356f2dfd2 100644 --- a/perms/perms.go +++ b/perms/perms.go @@ -212,6 +212,22 @@ var ( Entity: "universe", Action: "read", }}, + "/rfqrpc.Rfq/AddAssetBuyOrder": {{ + Entity: "rfq", + Action: "write", + }}, + "/rfqrpc.Rfq/AddAssetSellOffer": {{ + Entity: "rfq", + Action: "write", + }}, + "/rfqrpc.Rfq/QueryRfqAcceptedQuotes": {{ + Entity: "rfq", + Action: "read", + }}, + "/rfqrpc.Rfq/SubscribeRfqEventNtfns": {{ + Entity: "rfq", + Action: "write", + }}, "/tapdevrpc.TapDev/ImportProof": {{ Entity: "proofs", Action: "write", diff --git a/rpcserver.go b/rpcserver.go index baad718cb..f8ea8e149 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -29,6 +29,8 @@ import ( "github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/mssmt" "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/rfq" + "github.com/lightninglabs/taproot-assets/rfqmsg" "github.com/lightninglabs/taproot-assets/rpcperms" "github.com/lightninglabs/taproot-assets/tapfreighter" "github.com/lightninglabs/taproot-assets/tapgarden" @@ -36,6 +38,7 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" wrpc "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc" unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightninglabs/taproot-assets/tapscript" @@ -45,6 +48,8 @@ import ( "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnwallet/chainfee" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/signal" "golang.org/x/time/rate" "google.golang.org/grpc" @@ -106,6 +111,7 @@ type rpcServer struct { taprpc.UnimplementedTaprootAssetsServer wrpc.UnimplementedAssetWalletServer mintrpc.UnimplementedMintServer + rfqrpc.UnimplementedRfqServer unirpc.UnimplementedUniverseServer tapdevrpc.UnimplementedTapDevServer @@ -178,6 +184,7 @@ func (r *rpcServer) RegisterWithGrpcServer(grpcServer *grpc.Server) error { taprpc.RegisterTaprootAssetsServer(grpcServer, r) wrpc.RegisterAssetWalletServer(grpcServer, r) mintrpc.RegisterMintServer(grpcServer, r) + rfqrpc.RegisterRfqServer(grpcServer, r) unirpc.RegisterUniverseServer(grpcServer, r) tapdevrpc.RegisterGrpcServer(grpcServer, r) return nil @@ -4807,3 +4814,314 @@ func MarshalAssetFedSyncCfg( AllowSyncExport: config.AllowSyncExport, }, nil } + +// unmarshalAssetSpecifier unmarshals an asset specifier from the RPC form. +func unmarshalAssetSpecifier(req *rfqrpc.AssetSpecifier) (*asset.ID, + *btcec.PublicKey, error) { + + // Attempt to decode the asset specifier from the RPC request. In cases + // where both the asset ID and asset group key are provided, we will + // give precedence to the asset ID due to its higher level of + // specificity. + var ( + assetID *asset.ID + + groupKeyBytes []byte + groupKey *btcec.PublicKey + + err error + ) + + switch { + // Parse the asset ID if it's set. + case len(req.GetAssetId()) > 0: + var assetIdBytes [32]byte + copy(assetIdBytes[:], req.GetAssetId()) + id := asset.ID(assetIdBytes) + assetID = &id + + case len(req.GetAssetIdStr()) > 0: + assetIDBytes, err := hex.DecodeString(req.GetAssetIdStr()) + if err != nil { + return nil, nil, fmt.Errorf("error decoding asset "+ + "ID: %w", err) + } + + var id asset.ID + copy(id[:], assetIDBytes) + assetID = &id + + // Parse the group key if it's set. + case len(req.GetGroupKey()) > 0: + groupKeyBytes = req.GetGroupKey() + groupKey, err = btcec.ParsePubKey(groupKeyBytes) + if err != nil { + return nil, nil, fmt.Errorf("error parsing group "+ + "key: %w", err) + } + + case len(req.GetGroupKeyStr()) > 0: + groupKeyBytes, err := hex.DecodeString( + req.GetGroupKeyStr(), + ) + if err != nil { + return nil, nil, fmt.Errorf("error decoding group "+ + "key: %w", err) + } + + groupKey, err = btcec.ParsePubKey(groupKeyBytes) + if err != nil { + return nil, nil, fmt.Errorf("error parsing group "+ + "key: %w", err) + } + + default: + // At this point, we know that neither the asset ID nor the + // group key are specified. Return an error. + return nil, nil, fmt.Errorf("either asset ID or asset group " + + "key must be specified") + } + + return assetID, groupKey, nil +} + +// unmarshalAssetBuyOrder unmarshals an asset buy order from the RPC form. +func unmarshalAssetBuyOrder( + req *rfqrpc.AddAssetBuyOrderRequest) (*rfq.BuyOrder, 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.BuyOrder{ + AssetID: assetId, + AssetGroupKey: assetGroupKey, + MinAssetAmount: req.MinAssetAmount, + MaxBid: lnwire.MilliSatoshi(req.MaxBid), + Expiry: req.Expiry, + Peer: peer, + }, nil +} + +// AddAssetBuyOrder upserts a new buy 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) AddAssetBuyOrder(_ context.Context, + req *rfqrpc.AddAssetBuyOrderRequest) (*rfqrpc.AddAssetBuyOrderResponse, + error) { + + // Unmarshal the buy order from the RPC form. + buyOrder, err := unmarshalAssetBuyOrder(req) + if err != nil { + return nil, fmt.Errorf("error unmarshalling buy order: %w", err) + } + + var peer string + if buyOrder.Peer != nil { + peer = buyOrder.Peer.String() + } + rpcsLog.Debugf("[AddAssetBuyOrder]: upserting buy order "+ + "(dest_peer=%s)", peer) + + // Upsert the buy order into the RFQ manager. + err = r.cfg.RfqManager.UpsertAssetBuyOrder(*buyOrder) + if err != nil { + return nil, fmt.Errorf("error upserting buy order into RFQ "+ + "manager: %w", err) + } + + return &rfqrpc.AddAssetBuyOrderResponse{}, 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. +func (r *rpcServer) AddAssetSellOffer(_ context.Context, + req *rfqrpc.AddAssetSellOfferRequest) (*rfqrpc.AddAssetSellOfferResponse, + error) { + + // Unmarshal the sell offer from the RPC form. + assetID, assetGroupKey, err := unmarshalAssetSpecifier( + req.AssetSpecifier, + ) + if err != nil { + return nil, fmt.Errorf("error unmarshalling asset specifier: "+ + "%w", err) + } + + sellOffer := &rfq.SellOffer{ + AssetID: assetID, + AssetGroupKey: assetGroupKey, + MaxUnits: req.MaxUnits, + } + + rpcsLog.Debugf("[AddAssetSellOffer]: upserting sell offer "+ + "(sell_offer=%v)", sellOffer) + + // Upsert the sell offer into the RFQ manager. + err = r.cfg.RfqManager.UpsertAssetSellOffer(*sellOffer) + if err != nil { + return nil, fmt.Errorf("error upserting sell offer into RFQ "+ + "manager: %w", err) + } + + return &rfqrpc.AddAssetSellOfferResponse{}, nil +} + +// marshalAcceptedQuotes marshals a map of accepted quotes into the RPC form. +func marshalAcceptedQuotes( + acceptedQuotes map[rfq.SerialisedScid]rfqmsg.Accept) []*rfqrpc.AcceptedQuote { + + // Marshal the accepted quotes into the RPC form. + rpcQuotes := make([]*rfqrpc.AcceptedQuote, 0, len(acceptedQuotes)) + for scid, quote := range acceptedQuotes { + rpcQuote := &rfqrpc.AcceptedQuote{ + Peer: quote.Peer.String(), + Id: quote.ID[:], + Scid: uint64(scid), + AssetAmount: quote.AssetAmount, + AskPrice: uint64(quote.AskPrice), + Expiry: quote.Expiry, + } + rpcQuotes = append(rpcQuotes, rpcQuote) + } + + return rpcQuotes +} + +// QueryRfqAcceptedQuotes queries for accepted quotes from the RFQ system. +func (r *rpcServer) QueryRfqAcceptedQuotes(_ context.Context, + _ *rfqrpc.QueryRfqAcceptedQuotesRequest) ( + *rfqrpc.QueryRfqAcceptedQuotesResponse, error) { + + // Query the RFQ manager for accepted quotes. + acceptedQuotes := r.cfg.RfqManager.QueryAcceptedQuotes() + + rpcQuotes := marshalAcceptedQuotes(acceptedQuotes) + + return &rfqrpc.QueryRfqAcceptedQuotesResponse{ + AcceptedQuotes: rpcQuotes, + }, nil +} + +// marshallRfqEvent marshals an RFQ event into the RPC form. +func marshallRfqEvent(eventInterface fn.Event) (*rfqrpc.RfqEvent, error) { + timestamp := eventInterface.Timestamp().UTC().Unix() + + switch event := eventInterface.(type) { + case *rfq.IncomingAcceptQuoteEvent: + acceptedQuote := &rfqrpc.AcceptedQuote{ + Peer: event.Peer.String(), + Id: event.ID[:], + Scid: uint64(event.ShortChannelId()), + AssetAmount: event.AssetAmount, + AskPrice: uint64(event.AskPrice), + Expiry: event.Expiry, + } + + eventRpc := &rfqrpc.RfqEvent_IncomingAcceptQuote{ + IncomingAcceptQuote: &rfqrpc.IncomingAcceptQuoteEvent{ + Timestamp: uint64(timestamp), + AcceptedQuote: acceptedQuote, + }, + } + return &rfqrpc.RfqEvent{ + Event: eventRpc, + }, nil + + case *rfq.AcceptHtlcEvent: + eventRpc := &rfqrpc.RfqEvent_AcceptHtlc{ + AcceptHtlc: &rfqrpc.AcceptHtlcEvent{ + Timestamp: uint64(timestamp), + Scid: uint64(event.ChannelRemit.Scid), + }, + } + return &rfqrpc.RfqEvent{ + Event: eventRpc, + }, nil + + default: + return nil, fmt.Errorf("unknown RFQ event type: %T", + eventInterface) + } +} + +// SubscribeRfqEventNtfns subscribes to RFQ event notifications. +func (r *rpcServer) SubscribeRfqEventNtfns( + _ *rfqrpc.SubscribeRfqEventNtfnsRequest, + ntfnStream rfqrpc.Rfq_SubscribeRfqEventNtfnsServer) error { + + // Create a new event subscriber and pass a copy to the RFQ manager. + // We will then read events from the subscriber. + eventSubscriber := fn.NewEventReceiver[fn.Event](fn.DefaultQueueSize) + defer eventSubscriber.Stop() + + // Register the subscriber with the ChainPorter. + err := r.cfg.RfqManager.RegisterSubscriber( + eventSubscriber, false, 0, + ) + if err != nil { + return fmt.Errorf("failed to register RFQ manager event "+ + "notifications subscription: %w", err) + } + + for { + select { + // Handle new events from the subscriber. + case event := <-eventSubscriber.NewItemCreated.ChanOut(): + // Marshal the event into its RPC form. + rpcEvent, err := marshallRfqEvent(event) + if err != nil { + return fmt.Errorf("failed to marshall RFQ "+ + "event into RPC form: %w", err) + } + + err = ntfnStream.Send(rpcEvent) + if err != nil { + return err + } + + // Handle the case where the RPC stream is closed by the client. + case <-ntfnStream.Context().Done(): + // Remove the subscriber from the RFQ manager. + err := r.cfg.RfqManager.RemoveSubscriber( + eventSubscriber, + ) + if err != nil { + return fmt.Errorf("failed to remove RFQ "+ + "manager event notifications "+ + "subscription: %w", err) + } + + // Don't return an error if a normal context + // cancellation has occurred. + isCanceledContext := errors.Is( + ntfnStream.Context().Err(), context.Canceled, + ) + if isCanceledContext { + return nil + } + + return ntfnStream.Context().Err() + + // Handle the case where the RPC server is shutting down. + case <-r.quit: + return nil + } + } +} diff --git a/taprpc/gen_protos.sh b/taprpc/gen_protos.sh index 02457db2a..af02d11be 100755 --- a/taprpc/gen_protos.sh +++ b/taprpc/gen_protos.sh @@ -6,7 +6,7 @@ set -e function generate() { echo "Generating root gRPC server protos" - PROTOS="taprootassets.proto assetwalletrpc/assetwallet.proto mintrpc/mint.proto universerpc/universe.proto tapdevrpc/tapdev.proto" + PROTOS="taprootassets.proto assetwalletrpc/assetwallet.proto mintrpc/mint.proto rfqrpc/rfq.proto universerpc/universe.proto tapdevrpc/tapdev.proto" # For each of the sub-servers, we then generate their protos, but a restricted # set as they don't yet require REST proxies, or swagger docs. @@ -48,7 +48,7 @@ function generate() { --custom_opt="$opts" \ taprootassets.proto - PACKAGES="assetwalletrpc universerpc mintrpc" + PACKAGES="assetwalletrpc universerpc mintrpc rfqrpc" for package in $PACKAGES; do opts="package_name=$package,manual_import=$manual_import,js_stubs=1" diff --git a/taprpc/rfqrpc/rfq.pb.go b/taprpc/rfqrpc/rfq.pb.go new file mode 100644 index 000000000..5f2242123 --- /dev/null +++ b/taprpc/rfqrpc/rfq.pb.go @@ -0,0 +1,1112 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.31.0 +// protoc v3.21.12 +// source: rfqrpc/rfq.proto + +package rfqrpc + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AssetSpecifier struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Id: + // + // *AssetSpecifier_AssetId + // *AssetSpecifier_AssetIdStr + // *AssetSpecifier_GroupKey + // *AssetSpecifier_GroupKeyStr + Id isAssetSpecifier_Id `protobuf_oneof:"id"` +} + +func (x *AssetSpecifier) Reset() { + *x = AssetSpecifier{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AssetSpecifier) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AssetSpecifier) ProtoMessage() {} + +func (x *AssetSpecifier) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[0] + 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 AssetSpecifier.ProtoReflect.Descriptor instead. +func (*AssetSpecifier) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{0} +} + +func (m *AssetSpecifier) GetId() isAssetSpecifier_Id { + if m != nil { + return m.Id + } + return nil +} + +func (x *AssetSpecifier) GetAssetId() []byte { + if x, ok := x.GetId().(*AssetSpecifier_AssetId); ok { + return x.AssetId + } + return nil +} + +func (x *AssetSpecifier) GetAssetIdStr() string { + if x, ok := x.GetId().(*AssetSpecifier_AssetIdStr); ok { + return x.AssetIdStr + } + return "" +} + +func (x *AssetSpecifier) GetGroupKey() []byte { + if x, ok := x.GetId().(*AssetSpecifier_GroupKey); ok { + return x.GroupKey + } + return nil +} + +func (x *AssetSpecifier) GetGroupKeyStr() string { + if x, ok := x.GetId().(*AssetSpecifier_GroupKeyStr); ok { + return x.GroupKeyStr + } + return "" +} + +type isAssetSpecifier_Id interface { + isAssetSpecifier_Id() +} + +type AssetSpecifier_AssetId struct { + // The 32-byte asset ID specified as raw bytes (gRPC only). + AssetId []byte `protobuf:"bytes,1,opt,name=asset_id,json=assetId,proto3,oneof"` +} + +type AssetSpecifier_AssetIdStr struct { + // The 32-byte asset ID encoded as a hex string (use this for REST). + AssetIdStr string `protobuf:"bytes,2,opt,name=asset_id_str,json=assetIdStr,proto3,oneof"` +} + +type AssetSpecifier_GroupKey struct { + // The 32-byte asset group key specified as raw bytes (gRPC only). + GroupKey []byte `protobuf:"bytes,3,opt,name=group_key,json=groupKey,proto3,oneof"` +} + +type AssetSpecifier_GroupKeyStr struct { + // The 32-byte asset group key encoded as hex string (use this for + // REST). + GroupKeyStr string `protobuf:"bytes,4,opt,name=group_key_str,json=groupKeyStr,proto3,oneof"` +} + +func (*AssetSpecifier_AssetId) isAssetSpecifier_Id() {} + +func (*AssetSpecifier_AssetIdStr) isAssetSpecifier_Id() {} + +func (*AssetSpecifier_GroupKey) isAssetSpecifier_Id() {} + +func (*AssetSpecifier_GroupKeyStr) isAssetSpecifier_Id() {} + +type AddAssetBuyOrderRequest 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 minimum amount of the asset to buy. + MinAssetAmount uint64 `protobuf:"varint,2,opt,name=min_asset_amount,json=minAssetAmount,proto3" json:"min_asset_amount,omitempty"` + // The maximum amount BTC to spend (units: millisats). + MaxBid uint64 `protobuf:"varint,3,opt,name=max_bid,json=maxBid,proto3" json:"max_bid,omitempty"` + // The unix timestamp 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 *AddAssetBuyOrderRequest) Reset() { + *x = AddAssetBuyOrderRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddAssetBuyOrderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddAssetBuyOrderRequest) ProtoMessage() {} + +func (x *AddAssetBuyOrderRequest) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[1] + 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 AddAssetBuyOrderRequest.ProtoReflect.Descriptor instead. +func (*AddAssetBuyOrderRequest) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{1} +} + +func (x *AddAssetBuyOrderRequest) GetAssetSpecifier() *AssetSpecifier { + if x != nil { + return x.AssetSpecifier + } + return nil +} + +func (x *AddAssetBuyOrderRequest) GetMinAssetAmount() uint64 { + if x != nil { + return x.MinAssetAmount + } + return 0 +} + +func (x *AddAssetBuyOrderRequest) GetMaxBid() uint64 { + if x != nil { + return x.MaxBid + } + return 0 +} + +func (x *AddAssetBuyOrderRequest) GetExpiry() uint64 { + if x != nil { + return x.Expiry + } + return 0 +} + +func (x *AddAssetBuyOrderRequest) GetPeerPubKey() []byte { + if x != nil { + return x.PeerPubKey + } + return nil +} + +type AddAssetBuyOrderResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *AddAssetBuyOrderResponse) Reset() { + *x = AddAssetBuyOrderResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddAssetBuyOrderResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddAssetBuyOrderResponse) ProtoMessage() {} + +func (x *AddAssetBuyOrderResponse) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[2] + 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 AddAssetBuyOrderResponse.ProtoReflect.Descriptor instead. +func (*AddAssetBuyOrderResponse) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{2} +} + +type AddAssetSellOfferRequest 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"` + // max_units is the maximum amount of the asset to sell. + MaxUnits uint64 `protobuf:"varint,2,opt,name=max_units,json=maxUnits,proto3" json:"max_units,omitempty"` +} + +func (x *AddAssetSellOfferRequest) Reset() { + *x = AddAssetSellOfferRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddAssetSellOfferRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddAssetSellOfferRequest) ProtoMessage() {} + +func (x *AddAssetSellOfferRequest) 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 AddAssetSellOfferRequest.ProtoReflect.Descriptor instead. +func (*AddAssetSellOfferRequest) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{3} +} + +func (x *AddAssetSellOfferRequest) GetAssetSpecifier() *AssetSpecifier { + if x != nil { + return x.AssetSpecifier + } + return nil +} + +func (x *AddAssetSellOfferRequest) GetMaxUnits() uint64 { + if x != nil { + return x.MaxUnits + } + return 0 +} + +type AddAssetSellOfferResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *AddAssetSellOfferResponse) Reset() { + *x = AddAssetSellOfferResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AddAssetSellOfferResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddAssetSellOfferResponse) ProtoMessage() {} + +func (x *AddAssetSellOfferResponse) 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 AddAssetSellOfferResponse.ProtoReflect.Descriptor instead. +func (*AddAssetSellOfferResponse) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{4} +} + +type QueryRfqAcceptedQuotesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *QueryRfqAcceptedQuotesRequest) Reset() { + *x = QueryRfqAcceptedQuotesRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *QueryRfqAcceptedQuotesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryRfqAcceptedQuotesRequest) ProtoMessage() {} + +func (x *QueryRfqAcceptedQuotesRequest) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[5] + 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 QueryRfqAcceptedQuotesRequest.ProtoReflect.Descriptor instead. +func (*QueryRfqAcceptedQuotesRequest) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{5} +} + +type AcceptedQuote 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"` + // ask_price is the price in millisats for the entire asset amount. + AskPrice uint64 `protobuf:"varint,5,opt,name=ask_price,json=askPrice,proto3" json:"ask_price,omitempty"` + // The unix timestamp after which the quote is no longer valid. + Expiry uint64 `protobuf:"varint,6,opt,name=expiry,proto3" json:"expiry,omitempty"` +} + +func (x *AcceptedQuote) Reset() { + *x = AcceptedQuote{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AcceptedQuote) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AcceptedQuote) ProtoMessage() {} + +func (x *AcceptedQuote) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[6] + 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 AcceptedQuote.ProtoReflect.Descriptor instead. +func (*AcceptedQuote) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{6} +} + +func (x *AcceptedQuote) GetPeer() string { + if x != nil { + return x.Peer + } + return "" +} + +func (x *AcceptedQuote) GetId() []byte { + if x != nil { + return x.Id + } + return nil +} + +func (x *AcceptedQuote) GetScid() uint64 { + if x != nil { + return x.Scid + } + return 0 +} + +func (x *AcceptedQuote) GetAssetAmount() uint64 { + if x != nil { + return x.AssetAmount + } + return 0 +} + +func (x *AcceptedQuote) GetAskPrice() uint64 { + if x != nil { + return x.AskPrice + } + return 0 +} + +func (x *AcceptedQuote) GetExpiry() uint64 { + if x != nil { + return x.Expiry + } + return 0 +} + +type QueryRfqAcceptedQuotesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AcceptedQuotes []*AcceptedQuote `protobuf:"bytes,1,rep,name=accepted_quotes,json=acceptedQuotes,proto3" json:"accepted_quotes,omitempty"` +} + +func (x *QueryRfqAcceptedQuotesResponse) Reset() { + *x = QueryRfqAcceptedQuotesResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *QueryRfqAcceptedQuotesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QueryRfqAcceptedQuotesResponse) ProtoMessage() {} + +func (x *QueryRfqAcceptedQuotesResponse) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[7] + 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 QueryRfqAcceptedQuotesResponse.ProtoReflect.Descriptor instead. +func (*QueryRfqAcceptedQuotesResponse) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{7} +} + +func (x *QueryRfqAcceptedQuotesResponse) GetAcceptedQuotes() []*AcceptedQuote { + if x != nil { + return x.AcceptedQuotes + } + return nil +} + +type SubscribeRfqEventNtfnsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *SubscribeRfqEventNtfnsRequest) Reset() { + *x = SubscribeRfqEventNtfnsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SubscribeRfqEventNtfnsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeRfqEventNtfnsRequest) ProtoMessage() {} + +func (x *SubscribeRfqEventNtfnsRequest) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[8] + 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 SubscribeRfqEventNtfnsRequest.ProtoReflect.Descriptor instead. +func (*SubscribeRfqEventNtfnsRequest) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{8} +} + +type IncomingAcceptQuoteEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Unix timestamp. + Timestamp uint64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // The accepted quote. + AcceptedQuote *AcceptedQuote `protobuf:"bytes,2,opt,name=accepted_quote,json=acceptedQuote,proto3" json:"accepted_quote,omitempty"` +} + +func (x *IncomingAcceptQuoteEvent) Reset() { + *x = IncomingAcceptQuoteEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IncomingAcceptQuoteEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IncomingAcceptQuoteEvent) ProtoMessage() {} + +func (x *IncomingAcceptQuoteEvent) 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 IncomingAcceptQuoteEvent.ProtoReflect.Descriptor instead. +func (*IncomingAcceptQuoteEvent) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{9} +} + +func (x *IncomingAcceptQuoteEvent) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *IncomingAcceptQuoteEvent) GetAcceptedQuote() *AcceptedQuote { + if x != nil { + return x.AcceptedQuote + } + return nil +} + +type AcceptHtlcEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Unix timestamp. + Timestamp uint64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + // scid is the short channel ID of the channel over which the payment for + // the quote is made. + Scid uint64 `protobuf:"varint,2,opt,name=scid,proto3" json:"scid,omitempty"` +} + +func (x *AcceptHtlcEvent) Reset() { + *x = AcceptHtlcEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AcceptHtlcEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AcceptHtlcEvent) ProtoMessage() {} + +func (x *AcceptHtlcEvent) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[10] + 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 AcceptHtlcEvent.ProtoReflect.Descriptor instead. +func (*AcceptHtlcEvent) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{10} +} + +func (x *AcceptHtlcEvent) GetTimestamp() uint64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +func (x *AcceptHtlcEvent) GetScid() uint64 { + if x != nil { + return x.Scid + } + return 0 +} + +type RfqEvent struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to Event: + // + // *RfqEvent_IncomingAcceptQuote + // *RfqEvent_AcceptHtlc + Event isRfqEvent_Event `protobuf_oneof:"event"` +} + +func (x *RfqEvent) Reset() { + *x = RfqEvent{} + if protoimpl.UnsafeEnabled { + mi := &file_rfqrpc_rfq_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RfqEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RfqEvent) ProtoMessage() {} + +func (x *RfqEvent) ProtoReflect() protoreflect.Message { + mi := &file_rfqrpc_rfq_proto_msgTypes[11] + 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 RfqEvent.ProtoReflect.Descriptor instead. +func (*RfqEvent) Descriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{11} +} + +func (m *RfqEvent) GetEvent() isRfqEvent_Event { + if m != nil { + return m.Event + } + return nil +} + +func (x *RfqEvent) GetIncomingAcceptQuote() *IncomingAcceptQuoteEvent { + if x, ok := x.GetEvent().(*RfqEvent_IncomingAcceptQuote); ok { + return x.IncomingAcceptQuote + } + return nil +} + +func (x *RfqEvent) GetAcceptHtlc() *AcceptHtlcEvent { + if x, ok := x.GetEvent().(*RfqEvent_AcceptHtlc); ok { + return x.AcceptHtlc + } + return nil +} + +type isRfqEvent_Event interface { + isRfqEvent_Event() +} + +type RfqEvent_IncomingAcceptQuote struct { + // incoming_accept_quote is an event that is sent when an incoming + // accept quote message is received. + IncomingAcceptQuote *IncomingAcceptQuoteEvent `protobuf:"bytes,1,opt,name=incoming_accept_quote,json=incomingAcceptQuote,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"` +} + +func (*RfqEvent_IncomingAcceptQuote) isRfqEvent_Event() {} + +func (*RfqEvent_AcceptHtlc) isRfqEvent_Event() {} + +var File_rfqrpc_rfq_proto protoreflect.FileDescriptor + +var file_rfqrpc_rfq_proto_rawDesc = []byte{ + 0x0a, 0x10, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2f, 0x72, 0x66, 0x71, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x06, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x22, 0x9c, 0x01, 0x0a, 0x0e, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x1b, 0x0a, + 0x08, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x48, + 0x00, 0x52, 0x07, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x73, + 0x73, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x5f, 0x73, 0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x49, 0x64, 0x53, 0x74, 0x72, 0x12, 0x1d, + 0x0a, 0x09, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x48, 0x00, 0x52, 0x08, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x24, 0x0a, + 0x0d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x4b, 0x65, 0x79, + 0x53, 0x74, 0x72, 0x42, 0x04, 0x0a, 0x02, 0x69, 0x64, 0x22, 0xd7, 0x01, 0x0a, 0x17, 0x41, 0x64, + 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 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, 0x69, 0x6e, 0x5f, 0x61, 0x73, + 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, + 0x52, 0x0e, 0x6d, 0x69, 0x6e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x17, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x62, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x04, 0x52, 0x06, 0x6d, 0x61, 0x78, 0x42, 0x69, 0x64, 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, 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, 0x1f, 0x0a, 0x1d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, + 0x66, 0x71, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x9f, 0x01, 0x0a, 0x0d, 0x41, 0x63, 0x63, 0x65, + 0x70, 0x74, 0x65, 0x64, 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, 0x60, 0x0a, 0x1e, 0x51, 0x75, 0x65, + 0x72, 0x79, 0x52, 0x66, 0x71, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0f, 0x61, + 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x0e, 0x61, 0x63, 0x63, + 0x65, 0x70, 0x74, 0x65, 0x64, 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, 0x76, 0x0a, 0x18, + 0x49, 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 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, 0x3c, 0x0a, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, + 0x65, 0x64, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, + 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 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, 0xa7, 0x01, 0x0a, 0x08, 0x52, 0x66, + 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x56, 0x0a, 0x15, 0x69, 0x6e, 0x63, 0x6f, 0x6d, 0x69, + 0x6e, 0x67, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x49, + 0x6e, 0x63, 0x6f, 0x6d, 0x69, 0x6e, 0x67, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x51, 0x75, 0x6f, + 0x74, 0x65, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x13, 0x69, 0x6e, 0x63, 0x6f, 0x6d, + 0x69, 0x6e, 0x67, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 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, 0xf4, 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, 0x67, 0x0a, 0x16, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x66, 0x71, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, + 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x25, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x66, 0x71, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, + 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, + 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x52, 0x66, 0x71, 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 ( + file_rfqrpc_rfq_proto_rawDescOnce sync.Once + file_rfqrpc_rfq_proto_rawDescData = file_rfqrpc_rfq_proto_rawDesc +) + +func file_rfqrpc_rfq_proto_rawDescGZIP() []byte { + file_rfqrpc_rfq_proto_rawDescOnce.Do(func() { + file_rfqrpc_rfq_proto_rawDescData = protoimpl.X.CompressGZIP(file_rfqrpc_rfq_proto_rawDescData) + }) + return file_rfqrpc_rfq_proto_rawDescData +} + +var file_rfqrpc_rfq_proto_msgTypes = make([]protoimpl.MessageInfo, 12) +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 + (*QueryRfqAcceptedQuotesRequest)(nil), // 5: rfqrpc.QueryRfqAcceptedQuotesRequest + (*AcceptedQuote)(nil), // 6: rfqrpc.AcceptedQuote + (*QueryRfqAcceptedQuotesResponse)(nil), // 7: rfqrpc.QueryRfqAcceptedQuotesResponse + (*SubscribeRfqEventNtfnsRequest)(nil), // 8: rfqrpc.SubscribeRfqEventNtfnsRequest + (*IncomingAcceptQuoteEvent)(nil), // 9: rfqrpc.IncomingAcceptQuoteEvent + (*AcceptHtlcEvent)(nil), // 10: rfqrpc.AcceptHtlcEvent + (*RfqEvent)(nil), // 11: 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.QueryRfqAcceptedQuotesResponse.accepted_quotes:type_name -> rfqrpc.AcceptedQuote + 6, // 3: rfqrpc.IncomingAcceptQuoteEvent.accepted_quote:type_name -> rfqrpc.AcceptedQuote + 9, // 4: rfqrpc.RfqEvent.incoming_accept_quote:type_name -> rfqrpc.IncomingAcceptQuoteEvent + 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.QueryRfqAcceptedQuotes:input_type -> rfqrpc.QueryRfqAcceptedQuotesRequest + 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.QueryRfqAcceptedQuotes:output_type -> rfqrpc.QueryRfqAcceptedQuotesResponse + 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 +} + +func init() { file_rfqrpc_rfq_proto_init() } +func file_rfqrpc_rfq_proto_init() { + if File_rfqrpc_rfq_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_rfqrpc_rfq_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AssetSpecifier); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddAssetBuyOrderRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddAssetBuyOrderResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddAssetSellOfferRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AddAssetSellOfferResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*QueryRfqAcceptedQuotesRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AcceptedQuote); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*QueryRfqAcceptedQuotesResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[8].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[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IncomingAcceptQuoteEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rfqrpc_rfq_proto_msgTypes[10].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[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RfqEvent); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_rfqrpc_rfq_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*AssetSpecifier_AssetId)(nil), + (*AssetSpecifier_AssetIdStr)(nil), + (*AssetSpecifier_GroupKey)(nil), + (*AssetSpecifier_GroupKeyStr)(nil), + } + file_rfqrpc_rfq_proto_msgTypes[11].OneofWrappers = []interface{}{ + (*RfqEvent_IncomingAcceptQuote)(nil), + (*RfqEvent_AcceptHtlc)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_rfqrpc_rfq_proto_rawDesc, + NumEnums: 0, + NumMessages: 12, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_rfqrpc_rfq_proto_goTypes, + DependencyIndexes: file_rfqrpc_rfq_proto_depIdxs, + MessageInfos: file_rfqrpc_rfq_proto_msgTypes, + }.Build() + File_rfqrpc_rfq_proto = out.File + file_rfqrpc_rfq_proto_rawDesc = nil + file_rfqrpc_rfq_proto_goTypes = nil + file_rfqrpc_rfq_proto_depIdxs = nil +} diff --git a/taprpc/rfqrpc/rfq.pb.gw.go b/taprpc/rfqrpc/rfq.pb.gw.go new file mode 100644 index 000000000..79801a041 --- /dev/null +++ b/taprpc/rfqrpc/rfq.pb.gw.go @@ -0,0 +1,667 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: rfqrpc/rfq.proto + +/* +Package rfqrpc is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package rfqrpc + +import ( + "context" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var _ codes.Code +var _ io.Reader +var _ status.Status +var _ = runtime.String +var _ = utilities.NewDoubleArray +var _ = metadata.Join + +func request_Rfq_AddAssetBuyOrder_0(ctx context.Context, marshaler runtime.Marshaler, client RfqClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetBuyOrderRequest + 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.AddAssetBuyOrder(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Rfq_AddAssetBuyOrder_0(ctx context.Context, marshaler runtime.Marshaler, server RfqServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetBuyOrderRequest + 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.AddAssetBuyOrder(ctx, &protoReq) + return msg, metadata, err + +} + +func request_Rfq_AddAssetBuyOrder_1(ctx context.Context, marshaler runtime.Marshaler, client RfqClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetBuyOrderRequest + 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.AddAssetBuyOrder(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Rfq_AddAssetBuyOrder_1(ctx context.Context, marshaler runtime.Marshaler, server RfqServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetBuyOrderRequest + 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.AddAssetBuyOrder(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 + + 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.AddAssetSellOffer(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Rfq_AddAssetSellOffer_0(ctx context.Context, marshaler runtime.Marshaler, server RfqServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetSellOfferRequest + 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.AddAssetSellOffer(ctx, &protoReq) + return msg, metadata, err + +} + +func request_Rfq_AddAssetSellOffer_1(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 + + 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.AddAssetSellOffer(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Rfq_AddAssetSellOffer_1(ctx context.Context, marshaler runtime.Marshaler, server RfqServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq AddAssetSellOfferRequest + 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.AddAssetSellOffer(ctx, &protoReq) + return msg, metadata, err + +} + +func request_Rfq_QueryRfqAcceptedQuotes_0(ctx context.Context, marshaler runtime.Marshaler, client RfqClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryRfqAcceptedQuotesRequest + var metadata runtime.ServerMetadata + + msg, err := client.QueryRfqAcceptedQuotes(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Rfq_QueryRfqAcceptedQuotes_0(ctx context.Context, marshaler runtime.Marshaler, server RfqServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryRfqAcceptedQuotesRequest + var metadata runtime.ServerMetadata + + msg, err := server.QueryRfqAcceptedQuotes(ctx, &protoReq) + return msg, metadata, err + +} + +func request_Rfq_SubscribeRfqEventNtfns_0(ctx context.Context, marshaler runtime.Marshaler, client RfqClient, req *http.Request, pathParams map[string]string) (Rfq_SubscribeRfqEventNtfnsClient, runtime.ServerMetadata, error) { + var protoReq SubscribeRfqEventNtfnsRequest + 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) + } + + stream, err := client.SubscribeRfqEventNtfns(ctx, &protoReq) + if err != nil { + return nil, metadata, err + } + header, err := stream.Header() + if err != nil { + return nil, metadata, err + } + metadata.HeaderMD = header + return stream, metadata, nil + +} + +// RegisterRfqHandlerServer registers the http handlers for service Rfq to "mux". +// UnaryRPC :call RfqServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterRfqHandlerFromEndpoint instead. +func RegisterRfqHandlerServer(ctx context.Context, mux *runtime.ServeMux, server RfqServer) error { + + mux.Handle("POST", pattern_Rfq_AddAssetBuyOrder_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) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetBuyOrder", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/buyorder/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_AddAssetBuyOrder_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetBuyOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_Rfq_AddAssetBuyOrder_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) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetBuyOrder", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/buyorder/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_AddAssetBuyOrder_1(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetBuyOrder_1(ctx, 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() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetSellOffer", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/selloffer/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_AddAssetSellOffer_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetSellOffer_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_Rfq_AddAssetSellOffer_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) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetSellOffer", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/selloffer/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_AddAssetSellOffer_1(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetSellOffer_1(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_Rfq_QueryRfqAcceptedQuotes_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) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/rfqrpc.Rfq/QueryRfqAcceptedQuotes", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/quotes/accepted")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Rfq_QueryRfqAcceptedQuotes_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_QueryRfqAcceptedQuotes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_Rfq_SubscribeRfqEventNtfns_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + err := status.Error(codes.Unimplemented, "streaming calls are not yet supported in the in-process transport") + _, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + }) + + return nil +} + +// RegisterRfqHandlerFromEndpoint is same as RegisterRfqHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterRfqHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.Dial(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + + return RegisterRfqHandler(ctx, mux, conn) +} + +// RegisterRfqHandler registers the http handlers for service Rfq to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterRfqHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterRfqHandlerClient(ctx, mux, NewRfqClient(conn)) +} + +// RegisterRfqHandlerClient registers the http handlers for service Rfq +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "RfqClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "RfqClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "RfqClient" to call the correct interceptors. +func RegisterRfqHandlerClient(ctx context.Context, mux *runtime.ServeMux, client RfqClient) error { + + mux.Handle("POST", pattern_Rfq_AddAssetBuyOrder_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) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetBuyOrder", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/buyorder/asset-id/{asset_specifier.asset_id_str}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Rfq_AddAssetBuyOrder_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetBuyOrder_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_Rfq_AddAssetBuyOrder_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) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetBuyOrder", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/buyorder/group-key/{asset_specifier.group_key_str}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Rfq_AddAssetBuyOrder_1(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetBuyOrder_1(ctx, 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() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetSellOffer", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/selloffer/asset-id/{asset_specifier.asset_id_str}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Rfq_AddAssetSellOffer_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetSellOffer_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_Rfq_AddAssetSellOffer_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) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/rfqrpc.Rfq/AddAssetSellOffer", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/selloffer/group-key/{asset_specifier.group_key_str}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Rfq_AddAssetSellOffer_1(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_AddAssetSellOffer_1(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("GET", pattern_Rfq_QueryRfqAcceptedQuotes_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) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/rfqrpc.Rfq/QueryRfqAcceptedQuotes", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/quotes/accepted")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Rfq_QueryRfqAcceptedQuotes_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_QueryRfqAcceptedQuotes_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + + mux.Handle("POST", pattern_Rfq_SubscribeRfqEventNtfns_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) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/rfqrpc.Rfq/SubscribeRfqEventNtfns", runtime.WithHTTPPathPattern("/v1/taproot-assets/rfq/ntfs")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Rfq_SubscribeRfqEventNtfns_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Rfq_SubscribeRfqEventNtfns_0(ctx, mux, outboundMarshaler, w, req, func() (proto.Message, error) { return resp.Recv() }, mux.GetForwardResponseOptions()...) + + }) + + return nil +} + +var ( + pattern_Rfq_AddAssetBuyOrder_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", "buyorder", "asset-id", "asset_specifier.asset_id_str"}, "")) + + 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_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"}, "")) + + pattern_Rfq_QueryRfqAcceptedQuotes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"v1", "taproot-assets", "rfq", "quotes", "accepted"}, "")) + + pattern_Rfq_SubscribeRfqEventNtfns_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v1", "taproot-assets", "rfq", "ntfs"}, "")) +) + +var ( + forward_Rfq_AddAssetBuyOrder_0 = runtime.ForwardResponseMessage + + forward_Rfq_AddAssetBuyOrder_1 = runtime.ForwardResponseMessage + + forward_Rfq_AddAssetSellOffer_0 = runtime.ForwardResponseMessage + + forward_Rfq_AddAssetSellOffer_1 = runtime.ForwardResponseMessage + + forward_Rfq_QueryRfqAcceptedQuotes_0 = runtime.ForwardResponseMessage + + forward_Rfq_SubscribeRfqEventNtfns_0 = runtime.ForwardResponseStream +) diff --git a/taprpc/rfqrpc/rfq.pb.json.go b/taprpc/rfqrpc/rfq.pb.json.go new file mode 100644 index 000000000..94a38f886 --- /dev/null +++ b/taprpc/rfqrpc/rfq.pb.json.go @@ -0,0 +1,140 @@ +// Code generated by falafel 0.9.1. DO NOT EDIT. +// source: rfq.proto + +package rfqrpc + +import ( + "context" + + gateway "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "google.golang.org/grpc" + "google.golang.org/protobuf/encoding/protojson" +) + +func RegisterRfqJSONCallbacks(registry map[string]func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error))) { + + marshaler := &gateway.JSONPb{ + MarshalOptions: protojson.MarshalOptions{ + UseProtoNames: true, + EmitUnpopulated: true, + }, + } + + registry["rfqrpc.Rfq.AddAssetBuyOrder"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &AddAssetBuyOrderRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewRfqClient(conn) + resp, err := client.AddAssetBuyOrder(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)) { + + req := &AddAssetSellOfferRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewRfqClient(conn) + resp, err := client.AddAssetSellOffer(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.QueryRfqAcceptedQuotes"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &QueryRfqAcceptedQuotesRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewRfqClient(conn) + resp, err := client.QueryRfqAcceptedQuotes(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.SubscribeRfqEventNtfns"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &SubscribeRfqEventNtfnsRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewRfqClient(conn) + stream, err := client.SubscribeRfqEventNtfns(ctx, req) + if err != nil { + callback("", err) + return + } + + go func() { + for { + select { + case <-stream.Context().Done(): + callback("", stream.Context().Err()) + return + default: + } + + resp, err := stream.Recv() + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + }() + } +} diff --git a/taprpc/rfqrpc/rfq.proto b/taprpc/rfqrpc/rfq.proto new file mode 100644 index 000000000..ac1956eba --- /dev/null +++ b/taprpc/rfqrpc/rfq.proto @@ -0,0 +1,143 @@ +syntax = "proto3"; + +package rfqrpc; + +option go_package = "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"; + +service Rfq { + /* tapcli: `rfq buyorder` + 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. + */ + rpc AddAssetBuyOrder (AddAssetBuyOrderRequest) + returns (AddAssetBuyOrderResponse); + + /* 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. + */ + rpc AddAssetSellOffer (AddAssetSellOfferRequest) + returns (AddAssetSellOfferResponse); + + /* tapcli: `rfq acceptedquotes` + QueryRfqAcceptedQuotes is used to upsert a sell order for a specific + asset. + */ + rpc QueryRfqAcceptedQuotes (QueryRfqAcceptedQuotesRequest) + returns (QueryRfqAcceptedQuotesResponse); + + /* + SubscribeRfqEventNtfns is used to subscribe to RFQ events. + */ + rpc SubscribeRfqEventNtfns (SubscribeRfqEventNtfnsRequest) + returns (stream RfqEvent); +} + +message AssetSpecifier { + oneof id { + // The 32-byte asset ID specified as raw bytes (gRPC only). + bytes asset_id = 1; + + // The 32-byte asset ID encoded as a hex string (use this for REST). + string asset_id_str = 2; + + // The 32-byte asset group key specified as raw bytes (gRPC only). + bytes group_key = 3; + + // The 32-byte asset group key encoded as hex string (use this for + // REST). + string group_key_str = 4; + } +} + +message AddAssetBuyOrderRequest { + // asset_specifier is the subject asset. + AssetSpecifier asset_specifier = 1; + + // The minimum amount of the asset to buy. + uint64 min_asset_amount = 2; + + // The maximum amount BTC to spend (units: millisats). + uint64 max_bid = 3; + + // The unix timestamp 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 AddAssetBuyOrderResponse { +} + +message AddAssetSellOfferRequest { + // asset_specifier is the subject asset. + AssetSpecifier asset_specifier = 1; + + // max_units is the maximum amount of the asset to sell. + uint64 max_units = 2; +} + +message AddAssetSellOfferResponse { +} + +message QueryRfqAcceptedQuotesRequest { +} + +message AcceptedQuote { + // 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; + + // ask_price is the price in millisats for the entire asset amount. + uint64 ask_price = 5; + + // The unix timestamp after which the quote is no longer valid. + uint64 expiry = 6; +} + +message QueryRfqAcceptedQuotesResponse { + repeated AcceptedQuote accepted_quotes = 1; +} + +message SubscribeRfqEventNtfnsRequest { +} + +message IncomingAcceptQuoteEvent { + // Unix timestamp. + uint64 timestamp = 1; + + // The accepted quote. + AcceptedQuote accepted_quote = 2; +} + +message AcceptHtlcEvent { + // Unix timestamp. + uint64 timestamp = 1; + + // scid is the short channel ID of the channel over which the payment for + // the quote is made. + uint64 scid = 2; +} + +message RfqEvent { + oneof event { + // incoming_accept_quote is an event that is sent when an incoming + // accept quote message is received. + IncomingAcceptQuoteEvent incoming_accept_quote = 1; + + // accept_htlc is an event that is sent when a HTLC is accepted by the + // RFQ service. + AcceptHtlcEvent accept_htlc = 2; + } +} diff --git a/taprpc/rfqrpc/rfq.swagger.json b/taprpc/rfqrpc/rfq.swagger.json new file mode 100644 index 000000000..c75063dbc --- /dev/null +++ b/taprpc/rfqrpc/rfq.swagger.json @@ -0,0 +1,439 @@ +{ + "swagger": "2.0", + "info": { + "title": "rfqrpc/rfq.proto", + "version": "version not set" + }, + "tags": [ + { + "name": "Rfq" + } + ], + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "paths": { + "/v1/taproot-assets/rfq/buyorder/asset-id/{asset_specifier.asset_id_str}": { + "post": { + "summary": "tapcli: `rfq buyorder`\nAddAssetBuyOrder is used to add a buy order for a specific asset. If a buy\norder already exists for the asset, it will be updated.", + "operationId": "Rfq_AddAssetBuyOrder", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/rfqrpcAddAssetBuyOrderResponse" + } + }, + "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": { + "$ref": "#/definitions/rfqrpcAddAssetBuyOrderRequest" + } + } + ], + "tags": [ + "Rfq" + ] + } + }, + "/v1/taproot-assets/rfq/buyorder/group-key/{asset_specifier.group_key_str}": { + "post": { + "summary": "tapcli: `rfq buyorder`\nAddAssetBuyOrder is used to add a buy order for a specific asset. If a buy\norder already exists for the asset, it will be updated.", + "operationId": "Rfq_AddAssetBuyOrder2", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/rfqrpcAddAssetBuyOrderResponse" + } + }, + "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": { + "$ref": "#/definitions/rfqrpcAddAssetBuyOrderRequest" + } + } + ], + "tags": [ + "Rfq" + ] + } + }, + "/v1/taproot-assets/rfq/ntfs": { + "post": { + "summary": "SubscribeRfqEventNtfns is used to subscribe to RFQ events.", + "operationId": "Rfq_SubscribeRfqEventNtfns", + "responses": { + "200": { + "description": "A successful response.(streaming responses)", + "schema": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/rfqrpcRfqEvent" + }, + "error": { + "$ref": "#/definitions/rpcStatus" + } + }, + "title": "Stream result of rfqrpcRfqEvent" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/rfqrpcSubscribeRfqEventNtfnsRequest" + } + } + ], + "tags": [ + "Rfq" + ] + } + }, + "/v1/taproot-assets/rfq/quotes/accepted": { + "get": { + "summary": "tapcli: `rfq acceptedquotes`\nQueryRfqAcceptedQuotes is used to upsert a sell order for a specific\nasset.", + "operationId": "Rfq_QueryRfqAcceptedQuotes", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/rfqrpcQueryRfqAcceptedQuotesResponse" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "tags": [ + "Rfq" + ] + } + }, + "/v1/taproot-assets/rfq/selloffer/asset-id/{asset_specifier.asset_id_str}": { + "post": { + "summary": "tapcli: `rfq selloffer`\nAddAssetSellOffer is used to add a sell offer for a specific asset. If a\nsell offer already exists for the asset, it will be updated.", + "operationId": "Rfq_AddAssetSellOffer", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/rfqrpcAddAssetSellOfferResponse" + } + }, + "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": { + "$ref": "#/definitions/rfqrpcAddAssetSellOfferRequest" + } + } + ], + "tags": [ + "Rfq" + ] + } + }, + "/v1/taproot-assets/rfq/selloffer/group-key/{asset_specifier.group_key_str}": { + "post": { + "summary": "tapcli: `rfq selloffer`\nAddAssetSellOffer is used to add a sell offer for a specific asset. If a\nsell offer already exists for the asset, it will be updated.", + "operationId": "Rfq_AddAssetSellOffer2", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/rfqrpcAddAssetSellOfferResponse" + } + }, + "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": { + "$ref": "#/definitions/rfqrpcAddAssetSellOfferRequest" + } + } + ], + "tags": [ + "Rfq" + ] + } + } + }, + "definitions": { + "protobufAny": { + "type": "object", + "properties": { + "type_url": { + "type": "string" + }, + "value": { + "type": "string", + "format": "byte" + } + } + }, + "rfqrpcAcceptHtlcEvent": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "uint64", + "description": "Unix timestamp." + }, + "scid": { + "type": "string", + "format": "uint64", + "description": "scid is the short channel ID of the channel over which the payment for\nthe quote is made." + } + } + }, + "rfqrpcAcceptedQuote": { + "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." + }, + "ask_price": { + "type": "string", + "format": "uint64", + "description": "ask_price is the price in millisats for the entire asset amount." + }, + "expiry": { + "type": "string", + "format": "uint64", + "description": "The unix timestamp after which the quote is no longer valid." + } + } + }, + "rfqrpcAddAssetBuyOrderRequest": { + "type": "object", + "properties": { + "asset_specifier": { + "$ref": "#/definitions/rfqrpcAssetSpecifier", + "description": "asset_specifier is the subject asset." + }, + "min_asset_amount": { + "type": "string", + "format": "uint64", + "description": "The minimum amount of the asset to buy." + }, + "max_bid": { + "type": "string", + "format": "uint64", + "description": "The maximum amount BTC to spend (units: millisats)." + }, + "expiry": { + "type": "string", + "format": "uint64", + "description": "The unix timestamp 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." + } + } + }, + "rfqrpcAddAssetBuyOrderResponse": { + "type": "object" + }, + "rfqrpcAddAssetSellOfferRequest": { + "type": "object", + "properties": { + "asset_specifier": { + "$ref": "#/definitions/rfqrpcAssetSpecifier", + "description": "asset_specifier is the subject asset." + }, + "max_units": { + "type": "string", + "format": "uint64", + "description": "max_units is the maximum amount of the asset to sell." + } + } + }, + "rfqrpcAddAssetSellOfferResponse": { + "type": "object" + }, + "rfqrpcAssetSpecifier": { + "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)." + }, + "group_key_str": { + "type": "string", + "description": "The 32-byte asset group key encoded as hex string (use this for\nREST)." + } + } + }, + "rfqrpcIncomingAcceptQuoteEvent": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "uint64", + "description": "Unix timestamp." + }, + "accepted_quote": { + "$ref": "#/definitions/rfqrpcAcceptedQuote", + "description": "The accepted quote." + } + } + }, + "rfqrpcQueryRfqAcceptedQuotesResponse": { + "type": "object", + "properties": { + "accepted_quotes": { + "type": "array", + "items": { + "$ref": "#/definitions/rfqrpcAcceptedQuote" + } + } + } + }, + "rfqrpcRfqEvent": { + "type": "object", + "properties": { + "incoming_accept_quote": { + "$ref": "#/definitions/rfqrpcIncomingAcceptQuoteEvent", + "description": "incoming_accept_quote is an event that is sent when an incoming\naccept 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." + } + } + }, + "rfqrpcSubscribeRfqEventNtfnsRequest": { + "type": "object" + }, + "rpcStatus": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "message": { + "type": "string" + }, + "details": { + "type": "array", + "items": { + "$ref": "#/definitions/protobufAny" + } + } + } + } + } +} diff --git a/taprpc/rfqrpc/rfq.yaml b/taprpc/rfqrpc/rfq.yaml new file mode 100644 index 000000000..b91892d1e --- /dev/null +++ b/taprpc/rfqrpc/rfq.yaml @@ -0,0 +1,25 @@ +type: google.api.Service +config_version: 3 + +http: + rules: + - selector: rfqrpc.Rfq.AddAssetBuyOrder + post: "/v1/taproot-assets/rfq/buyorder/asset-id/{asset_specifier.asset_id_str}" + body: "*" + additional_bindings: + - post: "/v1/taproot-assets/rfq/buyorder/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: "*" + additional_bindings: + - post: "/v1/taproot-assets/rfq/selloffer/group-key/{asset_specifier.group_key_str}" + body: "*" + + - selector: rfqrpc.Rfq.QueryRfqAcceptedQuotes + get: "/v1/taproot-assets/rfq/quotes/accepted" + + - selector: rfqrpc.Rfq.SubscribeRfqEventNtfns + post: "/v1/taproot-assets/rfq/ntfs" + body: "*" \ No newline at end of file diff --git a/taprpc/rfqrpc/rfq_grpc.pb.go b/taprpc/rfqrpc/rfq_grpc.pb.go new file mode 100644 index 000000000..2f6488d9d --- /dev/null +++ b/taprpc/rfqrpc/rfq_grpc.pb.go @@ -0,0 +1,257 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package rfqrpc + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// RfqClient is the client API for Rfq service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type RfqClient interface { + // tapcli: `rfq buyorder` + // 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 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. + AddAssetSellOffer(ctx context.Context, in *AddAssetSellOfferRequest, opts ...grpc.CallOption) (*AddAssetSellOfferResponse, error) + // tapcli: `rfq acceptedquotes` + // QueryRfqAcceptedQuotes is used to upsert a sell order for a specific + // asset. + QueryRfqAcceptedQuotes(ctx context.Context, in *QueryRfqAcceptedQuotesRequest, opts ...grpc.CallOption) (*QueryRfqAcceptedQuotesResponse, error) + // SubscribeRfqEventNtfns is used to subscribe to RFQ events. + SubscribeRfqEventNtfns(ctx context.Context, in *SubscribeRfqEventNtfnsRequest, opts ...grpc.CallOption) (Rfq_SubscribeRfqEventNtfnsClient, error) +} + +type rfqClient struct { + cc grpc.ClientConnInterface +} + +func NewRfqClient(cc grpc.ClientConnInterface) RfqClient { + return &rfqClient{cc} +} + +func (c *rfqClient) AddAssetBuyOrder(ctx context.Context, in *AddAssetBuyOrderRequest, opts ...grpc.CallOption) (*AddAssetBuyOrderResponse, error) { + out := new(AddAssetBuyOrderResponse) + err := c.cc.Invoke(ctx, "/rfqrpc.Rfq/AddAssetBuyOrder", 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...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rfqClient) QueryRfqAcceptedQuotes(ctx context.Context, in *QueryRfqAcceptedQuotesRequest, opts ...grpc.CallOption) (*QueryRfqAcceptedQuotesResponse, error) { + out := new(QueryRfqAcceptedQuotesResponse) + err := c.cc.Invoke(ctx, "/rfqrpc.Rfq/QueryRfqAcceptedQuotes", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *rfqClient) SubscribeRfqEventNtfns(ctx context.Context, in *SubscribeRfqEventNtfnsRequest, opts ...grpc.CallOption) (Rfq_SubscribeRfqEventNtfnsClient, error) { + stream, err := c.cc.NewStream(ctx, &Rfq_ServiceDesc.Streams[0], "/rfqrpc.Rfq/SubscribeRfqEventNtfns", opts...) + if err != nil { + return nil, err + } + x := &rfqSubscribeRfqEventNtfnsClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Rfq_SubscribeRfqEventNtfnsClient interface { + Recv() (*RfqEvent, error) + grpc.ClientStream +} + +type rfqSubscribeRfqEventNtfnsClient struct { + grpc.ClientStream +} + +func (x *rfqSubscribeRfqEventNtfnsClient) Recv() (*RfqEvent, error) { + m := new(RfqEvent) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// RfqServer is the server API for Rfq service. +// All implementations must embed UnimplementedRfqServer +// for forward compatibility +type RfqServer interface { + // tapcli: `rfq buyorder` + // 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 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. + AddAssetSellOffer(context.Context, *AddAssetSellOfferRequest) (*AddAssetSellOfferResponse, error) + // tapcli: `rfq acceptedquotes` + // QueryRfqAcceptedQuotes is used to upsert a sell order for a specific + // asset. + QueryRfqAcceptedQuotes(context.Context, *QueryRfqAcceptedQuotesRequest) (*QueryRfqAcceptedQuotesResponse, error) + // SubscribeRfqEventNtfns is used to subscribe to RFQ events. + SubscribeRfqEventNtfns(*SubscribeRfqEventNtfnsRequest, Rfq_SubscribeRfqEventNtfnsServer) error + mustEmbedUnimplementedRfqServer() +} + +// UnimplementedRfqServer must be embedded to have forward compatible implementations. +type UnimplementedRfqServer struct { +} + +func (UnimplementedRfqServer) AddAssetBuyOrder(context.Context, *AddAssetBuyOrderRequest) (*AddAssetBuyOrderResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddAssetBuyOrder not implemented") +} +func (UnimplementedRfqServer) AddAssetSellOffer(context.Context, *AddAssetSellOfferRequest) (*AddAssetSellOfferResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AddAssetSellOffer not implemented") +} +func (UnimplementedRfqServer) QueryRfqAcceptedQuotes(context.Context, *QueryRfqAcceptedQuotesRequest) (*QueryRfqAcceptedQuotesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method QueryRfqAcceptedQuotes not implemented") +} +func (UnimplementedRfqServer) SubscribeRfqEventNtfns(*SubscribeRfqEventNtfnsRequest, Rfq_SubscribeRfqEventNtfnsServer) error { + return status.Errorf(codes.Unimplemented, "method SubscribeRfqEventNtfns not implemented") +} +func (UnimplementedRfqServer) mustEmbedUnimplementedRfqServer() {} + +// UnsafeRfqServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RfqServer will +// result in compilation errors. +type UnsafeRfqServer interface { + mustEmbedUnimplementedRfqServer() +} + +func RegisterRfqServer(s grpc.ServiceRegistrar, srv RfqServer) { + s.RegisterService(&Rfq_ServiceDesc, srv) +} + +func _Rfq_AddAssetBuyOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddAssetBuyOrderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RfqServer).AddAssetBuyOrder(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rfqrpc.Rfq/AddAssetBuyOrder", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RfqServer).AddAssetBuyOrder(ctx, req.(*AddAssetBuyOrderRequest)) + } + 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 { + return nil, err + } + if interceptor == nil { + return srv.(RfqServer).AddAssetSellOffer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rfqrpc.Rfq/AddAssetSellOffer", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RfqServer).AddAssetSellOffer(ctx, req.(*AddAssetSellOfferRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Rfq_QueryRfqAcceptedQuotes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryRfqAcceptedQuotesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RfqServer).QueryRfqAcceptedQuotes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/rfqrpc.Rfq/QueryRfqAcceptedQuotes", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RfqServer).QueryRfqAcceptedQuotes(ctx, req.(*QueryRfqAcceptedQuotesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Rfq_SubscribeRfqEventNtfns_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SubscribeRfqEventNtfnsRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(RfqServer).SubscribeRfqEventNtfns(m, &rfqSubscribeRfqEventNtfnsServer{stream}) +} + +type Rfq_SubscribeRfqEventNtfnsServer interface { + Send(*RfqEvent) error + grpc.ServerStream +} + +type rfqSubscribeRfqEventNtfnsServer struct { + grpc.ServerStream +} + +func (x *rfqSubscribeRfqEventNtfnsServer) Send(m *RfqEvent) error { + return x.ServerStream.SendMsg(m) +} + +// Rfq_ServiceDesc is the grpc.ServiceDesc for Rfq service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Rfq_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "rfqrpc.Rfq", + HandlerType: (*RfqServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "AddAssetBuyOrder", + Handler: _Rfq_AddAssetBuyOrder_Handler, + }, + { + MethodName: "AddAssetSellOffer", + Handler: _Rfq_AddAssetSellOffer_Handler, + }, + { + MethodName: "QueryRfqAcceptedQuotes", + Handler: _Rfq_QueryRfqAcceptedQuotes_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "SubscribeRfqEventNtfns", + Handler: _Rfq_SubscribeRfqEventNtfns_Handler, + ServerStreams: true, + }, + }, + Metadata: "rfqrpc/rfq.proto", +} From 5b6872b4adaff4c8e1fc99de5da6f661bef3ccc8 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 19 Feb 2024 17:44:00 +0000 Subject: [PATCH 8/8] itest: add RFQ system HTLC interception test This commit adds an itest which tests that the RFQ system can be used to reach an agreement on a quote between two peers and then validate the corresponding lightning payment HTLC. --- itest/interface.go | 2 + itest/loadtest/utils.go | 4 + itest/rfq_test.go | 354 +++++++++++++++++++++++++++++++++++++ itest/tapd_harness.go | 3 + itest/test_list_on_test.go | 6 + 5 files changed, 369 insertions(+) create mode 100644 itest/rfq_test.go diff --git a/itest/interface.go b/itest/interface.go index acd98f0a1..bc5157739 100644 --- a/itest/interface.go +++ b/itest/interface.go @@ -4,6 +4,7 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" unirpc "github.com/lightninglabs/taproot-assets/taprpc/universerpc" ) @@ -12,5 +13,6 @@ type TapdClient interface { taprpc.TaprootAssetsClient unirpc.UniverseClient mintrpc.MintClient + rfqrpc.RfqClient assetwalletrpc.AssetWalletClient } diff --git a/itest/loadtest/utils.go b/itest/loadtest/utils.go index 8e9e6f401..53237ac35 100644 --- a/itest/loadtest/utils.go +++ b/itest/loadtest/utils.go @@ -17,6 +17,7 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc" "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightningnetwork/lnd/macaroons" @@ -38,6 +39,7 @@ type rpcClient struct { assetwalletrpc.AssetWalletClient tapdevrpc.TapDevClient mintrpc.MintClient + rfqrpc.RfqClient universerpc.UniverseClient } @@ -171,6 +173,7 @@ func getTapClient(t *testing.T, ctx context.Context, assetWalletClient := assetwalletrpc.NewAssetWalletClient(conn) devClient := tapdevrpc.NewTapDevClient(conn) mintMintClient := mintrpc.NewMintClient(conn) + rfqClient := rfqrpc.NewRfqClient(conn) universeClient := universerpc.NewUniverseClient(conn) client := &rpcClient{ @@ -179,6 +182,7 @@ func getTapClient(t *testing.T, ctx context.Context, AssetWalletClient: assetWalletClient, TapDevClient: devClient, MintClient: mintMintClient, + RfqClient: rfqClient, UniverseClient: universeClient, } diff --git a/itest/rfq_test.go b/itest/rfq_test.go new file mode 100644 index 000000000..8e834af80 --- /dev/null +++ b/itest/rfq_test.go @@ -0,0 +1,354 @@ +package itest + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + "github.com/lightningnetwork/lnd/chainreg" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +// testRfqHtlcIntercept tests RFQ negotiation and HTLC interception and +// validation between three peers. +// +// The procedure is as follows: +// 1. Carol sends a tap asset request for quote (buy order) to Bob. +// 2. Bob's node accepts the quote. +// 3. Carol uses the quote accept message to construct a lightning invoice which +// will pay for the quote accepted by Bob. +// 4. Alice pays the invoice. +// 5. Bob's node intercepts the lightning payment from Alice and validates it +// against the quote accepted between Bob and Carol. +func testRfqHtlcIntercept(t *harnessTest) { + // Initialize a new test scenario. + ts := newRfqTestScenario(t) + + // Mint an asset with Bob's tapd node. + rpcAssets := MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, ts.BobTapd, + []*mintrpc.MintAssetRequest{issuableAssets[0]}, + ) + mintedAssetId := rpcAssets[0].AssetGenesis.AssetId + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout) + defer cancel() + + // Upsert an asset sell offer to Bob's tapd node. This will allow Bob to + // sell the newly minted asset to Carol. + _, err := ts.BobTapd.AddAssetSellOffer( + ctxt, &rfqrpc.AddAssetSellOfferRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + MaxUnits: 1000, + }, + ) + require.NoError(t.t, err, "unable to upsert asset sell offer") + + // Subscribe to Carol's RFQ events stream. + carolEventNtfns, err := ts.CarolTapd.SubscribeRfqEventNtfns( + ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{}, + ) + require.NoError(t.t, err) + + // Carol sends a buy order to Bob for some amount of the newly minted + // asset. + purchaseAssetAmt := uint64(200) + bidAmt := uint64(42000) + buyOrderExpiry := uint64(time.Now().Add(24 * time.Hour).Unix()) + + _, err = ts.CarolTapd.AddAssetBuyOrder( + ctxt, &rfqrpc.AddAssetBuyOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + MinAssetAmount: purchaseAssetAmt, + MaxBid: bidAmt, + Expiry: buyOrderExpiry, + + // Here we explicitly specify Bob as the destination + // peer for the buy order. This will prompt Carol'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 buy order") + + // Wait until Carol receives an incoming quote accept message (sent from + // Bob) RFQ event notification. + waitErr := wait.NoError(func() error { + event, err := carolEventNtfns.Recv() + require.NoError(t.t, err) + + _, ok := event.Event.(*rfqrpc.RfqEvent_IncomingAcceptQuote) + require.True(t.t, ok, "unexpected event: %v", event) + + return nil + }, defaultWaitTimeout) + require.NoError(t.t, waitErr) + + // Carol should have received an accepted quote from Bob. This accepted + // quote can be used by Carol to make a payment to Bob. + acceptedQuotes, err := ts.CarolTapd.QueryRfqAcceptedQuotes( + ctxt, &rfqrpc.QueryRfqAcceptedQuotesRequest{}, + ) + require.NoError(t.t, err, "unable to query accepted quotes") + require.Len(t.t, acceptedQuotes.AcceptedQuotes, 1) + + // Carol will now use the accepted quote (received from Bob) to create + // a lightning invoice which will be given to and settled by Alice. + // + // The payment will be routed through Bob (who will handle the + // BTC->asset conversion as a last step before reaching Carol). Recall + // that the payment path is: Alice -> Bob -> Carol. And the Bob -> Carol + // last hop will constitute the tap asset transfer. + // + // First, we need to get the short channel ID (scid) for the Alice->Bob + // channel which Carol will include in her invoice. Then, when Alice + // pays the invoice, the payment will arrive to Bob's node with the + // expected scid. Bob will then use the scid to identify the HTLC as + // relating to the accepted quote. + acceptedQuote := acceptedQuotes.AcceptedQuotes[0] + t.Logf("Accepted quote scid: %d", acceptedQuote.Scid) + scid := lnwire.NewShortChanIDFromInt(acceptedQuote.Scid) + + // Use the agreed upon scid found in the accepted quote to construct a + // route hop hint for the Alice->Bob step of the payment. The route hop + // hint will be included in the invoice that Carol hands to Alice. + aliceBobHopHint := &lnrpc.HopHint{ + NodeId: ts.BobLnd.PubKeyStr, + ChanId: scid.ToUint64(), + FeeBaseMsat: uint32( + chainreg.DefaultBitcoinBaseFeeMSat, + ), + FeeProportionalMillionths: uint32( + chainreg.DefaultBitcoinFeeRate, + ), + CltvExpiryDelta: chainreg.DefaultBitcoinTimeLockDelta, + } + routeHints := []*lnrpc.RouteHint{ + { + HopHints: []*lnrpc.HopHint{ + aliceBobHopHint, + }, + }, + } + + // Carol can now finalise the invoice and hand it over to Alice for + // settlement. + addInvoiceResp := ts.CarolLnd.RPC.AddInvoice(&lnrpc.Invoice{ + ValueMsat: int64(bidAmt), + RouteHints: routeHints, + }) + invoice := ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash) + + // 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) + + // Alice pays the invoice. + t.Log("Alice paying invoice") + req := &routerrpc.SendPaymentRequest{ + PaymentRequest: invoice.PaymentRequest, + TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()), + FeeLimitMsat: math.MaxInt64, + } + ts.AliceLnd.RPC.SendPayment(req) + t.Log("Alice payment sent") + + // 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. + waitErr = wait.NoError(func() error { + t.Log("Waiting for Bob to receive HTLC") + + event, err := bobEventNtfns.Recv() + require.NoError(t.t, err) + + acceptHtlc, ok := event.Event.(*rfqrpc.RfqEvent_AcceptHtlc) + if ok { + require.Equal( + t.t, acceptedQuote.Scid, + acceptHtlc.AcceptHtlc.Scid, + ) + t.Log("Bob has accepted the HTLC") + return nil + } + + return fmt.Errorf("unexpected event: %v", event) + }, defaultWaitTimeout) + require.NoError(t.t, waitErr) + + // Close event streams. + err = carolEventNtfns.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, + ht *lntest.HarnessTest) *node.HarnessNode { + + newNode := ht.NewNode(name, nil) + + // Fund node wallet with specified outputs. + totalTxes := len(outputFunds) + const ( + numBlocksSendOutput = 2 + minerFeeRate = btcutil.Amount(7500) + ) + + for i := range outputFunds { + amt := outputFunds[i] + + resp := newNode.RPC.NewAddress(&lnrpc.NewAddressRequest{ + Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH}, + ) + addr := ht.DecodeAddress(resp.Address) + addrScript := ht.PayToAddrScript(addr) + + output := &wire.TxOut{ + PkScript: addrScript, + Value: int64(amt), + } + ht.Miner.SendOutput(output, minerFeeRate) + } + + // Mine any funding transactions. + if totalTxes > 0 { + ht.MineBlocksAndAssertNumTxes(numBlocksSendOutput, totalTxes) + } + + return newNode +} + +// rfqTestScenario is a struct which holds test scenario helper infra. +type rfqTestScenario struct { + testHarness *harnessTest + + AliceLnd *node.HarnessNode + BobLnd *node.HarnessNode + CarolLnd *node.HarnessNode + + AliceBobChannel *lnrpc.ChannelPoint + BobCarolChannel *lnrpc.ChannelPoint + + AliceTapd *tapdHarness + BobTapd *tapdHarness + CarolTapd *tapdHarness +} + +// newRfqTestScenario initializes a new test scenario with three new LND nodes +// and connects them to have the following topology, +// +// Alice --> Bob --> Carol +// +// It also creates new tapd nodes for each of the LND nodes. +func newRfqTestScenario(t *harnessTest) *rfqTestScenario { + // Specify wallet outputs to fund the wallets of the new nodes. + const ( + fundAmount = 1 * btcutil.SatoshiPerBitcoin + numOutputs = 100 + totalAmount = fundAmount * numOutputs + ) + + var outputFunds [numOutputs]btcutil.Amount + for i := range outputFunds { + outputFunds[i] = fundAmount + } + + // Create three new nodes. + aliceLnd := newLndNode("AliceLnd", outputFunds[:], t.lndHarness) + bobLnd := newLndNode("BobLnd", outputFunds[:], t.lndHarness) + carolLnd := newLndNode("CarolLnd", outputFunds[:], t.lndHarness) + + // Now we want to wait for the nodes to catch up. + t.lndHarness.WaitForBlockchainSync(aliceLnd) + t.lndHarness.WaitForBlockchainSync(bobLnd) + t.lndHarness.WaitForBlockchainSync(carolLnd) + + // Now block until both wallets have fully synced up. + t.lndHarness.WaitForBalanceConfirmed(aliceLnd, totalAmount) + t.lndHarness.WaitForBalanceConfirmed(bobLnd, totalAmount) + t.lndHarness.WaitForBalanceConfirmed(carolLnd, totalAmount) + + // Connect the nodes. + t.lndHarness.EnsureConnected(aliceLnd, bobLnd) + t.lndHarness.EnsureConnected(bobLnd, carolLnd) + + // Open channels between the nodes: Alice -> Bob -> Carol + const chanAmt = btcutil.Amount(300000) + p := lntest.OpenChannelParams{Amt: chanAmt} + reqs := []*lntest.OpenChannelRequest{ + {Local: aliceLnd, Remote: bobLnd, Param: p}, + {Local: bobLnd, Remote: carolLnd, Param: p}, + } + resp := t.lndHarness.OpenMultiChannelsAsync(reqs) + aliceBobChannel, bobCarolChannel := resp[0], resp[1] + + // Make sure Alice is aware of channel Bob -> Carol. + t.lndHarness.AssertTopologyChannelOpen(aliceLnd, bobCarolChannel) + + // Create tapd nodes. + aliceTapd := setupTapdHarness(t.t, t, aliceLnd, t.universeServer) + bobTapd := setupTapdHarness(t.t, t, bobLnd, t.universeServer) + carolTapd := setupTapdHarness(t.t, t, carolLnd, t.universeServer) + + ts := rfqTestScenario{ + testHarness: t, + + AliceLnd: aliceLnd, + BobLnd: bobLnd, + CarolLnd: carolLnd, + + AliceBobChannel: aliceBobChannel, + BobCarolChannel: bobCarolChannel, + + AliceTapd: aliceTapd, + BobTapd: bobTapd, + CarolTapd: carolTapd, + } + + // Cleanup the test scenario on test completion. Here we register the + // test scenario's cleanup function with the test cleanup routine. + t.t.Cleanup(ts.Cleanup) + + return &ts +} + +// 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) + + // 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)) +} diff --git a/itest/tapd_harness.go b/itest/tapd_harness.go index 20068d369..c6fc4c218 100644 --- a/itest/tapd_harness.go +++ b/itest/tapd_harness.go @@ -21,6 +21,7 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/assetwalletrpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "github.com/lightninglabs/taproot-assets/taprpc/tapdevrpc" "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightningnetwork/lnd/lnrpc" @@ -93,6 +94,7 @@ type tapdHarness struct { taprpc.TaprootAssetsClient assetwalletrpc.AssetWalletClient mintrpc.MintClient + rfqrpc.RfqClient universerpc.UniverseClient tapdevrpc.TapDevClient } @@ -329,6 +331,7 @@ func (hs *tapdHarness) start(expectErrExit bool) error { hs.TaprootAssetsClient = taprpc.NewTaprootAssetsClient(rpcConn) hs.AssetWalletClient = assetwalletrpc.NewAssetWalletClient(rpcConn) hs.MintClient = mintrpc.NewMintClient(rpcConn) + hs.RfqClient = rfqrpc.NewRfqClient(rpcConn) hs.UniverseClient = universerpc.NewUniverseClient(rpcConn) hs.TapDevClient = tapdevrpc.NewTapDevClient(rpcConn) diff --git a/itest/test_list_on_test.go b/itest/test_list_on_test.go index e5bbe67ae..1ae2c631f 100644 --- a/itest/test_list_on_test.go +++ b/itest/test_list_on_test.go @@ -237,6 +237,12 @@ var testCases = []*testCase{ name: "mint proof repeat fed sync attempt", test: testMintProofRepeatFedSyncAttempt, }, + + // Request for quote (RFQ) tests. + { + name: "rfq htlc intercept", + test: testRfqHtlcIntercept, + }, } var optionalTestCases = []*testCase{