Commit e14f8f5b authored by Mark Tyneway's avatar Mark Tyneway

op-chain-ops: wip

parent cfa5e24a
package safe
import (
// "github.com/ethereum-optimism/optimism/op-bindings/bindings"
)
/**
batch := new(BatchFile)
to := common.Address{}
value := new(big.Int)
sig := "foo(uint256)"
args := []any{big.NewInt(0)}
batch.AddCall(to, value, sig, args)
calldata := []byte{}
batch.AddRawCall(to, value, calldata)
**/
...@@ -6,44 +6,201 @@ package safe ...@@ -6,44 +6,201 @@ package safe
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"golang.org/x/exp/maps"
"math/big" "math/big"
"strconv"
"strings"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
) )
// BatchFile represents a Safe tx-builder transaction. // Batch represents a Safe tx-builder transaction.
type BatchFile struct { type Batch struct {
Version string `json:"version"` Version string `json:"version"`
ChainID *big.Int `json:"chainId"` ChainID *big.Int `json:"chainId"`
CreatedAt uint64 `json:"createdAt"` CreatedAt uint64 `json:"createdAt"`
Meta BatchFileMeta `json:"meta"` Meta BatchMeta `json:"meta"`
Transactions []BatchTransaction `json:"transactions"` Transactions []BatchTransaction `json:"transactions"`
} }
func (b *Batch) AddCall(to common.Address, value *big.Int, sig string, args []any, iface abi.ABI) error {
// Attempt to pull out the signature from the top level methods.
// The abi package uses normalization that we do not want to be
// coupled to, so attempt to search for the raw name if the top
// level name is not found to handle overloading more gracefully.
method, ok := iface.Methods[sig]
if !ok {
for _, m := range iface.Methods {
if m.RawName == sig || m.Sig == sig {
method = m
ok = true
}
}
}
if !ok {
keys := maps.Keys(iface.Methods)
methods := strings.Join(keys, ",")
return fmt.Errorf("%s not found in abi, options are %s", sig, methods)
}
if len(args) != len(method.Inputs) {
return fmt.Errorf("requires %d inputs but got %d for %s", len(method.Inputs), len(args), method.RawName)
}
contractMethod := ContractMethod{
Name: method.RawName,
Payable: method.Payable,
}
inputValues := make(map[string]string)
// TODO: refactor this to be recursive
// it should return a ContractInput and take an Input in
for i, input := range method.Inputs {
inputType, err := stringifyType(input.Type)
if err != nil {
return err
}
internalType := input.Type.String()
if inputType == "tuple" {
internalType = input.Type.TupleRawName
}
components := make([]ContractInput, len(input.Type.TupleElems))
for j, elem := range input.Type.TupleElems {
str := input.Type.TupleRawNames[j]
components[j] = ContractInput{
InternalType: elem.String(),
Name: str,
Type: elem.String(),
}
}
contractInput := ContractInput{
InternalType: internalType,
Name: input.Name,
Type: inputType,
Components: components,
}
contractMethod.Inputs = append(contractMethod.Inputs, contractInput)
str, err := stringifyArg(args[i])
if err != nil {
return err
}
inputValues[input.Name] = str
}
batchTransaction := BatchTransaction{
To: to,
Value: value,
Method: contractMethod,
InputValues: inputValues,
}
b.Transactions = append(b.Transactions, batchTransaction)
return nil
}
func stringifyType(t abi.Type) (string, error) {
switch t.T {
case abi.TupleTy:
return "tuple", nil
case abi.BoolTy:
return "bool", nil
case abi.AddressTy:
return "address", nil
case abi.UintTy:
return t.String(), nil
case abi.IntTy:
return t.String(), nil
default:
return "", fmt.Errorf("unknown type: %T", t.T)
}
}
func stringifyArg(argument any) (string, error) {
switch arg := argument.(type) {
case common.Address:
return arg.String(), nil
case *common.Address:
return arg.String(), nil
case *big.Int:
return arg.String(), nil
case big.Int:
return arg.String(), nil
case bool:
if arg {
return "true", nil
}
return "false", nil
case int64:
return strconv.FormatInt(arg, 10), nil
case int32:
return strconv.FormatInt(int64(arg), 10), nil
case int16:
return strconv.FormatInt(int64(arg), 10), nil
case int8:
return strconv.FormatInt(int64(arg), 10), nil
case int:
return strconv.FormatInt(int64(arg), 10), nil
case uint64:
return strconv.FormatUint(uint64(arg), 10), nil
case uint32:
return strconv.FormatUint(uint64(arg), 10), nil
case uint16:
return strconv.FormatUint(uint64(arg), 10), nil
case uint8:
return strconv.FormatUint(uint64(arg), 10), nil
case uint:
return strconv.FormatUint(uint64(arg), 10), nil
case []byte:
return hexutil.Encode(arg), nil
case []any:
ret := make([]string, len(arg))
for i, v := range arg {
str, err := stringifyArg(v)
if err != nil {
return "", err
}
ret[i] = str
}
return "[" + strings.Join(ret, ",") + "]", nil
default:
return "", fmt.Errorf("unknown type as argument: %T", arg)
}
}
// bathcFileMarshaling is a helper type used for JSON marshaling. // bathcFileMarshaling is a helper type used for JSON marshaling.
type batchFileMarshaling struct { type batchMarshaling struct {
Version string `json:"version"` Version string `json:"version"`
ChainID string `json:"chainId"` ChainID string `json:"chainId"`
CreatedAt uint64 `json:"createdAt"` CreatedAt uint64 `json:"createdAt"`
Meta BatchFileMeta `json:"meta"` Meta BatchMeta `json:"meta"`
Transactions []BatchTransaction `json:"transactions"` Transactions []BatchTransaction `json:"transactions"`
} }
// MarshalJSON will marshal a BatchFile to JSON. // MarshalJSON will marshal a Batch to JSON.
func (b *BatchFile) MarshalJSON() ([]byte, error) { func (b *Batch) MarshalJSON() ([]byte, error) {
return json.Marshal(batchFileMarshaling{ batch := batchMarshaling{
Version: b.Version, Version: b.Version,
ChainID: b.ChainID.String(),
CreatedAt: b.CreatedAt, CreatedAt: b.CreatedAt,
Meta: b.Meta, Meta: b.Meta,
Transactions: b.Transactions, Transactions: b.Transactions,
}) }
if b.ChainID != nil {
batch.ChainID = b.ChainID.String()
}
return json.Marshal(batch)
} }
// UnmarshalJSON will unmarshal a BatchFile from JSON. // UnmarshalJSON will unmarshal a Batch from JSON.
func (b *BatchFile) UnmarshalJSON(data []byte) error { func (b *Batch) UnmarshalJSON(data []byte) error {
var bf batchFileMarshaling var bf batchMarshaling
if err := json.Unmarshal(data, &bf); err != nil { if err := json.Unmarshal(data, &bf); err != nil {
return err return err
} }
...@@ -59,9 +216,9 @@ func (b *BatchFile) UnmarshalJSON(data []byte) error { ...@@ -59,9 +216,9 @@ func (b *BatchFile) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// BatchFileMeta contains metadata about a BatchFile. Not all // BatchMeta contains metadata about a Batch. Not all
// of the fields are required. // of the fields are required.
type BatchFileMeta struct { type BatchMeta struct {
TxBuilderVersion string `json:"txBuilderVersion,omitempty"` TxBuilderVersion string `json:"txBuilderVersion,omitempty"`
Checksum string `json:"checksum,omitempty"` Checksum string `json:"checksum,omitempty"`
CreatedFromSafeAddress string `json:"createdFromSafeAddress"` CreatedFromSafeAddress string `json:"createdFromSafeAddress"`
......
...@@ -3,27 +3,71 @@ package safe ...@@ -3,27 +3,71 @@ package safe
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"math/big"
"os" "os"
"testing" "testing"
"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestBatchFileJSONPrepareBedrock(t *testing.T) { func TestBatchJSONPrepareBedrock(t *testing.T) {
testBatchFileJSON(t, "testdata/batch-prepare-bedrock.json") testBatchJSON(t, "testdata/batch-prepare-bedrock.json")
} }
func TestBatchFileJSONL2OO(t *testing.T) { func TestBatchJSONL2OO(t *testing.T) {
testBatchFileJSON(t, "testdata/l2-output-oracle.json") testBatchJSON(t, "testdata/l2-output-oracle.json")
} }
func testBatchFileJSON(t *testing.T, path string) { func testBatchJSON(t *testing.T, path string) {
b, err := os.ReadFile(path) b, err := os.ReadFile(path)
require.NoError(t, err) require.NoError(t, err)
dec := json.NewDecoder(bytes.NewReader(b)) dec := json.NewDecoder(bytes.NewReader(b))
decoded := new(BatchFile) decoded := new(Batch)
require.NoError(t, dec.Decode(decoded)) require.NoError(t, dec.Decode(decoded))
data, err := json.Marshal(decoded) data, err := json.Marshal(decoded)
require.NoError(t, err) require.NoError(t, err)
require.JSONEq(t, string(b), string(data)) require.JSONEq(t, string(b), string(data))
} }
func TestBatchAddCall(t *testing.T) {
file, err := os.ReadFile("testdata/portal-abi.json")
require.NoError(t, err)
portalABI, err := abi.JSON(bytes.NewReader(file))
require.NoError(t, err)
batch := new(Batch)
to := common.Address{19: 0x01}
value := big.NewInt(222)
/*
sig := "depositTransaction"
args := []any{
common.Address{01},
big.NewInt(2),
uint64(100),
false,
[]byte{},
}
*/
sig := "finalizeWithdrawalTransaction"
args := []any{
[]any{
big.NewInt(0),
common.Address{19: 0x01},
common.Address{19: 0x02},
big.NewInt(1),
big.NewInt(2),
[]byte{},
},
}
require.NoError(t, batch.AddCall(to, value, sig, args, portalABI))
d, err := json.MarshalIndent(batch, " ", " ")
require.NoError(t, err)
fmt.Println(string(d))
}
This diff is collapsed.
{"version":"1.0","chainId":"1","createdAt":1692061927446,"meta":{"name":"Transactions Batch","description":"","txBuilderVersion":"1.16.1","createdFromSafeAddress":"0xc9D26D376dD75573E0C3247C141881F053d27Ae8","createdFromOwnerAddress":"","checksum":"0x9d01f75b6ab0cb240db3e3be44efdb96d64b2c88ef160bb13f46f279845c1a72"},"transactions":[{"to":"0x9e760aBd847E48A56b4a348Cba56Ae7267FeCE80","value":"0","data":null,"contractMethod":{"inputs":[{"components":[{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"target","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"gasLimit","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Types.WithdrawalTransaction","name":"_tx","type":"tuple"}],"name":"finalizeWithdrawalTransaction","payable":false},"contractInputsValues":{"_tx":"[0,\"0x0000000000000000000000000000000000000000\",\"0x0000000000000000000000000000000000000000\",0,0,\"0x\"]"}}]}
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment