diff --git a/cmd/litcli/ln.go b/cmd/litcli/ln.go index 67ef137e6..695721478 100644 --- a/cmd/litcli/ln.go +++ b/cmd/litcli/ln.go @@ -43,6 +43,7 @@ var lnCommands = []cli.Command{ sendPaymentCommand, payInvoiceCommand, addInvoiceCommand, + decodeAssetInvoiceCommand, }, }, } @@ -650,3 +651,65 @@ func addInvoice(ctx *cli.Context) error { return nil } + +var decodeAssetInvoiceCommand = cli.Command{ + Name: "decodeassetinvoice", + Category: "Payments", + Usage: "Decodes an LN invoice and displays the invoice's amount in asset " + + "units specified by an asset ID", + Description: ` + This command can be used to display the information encoded in an invoice. + Given a chosen asset_id, the invoice's amount expressed in units of the asset + will be displayed. + + Other information such as the decimal display of an asset, and the asset + group information (if applicable) are also shown. + `, + ArgsUsage: "--pay_req=X --asset_id=X", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "pay_req", + Usage: "a zpay32 encoded payment request to fulfill", + }, + assetIDFlag, + }, + Action: decodeAssetInvoice, +} + +func decodeAssetInvoice(ctx *cli.Context) error { + ctxb := context.Background() + + switch { + case !ctx.IsSet("pay_req"): + return fmt.Errorf("pay_req argument missing") + case !ctx.IsSet(assetIDFlag.Name): + return fmt.Errorf("the --asset_id flag must be set") + } + + payReq := ctx.String("pay_req") + + assetIDStr := ctx.String(assetIDFlag.Name) + assetIDBytes, err := hex.DecodeString(assetIDStr) + if err != nil { + return fmt.Errorf("unable to decode assetID: %v", err) + } + + tapdConn, cleanup, err := connectSuperMacClient(ctx) + if err != nil { + return fmt.Errorf("unable to make rpc con: %w", err) + } + defer cleanup() + + channelsClient := tchrpc.NewTaprootAssetChannelsClient(tapdConn) + resp, err := channelsClient.DecodeAssetPayReq(ctxb, &tchrpc.AssetPayReq{ + AssetId: assetIDBytes, + PayReqString: payReq, + }) + if err != nil { + return fmt.Errorf("error adding invoice: %w", err) + } + + printRespJSON(resp) + + return nil +} diff --git a/go.mod b/go.mod index 7169f01b2..5a3fe791c 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/lightninglabs/pool v0.6.5-beta.0.20241015105339-044cb451b5df github.com/lightninglabs/pool/auctioneerrpc v1.1.2 github.com/lightninglabs/pool/poolrpc v1.0.0 - github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241217185126-a9a2744061f1 + github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241219112156-ecba8f87eb24 github.com/lightningnetwork/lnd v0.18.4-beta.rc2 github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/fn v1.2.3 diff --git a/go.sum b/go.sum index e6a5bab85..4831508c4 100644 --- a/go.sum +++ b/go.sum @@ -1179,6 +1179,12 @@ github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9 github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241217185126-a9a2744061f1 h1:YAiqidrUkbWLtCVMdW/j7WM1gOP7CXHqLNKIEowVAsA= github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241217185126-a9a2744061f1/go.mod h1:rkSWHSkPXX2k+PBOkEE1BA3L3qq5+Yv3m6LGkoH3tQk= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241218162444-57c2454c050e h1:9YWq4/tGgrhRFKg7eyAtahxTMD6dQrl91xLFmd8KgmA= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241218162444-57c2454c050e/go.mod h1:rkSWHSkPXX2k+PBOkEE1BA3L3qq5+Yv3m6LGkoH3tQk= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241218173823-610300af2da1 h1:MaHjknFlcS4cosKsx3LSgPG4FoYQxXU84l5EFQTvP0w= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241218173823-610300af2da1/go.mod h1:rkSWHSkPXX2k+PBOkEE1BA3L3qq5+Yv3m6LGkoH3tQk= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241219112156-ecba8f87eb24 h1:dTVZCgUZ3CeQAKHLY+jyvMfhHQtutXT93hoKwgTsv2M= +github.com/lightninglabs/taproot-assets v0.5.0-rc2.0.20241219112156-ecba8f87eb24/go.mod h1:rkSWHSkPXX2k+PBOkEE1BA3L3qq5+Yv3m6LGkoH3tQk= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= github.com/lightningnetwork/lnd v0.18.4-beta h1:4pGmIjIMisrs4TMDYp4fk8NeI1YFpcuqwaSiFwLcd1g= diff --git a/itest/litd_custom_channels_test.go b/itest/litd_custom_channels_test.go index 398f66811..b7c9e0597 100644 --- a/itest/litd_custom_channels_test.go +++ b/itest/litd_custom_channels_test.go @@ -21,6 +21,7 @@ import ( "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightninglabs/taproot-assets/tapscript" @@ -3678,3 +3679,129 @@ func testCustomChannelsForwardBandwidth(ctxb context.Context, universeTap, noOpCoOpCloseBalanceCheck, ) } + +// testCustomChannelsDecodeAssetInvoice tests that we're able to properly +// decode and display asset invoice related information. +// +// TODO(roasbeef): just move to tapd repo due to new version that doesn't req a +// chan? +func testCustomChannelsDecodeAssetInvoice(ctx context.Context, + net *NetworkHarness, t *harnessTest) { + + // First, we'll set up some information for our custom oracle that we'll use + // to feed in price information. + 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, + )) + + // For this test, Zane will be our dedicated Universe server for all parties. + 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(), + )) + + // We'll just make a single node here, as this doesn't actually rely on a set + // of active channels. + alice, err := net.NewNode(t.t, "Alice", lndArgs, false, true, litdArgs...) + require.NoError(t.t, err) + aliceTap := newTapClient(t.t, alice) + + // Fund Alice so she'll have enough funds to mint the asset. + fundAllNodes(t.t, net, []*HarnessNode{alice}) + + // Next, we'll make a new asset with a specified decimal display. We'll also + // make grouped asset as well. + 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, + NewGroupedAsset: true, + } + + // Mint an asset on Charlie and sync Dave to Charlie as the universe. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner.Client, aliceTap, + []*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) + + // We'll assume a price of $100,000.00 USD for a single BTC. This is just the + // current subjective price our oracle will use. From this BTC price, we'll + // scale things up to be in the precision of the asset we minted above. + btcPrice := rfqmath.NewBigIntFixedPoint( + 100_000_00, 2, + ) + factor := rfqmath.NewBigInt( + big.NewInt(int64(math.Pow10(decimalDisplay))), + ) + btcPrice.Coefficient = btcPrice.Coefficient.Mul(factor) + oracle.setPrice(id, btcPrice, btcPrice) + + // Now we'll make a normal invoice for 1 BTC using Alice. + expirySeconds := 10 + amountSat := 100_000_000 + invoiceResp, err := alice.AddInvoice(ctxb, &lnrpc.Invoice{ + Value: int64(amountSat), + Memo: "normal invoice", + Expiry: int64(expirySeconds), + }) + require.NoError(t.t, err) + + payReq := invoiceResp.PaymentRequest + + // Now that we have our payment request, we'll call into the new decode asset + // pay req call. + decodeResp, err := aliceTap.DecodeAssetPayReq(ctxb, &tapchannelrpc.AssetPayReq{ + AssetId: assetID, + PayReqString: payReq, + }) + require.NoError(t.t, err) + + // The decimal display information, genesis, and asset group information + // should all match. + require.Equal( + t.t, int64(decimalDisplay), int64(decodeResp.DecimalDisplay.DecimalDisplay), + ) + require.Equal(t.t, usdAsset.AssetGenesis, decodeResp.GenesisInfo) + require.Equal(t.t, usdAsset.AssetGroup, decodeResp.AssetGroup) + + // The 1 BTC invoice should map to 100k asset units, with decimal display 6 + // that's 100 billion asset units. + const expectedUnits = 100_000_000_000 + require.Equal(t.t, int64(expectedUnits), int64(decodeResp.AssetAmount)) +} diff --git a/itest/litd_test_list_on_test.go b/itest/litd_test_list_on_test.go index 4d2cef88b..6e104fe6b 100644 --- a/itest/litd_test_list_on_test.go +++ b/itest/litd_test_list_on_test.go @@ -72,4 +72,8 @@ var allTestCases = []*testCase{ name: "test custom channels forward bandwidth", test: testCustomChannelsForwardBandwidth, }, + { + name: "test custom channels decode payreq", + test: testCustomChannelsDecodeAssetInvoice, + }, }