diff --git a/pkg/services/rpcsrv/params/param.go b/pkg/services/rpcsrv/params/param.go index 3e07f22f4..167af48d6 100644 --- a/pkg/services/rpcsrv/params/param.go +++ b/pkg/services/rpcsrv/params/param.go @@ -34,6 +34,13 @@ type ( Type smartcontract.ParamType `json:"type"` Value Param `json:"value"` } + + // FuncParamKV represents a pair of function argument parameters + // a slice of which is stored in FuncParam of [smartcontract.MapType] type. + FuncParamKV struct { + Key FuncParam `json:"key"` + Value FuncParam `json:"value"` + } ) var ( @@ -358,6 +365,21 @@ func (p *Param) GetFuncParam() (FuncParam, error) { return fp, err } +// GetFuncParamPair returns a pair of function call parameters. +func (p *Param) GetFuncParamPair() (FuncParamKV, error) { + if p == nil { + return FuncParamKV{}, errMissingParameter + } + // This one doesn't need to be cached, it's used only once. + fpp := FuncParamKV{} + err := json.Unmarshal(p.RawMessage, &fpp) + if err != nil { + return FuncParamKV{}, err + } + + return fpp, nil +} + // GetBytesHex returns a []byte value of the parameter if // it is a hex-encoded string. func (p *Param) GetBytesHex() ([]byte, error) { diff --git a/pkg/services/rpcsrv/params/param_test.go b/pkg/services/rpcsrv/params/param_test.go index d7d866e53..ff1a701d3 100644 --- a/pkg/services/rpcsrv/params/param_test.go +++ b/pkg/services/rpcsrv/params/param_test.go @@ -426,6 +426,48 @@ func TestParamGetFuncParam(t *testing.T) { require.NotNil(t, err) } +func TestParamGetFuncParamPair(t *testing.T) { + fp := FuncParam{ + Type: smartcontract.MapType, + Value: Param{RawMessage: []byte(`[{"key": {"type": "String", "value": "key1"}, "value": {"type": "Integer", "value": 123}}, {"key": {"type": "String", "value": "key2"}, "value": {"type": "Integer", "value": 456}}]`)}, + } + p := Param{RawMessage: []byte(`{"type": "Map", "value": [{"key": {"type": "String", "value": "key1"}, "value": {"type": "Integer", "value": 123}}, {"key": {"type": "String", "value": "key2"}, "value": {"type": "Integer", "value": 456}}]}`)} + newfp, err := p.GetFuncParam() + assert.Equal(t, fp, newfp) + require.NoError(t, err) + + kvs, err := newfp.Value.GetArray() + require.NoError(t, err) + + p1, err := kvs[0].GetFuncParamPair() + require.NoError(t, err) + + fp1Key := FuncParam{ + Type: smartcontract.StringType, + Value: Param{RawMessage: []byte(`"key1"`)}, + } + fp1Value := FuncParam{ + Type: smartcontract.IntegerType, + Value: Param{RawMessage: []byte(`123`)}, + } + assert.Equal(t, fp1Key, p1.Key) + assert.Equal(t, fp1Value, p1.Value) + + p2, err := kvs[1].GetFuncParamPair() + require.NoError(t, err) + + fp2Key := FuncParam{ + Type: smartcontract.StringType, + Value: Param{RawMessage: []byte(`"key2"`)}, + } + fp2Value := FuncParam{ + Type: smartcontract.IntegerType, + Value: Param{RawMessage: []byte(`456`)}, + } + assert.Equal(t, fp2Key, p2.Key) + assert.Equal(t, fp2Value, p2.Value) +} + func TestParamGetBytesHex(t *testing.T) { in := "602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7" inb, _ := hex.DecodeString(in) diff --git a/pkg/services/rpcsrv/params/txBuilder.go b/pkg/services/rpcsrv/params/txBuilder.go index 9d2c9ea2d..29db92533 100644 --- a/pkg/services/rpcsrv/params/txBuilder.go +++ b/pkg/services/rpcsrv/params/txBuilder.go @@ -13,6 +13,90 @@ import ( "github.com/nspcc-dev/neo-go/pkg/vm/opcode" ) +// ExpandFuncParameterIntoScript pushes provided FuncParam parameter +// into the given buffer. +func ExpandFuncParameterIntoScript(script *io.BinWriter, fp FuncParam) error { + switch fp.Type { + case smartcontract.ByteArrayType: + str, err := fp.Value.GetBytesBase64() + if err != nil { + return err + } + emit.Bytes(script, str) + case smartcontract.SignatureType: + str, err := fp.Value.GetBytesBase64() + if err != nil { + return err + } + emit.Bytes(script, str) + case smartcontract.StringType: + str, err := fp.Value.GetString() + if err != nil { + return err + } + emit.String(script, str) + case smartcontract.Hash160Type: + hash, err := fp.Value.GetUint160FromHex() + if err != nil { + return err + } + emit.Bytes(script, hash.BytesBE()) + case smartcontract.Hash256Type: + hash, err := fp.Value.GetUint256() + if err != nil { + return err + } + emit.Bytes(script, hash.BytesBE()) + case smartcontract.PublicKeyType: + str, err := fp.Value.GetString() + if err != nil { + return err + } + key, err := keys.NewPublicKeyFromString(string(str)) + if err != nil { + return err + } + emit.Bytes(script, key.Bytes()) + case smartcontract.IntegerType: + bi, err := fp.Value.GetBigInt() + if err != nil { + return err + } + emit.BigInt(script, bi) + case smartcontract.BoolType: + val, err := fp.Value.GetBoolean() // not GetBooleanStrict(), because that's the way C# code works + if err != nil { + return errors.New("not a bool") + } + emit.Bool(script, val) + case smartcontract.ArrayType: + val, err := fp.Value.GetArray() + if err != nil { + return err + } + err = ExpandArrayIntoScriptAndPack(script, val) + if err != nil { + return err + } + case smartcontract.MapType: + val, err := fp.Value.GetArray() + if err != nil { + return err + } + err = ExpandMapIntoScriptAndPack(script, val) + if err != nil { + return err + } + case smartcontract.AnyType: + if fp.Value.IsNull() || len(fp.Value.RawMessage) == 0 { + emit.Opcodes(script, opcode.PUSHNULL) + } + default: + return fmt.Errorf("parameter type %v is not supported", fp.Type) + } + return script.Err +} + // ExpandArrayIntoScript pushes all FuncParam parameters from the given array // into the given buffer in the reverse order. func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error { @@ -21,74 +105,9 @@ func ExpandArrayIntoScript(script *io.BinWriter, slice []Param) error { if err != nil { return err } - switch fp.Type { - case smartcontract.ByteArrayType: - str, err := fp.Value.GetBytesBase64() - if err != nil { - return err - } - emit.Bytes(script, str) - case smartcontract.SignatureType: - str, err := fp.Value.GetBytesBase64() - if err != nil { - return err - } - emit.Bytes(script, str) - case smartcontract.StringType: - str, err := fp.Value.GetString() - if err != nil { - return err - } - emit.String(script, str) - case smartcontract.Hash160Type: - hash, err := fp.Value.GetUint160FromHex() - if err != nil { - return err - } - emit.Bytes(script, hash.BytesBE()) - case smartcontract.Hash256Type: - hash, err := fp.Value.GetUint256() - if err != nil { - return err - } - emit.Bytes(script, hash.BytesBE()) - case smartcontract.PublicKeyType: - str, err := fp.Value.GetString() - if err != nil { - return err - } - key, err := keys.NewPublicKeyFromString(string(str)) - if err != nil { - return err - } - emit.Bytes(script, key.Bytes()) - case smartcontract.IntegerType: - bi, err := fp.Value.GetBigInt() - if err != nil { - return err - } - emit.BigInt(script, bi) - case smartcontract.BoolType: - val, err := fp.Value.GetBoolean() // not GetBooleanStrict(), because that's the way C# code works - if err != nil { - return errors.New("not a bool") - } - emit.Bool(script, val) - case smartcontract.ArrayType: - val, err := fp.Value.GetArray() - if err != nil { - return err - } - err = ExpandArrayIntoScriptAndPack(script, val) - if err != nil { - return err - } - case smartcontract.AnyType: - if fp.Value.IsNull() || len(fp.Value.RawMessage) == 0 { - emit.Opcodes(script, opcode.PUSHNULL) - } - default: - return fmt.Errorf("parameter type %v is not supported", fp.Type) + err = ExpandFuncParameterIntoScript(script, fp) + if err != nil { + return fmt.Errorf("param %d: %w", j, err) } } return script.Err @@ -110,6 +129,32 @@ func ExpandArrayIntoScriptAndPack(script *io.BinWriter, slice []Param) error { return script.Err } +// ExpandMapIntoScriptAndPack expands provided array of key-value items into script +// and packs the resulting pairs in the [stackitem.Map]. +func ExpandMapIntoScriptAndPack(script *io.BinWriter, slice []Param) error { + if len(slice) == 0 { + emit.Opcodes(script, opcode.NEWMAP) + return script.Err + } + for i := len(slice) - 1; i >= 0; i-- { + pair, err := slice[i].GetFuncParamPair() + if err != nil { + return err + } + err = ExpandFuncParameterIntoScript(script, pair.Value) + if err != nil { + return fmt.Errorf("map value %d: %w", i, err) + } + err = ExpandFuncParameterIntoScript(script, pair.Key) + if err != nil { + return fmt.Errorf("map key %d: %w", i, err) + } + } + emit.Int(script, int64(len(slice))) + emit.Opcodes(script, opcode.PACKMAP) + return script.Err +} + // CreateFunctionInvocationScript creates a script to invoke the given contract with // the given parameters. func CreateFunctionInvocationScript(contract util.Uint160, method string, param *Param) ([]byte, error) { diff --git a/pkg/services/rpcsrv/params/tx_builder_test.go b/pkg/services/rpcsrv/params/tx_builder_test.go index ed495b21b..8df559817 100644 --- a/pkg/services/rpcsrv/params/tx_builder_test.go +++ b/pkg/services/rpcsrv/params/tx_builder_test.go @@ -124,6 +124,17 @@ func TestExpandArrayIntoScript(t *testing.T) { Input: []Param{{RawMessage: []byte(`{"type": "Integer", "value": "` + bi.String() + `"}`)}}, Expected: append([]byte{byte(opcode.PUSHINT256)}, rawInt...), }, + { + Input: []Param{{RawMessage: []byte(`{"type": "Map", "value": [{"key": {"type": "String", "value": "a" }, "value": {"type": "Integer", "value": 1}}, {"key": {"type": "String", "value": "b"}, "value": {"type": "Integer", "value": 2}}]}`)}}, + Expected: []byte{ + byte(opcode.PUSH2), // value of element #2 + byte(opcode.PUSHDATA1), 1, byte('b'), // key of element #2 + byte(opcode.PUSH1), // value of element #1 + byte(opcode.PUSHDATA1), 1, byte('a'), // key of element #1 + byte(opcode.PUSH2), // map len + byte(opcode.PACKMAP), + }, + }, } for _, c := range testCases { script := io.NewBufBinWriter() @@ -142,6 +153,12 @@ func TestExpandArrayIntoScript(t *testing.T) { {RawMessage: []byte(`{"type": "Integer", "value": "` + new(big.Int).Lsh(big.NewInt(1), 255).String() + `"}`)}, }, + { + {RawMessage: []byte(`{"type": "Map", "value": [{"key": {"type": "InvalidT", "value": "a" }, "value": {"type": "Integer", "value": 1}}]}`)}, + }, + { + {RawMessage: []byte(`{"type": "Map", "value": [{"key": {"type": "String", "value": "a" }, "value": {"type": "Integer", "value": "not-an-int"}}]}`)}, + }, } for _, c := range errorCases { script := io.NewBufBinWriter()