Skip to content
Merged
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
107 changes: 91 additions & 16 deletions assets/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"context"
"encoding/hex"
"fmt"
"math"
"math/big"
"os"
"path/filepath"
"sync"
"time"

"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/taproot-assets/rfqmath"
"github.com/lightninglabs/taproot-assets/rpcutils"
"github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
Expand Down Expand Up @@ -78,14 +79,19 @@ type TapdClient struct {
rfqrpc.RfqClient
universerpc.UniverseClient

cfg *TapdConfig
assetNameCache map[string]string
assetNameMutex sync.Mutex
cc *grpc.ClientConn
rfqTimeoutSeconds uint32
assetNameCache map[string]string
assetNameMutex sync.RWMutex
cc *grpc.ClientConn
}

// NewTapdClient returns a new taproot assets client.
func NewTapdClient(config *TapdConfig) (*TapdClient, error) {
rfqTimeoutSeconds, err := getRfqTimeoutSeconds(config.RFQtimeout)
if err != nil {
return nil, err
}

// Create the client connection to the server.
conn, err := getClientConn(config)
if err != nil {
Expand All @@ -96,7 +102,7 @@ func NewTapdClient(config *TapdConfig) (*TapdClient, error) {
client := &TapdClient{
assetNameCache: make(map[string]string),
cc: conn,
cfg: config,
rfqTimeoutSeconds: rfqTimeoutSeconds,
TaprootAssetsClient: taprpc.NewTaprootAssetsClient(conn),
TaprootAssetChannelsClient: tapchannelrpc.NewTaprootAssetChannelsClient(conn),
PriceOracleClient: priceoraclerpc.NewPriceOracleClient(conn),
Expand Down Expand Up @@ -139,7 +145,7 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context,
PeerPubKey: peerPubkey,
PaymentMaxAmt: uint64(paymentMaxAmt),
Expiry: uint64(expiry),
TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()),
TimeoutSeconds: c.rfqTimeoutSeconds,
})
if err != nil {
return nil, err
Expand All @@ -152,21 +158,26 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context,
rfq.GetRejectedQuote())
}

if rfq.GetAcceptedQuote() != nil {
return rfq.GetAcceptedQuote(), nil
acceptedQuote := rfq.GetAcceptedQuote()
if acceptedQuote == nil {
return nil, fmt.Errorf("no accepted quote")
}

return nil, fmt.Errorf("no accepted quote")
_, err = unmarshalAssetRate(acceptedQuote.BidAssetRate)
if err != nil {
return nil, fmt.Errorf("invalid accepted quote asset rate: %w",
err)
}

return acceptedQuote, nil
}

// GetAssetName returns the human-readable name of the asset.
func (c *TapdClient) GetAssetName(ctx context.Context,
assetId []byte) (string, error) {

c.assetNameMutex.Lock()
defer c.assetNameMutex.Unlock()
assetIdStr := hex.EncodeToString(assetId)
if name, ok := c.assetNameCache[assetIdStr]; ok {
if name, ok := c.getCachedAssetName(assetIdStr); ok {
return name, nil
}

Expand All @@ -192,11 +203,28 @@ func (c *TapdClient) GetAssetName(ctx context.Context,
assetName = assetStats.AssetStats[0].Asset.AssetName
}

c.assetNameCache[assetIdStr] = assetName
c.cacheAssetName(assetIdStr, assetName)

return assetName, nil
}

// getCachedAssetName returns an asset name from the cache.
func (c *TapdClient) getCachedAssetName(assetID string) (string, bool) {
c.assetNameMutex.RLock()
defer c.assetNameMutex.RUnlock()

name, ok := c.assetNameCache[assetID]
return name, ok
}

// cacheAssetName adds an asset name to the cache.
func (c *TapdClient) cacheAssetName(assetID, name string) {
c.assetNameMutex.Lock()
defer c.assetNameMutex.Unlock()

c.assetNameCache[assetID] = name
}

// 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.
Expand All @@ -220,7 +248,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string,
},
PaymentMaxAmt: uint64(msatAmt),
Expiry: uint64(rfqExpiry),
TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()),
TimeoutSeconds: c.rfqTimeoutSeconds,
PeerPubKey: peerPubkey,
})
if err != nil {
Expand Down Expand Up @@ -254,7 +282,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string,
func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) (
btcutil.Amount, error) {

rateFP, err := rpcutils.UnmarshalRfqFixedPoint(assetRate)
rateFP, err := unmarshalAssetRate(assetRate)
if err != nil {
return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err)
}
Expand All @@ -266,6 +294,33 @@ func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) (
return msatAmt.ToSatoshis(), nil
}

// unmarshalAssetRate validates and converts an RPC asset rate to the fixed
// point representation used for RFQ arithmetic.
func unmarshalAssetRate(assetRate *rfqrpc.FixedPoint) (
*rfqmath.BigIntFixedPoint, error) {

if assetRate == nil {
return nil, fmt.Errorf("asset rate cannot be nil")
}
if assetRate.Scale > math.MaxUint8 {
return nil, fmt.Errorf("scale value overflow: %v", assetRate.Scale)
}

coefficient, ok := new(big.Int).SetString(assetRate.Coefficient, 10)
if !ok {
return nil, fmt.Errorf("invalid asset rate coefficient: %q",
assetRate.Coefficient)
}
if coefficient.Sign() <= 0 {
return nil, fmt.Errorf("asset rate coefficient must be positive")
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We need a similar hardening for GetRfqForAsset. Here is an example how to fix it: https://gist.github.com/starius/c23dd24330c3d36c9e852d2f4596313f

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

extracted the checks and re-used it for GetFrqForAsset.


return &rfqmath.BigIntFixedPoint{
Coefficient: rfqmath.NewBigInt(coefficient),
Scale: uint8(assetRate.Scale),
}, nil
}

// getPaymentMaxAmount returns the milisat amount we are willing to pay for the
// payment.
func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) (
Expand All @@ -288,6 +343,26 @@ func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) (
)
}

// getRfqTimeoutSeconds converts the configured RFQ timeout to the whole
// seconds accepted by tapd. Fractional seconds are rounded up so tapd's
// timeout is never shorter than the configured duration.
func getRfqTimeoutSeconds(timeout time.Duration) (uint32, error) {
if timeout <= 0 {
return 0, fmt.Errorf("RFQ timeout must be greater than zero")
}

seconds := timeout / time.Second
if timeout%time.Second != 0 {
seconds++
}
if seconds > time.Duration(math.MaxUint32) {
return 0, fmt.Errorf("RFQ timeout exceeds maximum of %v seconds",
uint64(math.MaxUint32))
}

return uint32(seconds), nil
}

func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) {
// Load the specified TLS certificate and build transport credentials.
creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "")
Expand Down
Loading
Loading