Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/examples/basic-price-oracle/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ func (p *RpcPriceOracleServer) QueryAssetRates(_ context.Context,
Error: &oraclerpc.QueryAssetRatesErrResponse{
Message: "unsupported payment asset, " +
"only BTC is supported",
Code: 1,
},
},
}, nil
Expand All @@ -326,6 +327,7 @@ func (p *RpcPriceOracleServer) QueryAssetRates(_ context.Context,
Result: &oraclerpc.QueryAssetRatesResponse_Error{
Error: &oraclerpc.QueryAssetRatesErrResponse{
Message: "unsupported subject asset",
Code: 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there an enum val from our lib that we can use here instead of 1?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used a raw '1' at present to avoid updating the mock oracle's dependencies (it looks like there have been other RPC changes since it was last updated as well). Perhaps best to avoid changing the mock oracle for now, and just update it to use the latest RPC definitions in another PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(If you agree, I'll drop the commit that changes the mock oracle before merge.)

},
},
}, nil
Expand Down
17 changes: 13 additions & 4 deletions docs/release-notes/release-notes-0.7.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -236,5 +244,6 @@

- ffranr
- George Tsagkarelis
- jtobin
- Olaoluwa Osuntokun
- Oliver Gugger
107 changes: 89 additions & 18 deletions rfq/negotiator.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rfq

import (
"errors"
"fmt"
"sync"
"time"
Expand All @@ -27,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
Expand Down Expand Up @@ -141,22 +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 convert it to a string and return it as an error.
// If present, we will relay it with context.
if oracleResponse.Err != nil {
return nil, fmt.Errorf("failed to query price oracle for "+
"buy price: %s", 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.
Expand Down Expand Up @@ -277,22 +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 convert it to a string and return it as an error.
// If present, we will relay it with context.
if oracleResponse.Err != nil {
return nil, fmt.Errorf("failed to query price oracle for "+
"sell price: %s", 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.
Expand Down Expand Up @@ -372,10 +410,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)

Expand Down Expand Up @@ -473,10 +513,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)

Expand All @@ -495,6 +537,35 @@ 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 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
}

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
}
}

// HandleOutgoingSellOrder handles an outgoing sell order by constructing sell
// requests and passing them to the outgoing messages channel. These requests
// are sent to peers.
Expand Down
35 changes: 32 additions & 3 deletions rfq/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
// UnspecifiedOracleErrorCode represents the case where the oracle has
// declined to give a more specific reason for the error.
UnspecifiedOracleErrorCode OracleErrorCode = iota

// 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.
func (o *OracleError) Error() string {
// Sanitise price oracle error message by truncating to 255 characters.
Expand Down Expand Up @@ -356,7 +370,8 @@ func (r *RpcPriceOracle) QuerySellPrice(ctx context.Context,

return &OracleResponse{
Err: &OracleError{
Msg: result.Error.Message,
Msg: result.Error.Message,
Code: marshallErrorCode(result.Error.Code),
},
}, nil

Expand All @@ -365,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_UNSPECIFIED_ORACLE_ERROR_CODE:
return UnspecifiedOracleErrorCode
case oraclerpc.ErrorCode_UNSUPPORTED_ASSET_ORACLE_ERROR_CODE:
return UnsupportedAssetOracleErrorCode
default:
return UnspecifiedOracleErrorCode
}
}

// QueryBuyPrice returns a buy price for the given asset amount.
func (r *RpcPriceOracle) QueryBuyPrice(ctx context.Context,
assetSpecifier asset.Specifier, assetMaxAmt fn.Option[uint64],
Expand Down Expand Up @@ -467,7 +495,8 @@ func (r *RpcPriceOracle) QueryBuyPrice(ctx context.Context,

return &OracleResponse{
Err: &OracleError{
Msg: result.Error.Message,
Msg: result.Error.Message,
Code: marshallErrorCode(result.Error.Code),
},
}, nil

Expand Down
28 changes: 24 additions & 4 deletions rfqmsg/reject.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,22 +76,42 @@ 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",
}
)

// ErrRejectWithCustomMsg produces the "unknown" error code, but pairs
// it with a custom error message.
func ErrRejectWithCustomMsg(msg string) RejectErr {
return RejectErr{
Code: UnspecifiedRejectCode,
Msg: msg,
}
}

const (
// latestRejectVersion is the latest supported reject wire message data
// field version.
Expand Down
Loading
Loading