From 25d64a1694fc02b1a5cbe557b896ff415e3c6773 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Wed, 23 Oct 2024 09:31:47 +0200 Subject: [PATCH] itest: add RPC oracle with spread based asset rates This commit adds a fully configurable RPC oracle that can serve asset rates that differ for buy and sell. We then add a test that shows and asserts proper asset conversion to satoshis and back. --- itest/assets_test.go | 167 ++++++++++- itest/litd_custom_channels_test.go | 439 ++++++++++++++++++++++++----- itest/litd_test.go | 5 + itest/litd_test_list_on_test.go | 4 + itest/log.go | 24 ++ itest/oracle_test.go | 279 ++++++++++++++++++ 6 files changed, 830 insertions(+), 88 deletions(-) create mode 100644 itest/log.go create mode 100644 itest/oracle_test.go diff --git a/itest/assets_test.go b/itest/assets_test.go index 312b7699e..1658df952 100644 --- a/itest/assets_test.go +++ b/itest/assets_test.go @@ -61,8 +61,8 @@ func createTestAssetNetwork(t *harnessTest, net *NetworkHarness, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, universeTap *tapClient, mintedAsset *taprpc.Asset, assetSendAmount, charlieFundingAmount, daveFundingAmount, - erinFundingAmount uint64, pushSat int64) (*tchrpc.FundChannelResponse, - *tchrpc.FundChannelResponse, *tchrpc.FundChannelResponse) { + erinFundingAmount uint64, pushSat int64) (*lnrpc.ChannelPoint, + *lnrpc.ChannelPoint, *lnrpc.ChannelPoint) { ctxb := context.Background() assetID := mintedAsset.AssetGenesis.AssetId @@ -256,7 +256,26 @@ func createTestAssetNetwork(t *harnessTest, net *NetworkHarness, charlieTap, t.t, erinTap.node, fabiaTap.node, erinFundingAmount, assetID, ) - return fundRespCD, fundRespDY, fundRespEF + chanPointCD := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespCD.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespCD.Txid, + }, + } + chanPointDY := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespDY.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespDY.Txid, + }, + } + chanPointEF := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundRespEF.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundRespEF.Txid, + }, + } + + return chanPointCD, chanPointDY, chanPointEF } func assertNumAssetUTXOs(t *testing.T, tapdClient *tapClient, @@ -586,6 +605,67 @@ func getAssetChannelBalance(t *testing.T, node *HarnessNode, assetID []byte, balance.RemoteBalance.Sat } +func fetchChannel(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint) *lnrpc.Channel { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + channelResp, err := node.ListChannels(ctxt, &lnrpc.ListChannelsRequest{ + ActiveOnly: true, + }) + require.NoError(t, err) + + chanFundingHash, err := lnrpc.GetChanPointFundingTxid(chanPoint) + require.NoError(t, err) + + chanPointStr := fmt.Sprintf("%v:%v", chanFundingHash, + chanPoint.OutputIndex) + + var targetChan *lnrpc.Channel + for _, channel := range channelResp.Channels { + if channel.ChannelPoint == chanPointStr { + targetChan = channel + + break + } + } + require.NotNil(t, targetChan) + + return targetChan +} + +func assertChannelSatBalance(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint, local, remote int64) { + + targetChan := fetchChannel(t, node, chanPoint) + + require.InDelta(t, local, targetChan.LocalBalance, 1) + require.InDelta(t, remote, targetChan.RemoteBalance, 1) +} + +func assertChannelAssetBalance(t *testing.T, node *HarnessNode, + chanPoint *lnrpc.ChannelPoint, local, remote uint64) { + + targetChan := fetchChannel(t, node, chanPoint) + + var assetBalance rfqmsg.JsonAssetChannel + err := json.Unmarshal(targetChan.CustomChannelData, &assetBalance) + require.NoError(t, err) + + require.Len(t, assetBalance.Assets, 1) + + require.InDelta(t, local, assetBalance.Assets[0].LocalBalance, 1) + require.InDelta(t, remote, assetBalance.Assets[0].RemoteBalance, 1) +} + +// addRoutingFee adds the default routing fee (1 part per million fee rate plus +// 1000 milli-satoshi base fee) to the given milli-satoshi amount. +func addRoutingFee(amt lnwire.MilliSatoshi) lnwire.MilliSatoshi { + return amt + (amt / 1000_000) + 1000 +} + func sendAssetKeySendPayment(t *testing.T, src, dst *HarnessNode, amt uint64, assetID []byte, btcAmt fn.Option[int64], expectedStatus lnrpc.Payment_PaymentStatus, @@ -702,9 +782,11 @@ func createAndPayNormalInvoice(t *testing.T, src, rfqPeer, dst *HarnessNode, }) require.NoError(t, err) - return payInvoiceWithAssets( + numUnits, _ := payInvoiceWithAssets( t, src, rfqPeer, invoiceResp, assetID, smallShards, ) + + return numUnits } func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode, @@ -730,7 +812,7 @@ func payInvoiceWithSatoshi(t *testing.T, payer *HarnessNode, func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, invoice *lnrpc.AddInvoiceResponse, assetID []byte, - smallShards bool) uint64 { + smallShards bool) (uint64, rfqmath.BigIntFixedPoint) { ctxb := context.Background() ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) @@ -774,11 +856,13 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate) require.NoError(t, err) + t.Logf("Got quote for %v asset units per BTC", rate) + amountMsat := lnwire.MilliSatoshi(decodedInvoice.NumMsat) milliSatsFP := rfqmath.MilliSatoshiToUnits(amountMsat, *rate) numUnits := milliSatsFP.ScaleTo(0).ToUint64() - msatPerUnit := uint64(decodedInvoice.NumMsat) / numUnits - t.Logf("Got quote for %v asset units at %v msat/unit from peer %s "+ + msatPerUnit := float64(decodedInvoice.NumMsat) / float64(numUnits) + t.Logf("Got quote for %v asset units at %3f msat/unit from peer %s "+ "with SCID %d", numUnits, msatPerUnit, peerPubKey, acceptedQuote.Scid) @@ -786,7 +870,7 @@ func payInvoiceWithAssets(t *testing.T, payer, rfqPeer *HarnessNode, require.NoError(t, err) require.Equal(t, lnrpc.Payment_SUCCEEDED, result.Status) - return numUnits + return numUnits, *rate } func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, @@ -825,19 +909,70 @@ func createAssetInvoice(t *testing.T, dstRfqPeer, dst *HarnessNode, rate, err := rfqrpc.UnmarshalFixedPoint(rpcRate) require.NoError(t, err) + t.Logf("Got quote for %v asset units per BTC", rate) + assetUnits := rfqmath.NewBigIntFixedPoint(assetAmount, 0) numMSats := rfqmath.UnitsToMilliSatoshi(assetUnits, *rate) - mSatPerUnit := uint64(decodedInvoice.NumMsat) / assetAmount + mSatPerUnit := float64(decodedInvoice.NumMsat) / float64(assetAmount) require.EqualValues(t, numMSats, decodedInvoice.NumMsat) - t.Logf("Got quote for %d sats at %v msat/unit from peer %x with SCID "+ - "%d", decodedInvoice.NumMsat, mSatPerUnit, dstRfqPeer.PubKey[:], - resp.AcceptedBuyQuote.Scid) + t.Logf("Got quote for %d mSats at %3f msat/unit from peer %x with "+ + "SCID %d", decodedInvoice.NumMsat, mSatPerUnit, + dstRfqPeer.PubKey[:], resp.AcceptedBuyQuote.Scid) return resp.InvoiceResult } +// assertPaymentHtlcAssets makes sure the payment with the given hash shows the +// individual HTLCs that arrived for it and that they show the correct asset +// amounts for the given ID when decoded. +func assertPaymentHtlcAssets(t *testing.T, node *HarnessNode, payHash []byte, + assetID []byte, assetAmount uint64) { + + ctxb := context.Background() + ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout) + defer cancel() + + stream, err := node.RouterClient.TrackPaymentV2( + ctxt, &routerrpc.TrackPaymentRequest{ + PaymentHash: payHash, + NoInflightUpdates: true, + }, + ) + require.NoError(t, err) + + payment, err := stream.Recv() + require.NoError(t, err) + require.NotNil(t, payment) + require.NotEmpty(t, payment.Htlcs) + + t.Logf("Asset payment: %v", toProtoJSON(t, payment)) + + targetID := hex.EncodeToString(assetID) + + var totalAssetAmount uint64 + for _, htlc := range payment.Htlcs { + require.NotNil(t, htlc.Route) + require.NotEmpty(t, htlc.Route.CustomChannelData) + + jsonHtlc := &rfqmsg.JsonHtlc{} + err := json.Unmarshal(htlc.Route.CustomChannelData, jsonHtlc) + require.NoError(t, err) + + for _, balance := range jsonHtlc.Balances { + if balance.AssetID != targetID { + continue + } + + totalAssetAmount += balance.Amount + } + } + + // Due to rounding we allow up to 1 unit of error. + require.InDelta(t, assetAmount, totalAssetAmount, 1) +} + func waitForSendEvent(t *testing.T, sendEvents taprpc.TaprootAssets_SubscribeSendEventsClient, expectedState tapfreighter.SendState) { @@ -862,6 +997,14 @@ type coOpCloseBalanceCheck func(t *testing.T, local, remote *HarnessNode, closeTx *wire.MsgTx, closeUpdate *lnrpc.ChannelCloseUpdate, assetID, groupKey []byte, universeTap *tapClient) +// noOpCoOpCloseBalanceCheck is a no-op implementation of the co-op close +// balance check that can be used in tests. +func noOpCoOpCloseBalanceCheck(_ *testing.T, _, _ *HarnessNode, _ *wire.MsgTx, + _ *lnrpc.ChannelCloseUpdate, _, _ []byte, _ *tapClient) { + + // This is a no-op function. +} + // closeAssetChannelAndAssert closes the channel between the local and remote // node and asserts the final balances of the closing transaction. func closeAssetChannelAndAssert(t *harnessTest, net *NetworkHarness, diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index d955b49ae..903c35e37 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -3,14 +3,19 @@ package itest import ( "context" "fmt" + "math" + "math/big" "slices" "time" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/itest" "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightninglabs/taproot-assets/tapchannel" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" @@ -19,7 +24,9 @@ import ( "github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/port" "github.com/lightningnetwork/lnd/lntest/wait" + "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) @@ -55,7 +62,7 @@ var ( "--accept-keysend", "--debuglevel=trace,GRPC=error,BTCN=info", } - litdArgsTemplate = []string{ + litdArgsTemplateNoOracle = []string{ "--taproot-assets.allow-public-uni-proof-courier", "--taproot-assets.universe.public-access=rw", "--taproot-assets.universe.sync-all-assets", @@ -64,17 +71,19 @@ var ( "--taproot-assets.universerpccourier.numtries=5", "--taproot-assets.universerpccourier.initialbackoff=300ms", "--taproot-assets.universerpccourier.maxbackoff=600ms", - "--taproot-assets.experimental.rfq.priceoracleaddress=" + - "use_mock_price_oracle_service_promise_to_" + - "not_use_on_mainnet", - "--taproot-assets.experimental.rfq.mockoracleassetsperbtc=" + - "5820600", "--taproot-assets.universerpccourier.skipinitdelay", "--taproot-assets.universerpccourier.backoffresetwait=100ms", "--taproot-assets.universerpccourier.initialbackoff=300ms", "--taproot-assets.universerpccourier.maxbackoff=600ms", "--taproot-assets.custodianproofretrievaldelay=500ms", } + litdArgsTemplate = append(litdArgsTemplateNoOracle, []string{ + "--taproot-assets.experimental.rfq.priceoracleaddress=" + + "use_mock_price_oracle_service_promise_to_" + + "not_use_on_mainnet", + "--taproot-assets.experimental.rfq.mockoracleassetsperbtc=" + + "5820600", + }...) ) const ( @@ -90,7 +99,7 @@ func testCustomChannelsLarge(_ context.Context, net *NetworkHarness, lndArgs := slices.Clone(lndArgsTemplate) litdArgs := slices.Clone(litdArgsTemplate) - // Explicitly set the proof courier as Alice (how has no other role + // Explicitly set the proof courier as Zane (now has no other role // other than proof shuffling), otherwise a hashmail courier will be // used. For the funding transaction, we're just posting it and don't // expect a true receiver. @@ -186,7 +195,7 @@ func testCustomChannelsLarge(_ context.Context, net *NetworkHarness, ) charlieFundingAmount := cents.Amount - uint64(2*400_000) - fundRespCD, _, _ := createTestAssetNetwork( + chanPointCD, _, _ := createTestAssetNetwork( t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, universeTap, cents, 400_000, charlieFundingAmount, daveFundingAmount, erinFundingAmount, DefaultPushSat, @@ -246,16 +255,9 @@ func testCustomChannelsLarge(_ context.Context, net *NetworkHarness, // And now we close the channel to test how things look if all the // balance is on the non-initiator (recipient) side. - charlieChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespCD.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespCD.Txid, - }, - } - t.Logf("Closing Charlie -> Dave channel") closeAssetChannelAndAssert( - t, net, charlie, dave, charlieChanPoint, assetID, nil, + t, net, charlie, dave, chanPointCD, assetID, nil, universeTap, initiatorZeroAssetBalanceCoOpBalanceCheck, ) } @@ -269,7 +271,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, lndArgs := slices.Clone(lndArgsTemplate) litdArgs := slices.Clone(litdArgsTemplate) - // Explicitly set the proof courier as Alice (how has no other role + // Explicitly set the proof courier as Zane (now has no other role // other than proof shuffling), otherwise a hashmail courier will be // used. For the funding transaction, we're just posting it and don't // expect a true receiver. @@ -368,7 +370,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, ) charlieFundingAmount := cents.Amount - 2*startAmount - fundRespCD, fundRespDY, fundRespEF := createTestAssetNetwork( + chanPointCD, chanPointDY, chanPointEF := createTestAssetNetwork( t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, universeTap, cents, startAmount, charlieFundingAmount, daveFundingAmount, erinFundingAmount, DefaultPushSat, @@ -569,40 +571,21 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, // Test case 8: Now we'll close each of the channels, starting with the // Charlie -> Dave custom channel. // ------------ - charlieChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespCD.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespCD.Txid, - }, - } - daveChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespDY.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespDY.Txid, - }, - } - erinChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespEF.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespEF.Txid, - }, - } - t.Logf("Closing Charlie -> Dave channel") closeAssetChannelAndAssert( - t, net, charlie, dave, charlieChanPoint, assetID, nil, + t, net, charlie, dave, chanPointCD, assetID, nil, universeTap, assertDefaultCoOpCloseBalance(true, true), ) t.Logf("Closing Dave -> Yara channel, close initiated by Yara") closeAssetChannelAndAssert( - t, net, yara, dave, daveChanPoint, assetID, nil, + t, net, yara, dave, chanPointDY, assetID, nil, universeTap, assertDefaultCoOpCloseBalance(false, true), ) t.Logf("Closing Erin -> Fabia channel") closeAssetChannelAndAssert( - t, net, erin, fabia, erinChanPoint, assetID, nil, + t, net, erin, fabia, chanPointEF, assetID, nil, universeTap, assertDefaultCoOpCloseBalance(true, true), ) @@ -624,7 +607,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, // Test case 10: We now open a new asset channel and close it again, to // make sure that a non-existent remote balance is handled correctly. t.Logf("Opening new asset channel between Charlie and Dave...") - fundRespCD, err = charlieTap.FundChannel( + fundRespCD, err := charlieTap.FundChannel( ctxb, &tchrpc.FundChannelRequest{ AssetAmount: fundingAmount, AssetId: assetID, @@ -646,7 +629,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, assertAssetChan(t.t, charlie, dave, fundingAmount, assetID) // And let's just close the channel again. - charlieChanPoint = &lnrpc.ChannelPoint{ + chanPointCD = &lnrpc.ChannelPoint{ OutputIndex: uint32(fundRespCD.OutputIndex), FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ FundingTxidStr: fundRespCD.Txid, @@ -655,7 +638,7 @@ func testCustomChannels(_ context.Context, net *NetworkHarness, t.Logf("Closing Charlie -> Dave channel") closeAssetChannelAndAssert( - t, net, charlie, dave, charlieChanPoint, assetID, nil, + t, net, charlie, dave, chanPointCD, assetID, nil, universeTap, assertDefaultCoOpCloseBalance(false, false), ) @@ -726,10 +709,10 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, lndArgs := slices.Clone(lndArgsTemplate) litdArgs := slices.Clone(litdArgsTemplate) - // Explicitly set the proof courier as Alice (has no other role other - // than proof shuffling), otherwise a hashmail courier will be used. - // For the funding transaction, we're just posting it and don't expect a - // true receiver. + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. zane, err := net.NewNode( t.t, "Zane", lndArgs, false, true, litdArgs..., ) @@ -828,7 +811,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, ) charlieFundingAmount := cents.Amount - 2*startAmount - fundRespCD, fundRespDY, fundRespEF := createTestAssetNetwork( + chanPointCD, chanPointDY, chanPointEF := createTestAssetNetwork( t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, universeTap, cents, startAmount, charlieFundingAmount, daveFundingAmount, erinFundingAmount, DefaultPushSat, @@ -998,40 +981,21 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, // Test case 8: Now we'll close each of the channels, starting with the // Charlie -> Dave custom channel. // ------------ - charlieChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespCD.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespCD.Txid, - }, - } - daveChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespDY.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespDY.Txid, - }, - } - erinChanPoint := &lnrpc.ChannelPoint{ - OutputIndex: uint32(fundRespEF.OutputIndex), - FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ - FundingTxidStr: fundRespEF.Txid, - }, - } - t.Logf("Closing Charlie -> Dave channel") closeAssetChannelAndAssert( - t, net, charlie, dave, charlieChanPoint, assetID, groupID, + t, net, charlie, dave, chanPointCD, assetID, groupID, universeTap, assertDefaultCoOpCloseBalance(true, true), ) t.Logf("Closing Dave -> Yara channel, close initiated by Yara") closeAssetChannelAndAssert( - t, net, yara, dave, daveChanPoint, assetID, groupID, + t, net, yara, dave, chanPointDY, assetID, groupID, universeTap, assertDefaultCoOpCloseBalance(false, true), ) t.Logf("Closing Erin -> Fabia channel") closeAssetChannelAndAssert( - t, net, erin, fabia, erinChanPoint, assetID, groupID, + t, net, erin, fabia, chanPointEF, assetID, groupID, universeTap, assertDefaultCoOpCloseBalance(true, true), ) @@ -1054,7 +1018,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, // Test case 10: We now open a new asset channel and close it again, to // make sure that a non-existent remote balance is handled correctly. t.Logf("Opening new asset channel between Charlie and Dave...") - fundRespCD, err = charlieTap.FundChannel( + fundRespCD, err := charlieTap.FundChannel( ctxb, &tchrpc.FundChannelRequest{ AssetAmount: fundingAmount, AssetId: assetID, @@ -1076,7 +1040,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, assertAssetChan(t.t, charlie, dave, fundingAmount, assetID) // And let's just close the channel again. - charlieChanPoint = &lnrpc.ChannelPoint{ + chanPointCD = &lnrpc.ChannelPoint{ OutputIndex: uint32(fundRespCD.OutputIndex), FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ FundingTxidStr: fundRespCD.Txid, @@ -1085,7 +1049,7 @@ func testCustomChannelsGroupedAsset(_ context.Context, net *NetworkHarness, t.Logf("Closing Charlie -> Dave channel") closeAssetChannelAndAssert( - t, net, charlie, dave, charlieChanPoint, assetID, groupID, + t, net, charlie, dave, chanPointCD, assetID, groupID, universeTap, assertDefaultCoOpCloseBalance(false, false), ) @@ -1118,7 +1082,10 @@ func testCustomChannelsForceClose(_ context.Context, net *NetworkHarness, lndArgs := slices.Clone(lndArgsTemplate) litdArgs := slices.Clone(litdArgsTemplate) - // Zane will act as our Universe server for the duration of the test. + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. zane, err := net.NewNode( t.t, "Zane", lndArgs, false, true, litdArgs..., ) @@ -1479,7 +1446,10 @@ func testCustomChannelsBreach(_ context.Context, net *NetworkHarness, lndArgs := slices.Clone(lndArgsTemplate) litdArgs := slices.Clone(litdArgsTemplate) - // Zane will act as our Universe server for the duration of the test. + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. zane, err := net.NewNode( t.t, "Zane", lndArgs, false, true, litdArgs..., ) @@ -1700,7 +1670,7 @@ func testCustomChannelsLiquidityEdgeCases(_ context.Context, lndArgs := slices.Clone(lndArgsTemplate) litdArgs := slices.Clone(litdArgsTemplate) - // Explicitly set the proof courier as Alice (how has no other role + // Explicitly set the proof courier as Zane (now has no other role // other than proof shuffling), otherwise a hashmail courier will be // used. For the funding transaction, we're just posting it and don't // expect a true receiver. @@ -1957,6 +1927,10 @@ func testCustomChannelsBalanceConsistency(_ context.Context, lndArgs := slices.Clone(lndArgsTemplate) litdArgs := slices.Clone(litdArgsTemplate) + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. zane, err := net.NewNode( t.t, "Zane", lndArgs, false, true, litdArgs..., ) @@ -2267,3 +2241,316 @@ func testCustomChannelsSingleAssetMultiInput(_ context.Context, t.t, charlieTap.node, daveTap.node, 2*halfCentsAmount, assetID, ) } + +// testCustomChannelsOraclePricing tests that all asset transfers are correctly +// priced when using an oracle that isn't tapd's mock oracle. +func testCustomChannelsOraclePricing(_ context.Context, + net *NetworkHarness, t *harnessTest) { + + usdMetaData := &taprpc.AssetMeta{ + Data: []byte(`{ +"description":"this is a USD stablecoin with decimal display of 6" +}`), + Type: taprpc.AssetMetaType_META_TYPE_JSON, + } + + const decimalDisplay = 6 + itestAsset = &mintrpc.MintAsset{ + AssetType: taprpc.AssetType_NORMAL, + Name: "USD", + AssetMeta: usdMetaData, + // We mint 1 million USD with a decimal display of 6, which + // results in 1 trillion asset units. + Amount: 1_000_000_000_000, + DecimalDisplay: decimalDisplay, + } + + oracleAddr := fmt.Sprintf("localhost:%d", port.NextAvailablePort()) + oracle := newOracleHarness(oracleAddr) + oracle.start(t.t) + t.t.Cleanup(oracle.stop) + + ctxb := context.Background() + lndArgs := slices.Clone(lndArgsTemplate) + litdArgs := slices.Clone(litdArgsTemplateNoOracle) + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.experimental.rfq.priceoracleaddress="+ + "rfqrpc://%s", oracleAddr, + )) + + // Explicitly set the proof courier as Zane (now has no other role + // other than proof shuffling), otherwise a hashmail courier will be + // used. For the funding transaction, we're just posting it and don't + // expect a true receiver. + zane, err := net.NewNode( + t.t, "Zane", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + litdArgs = append(litdArgs, fmt.Sprintf( + "--taproot-assets.proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, zane.Cfg.LitAddr(), + )) + + // The topology we are going for looks like the following: + // + // Charlie --[assets]--> Dave --[sats]--> Erin --[assets]--> Fabia + // | + // | + // [assets] + // | + // v + // Yara + // + // With [assets] being a custom channel and [sats] being a normal, BTC + // only channel. + // All 5 nodes need to be full litd nodes running in integrated mode + // with tapd included. We also need specific flags to be enabled, so we + // create 5 completely new nodes, ignoring the two default nodes that + // are created by the harness. + charlie, err := net.NewNode( + t.t, "Charlie", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + dave, err := net.NewNode(t.t, "Dave", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + erin, err := net.NewNode(t.t, "Erin", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + fabia, err := net.NewNode( + t.t, "Fabia", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + yara, err := net.NewNode( + t.t, "Yara", lndArgs, false, true, litdArgs..., + ) + require.NoError(t.t, err) + + nodes := []*HarnessNode{charlie, dave, erin, fabia, yara} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Create the normal channel between Dave and Erin. + t.Logf("Opening normal channel between Dave and Erin...") + const btcChannelFundingAmount = 10_000_000 + chanPointDE := openChannelAndAssert( + t, net, dave, erin, lntest.OpenChannelParams{ + Amt: btcChannelFundingAmount, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, dave, chanPointDE, false) + + // This is the only public channel, we need everyone to be aware of it. + assertChannelKnown(t.t, charlie, chanPointDE) + assertChannelKnown(t.t, fabia, chanPointDE) + + universeTap := newTapClient(t.t, zane) + charlieTap := newTapClient(t.t, charlie) + daveTap := newTapClient(t.t, dave) + erinTap := newTapClient(t.t, erin) + fabiaTap := newTapClient(t.t, fabia) + yaraTap := newTapClient(t.t, yara) + + // Mint an asset on Charlie and sync Dave to Charlie as the universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, charlieTap, + []*mintrpc.MintAssetRequest{ + { + Asset: itestAsset, + }, + }, + ) + usdAsset := mintedAssets[0] + assetID := usdAsset.AssetGenesis.AssetId + + // Now that we've minted the asset, we can set the price in the oracle. + var id asset.ID + copy(id[:], assetID) + + // Let's assume the current USD price for 1 BTC is 66,548.40. We'll take + // that price and add a 4% spread, 2% on each side (buy/sell) to earn + // money as the oracle. 2% is 1,330.97, so we'll set the sell price to + // 65,217.43 and the purchase price to 67,879.37. + // The following numbers are to help understand the magic numbers below. + // They're the price in USD/BTC, the price of 1 USD in sats and the + // expected price in asset units per BTC. + // 65,217.43 => 1533.332 => 65_217_430_000 + // 66,548.40 => 1502.666 => 66_548_400_000 + // 67,879.37 => 1473.202 => 67_879_370_000 + salePrice := rfqmath.NewBigIntFixedPoint(65_217_43, 2) + purchasePrice := rfqmath.NewBigIntFixedPoint(67_879_37, 2) + + // We now have the prices defined in USD. But the asset has a decimal + // display of 6, so we need to multiply them by 10^6. + factor := rfqmath.NewBigInt( + big.NewInt(int64(math.Pow10(decimalDisplay))), + ) + salePrice.Coefficient = salePrice.Coefficient.Mul(factor) + purchasePrice.Coefficient = purchasePrice.Coefficient.Mul(factor) + oracle.setPrice(id, purchasePrice, salePrice) + + t.Logf("Minted %d USD assets, syncing universes...", usdAsset.Amount) + syncUniverses(t.t, charlieTap, dave, erin, fabia, yara) + t.Logf("Universes synced between all nodes, distributing assets...") + + const ( + sendAmount = uint64(400_000_000) + daveFundingAmount = uint64(400_000_000) + erinFundingAmount = uint64(200_000_000) + ) + charlieFundingAmount := usdAsset.Amount - 2*sendAmount + + chanPointCD, chanPointDY, chanPointEF := createTestAssetNetwork( + t, net, charlieTap, daveTap, erinTap, fabiaTap, yaraTap, + universeTap, usdAsset, sendAmount, charlieFundingAmount, + daveFundingAmount, erinFundingAmount, 0, + ) + + // Before we start sending out payments, let's make sure each node can + // see the other one in the graph and has all required features. + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, charlie)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(dave, yara)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(yara, dave)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(erin, fabia)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(fabia, erin)) + require.NoError(t.t, t.lndHarness.AssertNodeKnown(charlie, erin)) + + // We now create an invoice at Fabia for 100 USD, which is 100_000_000 + // asset units with decimal display of 6. + const fabiaInvoiceAssetAmount = 100_000_000 + invoiceResp := createAssetInvoice( + t.t, erin, fabia, fabiaInvoiceAssetAmount, assetID, + ) + decodedInvoice, err := fabia.DecodePayReq(ctxb, &lnrpc.PayReqString{ + PayReq: invoiceResp.PaymentRequest, + }) + require.NoError(t.t, err) + + // The invoice amount should come out as 100 * 1533.332. + require.EqualValues(t.t, 153_333_242, decodedInvoice.NumMsat) + + numUnits, rate := payInvoiceWithAssets( + t.t, charlie, dave, invoiceResp, assetID, false, + ) + logBalance(t.t, nodes, assetID, "after invoice") + + // The calculated amount Charlie has to pay should come out as + // 153_333_242 / 1473.202, which is quite exactly 4% more than will + // arrive at the destination (which is the oracle's configured spread). + // This is before routing fees though. + const charlieInvoiceAmount = 104_081_638 + require.EqualValues(t.t, charlieInvoiceAmount, numUnits) + + // The default routing fees are 1ppm + 1msat per hop, and we have 2 + // hops in total. + charliePaidMSat := addRoutingFee(addRoutingFee(lnwire.MilliSatoshi( + decodedInvoice.NumMsat, + ))) + charliePaidAmount := rfqmath.MilliSatoshiToUnits( + charliePaidMSat, rate, + ).ScaleTo(0).ToUint64() + assertPaymentHtlcAssets( + t.t, charlie, invoiceResp.RHash, assetID, charliePaidAmount, + ) + + // We now make sure the asset and satoshi channel balances are exactly + // what we expect them to be. + var ( + // channelFundingAmount is the hard coded satoshi amount that + // currently goes into asset channels. + channelFundingAmount int64 = 100_000 + + // commitFeeP2TR is the default commit fee for a P2TR channel + // commitment with 4 outputs (to_local, to_remote, 2 anchors). + commitFeeP2TR int64 = 2420 + commitFeeP2WSH int64 = 2810 + anchorAmount int64 = 330 + assetHtlcCarryAmount = int64( + tapchannel.DefaultOnChainHtlcAmount, + ) + unbalancedLocalAmount = channelFundingAmount - commitFeeP2TR - + anchorAmount + balancedLocalAmount = unbalancedLocalAmount - anchorAmount + ) + + // Checking Charlie's sat and asset balances in channel Charlie->Dave. + assertChannelSatBalance( + t.t, charlie, chanPointCD, + balancedLocalAmount-assetHtlcCarryAmount, assetHtlcCarryAmount, + ) + assertChannelAssetBalance( + t.t, charlie, chanPointCD, + charlieFundingAmount-charliePaidAmount, charliePaidAmount, + ) + + // Checking Dave's sat and asset balances in channel Charlie->Dave. + assertChannelSatBalance( + t.t, dave, chanPointCD, + assetHtlcCarryAmount, balancedLocalAmount-assetHtlcCarryAmount, + ) + assertChannelAssetBalance( + t.t, dave, chanPointCD, + charliePaidAmount, charlieFundingAmount-charliePaidAmount, + ) + + // Checking Dave's sat balance in channel Dave->Erin. + forwardAmountDave := addRoutingFee( + lnwire.MilliSatoshi(decodedInvoice.NumMsat), + ).ToSatoshis() + assertChannelSatBalance( + t.t, dave, chanPointDE, + btcChannelFundingAmount-commitFeeP2WSH-2*anchorAmount- + int64(forwardAmountDave), + int64(forwardAmountDave), + ) + + // Checking Erin's sat balance in channel Dave->Erin. + assertChannelSatBalance( + t.t, erin, chanPointDE, + int64(forwardAmountDave), + btcChannelFundingAmount-commitFeeP2WSH-2*anchorAmount- + int64(forwardAmountDave), + ) + + // Checking Erin's sat and asset balances in channel Erin->Fabia. + assertChannelSatBalance( + t.t, erin, chanPointEF, + balancedLocalAmount-assetHtlcCarryAmount, assetHtlcCarryAmount, + ) + assertChannelAssetBalance( + t.t, erin, chanPointEF, + erinFundingAmount-fabiaInvoiceAssetAmount, + fabiaInvoiceAssetAmount, + ) + + // Checking Fabia's sat and asset balances in channel Erin->Fabia. + assertChannelSatBalance( + t.t, fabia, chanPointEF, + assetHtlcCarryAmount, balancedLocalAmount-assetHtlcCarryAmount, + ) + assertChannelAssetBalance( + t.t, erin, chanPointEF, + fabiaInvoiceAssetAmount, + erinFundingAmount-fabiaInvoiceAssetAmount, + ) + + t.Logf("Closing Charlie -> Dave channel") + closeAssetChannelAndAssert( + t, net, charlie, dave, chanPointCD, assetID, nil, universeTap, + noOpCoOpCloseBalanceCheck, + ) + + t.Logf("Closing Dave -> Yara channel, close initiated by Yara") + closeAssetChannelAndAssert( + t, net, yara, dave, chanPointDY, assetID, nil, universeTap, + noOpCoOpCloseBalanceCheck, + ) + + t.Logf("Closing Erin -> Fabia channel") + closeAssetChannelAndAssert( + t, net, erin, fabia, chanPointEF, assetID, nil, universeTap, + noOpCoOpCloseBalanceCheck, + ) +} diff --git a/itest/litd_test.go b/itest/litd_test.go index ab5a6e618..2736baf6c 100644 --- a/itest/litd_test.go +++ b/itest/litd_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/btcsuite/btclog" "github.com/lightningnetwork/lnd/build" "github.com/lightningnetwork/lnd/lntest" "github.com/lightningnetwork/lnd/signal" @@ -131,6 +132,10 @@ func (h *harnessTest) setupLogging() { require.NoError(h.t, err) interceptor = &ic + UseLogger(build.NewSubLogger(Subsystem, func(tag string) btclog.Logger { + return logWriter.GenSubLogger(tag, func() {}) + })) + err = build.ParseAndSetDebugLevels("debug", logWriter) require.NoError(h.t, err) } diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index d07248fd9..1726b928e 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -56,4 +56,8 @@ var allTestCases = []*testCase{ name: "test custom channels single asset multi input", test: testCustomChannelsSingleAssetMultiInput, }, + { + name: "test custom channels oracle pricing", + test: testCustomChannelsOraclePricing, + }, } diff --git a/itest/log.go b/itest/log.go new file mode 100644 index 000000000..67211af57 --- /dev/null +++ b/itest/log.go @@ -0,0 +1,24 @@ +package itest + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +const Subsystem = "ITST" + +// log is a logger that is initialized with no output filters. This means the +// package will not perform any logging by default until the caller requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger(Subsystem, nil)) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/itest/oracle_test.go b/itest/oracle_test.go new file mode 100644 index 000000000..8f7cfd0c5 --- /dev/null +++ b/itest/oracle_test.go @@ -0,0 +1,279 @@ +package itest + +import ( + "context" + "crypto/tls" + "encoding/hex" + "fmt" + "net" + "testing" + "time" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightninglabs/taproot-assets/rfqmsg" + oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" + "github.com/lightningnetwork/lnd/cert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +// oracleHarness is a basic integration test RPC price oracle server harness. +type oracleHarness struct { + oraclerpc.UnimplementedPriceOracleServer + + listenAddr string + + grpcListener net.Listener + grpcServer *grpc.Server + + purchasePrices map[asset.ID]rfqmath.BigIntFixedPoint + salePrices map[asset.ID]rfqmath.BigIntFixedPoint +} + +func newOracleHarness(listenAddr string) *oracleHarness { + return &oracleHarness{ + listenAddr: listenAddr, + purchasePrices: make(map[asset.ID]rfqmath.BigIntFixedPoint), + salePrices: make(map[asset.ID]rfqmath.BigIntFixedPoint), + } +} + +func (o *oracleHarness) setPrice(assetID asset.ID, purchasePrice, + salePrice rfqmath.BigIntFixedPoint) { + + o.purchasePrices[assetID] = purchasePrice + o.salePrices[assetID] = salePrice +} + +func (o *oracleHarness) start(t *testing.T) { + // Start the mock RPC price oracle service. + // + // Generate self-signed certificate. This allows us to use TLS for the + // gRPC server. + tlsCert, err := generateSelfSignedCert() + require.NoError(t, err) + + // Create the gRPC server with TLS + transportCredentials := credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + }) + o.grpcServer = grpc.NewServer(grpc.Creds(transportCredentials)) + + serviceAddr := fmt.Sprintf("rfqrpc://%s", o.listenAddr) + log.Infof("Starting RPC price oracle service at address: %s\n", + serviceAddr) + + oraclerpc.RegisterPriceOracleServer(o.grpcServer, o) + + go func() { + var err error + o.grpcListener, err = net.Listen("tcp", o.listenAddr) + if err != nil { + log.Errorf("Error oracle listening: %v", err) + return + } + if err := o.grpcServer.Serve(o.grpcListener); err != nil { + log.Errorf("Error oracle serving: %v", err) + } + }() +} + +func (o *oracleHarness) stop() { + if o.grpcServer != nil { + o.grpcServer.Stop() + } + if o.grpcListener != nil { + _ = o.grpcListener.Close() + } +} + +// getAssetRates returns the asset rates for a given transaction type and +// subject asset max amount. +func (o *oracleHarness) getAssetRates(id asset.ID, + transactionType oraclerpc.TransactionType) (oraclerpc.AssetRates, + error) { + + // Determine the rate based on the transaction type. + var subjectAssetRate rfqmath.BigIntFixedPoint + if transactionType == oraclerpc.TransactionType_PURCHASE { + rate, ok := o.purchasePrices[id] + if !ok { + return oraclerpc.AssetRates{}, fmt.Errorf("purchase "+ + "price not found for asset ID=%v", id) + } + subjectAssetRate = rate + } else { + rate, ok := o.salePrices[id] + if !ok { + return oraclerpc.AssetRates{}, fmt.Errorf("sale "+ + "price not found for asset ID=%v", id) + } + subjectAssetRate = rate + } + + // Marshal subject asset rate to RPC format. + rpcSubjectAssetToBtcRate, err := oraclerpc.MarshalBigIntFixedPoint( + subjectAssetRate, + ) + if err != nil { + return oraclerpc.AssetRates{}, err + } + + // Marshal payment asset rate to RPC format. + rpcPaymentAssetToBtcRate, err := oraclerpc.MarshalBigIntFixedPoint( + rfqmsg.MilliSatPerBtc, + ) + if err != nil { + return oraclerpc.AssetRates{}, err + } + + expiry := time.Now().Add(5 * time.Minute).Unix() + return oraclerpc.AssetRates{ + SubjectAssetRate: rpcSubjectAssetToBtcRate, + PaymentAssetRate: rpcPaymentAssetToBtcRate, + ExpiryTimestamp: uint64(expiry), + }, nil +} + +// QueryAssetRates queries the asset rates for a given transaction type, subject +// asset, and payment asset. An asset rate is the number of asset units per +// BTC. +// +// Example use case: +// +// Alice is trying to pay an invoice by spending an asset. Alice therefore +// requests that Bob (her asset channel counterparty) purchase the asset from +// her. Bob's payment, in BTC, will pay the invoice. +// +// Alice requests a bid quote from Bob. Her request includes an asset rates hint +// (ask). Alice obtains the asset rates hint by calling this endpoint. She sets: +// - `SubjectAsset` to the asset she is trying to sell. +// - `SubjectAssetMaxAmount` to the max channel asset outbound. +// - `PaymentAsset` to BTC. +// - `TransactionType` to SALE. +// - `AssetRateHint` to nil. +// +// Bob calls this endpoint to get the bid quote asset rates that he will send as +// a response to Alice's request. He sets: +// - `SubjectAsset` to the asset that Alice is trying to sell. +// - `SubjectAssetMaxAmount` to the value given in Alice's quote request. +// - `PaymentAsset` to BTC. +// - `TransactionType` to PURCHASE. +// - `AssetRateHint` to the value given in Alice's quote request. +func (o *oracleHarness) QueryAssetRates(_ context.Context, + req *oraclerpc.QueryAssetRatesRequest) ( + *oraclerpc.QueryAssetRatesResponse, error) { + + // Ensure that the payment asset is BTC. We only support BTC as the + // payment asset in this example. + if !oraclerpc.IsAssetBtc(req.PaymentAsset) { + log.Infof("Payment asset is not BTC: %v", req.PaymentAsset) + + return &oraclerpc.QueryAssetRatesResponse{ + Result: &oraclerpc.QueryAssetRatesResponse_Error{ + Error: &oraclerpc.QueryAssetRatesErrResponse{ + Message: "unsupported payment asset, " + + "only BTC is supported", + }, + }, + }, nil + } + + // Ensure that the subject asset is set correctly. + subjectAssetID, err := parseSubjectAsset(req.SubjectAsset) + if err != nil { + log.Errorf("Error parsing subject asset: %v", err) + return nil, fmt.Errorf("error parsing subject asset: %w", err) + } + + _, hasPurchase := o.purchasePrices[subjectAssetID] + _, hasSale := o.salePrices[subjectAssetID] + + log.Infof("Have for asset=%x, purchase=%v, sale=%v", subjectAssetID[:], + hasPurchase, hasSale) + + // Ensure that the subject asset is supported. + if !hasPurchase || !hasSale { + log.Infof("Unsupported subject asset ID str: %v\n", + req.SubjectAsset) + + return &oraclerpc.QueryAssetRatesResponse{ + Result: &oraclerpc.QueryAssetRatesResponse_Error{ + Error: &oraclerpc.QueryAssetRatesErrResponse{ + Message: "unsupported subject asset", + }, + }, + }, nil + } + + assetRates, err := o.getAssetRates(subjectAssetID, req.TransactionType) + if err != nil { + return nil, err + } + + log.Infof("QueryAssetRates returning rates (subject_asset_rate=%v, "+ + "payment_asset_rate=%v)", assetRates.SubjectAssetRate, + assetRates.PaymentAssetRate) + + return &oraclerpc.QueryAssetRatesResponse{ + Result: &oraclerpc.QueryAssetRatesResponse_Ok{ + Ok: &oraclerpc.QueryAssetRatesOkResponse{ + AssetRates: &assetRates, + }, + }, + }, nil +} + +// parseSubjectAsset parses the subject asset from the given asset specifier. +func parseSubjectAsset(subjectAsset *oraclerpc.AssetSpecifier) (asset.ID, + error) { + + // Ensure that the subject asset is set. + if subjectAsset == nil { + return asset.ID{}, fmt.Errorf("subject asset is not set (nil)") + } + + // Check the subject asset bytes if set. + var subjectAssetID asset.ID + switch { + case len(subjectAsset.GetAssetId()) > 0: + copy(subjectAssetID[:], subjectAsset.GetAssetId()) + + case len(subjectAsset.GetAssetIdStr()) > 0: + assetIDBytes, err := hex.DecodeString( + subjectAsset.GetAssetIdStr(), + ) + if err != nil { + return asset.ID{}, fmt.Errorf("error decoding asset "+ + "ID hex string: %w", err) + } + + copy(subjectAssetID[:], assetIDBytes) + + default: + return asset.ID{}, fmt.Errorf("subject asset ID bytes and ID " + + "str not set") + } + + return subjectAssetID, nil +} + +// generateSelfSignedCert generates a self-signed TLS certificate and private +// key. +func generateSelfSignedCert() (tls.Certificate, error) { + certBytes, keyBytes, err := cert.GenCertPair( + "itest price oracle", nil, nil, false, 24*time.Hour, + ) + if err != nil { + return tls.Certificate{}, err + } + + tlsCert, err := tls.X509KeyPair(certBytes, keyBytes) + if err != nil { + return tls.Certificate{}, err + } + + return tlsCert, nil +}