Skip to content

Commit

Permalink
Merge pull request #876 from sputn1ck/asset_loop_out_fixes
Browse files Browse the repository at this point in the history
Minor asset loop out fixes
  • Loading branch information
sputn1ck authored Jan 22, 2025
2 parents 684db85 + d0191d2 commit bb859c5
Show file tree
Hide file tree
Showing 10 changed files with 825 additions and 413 deletions.
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) (
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) {

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

return rfqrpc.UnmarshalFixedPoint(rfqrpcFP)
}
Loading

0 comments on commit bb859c5

Please sign in to comment.