diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 41f32a6..09f9be4 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -29,7 +29,7 @@ ## Developer Workflows - `make sdk` (same as `make build`) compiles all packages; `make examples` or `make example-` drops binaries into `build/`. - `make test` runs `go test -race -coverprofile=coverage.out ./...` and emits `coverage.html`; `make lint` requires `golangci-lint`. -- Stick to Go 1.25.5 (per `go.mod`) and respect the `replace` pins for CometBFT/Cosmos; update both blockchain + SuperNode deps together when bumping versions. +- Stick to Go 1.26.2 (per `go.mod`) and respect the `replace` pins for CometBFT/Cosmos; update both blockchain + SuperNode deps together when bumping versions. ## Conventions - Errors wrap context with `fmt.Errorf("context: %w", err)` so callers can unwrap lower layers. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..21da017 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,222 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [v1.2.0] - 2026-05-27 + +### Added + +- Added EVM migration support with query helpers, migration message constructors, and transaction helpers for claiming legacy accounts and migrating validators. +- Added EVM address and signing utilities, including Ethereum address derivation, Bech32/EVM address conversion, wei/ulume conversion helpers, Ethereum transaction signing, sender recovery, and `MsgEthereumTx` wrapping. +- Registered cosmos/evm interfaces in the default transaction config. +- Added read-only clients for cosmos/evm modules: EVM, FeeMarket, and PreciseBank. +- Added Ethereum-format transaction support through `EVMClient`, including nonce resolution, gas estimation, EIP-1559 fee cap resolution, raw Ethereum transaction broadcast, contract deployment, and read-only contract calls. +- Added ERC20 module support with token-pair queries, coin/ERC20 conversion transaction helpers, and ABI helpers for balance, supply, allowance, and metadata reads. +- Added Lumera precompile support with embedded Action, Supernode, and Wasm ABIs plus generic precompile call/send wrappers. +- Added EVM-focused examples for balance queries, transfers, ERC20 conversion, and Action precompile usage. +- Added a Cosmos EVM API design document and split developer documentation into focused guides. + +### Changed + +- Refactored the base transaction build/sign pipeline to support explicit transaction build options, manual signer metadata, simulation-based gas estimation, fee overrides, and multi-message validation. +- Added configurable EVM options to the client configuration, including EVM chain ID, EVM native/extended denoms, and EVM gas caps. +- Exposed chain-economics overrides on the top-level client config (`AccountHRP`, `FeeDenom`, `GasPrice`) via `WithAccountHRP`, `WithFeeDenom`, and `WithGasPrice`, forwarding them into the blockchain config (previously settable only at the blockchain layer). +- Replaced the local ethsecp256k1 implementation with the cosmos/evm implementation and updated keyring handling for EVM keys. +- Updated dependency pins for the EVM stack, including Lumera, Cosmos SDK, cosmos/evm, the forked go-ethereum replacement, and SuperNode SDK `v2.5.2`. +- Kept local Lumera/SuperNode development `replace` directives commented out for release safety. + +### Fixed + +- Guarded Cosmos transaction building from accidentally routing `MsgEthereumTx` through the regular Cosmos signing pipeline. +- Fixed EVM denom resolution so transaction helpers can derive missing EVM denoms from chain params while still failing clearly when required values are unavailable. +- Tightened EVM nonce, gas, and fee cap validation, including negative-value checks and large gas fee overflow coverage. +- Hardened EVM API response handling for nil math values and malformed EVM transaction response data. +- `ImportKey` now verifies that an existing key was derived from the supplied mnemonic, instead of silently returning the stored key when a different mnemonic is imported under the same name. +- ERC20 read helpers (`Erc20Balance`/`Erc20TotalSupply`/`Erc20Allowance`/`Erc20Metadata`) return a clear error when the call target returns no data (no contract code or reverted), rather than a cryptic ABI unmarshal error. +- Gas estimation now surfaces simulation failures instead of silently falling back to a fixed gas limit that could under-gas the transaction; callers can bypass via `GasLimit` or `SkipSimulation`. +- `DeployContract` returns the zero address and an error when the constructor reverts, instead of an address where no contract was deployed. +- `buildEthereumTxBytes` rejects an empty native denom (previously panicked in `sdk.NewCoin`), `FeeMarket.BlockGas` rejects negative gas, and `GasUsed` is guarded against negative-to-`uint64` wraparound. +- The tx-wait helper surfaces the subscriber error when the poller also fails, preserving the more diagnostic root cause. +- Fixed linter issues in EVM examples, ERC20 helpers, and EVM transaction tests. + +### Tests + +- Added unit coverage for EVM address conversion, Ethereum signing, transaction wrapping, EVM query wrappers, Ethereum transaction byte construction, ERC20 helpers, precompile wrappers, EVMigration queries/messages, and transaction build pipeline validation. +- Added regression coverage for large `uint64` gas fee calculation. + +## [v1.1.2] - 2026-05-15 + +### Changed + +- Bumped the SuperNode SDK dependency to `v2.5.0-rc`. +- Refreshed module sums for the SuperNode dependency update. + +## [v1.1.1] - 2026-03-26 + +### Changed + +- Bumped the SuperNode SDK dependency to `v2.4.72`. +- Refreshed module sums for the SuperNode dependency update. + +## [v1.1.0] - 2026-03-24 + +### Changed + +- Updated Lumera and SuperNode SDK dependencies. +- Fixed compatibility issues related to the Cosmos SDK update. +- Updated release workflow permissions. +- Adjusted Cascade upload and SuperNode logging behavior for the dependency updates. + +### Fixed + +- Fixed linter issues. + +## [v1.0.9] - 2026-02-16 + +### Changed + +- Unified the `KeyType` API for multi-chain keyring usage. +- Added ICA `HostKeyName` support. +- Updated keyring docs and examples to use the unified key-type flow. +- Updated ICA controller types and helper code for host-key-name aware requests. + +### Tests + +- Updated crypto/keyring tests for the unified multi-chain keyring behavior. + +## [v1.0.8] - 2026-01-30 + +### Changed + +- Updated Cosmos SDK to `v0.53.5`. +- Updated Lumera dependency to `v1.10.0`. +- Updated Go setup/release workflow configuration for the dependency bump. +- Refreshed repository guidance for the newer dependency stack. + +## [v1.0.7] - 2026-01-30 + +### Added + +- Added multi-chain keyring support. +- Added Injective key support. +- Added ethsecp256k1 test coverage. + +### Fixed + +- Fixed verify logic in the ICA request verification example. +- Fixed verify behavior for modified injected keyring flows. + +### Changed + +- Bumped the SuperNode SDK dependency. +- Updated ICA request and verification examples for the expanded keyring support. +- Updated build tooling and module sums for the new crypto/keyring dependencies. + +## [v1.0.6] - 2026-01-15 + +### Added + +- Added ICA controller support. +- Added standalone ICA package helpers for acknowledgements, packing, controller operations, and request helpers. +- Added ICA examples for request, approval, and multi-account flows. + +### Changed + +- Refactored the blockchain client base. +- Split lower-level blockchain client configuration and transaction building into the `blockchain/base` package. +- Updated high-level client options and configuration wiring around the refactored base client. +- Updated Cascade upload/download/request flows to use the refactored client and ICA helpers. +- Expanded API and developer documentation for the refactored client and ICA flows. + +### Tests + +- Added unit tests for ICA acknowledgement helpers, packet packing, controller helpers, Cascade request handling, and base transaction behavior. + +## [v1.0.5] - 2026-01-10 + +### Added + +- Added ICA flow support for Cascade actions. +- Added ICA acknowledgement helpers. +- Added ICA packet packing helpers and tests. +- Added ICA request/approval examples and multi-account examples. + +### Changed + +- Updated Cascade upload/download flows to support ICA-backed action workflows. +- Updated action approval and claim-token examples. +- Expanded README, API, and developer guide documentation for ICA and Cascade flows. +- Moved crypto/keyring helper code under `pkg/crypto`. + +### Tests + +- Added tests for ICA acknowledgement handling, packet packing, and Cascade request behavior. + +## [v1.0.4] - 2025-12-28 + +### Changed + +- Updated dependencies to their latest compatible versions at the time of release. +- Updated Action and Cascade request structures for file-size metadata in kilobytes. +- Refreshed API and developer documentation for the Action/Cascade changes. +- Updated README guidance for the dependency and request changes. + +## [v1.0.3] - 2025-12-10 + +### Fixed + +- Handled transaction indexing lag in `WaitForTxInclusion`. +- Added retry behavior when a transaction is observed but not yet available through the transaction query service. +- Updated transaction waiting tests for delayed indexing behavior. + +### Changed + +- Updated CI workflow configuration for lint, release, and test jobs. + +## [v1.0.2] - 2025-12-03 + +### Added + +- Added SDK event changes from the event-change branch. +- Added Cascade event type support. +- Added event handling updates in Cascade client/upload flows. + +### Changed + +- Updated SuperNode SDK dependency to `v2.4.10`. +- Updated Cascade upload example for the event changes. +- Refreshed module sums for the SuperNode and event dependency updates. + +### Fixed + +- Fixed linter warnings and formatting issues. + +## [v1.0.1] - 2025-11-26 + +### Changed + +- Added `go.sum` verification to the Makefile flow. +- Updated Lumera dependency to `v1.8.5`. +- Updated SuperNode SDK dependency to `v2.4.9`. +- Refreshed module sums for the Lumera and SuperNode dependency updates. + +## [v1.0.0] - 2025-11-20 + +### Added + +- Initial SDK release. +- Added address helper support. +- Added ICA helpers and examples. +- Added Action approval support. +- Added Cascade upload support with request creation, request sending, and SuperNode upload stages. +- Added Cascade action status checks after file upload. +- Added blockchain message constructors and transaction handling for Action/Cascade flows. +- Added README and developer/API documentation for the first SDK surface. + +### Changed + +- Split upload flow into request creation, request sending, and SuperNode upload steps. +- Added action status checks after Cascade uploads. +- Refactored message constructors and transaction handling for Cascade flows. +- Updated Lumera dependency to `v1.8.4`. +- Updated SuperNode SDK dependency to `v2.4.2`. diff --git a/README.md b/README.md index 244605b..f896aa8 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Official Go SDK for the Lumera Protocol - a next-generation blockchain platform - ๐Ÿ“ฆ Type-Safe โ€” Full Go type definitions for all Lumera modules - ๐Ÿš€ High-Level API โ€” Simple methods for complex operations - ๐Ÿ” Secure โ€” Built on Cosmos SDK's proven cryptography +- โšก EVM-Ready โ€” Ethereum-format transactions, ERC20 conversion, Lumera precompiles, and EVM-account migration (see the [EVM guide](docs/guides/evm.md)) - ๐Ÿ“ Well-Documented โ€” Comprehensive examples and documentation ## Unified APIs @@ -294,11 +295,17 @@ See the [examples](./examples) directory for complete working examples: - [Query Actions](./examples/query-actions) - Query blockchain actions - [Claim Tokens](./examples/claim-tokens) - Claim tokens from old chain - [Multi-Account Factory](./examples/multi-account) - Reuse a config while swapping local signers +- [EVM Balance](./examples/evm-balance) - Read EVM-side account state (balance, nonce, base fee) +- [EVM Transfer](./examples/evm-transfer) - Send an Ethereum-format transaction +- [ERC20 Convert](./examples/erc20-convert) - Convert between Cosmos coins and ERC20 tokens +- [Precompile Action](./examples/precompile-action) - Call a Lumera precompile from EVM +- [ICA Request / Approve](./examples/ica-request-tx) - Interchain-account action flows ## Documentation - [Developer Guide & Tutorials](docs/DEVELOPER_GUIDE.md) - [API Overview](docs/API.md) +- Topic guides: [Getting Started](docs/guides/getting-started.md) ยท [Crypto](docs/guides/crypto.md) ยท [Actions](docs/guides/actions.md) ยท [Cascade](docs/guides/cascade.md) ยท [ICA](docs/guides/ica.md) ยท [EVM](docs/guides/evm.md) - [API Documentation](https://pkg.go.dev/github.com/LumeraProtocol/sdk-go) - [Lumera Documentation](https://docs.lumera.io) diff --git a/blockchain/base/client.go b/blockchain/base/client.go index 04c7bb1..af1115f 100644 --- a/blockchain/base/client.go +++ b/blockchain/base/client.go @@ -13,6 +13,11 @@ import ( clientconfig "github.com/LumeraProtocol/sdk-go/client/config" ) +const ( + defaultMaxRecvMessageSize = 4 * 1024 * 1024 + defaultMaxSendMessageSize = 50 * 1024 * 1024 +) + // Client provides common Cosmos SDK gRPC and tx helpers. type Client struct { conn *grpc.ClientConn @@ -23,6 +28,8 @@ type Client struct { // New creates a base blockchain client with a gRPC connection. func New(ctx context.Context, cfg Config, kr keyring.Keyring, keyName string) (*Client, error) { + applyConfigDefaults(&cfg) + // Determine if we should use TLS based on the endpoint. // Use TLS if: port is 443, or hostname doesn't start with "localhost"/"127.0.0.1". useTLS := shouldUseTLS(cfg.GRPCAddr) @@ -48,8 +55,6 @@ func New(ctx context.Context, cfg Config, kr keyring.Keyring, keyName string) (* ), } - clientconfig.ApplyWaitTxDefaults(&cfg.WaitTx) - conn, err := grpc.NewClient(cfg.GRPCAddr, dialOpts...) if err != nil { return nil, fmt.Errorf("failed to connect to gRPC: %w", err) @@ -63,6 +68,19 @@ func New(ctx context.Context, cfg Config, kr keyring.Keyring, keyName string) (* }, nil } +func applyConfigDefaults(cfg *Config) { + if cfg == nil { + return + } + if cfg.MaxRecvMsgSize <= 0 { + cfg.MaxRecvMsgSize = defaultMaxRecvMessageSize + } + if cfg.MaxSendMsgSize <= 0 { + cfg.MaxSendMsgSize = defaultMaxSendMessageSize + } + clientconfig.ApplyWaitTxDefaults(&cfg.WaitTx) +} + // Close closes the underlying gRPC connection. func (c *Client) Close() error { if c.conn != nil { @@ -76,6 +94,21 @@ func (c *Client) GRPCConn() *grpc.ClientConn { return c.conn } +// Keyring returns the keyring used for signing. +func (c *Client) Keyring() keyring.Keyring { + return c.keyring +} + +// KeyName returns the key uid used for signing. +func (c *Client) KeyName() string { + return c.keyName +} + +// Cfg returns a copy of the base client configuration. +func (c *Client) Cfg() Config { + return c.config +} + // shouldUseTLS determines if TLS should be used based on the gRPC address. func shouldUseTLS(addr string) bool { // Check for explicit port 443 (standard HTTPS/gRPC-TLS port). diff --git a/blockchain/base/config.go b/blockchain/base/config.go index 182425a..aca0f50 100644 --- a/blockchain/base/config.go +++ b/blockchain/base/config.go @@ -1,6 +1,7 @@ package base import ( + "math/big" "time" sdkmath "cosmossdk.io/math" @@ -21,4 +22,25 @@ type Config struct { MaxSendMsgSize int InsecureGRPC bool WaitTx clientconfig.WaitTxConfig + + // EVMChainID is the EIP-155 chain ID for Ethereum-format transactions. + // Distinct from the Cosmos ChainID. Nil disables EVM-tx helpers. + EVMChainID *big.Int + + // EVMNativeDenom is the cosmos/evm `evm_denom` parameter โ€” the bank denom + // fees are deducted in (Lumera: "ulume"). Used by + // MsgEthereumTx.BuildTxWithEvmParams when constructing the cosmos fee coin + // from the inner Ethereum tx. + EVMNativeDenom string + + // EVMExtendedDenom is the 18-decimal precisebank denom (Lumera: "alume"). + // Ethereum tx value, fee caps, balances, and receipts use this denom's + // integer "wei-like" representation. + EVMExtendedDenom string + + // EVMGasTipCap and EVMGasFeeCap are optional defaults (in the extended + // denom's integer unit) for EIP-1559 gas pricing. Nil means "fetch from + // chain state". + EVMGasTipCap *big.Int + EVMGasFeeCap *big.Int } diff --git a/blockchain/base/tx.go b/blockchain/base/tx.go index 6132560..63e72ea 100644 --- a/blockchain/base/tx.go +++ b/blockchain/base/tx.go @@ -7,9 +7,13 @@ import ( "time" txtypes "cosmossdk.io/api/cosmos/tx/v1beta1" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/crypto/keyring" sdk "github.com/cosmos/cosmos-sdk/types" signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + evmtypes "github.com/cosmos/evm/x/vm/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -17,6 +21,29 @@ import ( sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" ) +const defaultSignedTxGasLimit = 200000 + +var msgEthereumTxTypeURL = sdk.MsgTypeURL(&evmtypes.MsgEthereumTx{}) + +// TxBuildOptions controls how a transaction is assembled and signed. +type TxBuildOptions struct { + Messages []sdk.Msg + Memo string + GasAdjustment float64 + GasLimit uint64 + SkipSimulation bool + AccountNumber *uint64 + Sequence *uint64 + FeeAmount sdk.Coins + ZeroFee bool +} + +// TxSignerInfo contains the signer account metadata used for signing. +type TxSignerInfo struct { + AccountNumber uint64 + Sequence uint64 +} + // Simulate runs a gas simulation for a provided tx bytes. func (c *Client) Simulate(ctx context.Context, txBytes []byte) (uint64, error) { svc := txtypes.NewServiceClient(c.conn) @@ -54,9 +81,34 @@ func (c *Client) Broadcast(ctx context.Context, txBytes []byte, mode txtypes.Bro return resp.TxResponse.GetTxhash(), nil } +// BroadcastAndWait broadcasts signed tx bytes, then waits for final inclusion. +func (c *Client) BroadcastAndWait(ctx context.Context, txBytes []byte, mode txtypes.BroadcastMode) (string, *txtypes.GetTxResponse, error) { + txHash, err := c.Broadcast(ctx, txBytes, mode) + if err != nil { + return "", nil, err + } + + resp, err := c.WaitForTxInclusion(ctx, txHash) + if err != nil { + return txHash, nil, err + } + if resp == nil || resp.TxResponse == nil { + return txHash, nil, fmt.Errorf("empty tx response") + } + if resp.TxResponse.Code != 0 { + return txHash, resp, fmt.Errorf("tx failed with code %d: %s", resp.TxResponse.Code, resp.TxResponse.RawLog) + } + + return txHash, resp, nil +} + // BuildAndSignTx builds a transaction with one message, simulates gas, then signs it. func (c *Client) BuildAndSignTx(ctx context.Context, msg sdk.Msg, memo string) ([]byte, error) { - return c.buildAndSignTx(ctx, msg, memo, 1.3) + return c.BuildAndSignTxWithOptions(ctx, TxBuildOptions{ + Messages: []sdk.Msg{msg}, + Memo: memo, + GasAdjustment: 1.3, + }) } // BuildAndSignTxWithGasAdjustment builds a transaction with one message, simulates gas, @@ -65,61 +117,45 @@ func (c *Client) BuildAndSignTxWithGasAdjustment(ctx context.Context, msg sdk.Ms if gasAdjustment <= 0 { gasAdjustment = 1.3 } - return c.buildAndSignTx(ctx, msg, memo, gasAdjustment) + return c.BuildAndSignTxWithOptions(ctx, TxBuildOptions{ + Messages: []sdk.Msg{msg}, + Memo: memo, + GasAdjustment: gasAdjustment, + }) } -func (c *Client) buildAndSignTx(ctx context.Context, msg sdk.Msg, memo string, gasAdjustment float64) ([]byte, error) { - if c.keyring == nil { - return nil, fmt.Errorf("keyring is required") - } - if strings.TrimSpace(c.keyName) == "" { - return nil, fmt.Errorf("key name is required") - } - if strings.TrimSpace(c.config.AccountHRP) == "" { - return nil, fmt.Errorf("account HRP is required") - } - if strings.TrimSpace(c.config.FeeDenom) == "" { - return nil, fmt.Errorf("fee denom is required") - } - if c.config.GasPrice.IsNil() || c.config.GasPrice.IsZero() { - return nil, fmt.Errorf("gas price is required") +// BuildAndSignTxWithOptions builds and signs a transaction using explicit options. +func (c *Client) BuildAndSignTxWithOptions(ctx context.Context, opts TxBuildOptions) ([]byte, error) { + txCfg, builder, signerInfo, err := c.PrepareTx(ctx, opts) + if err != nil { + return nil, err } + return c.SignPreparedTx(ctx, txCfg, builder, signerInfo) +} - // 1) Tx config and builder +// PrepareTx builds an unsigned tx builder and resolves the signer metadata. +func (c *Client) PrepareTx(ctx context.Context, opts TxBuildOptions) (client.TxConfig, client.TxBuilder, TxSignerInfo, error) { txCfg := sdkcrypto.NewDefaultTxConfig() builder := txCfg.NewTxBuilder() - if err := builder.SetMsgs(msg); err != nil { - return nil, fmt.Errorf("set msgs: %w", err) - } - if memo != "" { - builder.SetMemo(memo) - } - // 2) Resolve account number/sequence BEFORE simulation - rec, err := c.keyring.Key(c.keyName) - if err != nil { - return nil, fmt.Errorf("load key %q: %w", c.keyName, err) + if err := c.validateTxBuildOptions(opts); err != nil { + return nil, nil, TxSignerInfo{}, err } - accAddr, err := sdkcrypto.AddressFromKey(c.keyring, c.keyName, c.config.AccountHRP) - if err != nil { - return nil, fmt.Errorf("derive address for %q: %w", c.keyName, err) + if err := builder.SetMsgs(opts.Messages...); err != nil { + return nil, nil, TxSignerInfo{}, fmt.Errorf("set msgs: %w", err) + } + if opts.Memo != "" { + builder.SetMemo(opts.Memo) } - authq := authtypes.NewQueryClient(c.conn) - acctResp, err := authq.AccountInfo(ctx, &authtypes.QueryAccountInfoRequest{ - Address: accAddr, - }) + rec, signerInfo, err := c.resolveSignerInfo(ctx, opts) if err != nil { - return nil, fmt.Errorf("query account info: %w", err) - } - if acctResp == nil || acctResp.Info == nil { - return nil, fmt.Errorf("empty account info response") + return nil, nil, TxSignerInfo{}, err } - // 3) Build placeholder signature using real sequence pk, err := rec.GetPubKey() if err != nil { - return nil, fmt.Errorf("get pubkey for %q: %w", c.keyName, err) + return nil, nil, TxSignerInfo{}, fmt.Errorf("get pubkey for %q: %w", c.keyName, err) } signMode := txCfg.SignModeHandler().DefaultMode() placeholder := signingtypes.SignatureV2{ @@ -127,53 +163,46 @@ func (c *Client) buildAndSignTx(ctx context.Context, msg sdk.Msg, memo string, g Data: &signingtypes.SingleSignatureData{ SignMode: signingtypes.SignMode(signMode), }, - Sequence: acctResp.Info.Sequence, // use real sequence for simulation + Sequence: signerInfo.Sequence, } if err := builder.SetSignatures(placeholder); err != nil { - return nil, fmt.Errorf("set placeholder signature: %w", err) + return nil, nil, TxSignerInfo{}, fmt.Errorf("set placeholder signature: %w", err) } - // 4) Simulate with placeholder to get gas - unsignedBytes, err := txCfg.TxEncoder()(builder.GetTx()) + gas, err := c.resolveGasLimit(ctx, txCfg, builder, opts) if err != nil { - return nil, fmt.Errorf("encode unsigned tx: %w", err) + return nil, nil, TxSignerInfo{}, err } + builder.SetGasLimit(gas) - gasUsed, err := c.Simulate(ctx, unsignedBytes) - gas := uint64(0) - if err == nil && gasUsed > 0 { - // add an adjustable buffer - gas = uint64(float64(gasUsed) * gasAdjustment) - if gas == 0 { - gas = gasUsed - } - } else { - // On simulation failure, proceed with a conservative default gas - if builder.GetTx().GetGas() == 0 { - gas = 200000 - } + if err := builder.SetSignatures(); err != nil { + return nil, nil, TxSignerInfo{}, fmt.Errorf("clear placeholder signature: %w", err) } - builder.SetGasLimit(gas) - err = builder.SetSignatures() // clear placeholder signature + feeAmount, err := c.resolveFeeAmount(gas, opts) if err != nil { - return nil, fmt.Errorf("clear placeholder signature: %w", err) + return nil, nil, TxSignerInfo{}, err } + builder.SetFeeAmount(feeAmount) - // Ensure a minimum fee to satisfy chain requirements - feeDec := c.config.GasPrice.MulInt64(int64(gas)).Ceil().TruncateInt() - minFee := sdk.NewCoins(sdk.NewCoin(c.config.FeeDenom, feeDec)) - builder.SetFeeAmount(minFee) + return txCfg, builder, signerInfo, nil +} - // 5) Sign with real credentials, overwriting placeholder +// SignPreparedTx signs a prepared tx builder using explicit signer info. +func (c *Client) SignPreparedTx(ctx context.Context, txCfg client.TxConfig, builder client.TxBuilder, signerInfo TxSignerInfo) ([]byte, error) { + if c.keyring == nil { + return nil, fmt.Errorf("keyring is required") + } + if strings.TrimSpace(c.keyName) == "" { + return nil, fmt.Errorf("key name is required") + } if err := sdkcrypto.SignTxWithKeyring( ctx, txCfg, c.keyring, c.keyName, builder, - c.config.ChainID, acctResp.Info.AccountNumber, acctResp.Info.Sequence, true, + c.config.ChainID, signerInfo.AccountNumber, signerInfo.Sequence, true, ); err != nil { return nil, fmt.Errorf("sign tx: %w", err) } - // 6) Encode signed tx signedBytes, err := txCfg.TxEncoder()(builder.GetTx()) if err != nil { return nil, fmt.Errorf("encode signed tx: %w", err) @@ -182,6 +211,128 @@ func (c *Client) buildAndSignTx(ctx context.Context, msg sdk.Msg, memo string, g return signedBytes, nil } +func (c *Client) validateTxBuildOptions(opts TxBuildOptions) error { + if c.keyring == nil { + return fmt.Errorf("keyring is required") + } + if strings.TrimSpace(c.keyName) == "" { + return fmt.Errorf("key name is required") + } + if len(opts.Messages) == 0 { + return fmt.Errorf("at least one message is required") + } + if strings.TrimSpace(c.config.AccountHRP) == "" { + return fmt.Errorf("account HRP is required") + } + if opts.ZeroFee && !opts.FeeAmount.Empty() { + return fmt.Errorf("zero fee cannot be combined with explicit fee amount") + } + if !opts.ZeroFee && opts.FeeAmount.Empty() { + if strings.TrimSpace(c.config.FeeDenom) == "" { + return fmt.Errorf("fee denom is required") + } + if c.config.GasPrice.IsNil() || c.config.GasPrice.IsZero() { + return fmt.Errorf("gas price is required") + } + } + for _, msg := range opts.Messages { + if sdk.MsgTypeURL(msg) == msgEthereumTxTypeURL { + return fmt.Errorf("MsgEthereumTx must use EVMClient.SendEthereumTransaction; the cosmos signing pipeline rejects it") + } + } + return nil +} + +func (c *Client) resolveSignerInfo(ctx context.Context, opts TxBuildOptions) (*keyring.Record, TxSignerInfo, error) { + rec, err := c.keyring.Key(c.keyName) + if err != nil { + return nil, TxSignerInfo{}, fmt.Errorf("load key %q: %w", c.keyName, err) + } + + info := TxSignerInfo{} + if opts.AccountNumber != nil { + info.AccountNumber = *opts.AccountNumber + } + if opts.Sequence != nil { + info.Sequence = *opts.Sequence + } + if opts.AccountNumber != nil && opts.Sequence != nil { + return rec, info, nil + } + + accAddr, err := sdkcrypto.AddressFromKey(c.keyring, c.keyName, c.config.AccountHRP) + if err != nil { + return nil, TxSignerInfo{}, fmt.Errorf("derive address for %q: %w", c.keyName, err) + } + + authq := authtypes.NewQueryClient(c.conn) + acctResp, err := authq.AccountInfo(ctx, &authtypes.QueryAccountInfoRequest{ + Address: accAddr, + }) + if err != nil { + return nil, TxSignerInfo{}, fmt.Errorf("query account info: %w", err) + } + if acctResp == nil || acctResp.Info == nil { + return nil, TxSignerInfo{}, fmt.Errorf("empty account info response") + } + if opts.AccountNumber == nil { + info.AccountNumber = acctResp.Info.AccountNumber + } + if opts.Sequence == nil { + info.Sequence = acctResp.Info.Sequence + } + + return rec, info, nil +} + +func (c *Client) resolveGasLimit(ctx context.Context, txCfg client.TxConfig, builder client.TxBuilder, opts TxBuildOptions) (uint64, error) { + if opts.GasLimit > 0 { + return opts.GasLimit, nil + } + if opts.SkipSimulation { + return defaultSignedTxGasLimit, nil + } + + unsignedBytes, err := txCfg.TxEncoder()(builder.GetTx()) + if err != nil { + return 0, fmt.Errorf("encode unsigned tx: %w", err) + } + + gasUsed, simErr := c.Simulate(ctx, unsignedBytes) + if simErr != nil { + // Don't silently fall back to a fixed gas limit: a failed simulation + // usually means the tx would fail on-chain, and guessing a default + // can under-gas it and produce an opaque on-chain error. Surface the + // reason and let callers opt out via SkipSimulation or an explicit + // GasLimit. + return 0, fmt.Errorf("simulate tx for gas estimation (set GasLimit or SkipSimulation to bypass): %w", simErr) + } + if gasUsed == 0 { + return defaultSignedTxGasLimit, nil + } + + gasAdjustment := opts.GasAdjustment + if gasAdjustment <= 0 { + gasAdjustment = 1.3 + } + gas := uint64(float64(gasUsed) * gasAdjustment) + if gas == 0 { + gas = gasUsed + } + return gas, nil +} + +func (c *Client) resolveFeeAmount(gas uint64, opts TxBuildOptions) (sdk.Coins, error) { + if opts.ZeroFee { + return nil, nil + } + if !opts.FeeAmount.Empty() { + return opts.FeeAmount, nil + } + feeDec := c.config.GasPrice.MulInt(sdkmath.NewIntFromUint64(gas)).Ceil().TruncateInt() + return sdk.NewCoins(sdk.NewCoin(c.config.FeeDenom, feeDec)), nil +} + // GetTx fetches a transaction by hash via the tx service. func (c *Client) GetTx(ctx context.Context, hash string) (*txtypes.GetTxResponse, error) { svc := txtypes.NewServiceClient(c.conn) diff --git a/blockchain/base/tx_test.go b/blockchain/base/tx_test.go index 49ac82c..158f390 100644 --- a/blockchain/base/tx_test.go +++ b/blockchain/base/tx_test.go @@ -3,13 +3,24 @@ package base import ( "context" "net" + "strings" "sync" "testing" "time" abcipb "cosmossdk.io/api/cosmos/base/abci/v1beta1" txtypes "cosmossdk.io/api/cosmos/tx/v1beta1" + sdkmath "cosmossdk.io/math" clientconfig "github.com/LumeraProtocol/sdk-go/client/config" + "github.com/LumeraProtocol/sdk-go/constants" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + evmtypes "github.com/cosmos/evm/x/vm/types" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials/insecure" @@ -29,6 +40,24 @@ type getTxStep struct { err error } +type txBuildServer struct { + txtypes.UnimplementedServiceServer + authtypes.UnimplementedQueryServer + mu sync.Mutex + gasUsed uint64 + simErr error + accountNumber uint64 + sequence uint64 + simulateCalls int + accountInfoCalls int +} + +type broadcastWaitServer struct { + txtypes.UnimplementedServiceServer + broadcast *abcipb.TxResponse + get *abcipb.TxResponse +} + func (s *getTxSequenceServer) GetTx(ctx context.Context, req *txtypes.GetTxRequest) (*txtypes.GetTxResponse, error) { s.mu.Lock() defer s.mu.Unlock() @@ -47,6 +76,44 @@ func (s *getTxSequenceServer) callCount() int { return s.calls } +func (s *txBuildServer) Simulate(context.Context, *txtypes.SimulateRequest) (*txtypes.SimulateResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.simulateCalls++ + if s.simErr != nil { + return nil, s.simErr + } + return &txtypes.SimulateResponse{ + GasInfo: &abcipb.GasInfo{GasUsed: s.gasUsed}, + }, nil +} + +func (s *txBuildServer) AccountInfo(context.Context, *authtypes.QueryAccountInfoRequest) (*authtypes.QueryAccountInfoResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.accountInfoCalls++ + return &authtypes.QueryAccountInfoResponse{ + Info: &authtypes.BaseAccount{ + AccountNumber: s.accountNumber, + Sequence: s.sequence, + }, + }, nil +} + +func (s *txBuildServer) counts() (simulateCalls int, accountInfoCalls int) { + s.mu.Lock() + defer s.mu.Unlock() + return s.simulateCalls, s.accountInfoCalls +} + +func (s *broadcastWaitServer) BroadcastTx(context.Context, *txtypes.BroadcastTxRequest) (*txtypes.BroadcastTxResponse, error) { + return &txtypes.BroadcastTxResponse{TxResponse: s.broadcast}, nil +} + +func (s *broadcastWaitServer) GetTx(context.Context, *txtypes.GetTxRequest) (*txtypes.GetTxResponse, error) { + return &txtypes.GetTxResponse{TxResponse: s.get}, nil +} + func TestWaitForTxInclusionRetriesNotFoundAfterWaitSuccess(t *testing.T) { const bufSize = 1024 * 1024 lis := bufconn.Listen(bufSize) @@ -113,3 +180,392 @@ func TestWaitForTxInclusionRetriesNotFoundAfterWaitSuccess(t *testing.T) { t.Fatalf("unexpected GetTx call count: got %d, want %d", got, want) } } + +func TestBuildAndSignTxWithOptions_ManualSignerInfo(t *testing.T) { + kr, addr := newSigningTestKeyring(t, "alice") + c := &Client{ + keyring: kr, + keyName: "alice", + config: Config{ + ChainID: "lumera-devnet-1", + AccountHRP: constants.LumeraAccountHRP, + FeeDenom: "ulume", + GasPrice: sdkmath.LegacyMustNewDecFromStr("0.025"), + }, + } + + accountNumber := uint64(7) + sequence := uint64(9) + txBytes, err := c.BuildAndSignTxWithOptions(context.Background(), TxBuildOptions{ + Messages: []sdk.Msg{newMsgSend(addr, addr)}, + Memo: "manual", + GasLimit: 250000, + SkipSimulation: true, + AccountNumber: &accountNumber, + Sequence: &sequence, + }) + if err != nil { + t.Fatalf("BuildAndSignTxWithOptions error: %v", err) + } + + assertDecodedTx(t, txBytes, 250000, 6250, sequence, 1) +} + +func TestBuildAndSignTxWithOptions_ZeroFee(t *testing.T) { + kr, addr := newSigningTestKeyring(t, "alice") + c := &Client{ + keyring: kr, + keyName: "alice", + config: Config{ + ChainID: "lumera-devnet-1", + AccountHRP: constants.LumeraAccountHRP, + }, + } + + accountNumber := uint64(7) + sequence := uint64(9) + txBytes, err := c.BuildAndSignTxWithOptions(context.Background(), TxBuildOptions{ + Messages: []sdk.Msg{newMsgSend(addr, addr)}, + GasLimit: 250000, + SkipSimulation: true, + AccountNumber: &accountNumber, + Sequence: &sequence, + ZeroFee: true, + }) + if err != nil { + t.Fatalf("BuildAndSignTxWithOptions zero fee error: %v", err) + } + + assertDecodedTx(t, txBytes, 250000, 0, sequence, 1) +} + +func TestBroadcastAndWait_ChecksIncludedTxCode(t *testing.T) { + const bufSize = 1024 * 1024 + lis := bufconn.Listen(bufSize) + srv := grpc.NewServer() + t.Cleanup(func() { + srv.Stop() + _ = lis.Close() + }) + + handler := &broadcastWaitServer{ + broadcast: &abcipb.TxResponse{Txhash: "hash"}, + get: &abcipb.TxResponse{Txhash: "hash", Code: 7, RawLog: "deliver failed"}, + } + txtypes.RegisterServiceServer(srv, handler) + go func() { + _ = srv.Serve(lis) + }() + + conn, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("dial bufnet: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + c := &Client{ + conn: conn, + config: Config{ + WaitTx: clientconfig.WaitTxConfig{ + PollInterval: time.Millisecond, + PollMaxRetries: 1, + PollBackoffMultiplier: 1, + }, + }, + } + + _, _, err = c.BroadcastAndWait(context.Background(), []byte{0x01}, txtypes.BroadcastMode_BROADCAST_MODE_SYNC) + if err == nil { + t.Fatalf("expected included tx code error") + } + if !strings.Contains(err.Error(), "tx failed with code 7") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestBuildAndSignTxWithOptions_QueriesAccountInfoAndSimulates(t *testing.T) { + const bufSize = 1024 * 1024 + lis := bufconn.Listen(bufSize) + srv := grpc.NewServer() + t.Cleanup(func() { + srv.Stop() + _ = lis.Close() + }) + + handler := &txBuildServer{ + gasUsed: 2000, + accountNumber: 3, + sequence: 4, + } + txtypes.RegisterServiceServer(srv, handler) + authtypes.RegisterQueryServer(srv, handler) + go func() { + _ = srv.Serve(lis) + }() + + conn, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("dial bufnet: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + kr, addr := newSigningTestKeyring(t, "alice") + c := &Client{ + conn: conn, + keyring: kr, + keyName: "alice", + config: Config{ + ChainID: "lumera-devnet-1", + AccountHRP: constants.LumeraAccountHRP, + FeeDenom: "ulume", + GasPrice: sdkmath.LegacyMustNewDecFromStr("0.025"), + }, + } + + txBytes, err := c.BuildAndSignTxWithOptions(context.Background(), TxBuildOptions{ + Messages: []sdk.Msg{newMsgSend(addr, addr)}, + Memo: "queried", + GasAdjustment: 1.5, + }) + if err != nil { + t.Fatalf("BuildAndSignTxWithOptions error: %v", err) + } + + assertDecodedTx(t, txBytes, 3000, 75, 4, 1) + simCalls, acctCalls := handler.counts() + if simCalls != 1 || acctCalls != 1 { + t.Fatalf("unexpected query/simulate call counts: simulate=%d account_info=%d", simCalls, acctCalls) + } +} + +func TestBuildAndSignTxWithOptions_SimulationErrorPropagates(t *testing.T) { + const bufSize = 1024 * 1024 + lis := bufconn.Listen(bufSize) + srv := grpc.NewServer() + t.Cleanup(func() { + srv.Stop() + _ = lis.Close() + }) + + handler := &txBuildServer{ + simErr: status.Error(codes.InvalidArgument, "insufficient funds"), + accountNumber: 3, + sequence: 4, + } + txtypes.RegisterServiceServer(srv, handler) + authtypes.RegisterQueryServer(srv, handler) + go func() { + _ = srv.Serve(lis) + }() + + conn, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("dial bufnet: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + kr, addr := newSigningTestKeyring(t, "alice") + c := &Client{ + conn: conn, + keyring: kr, + keyName: "alice", + config: Config{ + ChainID: "lumera-devnet-1", + AccountHRP: constants.LumeraAccountHRP, + FeeDenom: "ulume", + GasPrice: sdkmath.LegacyMustNewDecFromStr("0.025"), + }, + } + + // A failed simulation must surface as an error, not silently fall back to + // the fixed default gas limit (which could under-gas the tx on-chain). + _, err = c.BuildAndSignTxWithOptions(context.Background(), TxBuildOptions{ + Messages: []sdk.Msg{newMsgSend(addr, addr)}, + }) + if err == nil { + t.Fatal("expected simulation error to propagate, got nil") + } + if !strings.Contains(err.Error(), "insufficient funds") { + t.Fatalf("error %q should include the simulation failure reason", err) + } + + // Setting an explicit gas limit must bypass simulation and succeed. + handler2 := &txBuildServer{accountNumber: 3, sequence: 4} + lis2 := bufconn.Listen(bufSize) + srv2 := grpc.NewServer() + t.Cleanup(func() { + srv2.Stop() + _ = lis2.Close() + }) + txtypes.RegisterServiceServer(srv2, handler2) + authtypes.RegisterQueryServer(srv2, handler2) + go func() { _ = srv2.Serve(lis2) }() + conn2, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { return lis2.Dial() }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("dial bufnet: %v", err) + } + t.Cleanup(func() { _ = conn2.Close() }) + c.conn = conn2 + + if _, err := c.BuildAndSignTxWithOptions(context.Background(), TxBuildOptions{ + Messages: []sdk.Msg{newMsgSend(addr, addr)}, + GasLimit: 50_000, + }); err != nil { + t.Fatalf("explicit GasLimit should bypass simulation: %v", err) + } + if sim, _ := handler2.counts(); sim != 0 { + t.Fatalf("explicit GasLimit should not call Simulate, got %d calls", sim) + } +} + +func TestValidateTxBuildOptions_RejectsMsgEthereumTx(t *testing.T) { + kr, _ := newSigningTestKeyring(t, "alice") + c := &Client{ + keyring: kr, + keyName: "alice", + config: Config{ + ChainID: "lumera-devnet-1", + AccountHRP: constants.LumeraAccountHRP, + FeeDenom: "ulume", + GasPrice: sdkmath.LegacyMustNewDecFromStr("0.025"), + }, + } + _, err := c.BuildAndSignTxWithOptions(context.Background(), TxBuildOptions{ + Messages: []sdk.Msg{&evmtypes.MsgEthereumTx{}}, + }) + if err == nil { + t.Fatalf("expected error rejecting MsgEthereumTx") + } + if !strings.Contains(err.Error(), "EVMClient.SendEthereumTransaction") { + t.Fatalf("expected guard error, got %v", err) + } +} + +func TestResolveFeeAmount_LargeGasDoesNotOverflow(t *testing.T) { + c := &Client{config: Config{ + FeeDenom: "ulume", + GasPrice: sdkmath.LegacyOneDec(), + }} + + gas := ^uint64(0) + fees, err := c.resolveFeeAmount(gas, TxBuildOptions{}) + if err != nil { + t.Fatalf("resolveFeeAmount: %v", err) + } + if len(fees) != 1 || fees[0].Denom != "ulume" { + t.Fatalf("unexpected fees: %s", fees) + } + if !fees[0].Amount.Equal(sdkmath.NewIntFromUint64(gas)) { + t.Fatalf("fee amount = %s, want %s", fees[0].Amount, sdkmath.NewIntFromUint64(gas)) + } +} + +func TestNewAppliesDefaultMessageSizes(t *testing.T) { + c, err := New(context.Background(), Config{GRPCAddr: "localhost:9090"}, nil, "") + if err != nil { + t.Fatalf("New error: %v", err) + } + t.Cleanup(func() { _ = c.Close() }) + + if c.config.MaxRecvMsgSize != defaultMaxRecvMessageSize || c.config.MaxSendMsgSize != defaultMaxSendMessageSize { + t.Fatalf("unexpected max message sizes: recv=%d send=%d", c.config.MaxRecvMsgSize, c.config.MaxSendMsgSize) + } +} + +func newSigningTestKeyring(t *testing.T, keyName string) (keyring.Keyring, string) { + t.Helper() + + kr, err := sdkcrypto.NewKeyring(sdkcrypto.KeyringParams{ + AppName: "lumera", + Backend: "test", + Dir: t.TempDir(), + Input: strings.NewReader(""), + }) + if err != nil { + t.Fatalf("create keyring: %v", err) + } + + mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + if _, err := kr.NewAccount(keyName, mnemonic, "", sdk.FullFundraiserPath, hd.Secp256k1); err != nil { + t.Fatalf("new account: %v", err) + } + + addr, err := sdkcrypto.AddressFromKey(kr, keyName, constants.LumeraAccountHRP) + if err != nil { + t.Fatalf("derive address: %v", err) + } + + return kr, addr +} + +func newMsgSend(fromAddr, toAddr string) *banktypes.MsgSend { + return &banktypes.MsgSend{ + FromAddress: fromAddr, + ToAddress: toAddr, + Amount: sdk.NewCoins(sdk.NewInt64Coin("ulume", 1)), + } +} + +func assertDecodedTx(t *testing.T, txBytes []byte, wantGas uint64, wantFee int64, wantSequence uint64, wantMsgs int) { + t.Helper() + + txCfg := sdkcrypto.NewDefaultTxConfig() + decoded, err := txCfg.TxDecoder()(txBytes) + if err != nil { + t.Fatalf("decode tx: %v", err) + } + + feeTx, ok := decoded.(sdk.FeeTx) + if !ok { + t.Fatalf("decoded tx does not implement sdk.FeeTx") + } + if feeTx.GetGas() != wantGas { + t.Fatalf("unexpected gas: got %d want %d", feeTx.GetGas(), wantGas) + } + fee := feeTx.GetFee() + if wantFee == 0 { + if !fee.Empty() { + t.Fatalf("unexpected fee: %s", fee) + } + } else if len(fee) != 1 || fee[0].Denom != "ulume" || !fee[0].Amount.Equal(sdkmath.NewInt(wantFee)) { + t.Fatalf("unexpected fee: %s", fee) + } + + sigTx, ok := decoded.(authsigning.SigVerifiableTx) + if !ok { + t.Fatalf("decoded tx does not implement authsigning.SigVerifiableTx") + } + sigs, err := sigTx.GetSignaturesV2() + if err != nil { + t.Fatalf("get signatures: %v", err) + } + if len(sigs) != 1 || sigs[0].Sequence != wantSequence { + t.Fatalf("unexpected signatures: %+v", sigs) + } + + if got := len(decoded.GetMsgs()); got != wantMsgs { + t.Fatalf("unexpected msg count: got %d want %d", got, wantMsgs) + } +} diff --git a/blockchain/client.go b/blockchain/client.go index 0e3c2a7..05550e6 100644 --- a/blockchain/client.go +++ b/blockchain/client.go @@ -7,12 +7,21 @@ import ( sdkmath "cosmossdk.io/math" "github.com/LumeraProtocol/sdk-go/blockchain/base" "github.com/LumeraProtocol/sdk-go/constants" + sdkgrpc "github.com/LumeraProtocol/sdk-go/internal/grpc" "github.com/cosmos/cosmos-sdk/crypto/keyring" actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" //audittypes "github.com/LumeraProtocol/lumera/x/audit/types" claimtypes "github.com/LumeraProtocol/lumera/x/claim/types" + evmigrationtypes "github.com/LumeraProtocol/lumera/x/evmigration/types" supernodetypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types" + "github.com/LumeraProtocol/sdk-go/pkg/evm/precompiles" + erc20types "github.com/cosmos/evm/x/erc20/types" + feemarkettypes "github.com/cosmos/evm/x/feemarket/types" + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" ) // Config mirrors the base blockchain config for Lumera-specific usage. @@ -23,10 +32,17 @@ type Client struct { *base.Client // Module-specific clients - Action *ActionClient - SuperNode *SuperNodeClient - Claim *ClaimClient - Audit *AuditClient + Action *ActionClient + SuperNode *SuperNodeClient + Claim *ClaimClient + EVMigration *EVMigrationClient + Audit *AuditClient + + // EVM module clients (cosmos/evm) + EVM *EVMClient + ERC20 *ERC20Client + FeeMarket *FeeMarketClient + PreciseBank *PreciseBankClient } // New creates a new Lumera blockchain client. @@ -47,19 +63,45 @@ func New(ctx context.Context, cfg Config, kr keyring.Keyring, keyName string) (* } conn := baseClient.GRPCConn() - return &Client{ + gogoConn := sdkgrpc.GogoClientConn(conn) + c := &Client{ Client: baseClient, Action: &ActionClient{ - query: actiontypes.NewQueryClient(conn), + query: actiontypes.NewQueryClient(gogoConn), }, SuperNode: &SuperNodeClient{ - query: supernodetypes.NewQueryClient(conn), + query: supernodetypes.NewQueryClient(gogoConn), }, Claim: &ClaimClient{ - query: claimtypes.NewQueryClient(conn), + query: claimtypes.NewQueryClient(gogoConn), + }, + EVMigration: &EVMigrationClient{ + query: evmigrationtypes.NewQueryClient(gogoConn), }, Audit: &AuditClient{ //query: audittypes.NewQueryClient(conn), }, - }, nil + EVM: &EVMClient{ + query: evmtypes.NewQueryClient(gogoConn), + }, + ERC20: &ERC20Client{ + query: erc20types.NewQueryClient(gogoConn), + }, + FeeMarket: &FeeMarketClient{ + query: feemarkettypes.NewQueryClient(gogoConn), + }, + PreciseBank: &PreciseBankClient{ + query: precisebanktypes.NewQueryClient(gogoConn), + }, + } + c.EVM.client = c + c.ERC20.client = c + c.EVM.Action = newPrecompileClient(c.EVM, precompiles.ActionAddress, precompiles.ActionABI) + c.EVM.Supernode = newPrecompileClient(c.EVM, precompiles.SupernodeAddress, precompiles.SupernodeABI) + c.EVM.Wasm = newPrecompileClient(c.EVM, precompiles.WasmAddress, precompiles.WasmABI) + return c, nil +} + +func newPrecompileClient(evm *EVMClient, addr common.Address, parsed abi.ABI) *PrecompileClient { + return &PrecompileClient{address: addr, abi: parsed, evm: evm} } diff --git a/blockchain/erc20.go b/blockchain/erc20.go new file mode 100644 index 0000000..91b18ce --- /dev/null +++ b/blockchain/erc20.go @@ -0,0 +1,209 @@ +package blockchain + +import ( + "context" + "fmt" + + txtypes "cosmossdk.io/api/cosmos/tx/v1beta1" + sdkmath "cosmossdk.io/math" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + erc20types "github.com/cosmos/evm/x/erc20/types" + "github.com/ethereum/go-ethereum/common" +) + +// ERC20Client provides x/erc20 module operations: queries plus opinionated +// tx helpers for the Cosmos<->ERC20 conversion flows. +type ERC20Client struct { + query erc20types.QueryClient + client *Client // backref for tx helpers +} + +// ConversionResult captures the outcome of a coin <-> ERC20 conversion. +type ConversionResult struct { + From string + To string + Amount sdkmath.Int + TxHash string + Height int64 +} + +// --- queries --- + +// TokenPairs returns the registered token pairs with optional pagination. +func (c *ERC20Client) TokenPairs(ctx context.Context, pagination *query.PageRequest) ([]erc20types.TokenPair, *query.PageResponse, error) { + resp, err := c.query.TokenPairs(ctx, &erc20types.QueryTokenPairsRequest{Pagination: pagination}) + if err != nil { + return nil, nil, err + } + if resp == nil { + return nil, nil, nil + } + return resp.TokenPairs, resp.Pagination, nil +} + +// TokenPair returns the registered pair for a given denom or 0x contract. +func (c *ERC20Client) TokenPair(ctx context.Context, token string) (erc20types.TokenPair, error) { + resp, err := c.query.TokenPair(ctx, &erc20types.QueryTokenPairRequest{Token: token}) + if err != nil { + return erc20types.TokenPair{}, err + } + if resp == nil { + return erc20types.TokenPair{}, fmt.Errorf("empty token pair response") + } + return resp.TokenPair, nil +} + +// Params returns the current x/erc20 module parameters. +func (c *ERC20Client) Params(ctx context.Context) (*erc20types.QueryParamsResponse, error) { + return c.query.Params(ctx, &erc20types.QueryParamsRequest{}) +} + +// --- message constructors --- + +// NewMsgConvertCoin builds a MsgConvertCoin (cosmos coin -> ERC20). +func NewMsgConvertCoin(coin sdk.Coin, receiver common.Address, sender string) *erc20types.MsgConvertCoin { + return &erc20types.MsgConvertCoin{ + Coin: coin, + Receiver: receiver.Hex(), + Sender: sender, + } +} + +// NewMsgConvertERC20 builds a MsgConvertERC20 (ERC20 -> cosmos coin). +func NewMsgConvertERC20(amount sdkmath.Int, receiver string, contract, sender common.Address) *erc20types.MsgConvertERC20 { + return &erc20types.MsgConvertERC20{ + ContractAddress: contract.Hex(), + Amount: amount, + Receiver: receiver, + Sender: sender.Hex(), + } +} + +// NewMsgRegisterERC20 builds a MsgRegisterERC20 (governance-gated). +func NewMsgRegisterERC20(signer string, contracts []common.Address) *erc20types.MsgRegisterERC20 { + addrs := make([]string, len(contracts)) + for i, a := range contracts { + addrs[i] = a.Hex() + } + return &erc20types.MsgRegisterERC20{ + Signer: signer, + Erc20Addresses: addrs, + } +} + +// NewMsgToggleConversion builds a MsgToggleConversion (governance-gated). +func NewMsgToggleConversion(authority, token string) *erc20types.MsgToggleConversion { + return &erc20types.MsgToggleConversion{ + Authority: authority, + Token: token, + } +} + +// --- transaction helpers --- + +// ConvertCoinToERC20 wraps cosmos coins as ERC20 tokens. Receiver is the EVM +// (0x) address that gets the wrapped ERC20. Sender is the client's signer. +func (c *Client) ConvertCoinToERC20( + ctx context.Context, + coin sdk.Coin, + receiver common.Address, + memo string, +) (*ConversionResult, error) { + if c == nil || c.Client == nil { + return nil, fmt.Errorf("client not initialized") + } + sender, err := sdkcrypto.AddressFromKey(c.Keyring(), c.KeyName(), c.Cfg().AccountHRP) + if err != nil { + return nil, fmt.Errorf("derive sender address: %w", err) + } + + msg := NewMsgConvertCoin(coin, receiver, sender) + return c.broadcastConversion(ctx, msg, memo, sender, receiver.Hex(), coin.Amount) +} + +// ConvertERC20ToCoin unwraps ERC20 tokens to native cosmos coins. Receiver is +// the cosmos bech32 address; sender is the 0x address derived from the +// client's eth_secp256k1 signing key. +func (c *Client) ConvertERC20ToCoin( + ctx context.Context, + contract common.Address, + amount sdkmath.Int, + receiver string, + memo string, +) (*ConversionResult, error) { + if c == nil || c.Client == nil { + return nil, fmt.Errorf("client not initialized") + } + senderHex, err := sdkcrypto.EVMAddressFromKey(c.Keyring(), c.KeyName()) + if err != nil { + return nil, fmt.Errorf("derive sender 0x address: %w", err) + } + sender := common.HexToAddress(senderHex) + + msg := NewMsgConvertERC20(amount, receiver, contract, sender) + return c.broadcastConversion(ctx, msg, memo, senderHex, receiver, amount) +} + +// RegisterERC20Tx wraps MsgRegisterERC20 and broadcasts it. The chain may +// permit permissionless registration; if it requires governance, the signer +// must be the module authority. +func (c *Client) RegisterERC20Tx( + ctx context.Context, + contracts []common.Address, + memo string, +) (string, error) { + signer, err := sdkcrypto.AddressFromKey(c.Keyring(), c.KeyName(), c.Cfg().AccountHRP) + if err != nil { + return "", fmt.Errorf("derive signer: %w", err) + } + msg := NewMsgRegisterERC20(signer, contracts) + txBytes, err := c.BuildAndSignTx(ctx, msg, memo) + if err != nil { + return "", fmt.Errorf("build and sign tx: %w", err) + } + txHash, _, err := c.BroadcastAndWait(ctx, txBytes, txtypes.BroadcastMode_BROADCAST_MODE_SYNC) + return txHash, err +} + +// ToggleConversionTx wraps MsgToggleConversion (governance-gated). +func (c *Client) ToggleConversionTx( + ctx context.Context, + authority, token, memo string, +) (string, error) { + msg := NewMsgToggleConversion(authority, token) + txBytes, err := c.BuildAndSignTx(ctx, msg, memo) + if err != nil { + return "", fmt.Errorf("build and sign tx: %w", err) + } + txHash, _, err := c.BroadcastAndWait(ctx, txBytes, txtypes.BroadcastMode_BROADCAST_MODE_SYNC) + return txHash, err +} + +func (c *Client) broadcastConversion( + ctx context.Context, + msg sdk.Msg, + memo string, + from, to string, + amount sdkmath.Int, +) (*ConversionResult, error) { + txBytes, err := c.BuildAndSignTx(ctx, msg, memo) + if err != nil { + return nil, fmt.Errorf("build and sign tx: %w", err) + } + txHash, resp, err := c.BroadcastAndWait(ctx, txBytes, txtypes.BroadcastMode_BROADCAST_MODE_SYNC) + if err != nil { + return nil, fmt.Errorf("broadcast and wait: %w", err) + } + res := &ConversionResult{ + From: from, + To: to, + Amount: amount, + TxHash: txHash, + } + if resp != nil && resp.TxResponse != nil { + res.Height = resp.TxResponse.Height + } + return res, nil +} diff --git a/blockchain/erc20_abi.go b/blockchain/erc20_abi.go new file mode 100644 index 0000000..11335f3 --- /dev/null +++ b/blockchain/erc20_abi.go @@ -0,0 +1,156 @@ +package blockchain + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +// Minimal ERC20 ABI covering the methods the SDK exposes via Erc20* sugar. +// We hand-roll the JSON rather than embed a vendored file so we do not have +// to ship contract artifacts in the SDK. +const erc20MinimalABI = `[ +{"constant":true,"inputs":[{"name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"","type":"uint256"}],"type":"function"}, +{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"type":"function"}, +{"constant":true,"inputs":[{"name":"owner","type":"address"},{"name":"spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"type":"function"}, +{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"type":"function"}, +{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"type":"function"}, +{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"type":"function"} +]` + +var erc20ABI abi.ABI + +func init() { + parsed, err := abi.JSON(strings.NewReader(erc20MinimalABI)) + if err != nil { + panic(fmt.Sprintf("parse erc20 abi: %v", err)) + } + erc20ABI = parsed +} + +// Erc20Metadata bundles the static ERC20 metadata fields. +type Erc20Metadata struct { + Name string + Symbol string + Decimals uint8 +} + +// Erc20Balance calls balanceOf(holder) on the ERC20 contract. +func (c *ERC20Client) Erc20Balance(ctx context.Context, contract, holder common.Address) (*big.Int, error) { + data, err := erc20ABI.Pack("balanceOf", holder) + if err != nil { + return nil, fmt.Errorf("pack balanceOf: %w", err) + } + return c.callUint256(ctx, contract, "balanceOf", data) +} + +// Erc20TotalSupply calls totalSupply() on the ERC20 contract. +func (c *ERC20Client) Erc20TotalSupply(ctx context.Context, contract common.Address) (*big.Int, error) { + data, err := erc20ABI.Pack("totalSupply") + if err != nil { + return nil, fmt.Errorf("pack totalSupply: %w", err) + } + return c.callUint256(ctx, contract, "totalSupply", data) +} + +// Erc20Allowance calls allowance(owner, spender) on the ERC20 contract. +func (c *ERC20Client) Erc20Allowance(ctx context.Context, contract, owner, spender common.Address) (*big.Int, error) { + data, err := erc20ABI.Pack("allowance", owner, spender) + if err != nil { + return nil, fmt.Errorf("pack allowance: %w", err) + } + return c.callUint256(ctx, contract, "allowance", data) +} + +// Erc20Metadata returns name, symbol, decimals for the ERC20 contract. +func (c *ERC20Client) Erc20Metadata(ctx context.Context, contract common.Address) (Erc20Metadata, error) { + name, err := c.callString(ctx, contract, "name") + if err != nil { + return Erc20Metadata{}, fmt.Errorf("name: %w", err) + } + symbol, err := c.callString(ctx, contract, "symbol") + if err != nil { + return Erc20Metadata{}, fmt.Errorf("symbol: %w", err) + } + decimals, err := c.callUint8(ctx, contract, "decimals") + if err != nil { + return Erc20Metadata{}, fmt.Errorf("decimals: %w", err) + } + return Erc20Metadata{Name: name, Symbol: symbol, Decimals: decimals}, nil +} + +func (c *ERC20Client) callContract(ctx context.Context, contract common.Address, data []byte) ([]byte, error) { + if c.client == nil || c.client.EVM == nil { + return nil, fmt.Errorf("ERC20Client not wired to an EVM client") + } + return c.client.EVM.CallContract(ctx, contract, data) +} + +func (c *ERC20Client) callUint256(ctx context.Context, contract common.Address, method string, data []byte) (*big.Int, error) { + ret, err := c.callContract(ctx, contract, data) + if err != nil { + return nil, err + } + if len(ret) == 0 { + return nil, fmt.Errorf("%s: contract %s returned no data (address has no code or call reverted)", method, contract) + } + out, err := erc20ABI.Unpack(method, ret) + if err != nil { + return nil, fmt.Errorf("unpack %s: %w", method, err) + } + bn, ok := out[0].(*big.Int) + if !ok { + return nil, fmt.Errorf("unexpected return type for %s: %T", method, out[0]) + } + return bn, nil +} + +func (c *ERC20Client) callString(ctx context.Context, contract common.Address, method string) (string, error) { + data, err := erc20ABI.Pack(method) + if err != nil { + return "", fmt.Errorf("pack %s: %w", method, err) + } + ret, err := c.callContract(ctx, contract, data) + if err != nil { + return "", err + } + if len(ret) == 0 { + return "", fmt.Errorf("%s: contract %s returned no data (address has no code or call reverted)", method, contract) + } + out, err := erc20ABI.Unpack(method, ret) + if err != nil { + return "", fmt.Errorf("unpack %s: %w", method, err) + } + s, ok := out[0].(string) + if !ok { + return "", fmt.Errorf("unexpected return type for %s: %T", method, out[0]) + } + return s, nil +} + +func (c *ERC20Client) callUint8(ctx context.Context, contract common.Address, method string) (uint8, error) { + data, err := erc20ABI.Pack(method) + if err != nil { + return 0, fmt.Errorf("pack %s: %w", method, err) + } + ret, err := c.callContract(ctx, contract, data) + if err != nil { + return 0, err + } + if len(ret) == 0 { + return 0, fmt.Errorf("%s: contract %s returned no data (address has no code or call reverted)", method, contract) + } + out, err := erc20ABI.Unpack(method, ret) + if err != nil { + return 0, fmt.Errorf("unpack %s: %w", method, err) + } + v, ok := out[0].(uint8) + if !ok { + return 0, fmt.Errorf("unexpected return type for %s: %T", method, out[0]) + } + return v, nil +} diff --git a/blockchain/erc20_test.go b/blockchain/erc20_test.go new file mode 100644 index 0000000..5e41a02 --- /dev/null +++ b/blockchain/erc20_test.go @@ -0,0 +1,203 @@ +package blockchain + +import ( + "context" + "math/big" + "strings" + "testing" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/query" + erc20types "github.com/cosmos/evm/x/erc20/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/ethereum/go-ethereum/common" + "google.golang.org/grpc" +) + +type stubERC20Query struct { + pairs *erc20types.QueryTokenPairsResponse + pair *erc20types.QueryTokenPairResponse + params *erc20types.QueryParamsResponse + lastReq string + lastPage *query.PageRequest +} + +func (s *stubERC20Query) TokenPairs(_ context.Context, req *erc20types.QueryTokenPairsRequest, _ ...grpc.CallOption) (*erc20types.QueryTokenPairsResponse, error) { + s.lastPage = req.Pagination + return s.pairs, nil +} +func (s *stubERC20Query) TokenPair(_ context.Context, req *erc20types.QueryTokenPairRequest, _ ...grpc.CallOption) (*erc20types.QueryTokenPairResponse, error) { + s.lastReq = req.Token + return s.pair, nil +} +func (s *stubERC20Query) Params(_ context.Context, _ *erc20types.QueryParamsRequest, _ ...grpc.CallOption) (*erc20types.QueryParamsResponse, error) { + return s.params, nil +} + +func TestERC20Client_Queries(t *testing.T) { + stub := &stubERC20Query{ + pairs: &erc20types.QueryTokenPairsResponse{ + TokenPairs: []erc20types.TokenPair{{ + Erc20Address: "0x0000000000000000000000000000000000000001", + Denom: "ulume", + Enabled: true, + }}, + }, + pair: &erc20types.QueryTokenPairResponse{ + TokenPair: erc20types.TokenPair{ + Erc20Address: "0x0000000000000000000000000000000000000001", + Denom: "ulume", + }, + }, + params: &erc20types.QueryParamsResponse{ + Params: erc20types.Params{EnableErc20: true}, + }, + } + c := &ERC20Client{query: stub} + ctx := context.Background() + + pairs, _, err := c.TokenPairs(ctx, &query.PageRequest{Limit: 10}) + if err != nil || len(pairs) != 1 || pairs[0].Denom != "ulume" { + t.Fatalf("TokenPairs: %v %+v", err, pairs) + } + if stub.lastPage == nil || stub.lastPage.Limit != 10 { + t.Fatalf("pagination not forwarded: %+v", stub.lastPage) + } + + pair, err := c.TokenPair(ctx, "ulume") + if err != nil || pair.Denom != "ulume" { + t.Fatalf("TokenPair: %v %+v", err, pair) + } + if stub.lastReq != "ulume" { + t.Fatalf("server saw %q", stub.lastReq) + } + + p, err := c.Params(ctx) + if err != nil || !p.Params.EnableErc20 { + t.Fatalf("Params: %v %+v", err, p) + } +} + +// When CallContract returns empty data (the target address has no contract +// code, or the call reverted without data), the ERC-20 read helpers must +// surface a clear error rather than a cryptic ABI "empty string" error or a +// silent zero value. +func TestErc20Reads_EmptyReturnData(t *testing.T) { + evm := &EVMClient{query: &stubEVMQuery{ethCallResp: &evmtypes.MsgEthereumTxResponse{}}} + c := &ERC20Client{client: &Client{EVM: evm}} + ctx := context.Background() + contract := common.HexToAddress("0x0000000000000000000000000000000000000abc") + holder := common.HexToAddress("0x0000000000000000000000000000000000000def") + + if _, err := c.Erc20Balance(ctx, contract, holder); err == nil || !strings.Contains(err.Error(), "no data") { + t.Fatalf("Erc20Balance empty-return: got %v, want error containing \"no data\"", err) + } + if _, err := c.Erc20TotalSupply(ctx, contract); err == nil || !strings.Contains(err.Error(), "no data") { + t.Fatalf("Erc20TotalSupply empty-return: got %v, want error containing \"no data\"", err) + } + if _, err := c.Erc20Metadata(ctx, contract); err == nil || !strings.Contains(err.Error(), "no data") { + t.Fatalf("Erc20Metadata empty-return: got %v, want error containing \"no data\"", err) + } +} + +func TestErc20Reads_UnwiredClientReturnsError(t *testing.T) { + ctx := context.Background() + contract := common.HexToAddress("0x0000000000000000000000000000000000000abc") + holder := common.HexToAddress("0x0000000000000000000000000000000000000def") + + for name, c := range map[string]*ERC20Client{ + "nil base client": {}, + "nil EVM client": {client: &Client{}}, + } { + if _, err := c.Erc20Balance(ctx, contract, holder); err == nil || !strings.Contains(err.Error(), "not wired") { + t.Fatalf("%s Erc20Balance: got %v, want not-wired error", name, err) + } + if _, err := c.Erc20Metadata(ctx, contract); err == nil || !strings.Contains(err.Error(), "not wired") { + t.Fatalf("%s Erc20Metadata: got %v, want not-wired error", name, err) + } + } +} + +func TestNewMsgConvertCoin(t *testing.T) { + coin := sdk.NewCoin("ulume", sdkmath.NewInt(1_000_000)) + receiver := common.HexToAddress("0x1234567890123456789012345678901234567890") + msg := NewMsgConvertCoin(coin, receiver, "lumera1sender") + + if msg.Sender != "lumera1sender" { + t.Fatalf("sender mismatch: %s", msg.Sender) + } + if msg.Receiver != receiver.Hex() { + t.Fatalf("receiver mismatch: %s vs %s", msg.Receiver, receiver.Hex()) + } + if msg.Coin.Denom != "ulume" || msg.Coin.Amount.Int64() != 1_000_000 { + t.Fatalf("coin mismatch: %+v", msg.Coin) + } +} + +func TestNewMsgConvertERC20(t *testing.T) { + contract := common.HexToAddress("0x000000000000000000000000000000000000aaaa") + sender := common.HexToAddress("0x000000000000000000000000000000000000bbbb") + msg := NewMsgConvertERC20(sdkmath.NewInt(42), "lumera1recv", contract, sender) + + if msg.ContractAddress != contract.Hex() { + t.Fatalf("contract mismatch: %s", msg.ContractAddress) + } + if msg.Sender != sender.Hex() { + t.Fatalf("sender mismatch: %s", msg.Sender) + } + if msg.Receiver != "lumera1recv" { + t.Fatalf("receiver mismatch: %s", msg.Receiver) + } + if msg.Amount.Int64() != 42 { + t.Fatalf("amount mismatch: %s", msg.Amount) + } +} + +func TestNewMsgRegisterERC20(t *testing.T) { + addrs := []common.Address{ + common.HexToAddress("0x0000000000000000000000000000000000000001"), + common.HexToAddress("0x0000000000000000000000000000000000000002"), + } + msg := NewMsgRegisterERC20("lumera1auth", addrs) + if msg.Signer != "lumera1auth" { + t.Fatalf("signer mismatch: %s", msg.Signer) + } + if len(msg.Erc20Addresses) != 2 { + t.Fatalf("addrs len: %d", len(msg.Erc20Addresses)) + } + if msg.Erc20Addresses[0] != addrs[0].Hex() { + t.Fatalf("addr mismatch: %s", msg.Erc20Addresses[0]) + } +} + +func TestNewMsgToggleConversion(t *testing.T) { + msg := NewMsgToggleConversion("lumera1auth", "ulume") + if msg.Authority != "lumera1auth" || msg.Token != "ulume" { + t.Fatalf("unexpected msg: %+v", msg) + } +} + +func TestErc20ABI_PackUnpackUint256(t *testing.T) { + // Confirm the embedded ABI can pack balanceOf and unpack a uint256 result. + holder := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + calldata, err := erc20ABI.Pack("balanceOf", holder) + if err != nil { + t.Fatalf("pack: %v", err) + } + // First 4 bytes = selector; remaining must end with the holder address. + if len(calldata) != 4+32 { + t.Fatalf("calldata len %d, want 36", len(calldata)) + } + + // Synthesize a 32-byte uint256 = 12345 and unpack. + ret := make([]byte, 32) + big.NewInt(12345).FillBytes(ret) + out, err := erc20ABI.Unpack("balanceOf", ret) + if err != nil { + t.Fatalf("unpack: %v", err) + } + if got := out[0].(*big.Int).Int64(); got != 12345 { + t.Fatalf("unpack got %d", got) + } +} diff --git a/blockchain/evm.go b/blockchain/evm.go new file mode 100644 index 0000000..1a93603 --- /dev/null +++ b/blockchain/evm.go @@ -0,0 +1,603 @@ +package blockchain + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "math/big" + "strings" + + txtypes "cosmossdk.io/api/cosmos/tx/v1beta1" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + ethtypes "github.com/ethereum/go-ethereum/core/types" + ethcrypto "github.com/ethereum/go-ethereum/crypto" +) + +// EVMClient provides x/vm module query operations plus high-level Ethereum +// transaction helpers (SendEthereumTransaction, DeployContract, CallContract, +// RawEthereumTx). +type EVMClient struct { + query evmtypes.QueryClient + client *Client // backref for tx helpers (nil for query-only tests) + + // Lumera precompile wrappers. Each routes through this EVMClient. + Action *PrecompileClient + Supernode *PrecompileClient + Wasm *PrecompileClient +} + +// Code returns the EVM bytecode deployed at addr. +func (c *EVMClient) Code(ctx context.Context, addr common.Address) ([]byte, error) { + resp, err := c.query.Code(ctx, &evmtypes.QueryCodeRequest{Address: addr.Hex()}) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + return resp.Code, nil +} + +// Storage returns the storage slot value at key for the contract at addr. +func (c *EVMClient) Storage(ctx context.Context, addr common.Address, key common.Hash) (common.Hash, error) { + resp, err := c.query.Storage(ctx, &evmtypes.QueryStorageRequest{ + Address: addr.Hex(), + Key: key.Hex(), + }) + if err != nil { + return common.Hash{}, err + } + if resp == nil { + return common.Hash{}, nil + } + return common.HexToHash(resp.Value), nil +} + +// Balance returns the account balance in the EVM extended denom (alume on +// Lumera) as an integer wei-like *big.Int. +func (c *EVMClient) Balance(ctx context.Context, addr common.Address) (*big.Int, error) { + resp, err := c.query.Balance(ctx, &evmtypes.QueryBalanceRequest{Address: addr.Hex()}) + if err != nil { + return nil, err + } + if resp == nil || resp.Balance == "" { + return new(big.Int), nil + } + bal, ok := new(big.Int).SetString(resp.Balance, 10) + if !ok { + return nil, fmt.Errorf("decode balance %q", resp.Balance) + } + return bal, nil +} + +// EthAccount returns the raw EVM account record: balance, code hash, nonce. +// On Lumera the nonce is shared with the Cosmos auth sequence. +func (c *EVMClient) EthAccount(ctx context.Context, addr common.Address) (*evmtypes.QueryAccountResponse, error) { + return c.query.Account(ctx, &evmtypes.QueryAccountRequest{Address: addr.Hex()}) +} + +// CosmosAccount returns the cosmos bech32 address paired with the given EVM +// address, plus its current sequence and account number. +func (c *EVMClient) CosmosAccount(ctx context.Context, addr common.Address) (*evmtypes.QueryCosmosAccountResponse, error) { + return c.query.CosmosAccount(ctx, &evmtypes.QueryCosmosAccountRequest{Address: addr.Hex()}) +} + +// Params returns the current x/vm module parameters. +func (c *EVMClient) Params(ctx context.Context) (*evmtypes.QueryParamsResponse, error) { + return c.query.Params(ctx, &evmtypes.QueryParamsRequest{}) +} + +// BaseFee returns the EIP-1559 base fee in the EVM extended denom (alume) as +// a *big.Int. Differs from FeeMarketClient.BaseFee, which returns the native +// denom (ulume) decimal value. +func (c *EVMClient) BaseFee(ctx context.Context) (*big.Int, error) { + resp, err := c.query.BaseFee(ctx, &evmtypes.QueryBaseFeeRequest{}) + if err != nil { + return nil, err + } + if resp == nil || resp.BaseFee == nil || resp.BaseFee.IsNil() { + return new(big.Int), nil + } + bf := *resp.BaseFee + return bf.BigInt(), nil +} + +// Config returns the EVM configuration (chain config, denoms, active forks). +func (c *EVMClient) Config(ctx context.Context) (*evmtypes.QueryConfigResponse, error) { + return c.query.Config(ctx, &evmtypes.QueryConfigRequest{}) +} + +// GlobalMinGasPrice returns the chain-wide minimum gas price in alume/gas. +func (c *EVMClient) GlobalMinGasPrice(ctx context.Context) (*big.Int, error) { + resp, err := c.query.GlobalMinGasPrice(ctx, &evmtypes.QueryGlobalMinGasPriceRequest{}) + if err != nil { + return nil, err + } + if resp == nil || resp.MinGasPrice.IsNil() { + return new(big.Int), nil + } + return resp.MinGasPrice.BigInt(), nil +} + +// EthCall executes a read-only EVM call. from may be the zero address. +func (c *EVMClient) EthCall(ctx context.Context, from common.Address, to *common.Address, data []byte, gasCap uint64) (*evmtypes.MsgEthereumTxResponse, error) { + args, err := encodeEthCallArgs(from, to, data, nil) + if err != nil { + return nil, err + } + if gasCap == 0 { + gasCap = 25_000_000 + } + return c.query.EthCall(ctx, &evmtypes.EthCallRequest{ + Args: args, + GasCap: gasCap, + }) +} + +// EstimateGas estimates gas for a hypothetical tx. value may be nil. +func (c *EVMClient) EstimateGas(ctx context.Context, from common.Address, to *common.Address, data []byte, value *big.Int, gasCap uint64) (uint64, error) { + args, err := encodeEthCallArgs(from, to, data, value) + if err != nil { + return 0, err + } + if gasCap == 0 { + gasCap = 25_000_000 + } + resp, err := c.query.EstimateGas(ctx, &evmtypes.EthCallRequest{ + Args: args, + GasCap: gasCap, + }) + if err != nil { + return 0, err + } + if resp == nil { + return 0, nil + } + return resp.Gas, nil +} + +// TraceTx returns the raw JSON trace for a replayed EVM tx. Building the +// request is non-trivial (block context, predecessors) so callers construct +// the request themselves; this helper just forwards it. +func (c *EVMClient) TraceTx(ctx context.Context, req *evmtypes.QueryTraceTxRequest) ([]byte, error) { + resp, err := c.query.TraceTx(ctx, req) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + return resp.Data, nil +} + +// -------- Transaction Helpers -------- + +// EthereumTxOptions overrides defaults pulled from chain state. +type EthereumTxOptions struct { + Nonce *uint64 // default: query EthAccount + GasLimit uint64 // default: EstimateGas + 20% buffer + GasTipCap *big.Int // default: feemarket MinGasPrice scaled to alume/gas + GasFeeCap *big.Int // default: BaseFee * 2 + tipCap (alume/gas) + Value *big.Int // default: 0 (alume) + AccessList ethtypes.AccessList + Memo string // unused for EVM-path txs; reserved for future +} + +// EthereumTransactionResult captures the outcome of an Ethereum-format tx. +type EthereumTransactionResult struct { + EthTxHash common.Hash + CosmosHash string + Height int64 + GasUsed uint64 + VMError string + ReturnData []byte + Logs []*evmtypes.Log +} + +// SendEthereumTransaction signs and broadcasts an Ethereum-format tx using +// the client's key. Pulls nonce, gas, and fees from chain when not set. +// Waits for inclusion. Returns both the eth tx hash and cosmos hash so +// callers can correlate. +// +// to=nil deploys a contract; pass DeployContract for that case. +func (c *EVMClient) SendEthereumTransaction( + ctx context.Context, + to *common.Address, + data []byte, + opts *EthereumTxOptions, +) (*EthereumTransactionResult, error) { + if c.client == nil { + return nil, fmt.Errorf("EVMClient not wired to a base.Client (constructed standalone)") + } + cfg := c.client.Cfg() + if cfg.EVMChainID == nil || cfg.EVMChainID.Sign() <= 0 { + return nil, fmt.Errorf("EVMChainID is required to send Ethereum transactions") + } + keyName := c.client.KeyName() + if keyName == "" { + return nil, fmt.Errorf("base client has no KeyName") + } + if opts == nil { + opts = &EthereumTxOptions{} + } + + from, err := sdkcrypto.EVMAddressFromKey(c.client.Keyring(), keyName) + if err != nil { + return nil, fmt.Errorf("derive sender address: %w", err) + } + sender := common.HexToAddress(from) + + value := opts.Value + if value == nil { + value = new(big.Int) + } + if value.Sign() < 0 { + return nil, fmt.Errorf("value must be non-negative") + } + + nonce, err := c.resolveNonce(ctx, sender, opts.Nonce) + if err != nil { + return nil, fmt.Errorf("resolve nonce: %w", err) + } + + gasLimit, err := c.resolveGasLimit(ctx, sender, to, data, value, opts.GasLimit) + if err != nil { + return nil, fmt.Errorf("resolve gas limit: %w", err) + } + + tipCap, feeCap, err := c.resolveGasCaps(ctx, opts.GasTipCap, opts.GasFeeCap) + if err != nil { + return nil, fmt.Errorf("resolve gas caps: %w", err) + } + + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: cfg.EVMChainID, + Nonce: nonce, + GasTipCap: tipCap, + GasFeeCap: feeCap, + Gas: gasLimit, + To: to, + Value: value, + Data: data, + AccessList: opts.AccessList, + }) + + signed, err := sdkcrypto.SignEthereumTx(c.client.Keyring(), keyName, cfg.EVMChainID, tx) + if err != nil { + return nil, fmt.Errorf("sign tx: %w", err) + } + + return c.RawEthereumTx(ctx, signed) +} + +// DeployContract is sugar over SendEthereumTransaction for contract creation. +// Returns the deployed contract address derived from sender+nonce per EIP-161. +func (c *EVMClient) DeployContract( + ctx context.Context, + bytecode []byte, + opts *EthereumTxOptions, +) (common.Address, *EthereumTransactionResult, error) { + if c.client == nil { + return common.Address{}, nil, fmt.Errorf("EVMClient not wired to a base.Client (constructed standalone)") + } + if opts == nil { + opts = &EthereumTxOptions{} + } else { + copied := *opts + opts = &copied + } + + from, err := sdkcrypto.EVMAddressFromKey(c.client.Keyring(), c.client.KeyName()) + if err != nil { + return common.Address{}, nil, fmt.Errorf("derive sender address: %w", err) + } + sender := common.HexToAddress(from) + nonce, err := c.resolveNonce(ctx, sender, opts.Nonce) + if err != nil { + return common.Address{}, nil, fmt.Errorf("resolve nonce: %w", err) + } + + opts.Nonce = &nonce + addr := ethCreateAddress(sender, nonce) + res, err := c.SendEthereumTransaction(ctx, nil, bytecode, opts) + if err != nil { + return common.Address{}, nil, err + } + return finalizeDeployResult(addr, res) +} + +// finalizeDeployResult resolves the outcome of a contract deployment. The EVM +// can revert a constructor while the surrounding cosmos tx still succeeds (fees +// charged, VmError set in the response). In that case no contract exists at the +// CREATE address, so return the zero address and an error while still surfacing +// the result for revert-reason inspection. +func finalizeDeployResult(addr common.Address, res *EthereumTransactionResult) (common.Address, *EthereumTransactionResult, error) { + if res != nil && res.VMError != "" { + return common.Address{}, res, fmt.Errorf("contract deployment reverted: %s", res.VMError) + } + return addr, res, nil +} + +// CallContract is a read-only call (forwards to EthCall). +func (c *EVMClient) CallContract( + ctx context.Context, + to common.Address, + data []byte, +) ([]byte, error) { + var from common.Address + if c.client != nil && c.client.KeyName() != "" { + hex, err := sdkcrypto.EVMAddressFromKey(c.client.Keyring(), c.client.KeyName()) + if err != nil { + return nil, fmt.Errorf("derive sender address: %w", err) + } + from = common.HexToAddress(hex) + } + resp, err := c.EthCall(ctx, from, &to, data, 0) + if err != nil { + return nil, err + } + if resp == nil { + return nil, nil + } + if resp.VmError != "" { + return resp.Ret, fmt.Errorf("vm error: %s", resp.VmError) + } + return resp.Ret, nil +} + +// RawEthereumTx broadcasts a pre-signed go-ethereum transaction unchanged. +// For callers bringing their own signer (e.g. hardware wallet). +func (c *EVMClient) RawEthereumTx( + ctx context.Context, + signed *ethtypes.Transaction, +) (*EthereumTransactionResult, error) { + if c.client == nil { + return nil, fmt.Errorf("EVMClient not wired to a base.Client") + } + cfg := c.client.Cfg() + if cfg.EVMChainID == nil { + return nil, fmt.Errorf("EVMChainID required to encode MsgEthereumTx") + } + if signed == nil { + return nil, fmt.Errorf("signed tx is required") + } + signedChainID := signed.ChainId() + if signedChainID == nil { + return nil, fmt.Errorf("signed tx chain ID is missing") + } + if signedChainID.Cmp(cfg.EVMChainID) != 0 { + return nil, fmt.Errorf("signed tx chain ID %s does not match configured EVMChainID %s", signedChainID, cfg.EVMChainID) + } + + nativeDenom, extendedDenom, err := c.resolveEVMDenoms(ctx, cfg) + if err != nil { + return nil, err + } + + txBytes, err := buildEthereumTxBytes(signed, nativeDenom, extendedDenom, cfg.FeeDenom) + if err != nil { + return nil, err + } + + cosmosHash, getResp, err := c.client.BroadcastAndWait(ctx, txBytes, txtypes.BroadcastMode_BROADCAST_MODE_SYNC) + if err != nil { + return nil, fmt.Errorf("broadcast tx: %w", err) + } + + res := &EthereumTransactionResult{ + EthTxHash: signed.Hash(), + CosmosHash: cosmosHash, + } + if getResp != nil && getResp.TxResponse != nil { + res.Height = getResp.TxResponse.Height + // GasUsed is int64 in the proto; a negative value is malformed and would + // wrap to a huge uint64. Clamp to 0 rather than propagate garbage. + if gu := getResp.TxResponse.GasUsed; gu > 0 { + res.GasUsed = uint64(gu) + } + if data := getResp.TxResponse.Data; len(data) > 0 { + decoded, derr := decodeMsgEthereumTxResponses([]byte(data)) + if derr != nil { + return nil, fmt.Errorf("decode EVM tx response: %w", derr) + } + if len(decoded) > 0 { + res.ReturnData = decoded[0].Ret + res.VMError = decoded[0].VmError + res.Logs = decoded[0].Logs + } + } + } + return res, nil +} + +func (c *EVMClient) resolveNonce(ctx context.Context, sender common.Address, override *uint64) (uint64, error) { + if override != nil { + return *override, nil + } + acct, err := c.EthAccount(ctx, sender) + if err != nil { + return 0, err + } + if acct == nil { + return 0, nil + } + return acct.Nonce, nil +} + +func (c *EVMClient) resolveGasLimit(ctx context.Context, from common.Address, to *common.Address, data []byte, value *big.Int, override uint64) (uint64, error) { + if override > 0 { + return override, nil + } + estimate, err := c.EstimateGas(ctx, from, to, data, value, 0) + if err != nil { + return 0, err + } + if estimate == 0 { + return 0, fmt.Errorf("gas estimator returned 0") + } + // 20% buffer. + buffer := estimate / 5 + return estimate + buffer, nil +} + +func (c *EVMClient) resolveGasCaps(ctx context.Context, tipOverride, feeOverride *big.Int) (*big.Int, *big.Int, error) { + cfg := c.client.Cfg() + tip := tipOverride + if tip == nil { + tip = cfg.EVMGasTipCap + } + if tip == nil { + mg, err := c.resolveMinGasTipCap(ctx) + if err != nil { + return nil, nil, fmt.Errorf("min gas price: %w", err) + } + tip = mg + } + fee := feeOverride + if fee == nil { + fee = cfg.EVMGasFeeCap + } + if fee == nil { + base, err := c.BaseFee(ctx) + if err != nil { + return nil, nil, fmt.Errorf("base fee: %w", err) + } + // feeCap = base*2 + tip + fee = new(big.Int).Add(new(big.Int).Mul(base, big.NewInt(2)), tip) + } + if tip.Sign() < 0 || fee.Sign() < 0 { + return nil, nil, fmt.Errorf("gas caps must be non-negative") + } + if tip.Cmp(fee) > 0 { + return nil, nil, fmt.Errorf("tip cap %s exceeds fee cap %s", tip, fee) + } + return tip, fee, nil +} + +func (c *EVMClient) resolveMinGasTipCap(ctx context.Context) (*big.Int, error) { + if c.client == nil || c.client.FeeMarket == nil { + return nil, fmt.Errorf("feemarket client is required") + } + params, err := c.client.FeeMarket.Params(ctx) + if err != nil { + return nil, err + } + if params == nil { + return nil, fmt.Errorf("empty feemarket params response") + } + return sdkcrypto.ULumeDecToWei(params.Params.MinGasPrice) +} + +func (c *EVMClient) resolveEVMDenoms(ctx context.Context, cfg Config) (string, string, error) { + nativeDenom := strings.TrimSpace(cfg.EVMNativeDenom) + extendedDenom := strings.TrimSpace(cfg.EVMExtendedDenom) + if nativeDenom != "" && extendedDenom != "" { + return nativeDenom, extendedDenom, nil + } + + if c.query == nil { + return "", "", fmt.Errorf("EVMNativeDenom and EVMExtendedDenom are required when EVM params query client is unavailable") + } + resp, err := c.Params(ctx) + if err != nil { + return "", "", fmt.Errorf("query EVM params: %w", err) + } + if resp != nil { + if nativeDenom == "" { + nativeDenom = strings.TrimSpace(resp.Params.EvmDenom) + } + if extendedDenom == "" && resp.Params.ExtendedDenomOptions != nil { + extendedDenom = strings.TrimSpace(resp.Params.ExtendedDenomOptions.ExtendedDenom) + } + } + + if nativeDenom == "" { + return "", "", fmt.Errorf("EVM native denom could not be resolved from chain params; set EVMNativeDenom in the config") + } + if extendedDenom == "" { + return "", "", fmt.Errorf("EVM extended denom could not be resolved from chain params; set EVMExtendedDenom in the config") + } + return nativeDenom, extendedDenom, nil +} + +// buildEthereumTxBytes wraps a signed Ethereum tx as a MsgEthereumTx, builds +// the cosmos envelope with the EVM extension option and the correct fee/gas, +// and returns the encoded bytes ready for broadcast. +func buildEthereumTxBytes(signed *ethtypes.Transaction, nativeDenom, extendedDenom, feeDenom string) ([]byte, error) { + msg, err := sdkcrypto.WrapAsMsgEthereumTx(signed) + if err != nil { + return nil, fmt.Errorf("wrap MsgEthereumTx: %w", err) + } + + if nativeDenom == "" { + nativeDenom = feeDenom + } + // Guard against empty denoms: sdk.NewCoin (called by BuildTxWithEvmParams) + // panics on an invalid/empty denom, so reject early with a clear error. + if nativeDenom == "" { + return nil, fmt.Errorf("EVM native denom is required") + } + if extendedDenom == "" { + return nil, fmt.Errorf("EVM extended denom is required") + } + + txCfg := sdkcrypto.NewDefaultTxConfig() + builder := txCfg.NewTxBuilder() + if _, err := msg.BuildTxWithEvmParams(builder, evmtypes.Params{ + EvmDenom: nativeDenom, + ExtendedDenomOptions: &evmtypes.ExtendedDenomOptions{ + ExtendedDenom: extendedDenom, + }, + }); err != nil { + return nil, fmt.Errorf("build cosmos envelope: %w", err) + } + + txBytes, err := txCfg.TxEncoder()(builder.GetTx()) + if err != nil { + return nil, fmt.Errorf("encode tx: %w", err) + } + return txBytes, nil +} + +// decodeMsgEthereumTxResponses extracts MsgEthereumTxResponse entries from a +// cosmos TxResponse.Data field. The data is a hex-encoded TxMsgData proto. +func decodeMsgEthereumTxResponses(data []byte) ([]*evmtypes.MsgEthereumTxResponse, error) { + dataStr := string(data) + encoded := strings.TrimPrefix(dataStr, "0x") + raw, err := hex.DecodeString(encoded) + if err != nil { + if strings.HasPrefix(dataStr, "0x") { + return nil, err + } + raw = data + } + return evmtypes.DecodeTxResponses(raw) +} + +// ethCreateAddress computes the deployed contract address for sender+nonce +// using the standard CREATE rule: keccak256(rlp([sender, nonce]))[12:]. +func ethCreateAddress(sender common.Address, nonce uint64) common.Address { + return ethcrypto.CreateAddress(sender, nonce) +} + +func encodeEthCallArgs(from common.Address, to *common.Address, data []byte, value *big.Int) ([]byte, error) { + args := evmtypes.TransactionArgs{ + From: &from, + To: to, + } + if len(data) > 0 { + input := hexutil.Bytes(data) + args.Input = &input + } + if value != nil { + if value.Sign() < 0 { + return nil, fmt.Errorf("value must be non-negative") + } + args.Value = (*hexutil.Big)(value) + } + return json.Marshal(args) +} diff --git a/blockchain/evm_test.go b/blockchain/evm_test.go new file mode 100644 index 0000000..ddc9532 --- /dev/null +++ b/blockchain/evm_test.go @@ -0,0 +1,575 @@ +package blockchain + +import ( + "context" + "encoding/hex" + "encoding/json" + "math/big" + "net" + "strings" + "testing" + + sdkmath "cosmossdk.io/math" + blockbase "github.com/LumeraProtocol/sdk-go/blockchain/base" + "github.com/LumeraProtocol/sdk-go/constants" + sdkgrpc "github.com/LumeraProtocol/sdk-go/internal/grpc" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdk "github.com/cosmos/cosmos-sdk/types" + feemarkettypes "github.com/cosmos/evm/x/feemarket/types" + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/cosmos/gogoproto/proto" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +// Mock the QueryClient interfaces directly. Going through bufconn breaks on +// cosmos custom types (sdkmath.Int / LegacyDec) because the default gRPC +// codec uses proto reflection v2, which does not understand gogoproto +// `customtype=` annotations. The production base client gets the right +// codec via cosmos-sdk's gRPC wiring; here we test the wrapper logic only. + +type stubEVMQuery struct { + codeResp *evmtypes.QueryCodeResponse + storageResp *evmtypes.QueryStorageResponse + balanceResp *evmtypes.QueryBalanceResponse + accountResp *evmtypes.QueryAccountResponse + cosmosAccount *evmtypes.QueryCosmosAccountResponse + paramsResp *evmtypes.QueryParamsResponse + baseFeeResp *evmtypes.QueryBaseFeeResponse + configResp *evmtypes.QueryConfigResponse + globalMinResp *evmtypes.QueryGlobalMinGasPriceResponse + ethCallResp *evmtypes.MsgEthereumTxResponse + estimateGasResp *evmtypes.EstimateGasResponse + traceResp *evmtypes.QueryTraceTxResponse + lastEthCallArgs []byte + lastEstimateArgs []byte +} + +func (s *stubEVMQuery) Code(_ context.Context, _ *evmtypes.QueryCodeRequest, _ ...grpc.CallOption) (*evmtypes.QueryCodeResponse, error) { + return s.codeResp, nil +} +func (s *stubEVMQuery) Storage(_ context.Context, _ *evmtypes.QueryStorageRequest, _ ...grpc.CallOption) (*evmtypes.QueryStorageResponse, error) { + return s.storageResp, nil +} +func (s *stubEVMQuery) Balance(_ context.Context, _ *evmtypes.QueryBalanceRequest, _ ...grpc.CallOption) (*evmtypes.QueryBalanceResponse, error) { + return s.balanceResp, nil +} +func (s *stubEVMQuery) Account(_ context.Context, _ *evmtypes.QueryAccountRequest, _ ...grpc.CallOption) (*evmtypes.QueryAccountResponse, error) { + return s.accountResp, nil +} +func (s *stubEVMQuery) CosmosAccount(_ context.Context, _ *evmtypes.QueryCosmosAccountRequest, _ ...grpc.CallOption) (*evmtypes.QueryCosmosAccountResponse, error) { + return s.cosmosAccount, nil +} +func (s *stubEVMQuery) ValidatorAccount(_ context.Context, _ *evmtypes.QueryValidatorAccountRequest, _ ...grpc.CallOption) (*evmtypes.QueryValidatorAccountResponse, error) { + return nil, nil +} +func (s *stubEVMQuery) Params(_ context.Context, _ *evmtypes.QueryParamsRequest, _ ...grpc.CallOption) (*evmtypes.QueryParamsResponse, error) { + return s.paramsResp, nil +} +func (s *stubEVMQuery) BaseFee(_ context.Context, _ *evmtypes.QueryBaseFeeRequest, _ ...grpc.CallOption) (*evmtypes.QueryBaseFeeResponse, error) { + return s.baseFeeResp, nil +} +func (s *stubEVMQuery) Config(_ context.Context, _ *evmtypes.QueryConfigRequest, _ ...grpc.CallOption) (*evmtypes.QueryConfigResponse, error) { + return s.configResp, nil +} +func (s *stubEVMQuery) GlobalMinGasPrice(_ context.Context, _ *evmtypes.QueryGlobalMinGasPriceRequest, _ ...grpc.CallOption) (*evmtypes.QueryGlobalMinGasPriceResponse, error) { + return s.globalMinResp, nil +} +func (s *stubEVMQuery) EthCall(_ context.Context, req *evmtypes.EthCallRequest, _ ...grpc.CallOption) (*evmtypes.MsgEthereumTxResponse, error) { + s.lastEthCallArgs = append([]byte(nil), req.Args...) + return s.ethCallResp, nil +} +func (s *stubEVMQuery) EstimateGas(_ context.Context, req *evmtypes.EthCallRequest, _ ...grpc.CallOption) (*evmtypes.EstimateGasResponse, error) { + s.lastEstimateArgs = append([]byte(nil), req.Args...) + return s.estimateGasResp, nil +} +func (s *stubEVMQuery) TraceTx(_ context.Context, _ *evmtypes.QueryTraceTxRequest, _ ...grpc.CallOption) (*evmtypes.QueryTraceTxResponse, error) { + return s.traceResp, nil +} +func (s *stubEVMQuery) TraceBlock(_ context.Context, _ *evmtypes.QueryTraceBlockRequest, _ ...grpc.CallOption) (*evmtypes.QueryTraceBlockResponse, error) { + return nil, nil +} +func (s *stubEVMQuery) TraceCall(_ context.Context, _ *evmtypes.QueryTraceCallRequest, _ ...grpc.CallOption) (*evmtypes.QueryTraceCallResponse, error) { + return nil, nil +} + +func TestEVMClient_Queries(t *testing.T) { + baseFee := sdkmath.NewInt(2_500_000_000) + stub := &stubEVMQuery{ + codeResp: &evmtypes.QueryCodeResponse{Code: []byte{0x60, 0x60}}, + storageResp: &evmtypes.QueryStorageResponse{Value: "0x000000000000000000000000000000000000000000000000000000000000002a"}, + balanceResp: &evmtypes.QueryBalanceResponse{Balance: "1000000000000000000"}, + accountResp: &evmtypes.QueryAccountResponse{Balance: "1000000000000000000", CodeHash: "0x0", Nonce: 7}, + cosmosAccount: &evmtypes.QueryCosmosAccountResponse{ + CosmosAddress: "lumera1abc", + Sequence: 7, + AccountNumber: 42, + }, + baseFeeResp: &evmtypes.QueryBaseFeeResponse{BaseFee: &baseFee}, + globalMinResp: &evmtypes.QueryGlobalMinGasPriceResponse{MinGasPrice: sdkmath.NewInt(500_000_000)}, + ethCallResp: &evmtypes.MsgEthereumTxResponse{Ret: []byte{0x01}}, + estimateGasResp: &evmtypes.EstimateGasResponse{Gas: 21_000}, + } + c := &EVMClient{query: stub} + ctx := context.Background() + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + code, err := c.Code(ctx, addr) + if err != nil || len(code) != 2 { + t.Fatalf("Code: %v len=%d", err, len(code)) + } + + hash, err := c.Storage(ctx, addr, common.Hash{}) + if err != nil || hash.Big().Uint64() != 42 { + t.Fatalf("Storage: %v hash=%s", err, hash.Hex()) + } + + bal, err := c.Balance(ctx, addr) + if err != nil || bal.String() != "1000000000000000000" { + t.Fatalf("Balance: %v %s", err, bal) + } + + acct, err := c.EthAccount(ctx, addr) + if err != nil || acct.Nonce != 7 { + t.Fatalf("EthAccount: %v %+v", err, acct) + } + + cacct, err := c.CosmosAccount(ctx, addr) + if err != nil || cacct.AccountNumber != 42 { + t.Fatalf("CosmosAccount: %v %+v", err, cacct) + } + + bf, err := c.BaseFee(ctx) + if err != nil || bf.String() != "2500000000" { + t.Fatalf("BaseFee: %v %s", err, bf) + } + + mg, err := c.GlobalMinGasPrice(ctx) + if err != nil || mg.String() != "500000000" { + t.Fatalf("GlobalMinGasPrice: %v %s", err, mg) + } + + to := common.HexToAddress("0xdeadbeef00000000000000000000000000000000") + callResp, err := c.EthCall(ctx, addr, &to, []byte{0xaa, 0xbb}, 0) + if err != nil || len(callResp.Ret) != 1 { + t.Fatalf("EthCall: %v %+v", err, callResp) + } + + var decoded evmtypes.TransactionArgs + if err := json.Unmarshal(stub.lastEthCallArgs, &decoded); err != nil { + t.Fatalf("decode ethcall args: %v", err) + } + if decoded.From == nil || *decoded.From != addr { + t.Fatalf("from mismatch: %+v", decoded.From) + } + if decoded.To == nil || *decoded.To != to { + t.Fatalf("to mismatch: %+v", decoded.To) + } + if decoded.Input == nil || !equalBytes(*decoded.Input, hexutil.Bytes{0xaa, 0xbb}) { + t.Fatalf("input mismatch: %x", *decoded.Input) + } + + gas, err := c.EstimateGas(ctx, addr, &to, []byte{0x01}, big.NewInt(0), 0) + if err != nil || gas != 21_000 { + t.Fatalf("EstimateGas: %v %d", err, gas) + } +} + +func TestEVMClient_BaseFee_Nil(t *testing.T) { + stub := &stubEVMQuery{baseFeeResp: &evmtypes.QueryBaseFeeResponse{}} + c := &EVMClient{query: stub} + bf, err := c.BaseFee(context.Background()) + if err != nil { + t.Fatalf("BaseFee: %v", err) + } + if bf.Sign() != 0 { + t.Fatalf("expected zero, got %s", bf) + } + + nilBaseFee := sdkmath.Int{} + stub.baseFeeResp = &evmtypes.QueryBaseFeeResponse{BaseFee: &nilBaseFee} + bf, err = c.BaseFee(context.Background()) + if err != nil { + t.Fatalf("BaseFee with nil sdk.Int: %v", err) + } + if bf.Sign() != 0 { + t.Fatalf("expected zero for nil sdk.Int, got %s", bf) + } +} + +func TestEVMClient_GlobalMinGasPrice_NilInt(t *testing.T) { + stub := &stubEVMQuery{globalMinResp: &evmtypes.QueryGlobalMinGasPriceResponse{}} + c := &EVMClient{query: stub} + got, err := c.GlobalMinGasPrice(context.Background()) + if err != nil { + t.Fatalf("GlobalMinGasPrice: %v", err) + } + if got.Sign() != 0 { + t.Fatalf("expected zero, got %s", got) + } +} + +func TestEVMNumericQueries_RoundTripThroughGRPC(t *testing.T) { + const bufSize = 1024 * 1024 + lis := bufconn.Listen(bufSize) + srv := grpc.NewServer(grpc.ForceServerCodec(sdkgrpc.GogoCodec())) + t.Cleanup(func() { + srv.Stop() + _ = lis.Close() + }) + + evmtypes.RegisterQueryServer(srv, &evmNumericQueryServer{}) + feemarkettypes.RegisterQueryServer(srv, &feeMarketNumericQueryServer{}) + go func() { + _ = srv.Serve(lis) + }() + + conn, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("dial bufnet: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + evm := &EVMClient{query: evmtypes.NewQueryClient(sdkgrpc.GogoClientConn(conn))} + baseFee, err := evm.BaseFee(context.Background()) + if err != nil { + t.Fatalf("EVM BaseFee over gRPC: %v", err) + } + if baseFee.String() != "2500000000" { + t.Fatalf("EVM BaseFee = %s", baseFee) + } + + feeMarket := &FeeMarketClient{query: feemarkettypes.NewQueryClient(sdkgrpc.GogoClientConn(conn))} + feeMarketBaseFee, err := feeMarket.BaseFee(context.Background()) + if err != nil { + t.Fatalf("FeeMarket BaseFee over gRPC: %v", err) + } + if feeMarketBaseFee.String() != "0.002500000000000000" { + t.Fatalf("FeeMarket BaseFee = %s", feeMarketBaseFee) + } +} + +func TestEVMClient_ResolveGasCaps_UsesFeeMarketMinGasPrice(t *testing.T) { + ctx := context.Background() + baseClient, err := blockbase.New(ctx, blockbase.Config{ + GRPCAddr: "localhost:9090", + EVMChainID: big.NewInt(1414), + EVMNativeDenom: "ulume", + }, nil, "") + if err != nil { + t.Fatalf("base.New: %v", err) + } + t.Cleanup(func() { _ = baseClient.Close() }) + + minGasPrice := sdkmath.LegacyMustNewDecFromStr("0.0005") + client := &Client{ + Client: baseClient, + FeeMarket: &FeeMarketClient{query: &stubFeeMarketQuery{ + params: &feemarkettypes.QueryParamsResponse{ + Params: feemarkettypes.Params{MinGasPrice: minGasPrice}, + }, + }}, + } + baseFee := sdkmath.NewInt(2_500_000_000) + evm := &EVMClient{ + client: client, + query: &stubEVMQuery{ + baseFeeResp: &evmtypes.QueryBaseFeeResponse{BaseFee: &baseFee}, + }, + } + + tip, fee, err := evm.resolveGasCaps(ctx, nil, nil) + if err != nil { + t.Fatalf("resolveGasCaps: %v", err) + } + if tip.String() != "500000000" { + t.Fatalf("tip cap = %s, want 500000000", tip) + } + if fee.String() != "5500000000" { + t.Fatalf("fee cap = %s, want 5500000000", fee) + } +} + +func TestEVMClient_ResolveEVMDenoms_QueryParamsWhenConfigMissing(t *testing.T) { + evm := &EVMClient{query: &stubEVMQuery{ + paramsResp: &evmtypes.QueryParamsResponse{Params: evmtypes.Params{ + EvmDenom: "ulume", + ExtendedDenomOptions: &evmtypes.ExtendedDenomOptions{ + ExtendedDenom: "alume", + }, + }}, + }} + + nativeDenom, extendedDenom, err := evm.resolveEVMDenoms(context.Background(), Config{}) + if err != nil { + t.Fatalf("resolveEVMDenoms: %v", err) + } + if nativeDenom != "ulume" || extendedDenom != "alume" { + t.Fatalf("denoms = %q/%q, want ulume/alume", nativeDenom, extendedDenom) + } +} + +func TestEVMClient_ResolveEVMDenoms_ConfigShortCircuitsQuery(t *testing.T) { + // query is nil on purpose: when both denoms are configured, resolveEVMDenoms + // must not touch the query client. + evm := &EVMClient{} + nativeDenom, extendedDenom, err := evm.resolveEVMDenoms(context.Background(), Config{ + EVMNativeDenom: "ulume", + EVMExtendedDenom: "alume", + }) + if err != nil { + t.Fatalf("resolveEVMDenoms: %v", err) + } + if nativeDenom != "ulume" || extendedDenom != "alume" { + t.Fatalf("denoms = %q/%q, want ulume/alume", nativeDenom, extendedDenom) + } +} + +func TestEVMClient_ResolveEVMDenoms_NoQueryNoConfig(t *testing.T) { + evm := &EVMClient{} + _, _, err := evm.resolveEVMDenoms(context.Background(), Config{}) + if err == nil { + t.Fatalf("expected error when query client and config denoms are both absent") + } + if !strings.Contains(err.Error(), "EVMNativeDenom") { + t.Fatalf("error %q should mention the config field to set", err) + } +} + +func TestEVMClient_ResolveEVMDenoms_MissingExtendedDenom(t *testing.T) { + // Chain returns the native denom but no ExtendedDenomOptions, and config + // supplies neither: resolveEVMDenoms must surface a clear extended-denom error. + evm := &EVMClient{query: &stubEVMQuery{ + paramsResp: &evmtypes.QueryParamsResponse{Params: evmtypes.Params{ + EvmDenom: "ulume", + }}, + }} + _, _, err := evm.resolveEVMDenoms(context.Background(), Config{}) + if err == nil { + t.Fatalf("expected error when extended denom cannot be resolved") + } + if !strings.Contains(err.Error(), "extended denom") { + t.Fatalf("error %q should mention extended denom", err) + } +} + +func TestEncodeEthCallArgs_RejectsNegativeValue(t *testing.T) { + _, err := encodeEthCallArgs(common.Address{}, nil, nil, big.NewInt(-1)) + if err == nil { + t.Fatalf("expected negative value error") + } +} + +func TestDecodeMsgEthereumTxResponses_AcceptsHexPrefix(t *testing.T) { + want := &evmtypes.MsgEthereumTxResponse{Ret: []byte{0x01, 0x02}} + txData := &sdk.TxMsgData{ + MsgResponses: []*codectypes.Any{codectypes.UnsafePackAny(want)}, + } + raw, err := proto.Marshal(txData) + if err != nil { + t.Fatalf("marshal tx data: %v", err) + } + + got, err := decodeMsgEthereumTxResponses([]byte("0x" + hex.EncodeToString(raw))) + if err != nil { + t.Fatalf("decode tx responses: %v", err) + } + if len(got) != 1 || !equalBytes(got[0].Ret, want.Ret) { + t.Fatalf("decoded response mismatch: %+v", got) + } +} + +func TestDecodeMsgEthereumTxResponses_AcceptsRawProtoBytes(t *testing.T) { + want := &evmtypes.MsgEthereumTxResponse{Ret: []byte{0x03, 0x04}} + txData := &sdk.TxMsgData{ + MsgResponses: []*codectypes.Any{codectypes.UnsafePackAny(want)}, + } + raw, err := proto.Marshal(txData) + if err != nil { + t.Fatalf("marshal tx data: %v", err) + } + + got, err := decodeMsgEthereumTxResponses(raw) + if err != nil { + t.Fatalf("decode raw tx responses: %v", err) + } + if len(got) != 1 || !equalBytes(got[0].Ret, want.Ret) { + t.Fatalf("decoded response mismatch: %+v", got) + } +} + +func TestCallContract_ReturnsSenderDerivationError(t *testing.T) { + kr, _ := newCosmosSigningKeyringForEVM(t, "alice") + baseClient, err := blockbase.New(context.Background(), blockbase.Config{ + GRPCAddr: "127.0.0.1:1", + EVMChainID: big.NewInt(1414), + EVMNativeDenom: "ulume", + }, kr, "alice") + if err != nil { + t.Fatalf("base.New: %v", err) + } + t.Cleanup(func() { _ = baseClient.Close() }) + + evm := &EVMClient{ + client: &Client{Client: baseClient}, + query: &stubEVMQuery{ethCallResp: &evmtypes.MsgEthereumTxResponse{}}, + } + + _, err = evm.CallContract(context.Background(), common.HexToAddress("0x000000000000000000000000000000000000dEaD"), nil) + if err == nil { + t.Fatalf("expected sender derivation error") + } +} + +type stubFeeMarketQuery struct { + params *feemarkettypes.QueryParamsResponse + baseFee *feemarkettypes.QueryBaseFeeResponse + gas *feemarkettypes.QueryBlockGasResponse +} + +type evmNumericQueryServer struct { + evmtypes.UnimplementedQueryServer +} + +func (evmNumericQueryServer) BaseFee(context.Context, *evmtypes.QueryBaseFeeRequest) (*evmtypes.QueryBaseFeeResponse, error) { + baseFee := sdkmath.NewInt(2_500_000_000) + return &evmtypes.QueryBaseFeeResponse{BaseFee: &baseFee}, nil +} + +type feeMarketNumericQueryServer struct { + feemarkettypes.UnimplementedQueryServer +} + +func (feeMarketNumericQueryServer) BaseFee(context.Context, *feemarkettypes.QueryBaseFeeRequest) (*feemarkettypes.QueryBaseFeeResponse, error) { + baseFee := sdkmath.LegacyMustNewDecFromStr("0.0025") + return &feemarkettypes.QueryBaseFeeResponse{BaseFee: &baseFee}, nil +} + +func (s *stubFeeMarketQuery) Params(_ context.Context, _ *feemarkettypes.QueryParamsRequest, _ ...grpc.CallOption) (*feemarkettypes.QueryParamsResponse, error) { + return s.params, nil +} +func (s *stubFeeMarketQuery) BaseFee(_ context.Context, _ *feemarkettypes.QueryBaseFeeRequest, _ ...grpc.CallOption) (*feemarkettypes.QueryBaseFeeResponse, error) { + return s.baseFee, nil +} +func (s *stubFeeMarketQuery) BlockGas(_ context.Context, _ *feemarkettypes.QueryBlockGasRequest, _ ...grpc.CallOption) (*feemarkettypes.QueryBlockGasResponse, error) { + return s.gas, nil +} + +func TestFeeMarketClient_Queries(t *testing.T) { + bf := sdkmath.LegacyMustNewDecFromStr("0.0025") + stub := &stubFeeMarketQuery{ + params: &feemarkettypes.QueryParamsResponse{Params: feemarkettypes.Params{ + BaseFeeChangeDenominator: 16, + }}, + baseFee: &feemarkettypes.QueryBaseFeeResponse{BaseFee: &bf}, + gas: &feemarkettypes.QueryBlockGasResponse{Gas: 250_000}, + } + c := &FeeMarketClient{query: stub} + ctx := context.Background() + + p, err := c.Params(ctx) + if err != nil || p.Params.BaseFeeChangeDenominator != 16 { + t.Fatalf("Params: %v %+v", err, p) + } + + got, err := c.BaseFee(ctx) + if err != nil || got.String() != "0.002500000000000000" { + t.Fatalf("BaseFee: %v %s", err, got) + } + + g, err := c.BlockGas(ctx) + if err != nil || g != 250_000 { + t.Fatalf("BlockGas: %v %d", err, g) + } +} + +func TestFeeMarketClient_BlockGas_RejectsNegative(t *testing.T) { + c := &FeeMarketClient{query: &stubFeeMarketQuery{ + gas: &feemarkettypes.QueryBlockGasResponse{Gas: -1}, + }} + if _, err := c.BlockGas(context.Background()); err == nil { + t.Fatal("expected error for negative block gas") + } +} + +type stubPreciseBankQuery struct { + remainder *precisebanktypes.QueryRemainderResponse + frac *precisebanktypes.QueryFractionalBalanceResponse + lastAddr string +} + +func (s *stubPreciseBankQuery) Remainder(_ context.Context, _ *precisebanktypes.QueryRemainderRequest, _ ...grpc.CallOption) (*precisebanktypes.QueryRemainderResponse, error) { + return s.remainder, nil +} +func (s *stubPreciseBankQuery) FractionalBalance(_ context.Context, req *precisebanktypes.QueryFractionalBalanceRequest, _ ...grpc.CallOption) (*precisebanktypes.QueryFractionalBalanceResponse, error) { + s.lastAddr = req.Address + return s.frac, nil +} + +func TestPreciseBankClient_Queries(t *testing.T) { + stub := &stubPreciseBankQuery{ + remainder: &precisebanktypes.QueryRemainderResponse{Remainder: sdk.NewInt64Coin("ulume", 0)}, + frac: &precisebanktypes.QueryFractionalBalanceResponse{FractionalBalance: sdk.NewInt64Coin("ulume", 123)}, + } + c := &PreciseBankClient{query: stub} + ctx := context.Background() + + r, err := c.Remainder(ctx) + if err != nil || r.Denom != "ulume" { + t.Fatalf("Remainder: %v %+v", err, r) + } + + f, err := c.FractionalBalance(ctx, "lumera1abc") + if err != nil || f.Amount.Int64() != 123 { + t.Fatalf("FractionalBalance: %v %+v", err, f) + } + if stub.lastAddr != "lumera1abc" { + t.Fatalf("server saw wrong addr: %q", stub.lastAddr) + } +} + +func equalBytes(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func newCosmosSigningKeyringForEVM(t *testing.T, keyName string) (keyring.Keyring, string) { + t.Helper() + kr, err := sdkcrypto.NewKeyring(sdkcrypto.KeyringParams{ + AppName: "lumera", + Backend: "test", + Dir: t.TempDir(), + Input: strings.NewReader(""), + }) + if err != nil { + t.Fatalf("create keyring: %v", err) + } + if _, err := kr.NewAccount(keyName, evmTxTestMnemonic, "", sdk.FullFundraiserPath, hd.Secp256k1); err != nil { + t.Fatalf("new account: %v", err) + } + addr, err := sdkcrypto.AddressFromKey(kr, keyName, constants.LumeraAccountHRP) + if err != nil { + t.Fatalf("derive address: %v", err) + } + return kr, addr +} diff --git a/blockchain/evm_tx_test.go b/blockchain/evm_tx_test.go new file mode 100644 index 0000000..ec03eb1 --- /dev/null +++ b/blockchain/evm_tx_test.go @@ -0,0 +1,277 @@ +package blockchain + +import ( + "context" + "math/big" + "net" + "os" + "path/filepath" + "testing" + "time" + + abcipb "cosmossdk.io/api/cosmos/base/abci/v1beta1" + txtypes "cosmossdk.io/api/cosmos/tx/v1beta1" + blockbase "github.com/LumeraProtocol/sdk-go/blockchain/base" + clientconfig "github.com/LumeraProtocol/sdk-go/client/config" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + ethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +const evmTxTestMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + +func writeMnemonicForEVM(t *testing.T) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "mnemonic.txt") + require.NoError(t, os.WriteFile(path, []byte(evmTxTestMnemonic), 0o600)) + return path +} + +func TestBuildEthereumTxBytes_RoundTrip(t *testing.T) { + mnemonicFile := writeMnemonicForEVM(t) + kr, _, _, err := sdkcrypto.LoadKeyring("alice", mnemonicFile, sdkcrypto.KeyTypeEVM) + require.NoError(t, err) + + chainID := big.NewInt(1414) + to := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: 3, + GasTipCap: big.NewInt(500_000_000), + GasFeeCap: big.NewInt(2_500_000_000), + Gas: 50_000, + To: &to, + Value: big.NewInt(100_000_000_000_000_000), // 0.1 alume-equivalent + }) + signed, err := sdkcrypto.SignEthereumTx(kr, "alice", chainID, tx) + require.NoError(t, err) + + bytes, err := buildEthereumTxBytes(signed, "ulume", "alume", "ulume") + require.NoError(t, err) + require.NotEmpty(t, bytes) + + // Decode using a permissive tx config to inspect the envelope. + txCfg := sdkcrypto.NewDefaultTxConfig() + decoded, err := txCfg.TxDecoder()(bytes) + require.NoError(t, err) + + msgs := decoded.GetMsgs() + require.Len(t, msgs, 1) + msg, ok := msgs[0].(*evmtypes.MsgEthereumTx) + require.True(t, ok, "expected MsgEthereumTx, got %T", msgs[0]) + + require.Equal(t, signed.Hash(), msg.AsTransaction().Hash()) + + feeTx, ok := decoded.(sdk.FeeTx) + require.True(t, ok) + require.Equal(t, uint64(50_000), feeTx.GetGas()) + fees := feeTx.GetFee() + require.Len(t, fees, 1) + require.Equal(t, "alume", fees[0].Denom) + + // Extension option must be ExtensionOptionsEthereumTx so the dual-route + // ante handler picks the EVM path. + withExt, ok := decoded.(interface { + GetExtensionOptions() []*codectypes.Any + }) + require.True(t, ok) + require.Len(t, withExt.GetExtensionOptions(), 1) + require.Equal(t, codectypes.MsgTypeURL(&evmtypes.ExtensionOptionsEthereumTx{}), withExt.GetExtensionOptions()[0].TypeUrl) +} + +func TestBuildEthereumTxBytes_RequiresExtendedDenom(t *testing.T) { + mnemonicFile := writeMnemonicForEVM(t) + kr, _, _, err := sdkcrypto.LoadKeyring("alice", mnemonicFile, sdkcrypto.KeyTypeEVM) + require.NoError(t, err) + + chainID := big.NewInt(1414) + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: 3, + GasTipCap: big.NewInt(500_000_000), + GasFeeCap: big.NewInt(2_500_000_000), + Gas: 50_000, + }) + signed, err := sdkcrypto.SignEthereumTx(kr, "alice", chainID, tx) + require.NoError(t, err) + + _, err = buildEthereumTxBytes(signed, "ulume", "", "ulume") + require.Error(t, err) + require.Contains(t, err.Error(), "extended denom") +} + +func TestBuildEthereumTxBytes_RequiresNativeDenom(t *testing.T) { + mnemonicFile := writeMnemonicForEVM(t) + kr, _, _, err := sdkcrypto.LoadKeyring("alice", mnemonicFile, sdkcrypto.KeyTypeEVM) + require.NoError(t, err) + + chainID := big.NewInt(1414) + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: 3, + GasTipCap: big.NewInt(500_000_000), + GasFeeCap: big.NewInt(2_500_000_000), + Gas: 50_000, + }) + signed, err := sdkcrypto.SignEthereumTx(kr, "alice", chainID, tx) + require.NoError(t, err) + + // Both nativeDenom and the feeDenom fallback are empty: must error + // explicitly rather than silently build a tx with an empty fee denom. + _, err = buildEthereumTxBytes(signed, "", "alume", "") + require.Error(t, err) + require.Contains(t, err.Error(), "native denom") +} + +func TestFinalizeDeployResult(t *testing.T) { + addr := common.HexToAddress("0x1234567890123456789012345678901234567890") + + // Successful deploy: the pre-computed address passes through unchanged. + res := &EthereumTransactionResult{EthTxHash: common.Hash{0x1}} + got, gotRes, err := finalizeDeployResult(addr, res) + require.NoError(t, err) + require.Equal(t, addr, got) + require.Same(t, res, gotRes) + + // Reverted deploy: no contract exists at addr, so return the zero address + // and an error, while still surfacing the result for revert inspection. + reverted := &EthereumTransactionResult{VMError: "execution reverted", ReturnData: []byte{0xde, 0xad}} + got, gotRes, err = finalizeDeployResult(addr, reverted) + require.Error(t, err) + require.Contains(t, err.Error(), "revert") + require.Equal(t, common.Address{}, got) + require.Same(t, reverted, gotRes) +} + +func TestEthCreateAddress_MatchesGoEthereum(t *testing.T) { + sender := common.HexToAddress("0x1234567890123456789012345678901234567890") + require.Equal(t, ethcrypto.CreateAddress(sender, 0), ethCreateAddress(sender, 0)) + require.Equal(t, ethcrypto.CreateAddress(sender, 42), ethCreateAddress(sender, 42)) + require.Equal(t, ethcrypto.CreateAddress(sender, 1_000_000), ethCreateAddress(sender, 1_000_000)) +} + +func TestSendEthereumTransaction_RequiresClientBackref(t *testing.T) { + c := &EVMClient{} // no backref + _, err := c.SendEthereumTransaction(t.Context(), nil, nil, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "not wired") +} + +func TestRawEthereumTx_RequiresClientBackref(t *testing.T) { + c := &EVMClient{} + _, err := c.RawEthereumTx(t.Context(), nil) + require.Error(t, err) + require.Contains(t, err.Error(), "not wired") +} + +func TestRawEthereumTx_RejectsChainIDMismatch(t *testing.T) { + baseClient, err := blockbase.New(t.Context(), blockbase.Config{ + GRPCAddr: "127.0.0.1:1", + EVMChainID: big.NewInt(1414), + EVMNativeDenom: "ulume", + EVMExtendedDenom: "alume", + FeeDenom: "ulume", + InsecureGRPC: true, + }, nil, "") + require.NoError(t, err) + t.Cleanup(func() { _ = baseClient.Close() }) + + signed := signedEVMTestTx(t, big.NewInt(1415)) + _, err = (&EVMClient{client: &Client{Client: baseClient}}).RawEthereumTx(t.Context(), signed) + require.Error(t, err) + require.Contains(t, err.Error(), "chain ID") +} + +func TestSendEthereumTransaction_HappyPath(t *testing.T) { + srv, endpoint := newEVMGRPCTestServer(t) + defer srv.Stop() + + mnemonicFile := writeMnemonicForEVM(t) + kr, _, _, err := sdkcrypto.LoadKeyring("alice", mnemonicFile, sdkcrypto.KeyTypeEVM) + require.NoError(t, err) + + baseClient, err := blockbase.New(t.Context(), blockbase.Config{ + ChainID: "lumera-devnet-1", + GRPCAddr: endpoint, + AccountHRP: "lumera", + FeeDenom: "ulume", + EVMChainID: big.NewInt(1414), + EVMNativeDenom: "ulume", + EVMExtendedDenom: "alume", + InsecureGRPC: true, + WaitTx: clientconfig.WaitTxConfig{ + PollInterval: time.Millisecond, + PollMaxRetries: 3, + PollBackoffMultiplier: 1, + }, + }, kr, "alice") + require.NoError(t, err) + t.Cleanup(func() { _ = baseClient.Close() }) + + to := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + evm := &EVMClient{client: &Client{Client: baseClient}} + res, err := evm.SendEthereumTransaction(t.Context(), &to, []byte{0xaa}, &EthereumTxOptions{ + Nonce: uint64Ptr(7), + GasLimit: 50_000, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(2), + Value: big.NewInt(3), + }) + require.NoError(t, err) + require.NotNil(t, res) + require.NotEmpty(t, res.EthTxHash) + require.Equal(t, "COSMOS_HASH", res.CosmosHash) + require.Equal(t, int64(12), res.Height) +} + +type evmTxServiceServer struct { + txtypes.UnimplementedServiceServer +} + +func (s evmTxServiceServer) BroadcastTx(context.Context, *txtypes.BroadcastTxRequest) (*txtypes.BroadcastTxResponse, error) { + return &txtypes.BroadcastTxResponse{TxResponse: &abcipb.TxResponse{Txhash: "COSMOS_HASH"}}, nil +} + +func (s evmTxServiceServer) GetTx(context.Context, *txtypes.GetTxRequest) (*txtypes.GetTxResponse, error) { + return &txtypes.GetTxResponse{TxResponse: &abcipb.TxResponse{Txhash: "COSMOS_HASH", Height: 12}}, nil +} + +func newEVMGRPCTestServer(t *testing.T) (*grpc.Server, string) { + t.Helper() + lis, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + srv := grpc.NewServer() + txtypes.RegisterServiceServer(srv, evmTxServiceServer{}) + go func() { + _ = srv.Serve(lis) + }() + t.Cleanup(func() { _ = lis.Close() }) + return srv, lis.Addr().String() +} + +func signedEVMTestTx(t *testing.T, chainID *big.Int) *ethtypes.Transaction { + t.Helper() + mnemonicFile := writeMnemonicForEVM(t) + kr, _, _, err := sdkcrypto.LoadKeyring("alice", mnemonicFile, sdkcrypto.KeyTypeEVM) + require.NoError(t, err) + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(2), + Gas: 21_000, + }) + signed, err := sdkcrypto.SignEthereumTx(kr, "alice", chainID, tx) + require.NoError(t, err) + return signed +} + +func uint64Ptr(v uint64) *uint64 { + return &v +} diff --git a/blockchain/evmigration.go b/blockchain/evmigration.go new file mode 100644 index 0000000..14d4f84 --- /dev/null +++ b/blockchain/evmigration.go @@ -0,0 +1,139 @@ +package blockchain + +import ( + "context" + "fmt" + + txtypes "cosmossdk.io/api/cosmos/tx/v1beta1" + evmigrationtypes "github.com/LumeraProtocol/lumera/x/evmigration/types" + blockbase "github.com/LumeraProtocol/sdk-go/blockchain/base" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// EVMigrationClient provides evmigration module query operations. +type EVMigrationClient struct { + query evmigrationtypes.QueryClient +} + +// MigrationResult contains the result of an evmigration tx. +type MigrationResult struct { + LegacyAddress string + NewAddress string + TxHash string + Height int64 +} + +// Params returns the current evmigration module parameters. +func (c *EVMigrationClient) Params(ctx context.Context) (*evmigrationtypes.QueryParamsResponse, error) { + return c.query.Params(ctx, &evmigrationtypes.QueryParamsRequest{}) +} + +// MigrationRecord returns the migration record for a legacy address. +// If no record exists, the underlying query returns its gRPC error. +func (c *EVMigrationClient) MigrationRecord(ctx context.Context, legacyAddress string) (*evmigrationtypes.QueryMigrationRecordResponse, error) { + return c.query.MigrationRecord(ctx, &evmigrationtypes.QueryMigrationRecordRequest{ + LegacyAddress: legacyAddress, + }) +} + +// MigrationRecordByNewAddress returns the migration record for a migrated +// destination address. If no record exists, the underlying query returns its gRPC error. +func (c *EVMigrationClient) MigrationRecordByNewAddress(ctx context.Context, newAddress string) (*evmigrationtypes.QueryMigrationRecordByNewAddressResponse, error) { + return c.query.MigrationRecordByNewAddress(ctx, &evmigrationtypes.QueryMigrationRecordByNewAddressRequest{ + NewAddress: newAddress, + }) +} + +// MigrationEstimate returns a pre-flight estimate of whether the migration +// would succeed, including asset counts and rejection reason. +func (c *EVMigrationClient) MigrationEstimate(ctx context.Context, legacyAddress string) (*evmigrationtypes.QueryMigrationEstimateResponse, error) { + return c.query.MigrationEstimate(ctx, &evmigrationtypes.QueryMigrationEstimateRequest{ + LegacyAddress: legacyAddress, + }) +} + +// MigrationStats returns aggregate migration statistics (total migrated, +// total legacy, etc.). +func (c *EVMigrationClient) MigrationStats(ctx context.Context) (*evmigrationtypes.QueryMigrationStatsResponse, error) { + return c.query.MigrationStats(ctx, &evmigrationtypes.QueryMigrationStatsRequest{}) +} + +// -------- Message Constructors -------- + +// NewMsgClaimLegacyAccount constructs a MsgClaimLegacyAccount. +func NewMsgClaimLegacyAccount(newAddress, legacyAddress string, legacyProof, newProof evmigrationtypes.MigrationProof) *evmigrationtypes.MsgClaimLegacyAccount { + return &evmigrationtypes.MsgClaimLegacyAccount{ + NewAddress: newAddress, + LegacyAddress: legacyAddress, + LegacyProof: legacyProof, + NewProof: newProof, + } +} + +// NewMsgMigrateValidator constructs a MsgMigrateValidator. +func NewMsgMigrateValidator(newAddress, legacyAddress string, legacyProof, newProof evmigrationtypes.MigrationProof) *evmigrationtypes.MsgMigrateValidator { + return &evmigrationtypes.MsgMigrateValidator{ + NewAddress: newAddress, + LegacyAddress: legacyAddress, + LegacyProof: legacyProof, + NewProof: newProof, + } +} + +// -------- Transaction Helpers -------- + +// ClaimLegacyAccountTx builds, signs, broadcasts and confirms a MsgClaimLegacyAccount. +func (c *Client) ClaimLegacyAccountTx(ctx context.Context, msg *evmigrationtypes.MsgClaimLegacyAccount, memo string) (*MigrationResult, error) { + if msg == nil { + return nil, fmt.Errorf("msg is required") + } + + txBytes, err := c.BuildAndSignTxWithOptions(ctx, blockbase.TxBuildOptions{ + Messages: []sdk.Msg{msg}, + Memo: memo, + ZeroFee: true, + }) + if err != nil { + return nil, fmt.Errorf("build and sign tx: %w", err) + } + + txHash, resp, err := c.BroadcastAndWait(ctx, txBytes, txtypes.BroadcastMode_BROADCAST_MODE_SYNC) + if err != nil { + return nil, fmt.Errorf("broadcast and wait: %w", err) + } + + return &MigrationResult{ + LegacyAddress: msg.LegacyAddress, + NewAddress: msg.NewAddress, + TxHash: txHash, + Height: resp.TxResponse.Height, + }, nil +} + +// MigrateValidatorTx builds, signs, broadcasts and confirms a MsgMigrateValidator. +func (c *Client) MigrateValidatorTx(ctx context.Context, msg *evmigrationtypes.MsgMigrateValidator, memo string) (*MigrationResult, error) { + if msg == nil { + return nil, fmt.Errorf("msg is required") + } + + txBytes, err := c.BuildAndSignTxWithOptions(ctx, blockbase.TxBuildOptions{ + Messages: []sdk.Msg{msg}, + Memo: memo, + ZeroFee: true, + }) + if err != nil { + return nil, fmt.Errorf("build and sign tx: %w", err) + } + + txHash, resp, err := c.BroadcastAndWait(ctx, txBytes, txtypes.BroadcastMode_BROADCAST_MODE_SYNC) + if err != nil { + return nil, fmt.Errorf("broadcast and wait: %w", err) + } + + return &MigrationResult{ + LegacyAddress: msg.LegacyAddress, + NewAddress: msg.NewAddress, + TxHash: txHash, + Height: resp.TxResponse.Height, + }, nil +} diff --git a/blockchain/evmigration_test.go b/blockchain/evmigration_test.go new file mode 100644 index 0000000..f3efc94 --- /dev/null +++ b/blockchain/evmigration_test.go @@ -0,0 +1,427 @@ +package blockchain + +import ( + "context" + "fmt" + "net" + "sync" + "testing" + "time" + + abcipb "cosmossdk.io/api/cosmos/base/abci/v1beta1" + txtypes "cosmossdk.io/api/cosmos/tx/v1beta1" + evmigrationtypes "github.com/LumeraProtocol/lumera/x/evmigration/types" + blockbase "github.com/LumeraProtocol/sdk-go/blockchain/base" + clientconfig "github.com/LumeraProtocol/sdk-go/client/config" + "github.com/LumeraProtocol/sdk-go/constants" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" +) + +type fakeEVMigrationServer struct { + evmigrationtypes.UnimplementedQueryServer + + mu sync.Mutex + + params *evmigrationtypes.QueryParamsResponse + record *evmigrationtypes.QueryMigrationRecordResponse + recordByNewAddress *evmigrationtypes.QueryMigrationRecordByNewAddressResponse + estimate *evmigrationtypes.QueryMigrationEstimateResponse + stats *evmigrationtypes.QueryMigrationStatsResponse + notFound bool + lastLegacyAddress string + lastNewAddress string + lastEstimateLegacy string +} + +type evmigrationTxServer struct { + txtypes.UnimplementedServiceServer + authtypes.UnimplementedQueryServer + + mu sync.Mutex + address string + txBytes [][]byte +} + +func (s *fakeEVMigrationServer) Params(_ context.Context, _ *evmigrationtypes.QueryParamsRequest) (*evmigrationtypes.QueryParamsResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.params, nil +} + +func (s *fakeEVMigrationServer) MigrationRecord(_ context.Context, req *evmigrationtypes.QueryMigrationRecordRequest) (*evmigrationtypes.QueryMigrationRecordResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.lastLegacyAddress = req.GetLegacyAddress() + if s.notFound { + return nil, status.Error(codes.NotFound, "no record") + } + return s.record, nil +} + +func (s *fakeEVMigrationServer) MigrationRecordByNewAddress(_ context.Context, req *evmigrationtypes.QueryMigrationRecordByNewAddressRequest) (*evmigrationtypes.QueryMigrationRecordByNewAddressResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.lastNewAddress = req.GetNewAddress() + return s.recordByNewAddress, nil +} + +func (s *fakeEVMigrationServer) MigrationEstimate(_ context.Context, req *evmigrationtypes.QueryMigrationEstimateRequest) (*evmigrationtypes.QueryMigrationEstimateResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.lastEstimateLegacy = req.GetLegacyAddress() + return s.estimate, nil +} + +func (s *fakeEVMigrationServer) MigrationStats(_ context.Context, _ *evmigrationtypes.QueryMigrationStatsRequest) (*evmigrationtypes.QueryMigrationStatsResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + return s.stats, nil +} + +func (s *fakeEVMigrationServer) reads() (legacy, newAddr, estimateLegacy string) { + s.mu.Lock() + defer s.mu.Unlock() + return s.lastLegacyAddress, s.lastNewAddress, s.lastEstimateLegacy +} + +func (s *evmigrationTxServer) Simulate(context.Context, *txtypes.SimulateRequest) (*txtypes.SimulateResponse, error) { + return &txtypes.SimulateResponse{ + GasInfo: &abcipb.GasInfo{GasUsed: 1000}, + }, nil +} + +func (s *evmigrationTxServer) AccountInfo(context.Context, *authtypes.QueryAccountInfoRequest) (*authtypes.QueryAccountInfoResponse, error) { + s.mu.Lock() + sequence := uint64(len(s.txBytes)) + s.mu.Unlock() + + return &authtypes.QueryAccountInfoResponse{ + Info: &authtypes.BaseAccount{ + Address: s.address, + AccountNumber: 7, + Sequence: sequence, + }, + }, nil +} + +func (s *evmigrationTxServer) BroadcastTx(_ context.Context, req *txtypes.BroadcastTxRequest) (*txtypes.BroadcastTxResponse, error) { + s.mu.Lock() + defer s.mu.Unlock() + + s.txBytes = append(s.txBytes, append([]byte(nil), req.TxBytes...)) + return &txtypes.BroadcastTxResponse{ + TxResponse: &abcipb.TxResponse{Txhash: fmt.Sprintf("HASH_%d", len(s.txBytes))}, + }, nil +} + +func (s *evmigrationTxServer) GetTx(_ context.Context, req *txtypes.GetTxRequest) (*txtypes.GetTxResponse, error) { + return &txtypes.GetTxResponse{ + TxResponse: &abcipb.TxResponse{Txhash: req.Hash, Height: 12}, + }, nil +} + +func (s *evmigrationTxServer) capturedTxBytes() [][]byte { + s.mu.Lock() + defer s.mu.Unlock() + + out := make([][]byte, len(s.txBytes)) + for i := range s.txBytes { + out[i] = append([]byte(nil), s.txBytes[i]...) + } + return out +} + +func newEVMigrationTestClient(t *testing.T, handler *fakeEVMigrationServer) *EVMigrationClient { + t.Helper() + + const bufSize = 1024 * 1024 + lis := bufconn.Listen(bufSize) + srv := grpc.NewServer() + t.Cleanup(func() { + srv.Stop() + _ = lis.Close() + }) + + evmigrationtypes.RegisterQueryServer(srv, handler) + go func() { + _ = srv.Serve(lis) + }() + + conn, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + t.Fatalf("dial bufnet: %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + return &EVMigrationClient{ + query: evmigrationtypes.NewQueryClient(conn), + } +} + +func newEVMigrationTxTestClient(t *testing.T, handler *evmigrationTxServer, keyName string) *Client { + t.Helper() + + lis, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen: %v", err) + } + srv := grpc.NewServer() + t.Cleanup(func() { + srv.Stop() + _ = lis.Close() + }) + + txtypes.RegisterServiceServer(srv, handler) + authtypes.RegisterQueryServer(srv, handler) + go func() { + _ = srv.Serve(lis) + }() + + kr, addr := newCosmosSigningKeyringForEVM(t, keyName) + handler.address = addr + baseClient, err := blockbase.New(context.Background(), blockbase.Config{ + ChainID: "lumera-devnet-1", + GRPCAddr: lis.Addr().String(), + AccountHRP: constants.LumeraAccountHRP, + InsecureGRPC: true, + WaitTx: clientconfig.WaitTxConfig{ + PollInterval: time.Millisecond, + PollMaxRetries: 1, + PollBackoffMultiplier: 1, + }, + }, kr, keyName) + if err != nil { + t.Fatalf("base.New: %v", err) + } + t.Cleanup(func() { _ = baseClient.Close() }) + + return &Client{Client: baseClient} +} + +func TestEVMigrationClient_Params(t *testing.T) { + handler := &fakeEVMigrationServer{ + params: &evmigrationtypes.QueryParamsResponse{ + Params: evmigrationtypes.Params{ + EnableMigration: true, + MigrationEndTime: 1234567890, + MaxMigrationsPerBlock: 42, + MaxValidatorDelegations: 1000, + }, + }, + } + c := newEVMigrationTestClient(t, handler) + + resp, err := c.Params(context.Background()) + if err != nil { + t.Fatalf("Params: %v", err) + } + if resp == nil || !resp.Params.EnableMigration { + t.Fatalf("unexpected params response: %+v", resp) + } + if resp.Params.MaxMigrationsPerBlock != 42 { + t.Fatalf("unexpected MaxMigrationsPerBlock: got %d", resp.Params.MaxMigrationsPerBlock) + } +} + +func TestEVMigrationClient_MigrationRecord(t *testing.T) { + const legacy = "lumera1legacyaddrxxxxxxxxxxxxxxxxxxxxxxxxxx" + handler := &fakeEVMigrationServer{ + record: &evmigrationtypes.QueryMigrationRecordResponse{ + Record: &evmigrationtypes.MigrationRecord{ + LegacyAddress: legacy, + NewAddress: "lumera1newaddrxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + MigrationTime: 1700000000, + MigrationHeight: 42, + }, + }, + } + c := newEVMigrationTestClient(t, handler) + + resp, err := c.MigrationRecord(context.Background(), legacy) + if err != nil { + t.Fatalf("MigrationRecord: %v", err) + } + if resp == nil || resp.Record == nil { + t.Fatalf("nil record") + } + if resp.Record.LegacyAddress != legacy { + t.Fatalf("unexpected legacy address: got %q want %q", resp.Record.LegacyAddress, legacy) + } + if resp.Record.MigrationHeight != 42 { + t.Fatalf("unexpected height: got %d", resp.Record.MigrationHeight) + } + + gotLegacy, _, _ := handler.reads() + if gotLegacy != legacy { + t.Fatalf("server saw wrong legacy address: got %q want %q", gotLegacy, legacy) + } +} + +func TestEVMigrationClient_MigrationRecord_NotFound(t *testing.T) { + handler := &fakeEVMigrationServer{notFound: true} + c := newEVMigrationTestClient(t, handler) + + resp, err := c.MigrationRecord(context.Background(), "lumera1missing") + if err == nil { + t.Fatalf("expected error, got resp=%+v", resp) + } + if status.Code(err) != codes.NotFound { + t.Fatalf("expected NotFound, got %v", err) + } +} + +func TestEVMigrationClient_MigrationRecordByNewAddress(t *testing.T) { + const newAddr = "lumera1newaddrxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + handler := &fakeEVMigrationServer{ + recordByNewAddress: &evmigrationtypes.QueryMigrationRecordByNewAddressResponse{ + Record: &evmigrationtypes.MigrationRecord{ + LegacyAddress: "lumera1legacyaddrxxxxxxxxxxxxxxxxxxxxxxxxxx", + NewAddress: newAddr, + }, + }, + } + c := newEVMigrationTestClient(t, handler) + + resp, err := c.MigrationRecordByNewAddress(context.Background(), newAddr) + if err != nil { + t.Fatalf("MigrationRecordByNewAddress: %v", err) + } + if resp == nil || resp.Record == nil || resp.Record.NewAddress != newAddr { + t.Fatalf("unexpected response: %+v", resp) + } + + _, gotNew, _ := handler.reads() + if gotNew != newAddr { + t.Fatalf("server saw wrong new address: got %q want %q", gotNew, newAddr) + } +} + +func TestEVMigrationClient_MigrationEstimate(t *testing.T) { + const legacy = "lumera1estimatemexxxxxxxxxxxxxxxxxxxxxxxxxx" + handler := &fakeEVMigrationServer{ + estimate: &evmigrationtypes.QueryMigrationEstimateResponse{ + IsValidator: true, + DelegationCount: 5, + UnbondingCount: 1, + TotalTouched: 6, + WouldSucceed: true, + }, + } + c := newEVMigrationTestClient(t, handler) + + resp, err := c.MigrationEstimate(context.Background(), legacy) + if err != nil { + t.Fatalf("MigrationEstimate: %v", err) + } + if !resp.WouldSucceed || !resp.IsValidator || resp.TotalTouched != 6 { + t.Fatalf("unexpected estimate: %+v", resp) + } + + _, _, gotLegacy := handler.reads() + if gotLegacy != legacy { + t.Fatalf("server saw wrong legacy address: got %q want %q", gotLegacy, legacy) + } +} + +func TestEVMigrationClient_MigrationStats(t *testing.T) { + handler := &fakeEVMigrationServer{ + stats: &evmigrationtypes.QueryMigrationStatsResponse{ + TotalMigrated: 10, + TotalLegacy: 100, + TotalLegacyStaked: 50, + TotalValidatorsMigrated: 2, + TotalValidatorsLegacy: 3, + }, + } + c := newEVMigrationTestClient(t, handler) + + resp, err := c.MigrationStats(context.Background()) + if err != nil { + t.Fatalf("MigrationStats: %v", err) + } + if resp.TotalMigrated != 10 || resp.TotalLegacy != 100 { + t.Fatalf("unexpected stats: %+v", resp) + } +} + +func TestNewMsgClaimLegacyAccount(t *testing.T) { + legacyProof := evmigrationtypes.MigrationProof{} + newProof := evmigrationtypes.MigrationProof{} + msg := NewMsgClaimLegacyAccount("new", "legacy", legacyProof, newProof) + if msg.NewAddress != "new" || msg.LegacyAddress != "legacy" { + t.Fatalf("unexpected msg: %+v", msg) + } +} + +func TestNewMsgMigrateValidator(t *testing.T) { + legacyProof := evmigrationtypes.MigrationProof{} + newProof := evmigrationtypes.MigrationProof{} + msg := NewMsgMigrateValidator("new", "legacy", legacyProof, newProof) + if msg.NewAddress != "new" || msg.LegacyAddress != "legacy" { + t.Fatalf("unexpected msg: %+v", msg) + } +} + +func TestEVMigrationTxHelpers_BuildZeroFeeTxs(t *testing.T) { + handler := &evmigrationTxServer{} + c := newEVMigrationTxTestClient(t, handler, "alice") + newAddr := handler.address + legacyAddr := "lumera1legacyaddrxxxxxxxxxxxxxxxxxxxxxxxxxx" + + helpers := []struct { + name string + run func(context.Context) (*MigrationResult, error) + }{ + { + name: "claim legacy account", + run: func(ctx context.Context) (*MigrationResult, error) { + msg := NewMsgClaimLegacyAccount(newAddr, legacyAddr, evmigrationtypes.MigrationProof{}, evmigrationtypes.MigrationProof{}) + return c.ClaimLegacyAccountTx(ctx, msg, "claim") + }, + }, + { + name: "migrate validator", + run: func(ctx context.Context) (*MigrationResult, error) { + msg := NewMsgMigrateValidator(newAddr, legacyAddr, evmigrationtypes.MigrationProof{}, evmigrationtypes.MigrationProof{}) + return c.MigrateValidatorTx(ctx, msg, "validator") + }, + }, + } + + for _, helper := range helpers { + if _, err := helper.run(context.Background()); err != nil { + t.Fatalf("%s: %v", helper.name, err) + } + } + + txBytes := handler.capturedTxBytes() + if len(txBytes) != len(helpers) { + t.Fatalf("captured %d txs, want %d", len(txBytes), len(helpers)) + } + for i, raw := range txBytes { + decoded, err := sdkcrypto.NewDefaultTxConfig().TxDecoder()(raw) + if err != nil { + t.Fatalf("decode tx %d: %v", i, err) + } + feeTx, ok := decoded.(sdk.FeeTx) + if !ok { + t.Fatalf("tx %d does not implement sdk.FeeTx", i) + } + if !feeTx.GetFee().Empty() { + t.Fatalf("tx %d fee = %s, want zero fee", i, feeTx.GetFee()) + } + } +} diff --git a/blockchain/feemarket.go b/blockchain/feemarket.go new file mode 100644 index 0000000..e3bc158 --- /dev/null +++ b/blockchain/feemarket.go @@ -0,0 +1,49 @@ +package blockchain + +import ( + "context" + "fmt" + + sdkmath "cosmossdk.io/math" + feemarkettypes "github.com/cosmos/evm/x/feemarket/types" +) + +// FeeMarketClient provides x/feemarket module query operations. +type FeeMarketClient struct { + query feemarkettypes.QueryClient +} + +// Params returns the current feemarket module parameters. +func (c *FeeMarketClient) Params(ctx context.Context) (*feemarkettypes.QueryParamsResponse, error) { + return c.query.Params(ctx, &feemarkettypes.QueryParamsRequest{}) +} + +// BaseFee returns the EIP-1559 base fee of the parent block, expressed in the +// native denom (ulume on Lumera). Callers that need an integer wei-like value +// for EIP-1559 fee fields should pass the result through +// pkg/crypto.ULumeDecToWei. +func (c *FeeMarketClient) BaseFee(ctx context.Context) (sdkmath.LegacyDec, error) { + resp, err := c.query.BaseFee(ctx, &feemarkettypes.QueryBaseFeeRequest{}) + if err != nil { + return sdkmath.LegacyDec{}, err + } + if resp == nil || resp.BaseFee == nil { + return sdkmath.LegacyZeroDec(), nil + } + return *resp.BaseFee, nil +} + +// BlockGas returns the gas used at the most recent block. +func (c *FeeMarketClient) BlockGas(ctx context.Context) (int64, error) { + resp, err := c.query.BlockGas(ctx, &feemarkettypes.QueryBlockGasRequest{}) + if err != nil { + return 0, err + } + if resp == nil { + return 0, fmt.Errorf("empty block gas response") + } + if resp.Gas < 0 { + return 0, fmt.Errorf("block gas response is negative: %d", resp.Gas) + } + return resp.Gas, nil +} diff --git a/blockchain/messages.go b/blockchain/messages.go deleted file mode 100644 index d37fce2..0000000 --- a/blockchain/messages.go +++ /dev/null @@ -1,3 +0,0 @@ -package blockchain - -// Deprecated: Message constructors have been moved into action.go and supernode.go. \ No newline at end of file diff --git a/blockchain/precisebank.go b/blockchain/precisebank.go new file mode 100644 index 0000000..50d9e45 --- /dev/null +++ b/blockchain/precisebank.go @@ -0,0 +1,41 @@ +package blockchain + +import ( + "context" + + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// PreciseBankClient provides x/precisebank module query operations. +type PreciseBankClient struct { + query precisebanktypes.QueryClient +} + +// Remainder returns the precisebank reserve amount not yet owned by any +// account (sub-ulume accounting bucket). +func (c *PreciseBankClient) Remainder(ctx context.Context) (sdk.Coin, error) { + resp, err := c.query.Remainder(ctx, &precisebanktypes.QueryRemainderRequest{}) + if err != nil { + return sdk.Coin{}, err + } + if resp == nil { + return sdk.Coin{}, nil + } + return resp.Remainder, nil +} + +// FractionalBalance returns the sub-ulume fractional balance for an address. +// Does not include the integer balance stored in x/bank. +func (c *PreciseBankClient) FractionalBalance(ctx context.Context, address string) (sdk.Coin, error) { + resp, err := c.query.FractionalBalance(ctx, &precisebanktypes.QueryFractionalBalanceRequest{ + Address: address, + }) + if err != nil { + return sdk.Coin{}, err + } + if resp == nil { + return sdk.Coin{}, nil + } + return resp.FractionalBalance, nil +} diff --git a/blockchain/precompiles.go b/blockchain/precompiles.go new file mode 100644 index 0000000..b96b532 --- /dev/null +++ b/blockchain/precompiles.go @@ -0,0 +1,58 @@ +package blockchain + +import ( + "context" + "fmt" + + "github.com/LumeraProtocol/sdk-go/pkg/evm/precompiles" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +// PrecompileClient wraps a Lumera precompile (action, supernode, or wasm) so +// callers can invoke any of its methods without packing calldata manually. +// +// Use Call for read-only methods (forwarded through EthCall) and Send for +// state-changing methods (signed and broadcast as a MsgEthereumTx). +type PrecompileClient struct { + address common.Address + abi abi.ABI + evm *EVMClient +} + +// Address returns the precompile's fixed Ethereum address. +func (p *PrecompileClient) Address() common.Address { return p.address } + +// ABI returns the parsed ABI so callers can introspect method signatures. +func (p *PrecompileClient) ABI() abi.ABI { return p.abi } + +// Call invokes a read-only precompile method and returns the unpacked +// outputs in the order the Solidity interface declares them. +func (p *PrecompileClient) Call(ctx context.Context, method string, args ...any) ([]any, error) { + if p.evm == nil { + return nil, fmt.Errorf("precompile not wired to an EVMClient") + } + data, err := precompiles.PackCall(p.abi, method, args...) + if err != nil { + return nil, err + } + ret, err := p.evm.CallContract(ctx, p.address, data) + if err != nil { + return nil, err + } + return precompiles.UnpackReturn(p.abi, method, ret) +} + +// Send signs and broadcasts a state-changing precompile call. opts may be +// nil; nonce/gas/fees are resolved from chain state when unset. +func (p *PrecompileClient) Send(ctx context.Context, method string, opts *EthereumTxOptions, args ...any) (*EthereumTransactionResult, error) { + if p.evm == nil { + return nil, fmt.Errorf("precompile not wired to an EVMClient") + } + data, err := precompiles.PackCall(p.abi, method, args...) + if err != nil { + return nil, err + } + addr := p.address + return p.evm.SendEthereumTransaction(ctx, &addr, data, opts) +} diff --git a/blockchain/precompiles_test.go b/blockchain/precompiles_test.go new file mode 100644 index 0000000..004a5df --- /dev/null +++ b/blockchain/precompiles_test.go @@ -0,0 +1,39 @@ +package blockchain + +import ( + "context" + "testing" + + "github.com/LumeraProtocol/sdk-go/pkg/evm/precompiles" + "github.com/stretchr/testify/require" +) + +func TestPrecompileClient_Address(t *testing.T) { + p := &PrecompileClient{address: precompiles.ActionAddress, abi: precompiles.ActionABI} + require.Equal(t, precompiles.ActionAddress, p.Address()) + require.NotEmpty(t, p.ABI().Methods) +} + +func TestPrecompileClient_CallWithoutEVMClient(t *testing.T) { + p := &PrecompileClient{address: precompiles.ActionAddress, abi: precompiles.ActionABI} + _, err := p.Call(context.Background(), "getParams") + require.Error(t, err) + require.Contains(t, err.Error(), "not wired") +} + +func TestPrecompileClient_SendWithoutEVMClient(t *testing.T) { + p := &PrecompileClient{address: precompiles.ActionAddress, abi: precompiles.ActionABI} + _, err := p.Send(context.Background(), "approveAction", nil, "some-action-id") + require.Error(t, err) + require.Contains(t, err.Error(), "not wired") +} + +func TestPrecompileClient_RejectsUnknownMethod(t *testing.T) { + p := &PrecompileClient{ + address: precompiles.ActionAddress, + abi: precompiles.ActionABI, + evm: &EVMClient{}, + } + _, err := p.Call(context.Background(), "thisMethodDoesNotExist") + require.Error(t, err) +} diff --git a/client/client.go b/client/client.go index 6587129..b02c9f1 100644 --- a/client/client.go +++ b/client/client.go @@ -37,13 +37,21 @@ func New(ctx context.Context, cfg Config, kr keyring.Keyring, opts ...Option) (* // Initialize blockchain client blockchainClient, err := blockchain.New(ctx, blockchain.Config{ - ChainID: cfg.ChainID, - GRPCAddr: cfg.GRPCEndpoint, - RPCEndpoint: cfg.RPCEndpoint, - Timeout: cfg.BlockchainTimeout, - MaxRecvMsgSize: cfg.MaxRecvMsgSize, - MaxSendMsgSize: cfg.MaxSendMsgSize, - WaitTx: cfg.WaitTx, + ChainID: cfg.ChainID, + GRPCAddr: cfg.GRPCEndpoint, + RPCEndpoint: cfg.RPCEndpoint, + AccountHRP: cfg.AccountHRP, + FeeDenom: cfg.FeeDenom, + GasPrice: cfg.GasPrice, + Timeout: cfg.BlockchainTimeout, + MaxRecvMsgSize: cfg.MaxRecvMsgSize, + MaxSendMsgSize: cfg.MaxSendMsgSize, + WaitTx: cfg.WaitTx, + EVMChainID: cfg.EVMChainID, + EVMNativeDenom: cfg.EVMNativeDenom, + EVMExtendedDenom: cfg.EVMExtendedDenom, + EVMGasTipCap: cfg.EVMGasTipCap, + EVMGasFeeCap: cfg.EVMGasFeeCap, }, kr, cfg.KeyName) if err != nil { return nil, fmt.Errorf("failed to initialize blockchain client: %w", err) diff --git a/client/config/config.go b/client/config/config.go index db7af8a..e97fe6d 100644 --- a/client/config/config.go +++ b/client/config/config.go @@ -2,9 +2,11 @@ package config import ( "fmt" + "math/big" "strings" "time" + sdkmath "cosmossdk.io/math" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -20,14 +22,21 @@ type Config struct { Address string // Your cosmos address (lumera1...) KeyName string // Key name in keyring + // Chain economics overrides. Empty/nil values fall back to Lumera defaults + // applied by blockchain.New (AccountHRP "lumera", FeeDenom "ulume", + // GasPrice 0.025). Set these for non-Lumera chains. + AccountHRP string // bech32 account prefix (Lumera: "lumera") + FeeDenom string // cosmos fee denom for non-EVM txs (Lumera: "ulume") + GasPrice sdkmath.LegacyDec // gas price in FeeDenom/gas (Lumera: 0.025) + // Timeouts BlockchainTimeout time.Duration StorageTimeout time.Duration // Optional overrides MaxRetries int - MaxRecvMsgSize int // Max message size for gRPC (default: 50MB) - MaxSendMsgSize int + MaxRecvMsgSize int // Max receive message size for gRPC (default: 4MB) + MaxSendMsgSize int // Max send message size for gRPC (default: 50MB) // WaitTx controls transaction confirmation behaviour. WaitTx WaitTxConfig @@ -37,6 +46,13 @@ type Config struct { // Logger is optional; when set, SDK operations emit diagnostics. Logger *zap.Logger + + // EVM settings (cosmos/evm). Set when the chain has EVM enabled. + EVMChainID *big.Int // EIP-155 chain ID (distinct from cosmos ChainID) + EVMNativeDenom string // cosmos/evm `evm_denom` (Lumera: "ulume") + EVMExtendedDenom string // 18-decimal precisebank denom (Lumera: "alume") + EVMGasTipCap *big.Int // optional default tip cap (alume/gas) + EVMGasFeeCap *big.Int // optional default fee cap (alume/gas) } // WaitTxConfig configures how the SDK waits for transaction inclusion. @@ -94,7 +110,7 @@ func (c *Config) Validate() error { c.StorageTimeout = 5 * time.Minute } if c.MaxRecvMsgSize == 0 { - c.MaxRecvMsgSize = 1024 * 1024 * 50 // 50MB + c.MaxRecvMsgSize = 1024 * 1024 * 4 // 4MB } if c.MaxSendMsgSize == 0 { c.MaxSendMsgSize = 1024 * 1024 * 50 // 50MB @@ -116,7 +132,7 @@ func Default() Config { BlockchainTimeout: 10 * time.Second, StorageTimeout: 5 * time.Minute, MaxRetries: 3, - MaxRecvMsgSize: 1024 * 1024 * 50, + MaxRecvMsgSize: 1024 * 1024 * 4, MaxSendMsgSize: 1024 * 1024 * 50, LogLevel: "error", WaitTx: DefaultWaitTxConfig(), diff --git a/client/config/config_test.go b/client/config/config_test.go new file mode 100644 index 0000000..847e690 --- /dev/null +++ b/client/config/config_test.go @@ -0,0 +1,39 @@ +package config + +import ( + "testing" + "time" +) + +func TestDefaultMessageSizes(t *testing.T) { + cfg := Default() + + if cfg.MaxRecvMsgSize != 4*1024*1024 { + t.Fatalf("MaxRecvMsgSize = %d, want 4MiB", cfg.MaxRecvMsgSize) + } + if cfg.MaxSendMsgSize != 50*1024*1024 { + t.Fatalf("MaxSendMsgSize = %d, want 50MiB", cfg.MaxSendMsgSize) + } +} + +func TestValidateAppliesDefaultMessageSizes(t *testing.T) { + cfg := Config{ + ChainID: "lumera-devnet-1", + GRPCEndpoint: "localhost:9090", + RPCEndpoint: "http://localhost:26657", + Address: "lumera1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqduux2w", + KeyName: "alice", + BlockchainTimeout: time.Second, + StorageTimeout: time.Second, + } + + if err := cfg.Validate(); err != nil { + t.Fatalf("Validate: %v", err) + } + if cfg.MaxRecvMsgSize != 4*1024*1024 { + t.Fatalf("MaxRecvMsgSize = %d, want 4MiB", cfg.MaxRecvMsgSize) + } + if cfg.MaxSendMsgSize != 50*1024*1024 { + t.Fatalf("MaxSendMsgSize = %d, want 50MiB", cfg.MaxSendMsgSize) + } +} diff --git a/client/options.go b/client/options.go index 216cf17..d2308b2 100644 --- a/client/options.go +++ b/client/options.go @@ -1,8 +1,10 @@ package client import ( + "math/big" "time" + sdkmath "cosmossdk.io/math" clientconfig "github.com/LumeraProtocol/sdk-go/client/config" "go.uber.org/zap" ) @@ -87,3 +89,59 @@ func WithLogger(logger *zap.Logger) Option { c.Logger = logger } } + +// WithAccountHRP sets the bech32 account address prefix (e.g. "lumera"). +// Empty leaves the Lumera default applied by blockchain.New. +func WithAccountHRP(hrp string) Option { + return func(c *Config) { + c.AccountHRP = hrp + } +} + +// WithFeeDenom sets the cosmos fee denom used for non-EVM transactions +// (e.g. "ulume"). Empty leaves the Lumera default applied by blockchain.New. +func WithFeeDenom(denom string) Option { + return func(c *Config) { + c.FeeDenom = denom + } +} + +// WithGasPrice sets the gas price in FeeDenom/gas (e.g. 0.025). A nil/zero +// LegacyDec leaves the Lumera default applied by blockchain.New. +func WithGasPrice(price sdkmath.LegacyDec) Option { + return func(c *Config) { + c.GasPrice = price + } +} + +// WithEVMChainID sets the EIP-155 chain ID used for Ethereum-format +// transactions. Distinct from the Cosmos ChainID. Required for EVM tx helpers. +func WithEVMChainID(id *big.Int) Option { + return func(c *Config) { + c.EVMChainID = id + } +} + +// WithEVMNativeDenom sets the cosmos/evm `evm_denom` (e.g. "ulume"). +func WithEVMNativeDenom(denom string) Option { + return func(c *Config) { + c.EVMNativeDenom = denom + } +} + +// WithEVMExtendedDenom sets the 18-decimal precisebank denom (e.g. "alume"). +func WithEVMExtendedDenom(denom string) Option { + return func(c *Config) { + c.EVMExtendedDenom = denom + } +} + +// WithEVMGasCaps sets optional defaults for EIP-1559 gas pricing in the +// extended denom's integer unit (alume/gas). Nil means "fetch from chain +// state at tx time". +func WithEVMGasCaps(tip, fee *big.Int) Option { + return func(c *Config) { + c.EVMGasTipCap = tip + c.EVMGasFeeCap = fee + } +} diff --git a/client/options_test.go b/client/options_test.go new file mode 100644 index 0000000..5f507c9 --- /dev/null +++ b/client/options_test.go @@ -0,0 +1,35 @@ +package client + +import ( + "testing" + + sdkmath "cosmossdk.io/math" +) + +func TestWithAccountSettings(t *testing.T) { + cfg := Config{} + + WithAccountHRP("cosmos")(&cfg) + WithFeeDenom("uatom")(&cfg) + gp := sdkmath.LegacyNewDecWithPrec(5, 2) // 0.05 + WithGasPrice(gp)(&cfg) + + if cfg.AccountHRP != "cosmos" { + t.Fatalf("AccountHRP = %q, want cosmos", cfg.AccountHRP) + } + if cfg.FeeDenom != "uatom" { + t.Fatalf("FeeDenom = %q, want uatom", cfg.FeeDenom) + } + if cfg.GasPrice.IsNil() || !cfg.GasPrice.Equal(gp) { + t.Fatalf("GasPrice = %s, want %s", cfg.GasPrice, gp) + } +} + +// An unset GasPrice must be a nil LegacyDec so that blockchain.New treats it as +// "not configured" and applies the chain default rather than a literal zero. +func TestGasPriceZeroValueIsNil(t *testing.T) { + cfg := Config{} + if !cfg.GasPrice.IsNil() { + t.Fatal("unset GasPrice should be nil so the chain default is applied") + } +} diff --git a/docs/API.md b/docs/API.md index 7c7bd6f..1bfb909 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,12 +1,23 @@ # Lumera Go SDK โ€“ API Overview -This is a concise map of the exported Go surface. For full GoDoc see `pkg.go.dev/github.com/LumeraProtocol/sdk-go`. +This is a concise map of the exported Go surface. For task-oriented tutorials see the [Developer Guide](DEVELOPER_GUIDE.md). For full GoDoc see `pkg.go.dev/github.com/LumeraProtocol/sdk-go`. + +Quick links by topic: + +| Topic | Guide | Packages | +| --- | --- | --- | +| Setup / configuration | [Getting Started](guides/getting-started.md) | `client`, `client/config` | +| Keys / signing | [Crypto](guides/crypto.md) | `pkg/crypto` | +| Actions, SuperNodes | [Actions](guides/actions.md) | `blockchain` | +| Cascade storage | [Cascade](guides/cascade.md) | `cascade` | +| Interchain Accounts | [ICA](guides/ica.md) | `ica` | +| EVM, ERC20, precompiles | [EVM](guides/evm.md) | `blockchain`, `pkg/evm/precompiles` | ## Package `client` - `client.New(ctx, Config, keyring, opts...) (*Client, error)` builds a unified client exposing `Blockchain` and `Cascade`. -- `Config` (alias of `client/config.Config`): chain endpoints, address/key, timeouts, wait-tx config, message sizes, retries, optional logger. -- Options: `WithChainID`, `WithKeyName`, `WithGRPCEndpoint`, `WithRPCEndpoint`, `WithBlockchainTimeout`, `WithStorageTimeout`, `WithMaxRetries`, `WithMaxMessageSize`, `WithWaitTxConfig`, `WithLogLevel`, `WithLogger`. +- `Config` (alias of `client/config.Config`): chain endpoints, address/key, timeouts, wait-tx config, message sizes, retries, optional logger, plus chain-economics overrides (`AccountHRP`, `FeeDenom`, `GasPrice`) that default to Lumera values when unset. The default gRPC receive limit is 4 MiB; callers can opt up when a chain endpoint needs larger responses. +- Options: `WithChainID`, `WithKeyName`, `WithGRPCEndpoint`, `WithRPCEndpoint`, `WithBlockchainTimeout`, `WithStorageTimeout`, `WithMaxRetries`, `WithMaxMessageSize`, `WithWaitTxConfig`, `WithLogLevel`, `WithLogger`, `WithAccountHRP`, `WithFeeDenom`, `WithGasPrice`, `WithEVMChainID`, `WithEVMNativeDenom`, `WithEVMExtendedDenom`, `WithEVMGasCaps`. - `Client.Blockchain` is a `*blockchain.Client`; `Client.Cascade` is a `*cascade.Client`. `Close()` tears both down. - `NewFactory` captures a base config/keyring for multi-signer flows; `Factory.WithSigner` returns a per-signer `Client`. @@ -31,7 +42,22 @@ This is a concise map of the exported Go surface. For full GoDoc see `pkg.go.dev - Queries: `GetSuperNode`, `GetSuperNodeBySuperNodeAddress`, `ListSuperNodes`, `GetTopSuperNodesForBlock`, `GetTopSuperNodesForBlockWithOptions`, `Params`. - Tx helpers: `RegisterSupernodeTx`, `DeregisterSupernodeTx`, `StartSupernodeTx`, `StopSupernodeTx`, `UpdateSupernodeTx`, `UpdateSuperNodeParamsTx`. Message constructors mirror these names. - Claim and Audit modules: query clients are wired; add methods as the chain exposes additional endpoints. -- Shared tx utilities: `BuildAndSignTx`, `Simulate`, `Broadcast`, `WaitForTxInclusion`, `GetTx`, `ExtractEventAttribute` (for parsing event attributes like `action_id`). +- EVMigration module: + - Queries: `Params`, `MigrationRecord`, `MigrationRecordByNewAddress`, `MigrationEstimate`, `MigrationStats`. + - Tx helpers: `ClaimLegacyAccountTx`, `MigrateValidatorTx` build zero-fee migration transactions. Message constructors: `NewMsgClaimLegacyAccount`, `NewMsgMigrateValidator`. Result type: `MigrationResult` (legacy/new address, tx hash, height). +- EVM module (x/vm): + - Queries: `Code`, `Storage`, `Balance` (alume), `EthAccount`, `CosmosAccount`, `Params`, `BaseFee` (alume), `Config`, `GlobalMinGasPrice`, `EthCall`, `EstimateGas`, `TraceTx`. + - Tx helpers: `SendEthereumTransaction`, `DeployContract`, `CallContract`, `RawEthereumTx`. Options struct: `EthereumTxOptions` (Nonce/GasLimit/GasTipCap/GasFeeCap/Value/AccessList). Result: `EthereumTransactionResult` (eth tx hash, cosmos hash, height, gas used, vm error, return data, logs). + - Precompile wrappers (`EVM.Action`, `EVM.Supernode`, `EVM.Wasm`): generic `Call(ctx, method, args...)` and `Send(ctx, method, opts, args...)` route through the precompile address using the embedded ABI; addresses + ABIs live in [`pkg/evm/precompiles`](../pkg/evm/precompiles). +- ERC20 module (x/erc20): + - Queries: `TokenPairs` (paginated), `TokenPair`, `Params`. + - Tx helpers: `ConvertCoinToERC20`, `ConvertERC20ToCoin`, `RegisterERC20Tx`, `ToggleConversionTx`. Message constructors: `NewMsgConvertCoin`, `NewMsgConvertERC20`, `NewMsgRegisterERC20`, `NewMsgToggleConversion`. Result: `ConversionResult`. + - ABI sugar: `Erc20Balance`, `Erc20TotalSupply`, `Erc20Allowance`, `Erc20Metadata` issue ABI-packed read calls through the EVM client. +- FeeMarket module (x/feemarket): + - Queries: `Params`, `BaseFee` (ulume decimal), `BlockGas`. +- PreciseBank module (x/precisebank): + - Queries: `Remainder`, `FractionalBalance` (sub-ulume sub-balances backing 18-decimal EVM views). +- Shared tx utilities: `BuildAndSignTx`, `BuildAndSignTxWithGasAdjustment`, `BuildAndSignTxWithOptions`, `PrepareTx` + `SignPreparedTx`, `Simulate`, `Broadcast`, `BroadcastAndWait`, `WaitForTxInclusion`, `GetTx`, `GetTxsByEvents`, `ExtractEventAttribute` (for parsing event attributes like `action_id`). `BuildAndSignTxWithOptions` rejects `MsgEthereumTx` to prevent accidental double-signing, and its `ZeroFee` option intentionally emits no fee coins for chain-waived flows such as evmigration. ## Package `types` @@ -49,8 +75,18 @@ Crypto helpers for keyring management, key import, address derivation, and trans - `LoadKeyring(keyName, mnemonicFile string, keyType KeyType) (keyring.Keyring, []byte, string, error)`: creates a test keyring and imports a mnemonic with the given key type; returns the keyring, pubkey bytes, and Lumera address. - `ImportKey(kr keyring.Keyring, keyName, mnemonicFile, hrp string, keyType KeyType) ([]byte, string, error)`: imports a mnemonic into an existing keyring under the given key name and key type; returns pubkey bytes and address for the specified HRP. - `AddressFromKey(kr, keyName, hrp) (string, error)`: derives an HRP-specific bech32 address from a keyring key without mutating global config. -- `NewDefaultTxConfig() client.TxConfig`: builds a protobuf tx config with Lumera action and crypto interfaces registered. +- `EVMAddressFromKey(kr, keyName) (string, error)`: derives the 0x-prefixed EIP-55 hex address for an `eth_secp256k1` key; returns an error for `secp256k1` keys. +- `NewDefaultTxConfig() client.TxConfig`: builds a protobuf tx config with Lumera action and crypto interfaces plus the EVM modules (`evmigration`, `erc20`, `feemarket`, `precisebank`, `vm`) registered. - `SignTxWithKeyring(kr, keyName, chainID string, txBuilder, txConfig) ([]byte, error)`: signs a transaction using Cosmos SDK builders. +- `SignEthereumTx(kr, keyName, chainID *big.Int, tx *ethtypes.Transaction)`: signs a go-ethereum tx using the keyring's `eth_secp256k1` key with the latest EIP-155 signer. `WrapAsMsgEthereumTx(signed)` packages it as a `MsgEthereumTx`; `RecoverSender(signed)` returns the recovered EVM address. +- `EVMToBech32(addr, hrp)` / `Bech32ToEVM(bech32Addr)`: byte-level conversion between 20-byte EVM addresses and bech32 (round-trips only for `eth_secp256k1`-derived accounts). +- `Wei(ulume)` / `ULume(alume)` / `ULumeDecToWei(dec)`: bridge the 6โ†”18 decimal boundary defined by `x/precisebank` (10^12 factor). + +## Package `pkg/evm/precompiles` + +- Embedded Hardhat-format ABIs for Lumera's custom precompiles (`ActionABI`, `SupernodeABI`, `WasmABI`) plus named address constants (`ActionAddress` `0x0901`, `SupernodeAddress` `0x0902`, `WasmAddress` `0x0903`). +- Address constants for the 8 standard cosmos/evm precompiles (`P256Address`, `Bech32Address`, `StakingAddress`, `DistributionAddress`, `ICS20Address`, `BankAddress`, `GovAddress`, `SlashingAddress`). +- Generic helpers: `PackCall(abi, method, args...)`, `UnpackReturn(abi, method, ret)`. ## Package `ica` diff --git a/docs/DEVELOPER_GUIDE.md b/docs/DEVELOPER_GUIDE.md index 4c256f5..a1170fa 100644 --- a/docs/DEVELOPER_GUIDE.md +++ b/docs/DEVELOPER_GUIDE.md @@ -1,316 +1,32 @@ # Lumera Go SDK โ€“ Developer Guide -This guide is for engineers building on the Lumera blockchain and Cascade storage. It shows how to configure the SDK, call the unified client, and run the included examples. +This is the entry point for engineers building on Lumera. The guide is split by topic โ€” each link below is self-contained. -## Prerequisites +## Topics -- Go 1.25+ with module support. -- Access to Lumera endpoints: `grpc` (chain queries/tx), `rpc` (websocket for tx inclusion), and at least one SuperNode for Cascade uploads/downloads. -- A Cosmos keyring entry that can sign Lumera transactions (`github.com/cosmos/cosmos-sdk/crypto/keyring` is used throughout the SDK). +- **[Getting Started](guides/getting-started.md)** โ€” prerequisites, install, configuration reference, creating a client, multi-signer factory, running examples, troubleshooting. +- **[Crypto Helpers](guides/crypto.md)** โ€” `pkg/crypto`: key types (Cosmos / EVM), keyring creation, mnemonic import, address derivation (bech32 + 0x), Ethereum-format signing primitives, decimal bridging (`Wei` / `ULume`). +- **[Actions and SuperNodes](guides/actions.md)** โ€” query / tx helpers for `x/action` and `x/supernode`. +- **[Cascade Storage](guides/cascade.md)** โ€” file upload / download, event subscriptions, stepwise control over the SuperNode + on-chain flow. +- **[Interchain Accounts](guides/ica.md)** โ€” `ica.Controller`, packet-building helpers, and when to use it vs. `interchaintest`. +- **[EVM Integration](guides/evm.md)** โ€” `x/vm`, `x/erc20`, `x/feemarket`, `x/precisebank`, custom precompiles (`0x0901`/`0x0902`/`0x0903`), and `x/evmigration`. -## Install and Configure +## Reference -```bash -go get github.com/LumeraProtocol/sdk-go -``` +- [API.md](API.md) โ€” exhaustive list of exported types, functions, and options. +- [docs/design/](design/) โ€” design records that motivate the larger API surfaces. -### Configuration reference +## Examples -`client.Config` (in `client/config`) drives both blockchain and Cascade clients: +All examples live in `examples/`. The most useful entry points: -- `ChainID`, `GRPCEndpoint`, `RPCEndpoint` โ€“ chain connection details. gRPC uses TLS automatically for non-local hosts/port 443. -- `Address`, `KeyName` โ€“ Cosmos account info in your keyring. -- `BlockchainTimeout`, `StorageTimeout` โ€“ default deadlines for chain and Cascade operations. -- `MaxRecvMsgSize`, `MaxSendMsgSize`, `MaxRetries` โ€“ transport tuning. -- `WaitTx` โ€“ controls websocket vs polling behaviour when waiting for tx inclusion (see defaults in `client/config`). -- `Logger` โ€“ optional; when set, SDK operations emit diagnostics. -- `LogLevel` โ€“ default logging threshold when no custom logger is supplied (default: error). +- `examples/query-actions` โ€” read-only Action module queries. +- `examples/cascade-upload` / `cascade-download` โ€” Cascade storage end-to-end. +- `examples/multi-account` โ€” `client.NewFactory` with multiple signers. +- `examples/ica-request-tx` โ€” build an ICA packet for a controller-chain broadcast. +- `examples/evm-transfer` โ€” sign and broadcast an EIP-1559 transaction. +- `examples/evm-balance` โ€” read EVM balance, nonce, base fee. +- `examples/erc20-convert` โ€” `ConvertCoinToERC20` and `ConvertERC20ToCoin`. +- `examples/precompile-action` โ€” invoke a Lumera precompile method. -You can override fields with `client.With...` option helpers when calling `client.New`. - -### Creating a client - -```go -ctx := context.Background() -kr, _ := keyring.New("lumera", "test", "/tmp", nil) - -cfg := client.Config{ - ChainID: "lumera-testnet-2", - GRPCEndpoint: "localhost:9090", - RPCEndpoint: "http://localhost:26657", - Address: "lumera1abc...", - KeyName: "my-key", -} - -logger := zap.NewExample() -lumera, err := client.New(ctx, cfg, kr, client.WithLogger(logger)) -if err != nil { - logger.Error("client init failed", zap.Error(err)) -} -defer lumera.Close() -``` - -`client.Client` exposes `Blockchain` (gRPC chain modules) and `Cascade` (SuperNode SDK + SnApi). - -### Using the factory for multiple signers - -`client.NewFactory` keeps a shared config/keyring and returns signer-specific clients: - -```go -factory, _ := client.NewFactory(cfg, kr) -alice, _ := factory.WithSigner(ctx, "lumera1alice...", "alice") -bob, _ := factory.WithSigner(ctx, "lumera1bob...", "bob") -defer alice.Close() -defer bob.Close() -``` - -## Crypto Helpers (`pkg/crypto`) - -The `pkg/crypto` package provides keyring creation, key import, and address derivation. A single keyring supports both Cosmos (`secp256k1`) and EVM (`eth_secp256k1`) key types. - -### Key types - -`KeyType` selects the cryptographic algorithm and BIP44 derivation path: - -| KeyType | Algorithm | BIP44 Coin Type | HD Path | -|---------|-----------|----------------|---------| -| `KeyTypeCosmos` | `secp256k1` | 118 | `m/44'/118'/0'/0/0` | -| `KeyTypeEVM` | `eth_secp256k1` | 60 | `m/44'/60'/0'/0/0` | - -### Creating a keyring - -`NewKeyring` creates a keyring that accepts both key types. The algorithm is selected when importing or creating keys, not at keyring creation time. - -```go -import sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" - -kr, err := sdkcrypto.NewKeyring(sdkcrypto.DefaultKeyringParams()) -``` - -### Importing keys from a mnemonic - -Use `LoadKeyring` to create a test keyring and import a key in one step: - -```go -kr, pubBytes, addr, err := sdkcrypto.LoadKeyring("alice", "mnemonic.txt", sdkcrypto.KeyTypeCosmos) -``` - -Use `ImportKey` to add a key to an existing keyring: - -```go -pubBytes, addr, err := sdkcrypto.ImportKey(kr, "bob", "mnemonic.txt", "lumera", sdkcrypto.KeyTypeCosmos) -``` - -### Using different key types per chain - -When controller and host chains use different cryptographic key types, import keys under separate names: - -```go -kr, _ := sdkcrypto.NewKeyring(sdkcrypto.DefaultKeyringParams()) - -// Controller chain: standard Cosmos key (secp256k1, coin type 118) -sdkcrypto.ImportKey(kr, "controller-key", "mnemonic.txt", "lumera", sdkcrypto.KeyTypeCosmos) - -// Host chain: EVM-compatible key (eth_secp256k1, coin type 60) -sdkcrypto.ImportKey(kr, "host-key", "mnemonic.txt", "inj", sdkcrypto.KeyTypeEVM) -``` - -The ICA controller supports this via the `HostKeyName` config field (see Tutorial 6 below). - -### Deriving addresses - -`AddressFromKey` derives a bech32 address for any HRP without mutating global SDK config: - -```go -addr, err := sdkcrypto.AddressFromKey(kr, "alice", "lumera") -``` - -## Tutorials - -### 1) Query actions (read-only) - -```go -action, err := lumera.Blockchain.Action.GetAction(ctx, "action-id") -if err != nil { - log.Fatal(err) -} -fmt.Println(action) -``` - -### 2) Upload a file to Cascade (one-shot) - -Steps: build Cascade metadata, register an action on-chain, upload bytes to SuperNodes, wait for completion. - -```go -result, err := lumera.Cascade.Upload(ctx, cfg.Address, lumera.Blockchain, "/path/to/file", - cascade.WithPublic(true), // optional: make file public -) -if err != nil { - log.Fatal(err) -} -log.Printf("action=%s task=%s", result.ActionID, result.TaskID) -``` - -`Upload` wraps `Client.CreateRequestActionMessage`, `Client.SendRequestActionMessage`, and `Client.UploadToSupernode`. For manual control, call those methods separately and reuse the returned `MsgRequestAction` or `types.ActionResult`. - -### 3) Download from Cascade - -```go -dl, err := lumera.Cascade.Download(ctx, "action-id", "/tmp/downloads") -if err != nil { - log.Fatal(err) -} -log.Printf("downloaded to %s", dl.OutputPath) -``` - -### 4) Subscribe to Cascade task events - -The Cascade client bridges SuperNode SDK events and adds SDK-specific ones (prefixed `sdk-go:`). - -```go -lumera.Cascade.SubscribeToAllEvents(ctx, func(_ context.Context, e event.Event) { - log.Printf("%s task=%s msg=%v", e.Type, e.TaskID, e.Data[event.KeyMessage]) -}) -``` - -### 5) Send on-chain actions explicitly - -```go -msg, meta, err := lumera.Cascade.CreateRequestActionMessage(ctx, cfg.Address, "/path/file", nil) -_ = meta // metadata bytes used in the action -if err != nil { log.Fatal(err) } - -ar, err := lumera.Cascade.SendRequestActionMessage(ctx, lumera.Blockchain, msg, "memo", nil) -if err != nil { log.Fatal(err) } -log.Printf("action registered: %s", ar.ActionID) - -// Approve the action (if your flow requires it) -approve := blockchain.NewMsgApproveAction(cfg.Address, ar.ActionID) -_, err = lumera.Cascade.SendApproveActionMessage(ctx, lumera.Blockchain, approve, "") -``` - -For offline/ICA-style flows, the package-level `cascade.CreateApproveActionMessage` helper builds approvals without SuperNode dependencies. - -### 6) Interchain Accounts (ICA) flow - -Use ICA when a controller chain account submits Lumera `MsgRequestAction` messages on behalf of an ICA address. The SDK helps build the request message and ICA packet, but you still broadcast the controller-chain `MsgSendTx` with your controller chain tooling. - -Key points: - -- You must provide Lumera chain `grpc` + `chain-id` so metadata (price/expiration) can be computed. -- For ICA, set the ICA creator address and app pubkey on the request message. -- The Cascade client uses `ICAOwnerKeyName` + `ICAOwnerHRP` to derive the controller owner address. - `appPubkey` should be the controller key's pubkey bytes from the keyring. -- When controller and host chains use different key types, import keys under separate names into the same keyring and set `HostKeyName` on the ICA `Config` (see the Crypto Helpers section above). - -```go -ctx := context.Background() -// Reuse kr from the client setup above. -cascadeClient, err := cascade.New(ctx, cascade.Config{ - ChainID: "lumera-testnet-2", - GRPCAddr: "localhost:9090", - Address: "lumera1abc...", - KeyName: "my-key", - ICAOwnerKeyName: "my-key", - ICAOwnerHRP: "inj", -}, kr) -if err != nil { log.Fatal(err) } -defer cascadeClient.Close() - -uploadOpts := &cascade.UploadOptions{} -cascade.WithICACreatorAddress("lumera1ica...")(uploadOpts) -cascade.WithAppPubkey(appPubkey)(uploadOpts) - -msg, _, err := cascadeClient.CreateRequestActionMessage(ctx, "lumera1abc...", "/path/file", uploadOpts) -if err != nil { log.Fatal(err) } - -any, err := ica.PackRequestAny(msg) -if err != nil { log.Fatal(err) } - -packet, err := ica.BuildICAPacketData([]*codectypes.Any{any}) -if err != nil { log.Fatal(err) } - -msgSendTx, err := ica.BuildMsgSendTx(ownerAddr, "connection-0", 600_000_000_000, packet) -if err != nil { log.Fatal(err) } - -// Broadcast msgSendTx using your controller-chain SDK or CLI. -``` - -See `examples/ica-request-tx` for a full CLI that builds the ICA packet and prints the JSON. - -### 7) Manage SuperNodes - -Registration/updates use `lumera.Blockchain.SuperNode` transaction helpers: - -```go -_, err := lumera.Blockchain.RegisterSupernodeTx(ctx, cfg.Address, "lumeravaloper...", "1.2.3.4", "lumera1sn...", "26656", "") -if err != nil { log.Fatal(err) } -``` - -Query helpers include `GetSuperNode`, `ListSuperNodes`, and `GetTopSuperNodesForBlock`. - -## ICA Controller Overview - -The `ica` package provides a production-ready ICA (Interchain Accounts / ICS-27) controller that manages the full lifecycle of cross-chain message execution against Lumera. - -### What it does - -`ica.Controller` connects to both a controller chain and the Lumera host chain over gRPC. It handles ICA registration, IBC packet construction, transaction broadcasting, acknowledgement polling, and action ID extraction โ€” all behind a small set of methods: - -```go -ctrl, _ := ica.NewController(ctx, ica.Config{ - Controller: controllerBaseConfig, - Host: hostBaseConfig, - Keyring: kr, - KeyName: "controller-key", - HostKeyName: "host-key", // optional: separate key for host chain - ConnectionID: "connection-0", -}) -defer ctrl.Close() - -addr, _ := ctrl.EnsureICAAddress(ctx) // register + poll until ready -result, _ := ctrl.SendRequestAction(ctx, msg) // send, wait for ack, return action ID -txHash, _ := ctrl.SendApproveAction(ctx, approveMsg) -``` - -For lower-level or offline workflows, packet-building helpers are available separately: `PackRequestAny`, `BuildICAPacketData`, `BuildMsgSendTx`. - -### Strengths - -- **Minimal setup** โ€” only gRPC endpoints, a keyring, and an IBC connection ID are required. No Docker, no relayer binary, no chain binaries. -- **End-to-end in one call** โ€” `SendRequestAction` builds the ICA packet, broadcasts on the controller chain, waits for tx inclusion, resolves the counterparty channel, polls for the host-chain acknowledgement, and extracts the action ID. -- **Mixed key type support** โ€” controller and host chains can use different cryptographic key types (`KeyTypeCosmos` / `KeyTypeEVM`) by setting `HostKeyName` to a separate key in the same keyring. -- **Resilient polling** โ€” configurable retry counts and delays for both ICA registration (`PollRetries` / `PollDelay`) and acknowledgement waiting (`AckRetries`). -- **Tight Lumera integration** โ€” purpose-built for `MsgRequestAction` and `MsgApproveAction`, with typed results (`ActionResult`) and Cascade metadata compatibility. - -### Limitations - -- **Requires running chains** โ€” the controller connects to live gRPC endpoints. It does not spin up chains or relayers; infrastructure must already be deployed. -- **Lumera-specific high-level methods** โ€” `SendRequestAction` and `SendApproveAction` are tailored to Lumera action messages. Generic ICA message execution requires using the lower-level packet helpers directly. -- **No chain lifecycle management** โ€” unlike e2e testing frameworks (e.g., interchaintest), there is no built-in chain provisioning, genesis configuration, or relayer orchestration. -- **Relayer dependency** โ€” IBC packet relay between controller and host chains depends on an external relayer (e.g., Hermes). The controller does not relay packets itself. - -### When to use this vs. interchaintest - -| Aspect | `ica.Controller` | interchaintest | -| --- | --- | --- | -| **Purpose** | Production client / scripting | E2E integration testing | -| **Infrastructure** | Connects to running chains | Spins up chains + relayers in Docker | -| **Setup effort** | Config struct + keyring | Docker, chain binaries, genesis config | -| **Iteration speed** | Fast (gRPC calls) | Slower (container lifecycle + block production) | -| **Scope** | Lumera ICA operations | Any IBC flow, any chain | - -Use `ica.Controller` when you have running chains and need to execute ICA operations in production or automation scripts. Use interchaintest when you need to validate the full ICA flow in CI from scratch without external infrastructure. - -## Examples and testing - -- Run tests: `make test` -- Build samples: `make examples` -- Execute tutorials end-to-end: `go run ./examples/cascade-upload`, `go run ./examples/cascade-download`, `go run ./examples/query-actions`, `go run ./examples/multi-account`, `go run ./examples/ica-request-tx --help` - -## Troubleshooting - -- **Tx inclusion timing out**: adjust `WaitTx` polling/backoff (see `client/config`). Ensure `RPCEndpoint` allows websocket subscriptions. -- **gRPC TLS errors**: remote hosts/port 443 default to TLS; for local nodes use `localhost:9090` or `127.0.0.1:9090`. -- **Key not found**: confirm the key name exists in the keyring path you passed to `keyring.New`. -- **SuperNode availability**: Cascade operations require reachable SuperNodes; watch `sdk:supernodes_unavailable` events for diagnostics. +Run `make examples` to compile them all. diff --git a/docs/design/0001-cosmos-evm-api.md b/docs/design/0001-cosmos-evm-api.md new file mode 100644 index 0000000..d63199b --- /dev/null +++ b/docs/design/0001-cosmos-evm-api.md @@ -0,0 +1,543 @@ +# 0001 โ€“ Cosmos EVM API Surface + +**Status:** Draft +**Scope:** `sdk-go` exposure of `github.com/cosmos/evm` modules (`x/vm`, `x/erc20`, `x/feemarket`, `x/precisebank`), Ethereum-format signing, and Lumera's custom precompiles (action `0x0901`, supernode `0x0902`, wasm `0x0903`). +**Non-goals:** JSON-RPC server compatibility, contract source compilation, on-chain governance proposals beyond `MsgUpdateParams`. + +## Lumera-specific context + +Anchoring decisions in [../../../lumera/docs/evm-integration](../../../lumera/docs/evm-integration): + +- **Single shared nonce/sequence.** EVM nonce is backed by the Cosmos `auth` account sequence. A Cosmos tx and an EVM tx from the same key advance the same counter โ€” see [key-type-address.md](../../../lumera/docs/evm-integration/architecture/key-type-address.md). Nonce caching must invalidate on any cosmos-side tx too. +- **Dual encoding for `eth_secp256k1` keys.** The same 20-byte address can be rendered as `0x...` or `lumera1...`. `EVMToBech32` / `Bech32ToEVM` are lossless byte encoders/decoders, but a Bech32 string alone does not prove the account was derived with `eth_secp256k1`: legacy `secp256k1` accounts are also 20 bytes, but those bytes came from `RIPEMD160(SHA256(pk))` and do not identify the key's Ethereum address. +- **6 <-> 18 decimal bridging via `x/precisebank`.** Cosmos side uses `ulume` (6-dec); EVM side uses `alume` (18-dec) with conversion `1 ulume = 10^12 alume`. Lumera VM params keep `evm_denom = "ulume"` and set `extended_denom = "alume"`. `EthereumTxOptions.Value` and all EIP-1559 fee caps are in `alume` (wei-like integers). +- **Lumera fee-market defaults.** BaseFee `0.0025 ulume/gas`, MinGasPrice `0.0005 ulume/gas`, change-denominator 16 (gentle ~6.25%/block). `GasTipCap` default should pull from `feemarket.Params.MinGasPrice` and convert the decimal `ulume/gas` value to integer `alume/gas`. +- **Custom precompiles** at `0x0901` (action), `0x0902` (supernode), `0x0903` (wasm). The SDK ships Go-side helpers so consumers do not need to compile the Solidity ABI themselves. + +## Guiding principles + +1. **Mirror existing module-client pattern.** Each module gets a `Client` exposed off `blockchain.Client`, symmetrical with `ActionClient` / `SuperNodeClient` / `EVMigrationClient`. +2. **Lean opinionated.** High-level helpers fill nonce, gas, fee, and signing automatically. Thin wrappers exist underneath for callers that need control. +3. **Single keyring.** EVM keys (`eth_secp256k1`) and Cosmos keys (`secp256k1`) coexist in the same `keyring.Keyring`; selection is per-call via `KeyName`. +4. **Two signing worlds.** Cosmos-sign-mode `MsgEthereumTx` is *not* the same as RLP-signed Ethereum transactions; the SDK helpers always produce the latter (EIP-155), wrapped in `MsgEthereumTx` for broadcast. +5. **No silent breakage.** New surface is additive. Existing `BuildAndSignTx` / module clients stay untouched. + +## Configuration additions + +`blockchain/base/config.go` โ€” `Config`: + +```go +type Config struct { + // existing fields... + + // EVMChainID is the EIP-155 chain ID used to sign Ethereum-format + // transactions. Distinct from the Cosmos ChainID. Nil means "not an + // EVM-enabled chain" and disables EVM-tx helpers. + EVMChainID *big.Int + + // EVMNativeDenom is the chain VM param evm_denom. On Lumera this is + // "ulume" and is passed to MsgEthereumTx.BuildTxWithEvmParams so + // cosmos/evm can construct the fee coin before converting it to the + // extended denom. + EVMNativeDenom string + + // EVMExtendedDenom is the 18-decimal precisebank denom. On Lumera this is + // "alume". Ethereum tx value, fee caps, balances, and receipts use this + // denom's integer "wei-like" unit. + EVMExtendedDenom string + + // EVMGasTipCap / EVMGasFeeCap are optional defaults in the extended denom's + // integer unit (alume/wei-like) for EIP-1559 gas pricing. Nil means "fetch + // from chain state". + EVMGasTipCap *big.Int + EVMGasFeeCap *big.Int +} +``` + +Client options (in `client/`): + +```go +client.WithEVMChainID(id *big.Int) +client.WithEVMNativeDenom(denom string) +client.WithEVMExtendedDenom(denom string) +client.WithEVMGasCaps(tip, fee *big.Int) +``` + +`client/config.Config` mirrors these fields, and `client.New` copies them into +`blockchain.Config` when it constructs the blockchain client. + +## Files added + +``` +blockchain/ + evm.go # x/vm client + EthereumTransactionResult + evm_test.go + erc20.go # x/erc20 client + tx helpers + result types + erc20_test.go + feemarket.go # x/feemarket client (read-only) + feemarket_test.go + precisebank.go # x/precisebank client (read-only) + precisebank_test.go +pkg/crypto/ + ethsign.go # SignEthereumTx + SignEIP712Tx helpers + ethaddr.go # Bech32 <-> 0x address translation helpers +pkg/evm/precompiles/ + *.go, abi/*.json # Lumera precompile addresses, ABIs, typed wrappers +``` + +`blockchain/client.go` grows four fields on `Client`: + +```go +type Client struct { + *base.Client + + Action *ActionClient + SuperNode *SuperNodeClient + Claim *ClaimClient + EVMigration *EVMigrationClient + Audit *AuditClient + + EVM *EVMClient // x/vm + ERC20 *ERC20Client // x/erc20 + FeeMarket *FeeMarketClient // x/feemarket + PreciseBank *PreciseBankClient +} +``` + +`pkg/crypto/keyring.go` โ€” `MsgEthereumTx` interfaces are already registered. Add `github.com/cosmos/evm/ethereum/eip712.RegisterInterfaces` to `NewDefaultTxConfig` so EIP-712/Web3 extension options round-trip through the cosmos tx config. No new sign-mode handler is needed for v1; EIP-712 signs a typed-data hash and stores the resulting signature in a normal `SIGN_MODE_DIRECT` `SignatureV2`. + +## Address translation (`pkg/crypto/ethaddr.go`) + +```go +// EVMToBech32 converts a 20-byte EVM address to the bech32 form used by +// Cosmos bank/auth for the same account. +func EVMToBech32(addr common.Address, hrp string) (string, error) + +// Bech32ToEVM decodes a bech32 account address and returns those 20 bytes as +// an EVM address. It cannot prove the address came from an eth_secp256k1 key; +// callers that care must validate against a keyring entry, on-chain account +// metadata, or an evmigration record. +func Bech32ToEVM(bech32Addr string) (common.Address, error) + +// EVMAddressFromKey lives in address.go; pairs with these helpers. + +// Wei converts an integer ulume amount to its 18-decimal alume (wei-like) form +// using the 10^12 conversion factor defined by x/precisebank. +func Wei(ulume sdkmath.Int) *big.Int + +// ULumeDecToWei converts a decimal ulume/gas value, such as feemarket +// BaseFee or MinGasPrice, into integer alume/gas. It errors if the conversion +// would produce a fractional alume amount. +func ULumeDecToWei(ulume sdkmath.LegacyDec) (*big.Int, error) + +// ULume converts an alume amount to ulume, truncating any sub-ulume +// fractional component. The dropped fractional remainder is returned for +// callers that want to surface precision loss. +func ULume(alume *big.Int) (ulume sdkmath.Int, fractional *big.Int) +``` + +These are pure conversions (no keyring access). Used internally by `ERC20Client` to translate `Sender` / `Receiver` fields between the two address spaces. The `Wei` / `ULume` / `ULumeDecToWei` helpers are required wherever values cross the 6<->18 boundary: Ethereum tx values and fee caps are alume, while Cosmos `sdk.Coin` amounts and feemarket params are expressed in ulume. + +## Ethereum signing (`pkg/crypto/ethsign.go`) + +```go +// SignEthereumTx signs a go-ethereum *types.Transaction with the named +// keyring entry (must be eth_secp256k1) using the replay-protected Ethereum +// signer for chainID. Returns the signed transaction. +func SignEthereumTx( + kr keyring.Keyring, + keyName string, + chainID *big.Int, + tx *ethtypes.Transaction, +) (*ethtypes.Transaction, error) + +// WrapAsMsgEthereumTx packages a signed go-ethereum tx as a cosmos +// MsgEthereumTx ready for MsgEthereumTx.BuildTxWithEvmParams and broadcast. +func WrapAsMsgEthereumTx(signed *ethtypes.Transaction) (*evmtypes.MsgEthereumTx, error) + +// SignEIP712Tx signs an arbitrary cosmos tx using EIP-712 typed-data so +// MetaMask-style wallets can authorize cosmos messages. Out of scope for v1; +// stub the signature now to lock the API. +func SignEIP712Tx( + kr keyring.Keyring, + keyName string, + chainID *big.Int, + txCfg client.TxConfig, + builder client.TxBuilder, + signerData authsigning.SignerData, +) error +``` + +Implementation note: `keyring.Keyring.Sign` produces a 65-byte recoverable signature for `eth_secp256k1` (R, S, recovery ID). Pass that signature to `tx.WithSignature(signer, sig)` using the appropriate go-ethereum EIP-155/EIP-1559 signer; do not synthesize `V` manually. + +## `blockchain.EVMClient` (x/vm) + +```go +type EVMClient struct { + query evmtypes.QueryClient + client *Client // backref for tx helpers +} + +// --- queries (thin wrappers) --- + +func (c *EVMClient) Code(ctx context.Context, addr common.Address) ([]byte, error) +func (c *EVMClient) Storage(ctx context.Context, addr common.Address, key common.Hash) (common.Hash, error) +func (c *EVMClient) Balance(ctx context.Context, addr common.Address) (*big.Int, error) +func (c *EVMClient) EthAccount(ctx context.Context, addr common.Address) (*evmtypes.QueryAccountResponse, error) +func (c *EVMClient) CosmosAccount(ctx context.Context, addr common.Address) (string /*bech32*/, error) +func (c *EVMClient) Params(ctx context.Context) (*evmtypes.QueryParamsResponse, error) +func (c *EVMClient) BaseFee(ctx context.Context) (*big.Int, error) +func (c *EVMClient) Config(ctx context.Context) (*evmtypes.QueryConfigResponse, error) +func (c *EVMClient) GlobalMinGasPrice(ctx context.Context) (sdkmath.Int, error) + +// EthCall executes a read-only EVM call. data is ABI-encoded calldata. +func (c *EVMClient) EthCall(ctx context.Context, to *common.Address, from common.Address, data []byte, gas uint64) ([]byte, error) + +// EstimateGas returns the gas required for a hypothetical tx. +func (c *EVMClient) EstimateGas(ctx context.Context, to *common.Address, from common.Address, data []byte, value *big.Int) (uint64, error) + +// TraceTx returns the raw JSON trace for a replayed EVM tx. The cosmos/evm +// gRPC API requires the full message plus block/predecessor context; there is +// no tx-hash-only TraceTx query. +func (c *EVMClient) TraceTx(ctx context.Context, req *evmtypes.QueryTraceTxRequest) ([]byte /*raw JSON*/, error) + +// --- opinionated tx helpers --- + +// EthereumTxOptions overrides defaults pulled from chain state. +type EthereumTxOptions struct { + Nonce *uint64 // default: query Account + GasLimit uint64 // default: EstimateGas + 20% buffer + GasTipCap *big.Int // default: feemarket MinGasPrice scaled to alume + GasFeeCap *big.Int // default: baseFee*2 + tipCap + Value *big.Int // default: 0, in alume/wei-like units + AccessList ethtypes.AccessList + Memo string +} + +// EthereumTransactionResult mirrors ActionResult. +type EthereumTransactionResult struct { + EthTxHash common.Hash + CosmosHash string + Height int64 + GasUsed uint64 + Logs []*ethtypes.Log // decoded from MsgEthereumTxResponse +} + +// SendEthereumTransaction signs and broadcasts an Ethereum-format tx using +// the client's KeyName. Pulls nonce, gas, and fees from chain when not set. +// Waits for inclusion. Returns both the eth tx hash and cosmos hash so +// callers can correlate. +func (c *Client) SendEthereumTransaction( + ctx context.Context, + to *common.Address, // nil = contract creation + data []byte, + opts *EthereumTxOptions, +) (*EthereumTransactionResult, error) + +// DeployContract is sugar over SendEthereumTransaction with to=nil; returns +// the deployed contract address parsed from logs. +func (c *Client) DeployContract( + ctx context.Context, + bytecode []byte, + opts *EthereumTxOptions, +) (common.Address, *EthereumTransactionResult, error) + +// CallContract is sugar for read-only calls (forwards to EthCall). +func (c *Client) CallContract( + ctx context.Context, + to common.Address, + data []byte, +) ([]byte, error) + +// RawEthereumTx broadcasts a pre-signed go-ethereum transaction unchanged. +// For callers bringing their own signer (e.g. hardware wallet). +func (c *Client) RawEthereumTx( + ctx context.Context, + signed *ethtypes.Transaction, +) (*EthereumTransactionResult, error) +``` + +Internal flow for `SendEthereumTransaction`: + +``` +1. Resolve nonce -> EthAccount(addr).Nonce (= auth sequence on Lumera). + Optional NonceTracker must invalidate after any + cosmos-side BuildAndSignTx from the same key. +2. Resolve gas -> opts.GasLimit || EstimateGas() * 1.2 +3. Resolve fees -> opts.GasTipCap || ULumeDecToWei(feemarket.Params.MinGasPrice) + -> opts.GasFeeCap || vm.BaseFee * 2 + tipCap + Both are integer alume/gas values. +4. Build *ethtypes.DynamicFeeTx (EIP-1559); set ChainID = EVMChainID. +5. SignEthereumTx -> signed go-ethereum tx (uses ethtypes.LatestSignerForChainID) +6. WrapAsMsgEthereumTx +7. BuildEthereumTx over the wrapper via + MsgEthereumTx.BuildTxWithEvmParams(builder, evmtypes.Params{ + EvmDenom: EVMNativeDenom, + ExtendedDenomOptions: &evmtypes.ExtendedDenomOptions{ + ExtendedDenom: EVMExtendedDenom, + }, + }). The cosmos tx envelope carries + ExtensionOptionsEthereumTx so Lumera's dual-route ante handler routes + it down the EVM path. BuildTxWithEvmParams converts the fee coin to + EVMExtendedDenom without relying on process-global cosmos/evm coin config. + No cosmos signature on the wrapper itself. +8. BroadcastAndWait +9. Decode MsgEthereumTxResponse from TxResponse.Data via + evmtypes.DecodeTxResponse to populate logs + gas used +``` + +**Single-counter implication.** Because Lumera shares the EVM nonce with the auth sequence, mixing `SendEthereumTransaction` and `BuildAndSignTx` for the same key requires coordination. The simplest contract: never cache nonces by default; opt-in via `client.WithNonceCache()` only for single-flight EVM-only workloads. + +## `blockchain.ERC20Client` (x/erc20) + +```go +type ERC20Client struct { + query erc20types.QueryClient + client *Client +} + +// --- queries --- + +func (c *ERC20Client) TokenPairs(ctx context.Context, pagination *query.PageRequest) ([]erc20types.TokenPair, *query.PageResponse, error) +func (c *ERC20Client) TokenPair(ctx context.Context, token string /*denom or 0x addr*/) (erc20types.TokenPair, error) +func (c *ERC20Client) Params(ctx context.Context) (*erc20types.QueryParamsResponse, error) + +// --- message constructors (symmetric with action.go) --- + +func NewMsgConvertCoin(coin sdk.Coin, receiver common.Address, sender sdk.AccAddress) *erc20types.MsgConvertCoin +func NewMsgConvertERC20(amount sdkmath.Int, receiver sdk.AccAddress, contract, sender common.Address) *erc20types.MsgConvertERC20 +func NewMsgRegisterERC20(signer string, addrs []string) *erc20types.MsgRegisterERC20 +func NewMsgToggleConversion(authority string, token string) *erc20types.MsgToggleConversion + +// --- result type --- + +type ConversionResult struct { + From string // cosmos bech32 or 0x + To string // opposite address space + Amount sdkmath.Int + TxHash string + Height int64 +} + +// --- opinionated tx helpers --- + +// ConvertCoinToERC20 sends a MsgConvertCoin, signed by the client's KeyName. +// Receiver is the EVM (0x) address that gets the wrapped ERC20. +func (c *Client) ConvertCoinToERC20( + ctx context.Context, + coin sdk.Coin, + receiver common.Address, + memo string, +) (*ConversionResult, error) + +// ConvertERC20ToCoin sends a MsgConvertERC20. Receiver is the bech32 cosmos +// address. Sender is the 0x address that owns the ERC20 (must match the +// client's EVM-keyed signer). +func (c *Client) ConvertERC20ToCoin( + ctx context.Context, + contract common.Address, + amount sdkmath.Int, + receiver sdk.AccAddress, + memo string, +) (*ConversionResult, error) + +// RegisterERC20Tx wraps MsgRegisterERC20 and broadcasts it. The chain may +// allow permissionless registration; otherwise the signer must be the module +// authority. +func (c *Client) RegisterERC20Tx( + ctx context.Context, + contracts []common.Address, + memo string, +) (string /*tx hash*/, error) + +// ToggleConversionTx (governance). +func (c *Client) ToggleConversionTx( + ctx context.Context, + authority string, + token string, + memo string, +) (string, error) + +// --- ABI sugar (no chain calls) --- + +// Erc20Balance returns the balance of holder for the ERC20 contract by +// calling balanceOf(address) via EthCall. +func (c *ERC20Client) Erc20Balance( + ctx context.Context, + contract, holder common.Address, +) (*big.Int, error) + +// Erc20Allowance / Erc20TotalSupply / Erc20Metadata follow the same shape. +``` + +ABI calls (`Erc20Balance` etc.) ship a pre-compiled `abi.ABI` for the standard ERC20 interface as a package-level singleton so callers do not pull `go-ethereum/accounts/abi` themselves. + +## Lumera precompiles (`pkg/evm/precompiles`) + +Lumera ships three custom precompiles at fixed addresses (per [precompiles.md](../../../lumera/docs/evm-integration/precompiles/precompiles.md)). They are normal contracts from a caller's perspective, but the SDK can save consumers from compiling the Solidity ABI by hand. + +New subpackage `pkg/evm/precompiles` exports: + +```go +// Well-known addresses. +var ( + ActionPrecompile = common.HexToAddress("0x0000000000000000000000000000000000000901") + SupernodePrecompile = common.HexToAddress("0x0000000000000000000000000000000000000902") + WasmPrecompile = common.HexToAddress("0x0000000000000000000000000000000000000903") +) + +// Pre-compiled ABIs, loaded once from go:embed'd JSON. +var ( + ActionABI abi.ABI + SupernodeABI abi.ABI + WasmABI abi.ABI +) + +// ActionPrecompile helpers: typed wrappers that pack calldata and either +// EthCall or SendEthereumTransaction depending on read/write. +type ActionPrecompileClient struct{ evm *EVMClient } + +func (a *ActionPrecompileClient) RequestCascade(ctx context.Context, args RequestCascadeArgs, opts *EthereumTxOptions) (*EthereumTransactionResult, string /*actionID*/, error) +func (a *ActionPrecompileClient) FinalizeCascade(ctx context.Context, args FinalizeCascadeArgs, opts *EthereumTxOptions) (*EthereumTransactionResult, error) +func (a *ActionPrecompileClient) RequestSense(ctx context.Context, args RequestSenseArgs, opts *EthereumTxOptions) (*EthereumTransactionResult, string, error) +func (a *ActionPrecompileClient) FinalizeSense(ctx context.Context, args FinalizeSenseArgs, opts *EthereumTxOptions) (*EthereumTransactionResult, error) +func (a *ActionPrecompileClient) ApproveAction(ctx context.Context, actionID string, opts *EthereumTxOptions) (*EthereumTransactionResult, error) +func (a *ActionPrecompileClient) GetAction(ctx context.Context, actionID string) (*types.Action, error) +func (a *ActionPrecompileClient) GetActionFee(ctx context.Context, dataSizeKbs uint64) (*ActionFee, error) +func (a *ActionPrecompileClient) GetParams(ctx context.Context) (*ActionParams, error) +func (a *ActionPrecompileClient) GetActionsByState(ctx context.Context, state uint8, offset, limit uint64) ([]types.Action, uint64 /*total*/, error) +func (a *ActionPrecompileClient) GetActionsByCreator(ctx context.Context, creator common.Address, offset, limit uint64) ([]types.Action, uint64 /*total*/, error) +func (a *ActionPrecompileClient) GetActionsBySuperNode(ctx context.Context, superNode common.Address, offset, limit uint64) ([]types.Action, uint64 /*total*/, error) + +// SupernodePrecompileClient mirrors the supernode precompile surface. +type SupernodePrecompileClient struct{ evm *EVMClient } + +func (s *SupernodePrecompileClient) RegisterSupernode(ctx context.Context, args RegisterSupernodeArgs, opts *EthereumTxOptions) (*EthereumTransactionResult, error) +// ... DeregisterSupernode, StartSupernode, StopSupernode, UpdateSupernode, +// ReportMetrics, GetSuperNode, GetSuperNodeByAccount, ListSuperNodes, +// GetTopSuperNodesForBlock, GetMetrics, GetParams + +// WasmPrecompileClient. +type WasmPrecompileClient struct{ evm *EVMClient } + +func (w *WasmPrecompileClient) Execute(ctx context.Context, contract string /*bech32*/, msg []byte, opts *EthereumTxOptions) (*EthereumTransactionResult, []byte /*response*/, error) +func (w *WasmPrecompileClient) Query(ctx context.Context, contract string /*bech32*/, msg []byte) ([]byte, error) +func (w *WasmPrecompileClient) ContractInfo(ctx context.Context, contract string /*bech32*/) (*WasmContractInfo, error) +func (w *WasmPrecompileClient) RawQuery(ctx context.Context, contract string /*bech32*/, key []byte) ([]byte, error) +``` + +The Wasm precompile is phase-1 non-payable, matching the Lumera ABI: no `funds` +argument is exposed until the chain supports payable cross-runtime execution. + +Precompile ABI business amounts keep the units documented by the Lumera ABI. +Action prices and fee query results are `ulume`, even though +`EthereumTxOptions.Value` and gas caps are `alume`. Supernode validator and +account fields remain Bech32 `string` values because `lumeravaloper...` +addresses have no meaningful 20-byte EVM representation. + +Exposed off the EVM client: + +```go +type EVMClient struct { + // ... query, client backref + + Action *ActionPrecompileClient + Supernode *SupernodePrecompileClient + Wasm *WasmPrecompileClient +} +``` + +**Why both `blockchain.Action` (msg-based) and `EVM.Action` (precompile-based) exist.** Same on-chain effect, different signing/fee paths. Consumers using EVM keys / EIP-1559 fees / dApp flows should use the precompile path; cosmos-native consumers continue with `RequestActionTx`. Document this overlap explicitly so callers do not duplicate effort. + +**Standard Cosmos EVM precompiles** (bank, staking, distribution, gov, slashing, ICS20, bech32, p256) are out of scope for typed Go wrappers in v1 โ€” they are primarily a Solidity-facing surface. We do export their addresses as named constants for convenience: + +```go +var ( + BankPrecompile = common.HexToAddress("0x0000000000000000000000000000000000000804") + StakingPrecompile = common.HexToAddress("0x0000000000000000000000000000000000000800") + DistributionPrecompile = common.HexToAddress("0x0000000000000000000000000000000000000801") + ICS20Precompile = common.HexToAddress("0x0000000000000000000000000000000000000802") + GovPrecompile = common.HexToAddress("0x0000000000000000000000000000000000000805") + SlashingPrecompile = common.HexToAddress("0x0000000000000000000000000000000000000806") + Bech32Precompile = common.HexToAddress("0x0000000000000000000000000000000000000400") + P256Precompile = common.HexToAddress("0x0000000000000000000000000000000000000100") +) +``` + +## `blockchain.FeeMarketClient` + +Read-only, no tx helpers needed (params change only via governance): + +```go +type FeeMarketClient struct { + query feemarkettypes.QueryClient +} + +func (c *FeeMarketClient) Params(ctx context.Context) (*feemarkettypes.QueryParamsResponse, error) +func (c *FeeMarketClient) BaseFee(ctx context.Context) (sdkmath.LegacyDec, error) +func (c *FeeMarketClient) BlockGas(ctx context.Context) (*feemarkettypes.QueryBlockGasResponse, error) +``` + +Cached `BaseFee` lookup (single-flight, ~1 block TTL) is a follow-up optimization for high-throughput callers. + +## `blockchain.PreciseBankClient` + +Read-only: + +```go +type PreciseBankClient struct { + query precisebanktypes.QueryClient +} + +func (c *PreciseBankClient) Remainder(ctx context.Context) (sdk.Coin, error) +func (c *PreciseBankClient) FractionalBalance(ctx context.Context, addr sdk.AccAddress) (sdk.Coin, error) +``` + +## Tx pipeline interactions + +`MsgEthereumTx` does **not** go through the cosmos signature placeholder dance in [blockchain/base/tx.go:149-169](../../blockchain/base/tx.go#L149). The flow is: + +1. `BuildAndSignTxWithOptions` is bypassed for EVM txs. +2. A dedicated `BuildEthereumTx` (private, in `evm.go`) builds the cosmos `tx.Tx` envelope and calls `MsgEthereumTx.BuildTxWithEvmParams(builder, params)` to apply gas/fee from the inner Ethereum tx. The tx is encoded without Cosmos signatures; the Ethereum signature lives in `Raw`. +3. Broadcast uses the existing `BroadcastAndWait`. + +This is why `EVMClient` keeps a `client *Client` backref โ€” it needs the +base client's gRPC connection for broadcast plus read-only access to the base +config/keyring/key name for `EVMChainID` and Ethereum signing. Add small +accessors on `base.Client` for those fields rather than reaching across package +boundaries. + +## Testing strategy + +Pattern from [blockchain/evmigration_test.go](../../blockchain/evmigration_test.go) extends cleanly: + +- Each `Client` gets a bufconn-backed `fakeServer` implementing the `UnimplementedQueryServer` (and `MsgServer` where applicable). +- `SendEthereumTransaction` integration test: register fakes for `vm.QueryServer` (Account, BaseFee, EstimateGas) and `tx.ServiceServer` (BroadcastTx, GetTx). Verify that the broadcast `MsgEthereumTx` has nonce/gas pulled from the fakes and that `MsgEthereumTxResponse` is decoded from `TxResponse.Data`. +- Round-trip test for `SignEthereumTx` -> `WrapAsMsgEthereumTx` -> decode -> `MsgEthereumTx.GetSender()` matches `EVMAddressFromKey`. + +No tests for read-only ABI helpers beyond a mock `EthCall` returning crafted bytes. + +## Open questions + +1. **EIP-712 in v1?** The signature stub locks the shape but the implementation is non-trivial (requires `MakeMessages` + typed-data hashing per msg type). Defer to v2 unless a consumer needs it now. +2. **Nonce caching with shared sequence.** Because Lumera shares the EVM nonce with the auth sequence, any `BuildAndSignTx` from the same key invalidates a cached nonce. Default to no cache; offer `client.WithNonceCache()` for opt-in EVM-only workloads. +3. **Multi-msg cosmos tx that contains `MsgEthereumTx` + other cosmos msgs.** Cosmos EVM rejects this on-chain. Should `BuildAndSignTxWithOptions` validate and fail fast? Yes โ€” add a check in `validateTxBuildOptions` (route the message to its dedicated `BuildEthereumTx` path or return an error). +4. **Receipt struct.** Returning `[]*ethtypes.Log` from `EthereumTransactionResult` leaks go-ethereum types into the SDK's public API. Acceptable since we already import `common.Address`. Alternative is a wrapper `EVMLog` type โ€” costs surface, gains independence. +5. **`x/feegrant` / `x/authz` over EVM messages.** Out of scope; cosmos/evm currently disallows. +6. **Precompile vs Msg duplication.** `Action.RequestCascade` exists both as a cosmos msg (`MsgRequestAction`) and as a precompile call at `0x0901`. Document the choice rule (EVM keys / dApp callers โ†’ precompile; cosmos keys / existing tooling โ†’ msg) and avoid silently routing one to the other. +7. **Lumera-specific defaults.** Should the SDK ship a `client.LumeraDefaults()` option group that sets `EVMChainID`, `EVMNativeDenom = "ulume"`, `EVMExtendedDenom = "alume"`, and registers the precompile clients? Cuts boilerplate for the common case but ties the SDK to a single chain; the alternative is per-call configuration. +8. **ABI assets.** Precompile ABIs live in the Lumera repo. Vendor them via `go:embed` of JSON files copied at SDK release time, or fetch from the lumera module dependency at build time? Vendoring is simpler but adds a sync step on each Lumera ABI bump. + +## Build sequence + +Phased rollout that always leaves `main` green: + +1. **Phase 1 โ€” Foundations.** `pkg/crypto/ethsign.go` + `ethaddr.go` with `SignEthereumTx`, `WrapAsMsgEthereumTx`, `EVMToBech32`, `Bech32ToEVM`, `Wei` / `ULume` / `ULumeDecToWei` decimal helpers. Round-trip unit tests. No public client surface yet. +2. **Phase 2 โ€” Read-only clients.** `FeeMarketClient`, `PreciseBankClient`, `EVMClient` queries only (no tx helpers). Wire onto `blockchain.Client`. Bufconn tests. +3. **Phase 3 โ€” EVM tx pipeline.** `SendEthereumTransaction`, `DeployContract`, `CallContract`, `RawEthereumTx`. Add `EVMChainID` / `EVMNativeDenom` / `EVMExtendedDenom` to `Config` and `client.With...` options. Multi-msg validation in `validateTxBuildOptions`. +4. **Phase 4 โ€” ERC20.** `ERC20Client` queries + `ConvertCoinToERC20` / `ConvertERC20ToCoin` (these reuse the regular cosmos tx pipeline โ€” no Ethereum signing). ABI sugar. +5. **Phase 5 โ€” Lumera precompiles.** `pkg/evm/precompiles` with embedded ABIs, plus `EVMClient.Action` / `Supernode` / `Wasm` typed wrappers. Document the precompile-vs-msg choice rule. +6. **Phase 6 โ€” Docs + examples.** Tutorial in [docs/DEVELOPER_GUIDE.md](../DEVELOPER_GUIDE.md), one example under `examples/evm-transfer` and one under `examples/precompile-action`, API.md updates. +7. **Phase 7 (later).** EIP-712, opt-in nonce tracker, fee-market cache. + +Each phase ships independently and is reviewable in ~300-500 LOC chunks. diff --git a/docs/guides/actions.md b/docs/guides/actions.md new file mode 100644 index 0000000..04a8168 --- /dev/null +++ b/docs/guides/actions.md @@ -0,0 +1,44 @@ +# Actions and SuperNodes + +Lumera's `x/action` and `x/supernode` modules are exposed off `Client.Blockchain`. Both ship with read-only queries plus opinionated transaction helpers; message constructors are exported for callers that need finer control. + +## Query actions + +```go +action, err := lumera.Blockchain.Action.GetAction(ctx, "action-id") +if err != nil { + log.Fatal(err) +} +fmt.Println(action) +``` + +Other queries: `ListActions`, `ListActionsByType`, `ListActionsBySuperNode`, `ListActionsByBlockHeight`, `ListExpiredActions`, `QueryActionByMetadata`, `GetActionFee`, `Params`. + +## Send action transactions + +```go +res, err := lumera.Blockchain.RequestActionTx(ctx, creator, actionType, metadata, price, expiration, fileSizeKbs, "memo") +if err != nil { + log.Fatal(err) +} +log.Printf("action %s tx %s height %d", res.ActionID, res.TxHash, res.Height) +``` + +Companion helpers: `ApproveActionTx`, `FinalizeActionTx`, `UpdateActionParamsTx`. Message constructors: `NewMsgRequestAction`, `NewMsgApproveAction`, `NewMsgFinalizeAction`, `NewMsgUpdateParams`. + +## Manage SuperNodes + +Registration and lifecycle changes use the `lumera.Blockchain.SuperNode` transaction helpers: + +```go +_, err := lumera.Blockchain.RegisterSupernodeTx(ctx, cfg.Address, "lumeravaloper...", "1.2.3.4", "lumera1sn...", "26656", "") +if err != nil { + log.Fatal(err) +} +``` + +Other tx helpers: `DeregisterSupernodeTx`, `StartSupernodeTx`, `StopSupernodeTx`, `UpdateSupernodeTx`, `UpdateSuperNodeParamsTx`. Query helpers: `GetSuperNode`, `GetSuperNodeBySuperNodeAddress`, `ListSuperNodes`, `GetTopSuperNodesForBlock`, `GetTopSuperNodesForBlockWithOptions`, `Params`. + +## EVM-side equivalents + +Both `x/action` and `x/supernode` are also reachable through the Lumera precompiles at `0x0901` and `0x0902` for Solidity / EVM callers. See [evm.md](evm.md#lumera-precompiles) for the precompile wrapper. diff --git a/docs/guides/cascade.md b/docs/guides/cascade.md new file mode 100644 index 0000000..d2675d2 --- /dev/null +++ b/docs/guides/cascade.md @@ -0,0 +1,57 @@ +# Cascade Storage + +The `Cascade` client bundles SuperNode uploads/downloads with the on-chain action lifecycle. + +## Upload a file (one-shot) + +`Upload` builds Cascade metadata, registers an action on-chain, uploads bytes to SuperNodes, and waits for completion. + +```go +result, err := lumera.Cascade.Upload(ctx, cfg.Address, lumera.Blockchain, "/path/to/file", + cascade.WithPublic(true), // optional: make file public +) +if err != nil { + log.Fatal(err) +} +log.Printf("action=%s task=%s", result.ActionID, result.TaskID) +``` + +`Upload` wraps `Client.CreateRequestActionMessage`, `Client.SendRequestActionMessage`, and `Client.UploadToSupernode`. Call those methods separately for manual control and reuse the returned `MsgRequestAction` or `types.ActionResult`. + +## Download + +```go +dl, err := lumera.Cascade.Download(ctx, "action-id", "/tmp/downloads") +if err != nil { + log.Fatal(err) +} +log.Printf("downloaded to %s", dl.OutputPath) +``` + +## Subscribe to task events + +The Cascade client bridges SuperNode SDK events and adds SDK-specific diagnostics such as `sdk:supernodes_unavailable`. + +```go +lumera.Cascade.SubscribeToAllEvents(ctx, func(_ context.Context, e event.Event) { + log.Printf("%s task=%s msg=%v", e.Type, e.TaskID, e.Data[event.KeyMessage]) +}) +``` + +## Stepwise control + +```go +msg, meta, err := lumera.Cascade.CreateRequestActionMessage(ctx, cfg.Address, "/path/file", nil) +_ = meta // metadata bytes used in the action +if err != nil { log.Fatal(err) } + +ar, err := lumera.Cascade.SendRequestActionMessage(ctx, lumera.Blockchain, msg, "memo", nil) +if err != nil { log.Fatal(err) } +log.Printf("action registered: %s", ar.ActionID) + +// Approve the action (if your flow requires it) +approve := blockchain.NewMsgApproveAction(cfg.Address, ar.ActionID) +_, err = lumera.Cascade.SendApproveActionMessage(ctx, lumera.Blockchain, approve, "") +``` + +For offline / ICA-style flows, the package-level `cascade.CreateApproveActionMessage` helper builds approvals without SuperNode dependencies. diff --git a/docs/guides/crypto.md b/docs/guides/crypto.md new file mode 100644 index 0000000..784e40e --- /dev/null +++ b/docs/guides/crypto.md @@ -0,0 +1,83 @@ +# Crypto Helpers (`pkg/crypto`) + +The `pkg/crypto` package provides keyring creation, key import, and address derivation. A single keyring supports both Cosmos (`secp256k1`) and EVM (`eth_secp256k1`) key types. + +## Key types + +`KeyType` selects the cryptographic algorithm and BIP44 derivation path: + +| KeyType | Algorithm | BIP44 Coin Type | HD Path | +| --------------- | ---------------- | --------------- | -------------------- | +| `KeyTypeCosmos` | `secp256k1` | 118 | `m/44'/118'/0'/0/0` | +| `KeyTypeEVM` | `eth_secp256k1` | 60 | `m/44'/60'/0'/0/0` | + +## Creating a keyring + +`NewKeyring` creates a keyring that accepts both key types. The algorithm is selected when importing or creating keys, not at keyring creation time. + +```go +import sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + +kr, err := sdkcrypto.NewKeyring(sdkcrypto.DefaultKeyringParams()) +``` + +## Importing keys from a mnemonic + +Use `LoadKeyring` to create a test keyring and import a key in one step: + +```go +kr, pubBytes, addr, err := sdkcrypto.LoadKeyring("alice", "mnemonic.txt", sdkcrypto.KeyTypeCosmos) +``` + +Use `ImportKey` to add a key to an existing keyring: + +```go +pubBytes, addr, err := sdkcrypto.ImportKey(kr, "bob", "mnemonic.txt", "lumera", sdkcrypto.KeyTypeCosmos) +``` + +`ImportKey` is idempotent: re-importing the same name with the same mnemonic and key type returns the existing key. It errors if the name already exists with a different key type **or** a different mnemonic, so a typo cannot silently return the wrong account. + +## Mixing key types in the same keyring + +When controller and host chains use different cryptographic key types, import keys under separate names: + +```go +kr, _ := sdkcrypto.NewKeyring(sdkcrypto.DefaultKeyringParams()) + +// Controller chain: standard Cosmos key (secp256k1, coin type 118) +sdkcrypto.ImportKey(kr, "controller-key", "mnemonic.txt", "lumera", sdkcrypto.KeyTypeCosmos) + +// Host chain: EVM-compatible key (eth_secp256k1, coin type 60) +sdkcrypto.ImportKey(kr, "host-key", "mnemonic.txt", "inj", sdkcrypto.KeyTypeEVM) +``` + +The ICA controller supports this via the `HostKeyName` config field (see [ica.md](ica.md)). + +## Deriving addresses + +`AddressFromKey` derives a bech32 address for any HRP without mutating global SDK config: + +```go +addr, err := sdkcrypto.AddressFromKey(kr, "alice", "lumera") +``` + +For EVM keys (`KeyTypeEVM`), `EVMAddressFromKey` returns the 0x-prefixed EIP-55 hex address. The key must be `eth_secp256k1`; passing a Cosmos secp256k1 key returns an error. + +```go +hexAddr, err := sdkcrypto.EVMAddressFromKey(kr, "host-key") +// e.g. 0xAbC1234... +``` + +`EVMToBech32` and `Bech32ToEVM` are byte-level conversions for 20-byte account identifiers. They round-trip EVM-derived accounts, but a legacy Cosmos `secp256k1` bech32 address does not prove or recover the key's Ethereum address; use `EVMAddressFromKey` when the keyring entry is available. + +## Ethereum-format signing primitives + +When wrapping a raw `go-ethereum` transaction yourself, the helpers in `pkg/crypto` produce an EIP-155 signed tx using the keyring: + +```go +signed, err := sdkcrypto.SignEthereumTx(kr, "host-key", big.NewInt(1414), tx) +msg, err := sdkcrypto.WrapAsMsgEthereumTx(signed) // for broadcast as a cosmos MsgEthereumTx +from, err := sdkcrypto.RecoverSender(signed) // sanity-check recovery +``` + +`Wei`, `ULume`, `ULumeDecToWei` bridge the 6 โ†” 18 decimal boundary defined by `x/precisebank` (factor `10^12`) โ€” e.g. converting `feemarket` `BaseFee` (ulume decimal) to a wei-like integer suitable for an EIP-1559 fee field. diff --git a/docs/guides/evm.md b/docs/guides/evm.md new file mode 100644 index 0000000..844b79c --- /dev/null +++ b/docs/guides/evm.md @@ -0,0 +1,159 @@ +# EVM Integration + +Lumera's Cosmos EVM stack exposes four modules โ€” `x/vm`, `x/erc20`, `x/feemarket`, `x/precisebank` โ€” plus three custom precompiles (action `0x0901`, supernode `0x0902`, wasm `0x0903`). The SDK wraps each behind an opinionated client and shares Ethereum-format signing via `pkg/crypto`. + +## Configure + +EVM helpers require an EIP-155 chain ID and the precisebank denoms. Pass them as options to `client.New`: + +```go +lumera, err := client.New(ctx, cfg, kr, + client.WithKeyName("alice"), // eth_secp256k1 key + client.WithEVMChainID(big.NewInt(1414)), // EIP-155 chain ID + client.WithEVMNativeDenom("ulume"), // x/vm evm_denom (6 dec) + client.WithEVMExtendedDenom("alume"), // precisebank extended (18 dec) +) +``` + +When `EVMNativeDenom` / `EVMExtendedDenom` are left empty, the SDK queries `x/vm` params at the first EVM call and caches per-tx. + +## Key concepts + +- **Single shared nonce.** Lumera's EVM nonce is the cosmos `auth` sequence. Mixing `SendEthereumTransaction` and `BuildAndSignTx` for the same key works without coordination โ€” both advance the same counter. The SDK does not cache nonces by default. +- **Dual address encoding.** `eth_secp256k1` accounts can be rendered as `0xโ€ฆ` *or* `lumera1โ€ฆ`. `pkg/crypto.Bech32ToEVM` and `EVMToBech32` are pure byte conversions; legacy `secp256k1` cosmos addresses are not reversible to a meaningful 0x. See [crypto.md](crypto.md). +- **6 โ†” 18 decimal bridging.** Cosmos side uses `ulume` (6 dec); EVM side uses `alume` (18 dec) with `1 ulume = 10^12 alume`. Use `sdkcrypto.Wei(ulumeAmt)` for tx `Value`, `sdkcrypto.ULume(alumeAmt)` for the inverse, and `sdkcrypto.ULumeDecToWei(dec)` for feemarket params. +- **Cosmos pipeline rejects `MsgEthereumTx`.** `BuildAndSignTxWithOptions` fails fast if you slip a `MsgEthereumTx` into the cosmos signing path. Route EVM txs through `EVMClient.SendEthereumTransaction`. + +## Read-only queries + +`Blockchain.EVM` (`x/vm`): + +```go +code, _ := lumera.Blockchain.EVM.Code(ctx, addr) +slot, _ := lumera.Blockchain.EVM.Storage(ctx, addr, key) +bal, _ := lumera.Blockchain.EVM.Balance(ctx, addr) // alume +acct, _ := lumera.Blockchain.EVM.EthAccount(ctx, addr) // nonce, code hash +cosmos, _ := lumera.Blockchain.EVM.CosmosAccount(ctx, addr) // paired bech32 + sequence +params, _ := lumera.Blockchain.EVM.Params(ctx) +baseFee, _ := lumera.Blockchain.EVM.BaseFee(ctx) // alume integer +minGas, _ := lumera.Blockchain.EVM.GlobalMinGasPrice(ctx) // alume integer +ret, _ := lumera.Blockchain.EVM.EthCall(ctx, from, &to, calldata, gasCap) +gas, _ := lumera.Blockchain.EVM.EstimateGas(ctx, from, &to, calldata, value, gasCap) +``` + +`Blockchain.FeeMarket` (`x/feemarket`) โ€” `Params`, `BaseFee` (ulume decimal), `BlockGas`. +`Blockchain.PreciseBank` (`x/precisebank`) โ€” `Remainder`, `FractionalBalance`. + +`examples/evm-balance` walks through reading balance, nonce, base fee, and min gas price for a single address. + +## Send an Ethereum-format transaction + +`SendEthereumTransaction` resolves nonce, gas, and EIP-1559 caps from chain state, signs with the keyring's `eth_secp256k1` key, wraps in `MsgEthereumTx` with the required extension option, and broadcasts. + +```go +to := common.HexToAddress("0x000000000000000000000000000000000000dEaD") +amount := sdkcrypto.Wei(sdkmath.NewInt(1)) // 1 ulume = 10^12 alume + +res, err := lumera.Blockchain.EVM.SendEthereumTransaction(ctx, &to, nil, &blockchain.EthereumTxOptions{ + Value: amount, +}) +if err != nil { log.Fatal(err) } +log.Printf("eth %s cosmos %s height %d gas %d", + res.EthTxHash.Hex(), res.CosmosHash, res.Height, res.GasUsed) +``` + +Overrides on `EthereumTxOptions`: `Nonce`, `GasLimit`, `GasTipCap`, `GasFeeCap`, `Value`, `AccessList`. + +Other helpers on `Blockchain.EVM`: + +- `CallContract(ctx, to, calldata)` โ€” read-only ABI call (forwards to `EthCall`). +- `DeployContract(ctx, bytecode, opts)` โ€” contract creation; returns the deployed address (derived from sender + nonce). Returns the zero address and an error if the constructor reverts. +- `RawEthereumTx(ctx, signedTx)` โ€” broadcast a tx signed elsewhere (hardware wallet, MetaMask). + +`examples/evm-transfer` is a complete CLI. + +## ERC20 conversion + +`x/erc20` lets cosmos coins be wrapped as ERC20 and vice versa. Both directions use the regular cosmos signing pipeline (no Ethereum signature on the conversion message itself). + +```go +// ulume -> ERC20 for the EVM address. +coin := sdk.NewInt64Coin("ulume", 1_000_000) +recv := common.HexToAddress("0xAbC...") +_, err := lumera.Blockchain.ConvertCoinToERC20(ctx, coin, recv, "memo") + +// ERC20 -> ulume for the bech32 receiver. Sender is the 0x address derived +// from the client's eth_secp256k1 key. +contract := common.HexToAddress("0xCafe...") +_, err = lumera.Blockchain.ConvertERC20ToCoin(ctx, contract, sdkmath.NewInt(42), "lumera1recv...", "") + +// Governance flows: RegisterERC20Tx, ToggleConversionTx. +``` + +Read-only ABI sugar (no Solidity compilation needed): + +```go +bal, _ := lumera.Blockchain.ERC20.Erc20Balance(ctx, contract, holder) +sup, _ := lumera.Blockchain.ERC20.Erc20TotalSupply(ctx, contract) +allow, _ := lumera.Blockchain.ERC20.Erc20Allowance(ctx, contract, owner, spender) +meta, _ := lumera.Blockchain.ERC20.Erc20Metadata(ctx, contract) // name, symbol, decimals +``` + +Query helpers: `TokenPairs(ctx, pagination)`, `TokenPair(ctx, denomOr0x)`, `Params(ctx)`. + +`examples/erc20-convert` exercises both directions. + +## Lumera precompiles + +Custom precompiles at fixed addresses (per `pkg/evm/precompiles`): + +| Address | Module | Wrapper | +| ------- | ------------- | ----------------------------- | +| `0x0901` | `x/action` | `Blockchain.EVM.Action` | +| `0x0902` | `x/supernode` | `Blockchain.EVM.Supernode` | +| `0x0903` | CosmWasm | `Blockchain.EVM.Wasm` | + +Each wrapper holds the precompile address plus the embedded ABI (loaded from `pkg/evm/precompiles/abi/`). Two methods: + +- `Call(ctx, method, args...)` โ€” read-only, routes through `EthCall`, returns the unpacked outputs. +- `Send(ctx, method, opts, args...)` โ€” state-changing, signs and broadcasts as a `MsgEthereumTx`. + +```go +// Read: get action module params from the EVM side. +out, err := lumera.Blockchain.EVM.Action.Call(ctx, "getParams") + +// Write: approve an action via the precompile, signed with eth_secp256k1. +_, err = lumera.Blockchain.EVM.Action.Send(ctx, "approveAction", nil, "action-id-here") +``` + +The eight standard cosmos/evm precompiles (bank, staking, distribution, gov, ICS20, bech32, p256, slashing) are exposed as address constants only โ€” see `pkg/evm/precompiles`. + +`examples/precompile-action` issues a read-only `getParams` and a write `approveAction`. + +## EVM migration + +`x/evmigration` handles the one-time migration of pre-EVM (coin type 118) accounts and validators to the EVM-enabled (coin type 60) chain. Use queries for pre-flight, then submit the migration tx signed by the new address. + +The migration tx helpers intentionally build zero-fee transactions for the chain-waived evmigration flow. + +```go +// Pre-flight. +est, err := lumera.Blockchain.EVMigration.MigrationEstimate(ctx, legacyAddr) +if err != nil { log.Fatal(err) } +if !est.WouldSucceed { + log.Fatalf("cannot migrate: %s", est.RejectionReason) +} + +// Build proofs externally, then submit. +msg := blockchain.NewMsgClaimLegacyAccount(newAddr, legacyAddr, legacyProof, newProof) +res, err := lumera.Blockchain.ClaimLegacyAccountTx(ctx, msg, "migrate") +if err != nil { log.Fatal(err) } +log.Printf("migrated legacy=%s new=%s tx=%s height=%d", + res.LegacyAddress, res.NewAddress, res.TxHash, res.Height) + +// Validators use MigrateValidatorTx. +vmsg := blockchain.NewMsgMigrateValidator(newAddr, legacyAddr, legacyProof, newProof) +_, err = lumera.Blockchain.MigrateValidatorTx(ctx, vmsg, "") +``` + +Read-only helpers: `MigrationRecord(legacyAddress)`, `MigrationRecordByNewAddress(newAddress)`, `MigrationStats(ctx)`, `Params(ctx)`. `MigrationProof` construction is chain-specific โ€” see the `x/evmigration` proto definitions. diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md new file mode 100644 index 0000000..4287346 --- /dev/null +++ b/docs/guides/getting-started.md @@ -0,0 +1,79 @@ +# Getting Started + +## Prerequisites + +- Go 1.26+ with module support. +- Access to Lumera endpoints: `grpc` (chain queries/tx), `rpc` (websocket for tx inclusion), and at least one SuperNode for Cascade uploads/downloads. +- A Cosmos keyring entry that can sign Lumera transactions (`github.com/cosmos/cosmos-sdk/crypto/keyring` is used throughout the SDK). + +## Install + +```bash +go get github.com/LumeraProtocol/sdk-go +``` + +## Configuration reference + +`client.Config` (in `client/config`) drives both blockchain and Cascade clients: + +- `ChainID`, `GRPCEndpoint`, `RPCEndpoint` โ€“ chain connection details. gRPC uses TLS automatically for non-local hosts/port 443. +- `Address`, `KeyName` โ€“ Cosmos account info in your keyring. +- `BlockchainTimeout`, `StorageTimeout` โ€“ default deadlines for chain and Cascade operations. +- `MaxRecvMsgSize`, `MaxSendMsgSize`, `MaxRetries` โ€“ transport tuning. The default receive limit is 4 MiB; increase it only for endpoints that need larger query responses. +- `WaitTx` โ€“ controls websocket vs polling behaviour when waiting for tx inclusion (see defaults in `client/config`). +- `Logger` โ€“ optional; when set, SDK operations emit diagnostics. +- `LogLevel` โ€“ default logging threshold when no custom logger is supplied (default: error). +- `AccountHRP`, `FeeDenom`, `GasPrice` โ€“ chain economics. Empty/nil values fall back to Lumera defaults (`lumera`, `ulume`, `0.025`); set them for non-Lumera chains. +- EVM settings: `EVMChainID`, `EVMNativeDenom`, `EVMExtendedDenom`, `EVMGasTipCap`, `EVMGasFeeCap`. See [evm.md](evm.md). + +Override fields with `client.With...` option helpers when calling `client.New`. + +## Creating a client + +```go +ctx := context.Background() +kr, _ := keyring.New("lumera", "test", "/tmp", nil) + +cfg := client.Config{ + ChainID: "lumera-testnet-2", + GRPCEndpoint: "localhost:9090", + RPCEndpoint: "http://localhost:26657", + Address: "lumera1abc...", + KeyName: "my-key", +} + +logger := zap.NewExample() +lumera, err := client.New(ctx, cfg, kr, client.WithLogger(logger)) +if err != nil { + logger.Error("client init failed", zap.Error(err)) +} +defer lumera.Close() +``` + +`client.Client` exposes `Blockchain` (gRPC chain modules) and `Cascade` (SuperNode SDK + SnApi). + +## Multiple signers via the factory + +`client.NewFactory` keeps a shared config/keyring and returns signer-specific clients: + +```go +factory, _ := client.NewFactory(cfg, kr) +alice, _ := factory.WithSigner(ctx, "lumera1alice...", "alice") +bob, _ := factory.WithSigner(ctx, "lumera1bob...", "bob") +defer alice.Close() +defer bob.Close() +``` + +## Running the examples + +- Run tests: `make test` +- Build samples: `make examples` +- Try a flow: `go run ./examples/cascade-upload`, `./examples/query-actions`, `./examples/multi-account`, `./examples/evm-transfer`, `./examples/erc20-convert`, `./examples/precompile-action --help` + +## Troubleshooting + +- **Tx inclusion timing out**: adjust `WaitTx` polling/backoff (see `client/config`). Ensure `RPCEndpoint` allows websocket subscriptions. +- **gRPC TLS errors**: remote hosts/port 443 default to TLS; for local nodes use `localhost:9090` or `127.0.0.1:9090`. +- **Key not found**: confirm the key name exists in the keyring path you passed to `keyring.New`. +- **SuperNode availability**: Cascade operations require reachable SuperNodes; watch `sdk:supernodes_unavailable` events for diagnostics. +- **EVM tx fails to encode**: confirm `EVMChainID` is set and the signing key is `eth_secp256k1`. The SDK rejects `MsgEthereumTx` in the cosmos signing pipeline โ€” use `EVM.SendEthereumTransaction`. diff --git a/docs/guides/ica.md b/docs/guides/ica.md new file mode 100644 index 0000000..e6e0b48 --- /dev/null +++ b/docs/guides/ica.md @@ -0,0 +1,98 @@ +# Interchain Accounts (ICA) + +The `ica` package provides an ICS-27 controller plus low-level packet helpers. Use it when a controller-chain account submits Lumera `MsgRequestAction` / `MsgApproveAction` messages on behalf of an ICA address. + +## Controller flow + +`ica.Controller` connects to both a controller chain and the Lumera host chain over gRPC and handles ICA registration, IBC packet construction, transaction broadcasting, acknowledgement polling, and action ID extraction: + +```go +ctrl, _ := ica.NewController(ctx, ica.Config{ + Controller: controllerBaseConfig, + Host: hostBaseConfig, + Keyring: kr, + KeyName: "controller-key", + HostKeyName: "host-key", // optional: separate key for host chain + ConnectionID: "connection-0", +}) +defer ctrl.Close() + +addr, _ := ctrl.EnsureICAAddress(ctx) // register + poll until ready +result, _ := ctrl.SendRequestAction(ctx, msg) // send, wait for ack, return action ID +txHash, _ := ctrl.SendApproveAction(ctx, approveMsg) +``` + +For lower-level or offline workflows, the packet-building helpers are exported separately: `PackRequestAny`, `BuildICAPacketData`, `BuildMsgSendTx`. + +## Lower-level packet flow + +When you want to broadcast the controller-chain `MsgSendTx` with your own tooling, the SDK still helps build the Lumera-side payload: + +```go +ctx := context.Background() +// Reuse kr from the client setup. +cascadeClient, err := cascade.New(ctx, cascade.Config{ + ChainID: "lumera-testnet-2", + GRPCAddr: "localhost:9090", + Address: "lumera1abc...", + KeyName: "my-key", + ICAOwnerKeyName: "my-key", + ICAOwnerHRP: "inj", +}, kr) +if err != nil { log.Fatal(err) } +defer cascadeClient.Close() + +uploadOpts := &cascade.UploadOptions{} +cascade.WithICACreatorAddress("lumera1ica...")(uploadOpts) +cascade.WithAppPubkey(appPubkey)(uploadOpts) + +msg, _, err := cascadeClient.CreateRequestActionMessage(ctx, "lumera1abc...", "/path/file", uploadOpts) +if err != nil { log.Fatal(err) } + +any, err := ica.PackRequestAny(msg) +if err != nil { log.Fatal(err) } + +packet, err := ica.BuildICAPacketData([]*codectypes.Any{any}) +if err != nil { log.Fatal(err) } + +msgSendTx, err := ica.BuildMsgSendTx(ownerAddr, "connection-0", 600_000_000_000, packet) +if err != nil { log.Fatal(err) } + +// Broadcast msgSendTx using your controller-chain SDK or CLI. +``` + +See `examples/ica-request-tx` for a full CLI that builds the ICA packet and prints the JSON. + +## Tips + +- You must provide Lumera chain `grpc` + `chain-id` so metadata (price/expiration) can be computed. +- Set the ICA creator address and app pubkey on the request message. +- The Cascade client uses `ICAOwnerKeyName` + `ICAOwnerHRP` to derive the controller owner address; `appPubkey` should be the controller key's pubkey bytes from the keyring. +- When controller and host chains use different key types, import keys under separate names into the same keyring and set `HostKeyName` on the ICA `Config`. See [crypto.md](crypto.md). + +## Strengths + +- **Minimal setup** โ€” only gRPC endpoints, a keyring, and an IBC connection ID are required. No Docker, no relayer binary, no chain binaries. +- **End-to-end in one call** โ€” `SendRequestAction` builds the ICA packet, broadcasts on the controller chain, waits for tx inclusion, resolves the counterparty channel, polls for the host-chain acknowledgement, and extracts the action ID. +- **Mixed key type support** โ€” controller and host chains can use different cryptographic key types (`KeyTypeCosmos` / `KeyTypeEVM`) by setting `HostKeyName` to a separate key in the same keyring. +- **Resilient polling** โ€” configurable retry counts and delays for both ICA registration (`PollRetries` / `PollDelay`) and acknowledgement waiting (`AckRetries`). +- **Tight Lumera integration** โ€” purpose-built for `MsgRequestAction` and `MsgApproveAction`, with typed results (`ActionResult`) and Cascade metadata compatibility. + +## Limitations + +- **Requires running chains** โ€” the controller connects to live gRPC endpoints. It does not spin up chains or relayers; infrastructure must already be deployed. +- **Lumera-specific high-level methods** โ€” `SendRequestAction` and `SendApproveAction` are tailored to Lumera action messages. Generic ICA message execution requires using the lower-level packet helpers directly. +- **No chain lifecycle management** โ€” unlike e2e testing frameworks (e.g., interchaintest), there is no built-in chain provisioning, genesis configuration, or relayer orchestration. +- **Relayer dependency** โ€” IBC packet relay between controller and host chains depends on an external relayer (e.g., Hermes). The controller does not relay packets itself. + +## When to use this vs. interchaintest + +| Aspect | `ica.Controller` | interchaintest | +| ------------------- | ------------------------------- | ----------------------------------------- | +| **Purpose** | Production client / scripting | E2E integration testing | +| **Infrastructure** | Connects to running chains | Spins up chains + relayers in Docker | +| **Setup effort** | Config struct + keyring | Docker, chain binaries, genesis config | +| **Iteration speed** | Fast (gRPC calls) | Slower (container lifecycle + blocks) | +| **Scope** | Lumera ICA operations | Any IBC flow, any chain | + +Use `ica.Controller` when you have running chains and need to execute ICA operations in production or automation scripts. Use interchaintest when you need to validate the full ICA flow in CI from scratch without external infrastructure. diff --git a/examples/erc20-convert/main.go b/examples/erc20-convert/main.go new file mode 100644 index 0000000..c7aa190 --- /dev/null +++ b/examples/erc20-convert/main.go @@ -0,0 +1,112 @@ +// erc20-convert exercises Lumera's ERC20 conversion flow in both directions. +// Direction is controlled by --mode: +// +// coin-to-erc20: wraps ulume coins as ERC20 tokens at the configured 0x receiver +// erc20-to-coin: unwraps ERC20 tokens back to ulume at the configured bech32 receiver +// +// The signing key must be eth_secp256k1 so the 0x sender derivation matches +// Lumera's account semantics. +package main + +import ( + "context" + "flag" + "log" + "math/big" + + sdkmath "cosmossdk.io/math" + lumerasdk "github.com/LumeraProtocol/sdk-go/client" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/ethereum/go-ethereum/common" +) + +func main() { + ctx := context.Background() + + grpcEndpoint := flag.String("grpc-endpoint", "localhost:9090", "Lumera gRPC endpoint") + rpcEndpoint := flag.String("rpc-endpoint", "http://localhost:26657", "Lumera RPC endpoint") + chainID := flag.String("chain-id", "lumera-testnet-2", "Cosmos chain ID") + evmChainID := flag.Int64("evm-chain-id", 1414, "EIP-155 chain ID") + keyringBackend := flag.String("keyring-backend", "os", "Keyring backend: os|file|test") + keyringDir := flag.String("keyring-dir", "~/.lumera", "Keyring base directory") + keyName := flag.String("key-name", "alice", "Key name (eth_secp256k1)") + addrBech32 := flag.String("address", "", "Bech32 address paired with the key") + mode := flag.String("mode", "coin-to-erc20", "coin-to-erc20 | erc20-to-coin") + amount := flag.Int64("amount", 1, "amount (ulume for coin-to-erc20, raw integer for erc20-to-coin)") + contract := flag.String("contract", "", "ERC20 contract address (required for erc20-to-coin)") + to := flag.String("to", "", "0x receiver (coin-to-erc20) or bech32 receiver (erc20-to-coin)") + flag.Parse() + + kr, err := sdkcrypto.NewKeyring(sdkcrypto.KeyringParams{ + AppName: "lumera", + Backend: *keyringBackend, + Dir: *keyringDir, + }) + if err != nil { + log.Fatalf("create keyring: %v", err) + } + + client, err := lumerasdk.New(ctx, lumerasdk.Config{ + ChainID: *chainID, + GRPCEndpoint: *grpcEndpoint, + RPCEndpoint: *rpcEndpoint, + Address: *addrBech32, + KeyName: *keyName, + }, kr, + lumerasdk.WithEVMChainID(big.NewInt(*evmChainID)), + lumerasdk.WithEVMNativeDenom("ulume"), + lumerasdk.WithEVMExtendedDenom("alume"), + ) + if err != nil { + log.Fatalf("create client: %v", err) + } + defer func() { + if err := client.Close(); err != nil { + log.Printf("close client: %v", err) + } + }() + + switch *mode { + case "coin-to-erc20": + if *to == "" { + log.Fatalf("--to (0x receiver) is required") + } + coin := sdk.NewCoin("ulume", sdkmath.NewInt(*amount)) + recv := common.HexToAddress(*to) + res, err := client.Blockchain.ConvertCoinToERC20(ctx, coin, recv, "") + if err != nil { + log.Fatalf("ConvertCoinToERC20: %v", err) + } + log.Printf("wrapped %s ulume -> %s", res.Amount, res.To) + log.Printf("tx %s height %d", res.TxHash, res.Height) + + case "erc20-to-coin": + if *contract == "" || *to == "" { + log.Fatalf("--contract and --to (bech32 receiver) are required") + } + res, err := client.Blockchain.ConvertERC20ToCoin(ctx, + common.HexToAddress(*contract), + sdkmath.NewInt(*amount), + *to, + "", + ) + if err != nil { + log.Fatalf("ConvertERC20ToCoin: %v", err) + } + log.Printf("unwrapped %s from %s -> %s", res.Amount, res.From, res.To) + log.Printf("tx %s height %d", res.TxHash, res.Height) + + default: + log.Fatalf("unknown --mode %q (expected coin-to-erc20 or erc20-to-coin)", *mode) + } + + // Optional: print the receiver's ERC20 balance after coin-to-erc20. + if *mode == "coin-to-erc20" && *contract != "" { + holder := common.HexToAddress(*to) + bal, err := client.Blockchain.ERC20.Erc20Balance(ctx, common.HexToAddress(*contract), holder) + if err == nil { + log.Printf("post-tx ERC20 balance for %s: %s", holder.Hex(), bal) + } + } +} diff --git a/examples/evm-balance/main.go b/examples/evm-balance/main.go new file mode 100644 index 0000000..c508bb2 --- /dev/null +++ b/examples/evm-balance/main.go @@ -0,0 +1,121 @@ +// evm-balance prints EVM-side state for an address: balance (alume), nonce, +// the current feemarket BaseFee and MinGasPrice, plus the precisebank +// fractional balance. Read-only โ€” no signing or broadcasting. +// +// Usage: +// +// go run ./examples/evm-balance \ +// --grpc-endpoint localhost:9090 \ +// --address 0xAbC... +// +// If --address is omitted the example reads the keyring entry and derives +// the 0x address from it. +package main + +import ( + "context" + "flag" + "log" + + lumerasdk "github.com/LumeraProtocol/sdk-go/client" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + "github.com/ethereum/go-ethereum/common" +) + +func main() { + ctx := context.Background() + + grpcEndpoint := flag.String("grpc-endpoint", "localhost:9090", "Lumera gRPC endpoint") + rpcEndpoint := flag.String("rpc-endpoint", "http://localhost:26657", "Lumera RPC endpoint") + chainID := flag.String("chain-id", "lumera-testnet-2", "Cosmos chain ID") + keyringBackend := flag.String("keyring-backend", "os", "Keyring backend: os|file|test") + keyringDir := flag.String("keyring-dir", "~/.lumera", "Keyring base directory") + keyName := flag.String("key-name", "alice", "Key name (eth_secp256k1) โ€” used only if --address is empty") + addrFlag := flag.String("address", "", "0x address to inspect; defaults to the keyring entry") + flag.Parse() + + kr, err := sdkcrypto.NewKeyring(sdkcrypto.KeyringParams{ + AppName: "lumera", + Backend: *keyringBackend, + Dir: *keyringDir, + }) + if err != nil { + log.Fatalf("create keyring: %v", err) + } + + addr := *addrFlag + if addr == "" { + addr, err = sdkcrypto.EVMAddressFromKey(kr, *keyName) + if err != nil { + log.Fatalf("derive address from key %q: %v", *keyName, err) + } + } + evmAddr := common.HexToAddress(addr) + log.Printf("inspecting %s", evmAddr.Hex()) + + client, err := lumerasdk.New(ctx, lumerasdk.Config{ + ChainID: *chainID, + GRPCEndpoint: *grpcEndpoint, + RPCEndpoint: *rpcEndpoint, + Address: "lumera1placeholder", // not used for read-only flow + KeyName: *keyName, + }, kr) + if err != nil { + log.Fatalf("create client: %v", err) + } + defer func() { + if err := client.Close(); err != nil { + log.Printf("close client: %v", err) + } + }() + + bc := client.Blockchain + + balance, err := bc.EVM.Balance(ctx, evmAddr) + if err != nil { + log.Fatalf("Balance: %v", err) + } + log.Printf("balance (alume): %s", balance) + + if ulume, frac := sdkcrypto.ULume(balance); frac.Sign() == 0 { + log.Printf("balance (ulume): %s", ulume) + } else { + log.Printf("balance (ulume): %s + %s alume fractional", ulume, frac) + } + + acct, err := bc.EVM.EthAccount(ctx, evmAddr) + if err != nil { + log.Fatalf("EthAccount: %v", err) + } + log.Printf("nonce / sequence: %d", acct.Nonce) + log.Printf("code hash: %s", acct.CodeHash) + + baseFee, err := bc.EVM.BaseFee(ctx) + if err != nil { + log.Fatalf("EVM BaseFee: %v", err) + } + log.Printf("EVM base fee (alume): %s", baseFee) + + feeMarketBase, err := bc.FeeMarket.BaseFee(ctx) + if err != nil { + log.Fatalf("FeeMarket BaseFee: %v", err) + } + log.Printf("base fee (ulume): %s", feeMarketBase) + + params, err := bc.FeeMarket.Params(ctx) + if err != nil { + log.Fatalf("FeeMarket Params: %v", err) + } + log.Printf("min gas price (ulume): %s", params.Params.MinGasPrice) + + // Optional precisebank fractional balance (lookup by bech32 address). + bech32, err := sdkcrypto.EVMToBech32(evmAddr, "lumera") + if err == nil { + frac, err := bc.PreciseBank.FractionalBalance(ctx, bech32) + if err != nil { + log.Printf("FractionalBalance: %v", err) + } else { + log.Printf("fractional balance: %s", frac) + } + } +} diff --git a/examples/evm-transfer/main.go b/examples/evm-transfer/main.go new file mode 100644 index 0000000..d3afe8a --- /dev/null +++ b/examples/evm-transfer/main.go @@ -0,0 +1,99 @@ +// evm-transfer demonstrates sending an EIP-1559 transaction to Lumera using +// an eth_secp256k1 key in the SDK's keyring. It prints the eth tx hash, the +// cosmos tx hash, and the resolved gas/fee values pulled from chain state. +// +// Usage: +// +// go run ./examples/evm-transfer \ +// --grpc-endpoint localhost:9090 \ +// --rpc-endpoint http://localhost:26657 \ +// --chain-id lumera-testnet-2 \ +// --evm-chain-id 1414 \ +// --key-name alice \ +// --to 0x000000000000000000000000000000000000dEaD \ +// --amount-ulume 1 +package main + +import ( + "context" + "flag" + "log" + "math/big" + + sdkmath "cosmossdk.io/math" + "github.com/LumeraProtocol/sdk-go/blockchain" + lumerasdk "github.com/LumeraProtocol/sdk-go/client" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" + "github.com/ethereum/go-ethereum/common" +) + +func main() { + ctx := context.Background() + + grpcEndpoint := flag.String("grpc-endpoint", "localhost:9090", "Lumera gRPC endpoint") + rpcEndpoint := flag.String("rpc-endpoint", "http://localhost:26657", "Lumera RPC endpoint") + chainID := flag.String("chain-id", "lumera-testnet-2", "Cosmos chain ID") + evmChainID := flag.Int64("evm-chain-id", 1414, "EIP-155 chain ID") + keyringBackend := flag.String("keyring-backend", "os", "Keyring backend: os|file|test") + keyringDir := flag.String("keyring-dir", "~/.lumera", "Keyring base directory") + keyName := flag.String("key-name", "alice", "Key name (must be eth_secp256k1)") + addrBech32 := flag.String("address", "", "Bech32 address paired with the key") + to := flag.String("to", "0x000000000000000000000000000000000000dEaD", "Recipient 0x address") + amountULume := flag.Int64("amount-ulume", 1, "Amount to transfer in ulume (will be converted to alume)") + flag.Parse() + + kr, err := sdkcrypto.NewKeyring(sdkcrypto.KeyringParams{ + AppName: "lumera", + Backend: *keyringBackend, + Dir: *keyringDir, + }) + if err != nil { + log.Fatalf("create keyring: %v", err) + } + + from, err := sdkcrypto.EVMAddressFromKey(kr, *keyName) + if err != nil { + log.Fatalf("derive 0x sender (is %q an eth_secp256k1 key?): %v", *keyName, err) + } + log.Printf("sender 0x address: %s", from) + + cfg := lumerasdk.Config{ + ChainID: *chainID, + GRPCEndpoint: *grpcEndpoint, + RPCEndpoint: *rpcEndpoint, + Address: *addrBech32, + KeyName: *keyName, + } + client, err := lumerasdk.New(ctx, cfg, kr, + lumerasdk.WithEVMChainID(big.NewInt(*evmChainID)), + lumerasdk.WithEVMNativeDenom("ulume"), + lumerasdk.WithEVMExtendedDenom("alume"), + ) + if err != nil { + log.Fatalf("create client: %v", err) + } + defer func() { + if err := client.Close(); err != nil { + log.Printf("close client: %v", err) + } + }() + + recipient := common.HexToAddress(*to) + value := sdkcrypto.Wei(sdkmath.NewInt(*amountULume)) + log.Printf("sending %d ulume (%s alume) to %s", *amountULume, value, recipient.Hex()) + + res, err := client.Blockchain.EVM.SendEthereumTransaction(ctx, &recipient, nil, &blockchain.EthereumTxOptions{ + Value: value, + }) + if err != nil { + log.Fatalf("send tx: %v", err) + } + + log.Printf("eth tx hash: %s", res.EthTxHash.Hex()) + log.Printf("cosmos tx hash: %s", res.CosmosHash) + log.Printf("height: %d", res.Height) + log.Printf("gas used: %d", res.GasUsed) + if res.VMError != "" { + log.Fatalf("vm error: %s", res.VMError) + } +} diff --git a/examples/ica-request-verify/main.go b/examples/ica-request-verify/main.go index 9896269..a6d3bf4 100644 --- a/examples/ica-request-verify/main.go +++ b/examples/ica-request-verify/main.go @@ -20,7 +20,7 @@ import ( "github.com/LumeraProtocol/sdk-go/constants" "github.com/LumeraProtocol/sdk-go/ica" sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" - "github.com/LumeraProtocol/sdk-go/pkg/crypto/ethsecp256k1" + "github.com/cosmos/evm/crypto/ethsecp256k1" sdktypes "github.com/LumeraProtocol/sdk-go/types" codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" diff --git a/examples/precompile-action/main.go b/examples/precompile-action/main.go new file mode 100644 index 0000000..8e9e677 --- /dev/null +++ b/examples/precompile-action/main.go @@ -0,0 +1,99 @@ +// precompile-action invokes Lumera's action precompile at 0x0901 through the +// SDK's generic PrecompileClient. Two modes: +// +// get-params reads x/action params via Call (read-only, no signature) +// approve sends approveAction(actionID) via Send (signed, broadcast) +// +// The published ABI lives at pkg/evm/precompiles/abi/action.json โ€” the +// embedded copy here is the same artifact, parsed at SDK init. +package main + +import ( + "context" + "flag" + "fmt" + "log" + "math/big" + + lumerasdk "github.com/LumeraProtocol/sdk-go/client" + sdkcrypto "github.com/LumeraProtocol/sdk-go/pkg/crypto" +) + +func main() { + ctx := context.Background() + + grpcEndpoint := flag.String("grpc-endpoint", "localhost:9090", "Lumera gRPC endpoint") + rpcEndpoint := flag.String("rpc-endpoint", "http://localhost:26657", "Lumera RPC endpoint") + chainID := flag.String("chain-id", "lumera-testnet-2", "Cosmos chain ID") + evmChainID := flag.Int64("evm-chain-id", 1414, "EIP-155 chain ID") + keyringBackend := flag.String("keyring-backend", "os", "Keyring backend: os|file|test") + keyringDir := flag.String("keyring-dir", "~/.lumera", "Keyring base directory") + keyName := flag.String("key-name", "alice", "Key name (eth_secp256k1) for signed methods") + addrBech32 := flag.String("address", "", "Bech32 address paired with the key (required for signed methods)") + mode := flag.String("mode", "get-params", "get-params | approve") + actionID := flag.String("action-id", "", "Action ID to approve (required for --mode=approve)") + flag.Parse() + + kr, err := sdkcrypto.NewKeyring(sdkcrypto.KeyringParams{ + AppName: "lumera", + Backend: *keyringBackend, + Dir: *keyringDir, + }) + if err != nil { + log.Fatalf("create keyring: %v", err) + } + + client, err := lumerasdk.New(ctx, lumerasdk.Config{ + ChainID: *chainID, + GRPCEndpoint: *grpcEndpoint, + RPCEndpoint: *rpcEndpoint, + Address: *addrBech32, + KeyName: *keyName, + }, kr, + lumerasdk.WithEVMChainID(big.NewInt(*evmChainID)), + lumerasdk.WithEVMNativeDenom("ulume"), + lumerasdk.WithEVMExtendedDenom("alume"), + ) + if err != nil { + log.Fatalf("create client: %v", err) + } + defer func() { + if err := client.Close(); err != nil { + log.Printf("close client: %v", err) + } + }() + + action := client.Blockchain.EVM.Action + log.Printf("precompile address: %s", action.Address().Hex()) + + switch *mode { + case "get-params": + out, err := action.Call(ctx, "getParams") + if err != nil { + log.Fatalf("Call getParams: %v", err) + } + log.Printf("getParams returned %d values:", len(out)) + for i, v := range out { + log.Printf(" [%d] %s", i, fmt.Sprintf("%+v", v)) + } + + case "approve": + if *actionID == "" { + log.Fatalf("--action-id is required for --mode=approve") + } + res, err := action.Send(ctx, "approveAction", nil, *actionID) + if err != nil { + log.Fatalf("Send approveAction: %v", err) + } + log.Printf("eth tx: %s", res.EthTxHash.Hex()) + log.Printf("cosmos tx: %s", res.CosmosHash) + log.Printf("height: %d", res.Height) + log.Printf("gas used: %d", res.GasUsed) + if res.VMError != "" { + log.Fatalf("vm error: %s", res.VMError) + } + + default: + log.Fatalf("unknown --mode %q (expected get-params or approve)", *mode) + } +} diff --git a/go.mod b/go.mod index a0f36fa..b479990 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,18 @@ module github.com/LumeraProtocol/sdk-go -go 1.25.9 +go 1.26.2 // Pin compatible versions to prevent go mod tidy from updating replace ( + // Local development - uncomment these for local testing + // Comment these lines before releasing + //github.com/LumeraProtocol/lumera => ../lumera + //github.com/LumeraProtocol/supernode/v2 => ../supernode github.com/envoyproxy/protoc-gen-validate => github.com/bufbuild/protoc-gen-validate v1.3.0 + // cosmos/evm requires a forked go-ethereum with custom EVM operation methods + github.com/ethereum/go-ethereum => github.com/cosmos/go-ethereum v1.16.2-cosmos-1 github.com/lyft/protoc-gen-validate => github.com/envoyproxy/protoc-gen-validate v1.3.0 nhooyr.io/websocket => github.com/coder/websocket v1.8.7 -// Local development - uncomment these for local testing -// Comment these lines before releasing -//github.com/LumeraProtocol/lumera => ../lumera -//github.com/LumeraProtocol/supernode/v2 => ../supernode ) require ( @@ -18,47 +20,55 @@ require ( cosmossdk.io/math v1.5.3 // Lumera blockchain types (generated proto) - github.com/LumeraProtocol/lumera v1.12.0-rc + github.com/LumeraProtocol/lumera v1.20.0-rc2 // SuperNode SDK for storage operations github.com/LumeraProtocol/supernode/v2 v2.5.2 github.com/cometbft/cometbft v0.38.21 - github.com/cosmos/cosmos-sdk v0.53.5 + github.com/cosmos/cosmos-sdk v0.53.6 + github.com/cosmos/evm v0.6.0 github.com/cosmos/go-bip39 v1.0.0 github.com/cosmos/gogoproto v1.7.2 github.com/cosmos/ibc-go/v10 v10.5.0 - github.com/ethereum/go-ethereum v1.15.11 + github.com/ethereum/go-ethereum v1.17.0 github.com/stretchr/testify v1.11.1 go.uber.org/zap v1.27.0 // gRPC and networking - google.golang.org/grpc v1.77.0 + google.golang.org/grpc v1.80.0 lukechampine.com/blake3 v1.4.1 ) require ( - cosmossdk.io/collections v1.3.1 // indirect + cosmossdk.io/collections v1.4.0 // indirect cosmossdk.io/core v0.11.3 // indirect cosmossdk.io/depinject v1.2.1 // indirect - cosmossdk.io/errors v1.0.2 // indirect + cosmossdk.io/errors v1.1.0 // indirect cosmossdk.io/log v1.6.1 // indirect cosmossdk.io/schema v1.1.0 // indirect cosmossdk.io/store v1.1.2 // indirect + cosmossdk.io/x/feegrant v0.2.0 // indirect cosmossdk.io/x/tx v0.14.0 // indirect cosmossdk.io/x/upgrade v0.2.0 // indirect - filippo.io/edwards25519 v1.1.0 // indirect + filippo.io/edwards25519 v1.1.1 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.2 // indirect github.com/DataDog/datadog-go v4.8.3+incompatible // indirect github.com/DataDog/zstd v1.5.7 // indirect github.com/LumeraProtocol/rq-go v0.2.1 // indirect - github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/VictoriaMetrics/fastcache v1.12.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/speakeasy v0.2.0 // indirect + github.com/bits-and-blooms/bitset v1.24.3 // indirect + github.com/btcsuite/btcd v0.24.2 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect + github.com/btcsuite/btcd/btcutil v1.1.6 // indirect + github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.14.2 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect @@ -69,15 +79,19 @@ require ( github.com/cockroachdb/redact v1.1.6 // indirect github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/cometbft/cometbft-db v0.14.1 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-db v1.1.3 // indirect github.com/cosmos/cosmos-proto v1.0.0-beta.5 // indirect github.com/cosmos/gogogateway v1.2.0 // indirect github.com/cosmos/iavl v1.2.6 // indirect github.com/cosmos/ics23/go v0.11.0 // indirect - github.com/cosmos/ledger-cosmos-go v0.16.0 // indirect + github.com/cosmos/ledger-cosmos-go v1.0.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/danieljoos/wincred v1.2.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/desertbit/timer v1.0.1 // indirect github.com/dgraph-io/badger/v4 v4.2.0 // indirect @@ -86,21 +100,26 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/dvsekhvalnov/jose2go v1.7.0 // indirect github.com/emicklei/dot v1.6.2 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect github.com/fatih/color v1.18.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/getsentry/sentry-go v0.35.0 // indirect + github.com/getsentry/sentry-go v0.42.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-kit/kit v0.13.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gofrs/flock v0.13.0 // indirect github.com/gogo/googleapis v1.4.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e // indirect + github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -121,50 +140,66 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect + github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/huandu/skiplist v1.2.1 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/lib/pq v1.10.9 // indirect + github.com/lib/pq v1.11.2 // indirect github.com/linxGnu/grocksdb v1.9.8 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/minio/highwayhash v1.0.3 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect github.com/oklog/run v1.1.0 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/rs/zerolog v1.34.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/supranational/blst v0.3.14 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect - github.com/tidwall/btree v1.7.0 // indirect + github.com/tidwall/btree v1.8.1 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/tyler-smith/go-bip39 v1.1.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/zondax/golem v0.27.0 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v1.0.1 // indirect @@ -172,19 +207,19 @@ require ( go.opencensus.io v0.24.0 // indirect go.uber.org/mock v0.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.17.0 // indirect - golang.org/x/crypto v0.47.0 // indirect + golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect - golang.org/x/text v0.33.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/term v0.40.0 // indirect + golang.org/x/text v0.34.0 // indirect google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 3ae35bc..fd4b703 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= -cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -19,10 +19,10 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= -cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= -cloud.google.com/go/auth v0.16.4 h1:fXOAIQmkApVvcIn7Pc2+5J8QTMVbUGLscnSVNl11su8= -cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M= +cloud.google.com/go v0.121.2 h1:v2qQpN6Dx9x2NmwrqlesOt3Ys4ol5/lFZ6Mg1B7OJCg= +cloud.google.com/go v0.121.2/go.mod h1:nRFlrHq39MNVWu+zESP2PosMWA0ryJw8KUBZ2iZpxbw= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -50,20 +50,20 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.50.0 h1:3TbVkzTooBvnZsk7WaAQfOsNrdoM8QHusXA1cpk6QJs= -cloud.google.com/go/storage v1.50.0/go.mod h1:l7XeiD//vx5lfqE3RavfmU9yvk5Pp0Zhcv482poyafY= +cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHUAw= +cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA= cosmossdk.io/api v0.9.2 h1:9i9ptOBdmoIEVEVWLtYYHjxZonlF/aOVODLFaxpmNtg= cosmossdk.io/api v0.9.2/go.mod h1:CWt31nVohvoPMTlPv+mMNCtC0a7BqRdESjCsstHcTkU= cosmossdk.io/client/v2 v2.0.0-beta.11 h1:iHbjDw/NuNz2OVaPmx0iE9eu2HrbX+WAv2u9guRcd6o= cosmossdk.io/client/v2 v2.0.0-beta.11/go.mod h1:ZmmxMUpALO2r1aG6fNOonE7f8I1g/WsafJgVAeQ0ffs= -cosmossdk.io/collections v1.3.1 h1:09e+DUId2brWsNOQ4nrk+bprVmMUaDH9xvtZkeqIjVw= -cosmossdk.io/collections v1.3.1/go.mod h1:ynvkP0r5ruAjbmedE+vQ07MT6OtJ0ZIDKrtJHK7Q/4c= +cosmossdk.io/collections v1.4.0 h1:b373bkxCxKiRbapxZ42TRmcKJEnBVBebdQVk9I5IkkE= +cosmossdk.io/collections v1.4.0/go.mod h1:gxbieVY3tjbvWlkm3yOXf7sGyDrVi12haZH+sek6whw= cosmossdk.io/core v0.11.3 h1:mei+MVDJOwIjIniaKelE3jPDqShCc/F4LkNNHh+4yfo= cosmossdk.io/core v0.11.3/go.mod h1:9rL4RE1uDt5AJ4Tg55sYyHWXA16VmpHgbe0PbJc6N2Y= cosmossdk.io/depinject v1.2.1 h1:eD6FxkIjlVaNZT+dXTQuwQTKZrFZ4UrfCq1RKgzyhMw= cosmossdk.io/depinject v1.2.1/go.mod h1:lqQEycz0H2JXqvOgVwTsjEdMI0plswI7p6KX+MVqFOM= -cosmossdk.io/errors v1.0.2 h1:wcYiJz08HThbWxd/L4jObeLaLySopyyuUFB5w4AGpCo= -cosmossdk.io/errors v1.0.2/go.mod h1:0rjgiHkftRYPj//3DrD6y8hcm40HcPv/dR4R/4efr0k= +cosmossdk.io/errors v1.1.0 h1:X2DSt9JYgH7cuiaDr318aUqIl2z5Lfo/PdGzAtmczUU= +cosmossdk.io/errors v1.1.0/go.mod h1:lnjBmx7etZpMTLnxdspZupH0d9HGRWZhiezDZX2ayyI= cosmossdk.io/log v1.6.1 h1:YXNwAgbDwMEKwDlCdH8vPcoggma48MgZrTQXCfmMBeI= cosmossdk.io/log v1.6.1/go.mod h1:gMwsWyyDBjpdG9u2avCFdysXqxq28WJapJvu+vF1y+E= cosmossdk.io/math v1.5.3 h1:WH6tu6Z3AUCeHbeOSHg2mt9rnoiUWVWaQ2t6Gkll96U= @@ -83,8 +83,8 @@ cosmossdk.io/x/tx v0.14.0/go.mod h1:Tn30rSRA1PRfdGB3Yz55W4Sn6EIutr9xtMKSHij+9PM= cosmossdk.io/x/upgrade v0.2.0 h1:ZHy0xny3wBCSLomyhE06+UmQHWO8cYlVYjfFAJxjz5g= cosmossdk.io/x/upgrade v0.2.0/go.mod h1:DXDtkvi//TrFyHWSOaeCZGBoiGAE6Rs8/0ABt2pcDD0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= +filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= @@ -93,48 +93,51 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/CosmWasm/wasmd v0.61.6 h1:wa1rY/mZi8OYnf0f6a02N7o3vBockOfL3P37hSH0XtY= -github.com/CosmWasm/wasmd v0.61.6/go.mod h1:Wg2gfY2qrjjFY8UvpkTCRdy8t67qebOQn7UvRiGRzDw= -github.com/CosmWasm/wasmvm/v3 v3.0.2 h1:+MLkOX+IdklITLqfG26PCFv5OXdZvNb8z5Wq5JFXTRM= -github.com/CosmWasm/wasmvm/v3 v3.0.2/go.mod h1:oknpb1bFERvvKcY7vHRp1F/Y/z66xVrsl7n9uWkOAlM= +github.com/CosmWasm/wasmd v0.61.10 h1:BB5dhCys4MSvRKUNvW1eFuhm61/251mFJ5Hefvm7MD0= +github.com/CosmWasm/wasmd v0.61.10/go.mod h1:GMPqtsb1T8FvaKpQGJmGuj/JMPXBauk4ZhUErgmtEus= +github.com/CosmWasm/wasmvm/v3 v3.0.3 h1:rg8cZ2qXFj3HwUOqni53OZV/oTvmA2mTVabKA7L0sro= +github.com/CosmWasm/wasmvm/v3 v3.0.3/go.mod h1:oknpb1bFERvvKcY7vHRp1F/Y/z66xVrsl7n9uWkOAlM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v4.8.3+incompatible h1:fNGaYSuObuQb5nzeTQqowRAd9bpDIRRV4/gUtIBjh8Q= github.com/DataDog/datadog-go v4.8.3+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0 h1:5IT7xOdq17MtcdtL/vtl6mGfzhaq4m4vpollPRmlsBQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.50.0/go.mod h1:ZV4VOm0/eHR06JLrXWe09068dHpr3TRpY9Uo7T+anuA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0 h1:ig/FpDD2JofP/NExKQUbn7uOSZzJAQqogfqluZK4ed4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.50.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/LumeraProtocol/lumera v1.12.0-rc h1:Mfae496LpjYhf1SvAE/bsmtjgdoOD8WAJFRCier8xsg= -github.com/LumeraProtocol/lumera v1.12.0-rc/go.mod h1:/G9LTPZB+261tHoWoj7q+1fn+O/VV0zzagwLdsThSNo= +github.com/LumeraProtocol/lumera v1.20.0-rc2 h1:T7lQVigQfb+ywH925qqBILiR3awWTXGAOXWbYZppF4E= +github.com/LumeraProtocol/lumera v1.20.0-rc2/go.mod h1:Ru+SQjwg47CsceToex+izOA8ZMILl0vB0Fxd89umk5E= github.com/LumeraProtocol/rq-go v0.2.1 h1:8B3UzRChLsGMmvZ+UVbJsJj6JZzL9P9iYxbdUwGsQI4= github.com/LumeraProtocol/rq-go v0.2.1/go.mod h1:APnKCZRh1Es2Vtrd2w4kCLgAyaL5Bqrkz/BURoRJ+O8= -github.com/LumeraProtocol/supernode/v2 v2.5.0-rc h1:VSlIV56VfM27Qvd8XpSjPoipm4ZptTeHdBXyj4IerSw= -github.com/LumeraProtocol/supernode/v2 v2.5.0-rc/go.mod h1:ezTqf1uT80Ebb1C9NoDDEspzTnbfWdqoQsmkmDP45V8= github.com/LumeraProtocol/supernode/v2 v2.5.2 h1:3aacuY83E3LFvOa3du+2EiqTphknxxMWhyI/1UKKfZA= github.com/LumeraProtocol/supernode/v2 v2.5.2/go.mod h1:ezTqf1uT80Ebb1C9NoDDEspzTnbfWdqoQsmkmDP45V8= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/adlio/schema v1.3.6 h1:k1/zc2jNfeiZBA5aFTRy37jlBIuCkXCm0XmvpzCKI9I= github.com/adlio/schema v1.3.6/go.mod h1:qkxwLgPBd1FgLRHYVCmQT/rrBr3JH38J9LjmVzWNudg= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -160,22 +163,45 @@ github.com/bgentry/speakeasy v0.2.0 h1:tgObeVOf8WAvtuAX6DhJ4xks4CFNwPDZiqzGqIHE5 github.com/bgentry/speakeasy v0.2.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y= github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd v0.24.2 h1:aLmxPguqxza+4ag8R1I2nnJjSu2iFn/kqtHTIImswcY= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU= github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/bufbuild/protoc-gen-validate v1.3.0 h1:0lq2b9qA1uzfVnMW6oFJepiVVihDOOzj+VuTGSX4EgE= github.com/bufbuild/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= -github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= -github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= -github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= @@ -184,6 +210,7 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -205,8 +232,8 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= -github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= @@ -231,6 +258,8 @@ github.com/cometbft/cometbft v0.38.21 h1:qcIJSH9LiwU5s6ZgKR5eRbsLNucbubfraDs5bzg github.com/cometbft/cometbft v0.38.21/go.mod h1:UCu8dlHqvkAsmAFmWDRWNZJPlu6ya2fTWZlDrWsivwo= github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -243,10 +272,14 @@ github.com/cosmos/cosmos-db v1.1.3 h1:7QNT77+vkefostcKkhrzDK9uoIEryzFrU9eoMeaQOP github.com/cosmos/cosmos-db v1.1.3/go.mod h1:kN+wGsnwUJZYn8Sy5Q2O0vCYA99MJllkKASbs6Unb9U= github.com/cosmos/cosmos-proto v1.0.0-beta.5 h1:eNcayDLpip+zVLRLYafhzLvQlSmyab+RC5W7ZfmxJLA= github.com/cosmos/cosmos-proto v1.0.0-beta.5/go.mod h1:hQGLpiIUloJBMdQMMWb/4wRApmI9hjHH05nefC0Ojec= -github.com/cosmos/cosmos-sdk v0.53.5 h1:JPue+SFn2gyDzTV9TYb8mGpuIH3kGt7WbGadulkpTcU= -github.com/cosmos/cosmos-sdk v0.53.5/go.mod h1:AQJx0jpon70WAD4oOs/y+SlST4u7VIwEPR6F8S7JMdo= +github.com/cosmos/cosmos-sdk v0.53.6 h1:aJeInld7rbsHtH1qLHu2aZJF9t40mGlqp3ylBLDT0HI= +github.com/cosmos/cosmos-sdk v0.53.6/go.mod h1:N6YuprhAabInbT3YGumGDKONbvPX5dNro7RjHvkQoKE= +github.com/cosmos/evm v0.6.0 h1:jwJerLS7btDgDpZOYy7lUC+1rNRCGGE80TJ6r4guufo= +github.com/cosmos/evm v0.6.0/go.mod h1:QnaJDtxqon2mywiYqxM8VwW8FKeFazi0au0qzVpFAG8= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= +github.com/cosmos/go-ethereum v1.16.2-cosmos-1 h1:QIaIS6HIdPSBdTvpFhxswhMLUJgcr4irbd2o9ZKldAI= +github.com/cosmos/go-ethereum v1.16.2-cosmos-1/go.mod h1:X5CIOyo8SuK1Q5GnaEizQVLHT/DfsiGWuNeVdQcEMNA= github.com/cosmos/gogogateway v1.2.0 h1:Ae/OivNhp8DqBi/sh2A8a1D0y638GpL3tkmLQAiKxTE= github.com/cosmos/gogogateway v1.2.0/go.mod h1:iQpLkGWxYcnCdz5iAdLcRBSw3h7NXeOkZ4GUkT+tbFI= github.com/cosmos/gogoproto v1.4.2/go.mod h1:cLxOsn1ljAHSV527CHOtaIP91kK6cCrZETRBrkzItWU= @@ -260,22 +293,32 @@ github.com/cosmos/ibc-go/v10 v10.5.0 h1:NI+cX04fXdu9JfP0V0GYeRi1ENa7PPdq0BYtVYo8 github.com/cosmos/ibc-go/v10 v10.5.0/go.mod h1:a74pAPUSJ7NewvmvELU74hUClJhwnmm5MGbEaiTw/kE= github.com/cosmos/ics23/go v0.11.0 h1:jk5skjT0TqX5e5QJbEnwXIS2yI2vnmLOgpQPeM5RtnU= github.com/cosmos/ics23/go v0.11.0/go.mod h1:A8OjxPE67hHST4Icw94hOxxFEJMBG031xIGF/JHNIY0= -github.com/cosmos/ledger-cosmos-go v0.16.0 h1:YKlWPG9NnGZIEUb2bEfZ6zhON1CHlNTg0QKRRGcNEd0= -github.com/cosmos/ledger-cosmos-go v0.16.0/go.mod h1:WrM2xEa8koYoH2DgeIuZXNarF7FGuZl3mrIOnp3Dp0o= +github.com/cosmos/ledger-cosmos-go v1.0.0 h1:jNKW89nPf0vR0EkjHG8Zz16h6p3zqwYEOxlHArwgYtw= +github.com/cosmos/ledger-cosmos-go v1.0.0/go.mod h1:mGaw2wDOf+Z6SfRJsMGxU9DIrBa4du0MAiPlpPhLAOE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI= +github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/desertbit/timer v1.0.1 h1:yRpYNn5Vaaj6QXecdLMPMJsW81JLiI1eokUft5nBmeo= github.com/desertbit/timer v1.0.1/go.mod h1:htRrYeY5V/t4iu1xCJ5XsQvp4xve8QulXXctAzxqcwE= @@ -313,18 +356,22 @@ github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5y github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= -github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= -github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= -github.com/ethereum/go-ethereum v1.15.11 h1:JK73WKeu0WC0O1eyX+mdQAVHUV+UR1a9VB/domDngBU= -github.com/ethereum/go-ethereum v1.15.11/go.mod h1:mf8YiHIb0GR4x4TipcvBUPxJLw1mFdmxzoDi11sDRoI= +github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w= +github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= @@ -336,8 +383,8 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/getsentry/sentry-go v0.35.0 h1:+FJNlnjJsZMG3g0/rmmP7GiKjQoUF5EXfEtBwtPtkzY= -github.com/getsentry/sentry-go v0.35.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE= +github.com/getsentry/sentry-go v0.42.0 h1:eeFMACuZTbUQf90RE8dE4tXeSe4CZyfvR1MBL7RLEt8= +github.com/getsentry/sentry-go v0.42.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -348,8 +395,8 @@ github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -367,6 +414,9 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= @@ -377,8 +427,8 @@ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GO github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -388,6 +438,8 @@ github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/E github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.4.1-0.20201022092350-68b0159b7869/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= @@ -435,8 +487,8 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e h1:4bw4WeyTYPp0smaXiJZCNnLrvVBqirQVreixayXezGc= -github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -502,6 +554,7 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -547,8 +600,8 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= -github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -564,6 +617,8 @@ github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -572,6 +627,8 @@ github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0Jr github.com/huandu/skiplist v1.2.1 h1:dTi93MgjwErA/8idWTzIw4Y1kZsMWx35fmI2c8Rij7w= github.com/huandu/skiplist v1.2.1/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= @@ -584,6 +641,10 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -593,6 +654,7 @@ github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -610,10 +672,12 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -629,10 +693,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/linxGnu/grocksdb v1.9.8 h1:vOIKv9/+HKiqJAElJIEYv3ZLcihRxyP7Suu/Mu8Dxjs= @@ -657,10 +723,15 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -670,6 +741,8 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -704,20 +777,24 @@ github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU= -github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= +github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -749,6 +826,16 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -787,8 +874,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -796,11 +883,16 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= +github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -824,13 +916,15 @@ github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6v github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shamaton/msgpack/v2 v2.2.3 h1:uDOHmxQySlvlUYfQwdjxyybAOzjlQsD1Vjy+4jmO9NM= github.com/shamaton/msgpack/v2 v2.2.3/go.mod h1:6khjYnkx73f7VQU7wjcFS9DFjs+59naVWJv1TB7qdOI= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -844,8 +938,8 @@ github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8 github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -880,16 +974,35 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= +github.com/supranational/blst v0.3.14/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= -github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= -github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA= +github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= @@ -904,6 +1017,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zondax/golem v0.27.0 h1:IbBjGIXF3SoGOZHsILJvIM/F/ylwJzMcHAcggiqniPw= github.com/zondax/golem v0.27.0/go.mod h1:AmorCgJPt00L8xN1VrMBe13PSifoZksnQ1Ge906bu4A= github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= @@ -926,22 +1041,22 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= -go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -963,12 +1078,13 @@ go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -986,8 +1102,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= -golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1029,6 +1145,7 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1064,6 +1181,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -1084,8 +1202,8 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1095,8 +1213,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1113,8 +1231,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1134,6 +1252,7 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1155,10 +1274,12 @@ golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1185,16 +1306,18 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1204,8 +1327,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1221,8 +1344,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= -golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1296,8 +1419,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -1372,10 +1495,10 @@ google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM= +google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -1402,8 +1525,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= -google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= diff --git a/internal/grpc/codec.go b/internal/grpc/codec.go new file mode 100644 index 0000000..ffc018f --- /dev/null +++ b/internal/grpc/codec.go @@ -0,0 +1,54 @@ +package grpc + +import ( + "context" + "fmt" + + "github.com/cosmos/gogoproto/proto" + googlegrpc "google.golang.org/grpc" +) + +type gogoCodec struct{} +type gogoClientConn struct { + conn googlegrpc.ClientConnInterface +} + +// GogoCodec returns a gRPC codec for gogoproto-generated Cosmos SDK messages. +func GogoCodec() gogoCodec { + return gogoCodec{} +} + +func (gogoCodec) Name() string { + return "proto" +} + +func (gogoCodec) Marshal(v interface{}) ([]byte, error) { + msg, ok := v.(proto.Message) + if !ok { + return nil, fmt.Errorf("gogo codec expects proto.Message, got %T", v) + } + return proto.Marshal(msg) +} + +func (gogoCodec) Unmarshal(data []byte, v interface{}) error { + msg, ok := v.(proto.Message) + if !ok { + return fmt.Errorf("gogo codec expects proto.Message, got %T", v) + } + return proto.Unmarshal(data, msg) +} + +// GogoClientConn forces the gogo codec for gogoproto-generated query clients. +func GogoClientConn(conn googlegrpc.ClientConnInterface) googlegrpc.ClientConnInterface { + return gogoClientConn{conn: conn} +} + +func (c gogoClientConn) Invoke(ctx context.Context, method string, args interface{}, reply interface{}, opts ...googlegrpc.CallOption) error { + opts = append([]googlegrpc.CallOption{googlegrpc.ForceCodec(GogoCodec())}, opts...) + return c.conn.Invoke(ctx, method, args, reply, opts...) +} + +func (c gogoClientConn) NewStream(ctx context.Context, desc *googlegrpc.StreamDesc, method string, opts ...googlegrpc.CallOption) (googlegrpc.ClientStream, error) { + opts = append([]googlegrpc.CallOption{googlegrpc.ForceCodec(GogoCodec())}, opts...) + return c.conn.NewStream(ctx, desc, method, opts...) +} diff --git a/internal/wait-tx/waiter.go b/internal/wait-tx/waiter.go index f178793..e13f941 100644 --- a/internal/wait-tx/waiter.go +++ b/internal/wait-tx/waiter.go @@ -48,6 +48,7 @@ func (w *Waiter) Wait(ctx context.Context, txHash string, timeout time.Duration) defer cancel() } + var subErr error if w.subscriber != nil { // Bound the subscriber lifetime to setupDelay. If the WS handshake // or first event delivery has not landed by then we fall through to @@ -75,7 +76,7 @@ func (w *Waiter) Wait(ctx context.Context, txHash string, timeout time.Duration) select { case <-subCtx.Done(): - case <-errCh: + case subErr = <-errCh: case res := <-resCh: return res, nil } @@ -85,5 +86,12 @@ func (w *Waiter) Wait(ctx context.Context, txHash string, timeout time.Duration) if w.poller == nil { return Result{}, fmt.Errorf("poller is required") } - return w.poller.Wait(ctx, txHash) + res, err := w.poller.Wait(ctx, txHash) + // If the subscriber failed early and the poller also failed, surface both: + // the subscriber error is often the more diagnostic root cause (e.g. WS + // connection refused) and would otherwise be silently discarded. + if err != nil && subErr != nil { + return res, fmt.Errorf("subscriber failed (%v); poller failed: %w", subErr, err) + } + return res, err } diff --git a/internal/wait-tx/waiter_test.go b/internal/wait-tx/waiter_test.go index 479a73f..80cea79 100644 --- a/internal/wait-tx/waiter_test.go +++ b/internal/wait-tx/waiter_test.go @@ -3,6 +3,7 @@ package waittx import ( "context" "errors" + "strings" "testing" "time" @@ -65,6 +66,23 @@ func TestWaiterFallsBackToPoller(t *testing.T) { } } +func TestWaiterCombinesSubscriberAndPollerErrors(t *testing.T) { + sub := &stubSource{err: errors.New("subscriber-boom")} + poller := &stubSource{err: errors.New("poller-boom")} + w := &Waiter{subscriber: sub, poller: poller, setupDelay: 10 * time.Millisecond} + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + _, err := w.Wait(ctx, "hash", 0) + if err == nil { + t.Fatal("expected error when both subscriber and poller fail") + } + if msg := err.Error(); !strings.Contains(msg, "subscriber-boom") || !strings.Contains(msg, "poller-boom") { + t.Fatalf("error %q should mention both the subscriber and poller failures", msg) + } +} + type waiterStubQuerier struct { resp *txtypes.GetTxResponse err error diff --git a/pkg/crypto/address.go b/pkg/crypto/address.go index 61d9c95..bc4cbfd 100644 --- a/pkg/crypto/address.go +++ b/pkg/crypto/address.go @@ -1,37 +1,70 @@ -package crypto - -import ( - "fmt" - - "github.com/cosmos/cosmos-sdk/crypto/keyring" - sdkbech32 "github.com/cosmos/cosmos-sdk/types/bech32" -) - -// AddressFromKey derives an account address for the given HRP from the -// public key stored in the keyring under keyName, without mutating the -// global bech32 prefix configuration. -func AddressFromKey(kr keyring.Keyring, keyName, hrp string) (string, error) { - if kr == nil { - return "", fmt.Errorf("keyring is required") - } - if keyName == "" { - return "", fmt.Errorf("key name is required") - } - rec, err := kr.Key(keyName) - if err != nil { - return "", fmt.Errorf("key %s not found: %w", keyName, err) - } - pub, err := rec.GetPubKey() - if err != nil { - return "", fmt.Errorf("get pubkey: %w", err) - } - if pub == nil { - return "", fmt.Errorf("nil pubkey for key %s", keyName) - } - addrBz := pub.Address() - bech, err := sdkbech32.ConvertAndEncode(hrp, addrBz) - if err != nil { - return "", fmt.Errorf("bech32 encode: %w", err) - } - return bech, nil -} +package crypto + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + sdkbech32 "github.com/cosmos/cosmos-sdk/types/bech32" + "github.com/cosmos/evm/crypto/ethsecp256k1" + "github.com/ethereum/go-ethereum/common" +) + +// AddressFromKey derives an account address for the given HRP from the +// public key stored in the keyring under keyName, without mutating the +// global bech32 prefix configuration. +func AddressFromKey(kr keyring.Keyring, keyName, hrp string) (string, error) { + if kr == nil { + return "", fmt.Errorf("keyring is required") + } + if keyName == "" { + return "", fmt.Errorf("key name is required") + } + rec, err := kr.Key(keyName) + if err != nil { + return "", fmt.Errorf("key %s not found: %w", keyName, err) + } + pub, err := rec.GetPubKey() + if err != nil { + return "", fmt.Errorf("get pubkey: %w", err) + } + if pub == nil { + return "", fmt.Errorf("nil pubkey for key %s", keyName) + } + addrBz := pub.Address() + if len(addrBz) == 0 { + return "", fmt.Errorf("pubkey for key %s produced an empty address", keyName) + } + bech, err := sdkbech32.ConvertAndEncode(hrp, addrBz) + if err != nil { + return "", fmt.Errorf("bech32 encode: %w", err) + } + return bech, nil +} + +// EVMAddressFromKey derives the EIP-55 0x-prefixed hex address for the key +// stored in the keyring under keyName. The key must use the eth_secp256k1 +// signing algorithm; for cosmos secp256k1 keys an error is returned. +func EVMAddressFromKey(kr keyring.Keyring, keyName string) (string, error) { + if kr == nil { + return "", fmt.Errorf("keyring is required") + } + if keyName == "" { + return "", fmt.Errorf("key name is required") + } + rec, err := kr.Key(keyName) + if err != nil { + return "", fmt.Errorf("key %s not found: %w", keyName, err) + } + pub, err := rec.GetPubKey() + if err != nil { + return "", fmt.Errorf("get pubkey: %w", err) + } + if pub == nil { + return "", fmt.Errorf("nil pubkey for key %s", keyName) + } + if pub.Type() != ethsecp256k1.KeyType { + return "", fmt.Errorf("key %s has algorithm %s; EVM address requires %s", + keyName, pub.Type(), ethsecp256k1.KeyType) + } + return common.BytesToAddress(pub.Address()).Hex(), nil +} diff --git a/pkg/crypto/crypto_test.go b/pkg/crypto/crypto_test.go index 276664a..e0048e7 100644 --- a/pkg/crypto/crypto_test.go +++ b/pkg/crypto/crypto_test.go @@ -8,11 +8,14 @@ import ( "testing" "github.com/LumeraProtocol/sdk-go/constants" - sdkethsecp256k1 "github.com/LumeraProtocol/sdk-go/pkg/crypto/ethsecp256k1" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" "github.com/cosmos/cosmos-sdk/crypto/hd" "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/evm/crypto/ethsecp256k1" "github.com/cosmos/go-bip39" + "github.com/cosmos/gogoproto/proto" "github.com/stretchr/testify/require" ) @@ -25,6 +28,18 @@ var testMnemonic = func() string { return mnemonic }() +// secondTestMnemonic is a valid mnemonic distinct from testMnemonic, used to +// verify ImportKey rejects re-importing a different seed under an existing name. +var secondTestMnemonic = func() string { + entropy := make([]byte, 32) + entropy[31] = 1 + mnemonic, err := bip39.NewMnemonic(entropy) + if err != nil { + panic(err) + } + return mnemonic +}() + // --------------------------------------------------------------------------- // KeyType tests // --------------------------------------------------------------------------- @@ -41,7 +56,7 @@ func TestKeyType_HDPath(t *testing.T) { func TestKeyType_SigningAlgo(t *testing.T) { require.Equal(t, hd.Secp256k1.Name(), KeyTypeCosmos.SigningAlgo().Name()) - require.Equal(t, hd.PubKeyType(sdkethsecp256k1.KeyType), KeyTypeEVM.SigningAlgo().Name()) + require.Equal(t, hd.PubKeyType(ethsecp256k1.KeyType), KeyTypeEVM.SigningAlgo().Name()) } // --------------------------------------------------------------------------- @@ -85,7 +100,7 @@ func TestNewKeyring_SupportsBothAlgos(t *testing.T) { require.NoError(t, err) pk2, err := rec2.GetPubKey() require.NoError(t, err) - require.Equal(t, sdkethsecp256k1.KeyType, pk2.Type()) + require.Equal(t, ethsecp256k1.KeyType, pk2.Type()) } // --------------------------------------------------------------------------- @@ -140,7 +155,7 @@ func TestLoadKeyring_EVM(t *testing.T) { require.NoError(t, err) pk, err := rec.GetPubKey() require.NoError(t, err) - require.Equal(t, sdkethsecp256k1.KeyType, pk.Type()) + require.Equal(t, ethsecp256k1.KeyType, pk.Type()) } func TestLoadKeyring_DifferentKeyTypesDerivesDifferentAddress(t *testing.T) { @@ -184,7 +199,7 @@ func TestImportKey_EVM(t *testing.T) { require.NoError(t, err) pk, err := rec.GetPubKey() require.NoError(t, err) - require.Equal(t, sdkethsecp256k1.KeyType, pk.Type()) + require.Equal(t, ethsecp256k1.KeyType, pk.Type()) } // TestImportKey_MultiChain imports both Cosmos and EVM keys into a single @@ -219,7 +234,7 @@ func TestImportKey_MultiChain(t *testing.T) { require.NoError(t, err) evmPK, err := evmRec.GetPubKey() require.NoError(t, err) - require.Equal(t, sdkethsecp256k1.KeyType, evmPK.Type()) + require.Equal(t, ethsecp256k1.KeyType, evmPK.Type()) } // --------------------------------------------------------------------------- @@ -243,7 +258,7 @@ func TestKeyBehaviorMatrix(t *testing.T) { { name: "evm", keyType: KeyTypeEVM, - expectedAlg: sdkethsecp256k1.KeyType, + expectedAlg: ethsecp256k1.KeyType, }, } @@ -356,6 +371,32 @@ func TestImportKey_KeyTypeMismatch(t *testing.T) { require.Contains(t, err.Error(), "already exists with algorithm") } +func TestImportKey_RejectsMnemonicMismatch(t *testing.T) { + for _, keyType := range []KeyType{KeyTypeCosmos, KeyTypeEVM} { + t.Run(keyType.String(), func(t *testing.T) { + kr := newTestKeyring(t) + + // Import alice from the canonical test mnemonic. + file1 := writeMnemonicFileWith(t, testMnemonic) + _, addr1, err := ImportKey(kr, "alice", file1, constants.LumeraAccountHRP, keyType) + require.NoError(t, err) + + // Re-importing the SAME mnemonic stays idempotent. + _, addr1b, err := ImportKey(kr, "alice", file1, constants.LumeraAccountHRP, keyType) + require.NoError(t, err) + require.Equal(t, addr1, addr1b) + + // Re-importing a DIFFERENT mnemonic under the same name must error, + // not silently return the already-stored key's address. + file2 := writeMnemonicFileWith(t, secondTestMnemonic) + _, addr2, err := ImportKey(kr, "alice", file2, constants.LumeraAccountHRP, keyType) + require.Error(t, err, "expected error when re-importing a different mnemonic") + require.Contains(t, err.Error(), "different mnemonic") + require.Empty(t, addr2) + }) + } +} + func TestAddressFromKey_Errors(t *testing.T) { kr := newTestKeyring(t) @@ -490,6 +531,25 @@ func TestNewDefaultTxConfig(t *testing.T) { require.NotNil(t, builder) } +func TestInterfaceRegistry_UnpacksLegacyInjectiveEthSecp256k1PubKey(t *testing.T) { + pub := ðsecp256k1.PubKey{Key: []byte{ + 0x02, 0x5c, 0x9b, 0x07, 0x4e, 0x20, 0x75, 0xf2, 0x2f, 0xb4, 0x6e, + 0x60, 0x6d, 0x9a, 0x57, 0xa1, 0x55, 0x49, 0xfa, 0xa2, 0xac, 0xad, + 0x5b, 0x90, 0x31, 0xb6, 0x49, 0x3f, 0x06, 0x6d, 0x9b, 0x0b, 0x3f, + }} + value, err := proto.Marshal(pub) + require.NoError(t, err) + + any := &codectypes.Any{ + TypeUrl: "/injective.crypto.v1beta1.ethsecp256k1.PubKey", + Value: value, + } + var decoded cryptotypes.PubKey + require.NoError(t, newInterfaceRegistry().UnpackAny(any, &decoded)) + require.Equal(t, ethsecp256k1.KeyType, decoded.Type()) + require.Equal(t, pub.Bytes(), decoded.Bytes()) +} + func TestSignTxWithKeyring(t *testing.T) { kr := newTestKeyring(t) _, err := kr.NewAccount("alice", testMnemonic, "", sdk.FullFundraiserPath, hd.Secp256k1) @@ -536,8 +596,13 @@ func newTestKeyring(t *testing.T) keyring.Keyring { } func writeMnemonicFile(t *testing.T) string { + t.Helper() + return writeMnemonicFileWith(t, testMnemonic) +} + +func writeMnemonicFileWith(t *testing.T, mnemonic string) string { t.Helper() path := filepath.Join(t.TempDir(), "mnemonic.txt") - require.NoError(t, os.WriteFile(path, []byte(testMnemonic), 0o600)) + require.NoError(t, os.WriteFile(path, []byte(mnemonic), 0o600)) return path } diff --git a/pkg/crypto/ethaddr.go b/pkg/crypto/ethaddr.go new file mode 100644 index 0000000..a1663b8 --- /dev/null +++ b/pkg/crypto/ethaddr.go @@ -0,0 +1,87 @@ +package crypto + +import ( + "fmt" + "math/big" + + sdkmath "cosmossdk.io/math" + sdkbech32 "github.com/cosmos/cosmos-sdk/types/bech32" + "github.com/ethereum/go-ethereum/common" +) + +// uLumeToALumeFactor is the scaling factor between ulume (6 decimals) and +// alume (18 decimals), defined by x/precisebank as 10^12. +const uLumeToALumeFactor = 1_000_000_000_000 + +// uLumeToALumeBig is the multiplier used by Wei / ULume / ULumeDecToWei. +var uLumeToALumeBig = new(big.Int).SetUint64(uLumeToALumeFactor) + +// EVMToBech32 converts a 20-byte EVM address to the bech32 form used by the +// Cosmos bank/auth modules for the same account. +func EVMToBech32(addr common.Address, hrp string) (string, error) { + if hrp == "" { + return "", fmt.Errorf("hrp is required") + } + bech, err := sdkbech32.ConvertAndEncode(hrp, addr.Bytes()) + if err != nil { + return "", fmt.Errorf("bech32 encode: %w", err) + } + return bech, nil +} + +// Bech32ToEVM decodes a bech32 account address and reinterprets those 20 bytes +// as an EVM address. This is only a byte-level conversion: for legacy Cosmos +// secp256k1 accounts, the result is not the key's Ethereum address. Callers +// that care must validate against a keyring entry, on-chain account metadata, +// or an evmigration record. +func Bech32ToEVM(bech32Addr string) (common.Address, error) { + if bech32Addr == "" { + return common.Address{}, fmt.Errorf("bech32 address is required") + } + _, raw, err := sdkbech32.DecodeAndConvert(bech32Addr) + if err != nil { + return common.Address{}, fmt.Errorf("bech32 decode: %w", err) + } + if len(raw) != common.AddressLength { + return common.Address{}, fmt.Errorf("bech32 address has %d bytes, want %d", len(raw), common.AddressLength) + } + return common.BytesToAddress(raw), nil +} + +// Wei converts an integer ulume amount to its 18-decimal alume (wei-like) form +// using the 10^12 conversion factor defined by x/precisebank. +func Wei(ulume sdkmath.Int) *big.Int { + if ulume.IsNil() { + return new(big.Int) + } + return new(big.Int).Mul(ulume.BigInt(), uLumeToALumeBig) +} + +// ULumeDecToWei converts a decimal ulume value (such as feemarket BaseFee or +// MinGasPrice in ulume/gas) into the integer alume/gas representation used by +// EIP-1559 fee fields. It errors if the conversion would lose precision +// (i.e. the value has more than 12 decimal places). +func ULumeDecToWei(ulume sdkmath.LegacyDec) (*big.Int, error) { + if ulume.IsNil() { + return nil, fmt.Errorf("nil decimal value") + } + if ulume.IsNegative() { + return nil, fmt.Errorf("negative value %s", ulume) + } + scaled := ulume.MulInt64(uLumeToALumeFactor) + if !scaled.TruncateDec().Equal(scaled) { + return nil, fmt.Errorf("value %s has sub-alume precision", ulume) + } + return scaled.TruncateInt().BigInt(), nil +} + +// ULume converts an alume amount to ulume, truncating any sub-ulume fractional +// component. The dropped fractional remainder is returned so callers can +// surface precision loss. +func ULume(alume *big.Int) (ulume sdkmath.Int, fractional *big.Int) { + if alume == nil { + return sdkmath.ZeroInt(), new(big.Int) + } + q, r := new(big.Int).QuoRem(alume, uLumeToALumeBig, new(big.Int)) + return sdkmath.NewIntFromBigInt(q), r +} diff --git a/pkg/crypto/ethaddr_test.go b/pkg/crypto/ethaddr_test.go new file mode 100644 index 0000000..5693642 --- /dev/null +++ b/pkg/crypto/ethaddr_test.go @@ -0,0 +1,112 @@ +package crypto + +import ( + "math/big" + "strings" + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/LumeraProtocol/sdk-go/constants" + sdkbech32 "github.com/cosmos/cosmos-sdk/types/bech32" + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestEVMToBech32_RoundTrip(t *testing.T) { + original := common.HexToAddress("0xAbC0123456789abcdef0123456789aBcDef012345") + bech, err := EVMToBech32(original, constants.LumeraAccountHRP) + require.NoError(t, err) + require.True(t, strings.HasPrefix(bech, constants.LumeraAccountHRP+"1"), "bech32: %s", bech) + + decoded, err := Bech32ToEVM(bech) + require.NoError(t, err) + require.Equal(t, original, decoded) +} + +func TestEVMToBech32_RequiresHRP(t *testing.T) { + _, err := EVMToBech32(common.Address{}, "") + require.Error(t, err) +} + +func TestBech32ToEVM_RejectsEmpty(t *testing.T) { + _, err := Bech32ToEVM("") + require.Error(t, err) +} + +func TestBech32ToEVM_RejectsMalformed(t *testing.T) { + _, err := Bech32ToEVM("not-a-bech32-string") + require.Error(t, err) +} + +func TestBech32ToEVM_RejectsWrongLength(t *testing.T) { + // Encode a 32-byte payload (e.g. a consensus pubkey), which is not a + // valid EVM account address. + thirtyTwo := make([]byte, 32) + for i := range thirtyTwo { + thirtyTwo[i] = byte(i + 1) + } + bech, err := sdkbech32.ConvertAndEncode(constants.LumeraAccountHRP, thirtyTwo) + require.NoError(t, err) + + _, err = Bech32ToEVM(bech) + require.Error(t, err) + require.Contains(t, err.Error(), "want 20") +} + +func TestWeiAndULume_RoundTrip(t *testing.T) { + ulume := sdkmath.NewInt(123_456_789) + wei := Wei(ulume) + require.Equal(t, new(big.Int).Mul(big.NewInt(123_456_789), big.NewInt(1_000_000_000_000)), wei) + + back, fractional := ULume(wei) + require.True(t, ulume.Equal(back), "got %s want %s", back, ulume) + require.Equal(t, int64(0), fractional.Int64()) +} + +func TestWei_NilInt(t *testing.T) { + require.Equal(t, big.NewInt(0), Wei(sdkmath.Int{})) +} + +func TestULume_FractionalRemainder(t *testing.T) { + // 1 ulume + 1 = 10^12 + 1 alume. + alume := new(big.Int).Add(big.NewInt(1_000_000_000_000), big.NewInt(1)) + ulume, frac := ULume(alume) + require.True(t, ulume.Equal(sdkmath.NewInt(1))) + require.Equal(t, int64(1), frac.Int64()) +} + +func TestULume_NilInput(t *testing.T) { + ulume, frac := ULume(nil) + require.True(t, ulume.IsZero()) + require.Equal(t, int64(0), frac.Int64()) +} + +func TestULumeDecToWei(t *testing.T) { + // 0.0025 ulume/gas = 0.0025 * 10^12 alume/gas = 2,500,000,000 alume/gas + dec := sdkmath.LegacyMustNewDecFromStr("0.0025") + got, err := ULumeDecToWei(dec) + require.NoError(t, err) + require.Equal(t, big.NewInt(2_500_000_000), got) + + // MinGasPrice 0.0005 -> 500,000,000. + got2, err := ULumeDecToWei(sdkmath.LegacyMustNewDecFromStr("0.0005")) + require.NoError(t, err) + require.Equal(t, big.NewInt(500_000_000), got2) +} + +func TestULumeDecToWei_Negative(t *testing.T) { + _, err := ULumeDecToWei(sdkmath.LegacyMustNewDecFromStr("-1")) + require.Error(t, err) +} + +func TestULumeDecToWei_SubAlumePrecision(t *testing.T) { + // 18 decimal places exceeds the 12-decimal conversion precision. + dec := sdkmath.LegacyNewDecWithPrec(1, 18) + _, err := ULumeDecToWei(dec) + require.Error(t, err) +} + +func TestULumeDecToWei_Nil(t *testing.T) { + _, err := ULumeDecToWei(sdkmath.LegacyDec{}) + require.Error(t, err) +} diff --git a/pkg/crypto/ethsecp256k1/ethsecp256k1.go b/pkg/crypto/ethsecp256k1/ethsecp256k1.go deleted file mode 100644 index 52701c8..0000000 --- a/pkg/crypto/ethsecp256k1/ethsecp256k1.go +++ /dev/null @@ -1,141 +0,0 @@ -// sdk-go/pkg/crypto/ethsecp256k1/ethsecp256k1.go -package ethsecp256k1 - -import ( - "bytes" - "crypto/sha256" - "crypto/subtle" - "fmt" - - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" - "github.com/ethereum/go-ethereum/crypto" -) - -const ( - PrivKeySize = 32 - PubKeySize = 33 // compressed - KeyType = "eth_secp256k1" - - // Proto type URLs - must match Injective's registration - PubKeyName = "injective.crypto.v1beta1.ethsecp256k1.PubKey" - PrivKeyName = "injective.crypto.v1beta1.ethsecp256k1.PrivKey" -) - -var ( - _ cryptotypes.PrivKey = &PrivKey{} - _ cryptotypes.PubKey = &PubKey{} -) - -type PrivKey struct { - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` -} - -func (privKey *PrivKey) Bytes() []byte { - if privKey == nil { - return nil - } - return privKey.Key -} - -func (privKey *PrivKey) PubKey() cryptotypes.PubKey { - ecdsaPrivKey, err := crypto.ToECDSA(privKey.Key) - if err != nil { - return nil - } - return &PubKey{Key: crypto.CompressPubkey(&ecdsaPrivKey.PublicKey)} -} - -func (privKey *PrivKey) Equals(other cryptotypes.LedgerPrivKey) bool { - return privKey.Type() == other.Type() && subtle.ConstantTimeCompare(privKey.Bytes(), other.Bytes()) == 1 -} - -func (privKey *PrivKey) Type() string { - return KeyType -} - -func (privKey *PrivKey) Sign(digestBz []byte) ([]byte, error) { - if len(digestBz) != crypto.DigestLength { - // Use SHA256 like standard Cosmos, NOT Keccak256 - hash := sha256.Sum256(digestBz) - digestBz = hash[:] - } - key, err := crypto.ToECDSA(privKey.Key) - if err != nil { - return nil, err - } - - sig, err := crypto.Sign(digestBz, key) - if err != nil { - return nil, err - } - - // Remove recovery ID (65 โ†’ 64 bytes) to match Cosmos format - if len(sig) == 65 { - sig = sig[:64] - } - - return sig, nil -} - -// Proto methods -func (privKey *PrivKey) Reset() { *privKey = PrivKey{} } -func (privKey *PrivKey) String() string { return fmt.Sprintf("eth_secp256k1{%X}", privKey.Key) } -func (privKey *PrivKey) ProtoMessage() {} - -// XXX_MessageName returns the proto message name for proper type URL registration -func (*PrivKey) XXX_MessageName() string { return PrivKeyName } - -// PubKey defines a secp256k1 public key using Ethereum's format -type PubKey struct { - Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` -} - -func (pubKey *PubKey) Address() cryptotypes.Address { - if len(pubKey.Key) != PubKeySize { - return nil - } - uncompressed, err := crypto.DecompressPubkey(pubKey.Key) - if err != nil { - return nil - } - return crypto.PubkeyToAddress(*uncompressed).Bytes() -} - -func (pubKey *PubKey) Bytes() []byte { - if pubKey == nil { - return nil - } - return pubKey.Key -} - -func (pubKey *PubKey) Equals(other cryptotypes.PubKey) bool { - return pubKey.Type() == other.Type() && bytes.Equal(pubKey.Bytes(), other.Bytes()) -} - -func (pubKey *PubKey) Type() string { - return KeyType -} - -func (pubKey *PubKey) VerifySignature(msg, sig []byte) bool { - if len(sig) == crypto.SignatureLength { - sig = sig[:crypto.SignatureLength-1] // remove recovery ID - } - // Use SHA256 like standard Cosmos, NOT Keccak256 - hash := sha256.Sum256(msg) - return crypto.VerifySignature(pubKey.Key, hash[:], sig) -} - -// Proto methods -func (pubKey *PubKey) Reset() { *pubKey = PubKey{} } -func (pubKey *PubKey) String() string { return fmt.Sprintf("eth_secp256k1{%X}", pubKey.Key) } -func (pubKey *PubKey) ProtoMessage() {} - -// XXX_MessageName returns the proto message name for proper type URL registration -func (*PubKey) XXX_MessageName() string { return PubKeyName } - -// RegisterInterfaces registers the ethsecp256k1 types with Injective's type URLs -func RegisterInterfaces(registry codectypes.InterfaceRegistry) { - registry.RegisterImplementations((*cryptotypes.PubKey)(nil), &PubKey{}) - registry.RegisterImplementations((*cryptotypes.PrivKey)(nil), &PrivKey{}) -} diff --git a/pkg/crypto/ethsign.go b/pkg/crypto/ethsign.go new file mode 100644 index 0000000..fd7490c --- /dev/null +++ b/pkg/crypto/ethsign.go @@ -0,0 +1,118 @@ +package crypto + +import ( + "fmt" + "math/big" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/cosmos/evm/crypto/ethsecp256k1" + evmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" +) + +// SignEthereumTx signs a go-ethereum *types.Transaction with the named keyring +// entry (which must be an eth_secp256k1 key) using the latest replay-protected +// signer for chainID. Returns the signed transaction. +func SignEthereumTx( + kr keyring.Keyring, + keyName string, + chainID *big.Int, + tx *ethtypes.Transaction, +) (*ethtypes.Transaction, error) { + if kr == nil { + return nil, fmt.Errorf("keyring is required") + } + if keyName == "" { + return nil, fmt.Errorf("key name is required") + } + if chainID == nil || chainID.Sign() <= 0 { + return nil, fmt.Errorf("chainID must be positive") + } + if tx == nil { + return nil, fmt.Errorf("tx is required") + } + + rec, err := kr.Key(keyName) + if err != nil { + return nil, fmt.Errorf("load key %q: %w", keyName, err) + } + pub, err := rec.GetPubKey() + if err != nil { + return nil, fmt.Errorf("get pubkey: %w", err) + } + if pub == nil || pub.Type() != ethsecp256k1.KeyType { + return nil, fmt.Errorf("key %q has algorithm %q; EVM signing requires %q", + keyName, typeOf(pub), ethsecp256k1.KeyType) + } + + signer := ethtypes.LatestSignerForChainID(chainID) + hash := signer.Hash(tx).Bytes() + + sig, signPub, err := kr.Sign(keyName, hash, signingtypes.SignMode_SIGN_MODE_DIRECT) + if err != nil { + return nil, fmt.Errorf("keyring sign: %w", err) + } + if signPub != nil && !signPub.Equals(pub) { + return nil, fmt.Errorf("signer pubkey mismatch for key %q", keyName) + } + if len(sig) != 65 { + return nil, fmt.Errorf("expected 65-byte recoverable signature, got %d", len(sig)) + } + + signed, err := tx.WithSignature(signer, sig) + if err != nil { + return nil, fmt.Errorf("apply signature: %w", err) + } + recovered, err := RecoverSender(signed) + if err != nil { + return nil, fmt.Errorf("recover signed sender: %w", err) + } + want := common.BytesToAddress(pub.Address()) + if recovered != want { + return nil, fmt.Errorf("recovered sender %s does not match key %q address %s", recovered.Hex(), keyName, want.Hex()) + } + return signed, nil +} + +// WrapAsMsgEthereumTx packages a signed go-ethereum tx as a cosmos +// MsgEthereumTx ready for MsgEthereumTx.BuildTxWithEvmParams and broadcast. +func WrapAsMsgEthereumTx(signed *ethtypes.Transaction) (*evmtypes.MsgEthereumTx, error) { + if signed == nil { + return nil, fmt.Errorf("signed tx is required") + } + chainID := signed.ChainId() + if chainID == nil || chainID.Sign() <= 0 { + return nil, fmt.Errorf("signed tx has no chain id; refusing to wrap a non-EIP-155 tx") + } + + msg := &evmtypes.MsgEthereumTx{} + signer := ethtypes.LatestSignerForChainID(chainID) + if err := msg.FromSignedEthereumTx(signed, signer); err != nil { + return nil, fmt.Errorf("populate MsgEthereumTx: %w", err) + } + return msg, nil +} + +// RecoverSender returns the EVM address recovered from the signature of a +// signed go-ethereum tx. Useful for verifying that the keyring key produced +// the expected sender. +func RecoverSender(signed *ethtypes.Transaction) (common.Address, error) { + if signed == nil { + return common.Address{}, fmt.Errorf("signed tx is required") + } + chainID := signed.ChainId() + if chainID == nil || chainID.Sign() <= 0 { + return common.Address{}, fmt.Errorf("signed tx has no chain id") + } + signer := ethtypes.LatestSignerForChainID(chainID) + return signer.Sender(signed) +} + +func typeOf(pub interface{ Type() string }) string { + if pub == nil { + return "" + } + return pub.Type() +} diff --git a/pkg/crypto/ethsign_test.go b/pkg/crypto/ethsign_test.go new file mode 100644 index 0000000..5a20756 --- /dev/null +++ b/pkg/crypto/ethsign_test.go @@ -0,0 +1,174 @@ +package crypto + +import ( + "math/big" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + signingtypes "github.com/cosmos/cosmos-sdk/types/tx/signing" + "github.com/ethereum/go-ethereum/common" + ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +const evmSignTestMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" +const evmSignMismatchMnemonic = "legal winner thank year wave sausage worth useful legal winner thank yellow" + +func writeMnemonic(t *testing.T) string { + t.Helper() + dir := t.TempDir() + path := filepath.Join(dir, "mnemonic.txt") + require.NoError(t, os.WriteFile(path, []byte(evmSignTestMnemonic), 0o600)) + return path +} + +func TestSignEthereumTx_RoundTrip(t *testing.T) { + mnemonicFile := writeMnemonic(t) + kr, _, _, err := LoadKeyring("alice", mnemonicFile, KeyTypeEVM) + require.NoError(t, err) + + from, err := EVMAddressFromKey(kr, "alice") + require.NoError(t, err) + + chainID := big.NewInt(1414) + to := common.HexToAddress("0x000000000000000000000000000000000000dEaD") + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: 0, + GasTipCap: big.NewInt(1_000_000_000), + GasFeeCap: big.NewInt(2_000_000_000), + Gas: 21_000, + To: &to, + Value: big.NewInt(0), + Data: nil, + }) + + signed, err := SignEthereumTx(kr, "alice", chainID, tx) + require.NoError(t, err) + require.NotNil(t, signed) + require.Equal(t, chainID, signed.ChainId()) + + recovered, err := RecoverSender(signed) + require.NoError(t, err) + require.Equal(t, strings.ToLower(from), strings.ToLower(recovered.Hex())) +} + +type mismatchedSignerKeyring struct { + keyring.Keyring + reported *keyring.Record + signer keyring.Keyring + signAs string +} + +func (k mismatchedSignerKeyring) Key(string) (*keyring.Record, error) { + return k.reported, nil +} + +func (k mismatchedSignerKeyring) Sign(_ string, msg []byte, mode signingtypes.SignMode) ([]byte, cryptotypes.PubKey, error) { + sig, _, err := k.signer.Sign(k.signAs, msg, mode) + return sig, nil, err +} + +func TestSignEthereumTx_RejectsRecoveredSenderMismatch(t *testing.T) { + mnemonicFile := writeMnemonic(t) + alice, _, _, err := LoadKeyring("alice", mnemonicFile, KeyTypeEVM) + require.NoError(t, err) + aliceRec, err := alice.Key("alice") + require.NoError(t, err) + + bob, err := NewKeyring(KeyringParams{ + AppName: "lumera", + Backend: "test", + Dir: t.TempDir(), + Input: strings.NewReader(""), + }) + require.NoError(t, err) + _, err = bob.NewAccount("bob", evmSignMismatchMnemonic, "", KeyTypeEVM.HDPath(), KeyTypeEVM.SigningAlgo()) + require.NoError(t, err) + + chainID := big.NewInt(1414) + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + GasTipCap: big.NewInt(1), + GasFeeCap: big.NewInt(2), + Gas: 21_000, + }) + + _, err = SignEthereumTx(mismatchedSignerKeyring{ + reported: aliceRec, + signer: bob, + signAs: "bob", + }, "alice", chainID, tx) + require.Error(t, err) + require.Contains(t, err.Error(), "recovered sender") +} + +func TestSignEthereumTx_RejectsCosmosKey(t *testing.T) { + mnemonicFile := writeMnemonic(t) + kr, _, _, err := LoadKeyring("bob", mnemonicFile, KeyTypeCosmos) + require.NoError(t, err) + + chainID := big.NewInt(1414) + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ChainID: chainID, Gas: 21_000}) + + _, err = SignEthereumTx(kr, "bob", chainID, tx) + require.Error(t, err) + require.Contains(t, err.Error(), "EVM signing requires") +} + +func TestSignEthereumTx_RequiresPositiveChainID(t *testing.T) { + mnemonicFile := writeMnemonic(t) + kr, _, _, err := LoadKeyring("alice", mnemonicFile, KeyTypeEVM) + require.NoError(t, err) + + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{}) + _, err = SignEthereumTx(kr, "alice", nil, tx) + require.Error(t, err) + + _, err = SignEthereumTx(kr, "alice", big.NewInt(0), tx) + require.Error(t, err) +} + +func TestWrapAsMsgEthereumTx(t *testing.T) { + mnemonicFile := writeMnemonic(t) + kr, _, _, err := LoadKeyring("alice", mnemonicFile, KeyTypeEVM) + require.NoError(t, err) + + from, err := EVMAddressFromKey(kr, "alice") + require.NoError(t, err) + + chainID := big.NewInt(1414) + to := common.HexToAddress("0x0000000000000000000000000000000000000123") + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ + ChainID: chainID, + Nonce: 7, + GasTipCap: big.NewInt(500_000_000), + GasFeeCap: big.NewInt(2_500_000_000), + Gas: 50_000, + To: &to, + Value: big.NewInt(42), + }) + signed, err := SignEthereumTx(kr, "alice", chainID, tx) + require.NoError(t, err) + + msg, err := WrapAsMsgEthereumTx(signed) + require.NoError(t, err) + require.NotNil(t, msg) + + sender := msg.GetSender() + require.Equal(t, strings.ToLower(from), strings.ToLower(sender.Hex())) + + asTx := msg.AsTransaction() + require.Equal(t, uint64(7), asTx.Nonce()) + require.Equal(t, big.NewInt(42), asTx.Value()) +} + +func TestWrapAsMsgEthereumTx_RejectsUnsigned(t *testing.T) { + tx := ethtypes.NewTx(ðtypes.DynamicFeeTx{ChainID: big.NewInt(1414)}) + _, err := WrapAsMsgEthereumTx(tx) + require.Error(t, err) +} diff --git a/pkg/crypto/keyring.go b/pkg/crypto/keyring.go index f9df828..44f90fb 100644 --- a/pkg/crypto/keyring.go +++ b/pkg/crypto/keyring.go @@ -2,6 +2,7 @@ package crypto import ( "bufio" + "bytes" "fmt" "io" "os" @@ -9,7 +10,6 @@ import ( "strings" "github.com/LumeraProtocol/sdk-go/constants" - sdkethsecp256k1 "github.com/LumeraProtocol/sdk-go/pkg/crypto/ethsecp256k1" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" codectypes "github.com/cosmos/cosmos-sdk/codec/types" @@ -20,8 +20,23 @@ import ( "github.com/cosmos/cosmos-sdk/std" sdk "github.com/cosmos/cosmos-sdk/types" authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + authztypes "github.com/cosmos/cosmos-sdk/x/authz" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + distributiontypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + evmcryptocodec "github.com/cosmos/evm/crypto/codec" + "github.com/cosmos/evm/crypto/ethsecp256k1" + evmhd "github.com/cosmos/evm/crypto/hd" + erc20types "github.com/cosmos/evm/x/erc20/types" + feemarkettypes "github.com/cosmos/evm/x/feemarket/types" + precisebanktypes "github.com/cosmos/evm/x/precisebank/types" + vmtypes "github.com/cosmos/evm/x/vm/types" + "github.com/cosmos/gogoproto/proto" actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" + claimtypes "github.com/LumeraProtocol/lumera/x/claim/types" + evmigrationtypes "github.com/LumeraProtocol/lumera/x/evmigration/types" + supernodetypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types" ) const ( @@ -65,16 +80,12 @@ func (kt KeyType) HDPath() string { func (kt KeyType) SigningAlgo() keyring.SignatureAlgo { switch kt { case KeyTypeEVM: - return ethSecp256k1Alg + return evmhd.EthSecp256k1 default: return hd.Secp256k1 } } -var ( - ethSecp256k1Alg = ethSecp256k1Algo{} -) - // KeyringParams holds configuration for initializing a Cosmos keyring. type KeyringParams struct { // AppName names the keyring namespace. Default: "lumera" @@ -123,12 +134,9 @@ func NewKeyring(p KeyringParams) (keyring.Keyring, error) { in = bufio.NewReader(os.Stdin) } - registry := codectypes.NewInterfaceRegistry() - std.RegisterInterfaces(registry) - sdkethsecp256k1.RegisterInterfaces(registry) - cdc := codec.NewProtoCodec(registry) + cdc := codec.NewProtoCodec(newInterfaceRegistry()) - return keyring.New(app, backend, dir, in, cdc, ethSecp256k1Option()) + return keyring.New(app, backend, dir, in, cdc, evmhd.EthSecp256k1Option()) } // GetKey returns metadata for the named key in the provided keyring. @@ -231,6 +239,17 @@ func ImportKey(kr keyring.Keyring, keyName, mnemonicFile, hrp string, keyType Ke return nil, "", fmt.Errorf("key %q already exists with algorithm %s, but %s (%s) was requested", keyName, pub.Type(), keyType.String(), wantAlgo) } + // Verify the existing key was derived from the supplied mnemonic. + // Without this check, importing a different mnemonic under an existing + // name would silently return the stored key's pubkey/address, leaving + // the caller operating on the wrong account. + derivedPub, err := pubKeyFromMnemonic(mnemonic, keyType) + if err != nil { + return nil, "", fmt.Errorf("derive pubkey from mnemonic: %w", err) + } + if !bytes.Equal(derivedPub.Bytes(), pub.Bytes()) { + return nil, "", fmt.Errorf("key %q already exists with a different mnemonic", keyName) + } } addr, err := AddressFromKey(kr, keyName, hrp) @@ -254,14 +273,63 @@ func ImportKey(kr keyring.Keyring, keyName, mnemonicFile, hrp string, keyType Ke // NewDefaultTxConfig constructs a client.TxConfig backed by a protobuf codec, // registering Lumera action message interfaces as required for signing/encoding. func NewDefaultTxConfig() client.TxConfig { + proto := codec.NewProtoCodec(newInterfaceRegistry()) + return authtx.NewTxConfig(proto, authtx.DefaultSignModes) +} + +func newInterfaceRegistry() codectypes.InterfaceRegistry { reg := codectypes.NewInterfaceRegistry() // Register crypto and module interfaces + std.RegisterInterfaces(reg) cryptocodec.RegisterInterfaces(reg) - sdkethsecp256k1.RegisterInterfaces(reg) + evmcryptocodec.RegisterInterfaces(reg) + registerLegacyEthSecp256k1TypeURLs(reg) + authztypes.RegisterInterfaces(reg) actiontypes.RegisterInterfaces(reg) + banktypes.RegisterInterfaces(reg) + claimtypes.RegisterInterfaces(reg) + distributiontypes.RegisterInterfaces(reg) + evmigrationtypes.RegisterInterfaces(reg) + erc20types.RegisterInterfaces(reg) + feemarkettypes.RegisterInterfaces(reg) + precisebanktypes.RegisterInterfaces(reg) + vmtypes.RegisterInterfaces(reg) + stakingtypes.RegisterInterfaces(reg) + supernodetypes.RegisterInterfaces(reg) + + return reg +} - proto := codec.NewProtoCodec(reg) - return authtx.NewTxConfig(proto, authtx.DefaultSignModes) +func registerLegacyEthSecp256k1TypeURLs(reg codectypes.InterfaceRegistry) { + custom, ok := reg.(interface { + RegisterCustomTypeURL(any, string, proto.Message) + }) + if !ok { + panic("interface registry does not support custom type URLs") + } + custom.RegisterCustomTypeURL( + (*cryptotypes.PubKey)(nil), + "/injective.crypto.v1beta1.ethsecp256k1.PubKey", + ðsecp256k1.PubKey{}, + ) + custom.RegisterCustomTypeURL( + (*cryptotypes.PrivKey)(nil), + "/injective.crypto.v1beta1.ethsecp256k1.PrivKey", + ðsecp256k1.PrivKey{}, + ) +} + +// pubKeyFromMnemonic derives the public key for the given mnemonic and key +// type without persisting anything to a keyring. It uses the same signing +// algorithm and HD path that ImportKey/LoadKeyring use, so the result matches +// a key created via kr.NewAccount with the same inputs. +func pubKeyFromMnemonic(mnemonic string, keyType KeyType) (cryptotypes.PubKey, error) { + algo := keyType.SigningAlgo() + derivedPriv, err := algo.Derive()(mnemonic, "", keyType.HDPath()) + if err != nil { + return nil, fmt.Errorf("derive private key: %w", err) + } + return algo.Generate()(derivedPriv).PubKey(), nil } func readMnemonicFile(mnemonicFile string) (string, error) { @@ -275,29 +343,3 @@ func readMnemonicFile(mnemonicFile string) (string, error) { } return mnemonic, nil } - -func ethSecp256k1Option() keyring.Option { - return func(options *keyring.Options) { - options.SupportedAlgos = keyring.SigningAlgoList{ethSecp256k1Alg, hd.Secp256k1} - options.SupportedAlgosLedger = keyring.SigningAlgoList{ethSecp256k1Alg, hd.Secp256k1} - } -} - -type ethSecp256k1Algo struct{} - -func (s ethSecp256k1Algo) Name() hd.PubKeyType { - return hd.PubKeyType(sdkethsecp256k1.KeyType) -} - -func (s ethSecp256k1Algo) Derive() hd.DeriveFn { - // Reuse Cosmos derivation function with Ethereum BIP44 path. - return hd.Secp256k1.Derive() -} - -func (s ethSecp256k1Algo) Generate() hd.GenerateFn { - return func(bz []byte) cryptotypes.PrivKey { - bzArr := make([]byte, sdkethsecp256k1.PrivKeySize) - copy(bzArr, bz) - return &sdkethsecp256k1.PrivKey{Key: bzArr} - } -} diff --git a/pkg/evm/precompiles/abi/action.json b/pkg/evm/precompiles/abi/action.json new file mode 100644 index 0000000..d52a36a --- /dev/null +++ b/pkg/evm/precompiles/abi/action.json @@ -0,0 +1,706 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "IAction", + "sourceName": "solidity/precompiles/action/IAction.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "actionId", + "type": "string" + }, + { + "indexed": true, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "actionType", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "ActionRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "actionId", + "type": "string" + }, + { + "indexed": true, + "internalType": "address", + "name": "superNode", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "newState", + "type": "uint8" + } + ], + "name": "ActionFinalized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "actionId", + "type": "string" + }, + { + "indexed": true, + "internalType": "address", + "name": "creator", + "type": "address" + } + ], + "name": "ActionApproved", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "dataHash", + "type": "string" + }, + { + "internalType": "string", + "name": "fileName", + "type": "string" + }, + { + "internalType": "uint64", + "name": "rqIdsIc", + "type": "uint64" + }, + { + "internalType": "string", + "name": "signatures", + "type": "string" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int64", + "name": "expirationTime", + "type": "int64" + }, + { + "internalType": "uint64", + "name": "fileSizeKbs", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "string", + "name": "commitmentType", + "type": "string" + }, + { + "internalType": "uint8", + "name": "hashAlgo", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "chunkSize", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "totalSize", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "numChunks", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "root", + "type": "bytes" + }, + { + "internalType": "uint32[]", + "name": "challengeIndices", + "type": "uint32[]" + } + ], + "internalType": "struct IAction.AvailabilityCommitment", + "name": "commitment", + "type": "tuple" + } + ], + "name": "requestCascade", + "outputs": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "dataHash", + "type": "string" + }, + { + "internalType": "uint64", + "name": "ddAndFingerprintsIc", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int64", + "name": "expirationTime", + "type": "int64" + }, + { + "internalType": "uint64", + "name": "fileSizeKbs", + "type": "uint64" + } + ], + "name": "requestSense", + "outputs": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + }, + { + "internalType": "string[]", + "name": "rqIdsIds", + "type": "string[]" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "chunkIndex", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "leafHash", + "type": "bytes" + }, + { + "internalType": "bytes[]", + "name": "pathHashes", + "type": "bytes[]" + }, + { + "internalType": "bool[]", + "name": "pathDirections", + "type": "bool[]" + } + ], + "internalType": "struct IAction.ChunkProof[]", + "name": "chunkProofs", + "type": "tuple[]" + } + ], + "name": "finalizeCascade", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + }, + { + "internalType": "string[]", + "name": "ddAndFingerprintsIds", + "type": "string[]" + }, + { + "internalType": "string", + "name": "signatures", + "type": "string" + } + ], + "name": "finalizeSense", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + } + ], + "name": "approveAction", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + } + ], + "name": "getAction", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + }, + { + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "internalType": "uint8", + "name": "actionType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "state", + "type": "uint8" + }, + { + "internalType": "string", + "name": "metadata", + "type": "string" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int64", + "name": "expirationTime", + "type": "int64" + }, + { + "internalType": "int64", + "name": "blockHeight", + "type": "int64" + }, + { + "internalType": "address[]", + "name": "superNodes", + "type": "address[]" + } + ], + "internalType": "struct IAction.ActionInfo", + "name": "action", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "dataSizeKbs", + "type": "uint64" + } + ], + "name": "getActionFee", + "outputs": [ + { + "internalType": "uint256", + "name": "baseFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "perKbFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalFee", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "internalType": "uint64", + "name": "offset", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "limit", + "type": "uint64" + } + ], + "name": "getActionsByCreator", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + }, + { + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "internalType": "uint8", + "name": "actionType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "state", + "type": "uint8" + }, + { + "internalType": "string", + "name": "metadata", + "type": "string" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int64", + "name": "expirationTime", + "type": "int64" + }, + { + "internalType": "int64", + "name": "blockHeight", + "type": "int64" + }, + { + "internalType": "address[]", + "name": "superNodes", + "type": "address[]" + } + ], + "internalType": "struct IAction.ActionInfo[]", + "name": "actions", + "type": "tuple[]" + }, + { + "internalType": "uint64", + "name": "total", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "state", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "offset", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "limit", + "type": "uint64" + } + ], + "name": "getActionsByState", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + }, + { + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "internalType": "uint8", + "name": "actionType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "state", + "type": "uint8" + }, + { + "internalType": "string", + "name": "metadata", + "type": "string" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int64", + "name": "expirationTime", + "type": "int64" + }, + { + "internalType": "int64", + "name": "blockHeight", + "type": "int64" + }, + { + "internalType": "address[]", + "name": "superNodes", + "type": "address[]" + } + ], + "internalType": "struct IAction.ActionInfo[]", + "name": "actions", + "type": "tuple[]" + }, + { + "internalType": "uint64", + "name": "total", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "superNode", + "type": "address" + }, + { + "internalType": "uint64", + "name": "offset", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "limit", + "type": "uint64" + } + ], + "name": "getActionsBySuperNode", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "actionId", + "type": "string" + }, + { + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "internalType": "uint8", + "name": "actionType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "state", + "type": "uint8" + }, + { + "internalType": "string", + "name": "metadata", + "type": "string" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int64", + "name": "expirationTime", + "type": "int64" + }, + { + "internalType": "int64", + "name": "blockHeight", + "type": "int64" + }, + { + "internalType": "address[]", + "name": "superNodes", + "type": "address[]" + } + ], + "internalType": "struct IAction.ActionInfo[]", + "name": "actions", + "type": "tuple[]" + }, + { + "internalType": "uint64", + "name": "total", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getParams", + "outputs": [ + { + "internalType": "uint256", + "name": "baseActionFee", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feePerKbyte", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "maxActionsPerBlock", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "minSuperNodes", + "type": "uint64" + }, + { + "internalType": "int64", + "name": "expirationDuration", + "type": "int64" + }, + { + "internalType": "string", + "name": "superNodeFeeShare", + "type": "string" + }, + { + "internalType": "string", + "name": "foundationFeeShare", + "type": "string" + }, + { + "internalType": "uint32", + "name": "svcChallengeCount", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "svcMinChunksForChallenge", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/pkg/evm/precompiles/abi/supernode.json b/pkg/evm/precompiles/abi/supernode.json new file mode 100644 index 0000000..7356b3e --- /dev/null +++ b/pkg/evm/precompiles/abi/supernode.json @@ -0,0 +1,725 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "ISupernode", + "sourceName": "solidity/precompiles/supernode/ISupernode.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "indexed": true, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "newState", + "type": "uint8" + } + ], + "name": "SupernodeRegistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "indexed": true, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "oldState", + "type": "uint8" + } + ], + "name": "SupernodeDeregistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "indexed": true, + "internalType": "address", + "name": "creator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "newState", + "type": "uint8" + } + ], + "name": "SupernodeStateChanged", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "ipAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "supernodeAccount", + "type": "string" + }, + { + "internalType": "string", + "name": "p2pPort", + "type": "string" + } + ], + "name": "registerSupernode", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + } + ], + "name": "deregisterSupernode", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + } + ], + "name": "startSupernode", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "reason", + "type": "string" + } + ], + "name": "stopSupernode", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "ipAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "note", + "type": "string" + }, + { + "internalType": "string", + "name": "supernodeAccount", + "type": "string" + }, + { + "internalType": "string", + "name": "p2pPort", + "type": "string" + } + ], + "name": "updateSupernode", + "outputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "supernodeAccount", + "type": "string" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "versionMajor", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "versionMinor", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "versionPatch", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "cpuCoresTotal", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "cpuUsagePercent", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "memTotalGb", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "memUsagePercent", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "memFreeGb", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "diskTotalGb", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "diskUsagePercent", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "diskFreeGb", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "uptimeSeconds", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "peersCount", + "type": "uint32" + } + ], + "internalType": "struct ISupernode.MetricsReport", + "name": "metrics", + "type": "tuple" + } + ], + "name": "reportMetrics", + "outputs": [ + { + "internalType": "bool", + "name": "compliant", + "type": "bool" + }, + { + "internalType": "string[]", + "name": "issues", + "type": "string[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + } + ], + "name": "getSuperNode", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "supernodeAccount", + "type": "string" + }, + { + "internalType": "uint8", + "name": "currentState", + "type": "uint8" + }, + { + "internalType": "int64", + "name": "stateHeight", + "type": "int64" + }, + { + "internalType": "string", + "name": "ipAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "p2pPort", + "type": "string" + }, + { + "internalType": "string", + "name": "note", + "type": "string" + }, + { + "internalType": "uint64", + "name": "evidenceCount", + "type": "uint64" + } + ], + "internalType": "struct ISupernode.SuperNodeInfo", + "name": "info", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "supernodeAddress", + "type": "string" + } + ], + "name": "getSuperNodeByAccount", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "supernodeAccount", + "type": "string" + }, + { + "internalType": "uint8", + "name": "currentState", + "type": "uint8" + }, + { + "internalType": "int64", + "name": "stateHeight", + "type": "int64" + }, + { + "internalType": "string", + "name": "ipAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "p2pPort", + "type": "string" + }, + { + "internalType": "string", + "name": "note", + "type": "string" + }, + { + "internalType": "uint64", + "name": "evidenceCount", + "type": "uint64" + } + ], + "internalType": "struct ISupernode.SuperNodeInfo", + "name": "info", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "offset", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "limit", + "type": "uint64" + } + ], + "name": "listSuperNodes", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "supernodeAccount", + "type": "string" + }, + { + "internalType": "uint8", + "name": "currentState", + "type": "uint8" + }, + { + "internalType": "int64", + "name": "stateHeight", + "type": "int64" + }, + { + "internalType": "string", + "name": "ipAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "p2pPort", + "type": "string" + }, + { + "internalType": "string", + "name": "note", + "type": "string" + }, + { + "internalType": "uint64", + "name": "evidenceCount", + "type": "uint64" + } + ], + "internalType": "struct ISupernode.SuperNodeInfo[]", + "name": "nodes", + "type": "tuple[]" + }, + { + "internalType": "uint64", + "name": "total", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int32", + "name": "blockHeight", + "type": "int32" + }, + { + "internalType": "int32", + "name": "limit", + "type": "int32" + }, + { + "internalType": "uint8", + "name": "state", + "type": "uint8" + } + ], + "name": "getTopSuperNodesForBlock", + "outputs": [ + { + "components": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "supernodeAccount", + "type": "string" + }, + { + "internalType": "uint8", + "name": "currentState", + "type": "uint8" + }, + { + "internalType": "int64", + "name": "stateHeight", + "type": "int64" + }, + { + "internalType": "string", + "name": "ipAddress", + "type": "string" + }, + { + "internalType": "string", + "name": "p2pPort", + "type": "string" + }, + { + "internalType": "string", + "name": "note", + "type": "string" + }, + { + "internalType": "uint64", + "name": "evidenceCount", + "type": "uint64" + } + ], + "internalType": "struct ISupernode.SuperNodeInfo[]", + "name": "nodes", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "validatorAddress", + "type": "string" + } + ], + "name": "getMetrics", + "outputs": [ + { + "components": [ + { + "internalType": "uint32", + "name": "versionMajor", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "versionMinor", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "versionPatch", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "cpuCoresTotal", + "type": "uint32" + }, + { + "internalType": "uint64", + "name": "cpuUsagePercent", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "memTotalGb", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "memUsagePercent", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "memFreeGb", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "diskTotalGb", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "diskUsagePercent", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "diskFreeGb", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "uptimeSeconds", + "type": "uint64" + }, + { + "internalType": "uint32", + "name": "peersCount", + "type": "uint32" + } + ], + "internalType": "struct ISupernode.MetricsReport", + "name": "metrics", + "type": "tuple" + }, + { + "internalType": "uint64", + "name": "reportCount", + "type": "uint64" + }, + { + "internalType": "int64", + "name": "lastReportHeight", + "type": "int64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getParams", + "outputs": [ + { + "internalType": "uint256", + "name": "minimumStake", + "type": "uint256" + }, + { + "internalType": "uint64", + "name": "reportingThreshold", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "slashingThreshold", + "type": "uint64" + }, + { + "internalType": "string", + "name": "minSupernodeVersion", + "type": "string" + }, + { + "internalType": "uint64", + "name": "minCpuCores", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "minMemGb", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "minStorageGb", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/pkg/evm/precompiles/abi/wasm.json b/pkg/evm/precompiles/abi/wasm.json new file mode 100644 index 0000000..92c60d9 --- /dev/null +++ b/pkg/evm/precompiles/abi/wasm.json @@ -0,0 +1,142 @@ +{ + "_format": "hh-sol-artifact-1", + "contractName": "IWasm", + "sourceName": "solidity/precompiles/wasm/IWasm.sol", + "abi": [ + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "contractAddr", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "name": "WasmExecuted", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "contractAddr", + "type": "string" + } + ], + "name": "contractInfo", + "outputs": [ + { + "internalType": "uint64", + "name": "codeId", + "type": "uint64" + }, + { + "internalType": "string", + "name": "creator", + "type": "string" + }, + { + "internalType": "string", + "name": "admin", + "type": "string" + }, + { + "internalType": "string", + "name": "label", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "contractAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "msg", + "type": "bytes" + } + ], + "name": "execute", + "outputs": [ + { + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "contractAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "msg", + "type": "bytes" + } + ], + "name": "query", + "outputs": [ + { + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "contractAddr", + "type": "string" + }, + { + "internalType": "bytes", + "name": "key", + "type": "bytes" + } + ], + "name": "rawQuery", + "outputs": [ + { + "internalType": "bytes", + "name": "value", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {} +} diff --git a/pkg/evm/precompiles/precompiles.go b/pkg/evm/precompiles/precompiles.go new file mode 100644 index 0000000..ba08ad7 --- /dev/null +++ b/pkg/evm/precompiles/precompiles.go @@ -0,0 +1,112 @@ +// Package precompiles exposes Lumera and standard cosmos/evm precompile +// addresses, pre-parsed ABIs, and generic call helpers so SDK consumers can +// invoke precompile methods from Go without compiling Solidity themselves. +// +// Three Lumera-specific precompiles are wrapped: +// +// - Action 0x0901 exposes x/action (Cascade, Sense) +// - Supernode 0x0902 exposes x/supernode lifecycle and metrics +// - Wasm 0x0903 enables bidirectional CosmWasm <-> EVM calls +// +// The eight standard cosmos/evm precompiles are exposed as named address +// constants only; their Solidity-facing surface is documented in the lumera +// repo under docs/evm-integration/precompiles. +package precompiles + +import ( + _ "embed" + "encoding/json" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +// Lumera custom precompile addresses (per precompiles/precompiles.md). +var ( + ActionAddress = common.HexToAddress("0x0000000000000000000000000000000000000901") + SupernodeAddress = common.HexToAddress("0x0000000000000000000000000000000000000902") + WasmAddress = common.HexToAddress("0x0000000000000000000000000000000000000903") +) + +// Standard cosmos/evm precompile addresses (per precompiles/standard-precompiles.md). +var ( + P256Address = common.HexToAddress("0x0000000000000000000000000000000000000100") + Bech32Address = common.HexToAddress("0x0000000000000000000000000000000000000400") + StakingAddress = common.HexToAddress("0x0000000000000000000000000000000000000800") + DistributionAddress = common.HexToAddress("0x0000000000000000000000000000000000000801") + ICS20Address = common.HexToAddress("0x0000000000000000000000000000000000000802") + BankAddress = common.HexToAddress("0x0000000000000000000000000000000000000804") + GovAddress = common.HexToAddress("0x0000000000000000000000000000000000000805") + SlashingAddress = common.HexToAddress("0x0000000000000000000000000000000000000806") +) + +//go:embed abi/action.json +var actionABIJSON []byte + +//go:embed abi/supernode.json +var supernodeABIJSON []byte + +//go:embed abi/wasm.json +var wasmABIJSON []byte + +// Pre-parsed ABIs. Initialized at package load; init() panics if the embedded +// JSON is malformed (which would be a build-time error caught by tests). +var ( + ActionABI abi.ABI + SupernodeABI abi.ABI + WasmABI abi.ABI +) + +func init() { + var err error + if ActionABI, err = parseHardhatABI(actionABIJSON); err != nil { + panic(fmt.Sprintf("parse action precompile ABI: %v", err)) + } + if SupernodeABI, err = parseHardhatABI(supernodeABIJSON); err != nil { + panic(fmt.Sprintf("parse supernode precompile ABI: %v", err)) + } + if WasmABI, err = parseHardhatABI(wasmABIJSON); err != nil { + panic(fmt.Sprintf("parse wasm precompile ABI: %v", err)) + } +} + +// parseHardhatABI extracts the `abi` field from a Hardhat artifact JSON and +// parses it via go-ethereum's abi package. Falls back to direct parse if the +// payload is a bare ABI array. +func parseHardhatABI(raw []byte) (abi.ABI, error) { + var artifact struct { + ABI json.RawMessage `json:"abi"` + } + // A Hardhat artifact has a non-null "abi" field. A bare ABI array + // unmarshals into the struct without populating ABI, so it falls through + // to the raw parse below. Guard against an explicit null so we don't try + // to parse the literal "null" as an ABI. + if err := json.Unmarshal(raw, &artifact); err == nil && + len(artifact.ABI) > 0 && string(artifact.ABI) != "null" { + return abi.JSON(strings.NewReader(string(artifact.ABI))) + } + return abi.JSON(strings.NewReader(string(raw))) +} + +// PackCall produces ABI-encoded calldata for a precompile method. Pass the +// method args in the order the Solidity interface declares them. +func PackCall(parsed abi.ABI, method string, args ...any) ([]byte, error) { + data, err := parsed.Pack(method, args...) + if err != nil { + return nil, fmt.Errorf("pack %s: %w", method, err) + } + return data, nil +} + +// UnpackReturn decodes the raw return bytes of a precompile call into the +// declared output types. Returns the slice of values matching the Solidity +// outputs (single return: out[0]). +func UnpackReturn(parsed abi.ABI, method string, ret []byte) ([]any, error) { + out, err := parsed.Unpack(method, ret) + if err != nil { + return nil, fmt.Errorf("unpack %s: %w", method, err) + } + return out, nil +} diff --git a/pkg/evm/precompiles/precompiles_test.go b/pkg/evm/precompiles/precompiles_test.go new file mode 100644 index 0000000..82826fe --- /dev/null +++ b/pkg/evm/precompiles/precompiles_test.go @@ -0,0 +1,83 @@ +package precompiles + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEmbeddedABIs_Parse(t *testing.T) { + // init() panics on parse failure; reaching here confirms all three + // ABIs decoded. Sanity-check that each contains at least one method. + require.NotEmpty(t, ActionABI.Methods, "action ABI has no methods") + require.NotEmpty(t, SupernodeABI.Methods, "supernode ABI has no methods") + require.NotEmpty(t, WasmABI.Methods, "wasm ABI has no methods") +} + +func TestAddresses(t *testing.T) { + require.Equal(t, "0x0000000000000000000000000000000000000901", ActionAddress.Hex()) + require.Equal(t, "0x0000000000000000000000000000000000000902", SupernodeAddress.Hex()) + require.Equal(t, "0x0000000000000000000000000000000000000903", WasmAddress.Hex()) + + require.Equal(t, "0x0000000000000000000000000000000000000800", StakingAddress.Hex()) + require.Equal(t, "0x0000000000000000000000000000000000000804", BankAddress.Hex()) +} + +func TestActionABI_HasCoreMethods(t *testing.T) { + for _, m := range []string{"approveAction", "getAction", "getActionFee", "getParams"} { + if _, ok := ActionABI.Methods[m]; !ok { + t.Errorf("ActionABI missing method %q", m) + } + } +} + +func TestSupernodeABI_HasCoreMethods(t *testing.T) { + for _, m := range []string{"registerSupernode", "getSuperNode", "listSuperNodes", "getParams"} { + if _, ok := SupernodeABI.Methods[m]; !ok { + t.Errorf("SupernodeABI missing method %q", m) + } + } +} + +func TestWasmABI_HasCoreMethods(t *testing.T) { + for _, m := range []string{"execute", "query", "contractInfo", "rawQuery"} { + if _, ok := WasmABI.Methods[m]; !ok { + t.Errorf("WasmABI missing method %q", m) + } + } +} + +func TestWasmABI_ExecuteIsPhase1NonPayable(t *testing.T) { + execute := WasmABI.Methods["execute"] + require.Equal(t, "nonpayable", execute.StateMutability) + require.False(t, execute.Payable) + require.Len(t, execute.Inputs, 2) + require.Equal(t, "contractAddr", execute.Inputs[0].Name) + require.Equal(t, "string", execute.Inputs[0].Type.String()) + require.Equal(t, "msg", execute.Inputs[1].Name) + require.Equal(t, "bytes", execute.Inputs[1].Type.String()) +} + +func TestSupernodeABI_RegisterUsesStringAddresses(t *testing.T) { + register := SupernodeABI.Methods["registerSupernode"] + require.Len(t, register.Inputs, 4) + for _, input := range register.Inputs { + require.Equal(t, "string", input.Type.String(), "input %s should remain a bech32/string field", input.Name) + } +} + +func TestActionABI_FeeAmountsAreUint256(t *testing.T) { + getActionFee := ActionABI.Methods["getActionFee"] + require.Len(t, getActionFee.Outputs, 3) + for _, output := range getActionFee.Outputs { + require.Equal(t, "uint256", output.Type.String(), "output %s should remain an integer ulume amount", output.Name) + } +} + +func TestPackUnpack_RoundTrip(t *testing.T) { + // getParams takes no args, returns one struct. Just verify Pack does + // not error and produces a 4-byte selector. + data, err := PackCall(ActionABI, "getParams") + require.NoError(t, err) + require.Len(t, data, 4) +}