Skip to content

Commit e5a4a16

Browse files
author
ffranr
authored
Merge pull request #835 from lightninglabs/rfq-add-sell-support
Add asset sell support to the RFQ service
2 parents 47e5533 + ba506bc commit e5a4a16

20 files changed

+2569
-192
lines changed

itest/rfq_test.go

Lines changed: 212 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import (
44
"context"
55
"fmt"
66
"math"
7+
"math/rand"
8+
"testing"
79
"time"
810

911
"github.com/btcsuite/btcd/btcutil"
1012
"github.com/btcsuite/btcd/wire"
13+
"github.com/lightninglabs/taproot-assets/rfq"
1114
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
1215
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
1316
"github.com/lightningnetwork/lnd/chainreg"
@@ -213,6 +216,157 @@ func testRfqAssetBuyHtlcIntercept(t *harnessTest) {
213216
require.NoError(t.t, err)
214217
}
215218

219+
// testRfqAssetSellHtlcIntercept tests RFQ negotiation, HTLC interception, and
220+
// validation between three peers. The RFQ negotiation is initiated by an asset
221+
// sell request.
222+
func testRfqAssetSellHtlcIntercept(t *harnessTest) {
223+
// Initialize a new test scenario.
224+
ts := newRfqTestScenario(t)
225+
226+
// Mint an asset with Alice's tapd node.
227+
rpcAssets := MintAssetsConfirmBatch(
228+
t.t, t.lndHarness.Miner.Client, ts.AliceTapd,
229+
[]*mintrpc.MintAssetRequest{issuableAssets[0]},
230+
)
231+
mintedAssetId := rpcAssets[0].AssetGenesis.AssetId
232+
233+
ctxb := context.Background()
234+
ctxt, cancel := context.WithTimeout(ctxb, defaultWaitTimeout)
235+
defer cancel()
236+
237+
// TODO(ffranr): Add an asset buy offer to Bob's tapd node. This will
238+
// allow Alice to sell the newly minted asset to Bob.
239+
240+
// Subscribe to Alice's RFQ events stream.
241+
aliceEventNtfns, err := ts.AliceTapd.SubscribeRfqEventNtfns(
242+
ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{},
243+
)
244+
require.NoError(t.t, err)
245+
246+
// Alice sends a sell order to Bob for some amount of the newly minted
247+
// asset.
248+
purchaseAssetAmt := uint64(200)
249+
askAmt := uint64(42000)
250+
sellOrderExpiry := uint64(time.Now().Add(24 * time.Hour).Unix())
251+
252+
_, err = ts.AliceTapd.AddAssetSellOrder(
253+
ctxt, &rfqrpc.AddAssetSellOrderRequest{
254+
AssetSpecifier: &rfqrpc.AssetSpecifier{
255+
Id: &rfqrpc.AssetSpecifier_AssetId{
256+
AssetId: mintedAssetId,
257+
},
258+
},
259+
MaxAssetAmount: purchaseAssetAmt,
260+
MinAsk: askAmt,
261+
Expiry: sellOrderExpiry,
262+
263+
// Here we explicitly specify Bob as the destination
264+
// peer for the sell order. This will prompt Alice's
265+
// tapd node to send a request for quote message to
266+
// Bob's node.
267+
PeerPubKey: ts.BobLnd.PubKey[:],
268+
},
269+
)
270+
require.NoError(t.t, err, "unable to upsert asset sell order")
271+
272+
// Wait until Alice receives an incoming sell quote accept message (sent
273+
// from Bob) RFQ event notification.
274+
BeforeTimeout(t.t, func() {
275+
event, err := aliceEventNtfns.Recv()
276+
require.NoError(t.t, err)
277+
278+
_, ok := event.Event.(*rfqrpc.RfqEvent_PeerAcceptedSellQuote)
279+
require.True(t.t, ok, "unexpected event: %v", event)
280+
}, defaultWaitTimeout)
281+
282+
// Alice should have received an accepted quote from Bob. This accepted
283+
// quote can be used by Alice to make a payment to Bob.
284+
acceptedQuotes, err := ts.AliceTapd.QueryPeerAcceptedQuotes(
285+
ctxt, &rfqrpc.QueryPeerAcceptedQuotesRequest{},
286+
)
287+
require.NoError(t.t, err, "unable to query accepted quotes")
288+
require.Len(t.t, acceptedQuotes.SellQuotes, 1)
289+
290+
acceptedQuote := acceptedQuotes.SellQuotes[0]
291+
292+
// Register to receive RFQ events from Bob's tapd node. We'll use this
293+
// to wait for Bob to receive the HTLC with the asset transfer specific
294+
// scid.
295+
bobEventNtfns, err := ts.BobTapd.SubscribeRfqEventNtfns(
296+
ctxb, &rfqrpc.SubscribeRfqEventNtfnsRequest{},
297+
)
298+
require.NoError(t.t, err)
299+
300+
// Carol generates and invoice for Alice to settle via Bob.
301+
addInvoiceResp := ts.CarolLnd.RPC.AddInvoice(&lnrpc.Invoice{
302+
ValueMsat: int64(askAmt),
303+
})
304+
invoice := ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash)
305+
306+
// Decode the payment request to get the payment address.
307+
payReq := ts.CarolLnd.RPC.DecodePayReq(invoice.PaymentRequest)
308+
309+
// We now need to construct a route for the payment from Alice to Carol.
310+
// The route will be Alice -> Bob -> Carol. We'll add the accepted quote
311+
// ID as a record to the custom records field of the route's first hop.
312+
// This will allow Bob to validate the payment against the accepted
313+
// quote.
314+
routeBuildRequest := routerrpc.BuildRouteRequest{
315+
AmtMsat: int64(askAmt),
316+
HopPubkeys: [][]byte{
317+
ts.BobLnd.PubKey[:],
318+
ts.CarolLnd.PubKey[:],
319+
},
320+
PaymentAddr: payReq.PaymentAddr,
321+
}
322+
routeBuildResp := ts.AliceLnd.RPC.BuildRoute(&routeBuildRequest)
323+
324+
// Add the accepted quote ID as a record to the custom records field of
325+
// the route's first hop.
326+
aliceBobHop := routeBuildResp.Route.Hops[0]
327+
if aliceBobHop.CustomRecords == nil {
328+
aliceBobHop.CustomRecords = make(map[uint64][]byte)
329+
}
330+
331+
aliceBobHop.CustomRecords[rfq.LnCustomRecordType] =
332+
acceptedQuote.Id[:]
333+
334+
// Update the route with the modified first hop.
335+
routeBuildResp.Route.Hops[0] = aliceBobHop
336+
337+
// Send the payment to the route.
338+
t.Log("Alice paying invoice")
339+
routeReq := routerrpc.SendToRouteRequest{
340+
PaymentHash: invoice.RHash,
341+
Route: routeBuildResp.Route,
342+
}
343+
sendAttempt := ts.AliceLnd.RPC.SendToRouteV2(&routeReq)
344+
require.Equal(t.t, lnrpc.HTLCAttempt_SUCCEEDED, sendAttempt.Status)
345+
346+
// At this point Bob should have received a HTLC with the asset transfer
347+
// specific scid. We'll wait for Bob to publish an accept HTLC event and
348+
// then validate it against the accepted quote.
349+
t.Log("Waiting for Bob to receive HTLC")
350+
BeforeTimeout(t.t, func() {
351+
event, err := bobEventNtfns.Recv()
352+
require.NoError(t.t, err)
353+
354+
_, ok := event.Event.(*rfqrpc.RfqEvent_AcceptHtlc)
355+
require.True(t.t, ok, "unexpected event: %v", event)
356+
}, defaultWaitTimeout)
357+
358+
// Confirm that Carol receives the lightning payment from Alice via Bob.
359+
invoice = ts.CarolLnd.RPC.LookupInvoice(addInvoiceResp.RHash)
360+
require.Equal(t.t, invoice.State, lnrpc.Invoice_SETTLED)
361+
362+
// Close event notification streams.
363+
err = aliceEventNtfns.CloseSend()
364+
require.NoError(t.t, err)
365+
366+
err = bobEventNtfns.CloseSend()
367+
require.NoError(t.t, err)
368+
}
369+
216370
// newLndNode creates a new lnd node with the given name and funds its wallet
217371
// with the specified outputs.
218372
func newLndNode(name string, outputFunds []btcutil.Amount,
@@ -286,10 +440,15 @@ func newRfqTestScenario(t *harnessTest) *rfqTestScenario {
286440
outputFunds[i] = fundAmount
287441
}
288442

443+
// Generate a unique name for each new node.
444+
aliceName := genRandomNodeName("AliceLnd")
445+
bobName := genRandomNodeName("BobLnd")
446+
carolName := genRandomNodeName("CarolLnd")
447+
289448
// Create three new nodes.
290-
aliceLnd := newLndNode("AliceLnd", outputFunds[:], t.lndHarness)
291-
bobLnd := newLndNode("BobLnd", outputFunds[:], t.lndHarness)
292-
carolLnd := newLndNode("CarolLnd", outputFunds[:], t.lndHarness)
449+
aliceLnd := newLndNode(aliceName, outputFunds[:], t.lndHarness)
450+
bobLnd := newLndNode(bobName, outputFunds[:], t.lndHarness)
451+
carolLnd := newLndNode(carolName, outputFunds[:], t.lndHarness)
293452

294453
// Now we want to wait for the nodes to catch up.
295454
t.lndHarness.WaitForBlockchainSync(aliceLnd)
@@ -347,12 +506,59 @@ func newRfqTestScenario(t *harnessTest) *rfqTestScenario {
347506

348507
// Cleanup cleans up the test scenario.
349508
func (s *rfqTestScenario) Cleanup() {
350-
// Close the LND channels.
351-
s.testHarness.lndHarness.CloseChannel(s.AliceLnd, s.AliceBobChannel)
352-
s.testHarness.lndHarness.CloseChannel(s.BobLnd, s.BobCarolChannel)
509+
s.testHarness.t.Log("Cleaning up test scenario")
353510

354511
// Stop the tapd nodes.
355512
require.NoError(s.testHarness.t, s.AliceTapd.stop(!*noDelete))
356513
require.NoError(s.testHarness.t, s.BobTapd.stop(!*noDelete))
357514
require.NoError(s.testHarness.t, s.CarolTapd.stop(!*noDelete))
515+
516+
// Kill the LND nodes in the test harness node manager. If we don't
517+
// perform this step the LND test harness node manager will continue to
518+
// run the nodes in the background as "active nodes".
519+
s.testHarness.lndHarness.KillNode(s.AliceLnd)
520+
s.testHarness.lndHarness.KillNode(s.BobLnd)
521+
s.testHarness.lndHarness.KillNode(s.CarolLnd)
522+
}
523+
524+
// randomString generates a random string of the given length.
525+
func randomString(randStrLen int) string {
526+
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
527+
b := make([]byte, randStrLen)
528+
for i := range b {
529+
b[i] = letters[rand.Intn(len(letters))]
530+
}
531+
return string(b)
532+
}
533+
534+
// genRandomNodeName generates a random node name by appending a random string
535+
// to the given base name.
536+
func genRandomNodeName(baseName string) string {
537+
return fmt.Sprintf("%s-%s", baseName, randomString(8))
538+
}
539+
540+
// BeforeTimeout executes a function in a goroutine with a timeout. It waits for
541+
// the function to finish or for the timeout to expire, whichever happens first.
542+
// If the function exceeds the timeout, it logs a test error.
543+
func BeforeTimeout(t *testing.T, targetFunc func(),
544+
timeout time.Duration) {
545+
546+
// Create a channel to signal when the target function has completed.
547+
targetExecComplete := make(chan bool, 1)
548+
549+
// Execute the target function in a goroutine.
550+
go func() {
551+
targetFunc()
552+
targetExecComplete <- true
553+
}()
554+
555+
// Wait for the target function to complete or timeout.
556+
select {
557+
case <-targetExecComplete:
558+
return
559+
560+
case <-time.After(timeout):
561+
t.Errorf("targetFunc did not complete within timeout: %v",
562+
timeout)
563+
}
358564
}

itest/test_list_on_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ var testCases = []*testCase{
251251
name: "rfq asset buy htlc intercept",
252252
test: testRfqAssetBuyHtlcIntercept,
253253
},
254+
{
255+
name: "rfq asset sell htlc intercept",
256+
test: testRfqAssetSellHtlcIntercept,
257+
},
258+
254259
{
255260
name: "multi signature on all levels",
256261
test: testMultiSignature,

perms/perms.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ var (
224224
Entity: "rfq",
225225
Action: "write",
226226
}},
227+
"/rfqrpc.Rfq/AddAssetSellOrder": {{
228+
Entity: "rfq",
229+
Action: "write",
230+
}},
227231
"/rfqrpc.Rfq/AddAssetSellOffer": {{
228232
Entity: "rfq",
229233
Action: "write",

0 commit comments

Comments
 (0)