Skip to content

Commit

Permalink
itest: add RPC oracle with spread based asset rates
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
guggero committed Oct 30, 2024
1 parent 1a655be commit 25d64a1
Show file tree
Hide file tree
Showing 6 changed files with 830 additions and 88 deletions.
167 changes: 155 additions & 12 deletions itest/assets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -774,19 +856,21 @@ 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)

result, err := getAssetPaymentResult(stream)
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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
Loading

0 comments on commit 25d64a1

Please sign in to comment.