diff --git a/crates/erc20_payment_lib/src/eth.rs b/crates/erc20_payment_lib/src/eth.rs index d35e51bd..e157f4a5 100644 --- a/crates/erc20_payment_lib/src/eth.rs +++ b/crates/erc20_payment_lib/src/eth.rs @@ -471,155 +471,198 @@ pub struct GetBalanceArgs { pub chain_id: Option, } -pub async fn get_balance( +async fn get_balance_using_contract_wrapper( web3: Arc, args: GetBalanceArgs, -) -> Result { - log::debug!( - "Checking balance for address {:#x}, token address: {:#x}", - args.address, - args.token_address.unwrap_or_default(), - ); + token_address: Address, + call_with_details: Address, +) -> Result, PaymentError> { + let abi_encoded_get_balance = encode_erc20_balance_of(args.address).map_err(err_from!())?; - if let (Some(token_address), Some(call_with_details)) = - (args.token_address, args.call_with_details) + let call_data = + encode_call_with_details(token_address, abi_encoded_get_balance).map_err(err_from!())?; + + let block_id = if let Some(block_number) = args.block_number { + log::debug!( + "Checking balance (contract) for block number {}", + block_number + ); + Some(BlockId::Number(BlockNumber::Number(block_number.into()))) + } else { + log::debug!("Checking balance (contract) for latest block"); + None + }; + match web3 + .clone() + .eth_call( + CallRequest { + from: Some(args.address), + to: Some(call_with_details), + data: Some(Bytes::from(call_data)), + ..Default::default() + }, + block_id, + ) + .await { - let abi_encoded_get_balance = encode_erc20_balance_of(args.address).map_err(err_from!())?; + Ok(res) => { + let (block_info, call_result) = decode_call_with_details(&res.0)?; - let call_data = encode_call_with_details(token_address, abi_encoded_get_balance) - .map_err(err_from!())?; + if let Some(chain_id) = args.chain_id { + if block_info.chain_id != chain_id { + return Err(err_custom_create!( + "Invalid chain id in response: {}, expected {}", + block_info.chain_id, + chain_id + )); + } + } + + let token_balance = U256::from_big_endian(&call_result); - let block_id = if let Some(block_number) = args.block_number { log::debug!( - "Checking balance (contract) for block number {}", - block_number + "Token balance response: {:?} - token balance: {}", + block_info, + token_balance ); - Some(BlockId::Number(BlockNumber::Number(block_number.into()))) - } else { - log::debug!("Checking balance (contract) for latest block"); - None - }; + Ok(Some(GetBalanceResult { + gas_balance: Some(block_info.eth_balance), + token_balance: Some(token_balance), + block_number: block_info.block_number, + block_datetime: block_info.block_datetime, + })) + } + Err(e) => { + if e.to_string().to_lowercase().contains("insufficient funds") { + log::warn!( + "Balance check via wrapper contract failed, falling back to standard method" + ); + Ok(None) + } else { + log::error!( + "Error getting balance for account: {:#x} - {}", + args.address, + e + ); + Err(err_custom_create!( + "Error getting balance for account: {:#x} - {}", + args.address, + e + )) + } + } + } +} + +async fn get_balance_simple( + web3: Arc, + args: GetBalanceArgs, +) -> Result { + let block_id = if let Some(block_number) = args.block_number { + log::debug!("Checking balance for block number {}", block_number); + BlockId::Number(BlockNumber::Number(block_number.into())) + } else { + log::debug!("Checking balance for latest block"); + BlockId::Number(BlockNumber::Latest) + }; + let block_info = web3 + .clone() + .eth_block(block_id) + .await + .map_err(err_from!())? + .ok_or(err_custom_create!("Cannot found block_info"))?; + + let block_number = block_info + .number + .ok_or(err_custom_create!( + "Failed to found block number in block info", + ))? + .as_u64(); + let gas_balance = Some( + web3.clone() + .eth_balance(args.address, Some(BlockNumber::Number(block_number.into()))) + .await + .map_err(err_from!())?, + ); + + let block_number = block_info + .number + .ok_or(err_custom_create!( + "Failed to found block number in block info", + ))? + .as_u64(); + + let block_date = datetime_from_u256_timestamp(block_info.timestamp).ok_or( + err_custom_create!("Failed to found block date in block info"), + )?; + + let token_balance = if let Some(token_address) = args.token_address { + let call_data = encode_erc20_balance_of(args.address).map_err(err_from!())?; let res = web3 .clone() .eth_call( CallRequest { - from: Some(args.address), - to: Some(call_with_details), + from: None, + to: Some(token_address), + gas: None, + gas_price: None, + value: None, data: Some(Bytes::from(call_data)), - ..Default::default() + transaction_type: None, + access_list: None, + max_fee_per_gas: None, + max_priority_fee_per_gas: None, }, - block_id, + Some(BlockId::Number(BlockNumber::Number(block_number.into()))), ) .await .map_err(err_from!())?; + if res.0.len() != 32 { + return Err(err_create!(TransactionFailedError::new(&format!( + "Invalid balance response: {:?}. Probably not a valid ERC20 contract {:#x}", + res.0, token_address + )))); + }; + Some(U256::from_big_endian(&res.0)) + } else { + None + }; + Ok(GetBalanceResult { + gas_balance, + token_balance, + block_number, + block_datetime: block_date, + }) +} - let (block_info, call_result) = decode_call_with_details(&res.0)?; - - /*let now = chrono::Utc::now(); - let seconds_old = (now - block_info.block_datetime).num_seconds(); - if seconds_old > 10 { - log::warn!("Balance is {seconds_old}s old"); - }*/ - //decode call_result - if let Some(chain_id) = args.chain_id { - if block_info.chain_id != chain_id { - return Err(err_custom_create!( - "Invalid chain id in response: {}, expected {}", - block_info.chain_id, - chain_id - )); - } - } - - let token_balance = U256::from_big_endian(&call_result); +pub async fn get_balance( + web3: Arc, + args: GetBalanceArgs, +) -> Result { + log::debug!( + "Checking balance for address {:#x}, token address: {:#x}", + args.address, + args.token_address.unwrap_or_default(), + ); - log::debug!( - "Token balance response: {:?} - token balance: {}", - block_info, - token_balance - ); - Ok(GetBalanceResult { - gas_balance: Some(block_info.eth_balance), - token_balance: Some(token_balance), - block_number: block_info.block_number, - block_datetime: block_info.block_datetime, - }) + let balance = if let (Some(token_address), Some(call_with_details)) = + (args.token_address, args.call_with_details) + { + get_balance_using_contract_wrapper( + web3.clone(), + args.clone(), + token_address, + call_with_details, + ) + .await? } else { - let block_id = if let Some(block_number) = args.block_number { - log::debug!("Checking balance for block number {}", block_number); - BlockId::Number(BlockNumber::Number(block_number.into())) - } else { - log::debug!("Checking balance for latest block"); - BlockId::Number(BlockNumber::Latest) - }; - let block_info = web3 - .clone() - .eth_block(block_id) - .await - .map_err(err_from!())? - .ok_or(err_custom_create!("Cannot found block_info"))?; - - let block_number = block_info - .number - .ok_or(err_custom_create!( - "Failed to found block number in block info", - ))? - .as_u64(); - let gas_balance = Some( - web3.clone() - .eth_balance(args.address, Some(BlockNumber::Number(block_number.into()))) - .await - .map_err(err_from!())?, - ); + None + }; - let block_number = block_info - .number - .ok_or(err_custom_create!( - "Failed to found block number in block info", - ))? - .as_u64(); - - let block_date = datetime_from_u256_timestamp(block_info.timestamp).ok_or( - err_custom_create!("Failed to found block date in block info"), - )?; - - let token_balance = if let Some(token_address) = args.token_address { - let call_data = encode_erc20_balance_of(args.address).map_err(err_from!())?; - let res = web3 - .clone() - .eth_call( - CallRequest { - from: None, - to: Some(token_address), - gas: None, - gas_price: None, - value: None, - data: Some(Bytes::from(call_data)), - transaction_type: None, - access_list: None, - max_fee_per_gas: None, - max_priority_fee_per_gas: None, - }, - Some(BlockId::Number(BlockNumber::Number(block_number.into()))), - ) - .await - .map_err(err_from!())?; - if res.0.len() != 32 { - return Err(err_create!(TransactionFailedError::new(&format!( - "Invalid balance response: {:?}. Probably not a valid ERC20 contract {:#x}", - res.0, token_address - )))); - }; - Some(U256::from_big_endian(&res.0)) - } else { - None - }; - Ok(GetBalanceResult { - gas_balance, - token_balance, - block_number, - block_datetime: block_date, - }) + if let Some(balance) = balance { + Ok(balance) + } else { + get_balance_simple(web3, args).await } } diff --git a/scenarios/test_get_balance/config-payments_template.toml b/scenarios/test_get_balance/config-payments_template.toml new file mode 100644 index 00000000..8a3a1e9f --- /dev/null +++ b/scenarios/test_get_balance/config-payments_template.toml @@ -0,0 +1,174 @@ +[engine] +# proces interval (in seconds) is to set how often we want to recheck transaction status +# minimum 1 second, sensible maximum around 60 seconds +process-interval = 15 +# proces interval after send (in seconds) is to set how long to wait after sending transaction before checking for confirmation +# sensible minimum 20 seconds, sensible maximum around 60 seconds +process-interval-after-send = 30 +# proces interval after error (in seconds) is to set how long to wait after encountering error before trying again +# minimum 1 second, sensible maximum around 60 seconds +process-interval-after-error = 25 + +# proces interval after missing gas or token (in seconds) +# it is starting with checking every process-interval-after-no-gas-or-token-start +# and then increasing by multiplying by process-interval-after-no-gas-or-token-increase +# up to process-interval-after-no-gas-or-token-max +process-interval-after-no-gas-or-token-start = 20 +process-interval-after-no-gas-or-token-max = 40 +process-interval-after-no-gas-or-token-increase = 1.5 + +# report alive interval (in seconds) is to set how often we want to report that we are alive +# minimum 1 second, maximum is capped by gather-interval +report-alive-interval = 10 +# gather interval (in seconds) is to set how often payments are gathered +# minimum 1 second, no maximum limit +gather-interval = 60 +# gather payments on payment driver start (otherwise wait for first gather-interval) +gather-at-start = true +automatic-recover = false +# set to true to not respect deadlines attached to payments +ignore-deadlines = false + + +[chain.mainnet] +chain-name = "Mainnet" +chain-id = 1 +currency-symbol = "ETH" +priority-fee = 1.01 +max-fee-per-gas = 40.0 +transaction-timeout = 100 +token = { address = "0x7DD9c5Cba05E151C895FDe1CF355C9A1D5DA6429", symbol = "GLM" } +confirmation-blocks = 1 +block-explorer-url = "https://etherscan.io" +external-source-check-interval = 300 + +[[chain.mainnet.rpc-endpoints]] +endpoints = """ + %%RPC_ENDPOINT%% +""" +priority = 0 +max-timeout-ms = 5000 +verify-interval-secs = 60 +allowed-head-behind-secs = 120 + +[chain.base] +chain-name = "Base" +chain-id = 8453 +currency-symbol = "ETH" +priority-fee = 0.000001 +max-fee-per-gas = 20.0 +transaction-timeout = 100 +attestation-contract = { address = "0x4200000000000000000000000000000000000021" } +schema-registry-contract = { address = "0x4200000000000000000000000000000000000020" } +token = { address = "0x1200000000000000000000000000000000000021", symbol = "GLM" } +confirmation-blocks = 0 +block-explorer-url = "https://base.etherscan.io" +external-source-check-interval = 300 + +[[chain.base.rpc-endpoints]] +endpoints = """ + %%RPC_ENDPOINT%% +""" +priority = 0 +max-timeout-ms = 5000 +verify-interval-secs = 60 +allowed-head-behind-secs = 120 + +[chain.sepolia] +chain-name = "Sepolia" +chain-id = 11155111 +currency-symbol = "tETH" +priority-fee = 0.000001 +max-fee-per-gas = 20.0 +transaction-timeout = 100 +token = { address = "0x167b15ada84c63427c6c813B915a42eFC72E7175", symbol = "tGLM" } +mint-contract = { address = "0x31A2a20956a40c2F358Fa5cec59D55a9C5d6fF9A", max-glm-allowed = 400 } +attestation-contract = { address = "0xC2679fBD37d54388Ce493F1DB75320D236e1815e" } +schema-registry-contract = { address = "0x0a7E2Ff54e76B8E6659aedc9103FB21c038050D0" } +confirmation-blocks = 0 +block-explorer-url = "https://sepolia.etherscan.io" +external-source-check-interval = 300 + +[[chain.sepolia.rpc-endpoints]] +endpoints = """ + %%RPC_ENDPOINT%% +""" +priority = 0 +max-timeout-ms = 5000 +verify-interval-secs = 60 +allowed-head-behind-secs = 120 + +[chain.holesky] +chain-name = "Holesky" +chain-id = 17000 +currency-symbol = "tETH" +priority-fee = 0.000001 +max-fee-per-gas = 20.0 +transaction-timeout = 100 +wrapper-contract = { address = "0xE168bCa171ccf51066E2106d8955BF22705a6905" } +token = { address = "0x8888888815bf4DB87e57B609A50f938311EEd068", symbol = "tGLM" } +multi-contract = { address = "0xAaAAAaA00E1841A63342db7188abA84BDeE236c7", max-at-once = 10 } +mint-contract = { address = "0xFACe100969FF47EB58d2CF603321B581A84bcEaC", max-glm-allowed = 400 } +lock-contract = { address = "0x7167E731b0031d4326d46C8D1E1c2E111227aB5f" } +distributor-contract = { address = "0xb7Fb99e86f93dc3047A12932052236d853065173" } +faucet-client = { max-eth-allowed = 0.009, faucet-srv = "_holesky-faucet._tcp", faucet-host = "faucet.testnet.golem.network", faucet-lookup-domain = "dev.golem.network", faucet-srv-port = 4002 } +confirmation-blocks = 0 +block-explorer-url = "https://holesky.etherscan.io" +external-source-check-interval = 300 + + +[[chain.holesky.rpc-endpoints]] +endpoints = """ + %%RPC_ENDPOINT%% +""" +priority = 0 +max-timeout-ms = 5000 +verify-interval-secs = 60 +allowed-head-behind-secs = 120 + +[chain.mumbai] +chain-name = "Mumbai testnet" +chain-id = 80001 +currency-symbol = "tMATIC" +priority-fee = 1.0 +max-fee-per-gas = 14.0 +transaction-timeout = 60 +token = { address = "0x2036807B0B3aaf5b1858EE822D0e111fDdac7018", symbol = "tGLM" } +multi-contract = { address = "0x800010D7d0d315DCA795110ecCf0127cBd76b89f", max-at-once = 10 } +confirmation-blocks = 1 +block-explorer-url = "https://mumbai.polygonscan.com" +external-source-check-interval = 300 + +[[chain.mumbai.rpc-endpoints]] +endpoints = """ + %%RPC_ENDPOINT%% +""" +priority = 0 +max-timeout-ms = 5000 +allowed-head-behind-secs = 60 + +[chain.polygon] +chain-name = "Polygon mainnet" +chain-id = 137 +currency-symbol = "MATIC" +priority-fee = 30.111 +max-fee-per-gas = 500.0 +transaction-timeout = 100 +token = { address = "0x0B220b82F3eA3B7F6d9A1D8ab58930C064A2b5Bf", symbol = "GLM" } +wrapper-contract = { address = "0xbB6aad747990BB6F7f56851556A3277e474C656a" } +lock-contract = { address = "0x633193F5524849C84368ADF39aFDB0EedFAf8B29" } +multi-contract = { address = "0x50100d4faf5f3b09987dea36dc2eddd57a3e561b", max-at-once = 10 } +attestation-contract = { address = "0x5E634ef5355f45A855d02D66eCD687b1502AF790" } +schema-registry-contract = { address = "0x7876EEF51A891E737AF8ba5A5E0f0Fd29073D5a7" } +confirmation-blocks = 1 +block-explorer-url = "https://polygonscan.com" +external-source-check-interval = 300 + +[[chain.polygon.rpc-endpoints]] +endpoints = """ + %%RPC_ENDPOINT%% +""" + +priority = 0 +max-timeout-ms = 5000 +allowed-head-behind-secs = 120 diff --git a/scenarios/test_get_balance/run_test.py b/scenarios/test_get_balance/run_test.py new file mode 100644 index 00000000..6d1b443c --- /dev/null +++ b/scenarios/test_get_balance/run_test.py @@ -0,0 +1,80 @@ +import os +import json +import subprocess + +erc20_proc = "../../target/debug/erc20_processor" +if os.name == "nt": + erc20_proc = erc20_proc.replace("/", "\\") + ".exe" + +def test_endpoint(network, test_endp, no_accounts, use_contract=False): + print("Checking endpoint {}".format(test_endp)) + os.system(f"{erc20_proc} generate-key -n {no_accounts} > .env") + + with open("config-payments_template.toml", "r") as f: + text = f.read().replace("%%RPC_ENDPOINT%%", test_endp) + + with open("config-payments.toml", "w") as f: + f.write(text) + + comm = [erc20_proc, "balance", "-c", network] + if not use_contract: + comm.append("--no-wrapper-contract") + print("Running command {}".format(" ".join(comm))) + # Run and get output + s = subprocess.Popen(comm, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + # Run and get output + + stdout, stderr = s.communicate() + + print(stderr.decode("utf-8")) + + # load json + try: + data = json.loads(stdout) + except json.JSONDecodeError: + print("Error: failed to parse JSON") + print(stdout) + # print(stderr.decode("utf-8")) + raise + success_count = 0 + for el in data: + if data[el]["gas"] != "0": + raise Exception("Error: gas balance is not 0") + if data[el]["token"] != "0": + raise Exception("Error: token balance is not 0") + success_count += 1 + print(f"{test_endp} - {el} - OK - " + "{} - {}".format(data[el]["gas"], data[el]["token"])) + if success_count != no_accounts: + raise Exception("Error: wrong number of accounts") + + +def check_holesky_endpoints(endpoints): + for endpoint in endpoints: + test_endpoint("holesky", endpoint, 7, use_contract=True) + for endpoint in endpoints: + test_endpoint("holesky", endpoint, 7, use_contract=False) + + +def check_polygon_endpoints(endpoints): + for endpoint in endpoints: + test_endpoint("polygon", endpoint, 7, use_contract=True) + for endpoint in endpoints: + test_endpoint("polygon", endpoint, 7, use_contract=False) + + + +if __name__ == '__main__': + check_polygon_endpoints([ + "https://polygon-pokt.nodies.app", + "https://polygon-mainnet.public.blastapi.io", + "https://polygon-pokt.nodies.app", + "https://1rpc.io/matic", + "https://polygon-rpc.com", + ]) + check_holesky_endpoints([ + "https://holesky.drpc.org", + "https://ethereum-holesky.blockpi.network/v1/rpc/public", + "https://ethereum-holesky-rpc.publicnode.com" + ]) +