From 9f7249f14ffdef342a9885166d17cb2bf2ee6c1c Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 17 Feb 2025 15:55:05 +0100 Subject: [PATCH] asset: add function to get asset price --- assets/client.go | 70 +++++++++++++++++++++++++++++++++++++++++++ assets/client_test.go | 40 +++++++++++++++++++++++++ 2 files changed, 110 insertions(+) diff --git a/assets/client.go b/assets/client.go index 9a80547e..ae810e6e 100644 --- a/assets/client.go +++ b/assets/client.go @@ -9,6 +9,7 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/tapcfg" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" @@ -184,6 +185,75 @@ func (c *TapdClient) GetAssetName(ctx context.Context, return assetName, nil } +// GetAssetPrice returns the price of an asset in satoshis. NOTE: this currently +// uses the rfq process for the asset price. A future implementation should +// use a price oracle to not spam a peer. +func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string, + peerPubkey []byte, assetAmt uint64, paymentMaxAmt btcutil.Amount) ( + btcutil.Amount, error) { + + // We'll allow a short rfq expiry as we'll only use this rfq to + // gauge a price. + rfqExpiry := time.Now().Add(time.Minute).Unix() + + msatAmt := lnwire.NewMSatFromSatoshis(paymentMaxAmt) + + // First we'll rfq a random peer for the asset. + rfq, err := c.RfqClient.AddAssetSellOrder( + ctx, &rfqrpc.AddAssetSellOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetIdStr{ + AssetIdStr: assetID, + }, + }, + PaymentMaxAmt: uint64(msatAmt), + Expiry: uint64(rfqExpiry), + TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()), + PeerPubKey: peerPubkey, + }) + if err != nil { + return 0, err + } + if rfq == nil { + return 0, fmt.Errorf("no RFQ response") + } + + if rfq.GetInvalidQuote() != nil { + return 0, fmt.Errorf("peer %v sent an invalid quote response %v for "+ + "asset %v", peerPubkey, rfq.GetInvalidQuote(), assetID) + } + + if rfq.GetRejectedQuote() != nil { + return 0, fmt.Errorf("peer %v rejected the quote request for "+ + "asset %v, %v", peerPubkey, assetID, rfq.GetRejectedQuote()) + } + + acceptedRes := rfq.GetAcceptedQuote() + if acceptedRes == nil { + return 0, fmt.Errorf("no accepted quote") + } + + // We'll use the accepted quote to calculate the price. + return getSatsFromAssetAmt(assetAmt, acceptedRes.BidAssetRate) +} + +// getSatsFromAssetAmt returns the amount in satoshis for the given asset amount +// and asset rate. +func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( + btcutil.Amount, error) { + + rateFP, err := rfqrpc.UnmarshalFixedPoint(assetRate) + if err != nil { + return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err) + } + + assetUnits := rfqmath.NewBigIntFixedPoint(assetAmt, 0) + + msatAmt := rfqmath.UnitsToMilliSatoshi(assetUnits, *rateFP) + + return msatAmt.ToSatoshis(), nil +} + // getPaymentMaxAmount returns the milisat amount we are willing to pay for the // payment. func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( diff --git a/assets/client_test.go b/assets/client_test.go index d783b9d5..6b791aaf 100644 --- a/assets/client_test.go +++ b/assets/client_test.go @@ -4,7 +4,9 @@ import ( "testing" "github.com/btcsuite/btcd/btcutil" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" ) func TestGetPaymentMaxAmount(t *testing.T) { @@ -65,3 +67,41 @@ func TestGetPaymentMaxAmount(t *testing.T) { } } } + +func TestGetSatsFromAssetAmt(t *testing.T) { + tests := []struct { + assetAmt uint64 + assetRate *rfqrpc.FixedPoint + expected btcutil.Amount + expectError bool + }{ + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{Coefficient: "100000", Scale: 0}, + expected: btcutil.Amount(1000000), + expectError: false, + }, + { + assetAmt: 500000, + assetRate: &rfqrpc.FixedPoint{Coefficient: "200000000", Scale: 0}, + expected: btcutil.Amount(250000), + expectError: false, + }, + { + assetAmt: 0, + assetRate: &rfqrpc.FixedPoint{Coefficient: "100000000", Scale: 0}, + expected: btcutil.Amount(0), + expectError: false, + }, + } + + for _, test := range tests { + result, err := getSatsFromAssetAmt(test.assetAmt, test.assetRate) + if test.expectError { + require.NotNil(t, err) + } else { + require.Nil(t, err) + require.Equal(t, test.expected, result) + } + } +}