Skip to content

Commit 750d91b

Browse files
committed
itest: add oracle query end-to-end test
1 parent 4c377ad commit 750d91b

File tree

4 files changed

+212
-7
lines changed

4 files changed

+212
-7
lines changed

itest/oracle_harness.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/hex"
77
"fmt"
88
"net"
9+
"slices"
910
"testing"
1011
"time"
1112

@@ -16,6 +17,7 @@ import (
1617
"github.com/lightninglabs/taproot-assets/rpcutils"
1718
oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
1819
"github.com/lightningnetwork/lnd/cert"
20+
"github.com/stretchr/testify/mock"
1921
"github.com/stretchr/testify/require"
2022
"google.golang.org/grpc"
2123
"google.golang.org/grpc/credentials"
@@ -30,6 +32,14 @@ type oracleHarness struct {
3032
grpcListener net.Listener
3133
grpcServer *grpc.Server
3234

35+
// Mock is a mock object that can optionally be used to track calls to
36+
// the oracle harness. If no call expectations are set, the prices from
37+
// the maps below will be used.
38+
// NOTE: When setting up the call expectations, we need to use the
39+
// actual fields of the `QueryAssetRatesRequest` message, since
40+
// otherwise it is much harder to match the calls nicely.
41+
mock.Mock
42+
3343
// buyPrices is a map used internally by the oracle harness to store buy
3444
// prices for certain assets. We use the asset specifier string as a
3545
// unique identifier, since it will either contain an asset ID or a
@@ -181,6 +191,18 @@ func (o *oracleHarness) QueryAssetRates(_ context.Context,
181191
req *oraclerpc.QueryAssetRatesRequest) (
182192
*oraclerpc.QueryAssetRatesResponse, error) {
183193

194+
// Return early with the mocked value if call expectations are set up.
195+
if hasExpectedCall(o.ExpectedCalls, "QueryAssetRates") {
196+
args := o.Called(
197+
req.TransactionType, req.SubjectAsset,
198+
req.SubjectAssetMaxAmount, req.PaymentAsset,
199+
req.PaymentAssetMaxAmount, req.AssetRatesHint,
200+
req.Intent, req.CounterpartyId, req.Metadata,
201+
)
202+
resp, _ := args.Get(0).(*oraclerpc.QueryAssetRatesResponse)
203+
return resp, args.Error(1)
204+
}
205+
184206
// Ensure that the payment asset is BTC. We only support BTC as the
185207
// payment asset in this example.
186208
if !rpcutils.IsAssetBtc(req.PaymentAsset) {
@@ -324,3 +346,11 @@ func generateSelfSignedCert() (tls.Certificate, error) {
324346

325347
return tlsCert, nil
326348
}
349+
350+
// hasExpectedCall checks if the method call has been registered as an expected
351+
// call with the mock object.
352+
func hasExpectedCall(expectedCalls []*mock.Call, method string) bool {
353+
return slices.ContainsFunc(expectedCalls, func(call *mock.Call) bool {
354+
return call.Method == method
355+
})
356+
}

itest/rfq_test.go

Lines changed: 160 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package itest
22

33
import (
4+
"bytes"
45
"context"
56
"fmt"
67
"math"
@@ -15,6 +16,7 @@ import (
1516
"github.com/lightninglabs/taproot-assets/rfqmath"
1617
"github.com/lightninglabs/taproot-assets/rfqmsg"
1718
"github.com/lightninglabs/taproot-assets/taprpc/mintrpc"
19+
oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
1820
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc"
1921
"github.com/lightningnetwork/lnd/chainreg"
2022
"github.com/lightningnetwork/lnd/lnrpc"
@@ -25,6 +27,7 @@ import (
2527
"github.com/lightningnetwork/lnd/lntest/wait"
2628
"github.com/lightningnetwork/lnd/lnwire"
2729
"github.com/lightningnetwork/lnd/tlv"
30+
"github.com/stretchr/testify/mock"
2831
"github.com/stretchr/testify/require"
2932
)
3033

@@ -49,8 +52,17 @@ var (
4952
// As a final step (which is not part of this test), Bob's node will transfer
5053
// the tap asset to Carol's node.
5154
func testRfqAssetBuyHtlcIntercept(t *harnessTest) {
55+
// For this test we'll use an actual oracle RPC server harness.
56+
oracleAddr := fmt.Sprintf("localhost:%d", port.NextAvailablePort())
57+
oracle := newOracleHarness(oracleAddr)
58+
oracle.start(t.t)
59+
t.t.Cleanup(oracle.stop)
60+
61+
// We need to craft the oracle server URL in the correct format.
62+
oracleURL := fmt.Sprintf("rfqrpc://%s", oracleAddr)
63+
5264
// Initialize a new test scenario.
53-
ts := newRfqTestScenario(t)
65+
ts := newRfqTestScenario(t, WithRfqOracleServer(oracleURL))
5466

5567
// Mint an asset with Bob's tapd node.
5668
rpcAssets := MintAssetsConfirmBatch(
@@ -108,8 +120,74 @@ func testRfqAssetBuyHtlcIntercept(t *harnessTest) {
108120
// node.
109121
PeerPubKey: ts.BobLnd.PubKey[:],
110122

111-
TimeoutSeconds: uint32(rfqTimeout.Seconds()),
123+
TimeoutSeconds: uint32(rfqTimeout.Seconds()),
124+
PriceOracleMetadata: "buy-order-1",
112125
}
126+
127+
// We now set up the expected calls that should come in to the mock
128+
// oracle server during the process.
129+
buySpecifier := &oraclerpc.AssetSpecifier{
130+
Id: &oraclerpc.AssetSpecifier_AssetId{
131+
AssetId: mintedAssetId,
132+
},
133+
}
134+
btcSpecifier := &oraclerpc.AssetSpecifier{
135+
Id: &oraclerpc.AssetSpecifier_AssetId{
136+
AssetId: bytes.Repeat([]byte{0}, 32),
137+
},
138+
}
139+
mockResult := &oraclerpc.QueryAssetRatesResponse{
140+
Result: &oraclerpc.QueryAssetRatesResponse_Ok{
141+
Ok: &oraclerpc.QueryAssetRatesOkResponse{
142+
AssetRates: &oraclerpc.AssetRates{
143+
SubjectAssetRate: &oraclerpc.FixedPoint{
144+
Coefficient: "1000000",
145+
Scale: 3,
146+
},
147+
ExpiryTimestamp: uint64(
148+
time.Now().Add(time.Minute).
149+
Unix(),
150+
),
151+
},
152+
},
153+
},
154+
}
155+
156+
// The first call is expected to be the rate hint call for the local
157+
// oracle, using the hint intent.
158+
oracle.On(
159+
"QueryAssetRates", oraclerpc.TransactionType_PURCHASE,
160+
buySpecifier, purchaseAssetAmt, btcSpecifier,
161+
mock.Anything, mock.Anything,
162+
oraclerpc.Intent_INTENT_RECV_PAYMENT_HINT,
163+
mock.Anything, "buy-order-1",
164+
).Return(mockResult, nil).Once()
165+
166+
// Then the counterparty will do a sale call with the intent to receive
167+
// a payment.
168+
oracle.On(
169+
"QueryAssetRates", oraclerpc.TransactionType_SALE,
170+
buySpecifier, purchaseAssetAmt, btcSpecifier,
171+
mock.Anything, mock.Anything,
172+
oraclerpc.Intent_INTENT_RECV_PAYMENT,
173+
mock.Anything, "buy-order-1",
174+
).Return(mockResult, nil).Once()
175+
176+
// And finally, we'll qualify (validate) the price again on our end.
177+
oracle.On(
178+
"QueryAssetRates", oraclerpc.TransactionType_PURCHASE,
179+
buySpecifier, purchaseAssetAmt, btcSpecifier,
180+
mock.Anything, mock.Anything,
181+
oraclerpc.Intent_INTENT_RECV_PAYMENT_QUALIFY,
182+
mock.Anything, "buy-order-1",
183+
).Return(mockResult, nil).Once()
184+
185+
// At the end of the test, we also want to make sure each call came in
186+
// as expected.
187+
defer func() {
188+
oracle.AssertExpectations(t.t)
189+
}()
190+
113191
_, err = ts.AliceTapd.AddAssetBuyOrder(ctxt, buyReq)
114192
require.ErrorContains(
115193
t.t, err, "error checking peer channel: error checking asset "+
@@ -236,8 +314,17 @@ func testRfqAssetBuyHtlcIntercept(t *harnessTest) {
236314
// validation between three peers. The RFQ negotiation is initiated by an asset
237315
// sell request.
238316
func testRfqAssetSellHtlcIntercept(t *harnessTest) {
317+
// For this test we'll use an actual oracle RPC server harness.
318+
oracleAddr := fmt.Sprintf("localhost:%d", port.NextAvailablePort())
319+
oracle := newOracleHarness(oracleAddr)
320+
oracle.start(t.t)
321+
t.t.Cleanup(oracle.stop)
322+
323+
// We need to craft the oracle server URL in the correct format.
324+
oracleURL := fmt.Sprintf("rfqrpc://%s", oracleAddr)
325+
239326
// Initialize a new test scenario.
240-
ts := newRfqTestScenario(t)
327+
ts := newRfqTestScenario(t, WithRfqOracleServer(oracleURL))
241328

242329
// Mint an asset with Alice's tapd node.
243330
rpcAssets := MintAssetsConfirmBatch(
@@ -297,8 +384,74 @@ func testRfqAssetSellHtlcIntercept(t *harnessTest) {
297384
// Bob's node.
298385
PeerPubKey: ts.BobLnd.PubKey[:],
299386

300-
TimeoutSeconds: uint32(rfqTimeout.Seconds()),
387+
TimeoutSeconds: uint32(rfqTimeout.Seconds()),
388+
PriceOracleMetadata: "sell-order-1",
389+
}
390+
391+
// We now set up the expected calls that should come in to the mock
392+
// oracle server during the process.
393+
buySpecifier := &oraclerpc.AssetSpecifier{
394+
Id: &oraclerpc.AssetSpecifier_AssetId{
395+
AssetId: mintedAssetIdBytes,
396+
},
301397
}
398+
btcSpecifier := &oraclerpc.AssetSpecifier{
399+
Id: &oraclerpc.AssetSpecifier_AssetId{
400+
AssetId: bytes.Repeat([]byte{0}, 32),
401+
},
402+
}
403+
mockResult := &oraclerpc.QueryAssetRatesResponse{
404+
Result: &oraclerpc.QueryAssetRatesResponse_Ok{
405+
Ok: &oraclerpc.QueryAssetRatesOkResponse{
406+
AssetRates: &oraclerpc.AssetRates{
407+
SubjectAssetRate: &oraclerpc.FixedPoint{
408+
Coefficient: "1000000",
409+
Scale: 3,
410+
},
411+
ExpiryTimestamp: uint64(
412+
time.Now().Add(time.Minute).
413+
Unix(),
414+
),
415+
},
416+
},
417+
},
418+
}
419+
420+
// The first call is expected to be the rate hint call for the local
421+
// oracle, using the hint intent.
422+
oracle.On(
423+
"QueryAssetRates", oraclerpc.TransactionType_SALE,
424+
buySpecifier, mock.Anything, btcSpecifier,
425+
askAmt, mock.Anything,
426+
oraclerpc.Intent_INTENT_PAY_INVOICE_HINT,
427+
mock.Anything, "sell-order-1",
428+
).Return(mockResult, nil).Once()
429+
430+
// Then the counterparty will do a sale call with the intent to receive
431+
// a payment.
432+
oracle.On(
433+
"QueryAssetRates", oraclerpc.TransactionType_PURCHASE,
434+
buySpecifier, mock.Anything, btcSpecifier,
435+
askAmt, mock.Anything,
436+
oraclerpc.Intent_INTENT_PAY_INVOICE,
437+
mock.Anything, "sell-order-1",
438+
).Return(mockResult, nil).Once()
439+
440+
// And finally, we'll qualify (validate) the price again on our end.
441+
oracle.On(
442+
"QueryAssetRates", oraclerpc.TransactionType_SALE,
443+
buySpecifier, mock.Anything, btcSpecifier,
444+
askAmt, mock.Anything,
445+
oraclerpc.Intent_INTENT_PAY_INVOICE_QUALIFY,
446+
mock.Anything, "sell-order-1",
447+
).Return(mockResult, nil).Once()
448+
449+
// At the end of the test, we also want to make sure each call came in
450+
// as expected.
451+
defer func() {
452+
oracle.AssertExpectations(t.t)
453+
}()
454+
302455
_, err = ts.AliceTapd.AddAssetSellOrder(ctxt, sellReq)
303456
require.ErrorContains(
304457
t.t, err, "error checking peer channel: error checking asset "+
@@ -690,18 +843,18 @@ func newRfqTestScenario(t *harnessTest, opts ...RfqOption) *rfqTestScenario {
690843
aliceTapd := setupTapdHarness(
691844
t.t, t, aliceLnd, t.universeServer, WithOracleServer(
692845
rfqOpts.oracleServerAddr, rfqOpts.oracleServerAlice,
693-
),
846+
), WithSendPriceHint(),
694847
)
695848

696849
bobTapd := setupTapdHarness(
697850
t.t, t, bobLnd, t.universeServer, WithOracleServer(
698851
rfqOpts.oracleServerAddr, rfqOpts.oracleServerBob,
699-
),
852+
), WithSendPriceHint(),
700853
)
701854
carolTapd := setupTapdHarness(
702855
t.t, t, carolLnd, t.universeServer, WithOracleServer(
703856
rfqOpts.oracleServerAddr, rfqOpts.oracleServerCarol,
704-
),
857+
), WithSendPriceHint(),
705858
)
706859

707860
ts := rfqTestScenario{

itest/tapd_harness.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ type harnessOpts struct {
134134
// disableSyncCache is a flag that can be set to true to disable the
135135
// universe syncer cache.
136136
disableSyncCache bool
137+
138+
// sendPriceHint indicates whether the tapd should send price hints from
139+
// the local oracle to the counterparty when requesting a quote.
140+
sendPriceHint bool
137141
}
138142

139143
type harnessOption func(*harnessOpts)
@@ -316,6 +320,10 @@ func newTapdHarness(t *testing.T, ht *harnessTest, cfg tapdConfig,
316320
finalCfg.Universe.MultiverseCaches.SyncerCacheEnabled = true
317321
}
318322

323+
if opts.sendPriceHint {
324+
finalCfg.Experimental.Rfq.SendPriceHint = true
325+
}
326+
319327
return &tapdHarness{
320328
cfg: &cfg,
321329
clientCfg: finalCfg,

itest/test_harness.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,10 @@ type tapdHarnessParams struct {
368368
// oracleServerAddress defines the oracle server's address that this
369369
// tapd harness is going to use.
370370
oracleServerAddress string
371+
372+
// sendPriceHint indicates whether the tapd should send price hints from
373+
// the local oracle to the counterparty when requesting a quote.
374+
sendPriceHint bool
371375
}
372376

373377
// Option is a tapd harness option.
@@ -387,6 +391,15 @@ func WithOracleServer(global, override string) Option {
387391
}
388392
}
389393

394+
// WithSendPriceHint is a functional option that indicates that the tapd node
395+
// should send price hints from the local oracle to the counterparty when
396+
// requesting a quote.
397+
func WithSendPriceHint() Option {
398+
return func(th *tapdHarnessParams) {
399+
th.sendPriceHint = true
400+
}
401+
}
402+
390403
// setupTapdHarness creates a new tapd that connects to the given lnd node
391404
// and to the given universe server.
392405
func setupTapdHarness(t *testing.T, ht *harnessTest,
@@ -420,6 +433,7 @@ func setupTapdHarness(t *testing.T, ht *harnessTest,
420433
ho.sqliteDatabaseFilePath = params.sqliteDatabaseFilePath
421434
ho.disableSyncCache = params.disableSyncCache
422435
ho.oracleServerAddress = params.oracleServerAddress
436+
ho.sendPriceHint = params.sendPriceHint
423437
}
424438

425439
tapdCfg := tapdConfig{

0 commit comments

Comments
 (0)