diff --git a/cmd/tapcli/assets.go b/cmd/tapcli/assets.go index 2c37af289..74671e455 100644 --- a/cmd/tapcli/assets.go +++ b/cmd/tapcli/assets.go @@ -2,7 +2,6 @@ package main import ( "encoding/hex" - "encoding/json" "fmt" "math" "os" @@ -159,9 +158,7 @@ func parseAssetType(ctx *cli.Context) (taprpc.AssetType, error) { } } -func parseMetaType(metaType string, - metaBytes []byte) (taprpc.AssetMetaType, error) { - +func parseMetaType(metaType string) (taprpc.AssetMetaType, error) { switch metaType { case "opaque": fallthrough @@ -169,12 +166,6 @@ func parseMetaType(metaType string, return taprpc.AssetMetaType_META_TYPE_OPAQUE, nil case "json": - // If JSON is selected, the bytes must be valid. - if !json.Valid(metaBytes) { - return 0, fmt.Errorf("invalid JSON for meta: %s", - metaBytes) - } - return taprpc.AssetMetaType_META_TYPE_JSON, nil // Otherwise, this is a custom meta type, we may not understand it, but @@ -232,24 +223,56 @@ func mintAsset(ctx *cli.Context) error { } } - metaTypeStr := ctx.String(assetMetaTypeName) + var ( + metaTypeStr = ctx.String(assetMetaTypeName) + metaBytes = ctx.String(assetMetaBytesName) + metaFilePath = ctx.String(assetMetaFilePathName) + decDisplay = ctx.Uint64(assetDecimalDisplayName) + ) - // Both the meta bytes and the meta path can be set. + if decDisplay > math.MaxUint32 { + return fmt.Errorf("decimal display must be a valid uint32") + } + + metaType, err := parseMetaType(metaTypeStr) + if err != nil { + return fmt.Errorf("unable to parse meta type: %w", err) + } + + // Before setting a non-empty meta, reject invalid combinations of + // metadata-related flags. var assetMeta *taprpc.AssetMeta switch { - case ctx.String(assetMetaBytesName) != "" && - ctx.String(assetMetaFilePathName) != "": - return fmt.Errorf("meta bytes or meta file path cannot " + - "be both set") + case metaBytes != "" && metaFilePath != "": + return fmt.Errorf("meta bytes and meta file path cannot both " + + "be set") + + case metaBytes == "" && metaFilePath == "": + switch metaType { + // Opaque is the default if the meta_type flag is not set, so + // having empty metadata is allowed. + case taprpc.AssetMetaType_META_TYPE_OPAQUE: + case taprpc.AssetMetaType_META_TYPE_JSON: + // Set only the metadata type; if present, the decimal + // display will be added as the actual metadata later. + // The minter will ultimately reject empty metadata. + assetMeta = &taprpc.AssetMeta{ + Type: metaType, + } + // A custom meta type requires metadata to be present. + default: + return fmt.Errorf("metadata must be present for " + + "custom meta types") + } + } + + // One of meta bytes or the meta path can be set. + switch { case ctx.String(assetMetaBytesName) != "": assetMeta = &taprpc.AssetMeta{ Data: []byte(ctx.String(assetMetaBytesName)), - } - - assetMeta.Type, err = parseMetaType(metaTypeStr, assetMeta.Data) - if err != nil { - return fmt.Errorf("unable to parse meta type: %w", err) + Type: metaType, } case ctx.String(assetMetaFilePathName) != "": @@ -263,11 +286,7 @@ func mintAsset(ctx *cli.Context) error { assetMeta = &taprpc.AssetMeta{ Data: metaFileBytes, - } - - assetMeta.Type, err = parseMetaType(metaTypeStr, assetMeta.Data) - if err != nil { - return fmt.Errorf("unable to parse meta type: %w", err) + Type: metaType, } } @@ -304,12 +323,10 @@ func mintAsset(ctx *cli.Context) error { resp, err := client.MintAsset(ctxc, &mintrpc.MintAssetRequest{ Asset: &mintrpc.MintAsset{ - AssetType: assetType, - Name: ctx.String(assetTagName), - AssetMeta: assetMeta, - DecimalDisplay: uint32( - ctx.Uint64(assetDecimalDisplayName), - ), + AssetType: assetType, + Name: ctx.String(assetTagName), + AssetMeta: assetMeta, + DecimalDisplay: uint32(decDisplay), Amount: amount, NewGroupedAsset: ctx.Bool(assetNewGroupedAssetName), GroupedAsset: ctx.Bool(assetGroupedAssetName), diff --git a/itest/assertions.go b/itest/assertions.go index 5214c24f7..ca756f627 100644 --- a/itest/assertions.go +++ b/itest/assertions.go @@ -228,6 +228,25 @@ func AssetVersionCheck(version taprpc.AssetVersion) AssetCheck { } } +// AssetDecimalDisplayCheck returns a check function that tests an asset's +// decimal display value. The check function requires that the rpc Asset has a +// non-nil decimal display value. +func AssetDecimalDisplayCheck(decDisplay uint32) AssetCheck { + return func(a *taprpc.Asset) error { + if a.DecimalDisplay == nil { + return fmt.Errorf("asset decimal display is nil") + } + + if a.DecimalDisplay.DecimalDisplay != decDisplay { + return fmt.Errorf("unexpected asset decimal display, "+ + "got %v wanted %v", a.DecimalDisplay, + decDisplay) + } + + return nil + } +} + // GroupAssetsByName converts an unordered list of assets to a map of lists of // assets, where all assets in a list have the same name. func GroupAssetsByName(assets []*taprpc.Asset) map[string][]*taprpc.Asset { @@ -1817,6 +1836,9 @@ func AssertAssetsMinted(t *testing.T, tapClient TapdClient, AssetGroupTapscriptRootCheck( assetRequest.Asset.GroupTapscriptRoot, ), + AssetDecimalDisplayCheck( + assetRequest.Asset.DecimalDisplay, + ), ) assetList = append(assetList, mintedAsset) diff --git a/itest/asset_meta_test.go b/itest/asset_meta_test.go index 9ad6b1613..b988356df 100644 --- a/itest/asset_meta_test.go +++ b/itest/asset_meta_test.go @@ -173,7 +173,9 @@ func testMintAssetWithDecimalDisplayMetaField(t *harnessTest) { require.ErrorContains(t.t, err, "decimal display does not match") // If we update the decimal display to match the group anchor, minting - // should succeed. + // should succeed. We also unset the metadata to ensure that the decimal + // display is set as the sole JSON object if needed. + secondAssetReq.Asset.AssetMeta.Data = []byte{} secondAssetReq.Asset.DecimalDisplay = firstAsset.DecimalDisplay MintAssetsConfirmBatch( t.t, t.lndHarness.Miner.Client, t.tapd, diff --git a/itest/utils.go b/itest/utils.go index 45cb29e3c..914976a45 100644 --- a/itest/utils.go +++ b/itest/utils.go @@ -411,6 +411,9 @@ func FinalizeBatchUnconfirmed(t *testing.T, minerClient *rpcclient.Client, AssetGroupTapscriptRootCheck( assetRequest.Asset.GroupTapscriptRoot, ), + AssetDecimalDisplayCheck( + assetRequest.Asset.DecimalDisplay, + ), ) } diff --git a/rpcserver.go b/rpcserver.go index 6638ba3f1..adf5e331b 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -526,6 +526,16 @@ func (r *rpcServer) MintAsset(ctx context.Context, Type: metaType, } + // If a custom decimal display was requested correctly, but no + // metadata was provided, we'll set the metadata to an empty + // JSON object. The decimal display will be added as the only + // object. + if metaType == proof.MetaJson && req.Asset.DecimalDisplay != 0 { + if len(req.Asset.AssetMeta.Data) == 0 { + seedlingMeta.Data = []byte("{}") + } + } + err = seedlingMeta.Validate() if err != nil { return nil, err