diff --git a/accounts/abi/abi.go b/accounts/abi/abi.go index ba1774c647..65a983f009 100644 --- a/accounts/abi/abi.go +++ b/accounts/abi/abi.go @@ -89,6 +89,24 @@ func (abi ABI) Unpack(v interface{}, name string, output []byte) (err error) { return fmt.Errorf("abi: could not locate named method or event") } +// Unpack output into a map according to the abi specification +func (abi ABI) UnpackIntoMap(v map[string]interface{}, name string, output []byte) (err error) { + if len(output) == 0 { + return fmt.Errorf("abi: unmarshalling empty output") + } + // since there can't be naming collisions with contracts and events, + // we need to decide whether we're calling a method or an event + if method, ok := abi.Methods[name]; ok { + if len(output)%32 != 0 { + return fmt.Errorf("abi: improperly formatted output") + } + return method.Outputs.UnpackIntoMap(v, output) + } else if event, ok := abi.Events[name]; ok { + return event.Inputs.UnpackIntoMap(v, output) + } + return fmt.Errorf("abi: could not locate named method or event") +} + // UnmarshalJSON implements json.Unmarshaler interface func (abi *ABI) UnmarshalJSON(data []byte) error { var fields []struct { diff --git a/accounts/abi/abi_test.go b/accounts/abi/abi_test.go index b9444f9f0d..6c2b7c2f56 100644 --- a/accounts/abi/abi_test.go +++ b/accounts/abi/abi_test.go @@ -694,6 +694,62 @@ func TestUnpackEvent(t *testing.T) { } } +func TestUnpackIntoMapEvent(t *testing.T) { + const abiJSON = `[{"constant":false,"inputs":[{"name":"memo","type":"bytes"}],"name":"receive","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}],"name":"received","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"}],"name":"receivedAddr","type":"event"}]` + abi, err := JSON(strings.NewReader(abiJSON)) + if err != nil { + t.Fatal(err) + } + + const hexdata = `000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158` + data, err := hex.DecodeString(hexdata) + if err != nil { + t.Fatal(err) + } + if len(data)%32 == 0 { + t.Errorf("len(data) is %d, want a non-multiple of 32", len(data)) + } + + receivedMap := map[string]interface{}{} + expectedReceivedMap := map[string]interface{}{ + "sender": common.HexToAddress("0x376c47978271565f56DEB45495afa69E59c16Ab2"), + "amount": big.NewInt(1), + "memo": []uint8{88}, + } + + err = abi.UnpackIntoMap(receivedMap, "received", data) + if err != nil { + t.Error(err) + } + + if receivedMap["sender"] != expectedReceivedMap["sender"] { + t.Errorf("unpacked map does not match expected map") + } + + if receivedMap["amount"].(*big.Int).String() != expectedReceivedMap["amount"].(*big.Int).String() { + t.Errorf("unpacked map does not match expected map") + } + + u8 := receivedMap["memo"].([]uint8) + expectedU8 := expectedReceivedMap["memo"].([]uint8) + for i, v := range expectedU8 { + if u8[i] != v { + t.Errorf("unpacked map does not match expected map") + } + } + + receivedAddrMap := map[string]interface{}{} + + err = abi.UnpackIntoMap(receivedAddrMap, "receivedAddr", data) + if err != nil { + t.Error(err) + } + + if receivedAddrMap["sender"] != expectedReceivedMap["sender"] { + t.Errorf("unpacked map does not match expected map") + } +} + func TestABI_MethodById(t *testing.T) { const abiJSON = `[ {"type":"function","name":"receive","constant":false,"inputs":[{"name":"memo","type":"bytes"}],"outputs":[],"payable":true,"stateMutability":"payable"}, diff --git a/accounts/abi/argument.go b/accounts/abi/argument.go index d0a6b035c6..49cbf13997 100644 --- a/accounts/abi/argument.go +++ b/accounts/abi/argument.go @@ -102,6 +102,16 @@ func (arguments Arguments) Unpack(v interface{}, data []byte) error { return arguments.unpackAtomic(v, marshalledValues[0]) } +// Unpack performs the operation hexdata -> mapping of argument name to argument value +func (arguments Arguments) UnpackIntoMap(v map[string]interface{}, data []byte) error { + marshalledValues, err := arguments.UnpackValues(data) + if err != nil { + return err + } + + return arguments.unpackIntoMap(v, marshalledValues) +} + // unpack sets the unmarshalled value to go format. // Note the dst here must be settable. func unpack(t *Type, dst interface{}, src interface{}) error { @@ -160,6 +170,19 @@ func unpack(t *Type, dst interface{}, src interface{}) error { return nil } +// Unpack arguments into map +func (arguments Arguments) unpackIntoMap(v map[string]interface{}, marshalledValues []interface{}) error { + // Make sure map is not nil + if v == nil { + return fmt.Errorf("abi: cannot unpack into a nil map") + } + + for i, arg := range arguments.NonIndexed() { + v[arg.Name] = marshalledValues[i] + } + return nil +} + // unpackAtomic unpacks ( hexdata -> go ) a single value func (arguments Arguments) unpackAtomic(v interface{}, marshalledValues interface{}) error { if arguments.LengthNonIndexed() == 0 { diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index c37bdf11d5..f70f911d37 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -340,6 +340,22 @@ func (c *BoundContract) UnpackLog(out interface{}, event string, log types.Log) return parseTopics(out, indexed, log.Topics[1:]) } +// UnpackLogIntoMap unpacks a retrieved log into the provided map. +func (c *BoundContract) UnpackLogIntoMap(out map[string]interface{}, event string, log types.Log) error { + if len(log.Data) > 0 { + if err := c.abi.UnpackIntoMap(out, event, log.Data); err != nil { + return err + } + } + var indexed abi.Arguments + for _, arg := range c.abi.Events[event].Inputs { + if arg.Indexed { + indexed = append(indexed, arg) + } + } + return parseTopicsIntoMap(out, indexed, log.Topics[1:]) +} + // ensureContext is a helper method to ensure a context is not nil, even if the // user specified it as such. func ensureContext(ctx context.Context) context.Context { diff --git a/accounts/abi/bind/topics.go b/accounts/abi/bind/topics.go index 600dfcda97..cd84fb164c 100644 --- a/accounts/abi/bind/topics.go +++ b/accounts/abi/bind/topics.go @@ -187,3 +187,42 @@ func parseTopics(out interface{}, fields abi.Arguments, topics []common.Hash) er } return nil } + +// parseTopics converts the indexed topic field-value pairs into map key-value pairs +func parseTopicsIntoMap(out map[string]interface{}, fields abi.Arguments, topics []common.Hash) error { + // Sanity check that the fields and topics match up + if len(fields) != len(topics) { + return errors.New("topic/field count mismatch") + } + // Iterate over all the fields and reconstruct them from topics + for _, arg := range fields { + if !arg.Indexed { + return errors.New("non-indexed field in topic reconstruction") + } + + switch arg.Type.T { + case abi.BoolTy: + if topics[0][common.HashLength-1] == 1 { + out[arg.Name] = true + } else { + out[arg.Name] = false + } + case abi.IntTy, abi.UintTy: + num := new(big.Int).SetBytes(topics[0][:]) + out[arg.Name] = num + case abi.AddressTy: + var addr common.Address + copy(addr[:], topics[0][common.HashLength-common.AddressLength:]) + out[arg.Name] = addr + case abi.HashTy: + out[arg.Name] = topics[0] + case abi.BytesTy, abi.FixedBytesTy: + out[arg.Name] = topics[0][:] + default: + } + + topics = topics[1:] + } + + return nil +}