diff --git a/pkg/abi/abi.go b/pkg/abi/abi.go index 4a1e88e7..dce418de 100644 --- a/pkg/abi/abi.go +++ b/pkg/abi/abi.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -439,6 +439,30 @@ func (e *Entry) String() string { return s } +// SolString returns a Solidity - like string, with an empty function definition, +// and any struct definitions separated afterwards with a ; (semicolon) +func (e *Entry) SolString() string { + solStr, err := e.SolidityStringCtx(context.Background()) + if err != nil { + log.L(context.Background()).Warnf("ABI parsing failed: %s", err) + } + return solStr +} + +func (e *Entry) SolidityStringCtx(ctx context.Context) (string, error) { + solDef, childStructs, err := e.SolidityDefCtx(ctx) + if err != nil { + return "", err + } + buff := new(strings.Builder) + buff.WriteString(solDef) + for _, e := range childStructs { + buff.WriteString("; ") + buff.WriteString(e) + } + return buff.String(), nil +} + func (e *Entry) Signature() (string, error) { return e.SignatureCtx(context.Background()) } @@ -670,6 +694,63 @@ func (e *Entry) SignatureCtx(ctx context.Context) (string, error) { return buff.String(), nil } +func (e *Entry) SolidityDef() (string, []string, error) { + return e.SolidityDefCtx(context.Background()) +} + +// SolidityDefCtx returns a Solidity-like descriptor of the entry, including its type +func (e *Entry) SolidityDefCtx(ctx context.Context) (string, []string, error) { + // Everything apart from event and error is a type of function + isFunction := e.Type != Error && e.Type != Event + + allChildStructs := []string{} + buff := new(strings.Builder) + buff.WriteString(string(e.Type)) + buff.WriteRune(' ') + buff.WriteString(e.Name) + buff.WriteRune('(') + for i, p := range e.Inputs { + if i > 0 { + buff.WriteString(", ") + } + s, childStructs, err := p.SolidityDefCtx(ctx, isFunction) + if err != nil { + return "", nil, err + } + allChildStructs = append(allChildStructs, childStructs...) + buff.WriteString(s) + } + buff.WriteRune(')') + + if isFunction { + buff.WriteString(" external") + if e.StateMutability != "" && + // The state mutability nonpayable is reflected in Solidity by not specifying a state mutability modifier at all. + e.StateMutability != NonPayable { + buff.WriteRune(' ') + buff.WriteString(string(e.StateMutability)) + } + if len(e.Outputs) > 0 { + buff.WriteString(" returns (") + for i, p := range e.Outputs { + if i > 0 { + buff.WriteString(", ") + } + s, childStructs, err := p.SolidityDefCtx(ctx, isFunction) + if err != nil { + return "", nil, err + } + allChildStructs = append(allChildStructs, childStructs...) + buff.WriteString(s) + } + buff.WriteRune(')') + } + buff.WriteString(" { }") + } + + return buff.String(), allChildStructs, nil +} + // Validate processes all the components of the type of this ABI parameter. // - The elementary type // - The fixed/variable length array dimensions @@ -701,6 +782,16 @@ func (p *Parameter) SignatureStringCtx(ctx context.Context) (string, error) { return tc.String(), nil } +func (p *Parameter) SolidityDefCtx(ctx context.Context, inFunction bool) (string, []string, error) { + // Ensure the type component tree has been parsed + tc, err := p.TypeComponentTreeCtx(ctx) + if err != nil { + return "", nil, err + } + solDef, childStructs := tc.SolidityParamDef(inFunction) + return solDef, childStructs, nil +} + // String returns the signature string. If a Validate needs to be initiated, and that // parse fails, then the error is logged, but is not returned func (p *Parameter) String() string { diff --git a/pkg/abi/abi_test.go b/pkg/abi/abi_test.go index 1181f63c..853ec61b 100644 --- a/pkg/abi/abi_test.go +++ b/pkg/abi/abi_test.go @@ -29,147 +29,262 @@ import ( ) const sampleABI1 = `[ - { - "name": "foo", - "type": "function", - "inputs": [ - { - "name": "a", - "type": "tuple", - "components": [ - { - "name": "b", - "type": "uint" - }, - { - "name": "c", - "type": "string[2]" - }, - { - "name": "d", - "type": "bytes" - } - ] - } - ], - "outputs": [] - } + { + "name": "foo", + "type": "function", + "inputs": [ + { + "name": "a", + "type": "tuple", + "internalType": "struct AType", + "components": [ + { + "name": "b", + "type": "uint" + }, + { + "name": "c", + "type": "string[2]" + }, + { + "name": "d", + "type": "bytes" + } + ] + } + ], + "outputs": [ + { + "name": "e", + "type": "uint256" + }, + { + "name": "f", + "type": "string" + } + ] + } ]` const sampleABI2 = `[ - { - "name": "foo", - "type": "function", - "inputs": [ - { - "name": "a", - "type": "uint8" - }, - { - "name": "b", - "type": "int" - }, - { - "name": "c", - "type": "address" - }, - { - "name": "d", - "type": "bool" - }, - { - "name": "e", - "type": "fixed64x10" - }, - { - "name": "f", - "type": "ufixed" - }, - { - "name": "g", - "type": "bytes10" - }, - { - "name": "h", - "type": "bytes" - }, - { - "name": "i", - "type": "function" - }, - { - "name": "j", - "type": "string" - } - ], - "outputs": [] - } + { + "name": "foo", + "type": "function", + "inputs": [ + { + "name": "a", + "type": "uint8" + }, + { + "name": "b", + "type": "int" + }, + { + "name": "c", + "type": "address" + }, + { + "name": "d", + "type": "bool" + }, + { + "name": "e", + "type": "fixed64x10" + }, + { + "name": "f", + "type": "ufixed" + }, + { + "name": "g", + "type": "bytes10" + }, + { + "name": "h", + "type": "bytes" + }, + { + "name": "i", + "type": "function" + }, + { + "name": "j", + "type": "string" + } + ], + "outputs": [] + } ]` const sampleABI3 = `[ - { - "type": "constructor", - "inputs": [ - { - "name": "a", - "type": "tuple", - "components": [ - { - "name": "b", - "type": "uint" - }, - { - "name": "c", - "type": "string[2]" - }, - { - "name": "d", - "type": "bytes" - } - ] - } - ], - "outputs": [] - }, - { - "name": "foo", - "type": "function", - "inputs": [ - { - "name": "a", - "type": "tuple", - "components": [ - { - "name": "b", - "type": "uint" - }, - { - "name": "c", - "type": "string[2]" - }, - { - "name": "d", - "type": "bytes" - } - ] - } - ], - "outputs": [] - } + { + "type": "constructor", + "inputs": [ + { + "name": "a", + "type": "tuple", + "components": [ + { + "name": "b", + "type": "uint" + }, + { + "name": "c", + "type": "string[2]" + }, + { + "name": "d", + "type": "bytes" + } + ] + } + ], + "outputs": [] + }, + { + "name": "foo", + "type": "function", + "inputs": [ + { + "name": "a", + "type": "tuple", + "components": [ + { + "name": "b", + "type": "uint" + }, + { + "name": "c", + "type": "string[2]" + }, + { + "name": "d", + "type": "bytes" + } + ] + } + ], + "outputs": [] + } ]` const sampleABI4 = `[ - { - "name": "simple", - "type": "function", - "inputs": [ - { - "name": "a", - "type": "string" - } - ], - "outputs": [] - } + { + "name": "simple", + "type": "function", + "inputs": [ + { + "name": "a", + "type": "string" + } + ], + "outputs": [] + } + ]` + +const sampleABI5 = `[ + { + "anonymous": false, + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "locator", + "type": "bytes32" + } + ], + "indexed": false, + "internalType": "struct AribtraryWidgets.Customer", + "name": "customer", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "string[]", + "name": "attributes", + "type": "string[]" + } + ], + "indexed": false, + "internalType": "struct AribtraryWidgets.Widget[]", + "name": "widgets", + "type": "tuple[]" + } + ], + "name": "Invoiced", + "type": "event" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "locator", + "type": "bytes32" + } + ], + "internalType": "struct AribtraryWidgets.Customer", + "name": "customer", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "string", + "name": "description", + "type": "string" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "string[]", + "name": "attributes", + "type": "string[]" + } + ], + "internalType": "struct AribtraryWidgets.Widget[]", + "name": "widgets", + "type": "tuple[]" + } + ], + "internalType": "struct AribtraryWidgets.Invoice", + "name": "_invoice", + "type": "tuple" + } + ], + "name": "invoice", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } ]` func testABI(t *testing.T, abiJSON string) (abi ABI) { @@ -181,31 +296,31 @@ func testABI(t *testing.T, abiJSON string) (abi ABI) { func TestDocsFunctionCallExample(t *testing.T) { transferABI := `[ - { - "inputs": [ - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - } - ]` + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } + ]` // Parse the ABI definition var abi ABI @@ -214,9 +329,9 @@ func TestDocsFunctionCallExample(t *testing.T) { // Parse some JSON input data conforming to the ABI encodedValueTree, _ := f.Inputs.ParseJSON([]byte(`{ - "recipient": "0x03706Ff580119B130E7D26C5e816913123C24d89", - "amount": "1000000000000000000" - }`)) + "recipient": "0x03706Ff580119B130E7D26C5e816913123C24d89", + "amount": "1000000000000000000" + }`)) // We can serialize this directly to abi bytes abiData, _ := encodedValueTree.EncodeABIData() @@ -252,22 +367,28 @@ func TestDocsFunctionCallExample(t *testing.T) { assert.Equal(t, `{"amount":"1000000000000000000","recipient":"03706ff580119b130e7d26c5e816913123c24d89"}`, string(jsonData)) assert.Equal(t, `["0x03706ff580119b130e7d26c5e816913123c24d89","0xde0b6b3a7640000"]`, string(jsonData2)) assert.Equal(t, "0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b", sigHash.String()) + + // Check the solidity def + solDef, childStructs, err := f.SolidityDef() + assert.NoError(t, err) + assert.Equal(t, "function transfer(address recipient, uint256 amount) external returns (bool) { }", solDef) + assert.Empty(t, childStructs) } func TestTLdr(t *testing.T) { sampleABI, _ := ParseABI([]byte(`[ - { - "name": "transfer", - "inputs": [ - {"name": "recipient", "internalType": "address", "type": "address" }, - {"name": "amount", "internalType": "uint256", "type": "uint256"} - ], - "outputs": [{"internalType": "bool", "type": "bool"}], - "stateMutability": "nonpayable", - "type": "function" - } - ]`)) + { + "name": "transfer", + "inputs": [ + {"name": "recipient", "internalType": "address", "type": "address" }, + {"name": "amount", "internalType": "uint256", "type": "uint256"} + ], + "outputs": [{"internalType": "bool", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function" + } + ]`)) transferABIFn := sampleABI.Functions()["transfer"] sampleABICallBytes, _ := transferABIFn.EncodeCallDataJSON([]byte( `{"recipient":"0x4a0d852ebb58fc88cb260bb270ae240f72edc45b","amount":"100000000000000000"}`, @@ -294,6 +415,11 @@ func TestABIGetTupleTypeTree(t *testing.T) { tc, err := abi[0].Inputs[0].TypeComponentTree() assert.NoError(t, err) + solDef, childStructs, err := abi[0].SolidityDef() + assert.NoError(t, err) + assert.Equal(t, "function foo(AType memory a) external returns (uint256 e, string memory f) { }", solDef) + assert.Equal(t, []string{"struct AType { uint256 b; string[2] c; bytes d; }"}, childStructs) + assert.Equal(t, TupleComponent, tc.ComponentType()) assert.Len(t, tc.TupleChildren(), 3) assert.Equal(t, "(uint256,string[2],bytes)", tc.String()) @@ -315,18 +441,18 @@ func TestABIGetTupleTypeTree(t *testing.T) { func TestABIModifyReParse(t *testing.T) { abiString := `[ - { - "name": "foo", - "type": "function", - "inputs": [ - { - "name": "a", - "type": "uint256" - } - ], - "outputs": [] - } - ]` + { + "name": "foo", + "type": "function", + "inputs": [ + { + "name": "a", + "type": "uint256" + } + ], + "outputs": [] + } + ]` var abi ABI err := json.Unmarshal([]byte(abiString), &abi) assert.NoError(t, err) @@ -348,18 +474,18 @@ func TestABIModifyReParse(t *testing.T) { func TestABIModifyBadInputs(t *testing.T) { abiString := `[ - { - "name": "foo", - "type": "function", - "inputs": [ - { - "name": "a", - "type": "uint-1" - } - ], - "outputs": [] - } - ]` + { + "name": "foo", + "type": "function", + "inputs": [ + { + "name": "a", + "type": "uint-1" + } + ], + "outputs": [] + } + ]` var abi ABI err := json.Unmarshal([]byte(abiString), &abi) assert.NoError(t, err) @@ -383,18 +509,18 @@ func TestABIModifyBadInputs(t *testing.T) { func TestABIModifyBadOutputs(t *testing.T) { abiString := `[ - { - "name": "foo", - "type": "function", - "inputs": [], - "outputs": [ - { - "name": "a", - "type": "uint-1" - } - ] - } - ]` + { + "name": "foo", + "type": "function", + "inputs": [], + "outputs": [ + { + "name": "a", + "type": "uint-1" + } + ] + } + ]` var abi ABI err := json.Unmarshal([]byte(abiString), &abi) assert.NoError(t, err) @@ -417,12 +543,12 @@ func TestParseJSONObjectModeOk(t *testing.T) { inputs := testABI(t, sampleABI1)[0].Inputs values := `{ - "a": { - "b": 12345, - "c": ["string1", "string2"], - "d": "0xfeedbeef" - } - }` + "a": { + "b": 12345, + "c": ["string1", "string2"], + "d": "0xfeedbeef" + } + }` var jv interface{} err := json.Unmarshal([]byte(values), &jv) assert.NoError(t, err) @@ -443,12 +569,12 @@ func TestParseJSONArrayModeOk(t *testing.T) { inputs := testABI(t, sampleABI1)[0].Inputs values := `[ - [ - 12345, - ["string1", "string2"], - "0xfeedbeef" - ] - ]` + [ + 12345, + ["string1", "string2"], + "0xfeedbeef" + ] + ]` cv, err := inputs.ParseJSON([]byte(values)) assert.NoError(t, err) @@ -466,12 +592,12 @@ func TestParseJSONMixedModeOk(t *testing.T) { inputs := testABI(t, sampleABI1)[0].Inputs values := `[ - { - "b": 12345, - "c": ["string1", "string2"], - "d": "feedbeef" - } - ]` + { + "b": 12345, + "c": ["string1", "string2"], + "d": "feedbeef" + } + ]` cv, err := inputs.ParseJSON([]byte(values)) assert.NoError(t, err) @@ -513,17 +639,17 @@ func TestParseJSONArrayLotsOfTypes(t *testing.T) { inputs := testABI(t, sampleABI2)[0].Inputs values := `[ - "-12345", - "0x12345", - "0x4a0d852eBb58FC88Cb260Bb270AE240f72EdC45B", - true, - "-1.2345", - 1.2345, - "0xfeedbeef", - "00010203040506070809", - "00", - "test string" - ]` + "-12345", + "0x12345", + "0x4a0d852eBb58FC88Cb260Bb270AE240f72EdC45B", + true, + "-1.2345", + 1.2345, + "0xfeedbeef", + "00010203040506070809", + "00", + "test string" + ]` cv, err := inputs.ParseJSON([]byte(values)) assert.NoError(t, err) @@ -543,6 +669,11 @@ func TestParseJSONArrayLotsOfTypes(t *testing.T) { assert.Equal(t, "0x00", ethtypes.HexBytes0xPrefix(cv.Children[8].Value.([]byte)).String()) assert.Equal(t, "test string", cv.Children[9].Value) + solDef, childStructs, err := testABI(t, sampleABI2)[0].SolidityDef() + assert.NoError(t, err) + assert.Equal(t, "function foo(uint8 a, int256 b, address c, bool d, fixed64x10 e, ufixed128x18 f, bytes10 g, bytes memory h, function i, string memory j) external { }", solDef) + assert.Empty(t, childStructs) + } func TestParseJSONBadData(t *testing.T) { @@ -554,18 +685,18 @@ func TestParseJSONBadData(t *testing.T) { func TestParseJSONBadABI(t *testing.T) { inputs := testABI(t, `[ - { - "name": "foo", - "type": "function", - "inputs": [ - { - "name": "a", - "type": "wrong" - } - ], - "outputs": [] - } - ]`)[0].Inputs + { + "name": "foo", + "type": "function", + "inputs": [ + { + "name": "a", + "type": "wrong" + } + ], + "outputs": [] + } + ]`)[0].Inputs _, err := inputs.ParseJSON([]byte(`{}`)) assert.Regexp(t, "FF22025", err) @@ -573,18 +704,18 @@ func TestParseJSONBadABI(t *testing.T) { func TestEncodeABIDataCtxBadABI(t *testing.T) { f := testABI(t, `[ - { - "name": "foo", - "type": "function", - "inputs": [ - { - "name": "a", - "type": "wrong" - } - ], - "outputs": [] - } - ]`)[0] + { + "name": "foo", + "type": "function", + "inputs": [ + { + "name": "a", + "type": "wrong" + } + ], + "outputs": [] + } + ]`)[0] _, err := f.EncodeCallData(nil) assert.Regexp(t, "FF22025", err) } @@ -606,7 +737,23 @@ func TestSignatureHashInvalid(t *testing.T) { _, err := e.SignatureHash() assert.Regexp(t, "FF22025", err) + _, _, err = e.SolidityDef() + assert.Regexp(t, "FF22025", err) + + assert.Empty(t, e.SolString()) + assert.Equal(t, make(ethtypes.HexBytes0xPrefix, 32), e.SignatureHashBytes()) + + e = &Entry{ + Outputs: ParameterArray{ + { + Type: "foobar", + }, + }, + } + _, _, err = e.SolidityDef() + assert.Regexp(t, "FF22025", err) + } func TestDecodeEventIndexedOnly(t *testing.T) { @@ -642,10 +789,10 @@ func TestDecodeEventIndexedOnly(t *testing.T) { assert.NoError(t, err) assert.JSONEq(t, `{ - "from": "0000000000000000000000000000000000000000", - "to": "fb075bb99f2aa4c49955bf703509a227d7a12248", - "tokenId": "2333" - }`, string(j)) + "from": "0000000000000000000000000000000000000000", + "to": "fb075bb99f2aa4c49955bf703509a227d7a12248", + "tokenId": "2333" + }`, string(j)) } func TestDecodeEventMixed(t *testing.T) { @@ -694,13 +841,13 @@ func TestDecodeEventMixed(t *testing.T) { assert.NoError(t, err) assert.JSONEq(t, `{ - "indexed1": "11111", - "indexed2": "3968ef051b422d3d1cdc182a88bba8dd922e6fa4", - "unindexed1": "22222", - "unindexed2": true, - "indexed3": "592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba", - "unindexed3": "Hello World" - }`, string(j)) + "indexed1": "11111", + "indexed2": "3968ef051b422d3d1cdc182a88bba8dd922e6fa4", + "unindexed1": "22222", + "unindexed2": true, + "indexed3": "592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba", + "unindexed3": "Hello World" + }`, string(j)) } func TestDecodeEventBadABI(t *testing.T) { @@ -847,3 +994,31 @@ func TestEncodeCallDataValuesHelper(t *testing.T) { func TestABIDocumented(t *testing.T) { ffapi.CheckObjectDocumented(&ABI{}) } + +func TestComplexStructSolidityDef(t *testing.T) { + + var abi ABI + err := json.Unmarshal([]byte(sampleABI5), &abi) + assert.NoError(t, err) + + solDef, childStructs, err := abi.Functions()["invoice"].SolidityDef() + assert.NoError(t, err) + assert.Equal(t, "function invoice(Invoice memory _invoice) external payable { }", solDef) + assert.Equal(t, []string{ + "struct Customer { address owner; bytes32 locator; }", + "struct Widget { string description; uint256 price; string[] attributes; }", + "struct Invoice { Customer customer; Widget[] widgets; }", + }, childStructs) + + assert.Equal(t, "function invoice(Invoice memory _invoice) external payable { }; struct Customer { address owner; bytes32 locator; }; struct Widget { string description; uint256 price; string[] attributes; }; struct Invoice { Customer customer; Widget[] widgets; }", + abi.Functions()["invoice"].SolString()) + + solDef, childStructs, err = abi.Events()["Invoiced"].SolidityDef() + assert.NoError(t, err) + assert.Equal(t, "event Invoiced(Customer customer, Widget[] widgets)", solDef) + assert.Equal(t, []string{ + "struct Customer { address owner; bytes32 locator; }", + "struct Widget { string description; uint256 price; string[] attributes; }", + }, childStructs) + +} diff --git a/pkg/abi/abidecode_test.go b/pkg/abi/abidecode_test.go index a7ded4a0..ddab3719 100644 --- a/pkg/abi/abidecode_test.go +++ b/pkg/abi/abidecode_test.go @@ -252,7 +252,6 @@ func TestExampleABIDecode7(t *testing.T) { // a tuple of dynamic types (which is the same as a fixed-length array of the dynamic types) f := &Entry{ - Name: "g", Outputs: ParameterArray{ { Type: "tuple", @@ -285,7 +284,6 @@ func TestExampleABIDecode7(t *testing.T) { func TestExampleABIDecodeTupleDifferentOrder(t *testing.T) { f := &Entry{ - Name: "coupon", Outputs: ParameterArray{ { Type: "tuple[]", @@ -316,7 +314,6 @@ func TestExampleABIDecodeTupleDifferentOrder(t *testing.T) { func TestExampleABIDecodeTuple(t *testing.T) { f := &Entry{ - Name: "coupon", Outputs: ParameterArray{ { Type: "tuple[]", @@ -351,7 +348,6 @@ func TestExampleABIDecodeTuple(t *testing.T) { func TestExampleABIDecodeNonDynamicTuple(t *testing.T) { f := &Entry{ - Name: "coupon", Outputs: ParameterArray{ { Type: "tuple", @@ -378,7 +374,6 @@ func TestExampleABIDecodeNonDynamicTuple(t *testing.T) { func TestExampleABIDecodeDoubleNestedTuple(t *testing.T) { f := &Entry{ - Name: "f", Outputs: ParameterArray{ { Type: "tuple[]", @@ -442,7 +437,6 @@ func TestExampleABIDecodeDoubleNestedTuple(t *testing.T) { func TestExampleABIDecodeStaticNestedTupleInDynamicTuple(t *testing.T) { f := &Entry{ - Name: "f", Outputs: ParameterArray{ { Type: "tuple[]", @@ -498,7 +492,6 @@ func TestExampleABIDecodeStaticNestedTupleInDynamicTuple(t *testing.T) { func TestExampleABIDecodeDoubleStaticNestedTuple(t *testing.T) { f := &Entry{ - Name: "f", Outputs: ParameterArray{ { Type: "tuple[]", @@ -548,7 +541,6 @@ func TestExampleABIDecodeDoubleStaticNestedTuple(t *testing.T) { func TestExampleABIDecodeTupleFixed(t *testing.T) { f := &Entry{ - Name: "coupon", Outputs: ParameterArray{ { Type: "tuple[1]", @@ -573,7 +565,6 @@ func TestExampleABIDecode8(t *testing.T) { // a fixed-length array of dynamic types f := &Entry{ - Name: "g", Outputs: ParameterArray{ {Type: "string[2]"}, }, diff --git a/pkg/abi/abiencode.go b/pkg/abi/abiencode.go index f16f7dad..4c12719e 100644 --- a/pkg/abi/abiencode.go +++ b/pkg/abi/abiencode.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // diff --git a/pkg/abi/outputserialization.go b/pkg/abi/outputserialization.go index b97be12c..5ca8adca 100644 --- a/pkg/abi/outputserialization.go +++ b/pkg/abi/outputserialization.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // diff --git a/pkg/abi/signedi256.go b/pkg/abi/signedi256.go index 02d42f9d..1e11856d 100644 --- a/pkg/abi/signedi256.go +++ b/pkg/abi/signedi256.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // diff --git a/pkg/abi/typecomponents.go b/pkg/abi/typecomponents.go index 2740cccd..00b1abdc 100644 --- a/pkg/abi/typecomponents.go +++ b/pkg/abi/typecomponents.go @@ -19,6 +19,7 @@ package abi import ( "context" "fmt" + "regexp" "strconv" "strings" @@ -26,6 +27,9 @@ import ( "github.com/hyperledger/firefly-signer/internal/signermsgs" ) +// The format of the "internalType" in the Solidity compiler is of the form "struct MySmartContract.MyStruct[]` +var internalTypeStructExtractor = regexp.MustCompile(`^struct (.*\.)?([^.\[\]]+)(\[\d*\])*$`) + // TypeComponent is a modelled representation of a component of an ABI type. // We don't just go to the tuple level, we go down all the way through the arrays too. // This breaks things down into the way in which they are serialized/parsed. @@ -61,6 +65,10 @@ type TypeComponent interface { ParseExternalDesc(ctx context.Context, v interface{}, desc string) (*ComponentValue, error) DecodeABIData(d []byte, offset int) (*ComponentValue, error) DecodeABIDataCtx(ctx context.Context, d []byte, offest int) (*ComponentValue, error) + + SolidityParamDef(inFunction bool) (solDef string, structDefs []string) // gives a string that can be used to define this param in solidity + SolidityTypeDef() (isRef bool, typeDef string, childStructs []string) + SolidityStructDef() (structName string, structs []string) } type typeComponent struct { @@ -363,6 +371,69 @@ func (tc *typeComponent) String() string { } } +func (tc *typeComponent) SolidityParamDef(inFunction bool) (string, []string) { + isRef, paramDef, childStructs := tc.SolidityTypeDef() + if isRef && inFunction { + paramDef = fmt.Sprintf("%s memory", paramDef) + } + if tc.parameter != nil && tc.parameter.Name != "" { + paramDef = fmt.Sprintf("%s %s", paramDef, tc.parameter.Name) + } + return paramDef, childStructs +} + +func (tc *typeComponent) SolidityTypeDef() (isRef bool, typeDef string, childStructs []string) { + switch tc.cType { + case ElementaryComponent: + isRef = tc.elementaryType.dynamic(tc) + return isRef, fmt.Sprintf("%s%s", tc.elementaryType.name, tc.elementarySuffix), []string{} + case FixedArrayComponent: + _, childSol, childStructs := tc.arrayChild.SolidityTypeDef() + return true, fmt.Sprintf("%s[%d]", childSol, tc.arrayLength), childStructs + case DynamicArrayComponent: + _, childSol, childStructs := tc.arrayChild.SolidityTypeDef() + return true, fmt.Sprintf("%s[]", childSol), childStructs + case TupleComponent: + structName, childStructs := tc.SolidityStructDef() + return true, structName, childStructs + default: + return false, "", []string{} + } +} + +func (tc *typeComponent) SolidityStructDef() (string, []string) { + name := "" + if tc.parameter != nil { + name = tc.parameter.Name + match := internalTypeStructExtractor.FindStringSubmatch(tc.parameter.InternalType) + if match != nil { + name = match[2] + } + } + buff := new(strings.Builder) + buff.WriteString("struct ") + buff.WriteString(name) + buff.WriteString(" { ") + + allChildStructs := []string{} + for i, child := range tc.tupleChildren { + if i > 0 { + buff.WriteString(" ") + } + _, childType, childStructs := child.SolidityTypeDef() + allChildStructs = append(allChildStructs, childStructs...) + buff.WriteString(childType) + if child.parameter != nil && child.parameter.Name != "" { + buff.WriteRune(' ') + buff.WriteString(child.parameter.Name) + } + buff.WriteString(";") + } + + buff.WriteString(" }") + return name, append(allChildStructs, buff.String()) +} + func (tc *typeComponent) ComponentType() ComponentType { return tc.cType } diff --git a/pkg/abi/typecomponents_test.go b/pkg/abi/typecomponents_test.go index 64986708..7586183a 100644 --- a/pkg/abi/typecomponents_test.go +++ b/pkg/abi/typecomponents_test.go @@ -671,6 +671,11 @@ func TestTypeComponentStringInvalid(t *testing.T) { } assert.Empty(t, tc.String()) + isRef, solDef, childStructs := tc.SolidityTypeDef() + assert.False(t, isRef) + assert.Empty(t, solDef) + assert.Empty(t, childStructs) + } func TestTypeComponentParseExternalOk(t *testing.T) { diff --git a/pkg/keystorev3/pbkdf2.go b/pkg/keystorev3/pbkdf2.go index ba234d2e..7fb358b6 100644 --- a/pkg/keystorev3/pbkdf2.go +++ b/pkg/keystorev3/pbkdf2.go @@ -21,7 +21,6 @@ import ( "encoding/json" "fmt" - "github.com/hyperledger/firefly-signer/pkg/secp256k1" "golang.org/x/crypto/pbkdf2" ) @@ -29,11 +28,12 @@ const ( prfHmacSHA256 = "hmac-sha256" ) -func readPbkdf2WalletFile(jsonWallet []byte, password []byte) (WalletFile, error) { +func readPbkdf2WalletFile(jsonWallet []byte, password []byte, metadata map[string]interface{}) (WalletFile, error) { var w *walletFilePbkdf2 if err := json.Unmarshal(jsonWallet, &w); err != nil { return nil, fmt.Errorf("invalid pbkdf2 keystore: %s", err) } + w.metadata = metadata return w, w.decrypt(password) } @@ -45,9 +45,6 @@ func (w *walletFilePbkdf2) decrypt(password []byte) (err error) { derivedKey := pbkdf2.Key(password, w.Crypto.KDFParams.Salt, w.Crypto.KDFParams.C, w.Crypto.KDFParams.DKLen, sha256.New) w.privateKey, err = w.Crypto.decryptCommon(derivedKey) - if err == nil { - w.keypair, err = secp256k1.NewSecp256k1KeyPair(w.privateKey) - } return err } diff --git a/pkg/keystorev3/pbkdf2_test.go b/pkg/keystorev3/pbkdf2_test.go index 4fee98de..1ae5df52 100644 --- a/pkg/keystorev3/pbkdf2_test.go +++ b/pkg/keystorev3/pbkdf2_test.go @@ -42,10 +42,15 @@ func TestPbkdf2Wallet(t *testing.T) { w1 := &walletFilePbkdf2{ walletFileBase: walletFileBase{ - Address: ethtypes.AddressPlainHex(keypair.Address), - ID: fftypes.NewUUID(), - Version: version3, - keypair: keypair, + walletFileCoreFields: walletFileCoreFields{ + ID: fftypes.NewUUID(), + Version: version3, + }, + walletFileMetadata: walletFileMetadata{ + metadata: map[string]interface{}{ + "address": ethtypes.AddressPlainHex(keypair.Address).String(), + }, + }, }, Crypto: cryptoPbkdf2{ cryptoCommon: cryptoCommon{ @@ -78,14 +83,14 @@ func TestPbkdf2Wallet(t *testing.T) { func TestPbkdf2WalletFileDecryptInvalid(t *testing.T) { - _, err := readPbkdf2WalletFile([]byte(`!! not json`), []byte("")) + _, err := readPbkdf2WalletFile([]byte(`!! not json`), []byte(""), nil) assert.Regexp(t, "invalid pbkdf2 keystore", err) } func TestPbkdf2WalletFileUnsupportedPRF(t *testing.T) { - _, err := readPbkdf2WalletFile([]byte(`{}`), []byte("")) + _, err := readPbkdf2WalletFile([]byte(`{}`), []byte(""), nil) assert.Regexp(t, "invalid pbkdf2 wallet file: unsupported prf", err) } diff --git a/pkg/keystorev3/scrypt.go b/pkg/keystorev3/scrypt.go index 43671776..15362ad9 100644 --- a/pkg/keystorev3/scrypt.go +++ b/pkg/keystorev3/scrypt.go @@ -29,11 +29,12 @@ import ( const defaultR = 8 -func readScryptWalletFile(jsonWallet []byte, password []byte) (WalletFile, error) { +func readScryptWalletFile(jsonWallet []byte, password []byte, metadata map[string]interface{}) (WalletFile, error) { var w *walletFileScrypt if err := json.Unmarshal(jsonWallet, &w); err != nil { return nil, fmt.Errorf("invalid scrypt wallet file: %s", err) } + w.metadata = metadata return w, w.decrypt(password) } @@ -46,14 +47,14 @@ func mustGenerateDerivedScryptKey(password string, salt []byte, n, p int) []byte } // creates an ethereum address wallet file -func newScryptWalletFile(password string, keypair *secp256k1.KeyPair, n int, p int) WalletFile { - wf := newScryptWalletFileBytes(password, keypair.PrivateKeyBytes(), ethtypes.AddressPlainHex(keypair.Address), n, p) - wf.keypair = keypair +func newScryptWalletFileSecp256k1(password string, keypair *secp256k1.KeyPair, n int, p int) WalletFile { + wf := newScryptWalletFileBytes(password, keypair.PrivateKeyBytes(), n, p) + wf.Metadata()["address"] = ethtypes.AddressPlainHex(keypair.Address).String() return wf } // this allows creation of any size/type of key in the store -func newScryptWalletFileBytes(password string, privateKey []byte, addr ethtypes.AddressPlainHex, n int, p int) *walletFileScrypt { +func newScryptWalletFileBytes(password string, privateKey []byte, n int, p int) *walletFileScrypt { // Generate a sale for the scrypt salt := mustReadBytes(32, rand.Reader) @@ -75,9 +76,13 @@ func newScryptWalletFileBytes(password string, privateKey []byte, addr ethtypes. return &walletFileScrypt{ walletFileBase: walletFileBase{ - ID: fftypes.NewUUID(), - Address: addr, - Version: version3, + walletFileCoreFields: walletFileCoreFields{ + ID: fftypes.NewUUID(), + Version: version3, + }, + walletFileMetadata: walletFileMetadata{ + metadata: map[string]interface{}{}, + }, privateKey: privateKey, }, Crypto: cryptoScrypt{ @@ -107,8 +112,5 @@ func (w *walletFileScrypt) decrypt(password []byte) error { return fmt.Errorf("invalid scrypt keystore: %s", err) } w.privateKey, err = w.Crypto.decryptCommon(derivedKey) - if err == nil { - w.keypair, err = secp256k1.NewSecp256k1KeyPair(w.privateKey) - } return err } diff --git a/pkg/keystorev3/scrypt_test.go b/pkg/keystorev3/scrypt_test.go index bf058e5f..e1696969 100644 --- a/pkg/keystorev3/scrypt_test.go +++ b/pkg/keystorev3/scrypt_test.go @@ -58,7 +58,7 @@ func TestScryptWalletRoundTripStandard(t *testing.T) { func TestScryptReadInvalidFile(t *testing.T) { - _, err := readScryptWalletFile([]byte(`!bad JSON`), []byte("")) + _, err := readScryptWalletFile([]byte(`!bad JSON`), []byte(""), nil) assert.Error(t, err) } diff --git a/pkg/keystorev3/wallet.go b/pkg/keystorev3/wallet.go index 1d7245b4..7e11cc9f 100644 --- a/pkg/keystorev3/wallet.go +++ b/pkg/keystorev3/wallet.go @@ -21,7 +21,6 @@ import ( "fmt" "io" - "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/hyperledger/firefly-signer/pkg/secp256k1" "golang.org/x/crypto/sha3" ) @@ -33,32 +32,28 @@ const ( ) func NewWalletFileLight(password string, keypair *secp256k1.KeyPair) WalletFile { - return newScryptWalletFile(password, keypair, nLight, pDefault) + return newScryptWalletFileSecp256k1(password, keypair, nLight, pDefault) } func NewWalletFileStandard(password string, keypair *secp256k1.KeyPair) WalletFile { - return newScryptWalletFile(password, keypair, nStandard, pDefault) -} - -func addressFirst32(privateKey []byte) ethtypes.AddressPlainHex { - if len(privateKey) > 32 { - privateKey = privateKey[0:32] - } - kp, _ := secp256k1.NewSecp256k1KeyPair(privateKey) - return ethtypes.AddressPlainHex(kp.Address) + return newScryptWalletFileSecp256k1(password, keypair, nStandard, pDefault) } func NewWalletFileCustomBytesLight(password string, privateKey []byte) WalletFile { - return newScryptWalletFileBytes(password, privateKey, addressFirst32(privateKey), nStandard, pDefault) + return newScryptWalletFileBytes(password, privateKey, nStandard, pDefault) } func NewWalletFileCustomBytesStandard(password string, privateKey []byte) WalletFile { - return newScryptWalletFileBytes(password, privateKey, addressFirst32(privateKey), nStandard, pDefault) + return newScryptWalletFileBytes(password, privateKey, nStandard, pDefault) } func ReadWalletFile(jsonWallet []byte, password []byte) (WalletFile, error) { var w walletFileCommon - if err := json.Unmarshal(jsonWallet, &w); err != nil { + err := json.Unmarshal(jsonWallet, &w) + if err == nil { + err = json.Unmarshal(jsonWallet, &w.metadata) + } + if err != nil { return nil, fmt.Errorf("invalid wallet file: %s", err) } if w.ID == nil { @@ -69,9 +64,9 @@ func ReadWalletFile(jsonWallet []byte, password []byte) (WalletFile, error) { } switch w.Crypto.KDF { case kdfTypeScrypt: - return readScryptWalletFile(jsonWallet, password) + return readScryptWalletFile(jsonWallet, password, w.metadata) case kdfTypePbkdf2: - return readPbkdf2WalletFile(jsonWallet, password) + return readPbkdf2WalletFile(jsonWallet, password, w.metadata) default: return nil, fmt.Errorf("unsupported kdf: %s", w.Crypto.KDF) } diff --git a/pkg/keystorev3/wallet_test.go b/pkg/keystorev3/wallet_test.go index 43ea17a1..270bf5ca 100644 --- a/pkg/keystorev3/wallet_test.go +++ b/pkg/keystorev3/wallet_test.go @@ -18,6 +18,7 @@ package keystorev3 import ( "encoding/hex" + "encoding/json" "fmt" "testing" "testing/iotest" @@ -164,3 +165,33 @@ func TestWalletFileCustomBytesLight(t *testing.T) { assert.NoError(t, err) assert.Equal(t, kp.Address, w2.KeyPair().Address) } + +func TestMarshalWalletJSONFail(t *testing.T) { + _, err := marshalWalletJSON(&walletFileBase{}, map[bool]bool{false: true}) + assert.Error(t, err) +} + +func TestWalletFileCustomBytesUnsetAddress(t *testing.T) { + customBytes := ([]byte)("something deterministic for testing") + + w := NewWalletFileCustomBytesLight("correcthorsebatterystaple", customBytes) + + w.Metadata()["address"] = nil + w.Metadata()["myKeyIdentifier"] = "something I know works for me" + w.Metadata()["id"] = "attempting to set this does not work" + w.Metadata()["version"] = 42 + + jsonBytes, err := json.Marshal(w) + assert.NoError(t, err) + + var roundTripBackFromJSON map[string]interface{} + err = json.Unmarshal(jsonBytes, &roundTripBackFromJSON) + assert.NoError(t, err) + + _, hasAddress := roundTripBackFromJSON["address"] + assert.False(t, hasAddress) + assert.Equal(t, "something I know works for me", roundTripBackFromJSON["myKeyIdentifier"]) + assert.Equal(t, float64(w.GetVersion()), roundTripBackFromJSON["version"]) + assert.Equal(t, w.GetID().String(), roundTripBackFromJSON["id"]) + +} diff --git a/pkg/keystorev3/walletfile.go b/pkg/keystorev3/walletfile.go index cfecca36..0d506553 100644 --- a/pkg/keystorev3/walletfile.go +++ b/pkg/keystorev3/walletfile.go @@ -37,6 +37,16 @@ type WalletFile interface { PrivateKey() []byte KeyPair() *secp256k1.KeyPair JSON() []byte + GetID() *fftypes.UUID + GetVersion() int + + // Any fields set into this that do not conflict with the base fields (id/version/crypto) will + // be serialized into the JSON when it is marshalled. + // This includes setting the "address" field (which is not a core part of the V3 standard) to + // an arbitrary string, adding new fields for different key identifiers (like "bjj" or "btc" for + // different public key compression algos). + // If you want to remove the address field completely, simple set "address": nil in the map. + Metadata() map[string]interface{} } type kdfParamsScrypt struct { @@ -76,13 +86,20 @@ type cryptoPbkdf2 struct { KDFParams kdfParamsPbkdf2 `json:"kdfparams"` } -type walletFileBase struct { - Address ethtypes.AddressPlainHex `json:"address"` - ID *fftypes.UUID `json:"id"` - Version int `json:"version"` +type walletFileCoreFields struct { + ID *fftypes.UUID `json:"id"` + Version int `json:"version"` +} + +type walletFileMetadata struct { + // arbitrary additional fields that can be stored in the JSON, including overriding/removing the "address" field (other core fields cannot be overridden) + metadata map[string]interface{} +} +type walletFileBase struct { + walletFileCoreFields + walletFileMetadata privateKey []byte - keypair *secp256k1.KeyPair } type walletFileCommon struct { @@ -95,13 +112,51 @@ type walletFilePbkdf2 struct { Crypto cryptoPbkdf2 `json:"crypto"` } +func (w *walletFilePbkdf2) MarshalJSON() ([]byte, error) { + return marshalWalletJSON(&w.walletFileBase, w.Crypto) +} + type walletFileScrypt struct { walletFileBase Crypto cryptoScrypt `json:"crypto"` } +func (w *walletFileScrypt) MarshalJSON() ([]byte, error) { + return marshalWalletJSON(&w.walletFileBase, w.Crypto) +} + +func (w *walletFileBase) GetVersion() int { + return w.Version +} + +func (w *walletFileBase) GetID() *fftypes.UUID { + return w.ID +} + +func (w *walletFileBase) Metadata() map[string]interface{} { + return w.metadata +} + +func marshalWalletJSON(wc *walletFileBase, crypto interface{}) ([]byte, error) { + cryptoJSON, err := json.Marshal(crypto) + if err != nil { + return nil, err + } + jsonMap := map[string]interface{}{} + for k, v := range wc.metadata { + if v != nil { + jsonMap[k] = v + } + } + // cannot override these fields + jsonMap["id"] = wc.ID + jsonMap["version"] = wc.Version + jsonMap["crypto"] = json.RawMessage(cryptoJSON) + return json.Marshal(jsonMap) +} + func (w *walletFileBase) KeyPair() *secp256k1.KeyPair { - return w.keypair + return secp256k1.KeyPairFromBytes(w.privateKey) } func (w *walletFileBase) PrivateKey() []byte { diff --git a/pkg/secp256k1/keypair.go b/pkg/secp256k1/keypair.go index 0b1ca1da..a21e79e2 100644 --- a/pkg/secp256k1/keypair.go +++ b/pkg/secp256k1/keypair.go @@ -1,4 +1,4 @@ -// Copyright © 2022 Kaleido, Inc. +// Copyright © 2024 Kaleido, Inc. // // SPDX-License-Identifier: Apache-2.0 // @@ -43,11 +43,17 @@ func GenerateSecp256k1KeyPair() (*KeyPair, error) { return wrapSecp256k1Key(key, key.PubKey()), nil } +// Deprecated: Note there is no error condition returned by this function (use KeyPairFromBytes) func NewSecp256k1KeyPair(b []byte) (*KeyPair, error) { key, pubKey := btcec.PrivKeyFromBytes(b) return wrapSecp256k1Key(key, pubKey), nil } +func KeyPairFromBytes(b []byte) *KeyPair { + key, pubKey := btcec.PrivKeyFromBytes(b) + return wrapSecp256k1Key(key, pubKey) +} + func wrapSecp256k1Key(key *btcec.PrivateKey, pubKey *btcec.PublicKey) *KeyPair { return &KeyPair{ PrivateKey: key,