From 3a7986f775cada6d2f6c819dfe6d1a80f412a228 Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Wed, 20 Aug 2025 05:31:43 -0230 Subject: [PATCH 01/13] rfqmsg: add reject-with-custom-message utility ErrRejectWithCustomMsg constructs a RejectErr with error code 0 and the specified error message. --- rfqmsg/reject.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rfqmsg/reject.go b/rfqmsg/reject.go index 44eef2a6a8..42455c1ad4 100644 --- a/rfqmsg/reject.go +++ b/rfqmsg/reject.go @@ -92,6 +92,15 @@ var ( } ) +// ErrRejectWithCustomMsg produces the "unknown" error code, but pairs +// it with a custom error message. +func ErrRejectWithCustomMsg(msg string) RejectErr { + return RejectErr{ + Code: 0, + Msg: msg, + } +} + const ( // latestRejectVersion is the latest supported reject wire message data // field version. From f58d19eb7fa77a0d215a379d28a2204bceb0f4b5 Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Wed, 20 Aug 2025 09:00:30 -0230 Subject: [PATCH 02/13] rfq: add structured oracle error codes Formalizes the 'Code' of an 'OracleError' as an 'OracleErrorCode', and adds a couple of values corresponding to structured error cases. --- rfq/oracle.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rfq/oracle.go b/rfq/oracle.go index e7ee286945..c183a26e60 100644 --- a/rfq/oracle.go +++ b/rfq/oracle.go @@ -86,12 +86,26 @@ const ( // service. type OracleError struct { // Code is a code which uniquely identifies the error type. - Code uint8 + Code OracleErrorCode // Msg is a human-readable error message. Msg string } +// OracleErrorCode uniquely identifies the kinds of error an oracle may +// return. +type OracleErrorCode uint8 + +const ( + // ErrUnspecifiedOracleError represents the case where the oracle has + // declined to give a more specific reason for the error. + ErrUnspecifiedOracleError OracleErrorCode = iota + + // ErrUnsupportedOracleAsset represents the case in which an oracle does + // not provide quotes for the requested asset. + ErrUnsupportedOracleAsset +) + // Error returns a human-readable string representation of the error. func (o *OracleError) Error() string { // Sanitise price oracle error message by truncating to 255 characters. From 2f096e2e75f1e0d7eb7b19205cbff4b2fb0505f8 Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Thu, 21 Aug 2025 08:51:51 -0230 Subject: [PATCH 03/13] rfq: relay error code in oracle response Ensure the error code is part of an OracleError response, and pass the error intact in query{Bid,Ask}FromPriceOracle, instead of formatting it as a string. --- rfq/negotiator.go | 10 ++++------ rfq/oracle.go | 6 ++++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rfq/negotiator.go b/rfq/negotiator.go index b7606129c7..c4f48f6356 100644 --- a/rfq/negotiator.go +++ b/rfq/negotiator.go @@ -146,10 +146,9 @@ func (n *Negotiator) queryBuyFromPriceOracle(assetSpecifier asset.Specifier, } // Now we will check for an error in the response from the price oracle. - // If present, we will convert it to a string and return it as an error. + // If present, we will simply relay it. if oracleResponse.Err != nil { - return nil, fmt.Errorf("failed to query price oracle for "+ - "buy price: %s", oracleResponse.Err) + return nil, oracleResponse.Err } // By this point, the price oracle did not return an error or a buy @@ -282,10 +281,9 @@ func (n *Negotiator) querySellFromPriceOracle(assetSpecifier asset.Specifier, } // Now we will check for an error in the response from the price oracle. - // If present, we will convert it to a string and return it as an error. + // If present, we will simply relay it. if oracleResponse.Err != nil { - return nil, fmt.Errorf("failed to query price oracle for "+ - "sell price: %s", oracleResponse.Err) + return nil, oracleResponse.Err } // By this point, the price oracle did not return an error or a sell diff --git a/rfq/oracle.go b/rfq/oracle.go index c183a26e60..de8ae6e8d5 100644 --- a/rfq/oracle.go +++ b/rfq/oracle.go @@ -370,7 +370,8 @@ func (r *RpcPriceOracle) QuerySellPrice(ctx context.Context, return &OracleResponse{ Err: &OracleError{ - Msg: result.Error.Message, + Msg: result.Error.Message, + Code: OracleErrorCode(result.Error.Code), }, }, nil @@ -481,7 +482,8 @@ func (r *RpcPriceOracle) QueryBuyPrice(ctx context.Context, return &OracleResponse{ Err: &OracleError{ - Msg: result.Error.Message, + Msg: result.Error.Message, + Code: OracleErrorCode(result.Error.Code), }, }, nil From b9b018a7851dde9b56f1b83604f3329f7505e482 Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Thu, 21 Aug 2025 09:55:58 -0230 Subject: [PATCH 04/13] rfq: customize reject message by oracle error Uses the OracleError returned by an oracle to customize the rejection message sent to a peer. If the OracleError contains a "transparent" code that's deemed to be suitable for passing on to a peer, then simply relay that error. Otherwise relay an opaque "unknown reject error" message. --- rfq/negotiator.go | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/rfq/negotiator.go b/rfq/negotiator.go index c4f48f6356..dd3ab00763 100644 --- a/rfq/negotiator.go +++ b/rfq/negotiator.go @@ -1,6 +1,7 @@ package rfq import ( + "errors" "fmt" "sync" "time" @@ -370,10 +371,12 @@ func (n *Negotiator) HandleIncomingBuyRequest( peerID, request.PriceOracleMetadata, IntentRecvPayment, ) if err != nil { - // Send a reject message to the peer. + // Construct an appropriate RejectErr based on + // the oracle's response, and send it to the + // peer. msg := rfqmsg.NewReject( request.Peer, request.ID, - rfqmsg.ErrUnknownReject, + createCustomRejectErr(err), ) sendOutgoingMsg(msg) @@ -471,10 +474,12 @@ func (n *Negotiator) HandleIncomingSellRequest( peerID, request.PriceOracleMetadata, IntentPayInvoice, ) if err != nil { - // Send a reject message to the peer. + // Construct an appropriate RejectErr based on + // the oracle's response, and send it to the + // peer. msg := rfqmsg.NewReject( request.Peer, request.ID, - rfqmsg.ErrUnknownReject, + createCustomRejectErr(err), ) sendOutgoingMsg(msg) @@ -493,6 +498,36 @@ func (n *Negotiator) HandleIncomingSellRequest( return nil } +// createCustomRejectErr creates a RejectErr with code 0 and a custom message +// based on an error response from a price oracle. +func createCustomRejectErr(err error) rfqmsg.RejectErr { + var oracleError *OracleError + + if errors.As(err, &oracleError) { + // The error is of the expected type, so switch on the error + // code returned by the oracle. If the code is benign, then the + // RejectErr will simply relay the oracle's message. Otherwise, + // we'll return an opaque rejection message. + switch oracleError.Code { + + // The rejection message will state that the oracle doesn't + // support the asset. + case ErrUnsupportedOracleAsset: + msg := oracleError.Error() + return rfqmsg.ErrRejectWithCustomMsg(msg) + + // The rejection message will be opaque, with the error + // unspecified. + default: + return rfqmsg.ErrUnknownReject + } + } else { + // The error is of an unexpected type, so just return an opaque + // error message. + return rfqmsg.ErrUnknownReject + } +} + // HandleOutgoingSellOrder handles an outgoing sell order by constructing sell // requests and passing them to the outgoing messages channel. These requests // are sent to peers. From bbd3afeece068c893ffb2b6a3eec1eaa2b74d6aa Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Mon, 25 Aug 2025 05:49:09 -0230 Subject: [PATCH 05/13] taprpc: update priceoraclerpc proto defs --- taprpc/priceoraclerpc/price_oracle.pb.go | 191 ++++++++++++------ taprpc/priceoraclerpc/price_oracle.proto | 12 +- .../priceoraclerpc/price_oracle.swagger.json | 12 +- 3 files changed, 145 insertions(+), 70 deletions(-) diff --git a/taprpc/priceoraclerpc/price_oracle.pb.go b/taprpc/priceoraclerpc/price_oracle.pb.go index c0c0c60262..d45bdb732d 100644 --- a/taprpc/priceoraclerpc/price_oracle.pb.go +++ b/taprpc/priceoraclerpc/price_oracle.pb.go @@ -172,6 +172,56 @@ func (Intent) EnumDescriptor() ([]byte, []int) { return file_priceoraclerpc_price_oracle_proto_rawDescGZIP(), []int{1} } +// ErrorCode represents the possible error codes that can be returned in a +// QueryAssetRatesErrResponse. +type ErrorCode int32 + +const ( + // ERROR_UNSPECIFIED indicates an unspecified error. + ErrorCode_ERROR_UNSPECIFIED ErrorCode = 0 + // UNSUPPORTED indicates the asset is not supported. + ErrorCode_ERROR_UNSUPPORTED ErrorCode = 1 +) + +// Enum value maps for ErrorCode. +var ( + ErrorCode_name = map[int32]string{ + 0: "ERROR_UNSPECIFIED", + 1: "ERROR_UNSUPPORTED", + } + ErrorCode_value = map[string]int32{ + "ERROR_UNSPECIFIED": 0, + "ERROR_UNSUPPORTED": 1, + } +) + +func (x ErrorCode) Enum() *ErrorCode { + p := new(ErrorCode) + *p = x + return p +} + +func (x ErrorCode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ErrorCode) Descriptor() protoreflect.EnumDescriptor { + return file_priceoraclerpc_price_oracle_proto_enumTypes[2].Descriptor() +} + +func (ErrorCode) Type() protoreflect.EnumType { + return &file_priceoraclerpc_price_oracle_proto_enumTypes[2] +} + +func (x ErrorCode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ErrorCode.Descriptor instead. +func (ErrorCode) EnumDescriptor() ([]byte, []int) { + return file_priceoraclerpc_price_oracle_proto_rawDescGZIP(), []int{2} +} + // FixedPoint is a scaled integer representation of a fractional number. // // This type consists of two integer fields: a coefficient and a scale. @@ -657,7 +707,7 @@ type QueryAssetRatesErrResponse struct { // error is the error message. Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` // code is the error code. - Code uint32 `protobuf:"varint,2,opt,name=code,proto3" json:"code,omitempty"` + Code ErrorCode `protobuf:"varint,2,opt,name=code,proto3,enum=priceoraclerpc.ErrorCode" json:"code,omitempty"` } func (x *QueryAssetRatesErrResponse) Reset() { @@ -699,11 +749,11 @@ func (x *QueryAssetRatesErrResponse) GetMessage() string { return "" } -func (x *QueryAssetRatesErrResponse) GetCode() uint32 { +func (x *QueryAssetRatesErrResponse) GetCode() ErrorCode { if x != nil { return x.Code } - return 0 + return ErrorCode_ERROR_UNSPECIFIED } // QueryAssetRatesResponse is the response from a QueryAssetRates RPC call. @@ -862,50 +912,55 @@ var file_priceoraclerpc_price_oracle_proto_rawDesc = []byte{ 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x0a, - 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x22, 0x4a, 0x0a, 0x1a, 0x51, 0x75, + 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x22, 0x65, 0x0a, 0x1a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x45, 0x72, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, - 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, - 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x4f, - 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x02, 0x6f, 0x6b, 0x12, - 0x42, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, - 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x45, - 0x72, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2a, 0x29, 0x0a, - 0x0f, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, - 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x55, 0x52, 0x43, 0x48, 0x41, 0x53, 0x45, 0x10, 0x00, 0x12, 0x08, - 0x0a, 0x04, 0x53, 0x41, 0x4c, 0x45, 0x10, 0x01, 0x2a, 0xcd, 0x01, 0x0a, 0x06, 0x49, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x49, + 0x67, 0x65, 0x12, 0x2d, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x19, 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, + 0x63, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, + 0x65, 0x22, 0xa4, 0x01, 0x0a, 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, + 0x02, 0x6f, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x70, 0x72, 0x69, 0x63, + 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x4f, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x02, 0x6f, 0x6b, 0x12, 0x42, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x70, 0x72, 0x69, 0x63, + 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x45, 0x72, 0x72, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x42, 0x08, + 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2a, 0x29, 0x0a, 0x0f, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x08, 0x50, + 0x55, 0x52, 0x43, 0x48, 0x41, 0x53, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x41, 0x4c, + 0x45, 0x10, 0x01, 0x2a, 0xcd, 0x01, 0x0a, 0x06, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x16, + 0x0a, 0x12, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, + 0x5f, 0x50, 0x41, 0x59, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x48, 0x49, 0x4e, + 0x54, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x41, + 0x59, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x10, 0x02, 0x12, 0x1e, 0x0a, 0x1a, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, - 0x45, 0x5f, 0x48, 0x49, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x54, 0x45, - 0x4e, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x10, 0x02, - 0x12, 0x1e, 0x0a, 0x1a, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x5f, 0x49, - 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, 0x59, 0x10, 0x03, - 0x12, 0x1c, 0x0a, 0x18, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, - 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x48, 0x49, 0x4e, 0x54, 0x10, 0x04, 0x12, 0x17, - 0x0a, 0x13, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, - 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x05, 0x12, 0x1f, 0x0a, 0x1b, 0x49, 0x4e, 0x54, 0x45, 0x4e, - 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x51, - 0x55, 0x41, 0x4c, 0x49, 0x46, 0x59, 0x10, 0x06, 0x32, 0x71, 0x0a, 0x0b, 0x50, 0x72, 0x69, 0x63, - 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x51, 0x75, 0x65, 0x72, 0x79, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x70, 0x72, 0x69, - 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, - 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, - 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, - 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 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, 0x70, 0x72, - 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x45, 0x5f, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, 0x59, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x49, + 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, + 0x4e, 0x54, 0x5f, 0x48, 0x49, 0x4e, 0x54, 0x10, 0x04, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4e, 0x54, + 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, + 0x10, 0x05, 0x12, 0x1f, 0x0a, 0x1b, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, + 0x56, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, + 0x59, 0x10, 0x06, 0x2a, 0x39, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, + 0x12, 0x15, 0x0a, 0x11, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x5f, 0x55, 0x4e, 0x53, 0x55, 0x50, 0x50, 0x4f, 0x52, 0x54, 0x45, 0x44, 0x10, 0x01, 0x32, 0x71, + 0x0a, 0x0b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x12, 0x62, 0x0a, + 0x0f, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, + 0x12, 0x26, 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, + 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, + 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x3f, 0x5a, 0x3d, 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, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, + 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -920,37 +975,39 @@ func file_priceoraclerpc_price_oracle_proto_rawDescGZIP() []byte { return file_priceoraclerpc_price_oracle_proto_rawDescData } -var file_priceoraclerpc_price_oracle_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_priceoraclerpc_price_oracle_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_priceoraclerpc_price_oracle_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_priceoraclerpc_price_oracle_proto_goTypes = []any{ (TransactionType)(0), // 0: priceoraclerpc.TransactionType (Intent)(0), // 1: priceoraclerpc.Intent - (*FixedPoint)(nil), // 2: priceoraclerpc.FixedPoint - (*AssetRates)(nil), // 3: priceoraclerpc.AssetRates - (*AssetSpecifier)(nil), // 4: priceoraclerpc.AssetSpecifier - (*QueryAssetRatesRequest)(nil), // 5: priceoraclerpc.QueryAssetRatesRequest - (*QueryAssetRatesOkResponse)(nil), // 6: priceoraclerpc.QueryAssetRatesOkResponse - (*QueryAssetRatesErrResponse)(nil), // 7: priceoraclerpc.QueryAssetRatesErrResponse - (*QueryAssetRatesResponse)(nil), // 8: priceoraclerpc.QueryAssetRatesResponse + (ErrorCode)(0), // 2: priceoraclerpc.ErrorCode + (*FixedPoint)(nil), // 3: priceoraclerpc.FixedPoint + (*AssetRates)(nil), // 4: priceoraclerpc.AssetRates + (*AssetSpecifier)(nil), // 5: priceoraclerpc.AssetSpecifier + (*QueryAssetRatesRequest)(nil), // 6: priceoraclerpc.QueryAssetRatesRequest + (*QueryAssetRatesOkResponse)(nil), // 7: priceoraclerpc.QueryAssetRatesOkResponse + (*QueryAssetRatesErrResponse)(nil), // 8: priceoraclerpc.QueryAssetRatesErrResponse + (*QueryAssetRatesResponse)(nil), // 9: priceoraclerpc.QueryAssetRatesResponse } var file_priceoraclerpc_price_oracle_proto_depIdxs = []int32{ - 2, // 0: priceoraclerpc.AssetRates.subjectAssetRate:type_name -> priceoraclerpc.FixedPoint - 2, // 1: priceoraclerpc.AssetRates.paymentAssetRate:type_name -> priceoraclerpc.FixedPoint + 3, // 0: priceoraclerpc.AssetRates.subjectAssetRate:type_name -> priceoraclerpc.FixedPoint + 3, // 1: priceoraclerpc.AssetRates.paymentAssetRate:type_name -> priceoraclerpc.FixedPoint 0, // 2: priceoraclerpc.QueryAssetRatesRequest.transaction_type:type_name -> priceoraclerpc.TransactionType - 4, // 3: priceoraclerpc.QueryAssetRatesRequest.subject_asset:type_name -> priceoraclerpc.AssetSpecifier - 4, // 4: priceoraclerpc.QueryAssetRatesRequest.payment_asset:type_name -> priceoraclerpc.AssetSpecifier - 3, // 5: priceoraclerpc.QueryAssetRatesRequest.asset_rates_hint:type_name -> priceoraclerpc.AssetRates + 5, // 3: priceoraclerpc.QueryAssetRatesRequest.subject_asset:type_name -> priceoraclerpc.AssetSpecifier + 5, // 4: priceoraclerpc.QueryAssetRatesRequest.payment_asset:type_name -> priceoraclerpc.AssetSpecifier + 4, // 5: priceoraclerpc.QueryAssetRatesRequest.asset_rates_hint:type_name -> priceoraclerpc.AssetRates 1, // 6: priceoraclerpc.QueryAssetRatesRequest.intent:type_name -> priceoraclerpc.Intent - 3, // 7: priceoraclerpc.QueryAssetRatesOkResponse.asset_rates:type_name -> priceoraclerpc.AssetRates - 6, // 8: priceoraclerpc.QueryAssetRatesResponse.ok:type_name -> priceoraclerpc.QueryAssetRatesOkResponse - 7, // 9: priceoraclerpc.QueryAssetRatesResponse.error:type_name -> priceoraclerpc.QueryAssetRatesErrResponse - 5, // 10: priceoraclerpc.PriceOracle.QueryAssetRates:input_type -> priceoraclerpc.QueryAssetRatesRequest - 8, // 11: priceoraclerpc.PriceOracle.QueryAssetRates:output_type -> priceoraclerpc.QueryAssetRatesResponse - 11, // [11:12] is the sub-list for method output_type - 10, // [10:11] is the sub-list for method input_type - 10, // [10:10] is the sub-list for extension type_name - 10, // [10:10] is the sub-list for extension extendee - 0, // [0:10] is the sub-list for field type_name + 4, // 7: priceoraclerpc.QueryAssetRatesOkResponse.asset_rates:type_name -> priceoraclerpc.AssetRates + 2, // 8: priceoraclerpc.QueryAssetRatesErrResponse.code:type_name -> priceoraclerpc.ErrorCode + 7, // 9: priceoraclerpc.QueryAssetRatesResponse.ok:type_name -> priceoraclerpc.QueryAssetRatesOkResponse + 8, // 10: priceoraclerpc.QueryAssetRatesResponse.error:type_name -> priceoraclerpc.QueryAssetRatesErrResponse + 6, // 11: priceoraclerpc.PriceOracle.QueryAssetRates:input_type -> priceoraclerpc.QueryAssetRatesRequest + 9, // 12: priceoraclerpc.PriceOracle.QueryAssetRates:output_type -> priceoraclerpc.QueryAssetRatesResponse + 12, // [12:13] is the sub-list for method output_type + 11, // [11:12] is the sub-list for method input_type + 11, // [11:11] is the sub-list for extension type_name + 11, // [11:11] is the sub-list for extension extendee + 0, // [0:11] is the sub-list for field type_name } func init() { file_priceoraclerpc_price_oracle_proto_init() } @@ -1059,7 +1116,7 @@ func file_priceoraclerpc_price_oracle_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_priceoraclerpc_price_oracle_proto_rawDesc, - NumEnums: 2, + NumEnums: 3, NumMessages: 7, NumExtensions: 0, NumServices: 1, diff --git a/taprpc/priceoraclerpc/price_oracle.proto b/taprpc/priceoraclerpc/price_oracle.proto index 128ca4ae5e..b3f783ab86 100644 --- a/taprpc/priceoraclerpc/price_oracle.proto +++ b/taprpc/priceoraclerpc/price_oracle.proto @@ -81,6 +81,16 @@ enum Intent { INTENT_RECV_PAYMENT_QUALIFY = 6; } +// ErrorCode represents the possible error codes that can be returned in a +// QueryAssetRatesErrResponse. +enum ErrorCode { + // ERROR_UNSPECIFIED indicates an unspecified error. + ERROR_UNSPECIFIED = 0; + + // UNSUPPORTED indicates the asset is not supported. + ERROR_UNSUPPORTED = 1; +} + // FixedPoint is a scaled integer representation of a fractional number. // // This type consists of two integer fields: a coefficient and a scale. @@ -228,7 +238,7 @@ message QueryAssetRatesErrResponse { string message = 1; // code is the error code. - uint32 code = 2; + ErrorCode code = 2; } // QueryAssetRatesResponse is the response from a QueryAssetRates RPC call. diff --git a/taprpc/priceoraclerpc/price_oracle.swagger.json b/taprpc/priceoraclerpc/price_oracle.swagger.json index 04044cb66e..161ab24714 100644 --- a/taprpc/priceoraclerpc/price_oracle.swagger.json +++ b/taprpc/priceoraclerpc/price_oracle.swagger.json @@ -244,6 +244,15 @@ }, "description": "AssetSpecifier is a union type for specifying an asset by either its asset ID\nor group key." }, + "priceoraclerpcErrorCode": { + "type": "string", + "enum": [ + "ERROR_UNSPECIFIED", + "ERROR_UNSUPPORTED" + ], + "default": "ERROR_UNSPECIFIED", + "description": "ErrorCode represents the possible error codes that can be returned in a\nQueryAssetRatesErrResponse.\n\n - ERROR_UNSPECIFIED: ERROR_UNSPECIFIED indicates an unspecified error.\n - ERROR_UNSUPPORTED: UNSUPPORTED indicates the asset is not supported." + }, "priceoraclerpcFixedPoint": { "type": "object", "properties": { @@ -281,8 +290,7 @@ "description": "error is the error message." }, "code": { - "type": "integer", - "format": "int64", + "$ref": "#/definitions/priceoraclerpcErrorCode", "description": "code is the error code." } }, From 5274aaf4f6fd56a3167d103e254c4f6bfab55078 Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Mon, 25 Aug 2025 06:42:52 -0230 Subject: [PATCH 06/13] rfq: refine error code handling Avoids a blunt cast of the wire code, and also refrains from relaying the full Error() message to the peer. --- rfq/negotiator.go | 2 +- rfq/oracle.go | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/rfq/negotiator.go b/rfq/negotiator.go index dd3ab00763..7c03d7a297 100644 --- a/rfq/negotiator.go +++ b/rfq/negotiator.go @@ -513,7 +513,7 @@ func createCustomRejectErr(err error) rfqmsg.RejectErr { // The rejection message will state that the oracle doesn't // support the asset. case ErrUnsupportedOracleAsset: - msg := oracleError.Error() + msg := oracleError.Msg return rfqmsg.ErrRejectWithCustomMsg(msg) // The rejection message will be opaque, with the error diff --git a/rfq/oracle.go b/rfq/oracle.go index de8ae6e8d5..005924ca31 100644 --- a/rfq/oracle.go +++ b/rfq/oracle.go @@ -371,7 +371,7 @@ func (r *RpcPriceOracle) QuerySellPrice(ctx context.Context, return &OracleResponse{ Err: &OracleError{ Msg: result.Error.Message, - Code: OracleErrorCode(result.Error.Code), + Code: marshallErrorCode(result.Error.Code), }, }, nil @@ -380,6 +380,19 @@ func (r *RpcPriceOracle) QuerySellPrice(ctx context.Context, } } +// marshallErrorCode marshalls an over-the-wire error code into an +// OracleErrorCode. +func marshallErrorCode(code oraclerpc.ErrorCode) OracleErrorCode { + switch code { + case oraclerpc.ErrorCode_ERROR_UNSPECIFIED: + return ErrUnspecifiedOracleError + case oraclerpc.ErrorCode_ERROR_UNSUPPORTED: + return ErrUnsupportedOracleAsset + default: + return ErrUnspecifiedOracleError + } +} + // QueryBuyPrice returns a buy price for the given asset amount. func (r *RpcPriceOracle) QueryBuyPrice(ctx context.Context, assetSpecifier asset.Specifier, assetMaxAmt fn.Option[uint64], @@ -483,7 +496,7 @@ func (r *RpcPriceOracle) QueryBuyPrice(ctx context.Context, return &OracleResponse{ Err: &OracleError{ Msg: result.Error.Message, - Code: OracleErrorCode(result.Error.Code), + Code: marshallErrorCode(result.Error.Code), }, }, nil From e875f5afa005e81d9d2cd72ab6bb338510739eed Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Mon, 25 Aug 2025 07:41:00 -0230 Subject: [PATCH 07/13] basic-price-oracle: specify error code Simply passes an error code of 1 (unsupported subject asset) where appropriate in the mock oracle. The raw code is used instead of oraclerpc.UNSUPPORTED or similar to avoid dependency changes at present. Also adds the mock oracle's log to gitignore. --- .gitignore | 1 + docs/examples/basic-price-oracle/main.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 114e77ec67..0fc14d0d30 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ cmd/tapd/tapd /itest/itest.test /docs/examples/basic-price-oracle/basic-price-oracle +/docs/examples/basic-price-oracle/basic-price-oracle-example.log # Load test binaries and config /loadtest diff --git a/docs/examples/basic-price-oracle/main.go b/docs/examples/basic-price-oracle/main.go index 83609fc7c9..28c6358796 100644 --- a/docs/examples/basic-price-oracle/main.go +++ b/docs/examples/basic-price-oracle/main.go @@ -306,6 +306,7 @@ func (p *RpcPriceOracleServer) QueryAssetRates(_ context.Context, Error: &oraclerpc.QueryAssetRatesErrResponse{ Message: "unsupported payment asset, " + "only BTC is supported", + Code: 1, }, }, }, nil @@ -326,6 +327,7 @@ func (p *RpcPriceOracleServer) QueryAssetRates(_ context.Context, Result: &oraclerpc.QueryAssetRatesResponse_Error{ Error: &oraclerpc.QueryAssetRatesErrResponse{ Message: "unsupported subject asset", + Code: 1, }, }, }, nil From 6bdb36a379f44f2f2df4138ab7c0ba1a7ec7c912 Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Mon, 25 Aug 2025 08:28:26 -0230 Subject: [PATCH 08/13] docs: add release notes --- docs/release-notes/release-notes-0.7.0.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/release-notes-0.7.0.md b/docs/release-notes/release-notes-0.7.0.md index 17578b0ed1..b92d424fb9 100644 --- a/docs/release-notes/release-notes-0.7.0.md +++ b/docs/release-notes/release-notes-0.7.0.md @@ -26,8 +26,8 @@ - [An integration test flake was fixed](https://github.com/lightninglabs/taproot-assets/pull/1651). -- Fixed two send related bugs that would lead to either a `invalid transfer - asset witness` or `unable to fund address send: error funding packet: unable +- Fixed two send related bugs that would lead to either a `invalid transfer + asset witness` or `unable to fund address send: error funding packet: unable to list eligible coins: unable to query commitments: mismatch of managed utxo and constructed tap commitment root` error when sending assets. The [PR that fixed the two @@ -119,17 +119,25 @@ when requesting quotes. The field can contain optional user or authentication information that helps the price oracle to decide on the optimal price rate to return. + +- The error code returned in a response from a price oracle now has a + [structured + form](https://github.com/lightninglabs/taproot-assets/pull/1766), + allowing price oracles to either indicate that a given asset is + unsupported, or simply to return an opaque error. "Unsupported asset" + errors are forwarded in reject messages sent to peers. + - [Rename](https://github.com/lightninglabs/taproot-assets/pull/1682) the `MintAsset` RPC message field from `universe_commitments` to `enable_supply_commitments`. -- The `SubscribeSendEvents` RPC now supports [historical event replay of +- The `SubscribeSendEvents` RPC now supports [historical event replay of completed sends with efficient database-level filtering](https://github.com/lightninglabs/taproot-assets/pull/1685). - [Add universe RPC endpoint FetchSupplyLeaves](https://github.com/lightninglabs/taproot-assets/pull/1693) that allows users to fetch the supply leaves of a universe supply commitment. This is useful for verification. -- A [new field `unconfirmed_transfers` was added to the response of the +- A [new field `unconfirmed_transfers` was added to the response of the `ListBalances` RPC method](https://github.com/lightninglabs/taproot-assets/pull/1691) to indicate that unconfirmed asset-related transactions don't count toward the balance. @@ -236,5 +244,6 @@ - ffranr - George Tsagkarelis +- jtobin - Olaoluwa Osuntokun - Oliver Gugger From aef763a94cb6db3b21c8d90de86d006604f0b3b8 Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Mon, 25 Aug 2025 09:32:13 -0230 Subject: [PATCH 09/13] rfq: fix linter nit --- rfq/negotiator.go | 1 - 1 file changed, 1 deletion(-) diff --git a/rfq/negotiator.go b/rfq/negotiator.go index 7c03d7a297..e9d3dbfc36 100644 --- a/rfq/negotiator.go +++ b/rfq/negotiator.go @@ -509,7 +509,6 @@ func createCustomRejectErr(err error) rfqmsg.RejectErr { // RejectErr will simply relay the oracle's message. Otherwise, // we'll return an opaque rejection message. switch oracleError.Code { - // The rejection message will state that the oracle doesn't // support the asset. case ErrUnsupportedOracleAsset: From 6130dac62e9e3735aaf2017475a6cebd464ff3cd Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Tue, 26 Aug 2025 12:55:22 -0230 Subject: [PATCH 10/13] rfq: rename oracle error code values Changes these to avoid the 'Err..' convention presently reserved for type names. --- rfq/negotiator.go | 2 +- rfq/oracle.go | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/rfq/negotiator.go b/rfq/negotiator.go index e9d3dbfc36..d27c55e0cd 100644 --- a/rfq/negotiator.go +++ b/rfq/negotiator.go @@ -511,7 +511,7 @@ func createCustomRejectErr(err error) rfqmsg.RejectErr { switch oracleError.Code { // The rejection message will state that the oracle doesn't // support the asset. - case ErrUnsupportedOracleAsset: + case UnsupportedAssetOracleErrorCode: msg := oracleError.Msg return rfqmsg.ErrRejectWithCustomMsg(msg) diff --git a/rfq/oracle.go b/rfq/oracle.go index 005924ca31..cf64f78bca 100644 --- a/rfq/oracle.go +++ b/rfq/oracle.go @@ -97,13 +97,13 @@ type OracleError struct { type OracleErrorCode uint8 const ( - // ErrUnspecifiedOracleError represents the case where the oracle has + // UnspecifiedOracleErrorCode represents the case where the oracle has // declined to give a more specific reason for the error. - ErrUnspecifiedOracleError OracleErrorCode = iota + UnspecifiedOracleErrorCode OracleErrorCode = iota - // ErrUnsupportedOracleAsset represents the case in which an oracle does - // not provide quotes for the requested asset. - ErrUnsupportedOracleAsset + // UnsupportedAssetOracleErrorCode represents the case in which an + // oracle does not provide quotes for the requested asset. + UnsupportedAssetOracleErrorCode ) // Error returns a human-readable string representation of the error. @@ -385,11 +385,11 @@ func (r *RpcPriceOracle) QuerySellPrice(ctx context.Context, func marshallErrorCode(code oraclerpc.ErrorCode) OracleErrorCode { switch code { case oraclerpc.ErrorCode_ERROR_UNSPECIFIED: - return ErrUnspecifiedOracleError + return UnspecifiedOracleErrorCode case oraclerpc.ErrorCode_ERROR_UNSUPPORTED: - return ErrUnsupportedOracleAsset + return UnsupportedAssetOracleErrorCode default: - return ErrUnspecifiedOracleError + return UnspecifiedOracleErrorCode } } From 79308a2ff3e520c518a2ac181c174202ef17ea48 Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Tue, 26 Aug 2025 13:12:56 -0230 Subject: [PATCH 11/13] rfqmsg: use const/enum reject error codes Defines specific consts for the two RejectErr error codes, and uses those in place of the raw uint8 values. --- rfqmsg/reject.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/rfqmsg/reject.go b/rfqmsg/reject.go index 42455c1ad4..821a0798db 100644 --- a/rfqmsg/reject.go +++ b/rfqmsg/reject.go @@ -76,18 +76,29 @@ func (v *RejectErr) Record() tlv.Record { ) } +const ( + // UnspecifiedRejectCode indicates that a request-for-quote was + // rejected, without necessarily providing any further detail as to + // why. + UnspecifiedRejectCode uint8 = iota + + // UnavailableRejectCode indicates that a request-for-quote was + // rejected as a price oracle was unavailable. + UnavailableRejectCode +) + var ( // ErrUnknownReject is the error code for when the quote is rejected // for an unspecified reason. ErrUnknownReject = RejectErr{ - Code: 0, + Code: UnspecifiedRejectCode, Msg: "unknown reject error", } - // ErrPriceOracleUnavailable is the error code for when the price oracle - // is unavailable. + // ErrPriceOracleUnavailable is the error code for when the price + // oracle is unavailable. ErrPriceOracleUnavailable = RejectErr{ - Code: 1, + Code: UnavailableRejectCode, Msg: "price oracle unavailable", } ) @@ -96,7 +107,7 @@ var ( // it with a custom error message. func ErrRejectWithCustomMsg(msg string) RejectErr { return RejectErr{ - Code: 0, + Code: UnspecifiedRejectCode, Msg: msg, } } From 6ff7217b8b316954496798e94aca420edf3d763e Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Tue, 26 Aug 2025 14:13:52 -0230 Subject: [PATCH 12/13] taprpc: update priceoraclerpc proto definitions Changes 'ERROR_UNSPECIFIED' and 'ERROR_SUPPORTED' to more closely match the OracleErrorCode values in rfq/oracle.go. --- rfq/oracle.go | 4 +- taprpc/priceoraclerpc/price_oracle.pb.go | 53 ++++++++++--------- taprpc/priceoraclerpc/price_oracle.proto | 9 ++-- .../priceoraclerpc/price_oracle.swagger.json | 8 +-- 4 files changed, 39 insertions(+), 35 deletions(-) diff --git a/rfq/oracle.go b/rfq/oracle.go index cf64f78bca..503577bda5 100644 --- a/rfq/oracle.go +++ b/rfq/oracle.go @@ -384,9 +384,9 @@ func (r *RpcPriceOracle) QuerySellPrice(ctx context.Context, // OracleErrorCode. func marshallErrorCode(code oraclerpc.ErrorCode) OracleErrorCode { switch code { - case oraclerpc.ErrorCode_ERROR_UNSPECIFIED: + case oraclerpc.ErrorCode_UNSPECIFIED_ORACLE_ERROR_CODE: return UnspecifiedOracleErrorCode - case oraclerpc.ErrorCode_ERROR_UNSUPPORTED: + case oraclerpc.ErrorCode_UNSUPPORTED_ASSET_ORACLE_ERROR_CODE: return UnsupportedAssetOracleErrorCode default: return UnspecifiedOracleErrorCode diff --git a/taprpc/priceoraclerpc/price_oracle.pb.go b/taprpc/priceoraclerpc/price_oracle.pb.go index d45bdb732d..d26fd91e6e 100644 --- a/taprpc/priceoraclerpc/price_oracle.pb.go +++ b/taprpc/priceoraclerpc/price_oracle.pb.go @@ -177,21 +177,22 @@ func (Intent) EnumDescriptor() ([]byte, []int) { type ErrorCode int32 const ( - // ERROR_UNSPECIFIED indicates an unspecified error. - ErrorCode_ERROR_UNSPECIFIED ErrorCode = 0 - // UNSUPPORTED indicates the asset is not supported. - ErrorCode_ERROR_UNSUPPORTED ErrorCode = 1 + // UNSPECIFIED_ORACLE_ERROR_CODE indicates an unspecified error. + ErrorCode_UNSPECIFIED_ORACLE_ERROR_CODE ErrorCode = 0 + // UNSUPPORTED_ASSET_ORACLE_ERROR_CODE indicates the asset is not + // supported. + ErrorCode_UNSUPPORTED_ASSET_ORACLE_ERROR_CODE ErrorCode = 1 ) // Enum value maps for ErrorCode. var ( ErrorCode_name = map[int32]string{ - 0: "ERROR_UNSPECIFIED", - 1: "ERROR_UNSUPPORTED", + 0: "UNSPECIFIED_ORACLE_ERROR_CODE", + 1: "UNSUPPORTED_ASSET_ORACLE_ERROR_CODE", } ErrorCode_value = map[string]int32{ - "ERROR_UNSPECIFIED": 0, - "ERROR_UNSUPPORTED": 1, + "UNSPECIFIED_ORACLE_ERROR_CODE": 0, + "UNSUPPORTED_ASSET_ORACLE_ERROR_CODE": 1, } ) @@ -753,7 +754,7 @@ func (x *QueryAssetRatesErrResponse) GetCode() ErrorCode { if x != nil { return x.Code } - return ErrorCode_ERROR_UNSPECIFIED + return ErrorCode_UNSPECIFIED_ORACLE_ERROR_CODE } // QueryAssetRatesResponse is the response from a QueryAssetRates RPC call. @@ -945,22 +946,24 @@ var file_priceoraclerpc_price_oracle_proto_rawDesc = []byte{ 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x05, 0x12, 0x1f, 0x0a, 0x1b, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, - 0x59, 0x10, 0x06, 0x2a, 0x39, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, - 0x12, 0x15, 0x0a, 0x11, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x5f, 0x55, 0x4e, 0x53, 0x55, 0x50, 0x50, 0x4f, 0x52, 0x54, 0x45, 0x44, 0x10, 0x01, 0x32, 0x71, - 0x0a, 0x0b, 0x50, 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x12, 0x62, 0x0a, - 0x0f, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, - 0x12, 0x26, 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, - 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, - 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x3f, 0x5a, 0x3d, 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, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, - 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x59, 0x10, 0x06, 0x2a, 0x57, 0x0a, 0x09, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x43, 0x6f, 0x64, 0x65, + 0x12, 0x21, 0x0a, 0x1d, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, + 0x4f, 0x52, 0x41, 0x43, 0x4c, 0x45, 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, + 0x45, 0x10, 0x00, 0x12, 0x27, 0x0a, 0x23, 0x55, 0x4e, 0x53, 0x55, 0x50, 0x50, 0x4f, 0x52, 0x54, + 0x45, 0x44, 0x5f, 0x41, 0x53, 0x53, 0x45, 0x54, 0x5f, 0x4f, 0x52, 0x41, 0x43, 0x4c, 0x45, 0x5f, + 0x45, 0x52, 0x52, 0x4f, 0x52, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x10, 0x01, 0x32, 0x71, 0x0a, 0x0b, + 0x50, 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x12, 0x62, 0x0a, 0x0f, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x12, 0x26, + 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, + 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x3f, 0x5a, 0x3d, 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, 0x70, 0x72, 0x69, 0x63, 0x65, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x72, 0x70, 0x63, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/taprpc/priceoraclerpc/price_oracle.proto b/taprpc/priceoraclerpc/price_oracle.proto index b3f783ab86..c2fd14e23a 100644 --- a/taprpc/priceoraclerpc/price_oracle.proto +++ b/taprpc/priceoraclerpc/price_oracle.proto @@ -84,11 +84,12 @@ enum Intent { // ErrorCode represents the possible error codes that can be returned in a // QueryAssetRatesErrResponse. enum ErrorCode { - // ERROR_UNSPECIFIED indicates an unspecified error. - ERROR_UNSPECIFIED = 0; + // UNSPECIFIED_ORACLE_ERROR_CODE indicates an unspecified error. + UNSPECIFIED_ORACLE_ERROR_CODE = 0; - // UNSUPPORTED indicates the asset is not supported. - ERROR_UNSUPPORTED = 1; + // UNSUPPORTED_ASSET_ORACLE_ERROR_CODE indicates the asset is not + // supported. + UNSUPPORTED_ASSET_ORACLE_ERROR_CODE = 1; } // FixedPoint is a scaled integer representation of a fractional number. diff --git a/taprpc/priceoraclerpc/price_oracle.swagger.json b/taprpc/priceoraclerpc/price_oracle.swagger.json index 161ab24714..317da5bc4b 100644 --- a/taprpc/priceoraclerpc/price_oracle.swagger.json +++ b/taprpc/priceoraclerpc/price_oracle.swagger.json @@ -247,11 +247,11 @@ "priceoraclerpcErrorCode": { "type": "string", "enum": [ - "ERROR_UNSPECIFIED", - "ERROR_UNSUPPORTED" + "UNSPECIFIED_ORACLE_ERROR_CODE", + "UNSUPPORTED_ASSET_ORACLE_ERROR_CODE" ], - "default": "ERROR_UNSPECIFIED", - "description": "ErrorCode represents the possible error codes that can be returned in a\nQueryAssetRatesErrResponse.\n\n - ERROR_UNSPECIFIED: ERROR_UNSPECIFIED indicates an unspecified error.\n - ERROR_UNSUPPORTED: UNSUPPORTED indicates the asset is not supported." + "default": "UNSPECIFIED_ORACLE_ERROR_CODE", + "description": "ErrorCode represents the possible error codes that can be returned in a\nQueryAssetRatesErrResponse.\n\n - UNSPECIFIED_ORACLE_ERROR_CODE: UNSPECIFIED_ORACLE_ERROR_CODE indicates an unspecified error.\n - UNSUPPORTED_ASSET_ORACLE_ERROR_CODE: UNSUPPORTED_ASSET_ORACLE_ERROR_CODE indicates the asset is not\nsupported." }, "priceoraclerpcFixedPoint": { "type": "object", From 5eaf8a90e9e13600b980ee15acdb8c4ec577b984 Mon Sep 17 00:00:00 2001 From: Jared Tobin Date: Wed, 27 Aug 2025 11:02:38 -0230 Subject: [PATCH 13/13] rfq: preserve context in oracle query errors Introduces a structured QueryError that wraps an arbitrary error (usually from a price oracle) with arbitrary context, and adjusts query{Buy,Sell}FromPriceOracle to return these errors where appropriate. Also adjusts createCustomRejectErr to handle QueryErrors, instead of OracleErrors. --- rfq/negotiator.go | 103 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 32 deletions(-) diff --git a/rfq/negotiator.go b/rfq/negotiator.go index d27c55e0cd..f3a2b99475 100644 --- a/rfq/negotiator.go +++ b/rfq/negotiator.go @@ -28,6 +28,29 @@ const ( DefaultAcceptPriceDeviationPpm = 50_000 ) +// QueryError represents an error with additional context about the price +// oracle query that led to it. +type QueryError struct { + // Err is the error returned from a query attempt, possibly from a + // price oracle. + Err error + + // Context is the context of the price oracle query that led to the + // error. + Context string +} + +// Error returns a human-readable version of the QueryError, implementing the +// main error interface. +func (err *QueryError) Error() string { + // If there's no context, just fall back to the wrapped error. + if err.Context == "" { + return err.Err.Error() + } + // Otherwise prepend the context. + return err.Context + ": " + err.Err.Error() +} + // NegotiatorCfg holds the configuration for the negotiator. type NegotiatorCfg struct { // PriceOracle is the price oracle that the negotiator will use to @@ -142,21 +165,29 @@ func (n *Negotiator) queryBuyFromPriceOracle(assetSpecifier asset.Specifier, counterparty, metadata, intent, ) if err != nil { - return nil, fmt.Errorf("failed to query price oracle for "+ - "buy price: %w", err) + return nil, &QueryError{ + Err: err, + Context: "failed to query price oracle for buy price", + } } // Now we will check for an error in the response from the price oracle. - // If present, we will simply relay it. + // If present, we will relay it with context. if oracleResponse.Err != nil { - return nil, oracleResponse.Err + return nil, &QueryError{ + Err: oracleResponse.Err, + Context: "failed to query price oracle for buy price", + } } // By this point, the price oracle did not return an error or a buy // price. We will therefore return an error. if oracleResponse.AssetRate.Rate.ToUint64() == 0 { - return nil, fmt.Errorf("price oracle did not specify a " + - "buy price") + return nil, &QueryError{ + Err: errors.New("price oracle didn't specify " + + "a price"), + Context: "failed to query price oracle for buy price", + } } // TODO(ffranr): Check that the buy price is reasonable. @@ -277,21 +308,29 @@ func (n *Negotiator) querySellFromPriceOracle(assetSpecifier asset.Specifier, counterparty, metadata, intent, ) if err != nil { - return nil, fmt.Errorf("failed to query price oracle for "+ - "sell price: %w", err) + return nil, &QueryError{ + Err: err, + Context: "failed to query price oracle for sell price", + } } // Now we will check for an error in the response from the price oracle. - // If present, we will simply relay it. + // If present, we will relay it with context. if oracleResponse.Err != nil { - return nil, oracleResponse.Err + return nil, &QueryError{ + Err: oracleResponse.Err, + Context: "failed to query price oracle for sell price", + } } // By this point, the price oracle did not return an error or a sell // price. We will therefore return an error. if oracleResponse.AssetRate.Rate.Coefficient.ToUint64() == 0 { - return nil, fmt.Errorf("price oracle did not specify an " + - "asset to BTC rate") + return nil, &QueryError{ + Err: errors.New("price oracle didn't specify " + + "a price"), + Context: "failed to query price oracle for sell price", + } } // TODO(ffranr): Check that the sell price is reasonable. @@ -501,28 +540,28 @@ func (n *Negotiator) HandleIncomingSellRequest( // createCustomRejectErr creates a RejectErr with code 0 and a custom message // based on an error response from a price oracle. func createCustomRejectErr(err error) rfqmsg.RejectErr { + var queryError *QueryError + // Check if the error we've received is the expected QueryError, and + // return an opaque rejection error if not. + if !errors.As(err, &queryError) { + return rfqmsg.ErrUnknownReject + } + var oracleError *OracleError + // Check if the QueryError contains the expected OracleError, and + // return an opaque rejection error if not. + if !errors.As(queryError, &oracleError) { + return rfqmsg.ErrUnknownReject + } - if errors.As(err, &oracleError) { - // The error is of the expected type, so switch on the error - // code returned by the oracle. If the code is benign, then the - // RejectErr will simply relay the oracle's message. Otherwise, - // we'll return an opaque rejection message. - switch oracleError.Code { - // The rejection message will state that the oracle doesn't - // support the asset. - case UnsupportedAssetOracleErrorCode: - msg := oracleError.Msg - return rfqmsg.ErrRejectWithCustomMsg(msg) - - // The rejection message will be opaque, with the error - // unspecified. - default: - return rfqmsg.ErrUnknownReject - } - } else { - // The error is of an unexpected type, so just return an opaque - // error message. + switch oracleError.Code { + // The price oracle has indicated that it doesn't support the asset, + // so return a rejection error indicating that. + case UnsupportedAssetOracleErrorCode: + return rfqmsg.ErrRejectWithCustomMsg(oracleError.Msg) + // The error code is either unspecified or unknown, so return an + // opaque rejection error. + default: return rfqmsg.ErrUnknownReject } }