Skip to content

Commit 9f7249f

Browse files
committed
asset: add function to get asset price
1 parent 3c79e9d commit 9f7249f

File tree

2 files changed

+110
-0
lines changed

2 files changed

+110
-0
lines changed

assets/client.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/btcsuite/btcd/btcutil"
12+
"github.com/lightninglabs/taproot-assets/rfqmath"
1213
"github.com/lightninglabs/taproot-assets/tapcfg"
1314
"github.com/lightninglabs/taproot-assets/taprpc"
1415
"github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
@@ -184,6 +185,75 @@ func (c *TapdClient) GetAssetName(ctx context.Context,
184185
return assetName, nil
185186
}
186187

188+
// GetAssetPrice returns the price of an asset in satoshis. NOTE: this currently
189+
// uses the rfq process for the asset price. A future implementation should
190+
// use a price oracle to not spam a peer.
191+
func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string,
192+
peerPubkey []byte, assetAmt uint64, paymentMaxAmt btcutil.Amount) (
193+
btcutil.Amount, error) {
194+
195+
// We'll allow a short rfq expiry as we'll only use this rfq to
196+
// gauge a price.
197+
rfqExpiry := time.Now().Add(time.Minute).Unix()
198+
199+
msatAmt := lnwire.NewMSatFromSatoshis(paymentMaxAmt)
200+
201+
// First we'll rfq a random peer for the asset.
202+
rfq, err := c.RfqClient.AddAssetSellOrder(
203+
ctx, &rfqrpc.AddAssetSellOrderRequest{
204+
AssetSpecifier: &rfqrpc.AssetSpecifier{
205+
Id: &rfqrpc.AssetSpecifier_AssetIdStr{
206+
AssetIdStr: assetID,
207+
},
208+
},
209+
PaymentMaxAmt: uint64(msatAmt),
210+
Expiry: uint64(rfqExpiry),
211+
TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()),
212+
PeerPubKey: peerPubkey,
213+
})
214+
if err != nil {
215+
return 0, err
216+
}
217+
if rfq == nil {
218+
return 0, fmt.Errorf("no RFQ response")
219+
}
220+
221+
if rfq.GetInvalidQuote() != nil {
222+
return 0, fmt.Errorf("peer %v sent an invalid quote response %v for "+
223+
"asset %v", peerPubkey, rfq.GetInvalidQuote(), assetID)
224+
}
225+
226+
if rfq.GetRejectedQuote() != nil {
227+
return 0, fmt.Errorf("peer %v rejected the quote request for "+
228+
"asset %v, %v", peerPubkey, assetID, rfq.GetRejectedQuote())
229+
}
230+
231+
acceptedRes := rfq.GetAcceptedQuote()
232+
if acceptedRes == nil {
233+
return 0, fmt.Errorf("no accepted quote")
234+
}
235+
236+
// We'll use the accepted quote to calculate the price.
237+
return getSatsFromAssetAmt(assetAmt, acceptedRes.BidAssetRate)
238+
}
239+
240+
// getSatsFromAssetAmt returns the amount in satoshis for the given asset amount
241+
// and asset rate.
242+
func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) (
243+
btcutil.Amount, error) {
244+
245+
rateFP, err := rfqrpc.UnmarshalFixedPoint(assetRate)
246+
if err != nil {
247+
return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err)
248+
}
249+
250+
assetUnits := rfqmath.NewBigIntFixedPoint(assetAmt, 0)
251+
252+
msatAmt := rfqmath.UnitsToMilliSatoshi(assetUnits, *rateFP)
253+
254+
return msatAmt.ToSatoshis(), nil
255+
}
256+
187257
// getPaymentMaxAmount returns the milisat amount we are willing to pay for the
188258
// payment.
189259
func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) (

assets/client_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"testing"
55

66
"github.com/btcsuite/btcd/btcutil"
7+
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
78
"github.com/lightningnetwork/lnd/lnwire"
9+
"github.com/stretchr/testify/require"
810
)
911

1012
func TestGetPaymentMaxAmount(t *testing.T) {
@@ -65,3 +67,41 @@ func TestGetPaymentMaxAmount(t *testing.T) {
6567
}
6668
}
6769
}
70+
71+
func TestGetSatsFromAssetAmt(t *testing.T) {
72+
tests := []struct {
73+
assetAmt uint64
74+
assetRate *rfqrpc.FixedPoint
75+
expected btcutil.Amount
76+
expectError bool
77+
}{
78+
{
79+
assetAmt: 1000,
80+
assetRate: &rfqrpc.FixedPoint{Coefficient: "100000", Scale: 0},
81+
expected: btcutil.Amount(1000000),
82+
expectError: false,
83+
},
84+
{
85+
assetAmt: 500000,
86+
assetRate: &rfqrpc.FixedPoint{Coefficient: "200000000", Scale: 0},
87+
expected: btcutil.Amount(250000),
88+
expectError: false,
89+
},
90+
{
91+
assetAmt: 0,
92+
assetRate: &rfqrpc.FixedPoint{Coefficient: "100000000", Scale: 0},
93+
expected: btcutil.Amount(0),
94+
expectError: false,
95+
},
96+
}
97+
98+
for _, test := range tests {
99+
result, err := getSatsFromAssetAmt(test.assetAmt, test.assetRate)
100+
if test.expectError {
101+
require.NotNil(t, err)
102+
} else {
103+
require.Nil(t, err)
104+
require.Equal(t, test.expected, result)
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)