diff --git a/Taskfile.yml b/Taskfile.yml index e7e8bd945..fc24d33b4 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -73,11 +73,6 @@ tasks: generates: - .build/kwild - generate:grammar: - desc: Generate the kuneiform grammar go code. - cmds: - - rm -rf node/engine/parse/gen/* - - cd node/engine/parse/grammar && ./generate.sh generate:docs: desc: Generate docs for CLIs @@ -98,8 +93,10 @@ tasks: generate:abi: desc: Generate the ABI for the smart contracts cmds: - - abigen --abi=./node/exts/erc20reward/abigen/reward_distributor_abi.json --pkg abigen --out=./node/exts/erc20reward/abigen/reward_distributor.go --type RewardDistributor - - abigen --abi=./node/exts/erc20reward/abigen/erc20_abi.json --pkg abigen --out=./node/exts/erc20reward/abigen/erc20.go --type Erc20 + - abigen --abi=./node/exts/erc20-bridge/abigen/reward_distributor_abi.json --pkg abigen --out=./node/exts/erc20-bridge/abigen/reward_distributor.go --type RewardDistributor + - abigen --abi=./node/exts/erc20-bridge/abigen/erc20_abi.json --pkg abigen --out=./node/exts/erc20-bridge/abigen/erc20.go --type Erc20 + - abigen --abi=./node/exts/erc20-bridge/abigen/safe_abi.json --pkg abigen --out=./node/exts/erc20-bridge/abigen/safe.go --type Safe + - abigen --abi=./node/exts/erc20-bridge/abigen/multicall3_abi.json --pkg abigen --out=./node/exts/erc20-bridge/abigen/multicall3.go --type Multicall3 # ************ docker ************ vendor: diff --git a/app/node/build.go b/app/node/build.go index e1c37cf1e..97f2bc629 100644 --- a/app/node/build.go +++ b/app/node/build.go @@ -26,23 +26,23 @@ import ( "github.com/kwilteam/kwil-db/node/consensus" "github.com/kwilteam/kwil-db/node/engine" "github.com/kwilteam/kwil-db/node/engine/interpreter" + _ "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/erc20" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/signersvc" "github.com/kwilteam/kwil-db/node/listeners" "github.com/kwilteam/kwil-db/node/mempool" "github.com/kwilteam/kwil-db/node/meta" "github.com/kwilteam/kwil-db/node/migrations" "github.com/kwilteam/kwil-db/node/pg" - "github.com/kwilteam/kwil-db/node/snapshotter" - "github.com/kwilteam/kwil-db/node/store" - "github.com/kwilteam/kwil-db/node/txapp" - "github.com/kwilteam/kwil-db/node/types/sql" - "github.com/kwilteam/kwil-db/node/voting" - - _ "github.com/kwilteam/kwil-db/node/exts/erc20reward" rpcserver "github.com/kwilteam/kwil-db/node/services/jsonrpc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/adminsvc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/chainsvc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/funcsvc" "github.com/kwilteam/kwil-db/node/services/jsonrpc/usersvc" + "github.com/kwilteam/kwil-db/node/snapshotter" + "github.com/kwilteam/kwil-db/node/store" + "github.com/kwilteam/kwil-db/node/txapp" + "github.com/kwilteam/kwil-db/node/types/sql" + "github.com/kwilteam/kwil-db/node/voting" ) func buildServer(ctx context.Context, d *coreDependencies) *server { @@ -145,6 +145,8 @@ func buildServer(ctx context.Context, d *coreDependencies) *server { jsonRPCAdminServer.RegisterSvc(jsonChainSvc) } + erc20BridgeSignerMgr := buildErc20BridgeSignerMgr(d, db, e, node, bp) + s := &server{ cfg: d.cfg, closers: closers, @@ -155,6 +157,7 @@ func buildServer(ctx context.Context, d *coreDependencies) *server { jsonRPCAdminServer: jsonRPCAdminServer, dbCtx: db, log: d.logger, + erc20BridgeSigner: erc20BridgeSignerMgr, } return s @@ -504,6 +507,29 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB, return ce } +func buildErc20BridgeSignerMgr(d *coreDependencies, db *pg.DB, + engine *interpreter.ThreadSafeInterpreter, node *node.Node, + bp *blockprocessor.BlockProcessor) *signersvc.ServiceMgr { + // create shared state + stateFile := signersvc.StateFilePath(d.rootDir) + + if !fileExists(stateFile) { + emptyFile, err := os.Create(stateFile) + if err != nil { + failBuild(err, "Failed to create erc20 bridge signer state file") + } + _ = emptyFile.Close() + } + + state, err := signersvc.LoadStateFromFile(stateFile) + if err != nil { + failBuild(err, "Failed to load erc20 bridge signer state file") + } + + return signersvc.NewServiceMgr(d.genesisCfg.ChainID, db, engine, node, bp, + d.cfg.Erc20Bridge, state, d.logger.New("EVMRW")) +} + func buildNode(d *coreDependencies, mp *mempool.Mempool, bs *store.BlockStore, ce *consensus.ConsensusEngine, ss *snapshotter.SnapshotStore, db *pg.DB, bp *blockprocessor.BlockProcessor, p2p *node.P2PService) *node.Node { diff --git a/app/node/node.go b/app/node/node.go index af2f9b6a1..688d7a3e4 100644 --- a/app/node/node.go +++ b/app/node/node.go @@ -11,6 +11,8 @@ import ( "runtime" "slices" + "golang.org/x/sync/errgroup" + "github.com/kwilteam/kwil-db/app/key" "github.com/kwilteam/kwil-db/config" "github.com/kwilteam/kwil-db/core/crypto" @@ -20,11 +22,10 @@ import ( authExt "github.com/kwilteam/kwil-db/extensions/auth" "github.com/kwilteam/kwil-db/node" "github.com/kwilteam/kwil-db/node/consensus" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/signersvc" "github.com/kwilteam/kwil-db/node/listeners" rpcserver "github.com/kwilteam/kwil-db/node/services/jsonrpc" "github.com/kwilteam/kwil-db/version" - - "golang.org/x/sync/errgroup" ) type server struct { @@ -43,6 +44,7 @@ type server struct { listeners *listeners.ListenerManager jsonRPCServer *rpcserver.Server jsonRPCAdminServer *rpcserver.Server + erc20BridgeSigner *signersvc.ServiceMgr } func runNode(ctx context.Context, rootDir string, cfg *config.Config, autogen bool, dbOwner string) (err error) { @@ -259,6 +261,13 @@ func (s *server) Start(ctx context.Context) error { }) s.log.Info("listener manager started") + // Start erc20 bridge signer svc + if s.erc20BridgeSigner != nil { + group.Go(func() error { + return s.erc20BridgeSigner.Start(groupCtx) + }) + } + // TODO: node is starting the consensus engine for ease of testing // Start the consensus engine diff --git a/app/node/start.go b/app/node/start.go index 60ad6d1f0..f365ad6f3 100644 --- a/app/node/start.go +++ b/app/node/start.go @@ -104,13 +104,12 @@ func StartCmd() *cobra.Command { func parseExtensionFlags(args []string) (map[string]map[string]string, error) { exts := make(map[string]map[string]string) for i := 0; i < len(args); i++ { - if !strings.HasPrefix(args[i], "--extension.") { + if !strings.HasPrefix(args[i], "--extensions.") { return nil, fmt.Errorf("expected extension flag, got %q", args[i]) } - // split the flag into the extension name and the flag name - // we intentionally do not use SplitN because we want to verify - // there are exactly 3 parts. - parts := strings.Split(args[i], ".") + // split the flag into the extension name and the flag name; + // last part can have values like URL, will verify only 3 segements in the flag below + parts := strings.SplitN(args[i], ".", 3) if len(parts) != 3 { return nil, fmt.Errorf("invalid extension flag %q", args[i]) } @@ -131,8 +130,19 @@ func parseExtensionFlags(args []string) (map[string]map[string]string, error) { if strings.Contains(parts[2], "=") { // flag value is in the same argument val := strings.SplitN(parts[2], "=", 2) + + // flag can only have 3 segements + if strings.Contains(val[0], ".") { + return nil, fmt.Errorf("invalid extension flag %q", args[i]) + } + ext[val[0]] = val[1] } else { + // flag can only have 3 segements + if strings.Contains(parts[2], ".") { + return nil, fmt.Errorf("invalid extension flag %q", args[i]) + } + // flag value is in the next argument if i+1 >= len(args) { return nil, fmt.Errorf("missing value for extension flag %q", args[i]) diff --git a/app/node/start_test.go b/app/node/start_test.go index e4f2b5b06..b1ac80ebf 100644 --- a/app/node/start_test.go +++ b/app/node/start_test.go @@ -22,7 +22,7 @@ func Test_ExtensionFlags(t *testing.T) { }, { name: "single flag", - flagset: []string{"--extension.extname.flagname", "value"}, + flagset: []string{"--extensions.extname.flagname", "value"}, want: map[string]map[string]string{ "extname": { "flagname": "value", @@ -31,7 +31,7 @@ func Test_ExtensionFlags(t *testing.T) { }, { name: "multiple flags", - flagset: []string{"--extension.extname.flagname", "value", "--extension.extname2.flagname2=value2"}, + flagset: []string{"--extensions.extname.flagname", "value", "--extensions.extname2.flagname2=value2"}, want: map[string]map[string]string{ "extname": { "flagname": "value", @@ -41,17 +41,34 @@ func Test_ExtensionFlags(t *testing.T) { }, }, }, + { + name: "multiple flags with dot values", + flagset: []string{"--extensions.extname.flagname", "value.a.b", "--extensions.extname2.flagname2=value2.a.b"}, + want: map[string]map[string]string{ + "extname": { + "flagname": "value.a.b", + }, + "extname2": { + "flagname2": "value2.a.b", + }, + }, + }, + { + name: "more than 3 fields", + flagset: []string{"--extensions.extname.flagname.another", "value", "--extensions.extname.flagname.another=value"}, + wantErr: true, + }, { name: "missing value", flagset: []string{ - "--extension.extname.flagname", + "--extensions.extname.flagname", }, wantErr: true, }, { name: "pass flag as a value errors", flagset: []string{ - "--extension.extname.flagname", "--extension.extname2.flagname2=value2", + "--extensions.extname.flagname", "--extensions.extname2.flagname2=value2", }, wantErr: true, }, @@ -62,6 +79,7 @@ func Test_ExtensionFlags(t *testing.T) { got, err := parseExtensionFlags(tt.flagset) if tt.wantErr { require.Error(t, err) + t.Log(err) return } require.NoError(t, err) diff --git a/cmd/kwil-cli/cmds/call-action.go b/cmd/kwil-cli/cmds/call-action.go index 216cea7ee..6f3f6354c 100644 --- a/cmd/kwil-cli/cmds/call-action.go +++ b/cmd/kwil-cli/cmds/call-action.go @@ -17,7 +17,7 @@ import ( var ( callActionLong = `Call a view action. - + This command calls a view action against the database, and formats the results in a table. It can only be used to call view actions, not write actions. diff --git a/config/config.go b/config/config.go index ea7b1c5ad..dafd78de8 100644 --- a/config/config.go +++ b/config/config.go @@ -13,12 +13,14 @@ import ( "strings" "time" + "github.com/pelletier/go-toml/v2" + + ethCommon "github.com/ethereum/go-ethereum/common" "github.com/kwilteam/kwil-db/core/crypto" "github.com/kwilteam/kwil-db/core/crypto/auth" "github.com/kwilteam/kwil-db/core/log" "github.com/kwilteam/kwil-db/core/types" - - "github.com/pelletier/go-toml/v2" + "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" ) var ( @@ -311,6 +313,11 @@ func DefaultConfig() *Config { Height: 0, Hash: types.Hash{}, }, + Erc20Bridge: ERC20BridgeConfig{ + RPC: make(map[string]string), + BlockSyncChuckSize: make(map[string]string), + Signer: make(map[string]string), + }, } } @@ -335,6 +342,7 @@ type Config struct { GenesisState string `toml:"genesis_state" comment:"path to the genesis state file, relative to the root directory"` Migrations MigrationConfig `toml:"migrations" comment:"zero downtime migration configuration"` Checkpoint Checkpoint `toml:"checkpoint" comment:"checkpoint info for the leader to sync to before proposing a new block"` + Erc20Bridge ERC20BridgeConfig `toml:"erc20_bridge" comment:"ERC20 bridge configuration"` } // PeerConfig corresponds to the [p2p] section of the config. @@ -443,6 +451,37 @@ type Checkpoint struct { Hash types.Hash `toml:"hash" comment:"checkpoint block hash."` } +type ERC20BridgeConfig struct { + RPC map[string]string `toml:"rpc" comment:"evm websocket RPC; format: chain_name='rpc_url'"` + BlockSyncChuckSize map[string]string `toml:"block_sync_chuck_size" comment:"rpc option block sync chunk size; format: chain_name='chunk_size'"` + Signer map[string]string `toml:"signer" comment:"signer service configuration; format: ext_alias='file_path_to_private_key'"` +} + +// Validate validates the bridge general config, other validations will be performed +// when correspond components derive config from it. +// BlockSyncChuckSize config will be validated by evm-sync listener. +// Signer config will be validated by erc20 signerSvc. +func (cfg ERC20BridgeConfig) Validate() error { + for chain, rpc := range cfg.RPC { + if err := chains.Chain(strings.ToLower(chain)).Valid(); err != nil { + return fmt.Errorf("erc20_bridge.rpc: %s", chain) + } + + // enforce websocket + if !strings.HasPrefix(rpc, "wss://") && !strings.HasPrefix(rpc, "ws://") { + return fmt.Errorf("erc20_bridge.rpc: must start with wss:// or ws://") + } + } + + for _, pkPath := range cfg.Signer { + if !ethCommon.FileExist(pkPath) { + return fmt.Errorf("erc20_bridge.signer: private key file %s not found", pkPath) + } + } + + return nil +} + // ToTOML marshals the config to TOML. The `toml` struct field tag // specifies the field names. For example: // diff --git a/core/client/client.go b/core/client/client.go index 2ea782671..752a93a68 100644 --- a/core/client/client.go +++ b/core/client/client.go @@ -241,7 +241,7 @@ func (c *Client) ChainInfo(ctx context.Context) (*types.ChainInfo, error) { func (c *Client) Execute(ctx context.Context, namespace string, action string, tuples [][]any, opts ...clientType.TxOpt) (types.Hash, error) { encodedTuples := make([][]*types.EncodedValue, len(tuples)) for i, tuple := range tuples { - encoded, err := encodeTuple(tuple) + encoded, err := EncodeInputs(tuple) if err != nil { return types.Hash{}, err } @@ -303,7 +303,7 @@ func (c *Client) ExecuteSQL(ctx context.Context, stmt string, params map[string] // Call calls an action. It returns the result records. func (c *Client) Call(ctx context.Context, namespace string, action string, inputs []any) (*types.CallResult, error) { - encoded, err := encodeTuple(inputs) + encoded, err := EncodeInputs(inputs) if err != nil { return nil, err } @@ -374,8 +374,8 @@ func (c *Client) GetAccount(ctx context.Context, acctID *types.AccountID, status return c.txClient.GetAccount(ctx, acctID, status) } -// encodeTuple encodes a tuple for usage in a transaction. -func encodeTuple(tup []any) ([]*types.EncodedValue, error) { +// EncodeInputs encodes input(a tuple) for usage in a transaction. +func EncodeInputs(tup []any) ([]*types.EncodedValue, error) { encoded := make([]*types.EncodedValue, 0, len(tup)) for _, val := range tup { ev, err := types.EncodeValue(val) diff --git a/core/types/decimal.go b/core/types/decimal.go index 787193c74..8b45fcd9e 100644 --- a/core/types/decimal.go +++ b/core/types/decimal.go @@ -199,6 +199,16 @@ func (d *Decimal) IsNegative() bool { return d.dec.Negative } +// IsZero returns true if the decimal is zero. +func (d *Decimal) IsZero() bool { + return d.dec.IsZero() +} + +// IsPositive returns true if the decimal is positive. +func (d *Decimal) IsPositive() bool { + return !d.IsNegative() && !d.IsZero() +} + // String returns the string representation of the decimal. func (d *Decimal) String() string { return d.dec.String() diff --git a/go.mod b/go.mod index 2832fbf5a..6c592b6b2 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/pelletier/go-toml/v2 v2.2.3 github.com/prometheus/client_golang v1.20.5 + github.com/samber/lo v1.47.0 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.6 github.com/stretchr/testify v1.10.0 diff --git a/go.sum b/go.sum index 5c97769db..42d7c2728 100644 --- a/go.sum +++ b/go.sum @@ -566,6 +566,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= diff --git a/node/exts/erc20reward/abigen/erc20.go b/node/exts/erc20-bridge/abigen/erc20.go similarity index 100% rename from node/exts/erc20reward/abigen/erc20.go rename to node/exts/erc20-bridge/abigen/erc20.go diff --git a/node/exts/erc20reward/abigen/erc20_abi.json b/node/exts/erc20-bridge/abigen/erc20_abi.json similarity index 100% rename from node/exts/erc20reward/abigen/erc20_abi.json rename to node/exts/erc20-bridge/abigen/erc20_abi.json diff --git a/node/exts/erc20-bridge/abigen/multicall3.go b/node/exts/erc20-bridge/abigen/multicall3.go new file mode 100644 index 000000000..41bfe516c --- /dev/null +++ b/node/exts/erc20-bridge/abigen/multicall3.go @@ -0,0 +1,644 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package abigen + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// Multicall3Call is an auto generated low-level Go binding around an user-defined struct. +type Multicall3Call struct { + Target common.Address + CallData []byte +} + +// Multicall3Call3 is an auto generated low-level Go binding around an user-defined struct. +type Multicall3Call3 struct { + Target common.Address + AllowFailure bool + CallData []byte +} + +// Multicall3Call3Value is an auto generated low-level Go binding around an user-defined struct. +type Multicall3Call3Value struct { + Target common.Address + AllowFailure bool + Value *big.Int + CallData []byte +} + +// Multicall3Result is an auto generated low-level Go binding around an user-defined struct. +type Multicall3Result struct { + Success bool + ReturnData []byte +} + +// Multicall3MetaData contains all meta data concerning the Multicall3 contract. +var Multicall3MetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes[]\",\"name\":\"returnData\",\"type\":\"bytes[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"allowFailure\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call3[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate3\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bool\",\"name\":\"allowFailure\",\"type\":\"bool\"},{\"internalType\":\"uint256\",\"name\":\"value\",\"type\":\"uint256\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call3Value[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"aggregate3Value\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"blockAndAggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getBasefee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"basefee\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"name\":\"getBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getBlockNumber\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getChainId\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"chainid\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockCoinbase\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"coinbase\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockDifficulty\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"difficulty\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockGasLimit\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"gaslimit\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getCurrentBlockTimestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"addr\",\"type\":\"address\"}],\"name\":\"getEthBalance\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"balance\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getLastBlockHash\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"tryAggregate\",\"outputs\":[{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bool\",\"name\":\"requireSuccess\",\"type\":\"bool\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"callData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Call[]\",\"name\":\"calls\",\"type\":\"tuple[]\"}],\"name\":\"tryBlockAndAggregate\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"blockHash\",\"type\":\"bytes32\"},{\"components\":[{\"internalType\":\"bool\",\"name\":\"success\",\"type\":\"bool\"},{\"internalType\":\"bytes\",\"name\":\"returnData\",\"type\":\"bytes\"}],\"internalType\":\"structMulticall3.Result[]\",\"name\":\"returnData\",\"type\":\"tuple[]\"}],\"stateMutability\":\"payable\",\"type\":\"function\"}]", +} + +// Multicall3ABI is the input ABI used to generate the binding from. +// Deprecated: Use Multicall3MetaData.ABI instead. +var Multicall3ABI = Multicall3MetaData.ABI + +// Multicall3 is an auto generated Go binding around an Ethereum contract. +type Multicall3 struct { + Multicall3Caller // Read-only binding to the contract + Multicall3Transactor // Write-only binding to the contract + Multicall3Filterer // Log filterer for contract events +} + +// Multicall3Caller is an auto generated read-only Go binding around an Ethereum contract. +type Multicall3Caller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Multicall3Transactor is an auto generated write-only Go binding around an Ethereum contract. +type Multicall3Transactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Multicall3Filterer is an auto generated log filtering Go binding around an Ethereum contract events. +type Multicall3Filterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// Multicall3Session is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type Multicall3Session struct { + Contract *Multicall3 // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Multicall3CallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type Multicall3CallerSession struct { + Contract *Multicall3Caller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// Multicall3TransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type Multicall3TransactorSession struct { + Contract *Multicall3Transactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// Multicall3Raw is an auto generated low-level Go binding around an Ethereum contract. +type Multicall3Raw struct { + Contract *Multicall3 // Generic contract binding to access the raw methods on +} + +// Multicall3CallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type Multicall3CallerRaw struct { + Contract *Multicall3Caller // Generic read-only contract binding to access the raw methods on +} + +// Multicall3TransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type Multicall3TransactorRaw struct { + Contract *Multicall3Transactor // Generic write-only contract binding to access the raw methods on +} + +// NewMulticall3 creates a new instance of Multicall3, bound to a specific deployed contract. +func NewMulticall3(address common.Address, backend bind.ContractBackend) (*Multicall3, error) { + contract, err := bindMulticall3(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Multicall3{Multicall3Caller: Multicall3Caller{contract: contract}, Multicall3Transactor: Multicall3Transactor{contract: contract}, Multicall3Filterer: Multicall3Filterer{contract: contract}}, nil +} + +// NewMulticall3Caller creates a new read-only instance of Multicall3, bound to a specific deployed contract. +func NewMulticall3Caller(address common.Address, caller bind.ContractCaller) (*Multicall3Caller, error) { + contract, err := bindMulticall3(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &Multicall3Caller{contract: contract}, nil +} + +// NewMulticall3Transactor creates a new write-only instance of Multicall3, bound to a specific deployed contract. +func NewMulticall3Transactor(address common.Address, transactor bind.ContractTransactor) (*Multicall3Transactor, error) { + contract, err := bindMulticall3(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &Multicall3Transactor{contract: contract}, nil +} + +// NewMulticall3Filterer creates a new log filterer instance of Multicall3, bound to a specific deployed contract. +func NewMulticall3Filterer(address common.Address, filterer bind.ContractFilterer) (*Multicall3Filterer, error) { + contract, err := bindMulticall3(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &Multicall3Filterer{contract: contract}, nil +} + +// bindMulticall3 binds a generic wrapper to an already deployed contract. +func bindMulticall3(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := Multicall3MetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Multicall3 *Multicall3Raw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Multicall3.Contract.Multicall3Caller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Multicall3 *Multicall3Raw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Multicall3.Contract.Multicall3Transactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Multicall3 *Multicall3Raw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Multicall3.Contract.Multicall3Transactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Multicall3 *Multicall3CallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Multicall3.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Multicall3 *Multicall3TransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Multicall3.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Multicall3 *Multicall3TransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Multicall3.Contract.contract.Transact(opts, method, params...) +} + +// GetBasefee is a free data retrieval call binding the contract method 0x3e64a696. +// +// Solidity: function getBasefee() view returns(uint256 basefee) +func (_Multicall3 *Multicall3Caller) GetBasefee(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getBasefee") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetBasefee is a free data retrieval call binding the contract method 0x3e64a696. +// +// Solidity: function getBasefee() view returns(uint256 basefee) +func (_Multicall3 *Multicall3Session) GetBasefee() (*big.Int, error) { + return _Multicall3.Contract.GetBasefee(&_Multicall3.CallOpts) +} + +// GetBasefee is a free data retrieval call binding the contract method 0x3e64a696. +// +// Solidity: function getBasefee() view returns(uint256 basefee) +func (_Multicall3 *Multicall3CallerSession) GetBasefee() (*big.Int, error) { + return _Multicall3.Contract.GetBasefee(&_Multicall3.CallOpts) +} + +// GetBlockHash is a free data retrieval call binding the contract method 0xee82ac5e. +// +// Solidity: function getBlockHash(uint256 blockNumber) view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3Caller) GetBlockHash(opts *bind.CallOpts, blockNumber *big.Int) ([32]byte, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getBlockHash", blockNumber) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetBlockHash is a free data retrieval call binding the contract method 0xee82ac5e. +// +// Solidity: function getBlockHash(uint256 blockNumber) view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3Session) GetBlockHash(blockNumber *big.Int) ([32]byte, error) { + return _Multicall3.Contract.GetBlockHash(&_Multicall3.CallOpts, blockNumber) +} + +// GetBlockHash is a free data retrieval call binding the contract method 0xee82ac5e. +// +// Solidity: function getBlockHash(uint256 blockNumber) view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3CallerSession) GetBlockHash(blockNumber *big.Int) ([32]byte, error) { + return _Multicall3.Contract.GetBlockHash(&_Multicall3.CallOpts, blockNumber) +} + +// GetBlockNumber is a free data retrieval call binding the contract method 0x42cbb15c. +// +// Solidity: function getBlockNumber() view returns(uint256 blockNumber) +func (_Multicall3 *Multicall3Caller) GetBlockNumber(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getBlockNumber") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetBlockNumber is a free data retrieval call binding the contract method 0x42cbb15c. +// +// Solidity: function getBlockNumber() view returns(uint256 blockNumber) +func (_Multicall3 *Multicall3Session) GetBlockNumber() (*big.Int, error) { + return _Multicall3.Contract.GetBlockNumber(&_Multicall3.CallOpts) +} + +// GetBlockNumber is a free data retrieval call binding the contract method 0x42cbb15c. +// +// Solidity: function getBlockNumber() view returns(uint256 blockNumber) +func (_Multicall3 *Multicall3CallerSession) GetBlockNumber() (*big.Int, error) { + return _Multicall3.Contract.GetBlockNumber(&_Multicall3.CallOpts) +} + +// GetChainId is a free data retrieval call binding the contract method 0x3408e470. +// +// Solidity: function getChainId() view returns(uint256 chainid) +func (_Multicall3 *Multicall3Caller) GetChainId(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getChainId") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetChainId is a free data retrieval call binding the contract method 0x3408e470. +// +// Solidity: function getChainId() view returns(uint256 chainid) +func (_Multicall3 *Multicall3Session) GetChainId() (*big.Int, error) { + return _Multicall3.Contract.GetChainId(&_Multicall3.CallOpts) +} + +// GetChainId is a free data retrieval call binding the contract method 0x3408e470. +// +// Solidity: function getChainId() view returns(uint256 chainid) +func (_Multicall3 *Multicall3CallerSession) GetChainId() (*big.Int, error) { + return _Multicall3.Contract.GetChainId(&_Multicall3.CallOpts) +} + +// GetCurrentBlockCoinbase is a free data retrieval call binding the contract method 0xa8b0574e. +// +// Solidity: function getCurrentBlockCoinbase() view returns(address coinbase) +func (_Multicall3 *Multicall3Caller) GetCurrentBlockCoinbase(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getCurrentBlockCoinbase") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// GetCurrentBlockCoinbase is a free data retrieval call binding the contract method 0xa8b0574e. +// +// Solidity: function getCurrentBlockCoinbase() view returns(address coinbase) +func (_Multicall3 *Multicall3Session) GetCurrentBlockCoinbase() (common.Address, error) { + return _Multicall3.Contract.GetCurrentBlockCoinbase(&_Multicall3.CallOpts) +} + +// GetCurrentBlockCoinbase is a free data retrieval call binding the contract method 0xa8b0574e. +// +// Solidity: function getCurrentBlockCoinbase() view returns(address coinbase) +func (_Multicall3 *Multicall3CallerSession) GetCurrentBlockCoinbase() (common.Address, error) { + return _Multicall3.Contract.GetCurrentBlockCoinbase(&_Multicall3.CallOpts) +} + +// GetCurrentBlockDifficulty is a free data retrieval call binding the contract method 0x72425d9d. +// +// Solidity: function getCurrentBlockDifficulty() view returns(uint256 difficulty) +func (_Multicall3 *Multicall3Caller) GetCurrentBlockDifficulty(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getCurrentBlockDifficulty") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetCurrentBlockDifficulty is a free data retrieval call binding the contract method 0x72425d9d. +// +// Solidity: function getCurrentBlockDifficulty() view returns(uint256 difficulty) +func (_Multicall3 *Multicall3Session) GetCurrentBlockDifficulty() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockDifficulty(&_Multicall3.CallOpts) +} + +// GetCurrentBlockDifficulty is a free data retrieval call binding the contract method 0x72425d9d. +// +// Solidity: function getCurrentBlockDifficulty() view returns(uint256 difficulty) +func (_Multicall3 *Multicall3CallerSession) GetCurrentBlockDifficulty() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockDifficulty(&_Multicall3.CallOpts) +} + +// GetCurrentBlockGasLimit is a free data retrieval call binding the contract method 0x86d516e8. +// +// Solidity: function getCurrentBlockGasLimit() view returns(uint256 gaslimit) +func (_Multicall3 *Multicall3Caller) GetCurrentBlockGasLimit(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getCurrentBlockGasLimit") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetCurrentBlockGasLimit is a free data retrieval call binding the contract method 0x86d516e8. +// +// Solidity: function getCurrentBlockGasLimit() view returns(uint256 gaslimit) +func (_Multicall3 *Multicall3Session) GetCurrentBlockGasLimit() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockGasLimit(&_Multicall3.CallOpts) +} + +// GetCurrentBlockGasLimit is a free data retrieval call binding the contract method 0x86d516e8. +// +// Solidity: function getCurrentBlockGasLimit() view returns(uint256 gaslimit) +func (_Multicall3 *Multicall3CallerSession) GetCurrentBlockGasLimit() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockGasLimit(&_Multicall3.CallOpts) +} + +// GetCurrentBlockTimestamp is a free data retrieval call binding the contract method 0x0f28c97d. +// +// Solidity: function getCurrentBlockTimestamp() view returns(uint256 timestamp) +func (_Multicall3 *Multicall3Caller) GetCurrentBlockTimestamp(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getCurrentBlockTimestamp") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetCurrentBlockTimestamp is a free data retrieval call binding the contract method 0x0f28c97d. +// +// Solidity: function getCurrentBlockTimestamp() view returns(uint256 timestamp) +func (_Multicall3 *Multicall3Session) GetCurrentBlockTimestamp() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockTimestamp(&_Multicall3.CallOpts) +} + +// GetCurrentBlockTimestamp is a free data retrieval call binding the contract method 0x0f28c97d. +// +// Solidity: function getCurrentBlockTimestamp() view returns(uint256 timestamp) +func (_Multicall3 *Multicall3CallerSession) GetCurrentBlockTimestamp() (*big.Int, error) { + return _Multicall3.Contract.GetCurrentBlockTimestamp(&_Multicall3.CallOpts) +} + +// GetEthBalance is a free data retrieval call binding the contract method 0x4d2301cc. +// +// Solidity: function getEthBalance(address addr) view returns(uint256 balance) +func (_Multicall3 *Multicall3Caller) GetEthBalance(opts *bind.CallOpts, addr common.Address) (*big.Int, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getEthBalance", addr) + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetEthBalance is a free data retrieval call binding the contract method 0x4d2301cc. +// +// Solidity: function getEthBalance(address addr) view returns(uint256 balance) +func (_Multicall3 *Multicall3Session) GetEthBalance(addr common.Address) (*big.Int, error) { + return _Multicall3.Contract.GetEthBalance(&_Multicall3.CallOpts, addr) +} + +// GetEthBalance is a free data retrieval call binding the contract method 0x4d2301cc. +// +// Solidity: function getEthBalance(address addr) view returns(uint256 balance) +func (_Multicall3 *Multicall3CallerSession) GetEthBalance(addr common.Address) (*big.Int, error) { + return _Multicall3.Contract.GetEthBalance(&_Multicall3.CallOpts, addr) +} + +// GetLastBlockHash is a free data retrieval call binding the contract method 0x27e86d6e. +// +// Solidity: function getLastBlockHash() view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3Caller) GetLastBlockHash(opts *bind.CallOpts) ([32]byte, error) { + var out []interface{} + err := _Multicall3.contract.Call(opts, &out, "getLastBlockHash") + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// GetLastBlockHash is a free data retrieval call binding the contract method 0x27e86d6e. +// +// Solidity: function getLastBlockHash() view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3Session) GetLastBlockHash() ([32]byte, error) { + return _Multicall3.Contract.GetLastBlockHash(&_Multicall3.CallOpts) +} + +// GetLastBlockHash is a free data retrieval call binding the contract method 0x27e86d6e. +// +// Solidity: function getLastBlockHash() view returns(bytes32 blockHash) +func (_Multicall3 *Multicall3CallerSession) GetLastBlockHash() ([32]byte, error) { + return _Multicall3.Contract.GetLastBlockHash(&_Multicall3.CallOpts) +} + +// Aggregate is a paid mutator transaction binding the contract method 0x252dba42. +// +// Solidity: function aggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes[] returnData) +func (_Multicall3 *Multicall3Transactor) Aggregate(opts *bind.TransactOpts, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "aggregate", calls) +} + +// Aggregate is a paid mutator transaction binding the contract method 0x252dba42. +// +// Solidity: function aggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes[] returnData) +func (_Multicall3 *Multicall3Session) Aggregate(calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate(&_Multicall3.TransactOpts, calls) +} + +// Aggregate is a paid mutator transaction binding the contract method 0x252dba42. +// +// Solidity: function aggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes[] returnData) +func (_Multicall3 *Multicall3TransactorSession) Aggregate(calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate(&_Multicall3.TransactOpts, calls) +} + +// Aggregate3 is a paid mutator transaction binding the contract method 0x82ad56cb. +// +// Solidity: function aggregate3((address,bool,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) Aggregate3(opts *bind.TransactOpts, calls []Multicall3Call3) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "aggregate3", calls) +} + +// Aggregate3 is a paid mutator transaction binding the contract method 0x82ad56cb. +// +// Solidity: function aggregate3((address,bool,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) Aggregate3(calls []Multicall3Call3) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate3(&_Multicall3.TransactOpts, calls) +} + +// Aggregate3 is a paid mutator transaction binding the contract method 0x82ad56cb. +// +// Solidity: function aggregate3((address,bool,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) Aggregate3(calls []Multicall3Call3) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate3(&_Multicall3.TransactOpts, calls) +} + +// Aggregate3Value is a paid mutator transaction binding the contract method 0x174dea71. +// +// Solidity: function aggregate3Value((address,bool,uint256,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) Aggregate3Value(opts *bind.TransactOpts, calls []Multicall3Call3Value) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "aggregate3Value", calls) +} + +// Aggregate3Value is a paid mutator transaction binding the contract method 0x174dea71. +// +// Solidity: function aggregate3Value((address,bool,uint256,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) Aggregate3Value(calls []Multicall3Call3Value) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate3Value(&_Multicall3.TransactOpts, calls) +} + +// Aggregate3Value is a paid mutator transaction binding the contract method 0x174dea71. +// +// Solidity: function aggregate3Value((address,bool,uint256,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) Aggregate3Value(calls []Multicall3Call3Value) (*types.Transaction, error) { + return _Multicall3.Contract.Aggregate3Value(&_Multicall3.TransactOpts, calls) +} + +// BlockAndAggregate is a paid mutator transaction binding the contract method 0xc3077fa9. +// +// Solidity: function blockAndAggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) BlockAndAggregate(opts *bind.TransactOpts, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "blockAndAggregate", calls) +} + +// BlockAndAggregate is a paid mutator transaction binding the contract method 0xc3077fa9. +// +// Solidity: function blockAndAggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) BlockAndAggregate(calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.BlockAndAggregate(&_Multicall3.TransactOpts, calls) +} + +// BlockAndAggregate is a paid mutator transaction binding the contract method 0xc3077fa9. +// +// Solidity: function blockAndAggregate((address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) BlockAndAggregate(calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.BlockAndAggregate(&_Multicall3.TransactOpts, calls) +} + +// TryAggregate is a paid mutator transaction binding the contract method 0xbce38bd7. +// +// Solidity: function tryAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) TryAggregate(opts *bind.TransactOpts, requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "tryAggregate", requireSuccess, calls) +} + +// TryAggregate is a paid mutator transaction binding the contract method 0xbce38bd7. +// +// Solidity: function tryAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) TryAggregate(requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.TryAggregate(&_Multicall3.TransactOpts, requireSuccess, calls) +} + +// TryAggregate is a paid mutator transaction binding the contract method 0xbce38bd7. +// +// Solidity: function tryAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns((bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) TryAggregate(requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.TryAggregate(&_Multicall3.TransactOpts, requireSuccess, calls) +} + +// TryBlockAndAggregate is a paid mutator transaction binding the contract method 0x399542e9. +// +// Solidity: function tryBlockAndAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Transactor) TryBlockAndAggregate(opts *bind.TransactOpts, requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.contract.Transact(opts, "tryBlockAndAggregate", requireSuccess, calls) +} + +// TryBlockAndAggregate is a paid mutator transaction binding the contract method 0x399542e9. +// +// Solidity: function tryBlockAndAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3Session) TryBlockAndAggregate(requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.TryBlockAndAggregate(&_Multicall3.TransactOpts, requireSuccess, calls) +} + +// TryBlockAndAggregate is a paid mutator transaction binding the contract method 0x399542e9. +// +// Solidity: function tryBlockAndAggregate(bool requireSuccess, (address,bytes)[] calls) payable returns(uint256 blockNumber, bytes32 blockHash, (bool,bytes)[] returnData) +func (_Multicall3 *Multicall3TransactorSession) TryBlockAndAggregate(requireSuccess bool, calls []Multicall3Call) (*types.Transaction, error) { + return _Multicall3.Contract.TryBlockAndAggregate(&_Multicall3.TransactOpts, requireSuccess, calls) +} diff --git a/node/exts/erc20-bridge/abigen/multicall3_abi.json b/node/exts/erc20-bridge/abigen/multicall3_abi.json new file mode 100644 index 000000000..d9c5855e7 --- /dev/null +++ b/node/exts/erc20-bridge/abigen/multicall3_abi.json @@ -0,0 +1,440 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes[]", + "name": "returnData", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bool", + "name": "allowFailure", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call3[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate3", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bool", + "name": "allowFailure", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call3Value[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate3Value", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "blockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "getBasefee", + "outputs": [ + { + "internalType": "uint256", + "name": "basefee", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "getBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getChainId", + "outputs": [ + { + "internalType": "uint256", + "name": "chainid", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockCoinbase", + "outputs": [ + { + "internalType": "address", + "name": "coinbase", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockDifficulty", + "outputs": [ + { + "internalType": "uint256", + "name": "difficulty", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "gaslimit", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "getEthBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLastBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryAggregate", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryBlockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall3.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "payable", + "type": "function" + } +] \ No newline at end of file diff --git a/node/exts/erc20reward/abigen/reward_distributor.go b/node/exts/erc20-bridge/abigen/reward_distributor.go similarity index 86% rename from node/exts/erc20reward/abigen/reward_distributor.go rename to node/exts/erc20-bridge/abigen/reward_distributor.go index bcb550c07..69634710f 100644 --- a/node/exts/erc20reward/abigen/reward_distributor.go +++ b/node/exts/erc20-bridge/abigen/reward_distributor.go @@ -31,7 +31,7 @@ var ( // RewardDistributorMetaData contains all meta data concerning the RewardDistributor contract. var RewardDistributorMetaData = &bind.MetaData{ - ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_safe\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_posterFee\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_rewardToken\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"ReentrancyGuardReentrantCall\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newFee\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"nonce\",\"type\":\"uint256\"}],\"name\":\"PosterFeeUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"RewardClaimed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"poster\",\"type\":\"address\"}],\"name\":\"RewardPosted\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"kwilBlockHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"rewardRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32[]\",\"name\":\"proofs\",\"type\":\"bytes32[]\"}],\"name\":\"claimReward\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"isRewardClaimed\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"nonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"postReward\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"postedRewards\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"posterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"rewardPoster\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"rewardToken\",\"outputs\":[{\"internalType\":\"contractIERC20\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"safe\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"unpostedRewards\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newFee\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"_nonce\",\"type\":\"uint256\"}],\"name\":\"updatePosterFee\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", + ABI: "[{\"inputs\":[],\"name\":\"ReentrancyGuardReentrantCall\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"oldFee\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"newFee\",\"type\":\"uint256\"}],\"name\":\"PosterFeeUpdated\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"claimer\",\"type\":\"address\"}],\"name\":\"RewardClaimed\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"poster\",\"type\":\"address\"}],\"name\":\"RewardPosted\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"recipient\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"kwilBlockHash\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"rewardRoot\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32[]\",\"name\":\"proofs\",\"type\":\"bytes32[]\"}],\"name\":\"claimReward\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"isRewardClaimed\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"amount\",\"type\":\"uint256\"}],\"name\":\"postReward\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"postedRewards\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"posterFee\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"rewardPoster\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"rewardToken\",\"outputs\":[{\"internalType\":\"contractIERC20\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"safe\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_safe\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"_posterFee\",\"type\":\"uint256\"},{\"internalType\":\"address\",\"name\":\"_rewardToken\",\"type\":\"address\"}],\"name\":\"setup\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"unpostedRewards\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"newFee\",\"type\":\"uint256\"}],\"name\":\"updatePosterFee\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}]", } // RewardDistributorABI is the input ABI used to generate the binding from. @@ -211,37 +211,6 @@ func (_RewardDistributor *RewardDistributorCallerSession) IsRewardClaimed(arg0 [ return _RewardDistributor.Contract.IsRewardClaimed(&_RewardDistributor.CallOpts, arg0, arg1) } -// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. -// -// Solidity: function nonce() view returns(uint256) -func (_RewardDistributor *RewardDistributorCaller) Nonce(opts *bind.CallOpts) (*big.Int, error) { - var out []interface{} - err := _RewardDistributor.contract.Call(opts, &out, "nonce") - - if err != nil { - return *new(*big.Int), err - } - - out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) - - return out0, err - -} - -// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. -// -// Solidity: function nonce() view returns(uint256) -func (_RewardDistributor *RewardDistributorSession) Nonce() (*big.Int, error) { - return _RewardDistributor.Contract.Nonce(&_RewardDistributor.CallOpts) -} - -// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. -// -// Solidity: function nonce() view returns(uint256) -func (_RewardDistributor *RewardDistributorCallerSession) Nonce() (*big.Int, error) { - return _RewardDistributor.Contract.Nonce(&_RewardDistributor.CallOpts) -} - // PostedRewards is a free data retrieval call binding the contract method 0x122d52de. // // Solidity: function postedRewards() view returns(uint256) @@ -470,25 +439,46 @@ func (_RewardDistributor *RewardDistributorTransactorSession) PostReward(root [3 return _RewardDistributor.Contract.PostReward(&_RewardDistributor.TransactOpts, root, amount) } -// UpdatePosterFee is a paid mutator transaction binding the contract method 0xe08b22fa. +// Setup is a paid mutator transaction binding the contract method 0xf00e5686. +// +// Solidity: function setup(address _safe, uint256 _posterFee, address _rewardToken) returns() +func (_RewardDistributor *RewardDistributorTransactor) Setup(opts *bind.TransactOpts, _safe common.Address, _posterFee *big.Int, _rewardToken common.Address) (*types.Transaction, error) { + return _RewardDistributor.contract.Transact(opts, "setup", _safe, _posterFee, _rewardToken) +} + +// Setup is a paid mutator transaction binding the contract method 0xf00e5686. +// +// Solidity: function setup(address _safe, uint256 _posterFee, address _rewardToken) returns() +func (_RewardDistributor *RewardDistributorSession) Setup(_safe common.Address, _posterFee *big.Int, _rewardToken common.Address) (*types.Transaction, error) { + return _RewardDistributor.Contract.Setup(&_RewardDistributor.TransactOpts, _safe, _posterFee, _rewardToken) +} + +// Setup is a paid mutator transaction binding the contract method 0xf00e5686. +// +// Solidity: function setup(address _safe, uint256 _posterFee, address _rewardToken) returns() +func (_RewardDistributor *RewardDistributorTransactorSession) Setup(_safe common.Address, _posterFee *big.Int, _rewardToken common.Address) (*types.Transaction, error) { + return _RewardDistributor.Contract.Setup(&_RewardDistributor.TransactOpts, _safe, _posterFee, _rewardToken) +} + +// UpdatePosterFee is a paid mutator transaction binding the contract method 0xb19050bd. // -// Solidity: function updatePosterFee(uint256 newFee, uint256 _nonce) returns() -func (_RewardDistributor *RewardDistributorTransactor) UpdatePosterFee(opts *bind.TransactOpts, newFee *big.Int, _nonce *big.Int) (*types.Transaction, error) { - return _RewardDistributor.contract.Transact(opts, "updatePosterFee", newFee, _nonce) +// Solidity: function updatePosterFee(uint256 newFee) returns() +func (_RewardDistributor *RewardDistributorTransactor) UpdatePosterFee(opts *bind.TransactOpts, newFee *big.Int) (*types.Transaction, error) { + return _RewardDistributor.contract.Transact(opts, "updatePosterFee", newFee) } -// UpdatePosterFee is a paid mutator transaction binding the contract method 0xe08b22fa. +// UpdatePosterFee is a paid mutator transaction binding the contract method 0xb19050bd. // -// Solidity: function updatePosterFee(uint256 newFee, uint256 _nonce) returns() -func (_RewardDistributor *RewardDistributorSession) UpdatePosterFee(newFee *big.Int, _nonce *big.Int) (*types.Transaction, error) { - return _RewardDistributor.Contract.UpdatePosterFee(&_RewardDistributor.TransactOpts, newFee, _nonce) +// Solidity: function updatePosterFee(uint256 newFee) returns() +func (_RewardDistributor *RewardDistributorSession) UpdatePosterFee(newFee *big.Int) (*types.Transaction, error) { + return _RewardDistributor.Contract.UpdatePosterFee(&_RewardDistributor.TransactOpts, newFee) } -// UpdatePosterFee is a paid mutator transaction binding the contract method 0xe08b22fa. +// UpdatePosterFee is a paid mutator transaction binding the contract method 0xb19050bd. // -// Solidity: function updatePosterFee(uint256 newFee, uint256 _nonce) returns() -func (_RewardDistributor *RewardDistributorTransactorSession) UpdatePosterFee(newFee *big.Int, _nonce *big.Int) (*types.Transaction, error) { - return _RewardDistributor.Contract.UpdatePosterFee(&_RewardDistributor.TransactOpts, newFee, _nonce) +// Solidity: function updatePosterFee(uint256 newFee) returns() +func (_RewardDistributor *RewardDistributorTransactorSession) UpdatePosterFee(newFee *big.Int) (*types.Transaction, error) { + return _RewardDistributor.Contract.UpdatePosterFee(&_RewardDistributor.TransactOpts, newFee) } // Fallback is a paid mutator transaction binding the contract fallback function. @@ -602,14 +592,14 @@ func (it *RewardDistributorPosterFeeUpdatedIterator) Close() error { // RewardDistributorPosterFeeUpdated represents a PosterFeeUpdated event raised by the RewardDistributor contract. type RewardDistributorPosterFeeUpdated struct { + OldFee *big.Int NewFee *big.Int - Nonce *big.Int Raw types.Log // Blockchain specific contextual infos } // FilterPosterFeeUpdated is a free log retrieval operation binding the contract event 0x7c7423dff6eff60ac491456a649034ee92866801bb236290a4b9190e370e8952. // -// Solidity: event PosterFeeUpdated(uint256 newFee, uint256 nonce) +// Solidity: event PosterFeeUpdated(uint256 oldFee, uint256 newFee) func (_RewardDistributor *RewardDistributorFilterer) FilterPosterFeeUpdated(opts *bind.FilterOpts) (*RewardDistributorPosterFeeUpdatedIterator, error) { logs, sub, err := _RewardDistributor.contract.FilterLogs(opts, "PosterFeeUpdated") @@ -621,7 +611,7 @@ func (_RewardDistributor *RewardDistributorFilterer) FilterPosterFeeUpdated(opts // WatchPosterFeeUpdated is a free log subscription operation binding the contract event 0x7c7423dff6eff60ac491456a649034ee92866801bb236290a4b9190e370e8952. // -// Solidity: event PosterFeeUpdated(uint256 newFee, uint256 nonce) +// Solidity: event PosterFeeUpdated(uint256 oldFee, uint256 newFee) func (_RewardDistributor *RewardDistributorFilterer) WatchPosterFeeUpdated(opts *bind.WatchOpts, sink chan<- *RewardDistributorPosterFeeUpdated) (event.Subscription, error) { logs, sub, err := _RewardDistributor.contract.WatchLogs(opts, "PosterFeeUpdated") @@ -658,7 +648,7 @@ func (_RewardDistributor *RewardDistributorFilterer) WatchPosterFeeUpdated(opts // ParsePosterFeeUpdated is a log parse operation binding the contract event 0x7c7423dff6eff60ac491456a649034ee92866801bb236290a4b9190e370e8952. // -// Solidity: event PosterFeeUpdated(uint256 newFee, uint256 nonce) +// Solidity: event PosterFeeUpdated(uint256 oldFee, uint256 newFee) func (_RewardDistributor *RewardDistributorFilterer) ParsePosterFeeUpdated(log types.Log) (*RewardDistributorPosterFeeUpdated, error) { event := new(RewardDistributorPosterFeeUpdated) if err := _RewardDistributor.contract.UnpackLog(event, "PosterFeeUpdated", log); err != nil { diff --git a/node/exts/erc20-bridge/abigen/reward_distributor_abi.json b/node/exts/erc20-bridge/abigen/reward_distributor_abi.json new file mode 100644 index 000000000..446957efc --- /dev/null +++ b/node/exts/erc20-bridge/abigen/reward_distributor_abi.json @@ -0,0 +1,279 @@ +[ + { + "inputs": [ ], + "name": "ReentrancyGuardReentrantCall", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "oldFee", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "PosterFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "claimer", + "type": "address" + } + ], + "name": "RewardClaimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "poster", + "type": "address" + } + ], + "name": "RewardPosted", + "type": "event" + }, + { + "stateMutability": "payable", + "type": "fallback" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "kwilBlockHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "rewardRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32[]", + "name": "proofs", + "type": "bytes32[]" + } + ], + "name": "claimReward", + "outputs": [ ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "isRewardClaimed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "root", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "postReward", + "outputs": [ ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ ], + "name": "postedRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ ], + "name": "posterFee", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "rewardPoster", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ ], + "name": "rewardToken", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ ], + "name": "safe", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_safe", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_posterFee", + "type": "uint256" + }, + { + "internalType": "address", + "name": "_rewardToken", + "type": "address" + } + ], + "name": "setup", + "outputs": [ ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ ], + "name": "unpostedRewards", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "newFee", + "type": "uint256" + } + ], + "name": "updatePosterFee", + "outputs": [ ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/node/exts/erc20-bridge/abigen/safe.go b/node/exts/erc20-bridge/abigen/safe.go new file mode 100644 index 000000000..16a29e58b --- /dev/null +++ b/node/exts/erc20-bridge/abigen/safe.go @@ -0,0 +1,274 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package abigen + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription + _ = abi.ConvertType +) + +// SafeMetaData contains all meta data concerning the Safe contract. +var SafeMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"name\":\"nonce\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getThreshold\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"getOwners\",\"outputs\":[{\"internalType\":\"address[]\",\"name\":\"\",\"type\":\"address[]\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", +} + +// SafeABI is the input ABI used to generate the binding from. +// Deprecated: Use SafeMetaData.ABI instead. +var SafeABI = SafeMetaData.ABI + +// Safe is an auto generated Go binding around an Ethereum contract. +type Safe struct { + SafeCaller // Read-only binding to the contract + SafeTransactor // Write-only binding to the contract + SafeFilterer // Log filterer for contract events +} + +// SafeCaller is an auto generated read-only Go binding around an Ethereum contract. +type SafeCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SafeTransactor is an auto generated write-only Go binding around an Ethereum contract. +type SafeTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SafeFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type SafeFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// SafeSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type SafeSession struct { + Contract *Safe // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// SafeCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type SafeCallerSession struct { + Contract *SafeCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// SafeTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type SafeTransactorSession struct { + Contract *SafeTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// SafeRaw is an auto generated low-level Go binding around an Ethereum contract. +type SafeRaw struct { + Contract *Safe // Generic contract binding to access the raw methods on +} + +// SafeCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type SafeCallerRaw struct { + Contract *SafeCaller // Generic read-only contract binding to access the raw methods on +} + +// SafeTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type SafeTransactorRaw struct { + Contract *SafeTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewSafe creates a new instance of Safe, bound to a specific deployed contract. +func NewSafe(address common.Address, backend bind.ContractBackend) (*Safe, error) { + contract, err := bindSafe(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &Safe{SafeCaller: SafeCaller{contract: contract}, SafeTransactor: SafeTransactor{contract: contract}, SafeFilterer: SafeFilterer{contract: contract}}, nil +} + +// NewSafeCaller creates a new read-only instance of Safe, bound to a specific deployed contract. +func NewSafeCaller(address common.Address, caller bind.ContractCaller) (*SafeCaller, error) { + contract, err := bindSafe(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &SafeCaller{contract: contract}, nil +} + +// NewSafeTransactor creates a new write-only instance of Safe, bound to a specific deployed contract. +func NewSafeTransactor(address common.Address, transactor bind.ContractTransactor) (*SafeTransactor, error) { + contract, err := bindSafe(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &SafeTransactor{contract: contract}, nil +} + +// NewSafeFilterer creates a new log filterer instance of Safe, bound to a specific deployed contract. +func NewSafeFilterer(address common.Address, filterer bind.ContractFilterer) (*SafeFilterer, error) { + contract, err := bindSafe(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &SafeFilterer{contract: contract}, nil +} + +// bindSafe binds a generic wrapper to an already deployed contract. +func bindSafe(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := SafeMetaData.GetAbi() + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, *parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Safe *SafeRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Safe.Contract.SafeCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Safe *SafeRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Safe.Contract.SafeTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Safe *SafeRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Safe.Contract.SafeTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_Safe *SafeCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _Safe.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_Safe *SafeTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _Safe.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_Safe *SafeTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _Safe.Contract.contract.Transact(opts, method, params...) +} + +// GetOwners is a free data retrieval call binding the contract method 0xa0e67e2b. +// +// Solidity: function getOwners() view returns(address[]) +func (_Safe *SafeCaller) GetOwners(opts *bind.CallOpts) ([]common.Address, error) { + var out []interface{} + err := _Safe.contract.Call(opts, &out, "getOwners") + + if err != nil { + return *new([]common.Address), err + } + + out0 := *abi.ConvertType(out[0], new([]common.Address)).(*[]common.Address) + + return out0, err + +} + +// GetOwners is a free data retrieval call binding the contract method 0xa0e67e2b. +// +// Solidity: function getOwners() view returns(address[]) +func (_Safe *SafeSession) GetOwners() ([]common.Address, error) { + return _Safe.Contract.GetOwners(&_Safe.CallOpts) +} + +// GetOwners is a free data retrieval call binding the contract method 0xa0e67e2b. +// +// Solidity: function getOwners() view returns(address[]) +func (_Safe *SafeCallerSession) GetOwners() ([]common.Address, error) { + return _Safe.Contract.GetOwners(&_Safe.CallOpts) +} + +// GetThreshold is a free data retrieval call binding the contract method 0xe75235b8. +// +// Solidity: function getThreshold() view returns(uint256) +func (_Safe *SafeCaller) GetThreshold(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Safe.contract.Call(opts, &out, "getThreshold") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// GetThreshold is a free data retrieval call binding the contract method 0xe75235b8. +// +// Solidity: function getThreshold() view returns(uint256) +func (_Safe *SafeSession) GetThreshold() (*big.Int, error) { + return _Safe.Contract.GetThreshold(&_Safe.CallOpts) +} + +// GetThreshold is a free data retrieval call binding the contract method 0xe75235b8. +// +// Solidity: function getThreshold() view returns(uint256) +func (_Safe *SafeCallerSession) GetThreshold() (*big.Int, error) { + return _Safe.Contract.GetThreshold(&_Safe.CallOpts) +} + +// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. +// +// Solidity: function nonce() view returns(uint256) +func (_Safe *SafeCaller) Nonce(opts *bind.CallOpts) (*big.Int, error) { + var out []interface{} + err := _Safe.contract.Call(opts, &out, "nonce") + + if err != nil { + return *new(*big.Int), err + } + + out0 := *abi.ConvertType(out[0], new(*big.Int)).(**big.Int) + + return out0, err + +} + +// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. +// +// Solidity: function nonce() view returns(uint256) +func (_Safe *SafeSession) Nonce() (*big.Int, error) { + return _Safe.Contract.Nonce(&_Safe.CallOpts) +} + +// Nonce is a free data retrieval call binding the contract method 0xaffed0e0. +// +// Solidity: function nonce() view returns(uint256) +func (_Safe *SafeCallerSession) Nonce() (*big.Int, error) { + return _Safe.Contract.Nonce(&_Safe.CallOpts) +} diff --git a/node/exts/erc20-bridge/abigen/safe_abi.json b/node/exts/erc20-bridge/abigen/safe_abi.json new file mode 100644 index 000000000..34f158781 --- /dev/null +++ b/node/exts/erc20-bridge/abigen/safe_abi.json @@ -0,0 +1,41 @@ +[ + { + "inputs": [], + "name": "nonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getThreshold", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getOwners", + "outputs": [ + { + "internalType": "address[]", + "name": "", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/node/exts/erc20reward/.gitignore b/node/exts/erc20-bridge/erc20/.gitignore similarity index 100% rename from node/exts/erc20reward/.gitignore rename to node/exts/erc20-bridge/erc20/.gitignore diff --git a/node/exts/erc20reward/meta_extension.go b/node/exts/erc20-bridge/erc20/meta_extension.go similarity index 77% rename from node/exts/erc20reward/meta_extension.go rename to node/exts/erc20-bridge/erc20/meta_extension.go index 81fce5d1b..a70ee0734 100644 --- a/node/exts/erc20reward/meta_extension.go +++ b/node/exts/erc20-bridge/erc20/meta_extension.go @@ -1,4 +1,4 @@ -// package meta implements a meta extension that manages all rewards on a Kwil network. +// package erc20reward implements a meta extension that manages all rewards on a Kwil network. // It is used to create other extensions with which users can distribute erc20 tokens // to users. // It works by exposing an action to the DB owner which allows creation of new extensions @@ -10,7 +10,7 @@ // Internally, the node will start another event listener which is responsible for tracking // the erc20's Transfer event. When a transfer event is detected, the node will update the // reward balance of the recipient. -package erc20reward +package erc20 import ( "bytes" @@ -23,11 +23,13 @@ import ( "sync" "time" + "github.com/decred/dcrd/container/lru" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts/abi/bind" ethcommon "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" + "github.com/kwilteam/kwil-db/common" "github.com/kwilteam/kwil-db/core/log" "github.com/kwilteam/kwil-db/core/types" @@ -37,8 +39,8 @@ import ( "github.com/kwilteam/kwil-db/extensions/precompiles" "github.com/kwilteam/kwil-db/extensions/resolutions" "github.com/kwilteam/kwil-db/node/engine" - "github.com/kwilteam/kwil-db/node/exts/erc20reward/abigen" - "github.com/kwilteam/kwil-db/node/exts/erc20reward/reward" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/abigen" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/utils" evmsync "github.com/kwilteam/kwil-db/node/exts/evm-sync" "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" "github.com/kwilteam/kwil-db/node/types/sql" @@ -48,6 +50,8 @@ import ( const ( RewardMetaExtensionName = "kwil_erc20_meta" uint256Precision = 78 + + rewardMerkleTreeLRUSize = 1000 ) var ( @@ -68,6 +72,8 @@ var ( // so that we know how to decode them logTypeTransfer = []byte("e20trsnfr") logTypeConfirmedEpoch = []byte("cnfepch") + + mtLRUCache = lru.NewMap[[32]byte, []byte](rewardMerkleTreeLRUSize) // tree root => tree body ) // generates a deterministic UUID for the chain and escrow @@ -510,7 +516,7 @@ func init() { {Name: "epoch_period", Type: types.TextType}, {Name: "erc20", Type: types.TextType, Nullable: true}, {Name: "decimals", Type: types.IntType, Nullable: true}, - {Name: "balance", Type: types.TextType, Nullable: true}, // total unspent balance + {Name: "balance", Type: uint256Numeric, Nullable: true}, // total unspent balance {Name: "synced", Type: types.BoolType}, {Name: "synced_at", Type: types.IntType, Nullable: true}, {Name: "enabled", Type: types.BoolType}, @@ -529,7 +535,8 @@ func init() { defer info.mu.RUnlock() // these values can be null if the extension is not synced - var erc20Address, ownedBalance *string + var erc20Address *string + var ownedBalance *types.Decimal var decimals, syncedAt *int64 dur := time.Duration(info.userProvidedData.DistributionPeriod) * time.Second @@ -538,8 +545,7 @@ func init() { erc20Addr := info.syncedRewardData.Erc20Address.Hex() erc20Address = &erc20Addr decimals = &info.syncedRewardData.Erc20Decimals - owbalStr := info.ownedBalance.String() - ownedBalance = &owbalStr + ownedBalance = info.ownedBalance syncedAt = &info.syncedAt } @@ -610,48 +616,7 @@ func init() { user := inputs[1].(string) amount := inputs[2].(*types.Decimal) - if amount.IsNegative() { - return fmt.Errorf("amount cannot be negative") - } - - info, err := SINGLETON.getUsableInstance(id) - if err != nil { - return err - } - info.mu.RLock() - // we cannot defer an RUnlock here because we need to unlock - // the read lock before we can acquire the write lock, which - // we do at the end of this - - newBal, err := types.DecimalSub(info.ownedBalance, amount) - if err != nil { - info.mu.RUnlock() - return err - } - - if newBal.IsNegative() { - info.mu.RUnlock() - return fmt.Errorf("network does not enough balance to issue %s to %s", amount, user) - } - - addr, err := ethAddressFromHex(user) - if err != nil { - info.mu.RUnlock() - return err - } - - err = issueReward(ctx.TxContext.Ctx, app, info.currentEpoch.ID, addr, amount) - if err != nil { - info.mu.RUnlock() - return err - } - - info.mu.RUnlock() - info.mu.Lock() - info.ownedBalance = newBal - info.mu.Unlock() - - return nil + return SINGLETON.issueTokens(ctx.TxContext.Ctx, app, id, user, amount) }, }, { @@ -816,34 +781,49 @@ func init() { return err } + if bal == nil { + bal, _ = erc20ValueFromBigInt(big.NewInt(0)) + } + return resultFn([]any{bal}) }, }, { - // lists epochs that have not been confirmed yet, but have been ended. - // It lists them in ascending order (e.g. oldest first). - Name: "list_unconfirmed_epochs", + // bridge will 'issue' token to the caller, from its own balance + Name: "bridge", Parameters: []precompiles.PrecompileValue{ {Name: "id", Type: types.UUIDType}, + {Name: "amount", Type: uint256Numeric, Nullable: true}, }, - Returns: &precompiles.MethodReturn{ - IsTable: true, - Fields: []precompiles.PrecompileValue{ - {Name: "epoch_id", Type: types.UUIDType}, - {Name: "start_height", Type: types.IntType}, - {Name: "start_timestamp", Type: types.IntType}, - {Name: "end_height", Type: types.IntType}, - {Name: "reward_root", Type: types.ByteaType}, - {Name: "end_block_hash", Type: types.ByteaType}, - }, - }, - AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { id := inputs[0].(*types.UUID) - return getUnconfirmedEpochs(ctx.TxContext.Ctx, app, id, func(e *Epoch) error { - return resultFn([]any{e.ID, e.StartHeight, e.StartTime.Unix(), *e.EndHeight, e.Root, e.BlockHash}) - }) + var err error + var amount *types.Decimal + // if 'amount' is omited, withdraw all balance + if inputs[1] == nil { + callerAddr, err := ethAddressFromHex(ctx.TxContext.Caller) + if err != nil { + return err + } + + amount, err = balanceOf(ctx.TxContext.Ctx, app, id, callerAddr) + if err != nil { + return err + } + } else { + amount = inputs[1].(*types.Decimal) + } + + // first, lock required 'amount' from caller to the network + err = SINGLETON.lockTokens(ctx.TxContext.Ctx, app, id, ctx.TxContext.Caller, amount) + if err != nil { + return err + } + + // then issue to caller itself + return SINGLETON.issueTokens(ctx.TxContext.Ctx, app, id, ctx.TxContext.Caller, amount) }, }, { @@ -942,107 +922,256 @@ func init() { return resultFn([]any{scaled}) }, }, - // { - // // Supposed to be called by Signer service - // // Returns epoch rewards after(non-include) after_height, in ASC order. - // Name: "list_epochs", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "after_height", Type: types.IntType}, - // {Name: "limit", Type: types.IntType}, - // }, - // Returns: &precompiles.MethodReturn{ - // IsTable: true, - // Fields: (&Epoch{}).UnpackTypes(), // TODO: I might need to update this depending on what happens with decimal types - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, - // { - // // Supposed to be called by the SignerService, to verify the reward root. - // // Could be merged into 'list_epochs' - // // Returns pending rewards from(include) start_height to(include) end_height, in ASC order. - // // NOTE: Rewards of same address will be aggregated. - // Name: "search_rewards", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "start_height", Type: types.IntType}, - // {Name: "end_height", Type: types.IntType}, - // }, - // Returns: &precompiles.MethodReturn{ - // IsTable: true, - // Fields: (&Reward{}).UnpackTypes(), // TODO: I might need to update this depending on what happens with decimal types - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, - // { - // // Supposed to be called by Kwil network in an end block hook. - // Name: "propose_epoch", - // AccessModifiers: []precompiles.Modifier{precompiles.SYSTEM}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // if !calledByExtension(ctx) { - // return errors.New("propose_epoch can only be called by the Kwil network") - // } - - // panic("finish me") - // }, - // }, - // { - // // Supposed to be called by a multisig signer - // Name: "vote_epoch", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "sign_hash", Type: types.ByteaType}, - // {Name: "signatures", Type: types.ByteaType}, - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, - // { - // // Lists all epochs that have received enough votes - // Name: "list_finalized", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "after_height", Type: types.IntType}, - // {Name: "limit", Type: types.IntType}, - // }, - // Returns: &precompiles.MethodReturn{ - // IsTable: true, - // Fields: (&FinalizedReward{}).UnpackTypes(), // TODO: I might need to update this depending on what happens with decimal types - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, - // { - // // Called by app / user - // Name: "claim_info", - // Parameters: []precompiles.PrecompileValue{ - // {Name: "id", Type: types.UUIDType}, - // {Name: "sign_hash", Type: types.ByteaType, Nullable: false}, - // {Name: "wallet_address", Type: types.TextType, Nullable: false}, - // }, - // Returns: &precompiles.MethodReturn{ - // Fields: []precompiles.PrecompileValue{ - // {Name: "amount", Type: types.TextType}, - // {Name: "block_hash", Type: types.TextType}, - // {Name: "root", Type: types.TextType}, - // {Name: "proofs", Type: types.TextArrayType}, - // }, - // }, - // AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - // Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { - // panic("finish me") - // }, - // }, + { + // get only active epochs: finalized epoch and collecting epoch + Name: "get_active_epochs", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "start_height", Type: types.IntType}, + {Name: "start_timestamp", Type: types.IntType}, + {Name: "end_height", Type: types.IntType, Nullable: true}, + {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "reward_amount", Type: uint256Numeric, Nullable: true}, + {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "confirmed", Type: types.BoolType}, + {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, + {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + id := inputs[0].(*types.UUID) + + return getActiveEpochs(ctx.TxContext.Ctx, app, id, func(e *Epoch) error { + var voters []string + if len(e.Voters) > 0 { + for _, item := range e.Voters { + voters = append(voters, item.String()) + } + } + + return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, e.Total, e.BlockHash, e.Confirmed, + voters, + e.VoteNonces, + e.VoteSigs, + }) + }) + }}, + { + // lists epochs after(non-include) given height, in ASC order. + Name: "list_epochs", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "after", Type: types.IntType}, + {Name: "limit", Type: types.IntType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "start_height", Type: types.IntType}, + {Name: "start_timestamp", Type: types.IntType}, + {Name: "end_height", Type: types.IntType, Nullable: true}, + {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "reward_amount", Type: uint256Numeric, Nullable: true}, + {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "confirmed", Type: types.BoolType}, + {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, + {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + id := inputs[0].(*types.UUID) + after := inputs[1].(int64) + limit := inputs[2].(int64) + + return getEpochs(ctx.TxContext.Ctx, app, id, after, limit, func(e *Epoch) error { + var voters []string + if len(e.Voters) > 0 { + for _, item := range e.Voters { + voters = append(voters, item.String()) + } + } + + return resultFn([]any{e.ID, e.StartHeight, e.StartTime, *e.EndHeight, e.Root, e.Total, e.BlockHash, e.Confirmed, + voters, + e.VoteNonces, + e.VoteSigs, + }) + }) + }, + }, + { + // get all rewards associated with given epoch_id + Name: "get_epoch_rewards", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "epoch_id", Type: types.UUIDType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "recipient", Type: types.TextType}, + {Name: "amount", Type: types.TextType}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + //id := inputs[0].(*types.UUID) + epochID := inputs[1].(*types.UUID) + return getRewardsForEpoch(ctx.TxContext.Ctx, app, epochID, func(reward *EpochReward) error { + return resultFn([]any{reward.Recipient.String(), reward.Amount.String()}) + }) + }, + }, + { + Name: "vote_epoch", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "epoch_id", Type: types.UUIDType}, + {Name: "nonce", Type: types.IntType}, + {Name: "signature", Type: types.ByteaType}, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + //id := inputs[0].(*types.UUID) + epochID := inputs[1].(*types.UUID) + nonce := inputs[2].(int64) + signature := inputs[3].([]byte) + + if len(signature) != utils.GnosisSafeSigLength { + return fmt.Errorf("signature is not 65 bytes") + } + + from, err := ethAddressFromHex(ctx.TxContext.Caller) + if err != nil { + return err + } + + // NOTE: if we have safe address and safe nonce, we can verify the signature + // But if we only have safe address, and safeNonce from the input, then it's no point + + ok, err := canVoteEpoch(ctx.TxContext.Ctx, app, epochID) + if err != nil { + return fmt.Errorf("check epoch can vote: %w", err) + } + + if !ok { + return fmt.Errorf("epoch cannot be voted") + } + + return voteEpoch(ctx.TxContext.Ctx, app, epochID, from, nonce, signature) + }, + }, + { + // list all the rewards of the given wallet; + // if pending=true, the results will include all finalized(not necessary confirmed) rewards + Name: "list_wallet_rewards", + Parameters: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "wallet", Type: types.TextType}, + {Name: "pending", Type: types.BoolType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "chain", Type: types.TextType}, + {Name: "chain_id", Type: types.TextType}, + {Name: "contract", Type: types.TextType}, + {Name: "created_at", Type: types.IntType}, + {Name: "param_recipient", Type: types.TextType}, + {Name: "param_amount", Type: uint256Numeric}, + {Name: "param_block_hash", Type: types.ByteaType}, + {Name: "param_root", Type: types.ByteaType}, + {Name: "param_proofs", Type: types.ByteaArrayType}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: func(ctx *common.EngineContext, app *common.App, inputs []any, resultFn func([]any) error) error { + id := inputs[0].(*types.UUID) + wallet := inputs[1].(string) + walletAddr, err := ethAddressFromHex(wallet) + if err != nil { + return err + } + + pending := inputs[2].(bool) + + info, err := SINGLETON.getUsableInstance(id) + if err != nil { + return err + } + + info.mu.RLock() + defer info.mu.RUnlock() + + var epochs []*Epoch + err = getWalletEpochs(ctx.TxContext.Ctx, app, id, walletAddr, pending, func(e *Epoch) error { + epochs = append(epochs, e) + return nil + }) + if err != nil { + return fmt.Errorf("get wallet epochs :%w", err) + } + + var jsonTree, root []byte + var ok bool + for _, epoch := range epochs { + var b32Root [32]byte + copy(b32Root[:], epoch.Root) + + jsonTree, ok = mtLRUCache.Get(b32Root) + if !ok { + var b32Hash [32]byte + copy(b32Hash[:], epoch.BlockHash) + _, jsonTree, root, _, err = genMerkleTreeForEpoch(ctx.TxContext.Ctx, app, epoch.ID, info.EscrowAddress.Hex(), b32Hash) + if err != nil { + return err + } + + if !bytes.Equal(root, epoch.Root) { + return fmt.Errorf("internal bug: epoch root mismatch") + } + + mtLRUCache.Put(b32Root, jsonTree) + } + + _, proofs, _, bh, amtBig, err := utils.GetMTreeProof(jsonTree, walletAddr.String()) + if err != nil { + return err + } + + uint256Amt, err := erc20ValueFromBigInt(amtBig) + if err != nil { + return err + } + + err = resultFn([]any{info.ChainInfo.Name.String(), + info.ChainInfo.ID, + info.EscrowAddress.String(), + epoch.EndHeight, + walletAddr.String(), + uint256Amt, + bh, + epoch.Root, + proofs, + }) + if err != nil { + return err + } + } + + return nil + }, + }, }, }, nil }) @@ -1091,44 +1220,64 @@ func init() { err := SINGLETON.ForEachInstance(true, func(id *types.UUID, info *rewardExtensionInfo) error { info.mu.RLock() defer info.mu.RUnlock() - // if the block is greater than or equal to the start time + distribution period, - // we should propose a new epoch. Otherwise, we should do nothing. - if block.Timestamp < info.currentEpoch.StartTime.Add(time.Duration(info.userProvidedData.DistributionPeriod)*time.Second).Unix() { + // If the block is greater than or equal to the start time + distribution period: Otherwise, we should do nothing. + if block.Timestamp-info.currentEpoch.StartTime < info.userProvidedData.DistributionPeriod { return nil } - rewards, err := getRewardsForEpoch(ctx, app, info.currentEpoch.ID) + // There will be always 2 epochs(except the very first epoch): + // - finalized epoch: finalized but not confirmed, wait to be confimed + // - current epoch: collect all new rewards, wait to be finalized + // Thus: + // - The first epoch should always be finalized + // - All other epochs wait for their previous epoch to be confirmed before finalizing and creating a new one. + + // NOTE: last epoch endHeight = curren epoch startHeight + preExists, preConfirmed, err := previousEpochConfirmed(ctx, app, id, info.currentEpoch.StartHeight) if err != nil { return err } - users := make([]string, len(rewards)) - amounts := make([]*big.Int, len(rewards)) + if !preExists || // first epoch should always be finalized + (preExists && preConfirmed) { // previous epoch exists and is confirmed + leafNum, jsonBody, root, total, err := genMerkleTreeForEpoch(ctx, app, info.currentEpoch.ID, info.EscrowAddress.Hex(), block.Hash) + if err != nil { + return err + } - for i, reward := range rewards { - users[i] = reward.Recipient.Hex() - amounts[i] = reward.Amount.BigInt() - } + if leafNum == 0 { + app.Service.Logger.Info("no rewards to distribute, delay finalized current epoch") + return nil + } - _, root, err := reward.GenRewardMerkleTree(users, amounts, info.EscrowAddress.Hex(), block.Hash) - if err != nil { - return err - } + erc20Total, err := erc20ValueFromBigInt(total) + if err != nil { + return err + } - err = finalizeEpoch(ctx, app, info.currentEpoch.ID, block.Height, block.Hash[:], root) - if err != nil { - return err - } + err = finalizeEpoch(ctx, app, info.currentEpoch.ID, block.Height, block.Hash[:], root, erc20Total) + if err != nil { + return err + } - // create a new epoch - newEpoch := newPendingEpoch(id, block) - err = createEpoch(ctx, app, newEpoch, id) - if err != nil { - return err - } + // put in cache + var b32Root [32]byte + copy(b32Root[:], root) + mtLRUCache.Put(b32Root, jsonBody) - newEpochs[*id] = newEpoch + // create a new epoch + newEpoch := newPendingEpoch(id, block) + err = createEpoch(ctx, app, newEpoch, id) + if err != nil { + return err + } + newEpochs[*id] = newEpoch + return nil + } + + // if previous epoch exists and not confirmed, we do nothing. + app.Service.Logger.Info("log previous epoch is not confirmed yet, skip finalize current epoch") return nil }) if err != nil { @@ -1151,6 +1300,39 @@ func init() { } } +func genMerkleTreeForEpoch(ctx context.Context, app *common.App, epochID *types.UUID, + escrowAddr string, blockHash [32]byte) (leafNum int, jsonTree []byte, root []byte, total *big.Int, err error) { + var rewards []*EpochReward + err = getRewardsForEpoch(ctx, app, epochID, func(reward *EpochReward) error { + rewards = append(rewards, reward) + return nil + }) + if err != nil { + return 0, nil, nil, nil, err + } + + if len(rewards) == 0 { // no rewards, delay finalize current epoch + return 0, nil, nil, nil, nil // should skip + } + + users := make([]string, len(rewards)) + amounts := make([]*big.Int, len(rewards)) + total = big.NewInt(0) + + for i, r := range rewards { + users[i] = r.Recipient.Hex() + amounts[i] = r.Amount.BigInt() + total.Add(total, amounts[i]) + } + + jsonTree, root, err = utils.GenRewardMerkleTree(users, amounts, escrowAddr, blockHash) + if err != nil { + return 0, nil, nil, nil, err + } + + return len(rewards), jsonTree, root, total, nil +} + func genesisExec(ctx context.Context, app *common.App) error { // we will create the schema at genesis err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, "USE kwil_erc20_meta AS kwil_erc20_meta", nil, nil) @@ -1192,8 +1374,8 @@ func (e *extensionInfo) lockTokens(ctx context.Context, app *common.App, id *typ return err } - if amount.IsNegative() { - return fmt.Errorf("amount cannot be negative") + if !amount.IsPositive() { + return fmt.Errorf("amount needs to be positive") } // we call getUsableInstance before transfer to ensure that the extension is active and synced. @@ -1221,6 +1403,56 @@ func (e *extensionInfo) lockTokens(ctx context.Context, app *common.App, id *typ return nil } +// issueTokens issues tokens from network's balance. +func (e *extensionInfo) issueTokens(ctx context.Context, app *common.App, id *types.UUID, to string, amount *types.Decimal) error { + if !amount.IsPositive() { + return fmt.Errorf("amount needs to be positive") + } + + // then issue to caller itself + // because this is in one tx, we can be sure that the instance has enough balance to issue. + info, err := e.getUsableInstance(id) + if err != nil { + return err + } + + info.mu.RLock() + // we cannot defer an RUnlock here because we need to unlock + // the read lock before we can acquire the write lock, which + // we do at the end of this + + newBal, err := types.DecimalSub(info.ownedBalance, amount) + if err != nil { + info.mu.RUnlock() + return err + } + + if newBal.IsNegative() { + info.mu.RUnlock() + return fmt.Errorf("network does not enough balance to issue %s to %s", amount, to) + } + + addr, err := ethAddressFromHex(to) + if err != nil { + info.mu.RUnlock() + return err + } + + err = issueReward(ctx, app, id, info.currentEpoch.ID, addr, amount) + if err != nil { + info.mu.RUnlock() + return err + } + + info.mu.RUnlock() + + info.mu.Lock() + info.ownedBalance = newBal + info.mu.Unlock() + + return nil +} + // getUsableInstance gets an instance and ensures it is active and synced. func (e *extensionInfo) getUsableInstance(id *types.UUID) (*rewardExtensionInfo, error) { info, ok := e.instances.Get(*id) @@ -1254,7 +1486,7 @@ func newPendingEpoch(rewardID *types.UUID, block *common.BlockContext) *PendingE return &PendingEpoch{ ID: generateEpochID(rewardID, block.Height), StartHeight: block.Height, - StartTime: time.Unix(block.Timestamp, 0), + StartTime: block.Timestamp, } } @@ -1262,7 +1494,7 @@ func newPendingEpoch(rewardID *types.UUID, block *common.BlockContext) *PendingE type PendingEpoch struct { ID *types.UUID StartHeight int64 - StartTime time.Time + StartTime int64 } // EpochReward is a reward given to a user within an epoch @@ -1276,15 +1508,25 @@ func (p *PendingEpoch) copy() *PendingEpoch { return &PendingEpoch{ ID: &id, StartHeight: p.StartHeight, + StartTime: p.StartTime, } } +type EpochVoteInfo struct { + Voters []ethcommon.Address + VoteSigs [][]byte + VoteNonces []int64 +} + // Epoch is a period in which rewards are distributed. type Epoch struct { PendingEpoch EndHeight *int64 // nil if not finalized - BlockHash []byte // hash of the block that finalized the epoch, nil if not finalized Root []byte // merkle root of all rewards, nil if not finalized + Total *types.Decimal + BlockHash []byte // hash of the block that finalized the epoch, nil if not finalized + Confirmed bool + EpochVoteInfo } type extensionInfo struct { @@ -1335,7 +1577,7 @@ func (e *extensionInfo) ForEachInstance(readOnly bool, fn func(id *types.UUID, i } for _, kv := range order.OrderMap(orderableMap) { - err := fn(kv.Value.userProvidedData.ID, kv.Value) + err = fn(kv.Value.userProvidedData.ID, kv.Value) if err != nil { return } diff --git a/node/exts/erc20reward/meta_extension_test.go b/node/exts/erc20-bridge/erc20/meta_extension_test.go similarity index 99% rename from node/exts/erc20reward/meta_extension_test.go rename to node/exts/erc20-bridge/erc20/meta_extension_test.go index a7a95a09a..4be7bd7e8 100644 --- a/node/exts/erc20reward/meta_extension_test.go +++ b/node/exts/erc20-bridge/erc20/meta_extension_test.go @@ -1,4 +1,4 @@ -package erc20reward +package erc20 import ( "bytes" diff --git a/node/exts/erc20reward/meta_schema.sql b/node/exts/erc20-bridge/erc20/meta_schema.sql similarity index 76% rename from node/exts/erc20reward/meta_schema.sql rename to node/exts/erc20-bridge/erc20/meta_schema.sql index d837d14c5..728d84b22 100644 --- a/node/exts/erc20reward/meta_schema.sql +++ b/node/exts/erc20-bridge/erc20/meta_schema.sql @@ -19,7 +19,8 @@ CREATE TABLE reward_instances ( erc20_address BYTEA, erc20_decimals INT8, synced_at INT8, -- the unix timestamp (in seconds) when the reward was synced - balance NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK(balance >= 0) -- the total balance owned by the database that can be distributed + balance NUMERIC(78, 0) NOT NULL DEFAULT 0 CHECK(balance >= 0), -- the total balance owned by the database that can be distributed + UNIQUE (chain_id, escrow_address) -- unique per chain and escrow ); -- balances tracks the balance of each user in a given reward instance. @@ -39,15 +40,18 @@ CREATE TABLE balances ( -- 3. Confirmed: the epoch has been confirmed on chain -- Ideally, Kwil would have a unique indexes on this table where "confirmed" is null (to enforce only one active epoch at a time), -- but this requires partial indexes which are not yet supported in Kwil +-- We'll always have two active epochs at the same time(except the very first epoch), +-- one is finalized and waiting to be confirmed, the other is collecting new rewards. CREATE TABLE epochs ( id UUID PRIMARY KEY, created_at_block INT8 NOT NULL, -- kwil block height created_at_unix INT8 NOT NULL, -- unix timestamp (in seconds) instance_id UUID NOT NULL REFERENCES reward_instances(id) ON UPDATE RESTRICT ON DELETE RESTRICT, reward_root BYTEA UNIQUE, -- the root of the merkle tree of rewards, it's unique per contract + reward_amount NUMERIC(78, 0), -- the total amount in current epoch ended_at INT8, -- kwil block height block_hash BYTEA, -- the hash of the block that is used in merkle tree leaf, which is the last block of the epoch - confirmed BOOLEAN -- whether the epoch has been confirmed on chain + confirmed BOOLEAN NOT NULL DEFAULT FALSE -- whether the epoch has been confirmed on chain ); -- index helps us query for unconfirmed epochs, which is very common @@ -63,6 +67,18 @@ CREATE TABLE epoch_rewards ( CREATE INDEX idx_epoch_rewards_epoch_id ON epoch_rewards(epoch_id); -CREATE TABLE meta ( +CREATE TABLE meta +( version INT8 PRIMARY KEY ); + +-- epoch_votes holds the votes from signer +-- A signer can vote multiple times with different safe_nonce +-- After an epoch is confirmed, we can delete all related votes. +CREATE TABLE epoch_votes ( + epoch_id UUID NOT NULL REFERENCES epochs(id) ON UPDATE RESTRICT ON DELETE RESTRICT, + voter BYTEA NOT NULL, + nonce INT8 NOT NULL, -- safe nonce; this helps to skip unnecessary dup votes + signature BYTEA NOT NULL, + PRIMARY KEY (epoch_id, voter, nonce) +); \ No newline at end of file diff --git a/node/exts/erc20reward/meta_sql.go b/node/exts/erc20-bridge/erc20/meta_sql.go similarity index 58% rename from node/exts/erc20reward/meta_sql.go rename to node/exts/erc20-bridge/erc20/meta_sql.go index c5dce2d83..1fea7dcae 100644 --- a/node/exts/erc20reward/meta_sql.go +++ b/node/exts/erc20-bridge/erc20/meta_sql.go @@ -1,11 +1,10 @@ -package erc20reward +package erc20 import ( "context" _ "embed" "errors" "fmt" - "time" ethcommon "github.com/ethereum/go-ethereum/common" @@ -54,34 +53,39 @@ func createEpoch(ctx context.Context, app *common.App, epoch *PendingEpoch, inst )`, map[string]any{ "id": epoch.ID, "created_at_block": epoch.StartHeight, - "created_at_unix": epoch.StartTime.Unix(), + "created_at_unix": epoch.StartTime, "instance_id": instanceID, }, nil) } // finalizeEpoch finalizes an epoch. // It sets the end height, block hash, and reward root -func finalizeEpoch(ctx context.Context, app *common.App, epochID *types.UUID, endHeight int64, blockHash []byte, root []byte) error { +func finalizeEpoch(ctx context.Context, app *common.App, epochID *types.UUID, endHeight int64, blockHash []byte, root []byte, total *types.Decimal) error { return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}UPDATE epochs SET ended_at = $ended_at, block_hash = $block_hash, - reward_root = $reward_root + reward_root = $reward_root, + reward_amount = $reward_amount WHERE id = $id `, map[string]any{ - "id": epochID, - "ended_at": endHeight, - "block_hash": blockHash, - "reward_root": root, + "id": epochID, + "ended_at": endHeight, + "block_hash": blockHash, + "reward_root": root, + "reward_amount": total, }, nil) } -// confirmEpoch confirms an epoch was received on-chain +// confirmEpoch confirms an epoch was received on-chain, also delete all the votes +// associated with the epoch. func confirmEpoch(ctx context.Context, app *common.App, root []byte) error { return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}UPDATE epochs SET confirmed = true - WHERE reward_root = $root + WHERE reward_root = $root; + + {kwil_erc20_meta}DELETE FROM epoch_votes where epoch_id=(SELECT id FROM epochs WHERE reward_root = $root); `, map[string]any{ "root": root, }, nil) @@ -104,7 +108,8 @@ func setRewardSynced(ctx context.Context, app *common.App, id *types.UUID, synce }, nil) } -// getStoredRewardInstances gets all stored reward instances. +// getStoredRewardInstances gets all stored reward instances. Also returns the +// current epoch(not finalized) that is being used. func getStoredRewardInstances(ctx context.Context, app *common.App) ([]*rewardExtensionInfo, error) { var rewards []*rewardExtensionInfo err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` @@ -112,7 +117,7 @@ func getStoredRewardInstances(ctx context.Context, app *common.App) ([]*rewardEx r.erc20_address, r.erc20_decimals, r.synced_at, r.balance, e.id AS epoch_id, e.created_at_block AS epoch_created_at_block, e.created_at_unix AS epoch_created_at_seconds FROM reward_instances r - LEFT JOIN epochs e on r.id = e.instance_id AND e.confirmed IS NULL + LEFT JOIN epochs e on r.id = e.instance_id AND e.confirmed IS NOT TRUE AND e.ended_at IS NULL `, nil, func(row *common.Row) error { if len(row.Values) != 13 { return fmt.Errorf("expected 13 values, got %d", len(row.Values)) @@ -154,7 +159,7 @@ func getStoredRewardInstances(ctx context.Context, app *common.App) ([]*rewardEx reward.currentEpoch = &PendingEpoch{ ID: epochID, StartHeight: epochCreatedAtBlock, - StartTime: time.Unix(epochCreatedAtUnix, 0), + StartTime: epochCreatedAtUnix, } if !reward.synced { @@ -193,7 +198,7 @@ func bytesToEthAddress(bts []byte) (ethcommon.Address, error) { // creditBalance credits a balance to a user. // The rewardId is the ID of the reward instance. -// It if is negative, it will subtract. +// If it is negative, it will subtract. func creditBalance(ctx context.Context, app *common.App, rewardId *types.UUID, user ethcommon.Address, amount *types.Decimal) error { balanceId := userBalanceID(rewardId, user) return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` @@ -233,18 +238,20 @@ func createSchema(ctx context.Context, app *common.App) error { } // issueReward issues a reward to a user. -func issueReward(ctx context.Context, app *common.App, epochID *types.UUID, user ethcommon.Address, amount *types.Decimal) error { +func issueReward(ctx context.Context, app *common.App, instanceId *types.UUID, epochID *types.UUID, user ethcommon.Address, amount *types.Decimal) error { return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}UPDATE reward_instances - SET balance = balance - $amount; + SET balance = balance - $amount + WHERE id = $instance_id; {kwil_erc20_meta}INSERT INTO epoch_rewards(epoch_id, recipient, amount) - VALUES ($id, $reward_id, $user, $amount) - ON CONFLICT (id, recipient) DO UPDATE SET amount = epoch_rewards.amount + $amount; + VALUES ($epoch_id, $user, $amount) + ON CONFLICT (epoch_id, recipient) DO UPDATE SET amount = epoch_rewards.amount + $amount; `, map[string]any{ - "id": epochID, - "user": user.Bytes(), - "amount": amount, + "instance_id": instanceId, + "epoch_id": epochID, + "user": user.Bytes(), + "amount": amount, }, nil) } @@ -328,9 +335,8 @@ func balanceOf(ctx context.Context, app *common.App, rewardID *types.UUID, user } // getRewardsForEpoch gets all rewards for an epoch. -func getRewardsForEpoch(ctx context.Context, app *common.App, epochID *types.UUID) ([]*EpochReward, error) { - var rewards []*EpochReward - err := app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` +func getRewardsForEpoch(ctx context.Context, app *common.App, epochID *types.UUID, fn func(reward *EpochReward) error) error { + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` {kwil_erc20_meta}SELECT recipient, amount FROM epoch_rewards WHERE epoch_id = $epoch_id @@ -346,49 +352,172 @@ func getRewardsForEpoch(ctx context.Context, app *common.App, epochID *types.UUI return err } - rewards = append(rewards, &EpochReward{ + return fn(&EpochReward{ Recipient: recipient, Amount: row.Values[1].(*types.Decimal), }) + }) +} + +// previousEpochConfirmed return whether previous exists and confirmed. +func previousEpochConfirmed(ctx context.Context, app *common.App, instanceID *types.UUID, endBlock int64) (exist bool, confirmed bool, err error) { + err = app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` + {kwil_erc20_meta}SELECT confirmed from epochs + WHERE instance_id = $instance_id AND ended_at = $end_block + `, map[string]any{ + "instance_id": instanceID, + "end_block": endBlock, + }, func(r *common.Row) error { + // might be not necessary + if exist { + return fmt.Errorf("internal bug: expected single record") + } + exist = true + + if len(r.Values) != 1 { + return fmt.Errorf("expected 1 values, got %d", len(r.Values)) + } + + confirmed = r.Values[0].(bool) return nil }) - if err != nil { - return nil, err + + return exist, confirmed, err +} + +func rowToEpoch(r *common.Row) (*Epoch, error) { + if len(r.Values) != 11 { + return nil, fmt.Errorf("expected 11 values, got %d", len(r.Values)) } - return rewards, nil + + id := r.Values[0].(*types.UUID) + createdAtBlock := r.Values[1].(int64) + createdAtUnix := r.Values[2].(int64) + + var rewardRoot []byte + if r.Values[3] != nil { + rewardRoot = r.Values[3].([]byte) + } + + var rewardAmount *types.Decimal + if r.Values[4] != nil { + rewardAmount = r.Values[4].(*types.Decimal) + } + + var endedAt int64 + if r.Values[5] != nil { + endedAt = r.Values[5].(int64) + } + + var blockHash []byte + if r.Values[6] != nil { + blockHash = r.Values[6].([]byte) + } + + confirmed := r.Values[7].(bool) + + // NOTE: empty value is [[]] + // values[8]-values[10] will all be empty if any, from the SQL; + var voters []ethcommon.Address + if r.Values[8] != nil { + rawVoters := r.Values[8].([][]byte) + // empty value is [[]], cannot use make(), otherwise we'll have a empty `ethcommon.Address` + for _, rawVoter := range rawVoters { + if len(rawVoter) == 0 { + continue + } + voter, err := bytesToEthAddress(rawVoter) + if err != nil { + return nil, err + } + voters = append(voters, voter) + } + } + + // NOTE: empty value is [] + var voteNonces []int64 + if r.Values[9] != nil { + rawNonces := r.Values[9].([]*int64) + for _, rawNonce := range rawNonces { + if rawNonce != nil { + // NOTE: this is probably problematic, since we can messup the index + // If we don't skip, return -1 ? + voteNonces = append(voteNonces, *rawNonce) + } + } + } + + // NOTE: empty value is [[]] + var signatures [][]byte + if r.Values[10] != nil { + // we skip the empty value, otherwise after conversion, [] will be returned + for _, rawSig := range r.Values[10].([][]byte) { + if len(rawSig) != 0 { + signatures = append(signatures, rawSig) + } + } + } + + return &Epoch{ + PendingEpoch: PendingEpoch{ + ID: id, + StartHeight: createdAtBlock, + StartTime: createdAtUnix, + }, + EndHeight: &endedAt, + BlockHash: blockHash, + Root: rewardRoot, + Total: rewardAmount, + Confirmed: confirmed, + EpochVoteInfo: EpochVoteInfo{ + Voters: voters, + VoteSigs: signatures, + VoteNonces: voteNonces, + }, + }, nil } -// getUnconfirmedEpochs gets all unconfirmed epochs. -func getUnconfirmedEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, fn func(*Epoch) error) error { - return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` - SELECT id, created_at_block, created_at_unix, ended_at, block_hash, reward_root - FROM epochs - WHERE instance_id = $instance_id AND confirmed IS FALSE - ORDER BY ended_at ASC - `, map[string]any{ +// getActiveEpochs get current active epochs, at most two: +// one collects all new rewards, and one waits to be confirmed. +func getActiveEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, fn func(*Epoch) error) error { + query := ` + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures + FROM epochs AS e + LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id + WHERE e.instance_id = $instance_id AND e.confirmed IS NOT true + GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed + ORDER BY e.created_at_block ASC ` + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, map[string]any{ "instance_id": instanceID, }, func(r *common.Row) error { - if len(r.Values) != 6 { - return fmt.Errorf("expected 6 values, got %d", len(r.Values)) + epoch, err := rowToEpoch(r) + if err != nil { + return err } + return fn(epoch) + }) +} - id := r.Values[0].(*types.UUID) - createdAtBlock := r.Values[1].(int64) - createdAtUnix := r.Values[2].(int64) - endedAt := r.Values[3].(int64) - blockHash := r.Values[4].([]byte) - rewardRoot := r.Values[5].([]byte) - - return fn(&Epoch{ - PendingEpoch: PendingEpoch{ - ID: id, - StartHeight: createdAtBlock, - StartTime: time.Unix(createdAtUnix, 0), - }, - EndHeight: &endedAt, - BlockHash: blockHash, - Root: rewardRoot, - }) +// getEpochs gets epochs. +func getEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, after int64, limit int64, fn func(*Epoch) error) error { + query := ` + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, array_agg(v.voter) as voters, array_agg(v.nonce) as nonces, array_agg(v.signature) as signatures + FROM epochs AS e + LEFT JOIN epoch_votes AS v ON v.epoch_id = e.id + WHERE e.instance_id = $instance_id AND e.created_at_block > $after + GROUP BY e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed + ORDER BY e.ended_at ASC LIMIT $limit` + + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, map[string]any{ + "instance_id": instanceID, + "after": after, + "limit": limit, + }, func(r *common.Row) error { + epoch, err := rowToEpoch(r) + if err != nil { + return err + } + return fn(epoch) }) } @@ -435,3 +564,70 @@ func setVersionToCurrent(ctx context.Context, app *common.App) error { "version": currentVersion, }, nil) } + +// canVoteEpoch returns a bool indicate whether an epoch can be voted. +func canVoteEpoch(ctx context.Context, app *common.App, epochID *types.UUID) (ok bool, err error) { + // get epoch that is finalized, but not confirmed. + err = app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` + {kwil_erc20_meta}SELECT confirmed + FROM epochs WHERE id = $id AND ended_at IS NOT NULL AND confirmed IS NOT true; + `, map[string]any{ + "id": epochID, + }, func(row *common.Row) error { + if len(row.Values) != 1 { + return fmt.Errorf("expected 1 value, got %d", len(row.Values)) + } + + ok = true + return nil + }) + + if err != nil { + return false, err + } + + return ok, nil +} + +// voteEpoch vote an epoch by submitting signature. +func voteEpoch(ctx context.Context, app *common.App, epochID *types.UUID, + voter ethcommon.Address, nonce int64, signature []byte) error { + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, ` + {kwil_erc20_meta}INSERT into epoch_votes(epoch_id, voter, nonce, signature) + VALUES ($epoch_id, $voter, $nonce, $signature); + `, map[string]any{ + "epoch_id": epochID, + "voter": voter.Bytes(), + "signature": signature, + "nonce": nonce, + }, nil) +} + +// getWalletEpochs returns all confirmed epochs that the given wallet has reward in. +// If pending=true, return all finalized epochs(no necessary confirmed). +func getWalletEpochs(ctx context.Context, app *common.App, instanceID *types.UUID, + wallet ethcommon.Address, pending bool, fn func(*Epoch) error) error { + + // WE don't need vote info, we just return empty arrays instead of JOIN + query := ` + {kwil_erc20_meta}SELECT e.id, e.created_at_block, e.created_at_unix, e.reward_root, e.reward_amount, e.ended_at, e.block_hash, e.confirmed, ARRAY[]::BYTEA[] as voters, ARRAY[]::INT8[] as nonces, ARRAY[]::BYTEA[] as signatures + FROM epoch_rewards AS r + JOIN epochs AS e ON r.epoch_id = e.id + WHERE recipient = $wallet AND e.instance_id = $instance_id AND e.ended_at IS NOT NULL` // at least finalized + if !pending { + query += ` AND e.confirmed IS true` + } + + query += ";" + return app.Engine.ExecuteWithoutEngineCtx(ctx, app.DB, query, + map[string]any{ + "wallet": wallet.Bytes(), + "instance_id": instanceID, + }, func(r *common.Row) error { + epoch, err := rowToEpoch(r) + if err != nil { + return err + } + return fn(epoch) + }) +} diff --git a/node/exts/erc20reward/meta_sql_test.go b/node/exts/erc20-bridge/erc20/meta_sql_test.go similarity index 97% rename from node/exts/erc20reward/meta_sql_test.go rename to node/exts/erc20-bridge/erc20/meta_sql_test.go index c247f511d..287e14281 100644 --- a/node/exts/erc20reward/meta_sql_test.go +++ b/node/exts/erc20-bridge/erc20/meta_sql_test.go @@ -1,9 +1,9 @@ -package erc20reward +package erc20 import ( "context" + "math/big" "testing" - "time" ethcommon "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" @@ -98,7 +98,7 @@ func TestCreateNewRewardInstance(t *testing.T) { pending := &PendingEpoch{ ID: newUUID(), StartHeight: 10, - StartTime: time.Unix(100, 0), + StartTime: 100, } err = createEpoch(ctx, app, pending, id) require.NoError(t, err) @@ -136,8 +136,9 @@ func TestCreateNewRewardInstance(t *testing.T) { require.Equal(t, int64(18), rewards[0].syncedRewardData.Erc20Decimals) root := []byte{0x03, 0x04} + amt, _ := erc20ValueFromBigInt(big.NewInt(100)) // finalize the epoch - err = finalizeEpoch(ctx, app, pending.ID, 20, []byte{0x01, 0x02}, root) + err = finalizeEpoch(ctx, app, pending.ID, 20, []byte{0x01, 0x02}, root, amt) require.NoError(t, err) // confirm the epoch diff --git a/node/exts/erc20reward/named_extension.go b/node/exts/erc20-bridge/erc20/named_extension.go similarity index 62% rename from node/exts/erc20reward/named_extension.go rename to node/exts/erc20-bridge/erc20/named_extension.go index e4630114f..936feb59c 100644 --- a/node/exts/erc20reward/named_extension.go +++ b/node/exts/erc20-bridge/erc20/named_extension.go @@ -1,4 +1,4 @@ -package erc20reward +package erc20 import ( "context" @@ -41,7 +41,7 @@ func init() { var distributionPeriod string distributionPeriodAny, ok := metadata["distribution_period"] if !ok { - distributionPeriod = "24h" + distributionPeriod = "24h" // 'h' is the highest units supported } else { distributionPeriod, ok = distributionPeriodAny.(string) if !ok { @@ -89,7 +89,7 @@ func init() { {Name: "epoch_period", Type: types.TextType}, {Name: "erc20", Type: types.TextType, Nullable: true}, {Name: "decimals", Type: types.IntType, Nullable: true}, - {Name: "balance", Type: types.TextType, Nullable: true}, // total unspent balance + {Name: "balance", Type: uint256Numeric, Nullable: true}, // total unspent balance {Name: "synced", Type: types.BoolType}, {Name: "synced_at", Type: types.IntType, Nullable: true}, {Name: "enabled", Type: types.BoolType}, @@ -114,7 +114,7 @@ func init() { Name: "issue", Parameters: []precompiles.PrecompileValue{ {Name: "user", Type: types.TextType}, - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, AccessModifiers: []precompiles.Modifier{precompiles.SYSTEM}, Handler: makeMetaHandler("issue"), @@ -123,7 +123,7 @@ func init() { Name: "transfer", Parameters: []precompiles.PrecompileValue{ {Name: "to", Type: types.TextType}, - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, // anybody can call this as long as they have the tokens. // There is no security risk if somebody calls this directly @@ -133,7 +133,7 @@ func init() { { Name: "lock", Parameters: []precompiles.PrecompileValue{ - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, Handler: makeMetaHandler("lock"), @@ -142,7 +142,7 @@ func init() { Name: "lock_admin", Parameters: []precompiles.PrecompileValue{ {Name: "user", Type: types.TextType}, - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, AccessModifiers: []precompiles.Modifier{precompiles.SYSTEM}, Handler: makeMetaHandler("lock_admin"), @@ -151,7 +151,7 @@ func init() { Name: "unlock", Parameters: []precompiles.PrecompileValue{ {Name: "user", Type: types.TextType}, - {Name: "amount", Type: types.TextType}, + {Name: "amount", Type: uint256Numeric}, }, AccessModifiers: []precompiles.Modifier{precompiles.SYSTEM}, Handler: makeMetaHandler("unlock"), @@ -164,27 +164,19 @@ func init() { }, Returns: &precompiles.MethodReturn{ Fields: []precompiles.PrecompileValue{ - {Name: "balance", Type: types.TextType}, + {Name: "balance", Type: uint256Numeric}, }, }, AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, Handler: makeMetaHandler("balance"), }, { - Name: "list_unconfirmed_epochs", - Returns: &precompiles.MethodReturn{ - IsTable: true, - Fields: []precompiles.PrecompileValue{ - {Name: "epoch_id", Type: types.UUIDType}, - {Name: "start_height", Type: types.IntType}, - {Name: "start_timestamp", Type: types.IntType}, - {Name: "end_height", Type: types.IntType}, - {Name: "reward_root", Type: types.ByteaType}, - {Name: "end_block_hash", Type: types.ByteaType}, - }, + Name: "bridge", + Parameters: []precompiles.PrecompileValue{ + {Name: "amount", Type: uint256Numeric}, }, - AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, - Handler: makeMetaHandler("list_unconfirmed_epochs"), + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, + Handler: makeMetaHandler("bridge"), }, { Name: "decimals", @@ -199,7 +191,7 @@ func init() { { Name: "scale_down", Parameters: []precompiles.PrecompileValue{ - {Name: "amount", Type: uint256Numeric}, + {Name: "amount", Type: types.TextType}, }, Returns: &precompiles.MethodReturn{ Fields: []precompiles.PrecompileValue{ @@ -222,6 +214,100 @@ func init() { AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, Handler: makeMetaHandler("scale_up"), }, + { + Name: "get_active_epochs", + Parameters: []precompiles.PrecompileValue{}, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "start_height", Type: types.IntType}, + {Name: "start_timestamp", Type: types.IntType}, + {Name: "end_height", Type: types.IntType, Nullable: true}, + {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "reward_amount", Type: uint256Numeric, Nullable: true}, + {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "confirmed", Type: types.BoolType}, + {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, + {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: makeMetaHandler("get_active_epochs"), + }, + { + Name: "list_epochs", + Parameters: []precompiles.PrecompileValue{ + {Name: "after", Type: types.IntType}, + {Name: "limit", Type: types.IntType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "id", Type: types.UUIDType}, + {Name: "start_height", Type: types.IntType}, + {Name: "start_timestamp", Type: types.IntType}, + {Name: "end_height", Type: types.IntType, Nullable: true}, + {Name: "reward_root", Type: types.ByteaType, Nullable: true}, + {Name: "reward_amount", Type: uint256Numeric, Nullable: true}, + {Name: "end_block_hash", Type: types.ByteaType, Nullable: true}, + {Name: "confirmed", Type: types.BoolType}, + {Name: "voters", Type: types.TextArrayType, Nullable: true}, + {Name: "vote_nonces", Type: types.IntArrayType, Nullable: true}, + {Name: "voter_signatures", Type: types.ByteaArrayType, Nullable: true}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: makeMetaHandler("list_epochs"), + }, + { + Name: "get_epoch_rewards", + Parameters: []precompiles.PrecompileValue{ + {Name: "epoch_id", Type: types.UUIDType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "recipient", Type: types.TextType}, + {Name: "amount", Type: types.TextType}, + }, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: makeMetaHandler("get_epoch_rewards"), + }, + { + Name: "vote_epoch", + Parameters: []precompiles.PrecompileValue{ + {Name: "epoch_id", Type: types.UUIDType}, + {Name: "nonce", Type: types.IntType}, + {Name: "signature", Type: types.ByteaType}, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC}, + Handler: makeMetaHandler("vote_epoch"), + }, + { + Name: "list_wallet_rewards", + Parameters: []precompiles.PrecompileValue{ + {Name: "wallet", Type: types.TextType}, // wallet address + {Name: "with_pending", Type: types.BoolType}, + }, + Returns: &precompiles.MethodReturn{ + IsTable: true, + Fields: []precompiles.PrecompileValue{ + {Name: "chain", Type: types.TextType}, + {Name: "chain_id", Type: types.TextType}, + {Name: "contract", Type: types.TextType}, + {Name: "created_at", Type: types.IntType}, + {Name: "param_recipient", Type: types.TextType}, + {Name: "param_amount", Type: uint256Numeric}, + {Name: "param_block_hash", Type: types.ByteaType}, + {Name: "param_root", Type: types.ByteaType}, + {Name: "param_proofs", Type: types.ByteaArrayType}}, + }, + AccessModifiers: []precompiles.Modifier{precompiles.PUBLIC, precompiles.VIEW}, + Handler: makeMetaHandler("list_wallet_rewards"), + }, }, }, nil }) diff --git a/node/exts/erc20-bridge/signersvc/.gitignore b/node/exts/erc20-bridge/signersvc/.gitignore new file mode 100644 index 000000000..2eea525d8 --- /dev/null +++ b/node/exts/erc20-bridge/signersvc/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/node/exts/erc20-bridge/signersvc/eth.go b/node/exts/erc20-bridge/signersvc/eth.go new file mode 100644 index 000000000..136188914 --- /dev/null +++ b/node/exts/erc20-bridge/signersvc/eth.go @@ -0,0 +1,219 @@ +package signersvc + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/abigen" + "github.com/samber/lo" +) + +var ( + safeABI = lo.Must(abi.JSON(strings.NewReader(abigen.SafeMetaData.ABI))) + + nonceCallData = lo.Must(safeABI.Pack("nonce")) // nonce() + thresholdCallData = lo.Must(safeABI.Pack("getThreshold")) // getThreshold() + ownersCallData = lo.Must(safeABI.Pack("getOwners")) // getOwners() +) + +type safeMetadata struct { + threshold *big.Int + owners []common.Address + nonce *big.Int +} + +type Safe struct { + chainID *big.Int + addr common.Address + + safe *abigen.Safe + safeABI *abi.ABI + eth *ethclient.Client +} + +func NewSafeFromEscrow(rpc string, escrowAddr string) (*Safe, error) { + client, err := ethclient.Dial(rpc) + if err != nil { + return nil, fmt.Errorf("create eth cliet: %w", err) + } + + chainID, err := client.ChainID(context.Background()) + if err != nil { + return nil, fmt.Errorf("create eth chainID: %w", err) + } + + rd, err := abigen.NewRewardDistributor(common.HexToAddress(escrowAddr), client) + if err != nil { + return nil, fmt.Errorf("create reward distributor: %w", err) + } + + safeAddr, err := rd.Safe(nil) + if err != nil { + return nil, fmt.Errorf("get safe address: %w", err) + } + + safe, err := abigen.NewSafe(safeAddr, client) + if err != nil { + return nil, fmt.Errorf("create safe: %w", err) + } + + return &Safe{ + chainID: chainID, + addr: safeAddr, + safe: safe, + safeABI: &safeABI, + eth: client, + }, nil +} + +func NewSafe(rpc string, addr string) (*Safe, error) { + client, err := ethclient.Dial(rpc) + if err != nil { + return nil, fmt.Errorf("create eth cliet: %w", err) + } + + chainID, err := client.ChainID(context.Background()) + if err != nil { + return nil, fmt.Errorf("create eth chainID: %w", err) + } + + safe, err := abigen.NewSafe(common.HexToAddress(addr), client) + if err != nil { + return nil, fmt.Errorf("create safe: %w", err) + } + + return &Safe{ + chainID: chainID, + addr: common.HexToAddress(addr), + safe: safe, + safeABI: &safeABI, + eth: client, + }, nil +} + +// height retrieves current block height. +func (s *Safe) height(ctx context.Context) (uint64, error) { + return s.eth.BlockNumber(ctx) +} + +// nonce retrieves the nonce of the Safe contract at a specified block number. +func (s *Safe) nonce(ctx context.Context, blockNumber *big.Int) (*big.Int, error) { + callOpts := &bind.CallOpts{ + Pending: false, + BlockNumber: blockNumber, + Context: ctx, + } + return s.safe.Nonce(callOpts) +} + +// threshold retrieves the threshold value of the Safe contract at a specified block number. +func (s *Safe) threshold(ctx context.Context, blockNumber *big.Int) (*big.Int, error) { + callOpts := &bind.CallOpts{ + Pending: false, + BlockNumber: blockNumber, + Context: ctx, + } + return s.safe.GetThreshold(callOpts) +} + +// owners retrieves the list of owner addresses of the Safe contract at a specified block number. +func (s *Safe) owners(ctx context.Context, blockNumber *big.Int) ([]common.Address, error) { + callOpts := &bind.CallOpts{ + Pending: false, + BlockNumber: blockNumber, + Context: ctx, + } + return s.safe.GetOwners(callOpts) +} + +func (s *Safe) latestMetadata(ctx context.Context) (*safeMetadata, error) { + height, err := s.height(ctx) + if err != nil { + return nil, err + } + + return s.metadata(ctx, new(big.Int).SetUint64(height)) +} + +func (s *Safe) metadata(ctx context.Context, blockNumber *big.Int) (*safeMetadata, error) { + if IsMulticall3Deployed(s.chainID.Uint64(), blockNumber) { + return s.getSafeMetadata3(ctx, blockNumber) + } + + return s.getSafeMetadataSeq(ctx, blockNumber) +} + +// getSafeMetadataSeq retrieves safe wallet metadata in sequence +func (s *Safe) getSafeMetadataSeq(ctx context.Context, blockNumber *big.Int) (*safeMetadata, error) { + nonce, err := s.nonce(ctx, blockNumber) + if err != nil { + return nil, err + } + + threshold, err := s.threshold(ctx, blockNumber) + if err != nil { + return nil, err + } + + owners, err := s.owners(ctx, blockNumber) + if err != nil { + return nil, err + } + + return &safeMetadata{ + threshold: threshold, + owners: owners, + nonce: nonce, + }, nil +} + +// getSafeMetadata3 retrieves safe wallet metadata in one go, using multicall3 +func (s *Safe) getSafeMetadata3(ctx context.Context, blockNumber *big.Int) (*safeMetadata, error) { + res, err := Aggregate3(ctx, s.chainID.Uint64(), []abigen.Multicall3Call3{ + { + Target: s.addr, + AllowFailure: false, + CallData: nonceCallData, + }, + { + Target: s.addr, + AllowFailure: false, + CallData: thresholdCallData, + }, + { + Target: s.addr, + AllowFailure: false, + CallData: ownersCallData, + }, + }, blockNumber, s.eth) + if err != nil { + return nil, err + } + + nonce, err := safeABI.Unpack("nonce", res[0].ReturnData) + if err != nil { + return nil, err + } + + threshold, err := safeABI.Unpack("getThreshold", res[1].ReturnData) + if err != nil { + return nil, err + } + + owners, err := safeABI.Unpack("getOwners", res[2].ReturnData) + if err != nil { + return nil, err + } + + return &safeMetadata{ + nonce: nonce[0].(*big.Int), + threshold: threshold[0].(*big.Int), + owners: owners[0].([]common.Address), + }, nil +} diff --git a/node/exts/erc20-bridge/signersvc/eth_test.go b/node/exts/erc20-bridge/signersvc/eth_test.go new file mode 100644 index 000000000..d9c2e7f28 --- /dev/null +++ b/node/exts/erc20-bridge/signersvc/eth_test.go @@ -0,0 +1,34 @@ +package signersvc + +import ( + "context" + "flag" + "math/big" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +var ethRpc = flag.String("eth-rpc", os.Getenv("ETH_RPC"), "eth provider rpc") + +func TestSafe_metadata(t *testing.T) { + if *ethRpc == "" { + t.Skip("no eth rpc configured") + } + + blockNumber := new(big.Int).SetUint64(7660784) + + s, err := NewSafe(*ethRpc, "0x56D510E4782cDed87F8B93D260282776adEd3f4B") + require.NoError(t, err) + + ctx := context.Background() + + got, err := s.getSafeMetadata3(ctx, blockNumber) + require.NoError(t, err) + + got2, err := s.getSafeMetadataSeq(ctx, blockNumber) + require.NoError(t, err) + + require.EqualValues(t, got, got2) +} diff --git a/node/exts/erc20-bridge/signersvc/kwil.go b/node/exts/erc20-bridge/signersvc/kwil.go new file mode 100644 index 000000000..eac43f306 --- /dev/null +++ b/node/exts/erc20-bridge/signersvc/kwil.go @@ -0,0 +1,381 @@ +package signersvc + +import ( + "context" + "fmt" + "math/big" + + "github.com/samber/lo" + + "github.com/kwilteam/kwil-db/common" + "github.com/kwilteam/kwil-db/core/client" + "github.com/kwilteam/kwil-db/core/crypto/auth" + rpcclient "github.com/kwilteam/kwil-db/core/rpc/client" + "github.com/kwilteam/kwil-db/core/types" + "github.com/kwilteam/kwil-db/node/types/sql" +) + +type RewardInstanceInfo struct { + Chain string + Escrow string + EpochPeriod string + Erc20 string + Decimals int64 + Balance *types.Decimal + Synced bool + SyncedAt int64 + Enabled bool +} + +type Epoch struct { + ID *types.UUID + StartHeight int64 + StartTimestamp int64 + EndHeight int64 + RewardRoot []byte + RewardAmount *types.Decimal + EndBlockHash []byte + Confirmed bool + Voters []string + VoteNonces []int64 + VoteSignatures [][]byte +} + +type EpochReward struct { + Recipient string + Amount string +} + +// bridgeSignerClient defines the ERC20 bridge extension client for signer service. +type bridgeSignerClient interface { + InstanceInfo(tx context.Context, namespace string) (*RewardInstanceInfo, error) + GetActiveEpochs(ctx context.Context, namespace string) ([]*Epoch, error) + GetEpochRewards(ctx context.Context, namespace string, epochID *types.UUID) ([]*EpochReward, error) + VoteEpoch(ctx context.Context, namespace string, txSigner auth.Signer, epochID *types.UUID, safeNonce int64, signature []byte) (types.Hash, error) +} + +type txBcast interface { + BroadcastTx(ctx context.Context, tx *types.Transaction, sync uint8) (*types.ResultBroadcastTx, error) +} + +type engineCall interface { + CallWithoutEngineCtx(ctx context.Context, db sql.DB, namespace, action string, args []any, resultFn func(*common.Row) error) (*common.CallResult, error) +} + +type nodeApp interface { + AccountInfo(ctx context.Context, db sql.DB, account *types.AccountID, pending bool) (balance *big.Int, nonce int64, err error) + Price(ctx context.Context, dbTx sql.DB, tx *types.Transaction) (*big.Int, error) +} + +type DB interface { + sql.ReadTxMaker + sql.DelayedReadTxMaker +} + +type signerClient struct { + chainID string + db DB + call engineCall + bcast txBcast + kwilNode nodeApp +} + +func NewSignerClient(chainID string, db DB, call engineCall, bcast txBcast, nodeApp nodeApp) *signerClient { + return &signerClient{ + chainID: chainID, + db: db, + call: call, + bcast: bcast, + kwilNode: nodeApp, + } +} + +func (k *signerClient) InstanceInfo(ctx context.Context, namespace string) (*RewardInstanceInfo, error) { + info := &RewardInstanceInfo{} + + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) + + _, err := k.call.CallWithoutEngineCtx(ctx, readTx, namespace, "info", []any{}, func(row *common.Row) error { + var ok bool + + info.Chain, ok = row.Values[0].(string) + if !ok { + return fmt.Errorf("failed to get chain") + } + + info.Escrow, ok = row.Values[1].(string) + if !ok { + return fmt.Errorf("failed to get escrow") + } + + info.EpochPeriod, ok = row.Values[2].(string) + if !ok { + return fmt.Errorf("failed to get epoch period") + } + + info.Erc20, ok = row.Values[3].(string) + if !ok { + return fmt.Errorf("failed to get erc20") + } + + info.Decimals, ok = row.Values[4].(int64) + if !ok { + return fmt.Errorf("failed to get decimals") + } + + info.Balance, ok = row.Values[5].(*types.Decimal) + if !ok { + return fmt.Errorf("failed to get balance") + } + + info.Synced, ok = row.Values[6].(bool) + if !ok { + return fmt.Errorf("failed to get synced") + } + + info.SyncedAt, ok = row.Values[7].(int64) + if !ok { + return fmt.Errorf("failed to get synced at") + } + + info.Enabled, ok = row.Values[8].(bool) + if !ok { + return fmt.Errorf("failed to get enabled") + } + + return nil + }) + + return info, err +} + +func (k *signerClient) GetActiveEpochs(ctx context.Context, namespace string) ([]*Epoch, error) { + var epochs []*Epoch + + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) + + _, err := k.call.CallWithoutEngineCtx(ctx, readTx, namespace, "get_active_epochs", []any{}, func(row *common.Row) error { + var ok bool + e := &Epoch{} + + e.ID, ok = row.Values[0].(*types.UUID) + if !ok { + return fmt.Errorf("failed to get id") + } + e.StartHeight, ok = row.Values[1].(int64) + if !ok { + return fmt.Errorf("failed to get start height") + } + e.StartTimestamp, ok = row.Values[2].(int64) + if !ok { + return fmt.Errorf("failed to get start timestamp") + } + + if row.Values[3] != nil { + e.EndHeight, ok = row.Values[3].(int64) + if !ok { + return fmt.Errorf("failed to get end height") + } + } + + if row.Values[4] != nil { + e.RewardRoot, ok = row.Values[4].([]byte) + if !ok { + return fmt.Errorf("failed to get reward root") + } + } + + if row.Values[5] != nil { + e.RewardAmount, ok = row.Values[5].(*types.Decimal) + if !ok { + return fmt.Errorf("failed to get reward amount") + } + } + + if row.Values[6] != nil { + e.EndBlockHash, ok = row.Values[6].([]byte) + if !ok { + return fmt.Errorf("failed to get end block hash") + } + } + + e.Confirmed, ok = row.Values[7].(bool) + if !ok { + return fmt.Errorf("failed to get confirmed") + } + + if row.Values[8] != nil { + voters, ok := row.Values[8].([]*string) + if !ok { + return fmt.Errorf("failed to get voters") + } + + e.Voters = lo.Map(voters, func(v *string, i int) string { + if v == nil { + return "" + } + return *v + + }) + } + + if row.Values[9] != nil { + voteNonces, ok := row.Values[9].([]*int64) + if !ok { + return fmt.Errorf("failed to get vote nonces") + } + + e.VoteNonces = lo.Map(voteNonces, func(v *int64, i int) int64 { + if v == nil { + return 0 + } + return *v + }) + + } + + if row.Values[10] != nil { + e.VoteSignatures, ok = row.Values[10].([][]byte) + if !ok { + return fmt.Errorf("failed to get vote signatures") + } + } + + epochs = append(epochs, e) + return nil + }) + + return epochs, err +} + +func (k *signerClient) GetEpochRewards(ctx context.Context, namespace string, epochID *types.UUID) ([]*EpochReward, error) { + var rewards []*EpochReward + + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) + + _, err := k.call.CallWithoutEngineCtx(ctx, readTx, namespace, "get_epoch_rewards", []any{epochID}, func(row *common.Row) error { + var ok bool + e := &EpochReward{} + + e.Recipient, ok = row.Values[0].(string) + if !ok { + return fmt.Errorf("failed to get recipient") + } + + e.Amount, ok = row.Values[1].(string) + if !ok { + return fmt.Errorf("failed to get amount") + } + + rewards = append(rewards, e) + + return nil + }) + + return rewards, err +} + +func (k *signerClient) VoteEpoch(ctx context.Context, namespace string, txSigner auth.Signer, epochID *types.UUID, safeNonce int64, signature []byte) (types.Hash, error) { + inputs := [][]any{{epochID, safeNonce, signature}} + res, err := k.execute(ctx, namespace, txSigner, "vote_epoch", inputs) + if err != nil { + return types.Hash{}, err + } + + return res, nil +} + +func (k *signerClient) estimatePrice(ctx context.Context, tx *types.Transaction) (*big.Int, error) { + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) + + return k.kwilNode.Price(ctx, readTx, tx) + +} + +func (k *signerClient) accountNonce(ctx context.Context, acc *types.AccountID) (uint64, error) { + readTx := k.db.BeginDelayedReadTx() + defer readTx.Rollback(ctx) + + _, nonce, err := k.kwilNode.AccountInfo(ctx, readTx, acc, true) + if err != nil { + return 0, fmt.Errorf("failed to get account info: %w", err) + } + + return uint64(nonce), nil +} + +// execute mimics client.Client.Execute, without client options. +func (k *signerClient) execute(ctx context.Context, namespace string, txSigner auth.Signer, action string, tuples [][]any) (types.Hash, error) { + encodedTuples := make([][]*types.EncodedValue, len(tuples)) + for i, tuple := range tuples { + encoded, err := client.EncodeInputs(tuple) + if err != nil { + return types.Hash{}, err + } + encodedTuples[i] = encoded + } + + executionBody := &types.ActionExecution{ + Action: action, + Namespace: namespace, + Arguments: encodedTuples, + } + + tx, err := k.newTx(ctx, executionBody, txSigner) + if err != nil { + return types.Hash{}, fmt.Errorf("failed to create tx: %w", err) + } + + res, err := k.bcast.BroadcastTx(ctx, tx, uint8(rpcclient.BroadcastWaitCommit)) + if err != nil { + return types.Hash{}, fmt.Errorf("failed to broadcast tx: %w", err) + } + + return res.Hash, nil +} + +// newTx mimics client.Client.newTx to create a new tx, without tx options. +func (k *signerClient) newTx(ctx context.Context, data types.Payload, txSigner auth.Signer) (*types.Transaction, error) { + // Get the latest nonce for the account, if it exists. + ident, err := types.GetSignerAccount(txSigner) + if err != nil { + return nil, fmt.Errorf("failed to get signer account: %w", err) + } + + nonce, err := k.accountNonce(ctx, ident) + if err != nil { + return nil, fmt.Errorf("failed to get account nonce: %w", err) + } + + // whether account gets balance or not + nonce += 1 + + // build transaction + tx, err := types.CreateTransaction(data, k.chainID, nonce) + if err != nil { + return nil, fmt.Errorf("failed to create transaction: %w", err) + } + + // estimate price + price, err := k.estimatePrice(ctx, tx) + if err != nil { + return nil, fmt.Errorf("failed to estimate price: %w", err) + } + + // set fee + tx.Body.Fee = price + + // sign transaction + err = tx.Sign(txSigner) + if err != nil { + return nil, fmt.Errorf("failed to sign transaction: %w", err) + } + + return tx, nil +} + +var _ bridgeSignerClient = (*signerClient)(nil) diff --git a/node/exts/erc20-bridge/signersvc/multicall.go b/node/exts/erc20-bridge/signersvc/multicall.go new file mode 100644 index 000000000..a4ebe2365 --- /dev/null +++ b/node/exts/erc20-bridge/signersvc/multicall.go @@ -0,0 +1,93 @@ +package signersvc + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/abigen" +) + +// Multicall https://github.com/mds1/multicall +// https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11 + +const ( + EthNetworkMainnet = 1 + EthNetworkSepolia = 11155111 +) + +var ( + AddressMulticall3 = common.HexToAddress("0xcA11bde05977b3631167028862bE2a173976CA11") +) + +var deployedAtMap = map[uint64]uint64{ + EthNetworkMainnet: 14353601, // https://etherscan.io/tx/0x00d9fcb7848f6f6b0aae4fb709c133d69262b902156c85a473ef23faa60760bd + EthNetworkSepolia: 751532, // https://sepolia.etherscan.io/tx/0x6313b2cee1ddd9a77a8a1edf93495a9eb3c51a4d85479f4f8fec0090ad82596b +} + +func IsMulticall3Deployed(chainID uint64, blockNumber *big.Int) bool { + deployedAt, exists := deployedAtMap[chainID] + if !exists { + return false + } + + if blockNumber == nil { + return true + } + + return deployedAt < blockNumber.Uint64() +} + +// Aggregate3 aggregates multicall result. +// based on https://github.com/RSS3-Network/Node/blob/947b387f11857144c48250dd95804b5069731153/provider/ethereum/contract/multicall3/contract.go +func Aggregate3(ctx context.Context, chainID uint64, calls []abigen.Multicall3Call3, + blockNumber *big.Int, contractBackend bind.ContractCaller) ([]*abigen.Multicall3Result, error) { + if !IsMulticall3Deployed(chainID, blockNumber) { + return nil, fmt.Errorf("multicall3 is not deployed on chainID %d yet", chainID) + } + + abi, err := abigen.Multicall3MetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("load abi: %w", err) + } + + callData, err := abi.Pack("aggregate3", calls) + if err != nil { + return nil, fmt.Errorf("pack data: %w", err) + } + + message := ethereum.CallMsg{ + To: &AddressMulticall3, + Data: callData, + } + + results := make([]abigen.Multicall3Result, 0, len(calls)) + + data, err := contractBackend.CallContract(ctx, message, blockNumber) + if err != nil { + return nil, fmt.Errorf("call contract: %w", err) + } + + if len(data) == 0 { + return nil, fmt.Errorf("data in empty") + } + + if err := abi.UnpackIntoInterface(&results, "aggregate3", data); err != nil { + return nil, fmt.Errorf("unpack result: %w", err) + } + + return ToSlicePtr(results), nil +} + +func ToSlicePtr[T any](collection []T) []*T { + result := make([]*T, len(collection)) + + for i := range collection { + result[i] = &collection[i] + } + return result +} diff --git a/node/exts/erc20-bridge/signersvc/signer.go b/node/exts/erc20-bridge/signersvc/signer.go new file mode 100644 index 000000000..7fe88d173 --- /dev/null +++ b/node/exts/erc20-bridge/signersvc/signer.go @@ -0,0 +1,388 @@ +// Package signersvc implements the SignerSvc of the Kwil reward system. +// It simply fetches the new Epoch from Kwil network and verify&sign it, then +// upload the signature back to the Kwil network. Each bridgeSigner targets one registered +// erc20 Reward instance. +package signersvc + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "fmt" + "math/big" + "os" + "path/filepath" + "slices" + "strings" + "sync" + "time" + + ethCommon "github.com/ethereum/go-ethereum/common" + ethCrypto "github.com/ethereum/go-ethereum/crypto" + + "github.com/kwilteam/kwil-db/config" + "github.com/kwilteam/kwil-db/core/crypto" + "github.com/kwilteam/kwil-db/core/crypto/auth" + "github.com/kwilteam/kwil-db/core/log" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/utils" + "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" +) + +// StateFilePath returns the state file. +func StateFilePath(dir string) string { + return filepath.Join(dir, "erc20_bridge_vote.json") +} + +// bridgeSigner handles the voting on one registered erc20 reward instance. +type bridgeSigner struct { + target string + escrowAddr ethCommon.Address + + kwil bridgeSignerClient + txSigner auth.Signer + signerPk *ecdsa.PrivateKey + signerAddr ethCommon.Address + safe *Safe + + logger log.Logger + state *State +} + +func newBridgeSigner(kwil bridgeSignerClient, safe *Safe, target string, txSigner auth.Signer, + signerPk *ecdsa.PrivateKey, signerAddr ethCommon.Address, escrowAddr ethCommon.Address, + state *State, logger log.Logger) (*bridgeSigner, error) { + if logger == nil { + logger = log.DiscardLogger + } + + return &bridgeSigner{ + kwil: kwil, + txSigner: txSigner, + signerPk: signerPk, + signerAddr: signerAddr, + state: state, + logger: logger, + target: target, + safe: safe, + escrowAddr: escrowAddr, + }, nil +} + +// canSkip returns true if: +// - signer is not one of the safe owners +// - signer has voted this epoch with the same nonce as current safe nonce +func (s *bridgeSigner) canSkip(epoch *Epoch, safeMeta *safeMetadata) bool { + if !slices.Contains(safeMeta.owners, s.signerAddr) { + s.logger.Warn("signer is not safe owner", "signer", s.signerAddr.String(), "owners", safeMeta.owners) + return true + } + + if epoch.Voters == nil { + return false + } + + for i, voter := range epoch.Voters { + if voter == s.signerAddr.String() && + safeMeta.nonce.Cmp(big.NewInt(epoch.VoteNonces[i])) == 0 { + return true + } + } + + return false +} + +// verify verifies if the reward root is correct, and return the total amount. +func (s *bridgeSigner) verify(ctx context.Context, epoch *Epoch, escrowAddr string) (*big.Int, error) { + rewards, err := s.kwil.GetEpochRewards(ctx, s.target, epoch.ID) + if err != nil { + return nil, err + } + + recipients := make([]string, len(rewards)) + amounts := make([]*big.Int, len(rewards)) + + var ok bool + total := big.NewInt(0) + for i, r := range rewards { + recipients[i] = r.Recipient + + amounts[i], ok = new(big.Int).SetString(r.Amount, 10) + if !ok { + return nil, fmt.Errorf("parse reward amount %s failed", r.Amount) + } + + total = total.Add(total, amounts[i]) + } + + var b32 [32]byte + copy(b32[:], epoch.EndBlockHash) + + _, root, err := utils.GenRewardMerkleTree(recipients, amounts, escrowAddr, b32) + if err != nil { + return nil, err + } + + if !slices.Equal(root, epoch.RewardRoot) { + return nil, fmt.Errorf("reward root mismatch: %s != %s", hex.EncodeToString(root), hex.EncodeToString(epoch.RewardRoot)) + } + + s.logger.Info("verified epoch", "id", epoch.ID.String(), "rewardRoot", hex.EncodeToString(epoch.RewardRoot)) + return total, nil +} + +// vote votes an epoch reward, and updates the state. +// It will first fetch metadata from ETH, then generate the safeTx, then vote. +func (s *bridgeSigner) vote(ctx context.Context, epoch *Epoch, safeMeta *safeMetadata, total *big.Int) error { + safeTxData, err := utils.GenPostRewardTxData(epoch.RewardRoot, total) + if err != nil { + return err + } + + // safeTxHash is the data that all signers will be signing(using personal_sign) + _, safeTxHash, err := utils.GenGnosisSafeTx(s.escrowAddr.String(), s.safe.addr.String(), + 0, safeTxData, s.safe.chainID.Int64(), safeMeta.nonce.Int64()) + if err != nil { + return err + } + + sig, err := utils.EthGnosisSign(safeTxHash, s.signerPk) + if err != nil { + return err + } + + h, err := s.kwil.VoteEpoch(ctx, s.target, s.txSigner, epoch.ID, safeMeta.nonce.Int64(), sig) + if err != nil { + return err + } + + // NOTE: it's fine if s.kwil.VoteEpoch succeed, but s.state.UpdateLastVote failed, + // as the epoch will be fetched again and skipped + err = s.state.UpdateLastVote(s.target, &voteRecord{ + Epoch: epoch.ID.String(), + RewardRoot: epoch.RewardRoot, + TxHash: h.String(), + SafeNonce: safeMeta.nonce.Uint64(), + }) + if err != nil { + return err + } + + s.logger.Info("vote epoch", "tx", h, "id", epoch.ID.String(), + "nonce", safeMeta.nonce.Int64()) + + return nil +} + +// sync polls on newer epochs and try to vote/sign them. +// Since there could be the case that the target(namespace/or id) not exist for whatever reason, +// this function won't return Error, and also won't log at Error level. +func (s *bridgeSigner) sync(ctx context.Context) { + s.logger.Debug("polling epochs") + + epochs, err := s.kwil.GetActiveEpochs(ctx, s.target) + if err != nil { + s.logger.Warn("fetch epoch", "error", err.Error()) + return + } + + if len(epochs) == 0 { + s.logger.Error("no epoch found") + return + } + + if len(epochs) == 1 { + // the very first round of epoch, we wait until there are 2 active epochs + return + } + + if len(epochs) != 2 { + s.logger.Error("unexpected number of epochs", "count", len(epochs)) + return + } + + finalizedEpoch := epochs[0] + + safeMeta, err := s.safe.latestMetadata(ctx) + if err != nil { + s.logger.Warn("fetch safe metadata", "error", err.Error()) + return + } + + if s.canSkip(finalizedEpoch, safeMeta) { + s.logger.Info("skip epoch", "id", finalizedEpoch.ID.String(), "height", finalizedEpoch.EndHeight) + return + } + + total, err := s.verify(ctx, finalizedEpoch, s.escrowAddr.String()) + if err != nil { + s.logger.Warn("verify epoch", "id", finalizedEpoch.ID.String(), "height", finalizedEpoch.EndHeight, "error", err.Error()) + return + } + + err = s.vote(ctx, finalizedEpoch, safeMeta, total) + if err != nil { + s.logger.Warn("vote epoch", "id", finalizedEpoch.ID.String(), "height", finalizedEpoch.EndHeight, "error", err.Error()) + return + } +} + +// getSigners verifies config and returns a list of signerSvc. +func getSigners(cfg config.ERC20BridgeConfig, kwil bridgeSignerClient, state *State, logger log.Logger) ([]*bridgeSigner, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + + ctx := context.Background() + + signers := make([]*bridgeSigner, 0, len(cfg.Signer)) + for target, pkPath := range cfg.Signer { + // pkPath is validated already + + // parse signer private key + rawPkBytes, err := os.ReadFile(pkPath) + if err != nil { + return nil, fmt.Errorf("read private key file %s failed: %w", pkPath, err) + } + + pkStr := strings.TrimSpace(string(rawPkBytes)) + pkBytes, err := hex.DecodeString(pkStr) + if err != nil { + return nil, fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) + } + + signerPk, err := ethCrypto.ToECDSA(pkBytes) + if err != nil { + return nil, fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) + } + + signerPubKey := signerPk.Public().(*ecdsa.PublicKey) + signerAddr := ethCrypto.PubkeyToAddress(*signerPubKey) + + // derive tx signer + key, err := crypto.UnmarshalSecp256k1PrivateKey(pkBytes) + if err != nil { + return nil, fmt.Errorf("parse erc20 bridge signer private key failed: %w", err) + } + + txSigner := &auth.EthPersonalSigner{Key: *key} + + // use instance info to create safe + instanceInfo, err := kwil.InstanceInfo(ctx, target) + if err != nil { + return nil, fmt.Errorf("get reward metadata failed: %w", err) + } + + chainRpc, ok := cfg.RPC[strings.ToLower(instanceInfo.Chain)] + if !ok { + return nil, fmt.Errorf("target '%s' chain '%s' not found in erc20_bridge.rpc config", target, instanceInfo.Chain) + } + + safe, err := NewSafeFromEscrow(chainRpc, instanceInfo.Escrow) + if err != nil { + return nil, fmt.Errorf("create safe failed: %w", err) + } + + chainInfo, ok := chains.GetChainInfo(chains.Chain(instanceInfo.Chain)) + if !ok { + return nil, fmt.Errorf("chainID %s not supported", safe.chainID.String()) + } + + if safe.chainID.String() != chainInfo.ID { + return nil, fmt.Errorf("chainID mismatch: configured %s != target %s", safe.chainID.String(), chainInfo.ID) + } + + // wilRpc, target, chainRpc, strings.TrimSpace(string(pkBytes)) + svc, err := newBridgeSigner(kwil, safe, target, txSigner, signerPk, signerAddr, ethCommon.HexToAddress(instanceInfo.Escrow), state, logger.New("EVMRW."+target)) + if err != nil { + return nil, fmt.Errorf("create erc20 bridge signer service failed: %w", err) + } + + signers = append(signers, svc) + } + + return signers, nil +} + +// ServiceMgr manages multiple bridgeSigner instances running in parallel. +type ServiceMgr struct { + kwil bridgeSignerClient // will be shared among all signers + state *State + bridgeCfg config.ERC20BridgeConfig + syncInterval time.Duration + logger log.Logger +} + +func NewServiceMgr( + chainID string, + db DB, + call engineCall, + bcast txBcast, + nodeApp nodeApp, + cfg config.ERC20BridgeConfig, + state *State, + logger log.Logger) *ServiceMgr { + return &ServiceMgr{ + kwil: NewSignerClient(chainID, db, call, bcast, nodeApp), + state: state, + bridgeCfg: cfg, + logger: logger, + syncInterval: time.Minute, // default to 1m + } +} + +// Start runs all rewardSigners. It returns error if there are issues initializing the bridgeSigner; +// no errors are returned after the bridgeSigner is running. +func (m *ServiceMgr) Start(ctx context.Context) error { + // since we need to wait on RPC running, we move the initialization logic into `init` + + var err error + var signers []*bridgeSigner + // To be able to run with docker, we need to apply a retry logic, because kwild + // won't have erc20 instance when boot + for { // naive way to keep retrying the init, on any error + select { + case <-ctx.Done(): + m.logger.Info("stop initializing erc20 bridge signer") + return nil + default: + } + + signers, err = getSigners(m.bridgeCfg, m.kwil, m.state, m.logger) + if err == nil { + break + } + + m.logger.Warn("failed to initialize erc20 bridge signer, will retry", "error", err.Error()) + time.Sleep(time.Second * 3) + } + + wg := &sync.WaitGroup{} + + for _, s := range signers { + wg.Add(1) + go func() { + defer wg.Done() + + s.logger.Info("start watching erc20 reward epoches") + tick := time.NewTicker(m.syncInterval) + + for { + s.sync(ctx) + + select { + case <-ctx.Done(): + s.logger.Info("stop watching erc20 reward epoches") + return + case <-tick.C: + } + } + }() + } + + <-ctx.Done() + wg.Wait() + + m.logger.Infof("Erc20 bridge signer service shutting down...") + + return nil +} diff --git a/node/exts/erc20-bridge/signersvc/state.go b/node/exts/erc20-bridge/signersvc/state.go new file mode 100644 index 000000000..06b2725cf --- /dev/null +++ b/node/exts/erc20-bridge/signersvc/state.go @@ -0,0 +1,121 @@ +// This file implements a naive KV persistent solution using a file, changes made +// through the exposed functions will be written to files on every invoking. +// This state won't grow, for signer svc, this is good enough. + +package signersvc + +import ( + "encoding/json" + "fmt" + "os" + "sync" +) + +// voteRecord holds the vote info of the epoch. +type voteRecord struct { + Epoch string `json:"epoch"` + RewardRoot []byte `json:"reward_root"` + TxHash string `json:"tx_hash"` + SafeNonce uint64 `json:"safe_nonce"` +} + +// State is a naive kv impl used by bridgeSigner. +type State struct { + path string + + mu sync.Mutex + + data map[string]*voteRecord // target => latest vote record +} + +// _sync will write State on to disk if it's loaded from disk. +func (s *State) _sync() error { + if s.path == "" { + return nil + } + + tmpPath := s.path + ".tmp" + + err := os.RemoveAll(tmpPath) + if err != nil { + return fmt.Errorf("ensure no tmp file: %w", err) + } + + tmpFile, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + return fmt.Errorf("open file: %w", err) + } + defer tmpFile.Close() + + err = json.NewEncoder(tmpFile).Encode(s.data) + if err != nil { + return fmt.Errorf("write state to file: %w", err) + } + + err = tmpFile.Sync() + if err != nil { + return fmt.Errorf("file sync: %w", err) + } + + err = os.Rename(tmpPath, s.path) + if err != nil { + return fmt.Errorf("") + } + + return err +} + +// UpdateLastVote updates the latest vote record, and syncs the changes to disk. +func (s *State) UpdateLastVote(target string, newVote *voteRecord) error { + s.mu.Lock() + defer s.mu.Unlock() + + s.data[target] = newVote + + return s._sync() +} + +func (s *State) LastVote(target string) *voteRecord { + s.mu.Lock() + defer s.mu.Unlock() + + if record, ok := s.data[target]; ok { + return record + } + + return nil +} + +// LoadStateFromFile load the state from a file. +func LoadStateFromFile(stateFile string) (*State, error) { + s := &State{ + path: stateFile, + data: make(map[string]*voteRecord), + } + + data, err := os.ReadFile(stateFile) + if err != nil { + return nil, err + } + + if len(data) == 0 { + return s, nil + } + + err = json.Unmarshal(data, &s.data) + if err != nil { + return nil, err + } + + return s, nil +} + +func NewMemState() *State { + return &State{} +} + +func NewTmpState() *State { + return &State{ + path: "/tmp/erc20rw-signer-state.json", + } +} diff --git a/node/exts/erc20reward/reward/crypto.go b/node/exts/erc20-bridge/utils/crypto.go similarity index 84% rename from node/exts/erc20reward/reward/crypto.go rename to node/exts/erc20-bridge/utils/crypto.go index a1b87e497..072baed45 100644 --- a/node/exts/erc20reward/reward/crypto.go +++ b/node/exts/erc20-bridge/utils/crypto.go @@ -1,4 +1,4 @@ -package reward +package utils import ( "bytes" @@ -6,36 +6,32 @@ import ( "fmt" "math/big" "slices" - "strings" ethAccounts "github.com/ethereum/go-ethereum/accounts" - ethAbi "github.com/ethereum/go-ethereum/accounts/abi" ethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" ethCrypto "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/signer/core" "github.com/ethereum/go-ethereum/signer/core/apitypes" + + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/abigen" ) -// ContractABI defines the ABI for the reward contract. -const ContractABI = `[{"name":"postReward","type":"function","inputs":[{"name":"rewardRoot","type":"bytes32"},{"name":"rewardAmount","type":"uint256"}],"outputs":[]}, -{"name":"updatePosterFee","type":"function","inputs":[{"name":"newFee","type":"uint256"}],"outputs":[]}, -{"name":"rewardPoster","type":"function","inputs":[{"name":"root","type":"bytes32"}],"outputs":[{"name":"","type":"address"}]}]` +const GnosisSafeSigLength = ethCrypto.SignatureLength func GenPostRewardTxData(root []byte, amount *big.Int) ([]byte, error) { - // Parse the ABI - parsedABI, err := ethAbi.JSON(strings.NewReader(ContractABI)) - if err != nil { - return nil, fmt.Errorf("failed to parse ABI: %v", err) - } - // Convert the root to a common.Hash type (because it's bytes32 in Ethereum) //rootHash := ethCommon.HexToHash(root) rootHash := ethCommon.BytesToHash(root) + rdABI, err := abigen.RewardDistributorMetaData.GetAbi() + if err != nil { + return nil, fmt.Errorf("failed to get ABI: %v", err) + } + // Encode the "postReward" function call with the given parameters - data, err := parsedABI.Pack("postReward", rootHash, amount) + data, err := rdABI.Pack("postReward", rootHash, amount) if err != nil { return nil, fmt.Errorf("failed to encode function call: %v", err) } @@ -45,7 +41,7 @@ func GenPostRewardTxData(root []byte, amount *big.Int) ([]byte, error) { // GenGnosisSafeTx returns a safe tx, and the tx hash to be used to generate signature. // More info: https://docs.safe.global/sdk/protocol-kit/guides/signatures/transactions -// Since Gnosis 1.3.0, ChainId is a part of the EIP-712 domain. +// Since Gnosis 1.3.0, ChainID is a part of the EIP-712 domain. func GenGnosisSafeTx(to, safe string, value int64, data hexutil.Bytes, chainID int64, nonce int64) (*core.GnosisSafeTx, []byte, error) { gnosisSafeTx := core.GnosisSafeTx{ @@ -111,7 +107,7 @@ func EthZeppelinSign(msg []byte, key *ecdsa.PrivateKey) ([]byte, error) { // https://docs.safe.global/advanced/smart-account-signatures // SDK: safe-core-sdk/packages/protocol-kit/src/utils/signatures/utils.ts `adjustVInSignature` // -// Since we use EIP-191, the V should be 31(0x1df) or 32(0x20). +// Since we use EIP-191, the V should be 31(0x1f) or 32(0x20). func EthGnosisSign(msg []byte, key *ecdsa.PrivateKey) ([]byte, error) { return EthGnosisSignDigest(ethAccounts.TextHash(msg), key) } @@ -136,9 +132,9 @@ func EthGnosisSignDigest(digest []byte, key *ecdsa.PrivateKey) ([]byte, error) { func EthGnosisVerifyDigest(sig []byte, digest []byte, address []byte) error { // signature is 65 bytes, [R || S || V] format - if len(sig) != ethCrypto.SignatureLength { + if len(sig) != GnosisSafeSigLength { return fmt.Errorf("invalid signature length: expected %d, received %d", - ethCrypto.SignatureLength, len(sig)) + GnosisSafeSigLength, len(sig)) } if sig[ethCrypto.RecoveryIDOffset] != 31 && sig[ethCrypto.RecoveryIDOffset] != 32 { @@ -164,9 +160,9 @@ func EthGnosisVerifyDigest(sig []byte, digest []byte, address []byte) error { func EthGnosisRecoverSigner(sig []byte, digest []byte) (*ethCommon.Address, error) { // signature is 65 bytes, [R || S || V] format - if len(sig) != ethCrypto.SignatureLength { + if len(sig) != GnosisSafeSigLength { return nil, fmt.Errorf("invalid signature length: expected %d, received %d", - ethCrypto.SignatureLength, len(sig)) + GnosisSafeSigLength, len(sig)) } if sig[ethCrypto.RecoveryIDOffset] != 31 && sig[ethCrypto.RecoveryIDOffset] != 32 { diff --git a/node/exts/erc20reward/reward/crypto_test.go b/node/exts/erc20-bridge/utils/crypto_test.go similarity index 86% rename from node/exts/erc20reward/reward/crypto_test.go rename to node/exts/erc20-bridge/utils/crypto_test.go index 4fe535e74..49a7b0020 100644 --- a/node/exts/erc20reward/reward/crypto_test.go +++ b/node/exts/erc20-bridge/utils/crypto_test.go @@ -1,4 +1,4 @@ -package reward_test +package utils_test import ( "crypto/ecdsa" @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/kwilteam/kwil-db/node/exts/erc20reward/reward" + "github.com/kwilteam/kwil-db/node/exts/erc20-bridge/utils" ) func getSigner(t *testing.T) (*ecdsa.PrivateKey, *ecdsa.PublicKey) { @@ -48,7 +48,7 @@ func TestSignHash(t *testing.T) { // THIS is what we want, with V adjusted gnosisAdjustedExpected := "8e7091f38dff5127c08580adaa07ab0b3ab5326beaca194f8703da1a31efdf735a4bddb505ec92ee52714a5591db71c9af57c5144458c5cc56098054e26ad44f1f" - gnosisAdjustedSig, err := reward.EthGnosisSign(safeTxHash, priKey) + gnosisAdjustedSig, err := utils.EthGnosisSign(safeTxHash, priKey) require.NoError(t, err) assert.Equal(t, gnosisAdjustedExpected, hex.EncodeToString(gnosisAdjustedSig)) } @@ -68,21 +68,21 @@ func TestGenSafeTx(t *testing.T) { root, err := hex.DecodeString(rootHex) require.NoError(t, err) - data, err := reward.GenPostRewardTxData(root, big.NewInt(21)) + data, err := utils.GenPostRewardTxData(root, big.NewInt(21)) require.NoError(t, err) fmt.Println("tx data:", hex.EncodeToString(data)) priKey, pubKey := getSigner(t) sender := crypto.PubkeyToAddress(*pubKey) - _, safeTxHash, err := reward.GenGnosisSafeTx(rewardAddress, safeAddress, value, data, chainID, nonce) + _, safeTxHash, err := utils.GenGnosisSafeTx(rewardAddress, safeAddress, value, data, chainID, nonce) require.NoError(t, err) assert.Equal(t, expectedSafeTxHash, hex.EncodeToString(safeTxHash)) - sig, err := reward.EthGnosisSign(safeTxHash, priKey) + sig, err := utils.EthGnosisSign(safeTxHash, priKey) require.NoError(t, err) assert.Equal(t, expectedSig, hex.EncodeToString(sig)) - err = reward.EthGnosisVerify(sig, safeTxHash, sender.Bytes()) + err = utils.EthGnosisVerify(sig, safeTxHash, sender.Bytes()) require.NoError(t, err) } diff --git a/node/exts/erc20reward/reward/mtree.go b/node/exts/erc20-bridge/utils/mtree.go similarity index 64% rename from node/exts/erc20reward/reward/mtree.go rename to node/exts/erc20-bridge/utils/mtree.go index 7cf4322e7..09fe8dbec 100644 --- a/node/exts/erc20reward/reward/mtree.go +++ b/node/exts/erc20-bridge/utils/mtree.go @@ -1,10 +1,11 @@ -package reward +package utils import ( "fmt" "math/big" "github.com/ethereum/go-ethereum/common" + smt "github.com/kwilteam/openzeppelin-merkle-tree-go/standard_merkle_tree" ) @@ -15,9 +16,9 @@ var ( MerkleLeafEncoding = []string{smt.SOL_ADDRESS, smt.SOL_UINT256, smt.SOL_ADDRESS, smt.SOL_BYTES32} ) -func GenRewardMerkleTree(users []string, amounts []*big.Int, contractAddress string, kwilBlockHash [32]byte) (string, []byte, error) { +func GenRewardMerkleTree(users []string, amounts []*big.Int, contractAddress string, kwilBlockHash [32]byte) ([]byte, []byte, error) { if len(users) != len(amounts) { - return "", nil, fmt.Errorf("users and amounts length not equal") + return nil, nil, fmt.Errorf("users and amounts length not equal") } values := [][]interface{}{} @@ -33,22 +34,22 @@ func GenRewardMerkleTree(users []string, amounts []*big.Int, contractAddress str rewardTree, err := smt.Of(values, MerkleLeafEncoding) if err != nil { - return "", nil, fmt.Errorf("create reward tree error: %w", err) + return nil, nil, fmt.Errorf("create reward tree error: %w", err) } dump, err := rewardTree.TreeMarshal() if err != nil { - return "", nil, fmt.Errorf("reward tree marshal error: %w", err) + return nil, nil, fmt.Errorf("reward tree marshal error: %w", err) } - return string(dump), rewardTree.GetRoot(), nil + return dump, rewardTree.GetRoot(), nil } // GetMTreeProof returns the leaf proof along with the leaf hash, amount. -func GetMTreeProof(mtreeJson string, addr string) (root []byte, proof [][]byte, leafHash []byte, blockHash []byte, amount string, err error) { - t, err := smt.Load([]byte(mtreeJson)) +func GetMTreeProof(mtreeJson []byte, addr string) (root []byte, proof [][]byte, leafHash []byte, blockHash []byte, amount *big.Int, err error) { + t, err := smt.Load(mtreeJson) if err != nil { - return nil, nil, nil, nil, "", fmt.Errorf("load mtree error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("load mtree error: %w", err) } entries := t.Entries() @@ -56,24 +57,24 @@ func GetMTreeProof(mtreeJson string, addr string) (root []byte, proof [][]byte, if v.Value[0] == smt.SolAddress(addr) { proof, err := t.GetProofWithIndex(i) if err != nil { - return nil, nil, nil, nil, "", fmt.Errorf("get proof error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("get proof error: %w", err) } amt, ok := v.Value[1].(*big.Int) if !ok { - return nil, nil, nil, nil, "", fmt.Errorf("internal bug: get leaf amount error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("internal bug: get leaf amount error: %w", err) } blockHash, ok := v.Value[3].([32]byte) if !ok { - return nil, nil, nil, nil, "", fmt.Errorf("internal bug: get leaf block hash error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("internal bug: get leaf block hash error: %w", err) } - return t.GetRoot(), proof, v.Hash, blockHash[:], amt.String(), nil + return t.GetRoot(), proof, v.Hash, blockHash[:], amt, nil } } - return nil, nil, nil, nil, "", fmt.Errorf("get proof error: %w", err) + return nil, nil, nil, nil, nil, fmt.Errorf("get proof error: %w", err) } func GetLeafAddresses(mtreeJson string) ([]string, error) { diff --git a/node/exts/erc20reward/reward/mtree_test.go b/node/exts/erc20-bridge/utils/mtree_test.go similarity index 96% rename from node/exts/erc20reward/reward/mtree_test.go rename to node/exts/erc20-bridge/utils/mtree_test.go index 94a37f2d5..13f2b3bd5 100644 --- a/node/exts/erc20reward/reward/mtree_test.go +++ b/node/exts/erc20-bridge/utils/mtree_test.go @@ -1,14 +1,16 @@ -package reward +package utils import ( "encoding/hex" + "math/big" "strings" "testing" "github.com/ethereum/go-ethereum/common/hexutil" - smt "github.com/kwilteam/openzeppelin-merkle-tree-go/standard_merkle_tree" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + smt "github.com/kwilteam/openzeppelin-merkle-tree-go/standard_merkle_tree" ) func Map[T1, T2 any](s []T1, f func(T1) T2) []T2 { @@ -43,11 +45,11 @@ func TestMerkleTree(t *testing.T) { Map([]string{"100", "200", "100"}, smt.SolNumber), contract, b32Hash) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(root)) - assert.JSONEq(t, treeLeafs3, mt) + assert.JSONEq(t, treeLeafs3, string(mt)) mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user2) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(mtRoot)) - require.Equal(t, "200", amt) + require.Equal(t, big.NewInt(200), amt) require.EqualValues(t, kwilBlockHashBytes, bh) require.Len(t, mtProof, 2) assert.Equal(t, "0x644f999664d65d1d2a3feefade54d643dc2b9696971e9070c36f0ec788e55f5b", hexutil.Encode(mtProof[0])) @@ -61,11 +63,11 @@ func TestMerkleTree(t *testing.T) { Map([]string{"100", "200", "100", "200"}, smt.SolNumber), contract, b32Hash) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(root)) - assert.JSONEq(t, treeLeafs4, mt) + assert.JSONEq(t, treeLeafs4, string(mt)) mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user2) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(mtRoot)) - require.Equal(t, "200", amt) + require.Equal(t, big.NewInt(200), amt) require.EqualValues(t, kwilBlockHashBytes, bh) require.Len(t, mtProof, 2) assert.Equal(t, "0x644f999664d65d1d2a3feefade54d643dc2b9696971e9070c36f0ec788e55f5b", hexutil.Encode(mtProof[0])) @@ -79,11 +81,11 @@ func TestMerkleTree(t *testing.T) { Map([]string{"100", "200", "100", "200", "100"}, smt.SolNumber), contract, b32Hash) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(root)) - assert.JSONEq(t, treeLeafs5, mt) + assert.JSONEq(t, treeLeafs5, string(mt)) mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user2) require.NoError(t, err) require.Equal(t, expectRoot, hex.EncodeToString(mtRoot)) - require.Equal(t, "200", amt) + require.Equal(t, big.NewInt(200), amt) require.EqualValues(t, kwilBlockHashBytes, bh) require.Len(t, mtProof, 3) assert.Equal(t, "0x644f999664d65d1d2a3feefade54d643dc2b9696971e9070c36f0ec788e55f5b", hexutil.Encode(mtProof[0])) @@ -100,7 +102,7 @@ func TestMerkleTree(t *testing.T) { mtRoot, mtProof, mtLeaf, bh, amt, err := GetMTreeProof(mt, user1) require.NoError(t, err) require.Equal(t, root, mtRoot) - require.Equal(t, "100", amt) + require.Equal(t, big.NewInt(100), amt) require.EqualValues(t, kwilBlockHashBytes, bh) require.Len(t, mtProof, 0) // no proofs assert.Equal(t, root, mtLeaf) // the leaf is the root diff --git a/node/exts/erc20reward/abigen/reward_distributor_abi.json b/node/exts/erc20reward/abigen/reward_distributor_abi.json deleted file mode 100644 index db4ec9acf..000000000 --- a/node/exts/erc20reward/abigen/reward_distributor_abi.json +++ /dev/null @@ -1,295 +0,0 @@ -[ - { - "inputs": [ - { - "internalType": "address", - "name": "_safe", - "type": "address" - }, - { - "internalType": "uint256", - "name": "_posterFee", - "type": "uint256" - }, - { - "internalType": "address", - "name": "_rewardToken", - "type": "address" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "inputs": [], - "name": "ReentrancyGuardReentrantCall", - "type": "error" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "uint256", - "name": "newFee", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "nonce", - "type": "uint256" - } - ], - "name": "PosterFeeUpdated", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "claimer", - "type": "address" - } - ], - "name": "RewardClaimed", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "internalType": "bytes32", - "name": "root", - "type": "bytes32" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "address", - "name": "poster", - "type": "address" - } - ], - "name": "RewardPosted", - "type": "event" - }, - { - "stateMutability": "payable", - "type": "fallback" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "internalType": "bytes32", - "name": "kwilBlockHash", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "rewardRoot", - "type": "bytes32" - }, - { - "internalType": "bytes32[]", - "name": "proofs", - "type": "bytes32[]" - } - ], - "name": "claimReward", - "outputs": [], - "stateMutability": "payable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - }, - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "isRewardClaimed", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "nonce", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "root", - "type": "bytes32" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "postReward", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "postedRewards", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "posterFee", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "bytes32", - "name": "", - "type": "bytes32" - } - ], - "name": "rewardPoster", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "rewardToken", - "outputs": [ - { - "internalType": "contract IERC20", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "safe", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "unpostedRewards", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "newFee", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "_nonce", - "type": "uint256" - } - ], - "name": "updatePosterFee", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "stateMutability": "payable", - "type": "receive" - } -] \ No newline at end of file diff --git a/node/exts/evm-sync/evm_state.go b/node/exts/evm-sync/evm_state.go index 125b2affa..8235a7a2d 100644 --- a/node/exts/evm-sync/evm_state.go +++ b/node/exts/evm-sync/evm_state.go @@ -211,7 +211,7 @@ func (s *statePoller) runPollFuncs(ctx context.Context, service *common.Service, } func makeNewClient(ctx context.Context, service *common.Service, chain chains.Chain) (*ethclient.Client, error) { - chainConf, err := getChainConf(service.LocalConfig.Extensions, chain) + chainConf, err := getChainConf(service.LocalConfig.Erc20Bridge, chain) if err != nil { return nil, fmt.Errorf("failed to get chain config for %s: %v", chain, err) } diff --git a/node/exts/evm-sync/instance.go b/node/exts/evm-sync/instance.go index d92c9de82..c92c89b85 100644 --- a/node/exts/evm-sync/instance.go +++ b/node/exts/evm-sync/instance.go @@ -205,7 +205,7 @@ type listenerInfo struct { func (l *listenerInfo) listen(ctx context.Context, service *common.Service, eventstore listeners.EventStore, syncConf *syncConfig) { logger := service.Logger.New(l.uniqueName + "." + string(l.chain.Name)) - chainConf, err := getChainConf(service.LocalConfig.Extensions, l.chain.Name) + chainConf, err := getChainConf(service.LocalConfig.Erc20Bridge, l.chain.Name) if err != nil { logger.Error("failed to get chain config", "err", err) return diff --git a/node/exts/evm-sync/listener.go b/node/exts/evm-sync/listener.go index 5a238bf16..56d41bc8b 100644 --- a/node/exts/evm-sync/listener.go +++ b/node/exts/evm-sync/listener.go @@ -16,6 +16,7 @@ import ( "github.com/ethereum/go-ethereum/ethclient" "github.com/kwilteam/kwil-db/common" + "github.com/kwilteam/kwil-db/config" "github.com/kwilteam/kwil-db/core/log" "github.com/kwilteam/kwil-db/extensions/listeners" "github.com/kwilteam/kwil-db/node/exts/evm-sync/chains" @@ -104,31 +105,45 @@ func (c *syncConfig) load(m map[string]string) error { } // getChainConf gets the chain config from the node's local configuration. -func getChainConf(m map[string]map[string]string, chain chains.Chain) (*chainConfig, error) { - var m2 map[string]string +func getChainConf(cfg config.ERC20BridgeConfig, chain chains.Chain) (*chainConfig, error) { + err := cfg.Validate() + if err != nil { + return nil, err + } + var ok bool + var provider string + var syncChunk string switch chain { - case chains.Ethereum: - m2, ok = m["ethereum_sync"] + case chains.Ethereum, chains.Sepolia: + provider, ok = cfg.RPC[chain.String()] if !ok { - return nil, errors.New("local configuration does not have an ethereum_sync config") + return nil, fmt.Errorf("local configuration does not have an '%s' config", chain.String()) } - case chains.Sepolia: - m2, ok = m["sepolia_sync"] + + syncChunk, ok = cfg.BlockSyncChuckSize[chains.Ethereum.String()] if !ok { - return nil, errors.New("local configuration does not have a sepolia_sync config") + syncChunk = "1000000" } default: // suggests an internal bug where we have not added a case for a new chain return nil, fmt.Errorf("unknown chain %s", chain) } - conf := &chainConfig{} - err := conf.load(m2) + blockSyncChunkSize, err := strconv.ParseInt(syncChunk, 10, 64) if err != nil { return nil, err } + if blockSyncChunkSize <= 0 { + return nil, errors.New("block_sync_chunk_size must be greater than 0") + } + + conf := &chainConfig{ + BlockSyncChunkSize: blockSyncChunkSize, + Provider: provider, + } + return conf, nil } @@ -143,33 +158,6 @@ type chainConfig struct { Provider string } -// load loads the config into the struct from the node's local configuration -func (c *chainConfig) load(m map[string]string) error { - if v, ok := m["block_sync_chunk_size"]; ok { - i, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return err - } - - if i <= 0 { - return errors.New("block_sync_chunk_size must be greater than 0") - } - - c.BlockSyncChunkSize = i - } else { - c.BlockSyncChunkSize = 1000000 - } - - v, ok := m["provider"] - if !ok { - return errors.New("provider is required") - } - - c.Provider = v - - return nil -} - // individualListener is a singler configured client that is responsible for listening to a single set of contracts. // Many individual listeners can exist for a single chain. type individualListener struct { @@ -260,6 +248,7 @@ func (i *individualListener) listen(ctx context.Context, eventstore listeners.Ev func (i *individualListener) processEvents(ctx context.Context, from, to int64, eventStore listeners.EventStore, logger log.Logger) error { logs, err := i.getLogsFunc(ctx, i.client.client, uint64(from), uint64(to), logger) + /// THIS IS WHERE I LEFT if err != nil { return err } diff --git a/test/go.mod b/test/go.mod index e7d29ed84..870243383 100644 --- a/test/go.mod +++ b/test/go.mod @@ -283,6 +283,7 @@ require ( github.com/r3labs/sse v0.0.0-20210224172625-26fe804710bc // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.47.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect diff --git a/test/go.sum b/test/go.sum index cd1fb0d54..7428003eb 100644 --- a/test/go.sum +++ b/test/go.sum @@ -870,6 +870,8 @@ github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3V github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=