Commit e857576e authored by Mark Tyneway's avatar Mark Tyneway

op-chain-ops: tx-builder safe types

Implement the types for working with the Safe
tx-builder application. These types should be able
to be round trip serialized into JSON. These types
will be used to create Safe tx-builder bundles
that can be dropped into the UI or can be combined
with additional tooling to send the transactions
to the Safe backend directly or directly interact
with the safe contracts.
parent ec4ed145
// Package safe contains types for working with Safe smart contract wallets. These are used to
// build batch transactions for the tx-builder app. The types are based on
// https://github.com/safe-global/safe-react-apps/blob/development/apps/tx-builder/src/typings/models.ts.
package safe
import (
"encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"math/big"
)
// BatchFile represents a Safe tx-builder transaction.
type BatchFile struct {
Version string `json:"version"`
ChainID *big.Int `json:"chainId"`
CreatedAt uint64 `json:"createdAt"`
Meta BatchFileMeta `json:"meta"`
Transactions []BatchTransaction `json:"transactions"`
}
// bathcFileMarshaling is a helper type used for JSON marshaling.
type batchFileMarshaling struct {
Version string `json:"version"`
ChainID string `json:"chainId"`
CreatedAt uint64 `json:"createdAt"`
Meta BatchFileMeta `json:"meta"`
Transactions []BatchTransaction `json:"transactions"`
}
// MarshalJSON will marshal a BatchFile to JSON.
func (b *BatchFile) MarshalJSON() ([]byte, error) {
return json.Marshal(batchFileMarshaling{
Version: b.Version,
ChainID: b.ChainID.String(),
CreatedAt: b.CreatedAt,
Meta: b.Meta,
Transactions: b.Transactions,
})
}
// UnmarshalJSON will unmarshal a BatchFile from JSON.
func (b *BatchFile) UnmarshalJSON(data []byte) error {
var bf batchFileMarshaling
if err := json.Unmarshal(data, &bf); err != nil {
return err
}
b.Version = bf.Version
chainId, ok := new(big.Int).SetString(bf.ChainID, 10)
if !ok {
return fmt.Errorf("cannot set chainId to %s", bf.ChainID)
}
b.ChainID = chainId
b.CreatedAt = bf.CreatedAt
b.Meta = bf.Meta
b.Transactions = bf.Transactions
return nil
}
// BatchFileMeta contains metadata about a BatchFile. Not all
// of the fields are required.
type BatchFileMeta struct {
TxBuilderVersion string `json:"txBuilderVersion,omitempty"`
Checksum string `json:"checksum,omitempty"`
CreatedFromSafeAddress string `json:"createdFromSafeAddress"`
CreatedFromOwnerAddress string `json:"createdFromOwnerAddress"`
Name string `json:"name"`
Description string `json:"description"`
}
// BatchTransaction represents a single call in a tx-builder transaction.
type BatchTransaction struct {
To common.Address `json:"to"`
Value *big.Int `json:"value"`
Data []byte `json:"data"`
Method ContractMethod `json:"contractMethod"`
InputValues map[string]string `json:"contractInputsValues"`
}
// UnmarshalJSON will unmarshal a BatchTransaction from JSON.
func (b *BatchTransaction) UnmarshalJSON(data []byte) error {
var bt batchTransactionMarshaling
if err := json.Unmarshal(data, &bt); err != nil {
return err
}
b.To = common.HexToAddress(bt.To)
b.Value = new(big.Int).SetUint64(bt.Value)
b.Method = bt.Method
b.InputValues = bt.InputValues
return nil
}
// MarshalJSON will marshal a BatchTransaction to JSON.
func (b *BatchTransaction) MarshalJSON() ([]byte, error) {
batch := batchTransactionMarshaling{
To: b.To.Hex(),
Value: b.Value.Uint64(),
Method: b.Method,
InputValues: b.InputValues,
}
if len(b.Data) != 0 {
hex := hexutil.Encode(b.Data)
batch.Data = &hex
}
return json.Marshal(batch)
}
// batchTransactionMarshaling is a helper type used for JSON marshaling.
type batchTransactionMarshaling struct {
To string `json:"to"`
Value uint64 `json:"value,string"`
Data *string `json:"data"`
Method ContractMethod `json:"contractMethod"`
InputValues map[string]string `json:"contractInputsValues"`
}
// ContractMethod represents a method call in a tx-builder transaction.
type ContractMethod struct {
Inputs []ContractInput `json:"inputs"`
Name string `json:"name"`
Payable bool `json:"payable"`
}
// ContractInput represents an input to a contract method.
type ContractInput struct {
InternalType string `json:"internalType"`
Name string `json:"name"`
Type string `json:"type"`
Components []ContractInput `json:"components,omitempty"`
}
package safe
import (
"bytes"
"encoding/json"
"os"
"testing"
"github.com/stretchr/testify/require"
)
func TestBatchFileJSONPrepareBedrock(t *testing.T) {
testBatchFileJSON(t, "testdata/batch-prepare-bedrock.json")
}
func TestBatchFileJSONL2OO(t *testing.T) {
testBatchFileJSON(t, "testdata/l2-output-oracle.json")
}
func testBatchFileJSON(t *testing.T, path string) {
b, err := os.ReadFile(path)
require.NoError(t, err)
dec := json.NewDecoder(bytes.NewReader(b))
decoded := new(BatchFile)
require.NoError(t, dec.Decode(decoded))
data, err := json.Marshal(decoded)
require.NoError(t, err)
require.JSONEq(t, string(b), string(data))
}
{"version":"1.0","chainId":"1","createdAt":1683299982633,"meta":{"name":"Transactions Batch","description":"","txBuilderVersion":"1.13.3","createdFromSafeAddress":"0xAB23dE0DbE0aedF356af3F815c8B47D88575D82d","createdFromOwnerAddress":"","checksum":"0x3be12fb2a12e07f516c3895194f8418c6640f27fb3324e4dc06dce337b7ae7c3"},"transactions":[{"to":"0x09AA72510eE2e1c705Dc4e2114b025a12E116bb8","value":"0","data":null,"contractMethod":{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","payable":false},"contractInputsValues":{"newOwner":"0x3C0dD22068a69433938097ad335Cb44a9DBf5c1A"}},{"to":"0x20835fbB5Dcb9B9c3074C0780bB07790a7525f41","value":"0","data":null,"contractMethod":{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","payable":false},"contractInputsValues":{"newOwner":"0x3C0dD22068a69433938097ad335Cb44a9DBf5c1A"}},{"to":"0xcAC4CDD0C2D87e65710C87dE3955974d6a0b6940","value":"0","data":null,"contractMethod":{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"setOwner","payable":false},"contractInputsValues":{"_owner":"0x3C0dD22068a69433938097ad335Cb44a9DBf5c1A"}},{"to":"0xE1229AbA7DC7e74C9995254bbaa40bedDB0B8B4d","value":"0","data":null,"contractMethod":{"inputs":[{"internalType":"address","name":"_admin","type":"address"}],"name":"changeAdmin","payable":false},"contractInputsValues":{"_admin":"0x3C0dD22068a69433938097ad335Cb44a9DBf5c1A"}}]}
\ No newline at end of file
{"version":"1.0","chainId":"1","createdAt":1691808995527,"meta":{"name":"Transactions Batch","description":"","txBuilderVersion":"1.16.1","createdFromSafeAddress":"0xc9D26D376dD75573E0C3247C141881F053d27Ae8","createdFromOwnerAddress":"","checksum":"0x2a88db9ce20d2eb5a80910842e9e94d5870497af45986a6c1c7e2c91d15e34f0"},"transactions":[{"to":"0xE5FF3b57695079f808a24256734483CD3889fA9E","value":"0","data":null,"contractMethod":{"inputs":[{"internalType":"bytes32","name":"_outputRoot","type":"bytes32"},{"internalType":"uint256","name":"_l2BlockNumber","type":"uint256"},{"internalType":"bytes32","name":"_l1BlockHash","type":"bytes32"},{"internalType":"uint256","name":"_l1BlockNumber","type":"uint256"}],"name":"proposeL2Output","payable":true},"contractInputsValues":{"_outputRoot":"0x5398552529cbd710f485e297bcf15233b8475bdad43280c99334f65a1d4278ff","_l2BlockNumber":"0","_l1BlockHash":"0x01f814a4547c01c18c0eb8b96cff19bc5dc83b1d2d8a8bbb03206587f594c80a","_l1BlockNumber":"1"}},{"to":"0xE5FF3b57695079f808a24256734483CD3889fA9E","value":"0","data":null,"contractMethod":{"inputs":[{"internalType":"uint256","name":"_l2OutputIndex","type":"uint256"}],"name":"deleteL2Outputs","payable":false},"contractInputsValues":{"_l2OutputIndex":"2"}}]}
\ 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