Skip to content

Commit 462e07c

Browse files
committed
rfq: add unit tests for HandleIncoming*Accept methods
Add unit tests for the negotiator subservice methods HandleIncomingBuyAccept and HandleIncomingSellAccept.
1 parent 385e9d1 commit 462e07c

File tree

1 file changed

+385
-0
lines changed

1 file changed

+385
-0
lines changed

rfq/negotiator_test.go

Lines changed: 385 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
package rfq
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/lightninglabs/taproot-assets/asset"
8+
"github.com/lightninglabs/taproot-assets/fn"
9+
"github.com/lightninglabs/taproot-assets/rfqmath"
10+
"github.com/lightninglabs/taproot-assets/rfqmsg"
11+
"github.com/lightningnetwork/lnd/lnwire"
12+
"github.com/lightningnetwork/lnd/routing/route"
13+
"github.com/stretchr/testify/mock"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
// testCaseIncomingSellAccept is a test case for the handling of an incoming
18+
// sell accept message.
19+
type testCaseIncomingSellAccept struct {
20+
// name is the name of the test case.
21+
name string
22+
23+
// incomingSellAcceptRate is the rate in the incoming sell accept
24+
// message.
25+
incomingSellAcceptRate rfqmsg.AssetRate
26+
27+
// priceOracleAskPrice is the rate returned by the price oracle.
28+
priceOracleAskPrice rfqmsg.AssetRate
29+
30+
// acceptPriceDeviationPpm is the acceptable price deviation in ppm.
31+
acceptPriceDeviationPpm uint64
32+
33+
// quoteRespStatus is the expected status of the quote check.
34+
quoteRespStatus fn.Option[QuoteRespStatus]
35+
}
36+
37+
// assertIncomingSellAcceptTestCase asserts the handling of an incoming sell
38+
// accept message for a test case.
39+
func assertIncomingSellAcceptTestCase(
40+
t *testing.T, tc testCaseIncomingSellAccept) {
41+
42+
// Create a mock price oracle.
43+
mockPriceOracle := &MockPriceOracle{}
44+
45+
// Register an expected call and response for price oracle method
46+
// QueryAskPrice.
47+
mockPriceOracle.On(
48+
"QueryAskPrice", mock.Anything, mock.Anything,
49+
mock.Anything, mock.Anything, mock.Anything,
50+
).Return(
51+
&OracleResponse{
52+
AssetRate: tc.priceOracleAskPrice,
53+
}, nil,
54+
)
55+
56+
// Define sell request and sell accept messages.
57+
var (
58+
assetSpecifier = asset.NewSpecifierFromId(asset.ID{1, 2, 3})
59+
peerID = route.Vertex{1, 2, 3}
60+
msgID = rfqmsg.ID{1, 2, 3}
61+
)
62+
63+
sellRequest := rfqmsg.SellRequest{
64+
Peer: peerID,
65+
ID: msgID,
66+
AssetSpecifier: assetSpecifier,
67+
PaymentMaxAmt: lnwire.MilliSatoshi(1000),
68+
AssetRateHint: fn.None[rfqmsg.AssetRate](),
69+
}
70+
71+
sellAccept := rfqmsg.SellAccept{
72+
Request: sellRequest,
73+
AssetRate: tc.incomingSellAcceptRate,
74+
}
75+
76+
// Create the negotiator.
77+
errChan := make(chan error, 1)
78+
negotiator, err := NewNegotiator(NegotiatorCfg{
79+
PriceOracle: mockPriceOracle,
80+
OutgoingMessages: make(chan rfqmsg.OutgoingMsg, 1),
81+
AcceptPriceDeviationPpm: tc.acceptPriceDeviationPpm,
82+
ErrChan: errChan,
83+
})
84+
require.NoError(t, err)
85+
86+
// Define the finalise callback function.
87+
finalise := func(msg rfqmsg.SellAccept,
88+
event fn.Option[InvalidQuoteRespEvent]) {
89+
90+
// If the actual event is none and the expected status is none,
91+
// then we don't need to check anything.
92+
if event.IsNone() && tc.quoteRespStatus.IsNone() {
93+
return
94+
}
95+
96+
require.Equal(t, tc.quoteRespStatus.IsSome(), event.IsSome())
97+
98+
// Extract the actual event status.
99+
var actualEventStatus QuoteRespStatus
100+
event.WhenSome(func(e InvalidQuoteRespEvent) {
101+
actualEventStatus = e.Status
102+
})
103+
104+
// Extract the expected event status.
105+
var expectedStatus QuoteRespStatus
106+
tc.quoteRespStatus.WhenSome(func(e QuoteRespStatus) {
107+
expectedStatus = e
108+
})
109+
110+
// Ensure that the actual and expected event statuses are equal.
111+
require.Equal(t, expectedStatus, actualEventStatus)
112+
}
113+
114+
// Handle the incoming sell accept message.
115+
negotiator.HandleIncomingSellAccept(sellAccept, finalise)
116+
117+
// Check that there are no errors.
118+
select {
119+
case err := <-errChan:
120+
t.Fatalf("unexpected error: %v", err)
121+
default:
122+
}
123+
124+
// Wait for the negotiator to finish.
125+
negotiator.Wg.Wait()
126+
}
127+
128+
// TestHandleIncomingSellAccept tests the handling of an incoming sell accept
129+
// message.
130+
func TestHandleIncomingSellAccept(t *testing.T) {
131+
defaultQuoteExpiry := time.Now().Add(time.Hour)
132+
133+
testCases := []testCaseIncomingSellAccept{
134+
{
135+
name: "accept price just within bounds 1",
136+
incomingSellAcceptRate: rfqmsg.AssetRate{
137+
Rate: rfqmath.NewBigIntFixedPoint(1000, 0),
138+
Expiry: defaultQuoteExpiry,
139+
},
140+
priceOracleAskPrice: rfqmsg.AssetRate{
141+
Rate: rfqmath.NewBigIntFixedPoint(1052, 0),
142+
Expiry: defaultQuoteExpiry,
143+
},
144+
acceptPriceDeviationPpm: DefaultAcceptPriceDeviationPpm,
145+
},
146+
{
147+
name: "accept price just within bounds 2",
148+
incomingSellAcceptRate: rfqmsg.AssetRate{
149+
Rate: rfqmath.NewBigIntFixedPoint(1000, 0),
150+
Expiry: defaultQuoteExpiry,
151+
},
152+
priceOracleAskPrice: rfqmsg.AssetRate{
153+
Rate: rfqmath.NewBigIntFixedPoint(950, 0),
154+
Expiry: defaultQuoteExpiry,
155+
},
156+
acceptPriceDeviationPpm: DefaultAcceptPriceDeviationPpm,
157+
},
158+
{
159+
name: "accept price outside bounds, higher than oracle",
160+
incomingSellAcceptRate: rfqmsg.AssetRate{
161+
Rate: rfqmath.NewBigIntFixedPoint(8000, 0),
162+
Expiry: defaultQuoteExpiry,
163+
},
164+
priceOracleAskPrice: rfqmsg.AssetRate{
165+
Rate: rfqmath.NewBigIntFixedPoint(1000, 0),
166+
Expiry: defaultQuoteExpiry,
167+
},
168+
acceptPriceDeviationPpm: DefaultAcceptPriceDeviationPpm,
169+
quoteRespStatus: fn.Some[QuoteRespStatus](
170+
InvalidAssetRatesQuoteRespStatus,
171+
),
172+
},
173+
{
174+
name: "accept price outside bounds, lower than oracle",
175+
incomingSellAcceptRate: rfqmsg.AssetRate{
176+
Rate: rfqmath.NewBigIntFixedPoint(1000, 0),
177+
Expiry: defaultQuoteExpiry,
178+
},
179+
priceOracleAskPrice: rfqmsg.AssetRate{
180+
Rate: rfqmath.NewBigIntFixedPoint(8000, 0),
181+
Expiry: defaultQuoteExpiry,
182+
},
183+
acceptPriceDeviationPpm: DefaultAcceptPriceDeviationPpm,
184+
quoteRespStatus: fn.Some[QuoteRespStatus](
185+
InvalidAssetRatesQuoteRespStatus,
186+
),
187+
},
188+
}
189+
190+
for idx := range testCases {
191+
tc := testCases[idx]
192+
193+
success := t.Run(tc.name, func(t *testing.T) {
194+
assertIncomingSellAcceptTestCase(t, tc)
195+
})
196+
if !success {
197+
break
198+
}
199+
}
200+
}
201+
202+
// testCaseIncomingBuyAccept is a test case for the handling of an incoming
203+
// buy accept message.
204+
type testCaseIncomingBuyAccept struct {
205+
// name is the name of the test case.
206+
name string
207+
208+
// incomingBuyAcceptRate is the rate in the incoming buy accept
209+
// message.
210+
incomingBuyAcceptRate rfqmsg.AssetRate
211+
212+
// priceOracleBidPrice is the rate returned by the price oracle.
213+
priceOracleBidPrice rfqmsg.AssetRate
214+
215+
// acceptPriceDeviationPpm is the acceptable price deviation in ppm.
216+
acceptPriceDeviationPpm uint64
217+
218+
// quoteRespStatus is the expected status of the quote check.
219+
quoteRespStatus fn.Option[QuoteRespStatus]
220+
}
221+
222+
// assertIncomingBuyAcceptTestCase asserts the handling of an incoming buy
223+
// accept message for a test case.
224+
func assertIncomingBuyAcceptTestCase(
225+
t *testing.T, tc testCaseIncomingBuyAccept) {
226+
227+
// Create a mock price oracle.
228+
mockPriceOracle := &MockPriceOracle{}
229+
230+
// Register an expected call and response for price oracle method
231+
// QueryBidPrice.
232+
mockPriceOracle.On(
233+
"QueryBidPrice", mock.Anything, mock.Anything,
234+
mock.Anything, mock.Anything, mock.Anything,
235+
).Return(
236+
&OracleResponse{
237+
AssetRate: tc.priceOracleBidPrice,
238+
}, nil,
239+
)
240+
241+
// Define buy request and buy accept messages.
242+
var (
243+
assetSpecifier = asset.NewSpecifierFromId(asset.ID{1, 2, 3})
244+
peerID = route.Vertex{1, 2, 3}
245+
msgID = rfqmsg.ID{1, 2, 3}
246+
)
247+
248+
buyRequest := rfqmsg.BuyRequest{
249+
Peer: peerID,
250+
ID: msgID,
251+
AssetSpecifier: assetSpecifier,
252+
AssetMaxAmt: 1000,
253+
AssetRateHint: fn.None[rfqmsg.AssetRate](),
254+
}
255+
256+
buyAccept := rfqmsg.BuyAccept{
257+
Request: buyRequest,
258+
AssetRate: tc.incomingBuyAcceptRate,
259+
}
260+
261+
// Create the negotiator.
262+
errChan := make(chan error, 1)
263+
negotiator, err := NewNegotiator(NegotiatorCfg{
264+
PriceOracle: mockPriceOracle,
265+
OutgoingMessages: make(chan rfqmsg.OutgoingMsg, 1),
266+
AcceptPriceDeviationPpm: tc.acceptPriceDeviationPpm,
267+
ErrChan: errChan,
268+
})
269+
require.NoError(t, err)
270+
271+
// Define the finalise callback function.
272+
finalise := func(msg rfqmsg.BuyAccept,
273+
event fn.Option[InvalidQuoteRespEvent]) {
274+
275+
// If the actual event is none and the expected status is none,
276+
// then we don't need to check anything.
277+
if event.IsNone() && tc.quoteRespStatus.IsNone() {
278+
return
279+
}
280+
281+
require.Equal(t, tc.quoteRespStatus.IsSome(), event.IsSome())
282+
283+
// Extract the actual event status.
284+
var actualEventStatus QuoteRespStatus
285+
event.WhenSome(func(e InvalidQuoteRespEvent) {
286+
actualEventStatus = e.Status
287+
})
288+
289+
// Extract the expected event status.
290+
var expectedStatus QuoteRespStatus
291+
tc.quoteRespStatus.WhenSome(func(e QuoteRespStatus) {
292+
expectedStatus = e
293+
})
294+
295+
// Ensure that the actual and expected event statuses are equal.
296+
require.Equal(t, expectedStatus, actualEventStatus)
297+
}
298+
299+
// Handle the incoming buy accept message.
300+
negotiator.HandleIncomingBuyAccept(buyAccept, finalise)
301+
302+
// Check that there are no errors.
303+
select {
304+
case err := <-errChan:
305+
t.Fatalf("unexpected error: %v", err)
306+
default:
307+
}
308+
309+
// Wait for the negotiator to finish.
310+
negotiator.Wg.Wait()
311+
}
312+
313+
// TestHandleIncomingBuyAccept tests the handling of an incoming buy accept
314+
// message.
315+
func TestHandleIncomingBuyAccept(t *testing.T) {
316+
defaultQuoteExpiry := time.Now().Add(time.Hour)
317+
318+
testCases := []testCaseIncomingBuyAccept{
319+
{
320+
name: "accept price just within bounds 1",
321+
incomingBuyAcceptRate: rfqmsg.AssetRate{
322+
Rate: rfqmath.NewBigIntFixedPoint(1000, 0),
323+
Expiry: defaultQuoteExpiry,
324+
},
325+
priceOracleBidPrice: rfqmsg.AssetRate{
326+
Rate: rfqmath.NewBigIntFixedPoint(1052, 0),
327+
Expiry: defaultQuoteExpiry,
328+
},
329+
acceptPriceDeviationPpm: DefaultAcceptPriceDeviationPpm,
330+
},
331+
{
332+
name: "accept price just within bounds 2",
333+
incomingBuyAcceptRate: rfqmsg.AssetRate{
334+
Rate: rfqmath.NewBigIntFixedPoint(1000, 0),
335+
Expiry: defaultQuoteExpiry,
336+
},
337+
priceOracleBidPrice: rfqmsg.AssetRate{
338+
Rate: rfqmath.NewBigIntFixedPoint(950, 0),
339+
Expiry: defaultQuoteExpiry,
340+
},
341+
acceptPriceDeviationPpm: DefaultAcceptPriceDeviationPpm,
342+
},
343+
{
344+
name: "accept price outside bounds, higher than oracle",
345+
incomingBuyAcceptRate: rfqmsg.AssetRate{
346+
Rate: rfqmath.NewBigIntFixedPoint(8000, 0),
347+
Expiry: defaultQuoteExpiry,
348+
},
349+
priceOracleBidPrice: rfqmsg.AssetRate{
350+
Rate: rfqmath.NewBigIntFixedPoint(1000, 0),
351+
Expiry: defaultQuoteExpiry,
352+
},
353+
acceptPriceDeviationPpm: DefaultAcceptPriceDeviationPpm,
354+
quoteRespStatus: fn.Some[QuoteRespStatus](
355+
InvalidAssetRatesQuoteRespStatus,
356+
),
357+
},
358+
{
359+
name: "accept price outside bounds, lower than oracle",
360+
incomingBuyAcceptRate: rfqmsg.AssetRate{
361+
Rate: rfqmath.NewBigIntFixedPoint(1000, 0),
362+
Expiry: defaultQuoteExpiry,
363+
},
364+
priceOracleBidPrice: rfqmsg.AssetRate{
365+
Rate: rfqmath.NewBigIntFixedPoint(8000, 0),
366+
Expiry: defaultQuoteExpiry,
367+
},
368+
acceptPriceDeviationPpm: DefaultAcceptPriceDeviationPpm,
369+
quoteRespStatus: fn.Some[QuoteRespStatus](
370+
InvalidAssetRatesQuoteRespStatus,
371+
),
372+
},
373+
}
374+
375+
for idx := range testCases {
376+
tc := testCases[idx]
377+
378+
success := t.Run(tc.name, func(t *testing.T) {
379+
assertIncomingBuyAcceptTestCase(t, tc)
380+
})
381+
if !success {
382+
break
383+
}
384+
}
385+
}

0 commit comments

Comments
 (0)