Skip to content

Commit

Permalink
rfq: OracleResponse uses new AssetRate type
Browse files Browse the repository at this point in the history
This commit does the following:
* Make use of AssetRate in the OracleResponse type.
* Modify functions queryBidFromPriceOracle and queryAskFromPriceOracle
  such that they return the new AssetRate types.

Added TODOs noting that New*AcceptFromRequest functions will be modified
similarly in future work.
  • Loading branch information
ffranr committed Oct 30, 2024
1 parent 0c9f87b commit 71d2293
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 57 deletions.
62 changes: 28 additions & 34 deletions rfq/negotiator.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ func NewNegotiator(cfg NegotiatorCfg) (*Negotiator, error) {
// 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,
assetRateHint fn.Option[rfqmsg.AssetRate]) (*rfqmath.BigIntFixedPoint,
uint64, error) {
assetRateHint fn.Option[rfqmsg.AssetRate]) (*rfqmsg.AssetRate, error) {

// TODO(ffranr): Optionally accept a peer's proposed ask price as an
// arg to this func and pass it to the price oracle. The price oracle
Expand All @@ -124,28 +123,28 @@ func (n *Negotiator) queryBidFromPriceOracle(peer route.Vertex,
ctx, assetId, assetGroupKey, assetAmount, assetRateHint,
)
if err != nil {
return nil, 0, fmt.Errorf("failed to query price oracle for "+
return nil, fmt.Errorf("failed to query price oracle for "+
"bid: %w", err)
}

// 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 oracleResponse.Err != nil {
return nil, 0, fmt.Errorf("failed to query price oracle for "+
return nil, fmt.Errorf("failed to query price oracle for "+
"bid price: %s", oracleResponse.Err)
}

// By this point, the price oracle did not return an error or a bid
// price. We will therefore return an error.
if oracleResponse.AssetRate.Coefficient.ToUint64() == 0 {
return nil, 0, fmt.Errorf("price oracle did not specify a " +
if oracleResponse.AssetRate.Rate.ToUint64() == 0 {
return nil, fmt.Errorf("price oracle did not specify a " +
"bid price")
}

// TODO(ffranr): Check that the bid price is reasonable.
// TODO(ffranr): Ensure that the expiry time is valid and sufficient.

return &oracleResponse.AssetRate, oracleResponse.Expiry, nil
return &oracleResponse.AssetRate, nil
}

// HandleOutgoingBuyOrder handles an outgoing buy order by constructing buy
Expand All @@ -166,7 +165,7 @@ func (n *Negotiator) HandleOutgoingBuyOrder(buyOrder BuyOrder) error {

if n.cfg.PriceOracle != nil {
// Query the price oracle for a bid price.
rate, expiryUnix, err := n.queryBidFromPriceOracle(
assetRate, err := n.queryBidFromPriceOracle(
*buyOrder.Peer, buyOrder.AssetID,
buyOrder.AssetGroupKey, buyOrder.MinAssetAmount,
fn.None[rfqmsg.AssetRate](),
Expand All @@ -180,10 +179,7 @@ func (n *Negotiator) HandleOutgoingBuyOrder(buyOrder BuyOrder) error {
"request: %v", err)
}

expiry := time.Unix(int64(expiryUnix), 0)
assetRateHint = fn.Some[rfqmsg.AssetRate](
rfqmsg.NewAssetRate(*rate, expiry),
)
assetRateHint = fn.Some[rfqmsg.AssetRate](*assetRate)
}

request, err := rfqmsg.NewBuyRequest(
Expand Down Expand Up @@ -220,8 +216,7 @@ func (n *Negotiator) HandleOutgoingBuyOrder(buyOrder BuyOrder) error {
// peer.
func (n *Negotiator) queryAskFromPriceOracle(peer *route.Vertex,
assetId *asset.ID, assetGroupKey *btcec.PublicKey, assetAmount uint64,
assetRateHint fn.Option[rfqmsg.AssetRate]) (
*rfqmath.BigIntFixedPoint, uint64, error) {
assetRateHint fn.Option[rfqmsg.AssetRate]) (*rfqmsg.AssetRate, error) {

// Query the price oracle for an asking price.
ctx, cancel := n.WithCtxQuitNoTimeout()
Expand All @@ -231,28 +226,28 @@ func (n *Negotiator) queryAskFromPriceOracle(peer *route.Vertex,
ctx, assetId, assetGroupKey, assetAmount, assetRateHint,
)
if err != nil {
return nil, 0, fmt.Errorf("failed to query price oracle for "+
return nil, fmt.Errorf("failed to query price oracle for "+
"ask price: %w", err)
}

// 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 oracleResponse.Err != nil {
return nil, 0, fmt.Errorf("failed to query price oracle for "+
return nil, fmt.Errorf("failed to query price oracle for "+
"ask price: %s", oracleResponse.Err)
}

// By this point, the price oracle did not return an error or an asking
// price. We will therefore return an error.
if oracleResponse.AssetRate.Coefficient.ToUint64() == 0 {
return nil, 0, fmt.Errorf("price oracle did not specify an " +
if oracleResponse.AssetRate.Rate.Coefficient.ToUint64() == 0 {
return nil, fmt.Errorf("price oracle did not specify an " +
"asset to BTC rate")
}

// TODO(ffranr): Check that the asking price is reasonable.
// TODO(ffranr): Ensure that the expiry time is valid and sufficient.

return &oracleResponse.AssetRate, oracleResponse.Expiry, nil
return &oracleResponse.AssetRate, nil
}

// HandleIncomingBuyRequest handles an incoming asset buy quote request.
Expand Down Expand Up @@ -315,7 +310,7 @@ func (n *Negotiator) HandleIncomingBuyRequest(
defer n.Wg.Done()

// Query the price oracle for an asking price.
assetRate, rateExpiry, err := n.queryAskFromPriceOracle(
assetRate, err := n.queryAskFromPriceOracle(
nil, request.AssetID, request.AssetGroupKey,
request.AssetAmount, request.AssetRateHint,
)
Expand All @@ -335,8 +330,9 @@ func (n *Negotiator) HandleIncomingBuyRequest(
}

// Construct and send a buy accept message.
expiry := uint64(assetRate.Expiry.Unix())
msg := rfqmsg.NewBuyAcceptFromRequest(
request, *assetRate, rateExpiry,
request, assetRate.Rate, expiry,
)
sendOutgoingMsg(msg)
}()
Expand Down Expand Up @@ -410,7 +406,7 @@ func (n *Negotiator) HandleIncomingSellRequest(
// Query the price oracle for a bid price. This is the price we
// are willing to pay for the asset that our peer is trying to
// sell to us.
assetRate, rateExpiry, err := n.queryBidFromPriceOracle(
assetRate, err := n.queryBidFromPriceOracle(
request.Peer, request.AssetID, request.AssetGroupKey,
request.AssetAmount, request.AssetRateHint,
)
Expand All @@ -430,8 +426,9 @@ func (n *Negotiator) HandleIncomingSellRequest(
}

// Construct and send a sell accept message.
expiry := uint64(assetRate.Expiry.Unix())
msg := rfqmsg.NewSellAcceptFromRequest(
request, *assetRate, rateExpiry,
request, assetRate.Rate, expiry,
)
sendOutgoingMsg(msg)
}()
Expand All @@ -453,11 +450,11 @@ func (n *Negotiator) HandleOutgoingSellOrder(order SellOrder) {
// We calculate a proposed ask price for our peer's
// consideration. If a price oracle is not specified we will
// skip this step.
var assetRate fn.Option[rfqmsg.AssetRate]
var assetRateHint fn.Option[rfqmsg.AssetRate]

if n.cfg.PriceOracle != nil {
// Query the price oracle for an asking price.
rate, expiryUnix, err := n.queryAskFromPriceOracle(
assetRate, err := n.queryAskFromPriceOracle(
order.Peer, order.AssetID, order.AssetGroupKey,
order.MaxAssetAmount,
fn.None[rfqmsg.AssetRate](),
Expand All @@ -469,15 +466,12 @@ func (n *Negotiator) HandleOutgoingSellOrder(order SellOrder) {
return
}

expiry := time.Unix(int64(expiryUnix), 0)
assetRate = fn.Some[rfqmsg.AssetRate](
rfqmsg.NewAssetRate(*rate, expiry),
)
assetRateHint = fn.Some[rfqmsg.AssetRate](*assetRate)
}

request, err := rfqmsg.NewSellRequest(
*order.Peer, order.AssetID, order.AssetGroupKey,
order.MaxAssetAmount, assetRate,
order.MaxAssetAmount, assetRateHint,
)
if err != nil {
err := fmt.Errorf("unable to create sell request "+
Expand Down Expand Up @@ -574,7 +568,7 @@ func (n *Negotiator) HandleIncomingBuyAccept(msg rfqmsg.BuyAccept,
// We will sanity check that price by querying our price oracle
// for an ask price. We will then compare the ask price returned
// by the price oracle with the ask price provided by the peer.
assetRate, _, err := n.queryAskFromPriceOracle(
assetRate, err := n.queryAskFromPriceOracle(
&msg.Peer, msg.Request.AssetID, nil,
msg.Request.AssetAmount, fn.None[rfqmsg.AssetRate](),
)
Expand Down Expand Up @@ -608,7 +602,7 @@ func (n *Negotiator) HandleIncomingBuyAccept(msg rfqmsg.BuyAccept,
big.NewInt(0).SetUint64(n.cfg.AcceptPriceDeviationPpm),
)
acceptablePrice := msg.AssetRate.WithinTolerance(
*assetRate, tolerance,
assetRate.Rate, tolerance,
)
if !acceptablePrice {
// The price is not within the acceptable tolerance.
Expand Down Expand Up @@ -698,7 +692,7 @@ func (n *Negotiator) HandleIncomingSellAccept(msg rfqmsg.SellAccept,
// We will sanity check that price by querying our price oracle
// for a bid price. We will then compare the bid price returned
// by the price oracle with the bid price provided by the peer.
assetRate, _, err := n.queryBidFromPriceOracle(
assetRate, err := n.queryBidFromPriceOracle(
msg.Peer, msg.Request.AssetID, nil,
msg.Request.AssetAmount, msg.Request.AssetRateHint,
)
Expand Down Expand Up @@ -732,7 +726,7 @@ func (n *Negotiator) HandleIncomingSellAccept(msg rfqmsg.SellAccept,
big.NewInt(0).SetUint64(n.cfg.AcceptPriceDeviationPpm),
)
acceptablePrice := msg.AssetRate.WithinTolerance(
*assetRate, tolerance,
assetRate.Rate, tolerance,
)
if !acceptablePrice {
// The price is not within the acceptable bounds.
Expand Down
49 changes: 32 additions & 17 deletions rfq/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"net/url"
"time"

"math"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightninglabs/taproot-assets/asset"
"github.com/lightninglabs/taproot-assets/fn"
Expand Down Expand Up @@ -46,11 +48,7 @@ func (o *OracleError) Error() string {
type OracleResponse struct {
// AssetRate is the asset to BTC rate. Other asset in the transfer is
// assumed to be BTC and therefore not included in the response.
AssetRate rfqmath.BigIntFixedPoint

// Expiry is the asset to BTC rate expiry lifetime unix timestamp. The
// rate is only valid until this time.
Expiry uint64
AssetRate rfqmsg.AssetRate

// Err is an optional error returned by the price oracle service.
Err *OracleError
Expand Down Expand Up @@ -256,9 +254,17 @@ func (r *RpcPriceOracle) QueryAskPrice(ctx context.Context,
return nil, err
}

// Unmarshal the expiry timestamp.
if result.Ok.AssetRates.ExpiryTimestamp > math.MaxInt64 {
return nil, fmt.Errorf("expiry timestamp exceeds " +
"int64 max")
}
expiry := time.Unix(int64(
result.Ok.AssetRates.ExpiryTimestamp,
), 0).UTC()

return &OracleResponse{
AssetRate: *rate,
Expiry: result.Ok.AssetRates.ExpiryTimestamp,
AssetRate: rfqmsg.NewAssetRate(*rate, expiry),
}, nil

case *oraclerpc.QueryAssetRatesResponse_Error:
Expand Down Expand Up @@ -343,9 +349,17 @@ func (r *RpcPriceOracle) QueryBidPrice(ctx context.Context, assetId *asset.ID,
return nil, err
}

// Unmarshal the expiry timestamp.
if result.Ok.AssetRates.ExpiryTimestamp > math.MaxInt64 {
return nil, fmt.Errorf("expiry timestamp exceeds " +
"int64 max")
}
expiry := time.Unix(int64(
result.Ok.AssetRates.ExpiryTimestamp,
), 0).UTC()

return &OracleResponse{
AssetRate: *rate,
Expiry: result.Ok.AssetRates.ExpiryTimestamp,
AssetRate: rfqmsg.NewAssetRate(*rate, expiry),
}, nil

case *oraclerpc.QueryAssetRatesResponse_Error:
Expand All @@ -371,6 +385,7 @@ 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 is the lifetime of a quote in seconds.
expiryDelay uint64
assetToBtcRate rfqmath.BigIntFixedPoint
}
Expand Down Expand Up @@ -407,12 +422,12 @@ func (m *MockPriceOracle) QueryAskPrice(_ context.Context,
_ *asset.ID, _ *btcec.PublicKey, _ uint64,
_ fn.Option[rfqmsg.AssetRate]) (*OracleResponse, error) {

// Calculate the rate expiryDelay lifetime.
expiry := uint64(time.Now().Unix()) + m.expiryDelay
// Calculate the rate expiry timestamp.
lifetime := time.Duration(m.expiryDelay) * time.Second
expiry := time.Now().Add(lifetime).UTC()

return &OracleResponse{
AssetRate: m.assetToBtcRate,
Expiry: expiry,
AssetRate: rfqmsg.NewAssetRate(m.assetToBtcRate, expiry),
}, nil
}

Expand All @@ -421,12 +436,12 @@ func (m *MockPriceOracle) QueryBidPrice(_ context.Context, _ *asset.ID,
_ *btcec.PublicKey, _ uint64,
_ fn.Option[rfqmsg.AssetRate]) (*OracleResponse, error) {

// Calculate the rate expiryDelay lifetime.
expiry := uint64(time.Now().Unix()) + m.expiryDelay
// Calculate the rate expiry timestamp.
lifetime := time.Duration(m.expiryDelay) * time.Second
expiry := time.Now().Add(lifetime).UTC()

return &OracleResponse{
AssetRate: m.assetToBtcRate,
Expiry: expiry,
AssetRate: rfqmsg.NewAssetRate(m.assetToBtcRate, expiry),
}, nil
}

Expand Down
12 changes: 6 additions & 6 deletions rfq/oracle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,11 @@ func runQueryAskPriceTest(t *testing.T, tc *testCaseQueryAskPrice) {
// The mock server should return the asset rates hint.
require.NotNil(t, resp.AssetRate)
require.Equal(
t, uint64(bidPrice), resp.AssetRate.Coefficient.ToUint64(),
t, uint64(bidPrice), resp.AssetRate.Rate.Coefficient.ToUint64(),
)

// Ensure that the expiry timestamp is in the future.
responseExpiry := time.Unix(int64(resp.Expiry), 0)
require.True(t, responseExpiry.After(time.Now()))
require.True(t, resp.AssetRate.Expiry.After(time.Now()))
}

// TestRpcPriceOracle tests the RPC price oracle client QueryAskPrice function.
Expand Down Expand Up @@ -276,11 +275,12 @@ func runQueryBidPriceTest(t *testing.T, tc *testCaseQueryBidPrice) {

// The mock server should return the asset rates hint.
require.NotNil(t, resp.AssetRate)
require.Equal(t, testAssetRate, resp.AssetRate.Coefficient.ToUint64())
require.Equal(
t, testAssetRate, resp.AssetRate.Rate.Coefficient.ToUint64(),
)

// Ensure that the expiry timestamp is in the future.
responseExpiry := time.Unix(int64(resp.Expiry), 0)
require.True(t, responseExpiry.After(time.Now()))
require.True(t, resp.AssetRate.Expiry.After(time.Now()))
}

// TestRpcPriceOracle tests the RPC price oracle client QueryBidPrice function.
Expand Down
2 changes: 2 additions & 0 deletions rfqmsg/buy_accept.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type BuyAccept struct {

// NewBuyAcceptFromRequest creates a new instance of a quote accept message
// given a quote request message.
//
// TODO(ffranr): Use new AssetRate type for assetRate arg.
func NewBuyAcceptFromRequest(request BuyRequest,
assetRate rfqmath.BigIntFixedPoint, expiry uint64) *BuyAccept {

Expand Down
2 changes: 2 additions & 0 deletions rfqmsg/sell_accept.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type SellAccept struct {

// NewSellAcceptFromRequest creates a new instance of an asset sell quote accept
// message given an asset sell quote request message.
//
// // TODO(ffranr): Use new AssetRate type for assetRate arg.
func NewSellAcceptFromRequest(request SellRequest,
assetRate rfqmath.BigIntFixedPoint, expiry uint64) *SellAccept {

Expand Down

0 comments on commit 71d2293

Please sign in to comment.