diff --git a/rfq/negotiator.go b/rfq/negotiator.go index 01ba9af5a..19a4a3dc4 100644 --- a/rfq/negotiator.go +++ b/rfq/negotiator.go @@ -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 @@ -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 @@ -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](), @@ -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( @@ -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() @@ -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. @@ -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, ) @@ -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) }() @@ -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, ) @@ -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) }() @@ -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](), @@ -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 "+ @@ -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](), ) @@ -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. @@ -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, ) @@ -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. diff --git a/rfq/oracle.go b/rfq/oracle.go index bc1d1c142..daf36afe4 100644 --- a/rfq/oracle.go +++ b/rfq/oracle.go @@ -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" @@ -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 @@ -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: @@ -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: @@ -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 } @@ -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 } @@ -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 } diff --git a/rfq/oracle_test.go b/rfq/oracle_test.go index 2faf8650a..796aca008 100644 --- a/rfq/oracle_test.go +++ b/rfq/oracle_test.go @@ -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. @@ -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. diff --git a/rfqmsg/buy_accept.go b/rfqmsg/buy_accept.go index 510287907..8ea39f1da 100644 --- a/rfqmsg/buy_accept.go +++ b/rfqmsg/buy_accept.go @@ -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 { diff --git a/rfqmsg/sell_accept.go b/rfqmsg/sell_accept.go index 379c4f446..81c3439fa 100644 --- a/rfqmsg/sell_accept.go +++ b/rfqmsg/sell_accept.go @@ -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 {