From 211d32b62638afc1db89ba41dcafa4e023a86f4f Mon Sep 17 00:00:00 2001 From: John Terzis Date: Mon, 9 Sep 2024 13:20:12 -0700 Subject: [PATCH 1/3] add workflow dispatch rule on gh actions (#1020) To prevent these actions from running on every branch in ci. They will be replaced / implemented in https://github.com/river-build/river/pull/1008/files. --- .github/workflows/Bytecode_diff_report.yml | 4 ++++ .github/workflows/Upgrade_network_facets.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/Bytecode_diff_report.yml b/.github/workflows/Bytecode_diff_report.yml index e69de29bb..a2557fe52 100644 --- a/.github/workflows/Bytecode_diff_report.yml +++ b/.github/workflows/Bytecode_diff_report.yml @@ -0,0 +1,4 @@ +name: Bytecode diff report + +on: + workflow_dispatch: diff --git a/.github/workflows/Upgrade_network_facets.yml b/.github/workflows/Upgrade_network_facets.yml index e69de29bb..b8c668eb9 100644 --- a/.github/workflows/Upgrade_network_facets.yml +++ b/.github/workflows/Upgrade_network_facets.yml @@ -0,0 +1,4 @@ +name: Upgrade network facets + +on: + workflow_dispatch: From 9e614060e029ae41b95f95d0d393317cd013b6d9 Mon Sep 17 00:00:00 2001 From: Miguel Gomes Date: Mon, 9 Sep 2024 17:28:04 -0300 Subject: [PATCH 2/3] stream-metadata: fix base url in space metadata endpoint (#1015) Previously: ```json { "name": "My fancy space", "description": "idk ;-;", "image": "https://gamma.river.deliveryspace/0x9e5318618c141f3f9949250bf73fb98a9b22b03e/image" } ``` This PR fix the url --- packages/stream-metadata/src/routes/spaceMetadata.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream-metadata/src/routes/spaceMetadata.ts b/packages/stream-metadata/src/routes/spaceMetadata.ts index 2004e87df..793178ebb 100644 --- a/packages/stream-metadata/src/routes/spaceMetadata.ts +++ b/packages/stream-metadata/src/routes/spaceMetadata.ts @@ -6,7 +6,7 @@ import { config } from '../environment' import { isValidEthereumAddress } from '../validators' import { spaceDapp } from '../contract-utils' -export const spaceMetadataBaseUrl = `${config.streamMetadataBaseUrl}space`.toLowerCase() +export const spaceMetadataBaseUrl = `${config.streamMetadataBaseUrl}/space`.toLowerCase() const paramsSchema = z.object({ spaceAddress: z.string().min(1, 'spaceAddress parameter is required'), From f9daf9957c74bae254aac4633f2af2e0f0dc6453 Mon Sep 17 00:00:00 2001 From: Crystal Lemire Date: Mon, 9 Sep 2024 14:15:44 -0700 Subject: [PATCH 3/3] Update ETH balance go implementation to consider all ether-based chains (#986) This PR also includes configuration for stream nodes that defaults to sane values if not explicitly set. It uses the same design as existing Xchain configuration and should be similarly forward-compatible with the move to on-chain configuration. This PR leaves the repo in an inconsistent state, as the client is still evaluating eth checks on a single chain - however, the eth check is already broken because it does not meet the spec, so I figured this was fine. --- .../entitlements/rule/IRuleEntitlement.sol | 2 +- core/config/blockchain_info.go | 88 ++++-- core/config/config.go | 7 +- core/contracts/types/entitlement_data.go | 2 +- core/contracts/types/entitlement_data_test.go | 4 +- core/contracts/types/entitlement_types.go | 6 +- .../contracts/types/test_util/entitlements.go | 2 +- core/xchain/entitlement/check_operation.go | 80 +++--- core/xchain/entitlement/entitlement_test.go | 256 ++++++++++-------- core/xchain/entitlement/evaluator.go | 20 ++ core/xchain/examples/common.go | 14 +- 11 files changed, 276 insertions(+), 205 deletions(-) diff --git a/contracts/src/spaces/entitlements/rule/IRuleEntitlement.sol b/contracts/src/spaces/entitlements/rule/IRuleEntitlement.sol index 9ad96aa06..73c217d26 100644 --- a/contracts/src/spaces/entitlements/rule/IRuleEntitlement.sol +++ b/contracts/src/spaces/entitlements/rule/IRuleEntitlement.sol @@ -51,7 +51,7 @@ interface IRuleEntitlementBase { ERC721, ERC1155, ISENTITLED, - NATIVE_COIN_BALANCE + ETH_BALANCE } // Enum for Operation oneof operation_clause diff --git a/core/config/blockchain_info.go b/core/config/blockchain_info.go index a1628ffcb..b72225dbf 100644 --- a/core/config/blockchain_info.go +++ b/core/config/blockchain_info.go @@ -1,24 +1,50 @@ package config -import "time" +import ( + "context" + "time" + + "github.com/river-build/river/core/node/dlog" +) type BlockchainInfo struct { - ChainId uint64 - Name string - Blocktime time.Duration + ChainId uint64 + Name string + // IsEtherBased is true for chains that use Ether as the currency for fees. + IsEtherBased bool + Blocktime time.Duration +} + +func GetEtherBasedBlockchains( + ctx context.Context, + chains []uint64, + defaultBlockchainInfo map[uint64]BlockchainInfo, +) []uint64 { + log := dlog.FromCtx(ctx) + etherBasedChains := make([]uint64, 0, len(chains)) + for _, chainId := range chains { + if info, ok := defaultBlockchainInfo[chainId]; ok && info.IsEtherBased { + etherBasedChains = append(etherBasedChains, chainId) + } else if !ok { + log.Error("Missing BlockchainInfo for chain", "chainId", chainId) + } + } + return etherBasedChains } func GetDefaultBlockchainInfo() map[uint64]BlockchainInfo { return map[uint64]BlockchainInfo{ 1: { - ChainId: 1, - Name: "Ethereum Mainnet", - Blocktime: 12 * time.Second, + ChainId: 1, + Name: "Ethereum Mainnet", + Blocktime: 12 * time.Second, + IsEtherBased: true, }, 11155111: { - ChainId: 11155111, - Name: "Ethereum Sepolia", - Blocktime: 12 * time.Second, + ChainId: 11155111, + Name: "Ethereum Sepolia", + Blocktime: 12 * time.Second, + IsEtherBased: true, }, 550: { ChainId: 550, @@ -31,14 +57,16 @@ func GetDefaultBlockchainInfo() map[uint64]BlockchainInfo { Blocktime: 2 * time.Second, }, 8453: { - ChainId: 8453, - Name: "Base Mainnet", - Blocktime: 2 * time.Second, + ChainId: 8453, + Name: "Base Mainnet", + Blocktime: 2 * time.Second, + IsEtherBased: true, }, 84532: { - ChainId: 84532, - Name: "Base Sepolia", - Blocktime: 2 * time.Second, + ChainId: 84532, + Name: "Base Sepolia", + Blocktime: 2 * time.Second, + IsEtherBased: true, }, 137: { ChainId: 137, @@ -46,24 +74,28 @@ func GetDefaultBlockchainInfo() map[uint64]BlockchainInfo { Blocktime: 2 * time.Second, }, 42161: { - ChainId: 42161, - Name: "Arbitrum One", - Blocktime: 250 * time.Millisecond, + ChainId: 42161, + Name: "Arbitrum One", + Blocktime: 250 * time.Millisecond, + IsEtherBased: true, }, 10: { - ChainId: 10, - Name: "Optimism Mainnet", - Blocktime: 2 * time.Second, + ChainId: 10, + Name: "Optimism Mainnet", + Blocktime: 2 * time.Second, + IsEtherBased: true, }, 31337: { - ChainId: 31337, - Name: "Anvil Base", - Blocktime: 2 * time.Second, + ChainId: 31337, + Name: "Anvil Base", + Blocktime: 2 * time.Second, + IsEtherBased: true, }, 31338: { - ChainId: 31338, - Name: "Anvil River", - Blocktime: 2 * time.Second, + ChainId: 31338, + Name: "Anvil River", + Blocktime: 2 * time.Second, + IsEtherBased: true, // This is set for ease of testing. }, 100: { ChainId: 100, diff --git a/core/config/config.go b/core/config/config.go index 6db083f4d..9238ea86e 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -468,8 +468,8 @@ func parseBlockchainDurations(str string, result map[uint64]BlockchainInfo) erro } func (c *Config) parseChains() error { - bcDurations := GetDefaultBlockchainInfo() - err := parseBlockchainDurations(c.ChainBlocktimes, bcDurations) + defaultChainInfo := GetDefaultBlockchainInfo() + err := parseBlockchainDurations(c.ChainBlocktimes, defaultChainInfo) if err != nil { return err } @@ -493,7 +493,7 @@ func (c *Config) parseChains() error { return WrapRiverError(Err_BAD_CONFIG, err).Message("Failed to pase chain Id").Tag("chainId", parts[0]) } - info, ok := bcDurations[chainID] + info, ok := defaultChainInfo[chainID] if !ok { return RiverError(Err_BAD_CONFIG, "Chain blocktime not set").Tag("chainId", chainID) } @@ -515,6 +515,7 @@ func (c *Config) parseChains() error { c.XChainBlockchains = append(c.XChainBlockchains, chainID) } } + return nil } diff --git a/core/contracts/types/entitlement_data.go b/core/contracts/types/entitlement_data.go index 368fe86ea..c8641cf10 100644 --- a/core/contracts/types/entitlement_data.go +++ b/core/contracts/types/entitlement_data.go @@ -245,7 +245,7 @@ func ConvertV1RuleDataToV2( fallthrough case ERC721: fallthrough - case NATIVE_COIN_BALANCE: + case ETH_BALANCE: params, err := (&ThresholdParams{ Threshold: checkOp.Threshold, }).AbiEncode() diff --git a/core/contracts/types/entitlement_data_test.go b/core/contracts/types/entitlement_data_test.go index 0e3d83746..85b0e4657 100644 --- a/core/contracts/types/entitlement_data_test.go +++ b/core/contracts/types/entitlement_data_test.go @@ -149,7 +149,7 @@ func TestConvertV1RuleDataToV2(t *testing.T) { }, }, }, - "NativeCoinBalance": { + "EthBalance": { ruleData: test_util.EthBalanceCheck(15, 1500), expected: base.IRuleEntitlementBaseRuleDataV2{ Operations: []base.IRuleEntitlementBaseOperation{ @@ -160,7 +160,7 @@ func TestConvertV1RuleDataToV2(t *testing.T) { }, CheckOperations: []base.IRuleEntitlementBaseCheckOperationV2{ { - OpType: uint8(types.NATIVE_COIN_BALANCE), + OpType: uint8(types.ETH_BALANCE), ChainId: big.NewInt(15), Params: encodeThresholdParams(t, 1500), }, diff --git a/core/contracts/types/entitlement_types.go b/core/contracts/types/entitlement_types.go index 708029bfd..d45334ee5 100644 --- a/core/contracts/types/entitlement_types.go +++ b/core/contracts/types/entitlement_types.go @@ -30,7 +30,7 @@ const ( ERC721 ERC1155 ISENTITLED - NATIVE_COIN_BALANCE + ETH_BALANCE ) func (t CheckOperationType) String() string { @@ -47,8 +47,8 @@ func (t CheckOperationType) String() string { return "ERC1155" case ISENTITLED: return "ISENTITLED" - case NATIVE_COIN_BALANCE: - return "NATIVE_COIN_BALANCE" + case ETH_BALANCE: + return "ETH_BALANCE" default: return "UNKNOWN" } diff --git a/core/contracts/types/test_util/entitlements.go b/core/contracts/types/test_util/entitlements.go index 2d9e3cce5..a137570d2 100644 --- a/core/contracts/types/test_util/entitlements.go +++ b/core/contracts/types/test_util/entitlements.go @@ -110,7 +110,7 @@ func EthBalanceCheck(chainId uint64, threshold uint64) base.IRuleEntitlementBase }, CheckOperations: []base.IRuleEntitlementBaseCheckOperation{ { - OpType: uint8(contract_types.NATIVE_COIN_BALANCE), + OpType: uint8(contract_types.ETH_BALANCE), ChainId: new(big.Int).SetUint64(chainId), ContractAddress: common.Address{}, Threshold: new(big.Int).SetUint64(threshold), diff --git a/core/xchain/entitlement/check_operation.go b/core/xchain/entitlement/check_operation.go index e35a51595..cd9a0cdae 100644 --- a/core/xchain/entitlement/check_operation.go +++ b/core/xchain/entitlement/check_operation.go @@ -48,13 +48,13 @@ func validateCheckOperation(ctx context.Context, op *types.CheckOperation) error // 3. Threshold is positive // 4. Token ID is non-negative log := dlog.FromCtx(ctx).With("function", "validateCheckOperation") - if op.ChainID == nil { + if op.CheckType != types.ETH_BALANCE && op.ChainID == nil { log.Error("Entitlement check: chain ID is nil for operation", "operation", op.CheckType.String()) return fmt.Errorf("validateCheckOperation: chain ID is nil for operation %s", op.CheckType) } zeroAddress := common.Address{} - if op.CheckType != types.NATIVE_COIN_BALANCE && op.ContractAddress == zeroAddress { + if op.CheckType != types.ETH_BALANCE && op.ContractAddress == zeroAddress { log.Error("Entitlement check: contract address is nil for operation", "operation", op.CheckType.String()) return fmt.Errorf( "validateCheckOperation: contract address is nil for operation %s", @@ -62,7 +62,7 @@ func validateCheckOperation(ctx context.Context, op *types.CheckOperation) error ) } - if op.CheckType == types.ERC20 || op.CheckType == types.ERC721 || op.CheckType == types.NATIVE_COIN_BALANCE { + if op.CheckType == types.ERC20 || op.CheckType == types.ERC721 || op.CheckType == types.ETH_BALANCE { params, err := types.DecodeThresholdParams(op.Params) if err != nil { log.Error( @@ -148,8 +148,8 @@ func (e *Evaluator) evaluateCheckOperation( return e.evaluateErc721Operation(ctx, op, linkedWallets) case types.ERC1155: return e.evaluateErc1155Operation(ctx, op, linkedWallets) - case types.NATIVE_COIN_BALANCE: - return e.evaluateNativeCoinBalanceOperation(ctx, op, linkedWallets) + case types.ETH_BALANCE: + return e.evaluateEthBalanceOperation(ctx, op, linkedWallets) case types.CheckNONE: fallthrough case types.MOCK: @@ -232,48 +232,54 @@ func (e *Evaluator) evaluateIsEntitledOperation( return false, nil } -// Check balance in decimals of native token -func (e *Evaluator) evaluateNativeCoinBalanceOperation( +// Check ETH balance, in decimals, across all supported chains that use Ether as the native token for payments. +func (e *Evaluator) evaluateEthBalanceOperation( ctx context.Context, op *types.CheckOperation, linkedWallets []common.Address, ) (bool, error) { - log := dlog.FromCtx(ctx).With("function", "evaluateNativeTokenBalanceOperation") - client, err := e.clients.Get(op.ChainID.Uint64()) - if err != nil { - log.Error("Chain ID not found", "chainID", op.ChainID) - return false, fmt.Errorf("evaluateNativeTokenBalanceOperation: Chain ID %v not found", op.ChainID) - } - params, err := types.DecodeThresholdParams(op.Params) - if err != nil { - log.Error("evaluateNativeCoinBalance: failed to decode threshold params", "error", err) - return false, fmt.Errorf("evaluateNativeCoinBalance: failed to decode threshold params, %w", err) - } + log := dlog.FromCtx(ctx).With("function", "evaluateEthBalanceOperation") + // Accumulator for the total balance across all chains. total := big.NewInt(0) - for _, wallet := range linkedWallets { - // Balance is returned as a representation of the balance according the denomination of the - // native token. The default decimals for most native tokens is 18, and we don't convert - // according to decimals here, but compare the threshold directly with the balance. - balance, err := client.BalanceAt(ctx, wallet, nil) + + for _, chainID := range e.ethChainIds { + log.Info("Evaluating ETH balance on chain", "chainID", chainID, "wallets", linkedWallets) + client, err := e.clients.Get(chainID) if err != nil { - log.Error("Failed to retrieve native token balance", "chain", op.ChainID, "error", err) - return false, err + log.Error("Provider for Chain ID not found", "chainID", chainID) + return false, fmt.Errorf("evaluateEthBalanceOperation: Providerfor chain ID %v not found", chainID) + } + params, err := types.DecodeThresholdParams(op.Params) + if err != nil { + log.Error("Failed to decode threshold params", "error", err) + return false, fmt.Errorf("evaluateEthBalanceOperation: failed to decode threshold params, %w", err) } - total.Add(total, balance) - log.Info("Retrieved native token balance", - "balance", balance.String(), - "total", total.String(), - "threshold", params.Threshold.String(), - "chainID", op.ChainID.String(), - ) + for _, wallet := range linkedWallets { + // Balance is returned as a representation of the balance according the denomination of the + // ETH, which is 18. We do not convert away from decimals here, but compare the threshold + // directly with the decimalized balance. + balance, err := client.BalanceAt(ctx, wallet, nil) + if err != nil { + log.Error("Failed to retrieve ETH balance", "chain", chainID, "error", err) + return false, err + } + total.Add(total, balance) + + log.Info("Accumulated ETH balance for chain", + "balance", balance.String(), + "total", total.String(), + "threshold", params.Threshold.String(), + "chainID", chainID, + ) - // Balance is a *big.Int - // Iteratively check if the total balance of evaluated wallets is greater than or equal to the - // threshold. Note threshold is always positive and total is non-negative. - if total.Cmp(params.Threshold) >= 0 { - return true, nil + // Balance is a *big.Int + // Iteratively check if the total balance of evaluated wallets is greater than or equal to the + // threshold. Note threshold is always positive and total is non-negative. + if total.Cmp(params.Threshold) >= 0 { + return true, nil + } } } return false, nil diff --git a/core/xchain/entitlement/entitlement_test.go b/core/xchain/entitlement/entitlement_test.go index 22eb1caf4..3508e7dc0 100644 --- a/core/xchain/entitlement/entitlement_test.go +++ b/core/xchain/entitlement/entitlement_test.go @@ -159,76 +159,36 @@ var erc1155CheckBaseSepolia_TokenId1_151Tokens = CheckOperation{ Params: encodeErc1155Params(big.NewInt(151), big.NewInt(1)), } -var nativeCoinBalance0_1EthEthereumSepolia = CheckOperation{ +var ethBalance_gt_0_7 = CheckOperation{ OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), - ChainID: examples.EthSepoliaChainId, + CheckType: CheckOperationType(ETH_BALANCE), ContractAddress: common.Address{}, - // .1ETH in Wei - Params: encodeThresholdParams(big.NewInt(100_000_000_000_000_000)), + // .7ETH in Wei + Params: encodeThresholdParams(big.NewInt(700_000_000_000_000_001)), } -var nativeCoinBalance0_2EthEthereumSepolia = CheckOperation{ +var ethBalance0_7 = CheckOperation{ OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), - ChainID: examples.EthSepoliaChainId, + CheckType: CheckOperationType(ETH_BALANCE), ContractAddress: common.Address{}, - // .2ETH in Wei - Params: encodeThresholdParams(big.NewInt(200_000_000_000_000_000)), + // .7ETH in Wei + Params: encodeThresholdParams(big.NewInt(700_000_000_000_000_000)), } -var nativeCoinBalance0_21EthEthereumSepolia = CheckOperation{ +var ethBalance0_5 = CheckOperation{ OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), - ChainID: examples.EthSepoliaChainId, - ContractAddress: common.Address{}, - // .21ETH in Wei - Params: encodeThresholdParams(big.NewInt(210_000_000_000_000_000)), -} - -var nativeCoinBalance0_3EthEthereumSepolia = CheckOperation{ - OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), - ChainID: examples.EthSepoliaChainId, - ContractAddress: common.Address{}, - // .3ETH in Wei - Params: encodeThresholdParams(big.NewInt(300_000_000_000_000_000)), -} - -var nativeCoinBalance0_4EthBaseSepolia = CheckOperation{ - OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), - ChainID: examples.BaseSepoliaChainId, - ContractAddress: common.Address{}, - // .4ETH in Wei - Params: encodeThresholdParams(big.NewInt(400_000_000_000_000_000)), -} - -var nativeCoinBalance0_5EthBaseSepolia = CheckOperation{ - OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), - ChainID: examples.BaseSepoliaChainId, + CheckType: CheckOperationType(ETH_BALANCE), ContractAddress: common.Address{}, // .5ETH in Wei Params: encodeThresholdParams(big.NewInt(500_000_000_000_000_000)), } -var nativeCoinBalance0_52EthBaseSepolia = CheckOperation{ +var ethBalance0_4 = CheckOperation{ OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), - ChainID: examples.BaseSepoliaChainId, + CheckType: CheckOperationType(ETH_BALANCE), ContractAddress: common.Address{}, - // .52ETH in Wei - Params: encodeThresholdParams(big.NewInt(520_000_000_000_000_000)), -} - -var nativeCoinBalance0_6EthBaseSepolia = CheckOperation{ - OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), - ChainID: examples.BaseSepoliaChainId, - ContractAddress: common.Address{}, - // .6ETH in Wei - Params: encodeThresholdParams(big.NewInt(600_000_000_000_000_000)), + // .4ETH in Wei + Params: encodeThresholdParams(big.NewInt(400_000_000_000_000_000)), } var erc20TrueCheckBaseSepolia = CheckOperation{ @@ -325,7 +285,10 @@ var cfg = &config.Config{ BlockTimeMs: 2000, }, }, - XChainBlockchains: []uint64{examples.EthSepoliaChainIdUint64, examples.BaseSepoliaChainIdUint64}, + XChainBlockchains: []uint64{ + examples.EthSepoliaChainIdUint64, + examples.BaseSepoliaChainIdUint64, + }, } var evaluator *Evaluator @@ -494,7 +457,7 @@ func TestCheckOperation(t *testing.T) { func TestCheckOperation_Untimed(t *testing.T) { testCases := map[string]struct { - a Operation + op Operation wallets []common.Address expected bool expectedErr error @@ -785,90 +748,44 @@ func TestCheckOperation_Untimed(t *testing.T) { nil, }, "ETH balance empty wallets": { - &nativeCoinBalance0_2EthEthereumSepolia, + ðBalance0_5, []common.Address{}, false, nil, }, - "ETH balance invalid check (no chainId)": { - &CheckOperation{ - OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), - Params: encodeThresholdParams(big.NewInt(1)), - }, - []common.Address{examples.Sepolia0_2EthWallet}, - false, - fmt.Errorf("validateCheckOperation: chain ID is nil for operation NATIVE_COIN_BALANCE"), - }, "ETH balance invalid check (invalid threshold: 0)": { &CheckOperation{ OpType: CHECK, - CheckType: CheckOperationType(NATIVE_COIN_BALANCE), + CheckType: CheckOperationType(ETH_BALANCE), ChainID: examples.EthSepoliaChainId, Params: encodeThresholdParams(big.NewInt(0)), ContractAddress: common.Address{}, }, - []common.Address{examples.Sepolia0_2EthWallet}, + []common.Address{}, false, - fmt.Errorf("validateCheckOperation: threshold 0 is nonpositive for operation NATIVE_COIN_BALANCE"), + fmt.Errorf("validateCheckOperation: threshold 0 is nonpositive for operation ETH_BALANCE"), }, - "ETH balance eth sepolia": { - &nativeCoinBalance0_2EthEthereumSepolia, - []common.Address{examples.Sepolia0_2EthWallet}, + "ETH balance across chains": { + ðBalance0_5, + []common.Address{examples.EthWallet_0_5Eth}, true, nil, }, - "ETH balance eth sepolia (multiwallet)": { - &nativeCoinBalance0_21EthEthereumSepolia, - []common.Address{examples.Sepolia0_2EthWallet, examples.Sepolia0_015EthWallet}, - true, - nil, - }, - "ETH balance eth sepolia (insufficient balance)": { - &nativeCoinBalance0_1EthEthereumSepolia, - []common.Address{examples.Sepolia0_015EthWallet}, + "Insufficient ETH balance": { + ðBalance0_5, + []common.Address{examples.EthWallet_0_2Eth}, false, nil, }, - "ETH balance eth sepolia (multiwallet, insufficient balance)": { - &nativeCoinBalance0_3EthEthereumSepolia, - []common.Address{examples.Sepolia0_2EthWallet, examples.Sepolia0_015EthWallet}, - false, - nil, - }, - "ETH balance eth sepolia (no eth)": { - &nativeCoinBalance0_1EthEthereumSepolia, - []common.Address{examples.EmptyEthTestAccount}, - false, - nil, - }, - "ETH balance base sepolia": { - &nativeCoinBalance0_5EthBaseSepolia, - []common.Address{examples.BaseSepolia0_5EthWallet}, - true, - nil, - }, - "ETH balance base sepolia (multiwallet)": { - &nativeCoinBalance0_52EthBaseSepolia, - []common.Address{examples.BaseSepolia0_5EthWallet, examples.BaseSepolia0_05EthWallet}, + "ETH balance across chains, multiwallet": { + ðBalance0_7, + []common.Address{examples.EthWallet_0_5Eth, examples.EthWallet_0_2Eth}, true, nil, }, - "ETH balance base sepolia (insufficient balance)": { - &nativeCoinBalance0_4EthBaseSepolia, - []common.Address{examples.BaseSepolia0_05EthWallet}, - false, - nil, - }, - "ETH balance base sepolia (multiwallet, insufficient balance)": { - &nativeCoinBalance0_6EthBaseSepolia, - []common.Address{examples.BaseSepolia0_5EthWallet, examples.BaseSepolia0_05EthWallet}, - false, - nil, - }, - "ETH balance base sepolia (no eth, insufficient balance)": { - &nativeCoinBalance0_4EthBaseSepolia, - []common.Address{examples.EmptyEthTestAccount}, + "ETH balance across chains, multiwallet, insufficient balance": { + ðBalance_gt_0_7, + []common.Address{examples.EthWallet_0_5Eth, examples.EthWallet_0_2Eth}, false, nil, }, @@ -876,14 +793,113 @@ func TestCheckOperation_Untimed(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - result, err := evaluator.evaluateOp(context.Background(), tc.a, tc.wallets) + result, err := evaluator.evaluateOp(context.Background(), tc.op, tc.wallets) if tc.expectedErr == nil { require.NoError(t, err) } else { require.EqualError(t, err, tc.expectedErr.Error()) } if result != tc.expected { - t.Errorf("evaluateCheckOperation result (%v) = %v; want %v", tc.a, result, tc.expected) + t.Errorf("evaluateCheckOperation result (%v) = %v; want %v", tc.op, result, tc.expected) + } + }) + } +} + +var singleEtherChainBlockChainInfo = map[uint64]config.BlockchainInfo{ + examples.EthSepoliaChainId.Uint64(): { + ChainId: examples.EthSepoliaChainId.Uint64(), + Name: "Ethereum Seplia", + Blocktime: 12000, + IsEtherBased: true, + }, + examples.BaseSepoliaChainId.Uint64(): { + ChainId: examples.BaseSepoliaChainId.Uint64(), + Name: "Base Sepolia", + Blocktime: 2000, + IsEtherBased: false, // for the sake of testing + }, +} + +func Test_evaluateEthBalance_withConfig(t *testing.T) { + tests := map[string]struct { + cfg config.Config + op Operation + wallets []common.Address + expected bool + expectedErr error + }{ + "Ether chains < supported chains (positive result)": { + cfg: config.Config{ + ChainConfigs: map[uint64]*config.ChainConfig{ + examples.EthSepoliaChainIdUint64: { + NetworkUrl: "https://ethereum-sepolia-rpc.publicnode.com", + ChainId: examples.EthSepoliaChainIdUint64, + BlockTimeMs: 12000, + }, + examples.BaseSepoliaChainIdUint64: { + NetworkUrl: "https://sepolia.base.org", + ChainId: examples.BaseSepoliaChainIdUint64, + BlockTimeMs: 2000, + }, + }, + XChainBlockchains: []uint64{ + examples.EthSepoliaChainIdUint64, + examples.BaseSepoliaChainIdUint64, + }, + }, + op: ðBalance0_4, + wallets: []common.Address{ + examples.EthWallet_0_5Eth, + }, + expected: true, + }, + "Ether chains < supported chains (negative result)": { + cfg: config.Config{ + ChainConfigs: map[uint64]*config.ChainConfig{ + examples.EthSepoliaChainIdUint64: { + NetworkUrl: "https://ethereum-sepolia-rpc.publicnode.com", + ChainId: examples.EthSepoliaChainIdUint64, + BlockTimeMs: 12000, + }, + examples.BaseSepoliaChainIdUint64: { + NetworkUrl: "https://sepolia.base.org", + ChainId: examples.BaseSepoliaChainIdUint64, + BlockTimeMs: 2000, + }, + }, + XChainBlockchains: []uint64{ + examples.EthSepoliaChainIdUint64, + examples.BaseSepoliaChainIdUint64, + }, + }, + op: ðBalance0_5, + wallets: []common.Address{ + examples.EthWallet_0_5Eth, + }, + expected: false, // This entitlement evaluation would pass if the balance of the wallet on both networks was considered + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + require := require.New(t) + customEvaluator, err := NewEvaluatorFromConfigWithBlockchainInfo( + context.Background(), + &tc.cfg, + singleEtherChainBlockChainInfo, + infra.NewMetricsFactory(nil, "", ""), + ) + require.NoError(err) + + result, err := customEvaluator.evaluateOp(context.Background(), tc.op, tc.wallets) + if tc.expectedErr == nil { + require.NoError(err) + } else { + require.EqualError(err, tc.expectedErr.Error()) + } + if result != tc.expected { + t.Errorf("evaluateCheckOperation result (%v) = %v; want %v", tc.op, result, tc.expected) } }) } diff --git a/core/xchain/entitlement/evaluator.go b/core/xchain/entitlement/evaluator.go index 187effd7f..1cd33f9f9 100644 --- a/core/xchain/entitlement/evaluator.go +++ b/core/xchain/entitlement/evaluator.go @@ -12,9 +12,24 @@ import ( type Evaluator struct { clients BlockchainClientPool evalHistrogram *prometheus.HistogramVec + ethChainIds []uint64 } func NewEvaluatorFromConfig(ctx context.Context, cfg *config.Config, metrics infra.MetricsFactory) (*Evaluator, error) { + return NewEvaluatorFromConfigWithBlockchainInfo( + ctx, + cfg, + config.GetDefaultBlockchainInfo(), + metrics, + ) +} + +func NewEvaluatorFromConfigWithBlockchainInfo( + ctx context.Context, + cfg *config.Config, + blockChainInfo map[uint64]config.BlockchainInfo, + metrics infra.MetricsFactory, +) (*Evaluator, error) { clients, err := NewBlockchainClientPool(ctx, cfg) if err != nil { return nil, err @@ -27,5 +42,10 @@ func NewEvaluatorFromConfig(ctx context.Context, cfg *config.Config, metrics inf infra.DefaultDurationBucketsSeconds, "operation", ), + ethChainIds: config.GetEtherBasedBlockchains( + ctx, + cfg.XChainBlockchains, + blockChainInfo, + ), }, nil } diff --git a/core/xchain/examples/common.go b/core/xchain/examples/common.go index 776e9b38c..757a58921 100644 --- a/core/xchain/examples/common.go +++ b/core/xchain/examples/common.go @@ -35,13 +35,9 @@ var ( // This wallet has no eth in it on any chains. EmptyEthTestAccount = common.HexToAddress("0xb227905F186095083869928BAb49cA9CE9546817") - // This wallet contains .5ETH on Base Sepolia - BaseSepolia0_5EthWallet = common.HexToAddress("0x4BCfC6962Ab0297aF801da21216014F53B46E991") - // This wallet contains .05 ETH on Base Sepolia - BaseSepolia0_05EthWallet = common.HexToAddress("0xB79Af997239A334355F60DBeD75bEDf30AcD37bD") - - // .2 ETH on Ethereum Sepolia - Sepolia0_2EthWallet = common.HexToAddress("0x8cECcB1e5537040Fc63A06C88b4c1dE61880dA4d") - // .015 ETH on Ethereum Sepolia - Sepolia0_015EthWallet = common.HexToAddress("0xB4d85De80afE92C97293c32B1C0c604133d0332E") + // This wallet has .4ETH on Sepolia, and .1ETH on Base Sepolia - .5ETH total + EthWallet_0_5Eth = common.HexToAddress("0x3ef41b0469c1B808Caad9d643F596023e2aa8f11") + + // This wallet has .1ETH on Sepolia, and .1ETH on Base Sepolia - .2ETH total + EthWallet_0_2Eth = common.HexToAddress("0x4BD04Bf2AAC02238bCcFA75D7bc4Cfd2c019c331") )