Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minor asset loop out fixes #876

Merged
merged 4 commits into from
Jan 22, 2025
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
34 changes: 30 additions & 4 deletions assets/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc"
"github.com/lightninglabs/taproot-assets/taprpc/universerpc"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
Expand Down Expand Up @@ -105,9 +106,12 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context,
expiry int64, feeLimitMultiplier float64) (
*rfqrpc.PeerAcceptedSellQuote, error) {

feeLimit, err := lnrpc.UnmarshallAmt(
int64(satAmount)+int64(satAmount.MulF64(feeLimitMultiplier)), 0,
)
// paymentMaxAmt is the maximum amount we are willing to pay for the
// payment.
// E.g. on a 250k sats payment we'll multiply the sat amount by 1.2.
// The resulting maximum amount we're willing to pay is 300k sats.
// The response asset amount will be for those 300k sats.
paymentMaxAmt, err := getPaymentMaxAmount(satAmount, feeLimitMultiplier)
if err != nil {
return nil, err
}
Expand All @@ -120,7 +124,7 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context,
},
},
PeerPubKey: peerPubkey,
PaymentMaxAmt: uint64(feeLimit),
PaymentMaxAmt: uint64(paymentMaxAmt),
Expiry: uint64(expiry),
TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()),
})
Expand Down Expand Up @@ -180,6 +184,28 @@ func (c *TapdClient) GetAssetName(ctx context.Context,
return assetName, nil
}

// getPaymentMaxAmount returns the milisat amount we are willing to pay for the
// payment.
func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) (
lnwire.MilliSatoshi, error) {

if satAmount == 0 {
return 0, fmt.Errorf("satAmount cannot be zero")
}
if feeLimitMultiplier < 1 {
return 0, fmt.Errorf("feeLimitMultiplier must be at least 1")
}

// paymentMaxAmt is the maximum amount we are willing to pay for the
// payment.
// E.g. on a 250k sats payment we'll multiply the sat amount by 1.2.
// The resulting maximum amount we're willing to pay is 300k sats.
// The response asset amount will be for those 300k sats.
return lnrpc.UnmarshallAmt(
int64(satAmount.MulF64(feeLimitMultiplier)), 0,
)
}

func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) {
// Load the specified TLS certificate and build transport credentials.
creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "")
Expand Down
67 changes: 67 additions & 0 deletions assets/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package assets

import (
"testing"

"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/lnwire"
)

func TestGetPaymentMaxAmount(t *testing.T) {
tests := []struct {
satAmount btcutil.Amount
feeLimitMultiplier float64
expectedAmount lnwire.MilliSatoshi
expectError bool
}{
{
satAmount: btcutil.Amount(250000),
feeLimitMultiplier: 1.2,
expectedAmount: lnwire.MilliSatoshi(300000000),
expectError: false,
},
{
satAmount: btcutil.Amount(100000),
feeLimitMultiplier: 1.5,
expectedAmount: lnwire.MilliSatoshi(150000000),
expectError: false,
},
{
satAmount: btcutil.Amount(50000),
feeLimitMultiplier: 2.0,
expectedAmount: lnwire.MilliSatoshi(100000000),
expectError: false,
},
{
satAmount: btcutil.Amount(0),
feeLimitMultiplier: 1.2,
expectedAmount: lnwire.MilliSatoshi(0),
expectError: true,
},
{
satAmount: btcutil.Amount(250000),
feeLimitMultiplier: 0.8,
expectedAmount: lnwire.MilliSatoshi(0),
expectError: true,
},
}

for _, test := range tests {
result, err := getPaymentMaxAmount(
test.satAmount, test.feeLimitMultiplier,
)
if test.expectError {
if err == nil {
t.Fatalf("expected error but got none")
}
} else {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != test.expectedAmount {
t.Fatalf("expected %v, got %v",
test.expectedAmount, result)
}
}
}
}
121 changes: 77 additions & 44 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/utils"
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"google.golang.org/grpc"
Expand Down Expand Up @@ -661,54 +662,12 @@ func (s *Client) LoopOutQuote(ctx context.Context,
// If we use an Asset we'll rfq to get the asset amounts to use for
// the swap.
if request.AssetRFQRequest != nil {
rfqReq := request.AssetRFQRequest
if rfqReq.Expiry == 0 {
rfqReq.Expiry = time.Now().Add(defaultRFQExpiry).Unix()
}

if rfqReq.MaxLimitMultiplier == 0 {
rfqReq.MaxLimitMultiplier = defaultRFQMaxLimitMultiplier
}

// First we'll get the prepay rfq.
prepayRfq, err := s.assetClient.GetRfqForAsset(
ctx, quote.PrepayAmount, rfqReq.AssetId,
rfqReq.AssetEdgeNode, rfqReq.Expiry,
rfqReq.MaxLimitMultiplier,
)
if err != nil {
return nil, err
}

// The actual invoice swap amount is the requested amount plus
// the swap fee minus the prepay amount.
invoiceAmt := request.Amount + quote.SwapFee -
quote.PrepayAmount

swapRfq, err := s.assetClient.GetRfqForAsset(
ctx, invoiceAmt, rfqReq.AssetId,
rfqReq.AssetEdgeNode, rfqReq.Expiry,
rfqReq.MaxLimitMultiplier,
)
rfq, err := s.getAssetRfq(ctx, loopOutQuote, request)
if err != nil {
return nil, err
}

// We'll also want the asset name to verify for the client.
assetName, err := s.assetClient.GetAssetName(
ctx, rfqReq.AssetId,
)
if err != nil {
return nil, err
}

loopOutQuote.LoopOutRfq = &LoopOutRfq{
PrepayRfqId: prepayRfq.Id,
PrepayAssetAmt: prepayRfq.AssetAmount,
SwapRfqId: swapRfq.Id,
SwapAssetAmt: swapRfq.AssetAmount,
AssetName: assetName,
}
loopOutQuote.LoopOutRfq = rfq
}

return loopOutQuote, nil
Expand Down Expand Up @@ -1000,3 +959,77 @@ func (s *Client) AbandonSwap(ctx context.Context,

return nil
}

// getAssetRfq returns a prepay and swap rfq for the asset swap.
func (s *Client) getAssetRfq(ctx context.Context, quote *LoopOutQuote,
request *LoopOutQuoteRequest) (*LoopOutRfq, error) {

if s.assetClient == nil {
return nil, errors.New("asset client must be set " +
"when trying to loop out with an asset")
}
rfqReq := request.AssetRFQRequest
if rfqReq.Expiry == 0 {
rfqReq.Expiry = time.Now().Add(defaultRFQExpiry).Unix()
}

if rfqReq.MaxLimitMultiplier == 0 {
rfqReq.MaxLimitMultiplier = defaultRFQMaxLimitMultiplier
}

// First we'll get the prepay rfq.
prepayRfq, err := s.assetClient.GetRfqForAsset(
ctx, quote.PrepayAmount, rfqReq.AssetId,
rfqReq.AssetEdgeNode, rfqReq.Expiry,
rfqReq.MaxLimitMultiplier,
)
if err != nil {
return nil, err
}

prepayAssetRate, err := rfqrpc.UnmarshalFixedPoint(
prepayRfq.BidAssetRate,
)
if err != nil {
return nil, err
}

// The actual invoice swap amount is the requested amount plus
// the swap fee minus the prepay amount.
invoiceAmt := request.Amount + quote.SwapFee -
quote.PrepayAmount

swapRfq, err := s.assetClient.GetRfqForAsset(
ctx, invoiceAmt, rfqReq.AssetId,
rfqReq.AssetEdgeNode, rfqReq.Expiry,
rfqReq.MaxLimitMultiplier,
)
if err != nil {
return nil, err
}

swapAssetRate, err := rfqrpc.UnmarshalFixedPoint(
swapRfq.BidAssetRate,
)
if err != nil {
return nil, err
}

// We'll also want the asset name to verify for the client.
assetName, err := s.assetClient.GetAssetName(
ctx, rfqReq.AssetId,
)
if err != nil {
return nil, err
}

return &LoopOutRfq{
PrepayRfqId: prepayRfq.Id,
MaxPrepayAssetAmt: prepayRfq.AssetAmount,
PrepayAssetRate: prepayAssetRate,
SwapRfqId: swapRfq.Id,
MaxSwapAssetAmt: swapRfq.AssetAmount,
SwapAssetRate: swapAssetRate,
AssetName: assetName,
}, nil
}
7 changes: 7 additions & 0 deletions cmd/loop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ const (
// Amount: 50 USD
assetAmtFmt = "%-36s %12d %s\n"

// rateFmt formats an exchange rate into a one line string, intended to
// prettify the terminal output. For Instance,
// fmt.Printf(f, "Exchange rate:", rate, "USD")
// prints out as,
// Exchange rate: 0.0002 USD/SAT
rateFmt = "%-36s %12.4f %s/SAT\n"

// blkFmt formats the number of blocks into a one line string, intended
// to prettify the terminal output. For Instance,
// fmt.Printf(f, "Conf target", target)
Expand Down
59 changes: 57 additions & 2 deletions cmd/loop/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/taproot-assets/rfqmath"
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli"
)
Expand Down Expand Up @@ -268,8 +271,20 @@ func printQuoteOutResp(req *looprpc.QuoteRequest,
totalFee := resp.HtlcSweepFeeSat + resp.SwapFeeSat

if resp.AssetRfqInfo != nil {
assetAmtSwap, err := getAssetAmt(
req.Amt, resp.AssetRfqInfo.SwapAssetRate,
)
if err != nil {
fmt.Printf("Error converting asset amount: %v\n", err)
return
}
exchangeRate := float64(assetAmtSwap) / float64(req.Amt)
fmt.Printf(assetAmtFmt, "Send off-chain:",
resp.AssetRfqInfo.SwapAssetAmt,
assetAmtSwap, resp.AssetRfqInfo.AssetName)
fmt.Printf(rateFmt, "Exchange rate:",
exchangeRate, resp.AssetRfqInfo.AssetName)
fmt.Printf(assetAmtFmt, "Limit Send off-chain:",
resp.AssetRfqInfo.MaxSwapAssetAmt,
resp.AssetRfqInfo.AssetName)
} else {
fmt.Printf(satAmtFmt, "Send off-chain:", req.Amt)
Expand All @@ -288,8 +303,18 @@ func printQuoteOutResp(req *looprpc.QuoteRequest,
fmt.Printf(satAmtFmt, "Estimated total fee:", totalFee)
fmt.Println()
if resp.AssetRfqInfo != nil {
assetAmtPrepay, err := getAssetAmt(
resp.PrepayAmtSat, resp.AssetRfqInfo.PrepayAssetRate,
)
if err != nil {
fmt.Printf("Error converting asset amount: %v\n", err)
return
}
fmt.Printf(assetAmtFmt, "No show penalty (prepay):",
resp.AssetRfqInfo.PrepayAssetAmt,
assetAmtPrepay,
resp.AssetRfqInfo.AssetName)
fmt.Printf(assetAmtFmt, "Limit no show penalty (prepay):",
resp.AssetRfqInfo.MaxPrepayAssetAmt,
resp.AssetRfqInfo.AssetName)
} else {
fmt.Printf(satAmtFmt, "No show penalty (prepay):",
Expand All @@ -302,3 +327,33 @@ func printQuoteOutResp(req *looprpc.QuoteRequest,
time.Unix(int64(req.SwapPublicationDeadline), 0),
)
}

// getAssetAmt returns the asset amount for the given amount in satoshis and
// the asset rate.
func getAssetAmt(amt int64, assetRate *looprpc.FixedPoint) (
sputn1ck marked this conversation as resolved.
Show resolved Hide resolved
uint64, error) {

askAssetRate, err := unmarshalFixedPoint(assetRate)
if err != nil {
return 0, err
}

msatAmt := lnwire.MilliSatoshi((amt * 1000))

assetAmt := rfqmath.MilliSatoshiToUnits(msatAmt, *askAssetRate)

return assetAmt.ToUint64(), nil
}

// unmarshalFixedPoint converts an RPC FixedPoint to a BigIntFixedPoint.
func unmarshalFixedPoint(fp *looprpc.FixedPoint) (*rfqmath.BigIntFixedPoint,
error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I propose to reuse https://pkg.go.dev/github.com/lightninglabs/taproot-assets/taprpc/rfqrpc#UnmarshalFixedPoint
looprpc.FixedPoint is the same type as rfqrpc.FixedPoint (has the same fields).

Copy link
Member Author

Choose a reason for hiding this comment

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

This does not seem to work for proto messages,

image

Copy link
Collaborator

Choose a reason for hiding this comment

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

It is possible to create a struct manually filling the fields.

Copy link
Member Author

Choose a reason for hiding this comment

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

created a rfqrpc.FixedPoint struct and called rfqrpc.UnmarshalFixedPoint


// convert the looprpc.FixedPoint to a rfqrpc.FixedPoint
rfqrpcFP := &rfqrpc.FixedPoint{
Coefficient: fp.Coefficient,
Scale: fp.Scale,
}

return rfqrpc.UnmarshalFixedPoint(rfqrpcFP)
}
Loading
Loading