Merge pull request #1264 from nspcc-dev/smartcontract/manifest/supported_standards

smartcontract: add list of supported standards to manifest
This commit is contained in:
Roman Khimov 2020-08-04 22:17:00 +03:00 committed by GitHub
commit ef53a45e7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 59 additions and 35 deletions

View file

@ -324,7 +324,9 @@ func initSmartContract(ctx *cli.Context) error {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
m := ProjectConfig{} m := ProjectConfig{
SupportedStandards: []string{},
}
b, err := yaml.Marshal(m) b, err := yaml.Marshal(m)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
@ -368,6 +370,7 @@ func contractCompile(ctx *cli.Context) error {
return err return err
} }
o.ContractFeatures = conf.GetFeatures() o.ContractFeatures = conf.GetFeatures()
o.ContractSupportedStandards = conf.SupportedStandards
} }
result, err := compiler.CompileAndSave(src, o) result, err := compiler.CompileAndSave(src, o)
@ -538,6 +541,7 @@ func testInvokeScript(ctx *cli.Context) error {
type ProjectConfig struct { type ProjectConfig struct {
HasStorage bool HasStorage bool
IsPayable bool IsPayable bool
SupportedStandards []string
Events []manifest.Event Events []manifest.Event
} }

View file

@ -35,8 +35,11 @@ type Options struct {
// The name of the output for contract manifest file. // The name of the output for contract manifest file.
ManifestFile string ManifestFile string
// Contract metadata. // Contract features.
ContractFeatures smartcontract.PropertyState ContractFeatures smartcontract.PropertyState
// The list of standards supported by the contract.
ContractSupportedStandards []string
} }
type buildInfo struct { type buildInfo struct {
@ -165,7 +168,7 @@ func CompileAndSave(src string, o *Options) ([]byte, error) {
} }
if o.ManifestFile != "" { if o.ManifestFile != "" {
m, err := di.ConvertToManifest(o.ContractFeatures) m, err := di.ConvertToManifest(o.ContractFeatures, o.ContractSupportedStandards...)
if err != nil { if err != nil {
return b, errors.Wrap(err, "failed to convert debug info to manifest") return b, errors.Wrap(err, "failed to convert debug info to manifest")
} }

View file

@ -359,7 +359,7 @@ func parsePairJSON(data []byte, sep string) (string, string, error) {
// ConvertToManifest converts contract to the manifest.Manifest struct for debugger. // ConvertToManifest converts contract to the manifest.Manifest struct for debugger.
// Note: manifest is taken from the external source, however it can be generated ad-hoc. See #1038. // Note: manifest is taken from the external source, however it can be generated ad-hoc. See #1038.
func (di *DebugInfo) ConvertToManifest(fs smartcontract.PropertyState) (*manifest.Manifest, error) { func (di *DebugInfo) ConvertToManifest(fs smartcontract.PropertyState, supportedStandards ...string) (*manifest.Manifest, error) {
var err error var err error
if di.MainPkg == "" { if di.MainPkg == "" {
return nil, errors.New("no Main method was found") return nil, errors.New("no Main method was found")
@ -384,6 +384,9 @@ func (di *DebugInfo) ConvertToManifest(fs smartcontract.PropertyState) (*manifes
result := manifest.NewManifest(di.Hash) result := manifest.NewManifest(di.Hash)
result.Features = fs result.Features = fs
if supportedStandards != nil {
result.SupportedStandards = supportedStandards
}
result.ABI = manifest.ABI{ result.ABI = manifest.ABI{
Hash: di.Hash, Hash: di.Hash,
Methods: methods, Methods: methods,

View file

@ -47,6 +47,7 @@ var _ interop.Contract = (*nep5TokenNative)(nil)
func newNEP5Native(name string) *nep5TokenNative { func newNEP5Native(name string) *nep5TokenNative {
n := &nep5TokenNative{ContractMD: *interop.NewContractMD(name)} n := &nep5TokenNative{ContractMD: *interop.NewContractMD(name)}
n.Manifest.SupportedStandards = []string{manifest.NEP5StandardName}
desc := newDescriptor("name", smartcontract.StringType) desc := newDescriptor("name", smartcontract.StringType)
md := newMethodAndPrice(n.Name, 0, smartcontract.NoneFlag) md := newMethodAndPrice(n.Name, 0, smartcontract.NoneFlag)

View file

@ -299,7 +299,7 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
} }
return c.GetContractState(hash) return c.GetContractState(hash)
}, },
serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"id":0,"script":"VgJXHwIMDWNvbnRyYWN0IGNhbGx4eVMTwEEFB5IWIXhKDANQdXSXJyQAAAAQVUGEGNYNIXJqeRDOeRHOU0FSoUH1IUURQCOPAgAASgwLdG90YWxTdXBwbHmXJxEAAABFAkBCDwBAI28CAABKDAhkZWNpbWFsc5cnDQAAAEUSQCNWAgAASgwEbmFtZZcnEgAAAEUMBFJ1YmxAIzwCAABKDAZzeW1ib2yXJxEAAABFDANSVUJAIyECAABKDAliYWxhbmNlT2aXJ2IAAAAQVUGEGNYNIXN5EM50bMoAFLQnIwAAAAwPaW52YWxpZCBhZGRyZXNzEVVBNtNSBiFFENsgQGtsUEEfLnsHIXUMCWJhbGFuY2VPZmxtUxPAQQUHkhYhRW1AI7IBAABKDAh0cmFuc2ZlcpcnKwEAABBVQYQY1g0hdnkQzncHbwfKABS0JyoAAAAMFmludmFsaWQgJ2Zyb20nIGFkZHJlc3MRVUE201IGIUUQ2yBAeRHOdwhvCMoAFLQnKAAAAAwUaW52YWxpZCAndG8nIGFkZHJlc3MRVUE201IGIUUQ2yBAeRLOdwlvCRC1JyIAAAAMDmludmFsaWQgYW1vdW50EVVBNtNSBiFFENsgQG5vB1BBHy57ByF3Cm8Kbwm1JyYAAAAMEmluc3VmZmljaWVudCBmdW5kcxFVQTbTUgYhRRDbIEBvCm8Jn3cKbm8HbwpTQVKhQfUhbm8IUEEfLnsHIXcLbwtvCZ53C25vCG8LU0FSoUH1IQwIdHJhbnNmZXJvB28IbwlUFMBBBQeSFiFFEUAjewAAAEoMBGluaXSXJ1AAAAAQVUGEGNYNIXcMEFVBh8PSZCF3DQJAQg8Adw5vDG8Nbw5TQVKhQfUhDAh0cmFuc2ZlcgwA2zBvDW8OVBTAQQUHkhYhRRFAIyMAAAAMEWludmFsaWQgb3BlcmF0aW9uQTbTUgY6IwUAAABFQA==","manifest":{"abi":{"hash":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176","methods":[],"events":[]},"groups":[],"features":{"payable":false,"storage":true},"permissions":null,"trusts":[],"safemethods":[],"extra":null},"hash":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176"}}`, serverResponse: `{"id":1,"jsonrpc":"2.0","result":{"id":0,"script":"VgJXHwIMDWNvbnRyYWN0IGNhbGx4eVMTwEEFB5IWIXhKDANQdXSXJyQAAAAQVUGEGNYNIXJqeRDOeRHOU0FSoUH1IUURQCOPAgAASgwLdG90YWxTdXBwbHmXJxEAAABFAkBCDwBAI28CAABKDAhkZWNpbWFsc5cnDQAAAEUSQCNWAgAASgwEbmFtZZcnEgAAAEUMBFJ1YmxAIzwCAABKDAZzeW1ib2yXJxEAAABFDANSVUJAIyECAABKDAliYWxhbmNlT2aXJ2IAAAAQVUGEGNYNIXN5EM50bMoAFLQnIwAAAAwPaW52YWxpZCBhZGRyZXNzEVVBNtNSBiFFENsgQGtsUEEfLnsHIXUMCWJhbGFuY2VPZmxtUxPAQQUHkhYhRW1AI7IBAABKDAh0cmFuc2ZlcpcnKwEAABBVQYQY1g0hdnkQzncHbwfKABS0JyoAAAAMFmludmFsaWQgJ2Zyb20nIGFkZHJlc3MRVUE201IGIUUQ2yBAeRHOdwhvCMoAFLQnKAAAAAwUaW52YWxpZCAndG8nIGFkZHJlc3MRVUE201IGIUUQ2yBAeRLOdwlvCRC1JyIAAAAMDmludmFsaWQgYW1vdW50EVVBNtNSBiFFENsgQG5vB1BBHy57ByF3Cm8Kbwm1JyYAAAAMEmluc3VmZmljaWVudCBmdW5kcxFVQTbTUgYhRRDbIEBvCm8Jn3cKbm8HbwpTQVKhQfUhbm8IUEEfLnsHIXcLbwtvCZ53C25vCG8LU0FSoUH1IQwIdHJhbnNmZXJvB28IbwlUFMBBBQeSFiFFEUAjewAAAEoMBGluaXSXJ1AAAAAQVUGEGNYNIXcMEFVBh8PSZCF3DQJAQg8Adw5vDG8Nbw5TQVKhQfUhDAh0cmFuc2ZlcgwA2zBvDW8OVBTAQQUHkhYhRRFAIyMAAAAMEWludmFsaWQgb3BlcmF0aW9uQTbTUgY6IwUAAABFQA==","manifest":{"abi":{"hash":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176","methods":[],"events":[]},"groups":[],"features":{"payable":false,"storage":true},"permissions":null,"trusts":[],"supportedstandards":[],"safemethods":[],"extra":null},"hash":"0x1b4357bff5a01bdf2a6581247cf9ed1e24629176"}}`,
result: func(c *Client) interface{} { result: func(c *Client) interface{} {
script, err := base64.StdEncoding.DecodeString("VgJXHwIMDWNvbnRyYWN0IGNhbGx4eVMTwEEFB5IWIXhKDANQdXSXJyQAAAAQVUGEGNYNIXJqeRDOeRHOU0FSoUH1IUURQCOPAgAASgwLdG90YWxTdXBwbHmXJxEAAABFAkBCDwBAI28CAABKDAhkZWNpbWFsc5cnDQAAAEUSQCNWAgAASgwEbmFtZZcnEgAAAEUMBFJ1YmxAIzwCAABKDAZzeW1ib2yXJxEAAABFDANSVUJAIyECAABKDAliYWxhbmNlT2aXJ2IAAAAQVUGEGNYNIXN5EM50bMoAFLQnIwAAAAwPaW52YWxpZCBhZGRyZXNzEVVBNtNSBiFFENsgQGtsUEEfLnsHIXUMCWJhbGFuY2VPZmxtUxPAQQUHkhYhRW1AI7IBAABKDAh0cmFuc2ZlcpcnKwEAABBVQYQY1g0hdnkQzncHbwfKABS0JyoAAAAMFmludmFsaWQgJ2Zyb20nIGFkZHJlc3MRVUE201IGIUUQ2yBAeRHOdwhvCMoAFLQnKAAAAAwUaW52YWxpZCAndG8nIGFkZHJlc3MRVUE201IGIUUQ2yBAeRLOdwlvCRC1JyIAAAAMDmludmFsaWQgYW1vdW50EVVBNtNSBiFFENsgQG5vB1BBHy57ByF3Cm8Kbwm1JyYAAAAMEmluc3VmZmljaWVudCBmdW5kcxFVQTbTUgYhRRDbIEBvCm8Jn3cKbm8HbwpTQVKhQfUhbm8IUEEfLnsHIXcLbwtvCZ53C25vCG8LU0FSoUH1IQwIdHJhbnNmZXJvB28IbwlUFMBBBQeSFiFFEUAjewAAAEoMBGluaXSXJ1AAAAAQVUGEGNYNIXcMEFVBh8PSZCF3DQJAQg8Adw5vDG8Nbw5TQVKhQfUhDAh0cmFuc2ZlcgwA2zBvDW8OVBTAQQUHkhYhRRFAIyMAAAAMEWludmFsaWQgb3BlcmF0aW9uQTbTUgY6IwUAAABFQA==") script, err := base64.StdEncoding.DecodeString("VgJXHwIMDWNvbnRyYWN0IGNhbGx4eVMTwEEFB5IWIXhKDANQdXSXJyQAAAAQVUGEGNYNIXJqeRDOeRHOU0FSoUH1IUURQCOPAgAASgwLdG90YWxTdXBwbHmXJxEAAABFAkBCDwBAI28CAABKDAhkZWNpbWFsc5cnDQAAAEUSQCNWAgAASgwEbmFtZZcnEgAAAEUMBFJ1YmxAIzwCAABKDAZzeW1ib2yXJxEAAABFDANSVUJAIyECAABKDAliYWxhbmNlT2aXJ2IAAAAQVUGEGNYNIXN5EM50bMoAFLQnIwAAAAwPaW52YWxpZCBhZGRyZXNzEVVBNtNSBiFFENsgQGtsUEEfLnsHIXUMCWJhbGFuY2VPZmxtUxPAQQUHkhYhRW1AI7IBAABKDAh0cmFuc2ZlcpcnKwEAABBVQYQY1g0hdnkQzncHbwfKABS0JyoAAAAMFmludmFsaWQgJ2Zyb20nIGFkZHJlc3MRVUE201IGIUUQ2yBAeRHOdwhvCMoAFLQnKAAAAAwUaW52YWxpZCAndG8nIGFkZHJlc3MRVUE201IGIUUQ2yBAeRLOdwlvCRC1JyIAAAAMDmludmFsaWQgYW1vdW50EVVBNtNSBiFFENsgQG5vB1BBHy57ByF3Cm8Kbwm1JyYAAAAMEmluc3VmZmljaWVudCBmdW5kcxFVQTbTUgYhRRDbIEBvCm8Jn3cKbm8HbwpTQVKhQfUhbm8IUEEfLnsHIXcLbwtvCZ53C25vCG8LU0FSoUH1IQwIdHJhbnNmZXJvB28IbwlUFMBBBQeSFiFFEUAjewAAAEoMBGluaXSXJ1AAAAAQVUGEGNYNIXcMEFVBh8PSZCF3DQJAQg8Adw5vDG8Nbw5TQVKhQfUhDAh0cmFuc2ZlcgwA2zBvDW8OVBTAQQUHkhYhRRFAIyMAAAAMEWludmFsaWQgb3BlcmF0aW9uQTbTUgY6IwUAAABFQA==")
if err != nil { if err != nil {

View file

@ -8,11 +8,18 @@ import (
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
) )
// MaxManifestSize is a max length for a valid contract manifest. const (
const MaxManifestSize = 2048 // MaxManifestSize is a max length for a valid contract manifest.
MaxManifestSize = 2048
// MethodInit is a name for default initialization method. // MethodInit is a name for default initialization method.
const MethodInit = "_initialize" MethodInit = "_initialize"
// NEP5StandardName represents the name of NEP5 smartcontract standard.
NEP5StandardName = "NEP-5"
// NEP10StandardName represents the name of NEP10 smartcontract standard.
NEP10StandardName = "NEP-10"
)
// ABI represents a contract application binary interface. // ABI represents a contract application binary interface.
type ABI struct { type ABI struct {
@ -30,6 +37,8 @@ type Manifest struct {
// Features is a set of contract's features. // Features is a set of contract's features.
Features smartcontract.PropertyState Features smartcontract.PropertyState
Permissions []Permission Permissions []Permission
// SupportedStandards is a list of standards supported by the contract.
SupportedStandards []string
// Trusts is a set of hashes to a which contract trusts. // Trusts is a set of hashes to a which contract trusts.
Trusts WildUint160s Trusts WildUint160s
// SafeMethods is a set of names of safe methods. // SafeMethods is a set of names of safe methods.
@ -43,6 +52,7 @@ type manifestAux struct {
Groups []Group `json:"groups"` Groups []Group `json:"groups"`
Features map[string]bool `json:"features"` Features map[string]bool `json:"features"`
Permissions []Permission `json:"permissions"` Permissions []Permission `json:"permissions"`
SupportedStandards []string `json:"supportedstandards"`
Trusts *WildUint160s `json:"trusts"` Trusts *WildUint160s `json:"trusts"`
SafeMethods *WildStrings `json:"safemethods"` SafeMethods *WildStrings `json:"safemethods"`
Extra interface{} `json:"extra"` Extra interface{} `json:"extra"`
@ -58,6 +68,7 @@ func NewManifest(h util.Uint160) *Manifest {
}, },
Groups: []Group{}, Groups: []Group{},
Features: smartcontract.NoProperties, Features: smartcontract.NoProperties,
SupportedStandards: []string{},
} }
m.Trusts.Restrict() m.Trusts.Restrict()
m.SafeMethods.Restrict() m.SafeMethods.Restrict()
@ -120,6 +131,7 @@ func (m *Manifest) MarshalJSON() ([]byte, error) {
Groups: m.Groups, Groups: m.Groups,
Features: features, Features: features,
Permissions: m.Permissions, Permissions: m.Permissions,
SupportedStandards: m.SupportedStandards,
Trusts: &m.Trusts, Trusts: &m.Trusts,
SafeMethods: &m.SafeMethods, SafeMethods: &m.SafeMethods,
Extra: m.Extra, Extra: m.Extra,
@ -148,6 +160,7 @@ func (m *Manifest) UnmarshalJSON(data []byte) error {
m.Groups = aux.Groups m.Groups = aux.Groups
m.Permissions = aux.Permissions m.Permissions = aux.Permissions
m.SupportedStandards = aux.SupportedStandards
m.Extra = aux.Extra m.Extra = aux.Extra
return nil return nil

View file

@ -13,39 +13,39 @@ import (
// https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs#L10 // https://github.com/neo-project/neo/blob/master/tests/neo.UnitTests/SmartContract/Manifest/UT_ContractManifest.cs#L10
func TestManifest_MarshalJSON(t *testing.T) { func TestManifest_MarshalJSON(t *testing.T) {
t.Run("default", func(t *testing.T) { t.Run("default", func(t *testing.T) {
s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":[],"extra":null}` s := `{"groups":[],"features":{"storage":false,"payable":false},"supportedstandards":[],"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":[],"extra":null}`
m := testUnmarshalMarshalManifest(t, s) m := testUnmarshalMarshalManifest(t, s)
require.Equal(t, DefaultManifest(util.Uint160{}), m) require.Equal(t, DefaultManifest(util.Uint160{}), m)
}) })
// this vector is missing from original repo // this vector is missing from original repo
t.Run("features", func(t *testing.T) { t.Run("features", func(t *testing.T) {
s := `{"groups":[],"features":{"storage":true,"payable":true},"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":[],"extra":null}` s := `{"groups":[],"features":{"storage":true,"payable":true},"supportedstandards":[],"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":[],"extra":null}`
testUnmarshalMarshalManifest(t, s) testUnmarshalMarshalManifest(t, s)
}) })
t.Run("permissions", func(t *testing.T) { t.Run("permissions", func(t *testing.T) {
s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"0x0000000000000000000000000000000000000000","methods":["method1","method2"]}],"trusts":[],"safemethods":[],"extra":null}` s := `{"groups":[],"features":{"storage":false,"payable":false},"supportedstandards":[],"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"0x0000000000000000000000000000000000000000","methods":["method1","method2"]}],"trusts":[],"safemethods":[],"extra":null}`
testUnmarshalMarshalManifest(t, s) testUnmarshalMarshalManifest(t, s)
}) })
t.Run("safe methods", func(t *testing.T) { t.Run("safe methods", func(t *testing.T) {
s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":["balanceOf"],"extra":null}` s := `{"groups":[],"features":{"storage":false,"payable":false},"supportedstandards":[],"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":["balanceOf"],"extra":null}`
testUnmarshalMarshalManifest(t, s) testUnmarshalMarshalManifest(t, s)
}) })
t.Run("trust", func(t *testing.T) { t.Run("trust", func(t *testing.T) {
s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":["0x0000000000000000000000000000000000000001"],"safemethods":[],"extra":null}` s := `{"groups":[],"features":{"storage":false,"payable":false},"supportedstandards":[],"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":["0x0000000000000000000000000000000000000001"],"safemethods":[],"extra":null}`
testUnmarshalMarshalManifest(t, s) testUnmarshalMarshalManifest(t, s)
}) })
t.Run("groups", func(t *testing.T) { t.Run("groups", func(t *testing.T) {
s := `{"groups":[{"pubkey":"03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c","signature":"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ=="}],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":[],"extra":null}` s := `{"groups":[{"pubkey":"03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c","signature":"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQQ=="}],"supportedstandards":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":[],"extra":null}`
testUnmarshalMarshalManifest(t, s) testUnmarshalMarshalManifest(t, s)
}) })
t.Run("extra", func(t *testing.T) { t.Run("extra", func(t *testing.T) {
s := `{"groups":[],"features":{"storage":false,"payable":false},"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":[],"extra":{"key":"value"}}` s := `{"groups":[],"features":{"storage":false,"payable":false},"supportedstandards":[],"abi":{"hash":"0x0000000000000000000000000000000000000000","methods":[],"events":[]},"permissions":[{"contract":"*","methods":"*"}],"trusts":[],"safemethods":[],"extra":{"key":"value"}}`
testUnmarshalMarshalManifest(t, s) testUnmarshalMarshalManifest(t, s)
}) })
} }