Skip to content

Commit 0c4b86b

Browse files
committed
assets: add basic asset client
1 parent 8062952 commit 0c4b86b

File tree

3 files changed

+384
-144
lines changed

3 files changed

+384
-144
lines changed

assets/client.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package assets
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"fmt"
7+
"math/big"
8+
"os"
9+
"sync"
10+
"time"
11+
12+
"github.com/btcsuite/btcd/btcutil"
13+
"github.com/lightninglabs/taproot-assets/rfqmath"
14+
"github.com/lightninglabs/taproot-assets/taprpc"
15+
"github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
16+
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
17+
"github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
18+
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
19+
"github.com/lightningnetwork/lnd/lnrpc"
20+
"github.com/lightningnetwork/lnd/macaroons"
21+
"google.golang.org/grpc"
22+
"google.golang.org/grpc/credentials"
23+
"gopkg.in/macaroon.v2"
24+
)
25+
26+
var (
27+
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(400 * 1024 * 1024)
28+
)
29+
30+
// TapdConfig is a struct that holds the configuration options to connect to a
31+
// taproot assets daemon.
32+
type TapdConfig struct {
33+
Host string `long:"host" description:"The host of the Tap daemon"`
34+
MacaroonPath string `long:"macaroonpath" description:"Path to the admin macaroon"`
35+
TLSPath string `long:"tlspath" description:"Path to the TLS certificate"`
36+
}
37+
38+
// DefaultTapdConfig returns a default configuration to connect to a taproot
39+
// assets daemon.
40+
func DefaultTapdConfig() *TapdConfig {
41+
return &TapdConfig{
42+
Host: "",
43+
MacaroonPath: "",
44+
TLSPath: "",
45+
}
46+
}
47+
48+
// TapdClient is a client for the Tap daemon.
49+
type TapdClient struct {
50+
sync.Mutex
51+
assetNameCache map[string]string
52+
cc *grpc.ClientConn
53+
taprpc.TaprootAssetsClient
54+
tapchannelrpc.TaprootAssetChannelsClient
55+
priceoraclerpc.PriceOracleClient
56+
rfqrpc.RfqClient
57+
universerpc.UniverseClient
58+
}
59+
60+
// NewTapdClient retusn a new taproot assets client.
61+
func NewTapdClient(config *TapdConfig) (*TapdClient, error) {
62+
// Create the client connection to the server.
63+
conn, err := getClientConn(config)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
// Create the TapdClient.
69+
client := &TapdClient{
70+
assetNameCache: make(map[string]string),
71+
cc: conn,
72+
TaprootAssetsClient: taprpc.NewTaprootAssetsClient(conn),
73+
TaprootAssetChannelsClient: tapchannelrpc.NewTaprootAssetChannelsClient(conn),
74+
PriceOracleClient: priceoraclerpc.NewPriceOracleClient(conn),
75+
RfqClient: rfqrpc.NewRfqClient(conn),
76+
UniverseClient: universerpc.NewUniverseClient(conn),
77+
}
78+
79+
return client, nil
80+
}
81+
82+
// Close closes the client connection to the server.
83+
func (c *TapdClient) Close() {
84+
c.cc.Close()
85+
}
86+
87+
// GetSatAmtFromRfq returns the amount in satoshis for the given asset amount
88+
// and pay rate.
89+
func GetSatAmtFromRfq(assetAmt btcutil.Amount,
90+
payRate *rfqrpc.FixedPoint) (btcutil.Amount, error) {
91+
92+
coefficient := new(big.Int)
93+
coefficient, ok := coefficient.SetString(payRate.Coefficient, 10)
94+
if !ok {
95+
return 0, fmt.Errorf("failed to parse coefficient %v",
96+
payRate.Coefficient)
97+
}
98+
99+
amt := rfqmath.FixedPointFromUint64[rfqmath.BigInt](
100+
uint64(assetAmt), 0,
101+
)
102+
103+
price := rfqmath.FixedPoint[rfqmath.BigInt]{
104+
Coefficient: rfqmath.NewBigInt(coefficient),
105+
Scale: uint8(payRate.Scale),
106+
}
107+
108+
msats := rfqmath.UnitsToMilliSatoshi(amt, price)
109+
return msats.ToSatoshis(), nil
110+
}
111+
112+
// GetRfqForAsset returns a RFQ for the given asset with the given amount and
113+
// to the given peer.
114+
func (c *TapdClient) GetRfqForAsset(ctx context.Context,
115+
satAmount btcutil.Amount, assetId, peerPubkey []byte) (
116+
*rfqrpc.PeerAcceptedSellQuote, error) {
117+
118+
// TODO(sputn1ck): magic value, should be configurable?
119+
feeLimit, err := lnrpc.UnmarshallAmt(
120+
int64(satAmount)+int64(satAmount.MulF64(1.1)), 0,
121+
)
122+
if err != nil {
123+
return nil, err
124+
}
125+
126+
// TODO(sputn1ck): magic value, should be configurable?
127+
expiry := time.Now().Add(1 * time.Hour).Unix()
128+
129+
rfq, err := c.RfqClient.AddAssetSellOrder(
130+
ctx, &rfqrpc.AddAssetSellOrderRequest{
131+
AssetSpecifier: &rfqrpc.AssetSpecifier{
132+
Id: &rfqrpc.AssetSpecifier_AssetId{
133+
AssetId: assetId,
134+
},
135+
},
136+
PeerPubKey: peerPubkey,
137+
PaymentMaxAmt: uint64(feeLimit),
138+
Expiry: uint64(expiry),
139+
TimeoutSeconds: 60,
140+
})
141+
if err != nil {
142+
return nil, err
143+
}
144+
if rfq.GetInvalidQuote() != nil {
145+
return nil, fmt.Errorf("invalid RFQ: %v", rfq.GetInvalidQuote())
146+
}
147+
if rfq.GetRejectedQuote() != nil {
148+
return nil, fmt.Errorf("rejected RFQ: %v",
149+
rfq.GetRejectedQuote())
150+
}
151+
152+
if rfq.GetAcceptedQuote() != nil {
153+
return rfq.GetAcceptedQuote(), nil
154+
}
155+
156+
return nil, fmt.Errorf("no accepted quote")
157+
}
158+
159+
// GetAssetName returns the human readable name of the asset.
160+
func (c *TapdClient) GetAssetName(ctx context.Context,
161+
assetId []byte) (string, error) {
162+
163+
c.Lock()
164+
defer c.Unlock()
165+
assetIdStr := hex.EncodeToString(assetId)
166+
if name, ok := c.assetNameCache[assetIdStr]; ok {
167+
return name, nil
168+
}
169+
170+
assetStats, err := c.UniverseClient.QueryAssetStats(
171+
ctx, &universerpc.AssetStatsQuery{
172+
AssetIdFilter: assetId,
173+
},
174+
)
175+
if err != nil {
176+
return "", err
177+
}
178+
179+
if len(assetStats.AssetStats) == 0 {
180+
return "", fmt.Errorf("asset not found")
181+
}
182+
183+
var assetName string
184+
185+
// If the asset belongs to a group, return the group name.
186+
if assetStats.AssetStats[0].GroupAnchor != nil {
187+
assetName = assetStats.AssetStats[0].GroupAnchor.AssetName
188+
} else {
189+
assetName = assetStats.AssetStats[0].Asset.AssetName
190+
}
191+
192+
c.assetNameCache[assetIdStr] = assetName
193+
194+
return assetName, nil
195+
}
196+
197+
func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) {
198+
// Load the specified TLS certificate and build transport credentials.
199+
creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "")
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
// Load the specified macaroon file.
205+
macBytes, err := os.ReadFile(config.MacaroonPath)
206+
if err != nil {
207+
return nil, err
208+
}
209+
mac := &macaroon.Macaroon{}
210+
if err := mac.UnmarshalBinary(macBytes); err != nil {
211+
return nil, err
212+
}
213+
214+
macaroon, err := macaroons.NewMacaroonCredential(mac)
215+
if err != nil {
216+
return nil, err
217+
}
218+
// Create the DialOptions with the macaroon credentials.
219+
opts := []grpc.DialOption{
220+
grpc.WithTransportCredentials(creds),
221+
grpc.WithPerRPCCredentials(macaroon),
222+
grpc.WithDefaultCallOptions(maxMsgRecvSize),
223+
}
224+
225+
// Dial the gRPC server.
226+
conn, err := grpc.Dial(config.Host, opts...)
227+
if err != nil {
228+
return nil, err
229+
}
230+
231+
return conn, nil
232+
}

0 commit comments

Comments
 (0)