From 7acf170751888cccea1b6ae0ec96efb6fdf97772 Mon Sep 17 00:00:00 2001 From: PatrickAlphac <54278053+PatrickAlphaC@users.noreply.github.com> Date: Fri, 4 Feb 2022 16:18:19 -0500 Subject: [PATCH] added listening for events for testnet tests; made verification process more robust --- brownie-config.yaml | 60 +++++++++---------- contracts/APIConsumer.sol | 10 ++-- contracts/VRFConsumer.sol | 3 + .../01_deploy_api_consumer.py | 6 +- scripts/helpful_scripts.py | 40 ++++++++++++- .../01_deploy_price_consumer_v3.py | 7 ++- scripts/vrf_scripts/01_deploy_vrf.py | 9 ++- tests/conftest.py | 10 +++- tests/test_api_consumer.py | 32 +++++++--- tests/test_vrf.py | 16 +++-- 10 files changed, 136 insertions(+), 57 deletions(-) diff --git a/brownie-config.yaml b/brownie-config.yaml index da5c8ef..d77df38 100644 --- a/brownie-config.yaml +++ b/brownie-config.yaml @@ -9,8 +9,8 @@ dependencies: compiler: solc: remappings: - - '@chainlink=smartcontractkit/chainlink-brownie-contracts@0.2.2' - - '@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.3.2' + - "@chainlink=smartcontractkit/chainlink-brownie-contracts@0.2.2" + - "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.3.2" # automatically fetch contract sources from Etherscan autofetch_sources: True # Uncomment to use the .env file @@ -19,58 +19,58 @@ autofetch_sources: True networks: default: development development: - keyhash: '0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4' + keyhash: "0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4" fee: 100000000000000000 - jobId: '29fa9aa13bf1468788b7cc4a500a45b8' + jobId: "29fa9aa13bf1468788b7cc4a500a45b8" update_interval: 60 verify: False kovan: - vrf_coordinator: '0xdD3782915140c8f3b190B5D67eAc6dc5760C46E9' - link_token: '0xa36085F69e2889c224210F603D836748e7dC0088' - keyhash: '0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4' + vrf_coordinator: "0xdD3782915140c8f3b190B5D67eAc6dc5760C46E9" + link_token: "0xa36085F69e2889c224210F603D836748e7dC0088" + keyhash: "0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4" fee: 100000000000000000 - oracle: '0xc57b33452b4f7bb189bb5afae9cc4aba1f7a4fd8' - jobId: 'd5270d1c311941d0b08bead21fea7747' - eth_usd_price_feed: '0x9326BFA02ADD2366b30bacB125260Af641031331' + oracle: "0xc57b33452b4f7bb189bb5afae9cc4aba1f7a4fd8" + jobId: "d5270d1c311941d0b08bead21fea7747" + eth_usd_price_feed: "0x9326BFA02ADD2366b30bacB125260Af641031331" # Change to True if you have an Etherscan API key and want to verify - verify: False + verify: True update_interval: 60 ganache: - keyhash: '0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4' + keyhash: "0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4" fee: 100000000000000000 - jobId: '29fa9aa13bf1468788b7cc4a500a45b8' + jobId: "29fa9aa13bf1468788b7cc4a500a45b8" update_interval: 60 verify: False rinkeby: - vrf_coordinator: '0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B' - link_token: '0x01be23585060835e02b77ef475b0cc51aa1e0709' - keyhash: '0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311' + vrf_coordinator: "0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B" + link_token: "0x01be23585060835e02b77ef475b0cc51aa1e0709" + keyhash: "0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311" fee: 100000000000000000 - oracle: '0xc57b33452b4f7bb189bb5afae9cc4aba1f7a4fd8' - jobId: '6b88e0402e5d415eb946e528b8e0c7ba' - eth_usd_price_feed: '0x8A753747A1Fa494EC906cE90E9f37563A8AF630e' + oracle: "0xc57b33452b4f7bb189bb5afae9cc4aba1f7a4fd8" + jobId: "6b88e0402e5d415eb946e528b8e0c7ba" + eth_usd_price_feed: "0x8A753747A1Fa494EC906cE90E9f37563A8AF630e" # Change to True if you have an Etherscan API key and want to verify verify: False fuji: - link_token: '0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846' + link_token: "0x0b9d5D9136855f6FEc3c0993feE6E9CE8a297846" fee: 100000000000000000 - oracle: '0xcc80934eaf22b2c8dbf7a69e8e0d356a7cac5754' - jobId: '5ca4fa9b2d64462290abfbda84e38cf4' + oracle: "0xcc80934eaf22b2c8dbf7a69e8e0d356a7cac5754" + jobId: "5ca4fa9b2d64462290abfbda84e38cf4" mumbai: - eth_usd_price_feed: '0x0715A7794a1dc8e42615F059dD6e406A6594651A' - link_token: '0x326C977E6efc84E512bB9C30f76E30c160eD06FB' - vrf_coordinator: '0x8C7382F9D8f56b33781fE506E897a4F1e2d17255' - keyhash: '0x6e75b569a01ef56d18cab6a8e71e6600d6ce853834d4a5748b720d06f878b3a4' + eth_usd_price_feed: "0x0715A7794a1dc8e42615F059dD6e406A6594651A" + link_token: "0x326C977E6efc84E512bB9C30f76E30c160eD06FB" + vrf_coordinator: "0x8C7382F9D8f56b33781fE506E897a4F1e2d17255" + keyhash: "0x6e75b569a01ef56d18cab6a8e71e6600d6ce853834d4a5748b720d06f878b3a4" fee: 1000000000000000000 binance: # link_token: ?? - eth_usd_price_feed: '0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e' + eth_usd_price_feed: "0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e" binance-fork: - eth_usd_price_feed: '0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e' + eth_usd_price_feed: "0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e" mainnet-fork: - eth_usd_price_feed: '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419' + eth_usd_price_feed: "0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419" matic-fork: - eth_usd_price_feed: '0xF9680D99D6C9589e2a93a78A04A279e509205945' + eth_usd_price_feed: "0xF9680D99D6C9589e2a93a78A04A279e509205945" wallets: from_key: ${PRIVATE_KEY} from_mnemonic: ${MNEMONIC} diff --git a/contracts/APIConsumer.sol b/contracts/APIConsumer.sol index cf68953..b387a8b 100644 --- a/contracts/APIConsumer.sol +++ b/contracts/APIConsumer.sol @@ -6,10 +6,11 @@ import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; contract APIConsumer is ChainlinkClient { using Chainlink for Chainlink.Request; uint256 public volume; - - address private oracle; - bytes32 private jobId; - uint256 private fee; + address public oracle; + bytes32 public jobId; + uint256 public fee; + + event DataFullfilled(uint256 volume); /** * Network: Kovan @@ -66,6 +67,7 @@ contract APIConsumer is ChainlinkClient { function fulfill(bytes32 _requestId, uint256 _volume) public recordChainlinkFulfillment(_requestId) { volume = _volume; + emit DataFullfilled(volume); } // function stringToBytes32(string memory source) public pure returns (bytes32 result) { diff --git a/contracts/VRFConsumer.sol b/contracts/VRFConsumer.sol index ffa708b..a3cb226 100644 --- a/contracts/VRFConsumer.sol +++ b/contracts/VRFConsumer.sol @@ -9,6 +9,8 @@ contract VRFConsumer is VRFConsumerBase { uint256 internal fee; uint256 public randomResult; + event ReturnedRandomness(uint256 randomNumber); + /** * Constructor inherits VRFConsumerBase @@ -42,5 +44,6 @@ contract VRFConsumer is VRFConsumerBase { */ function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override { randomResult = randomness; + emit ReturnedRandomness(randomResult); } } diff --git a/scripts/chainlink_api_scripts/01_deploy_api_consumer.py b/scripts/chainlink_api_scripts/01_deploy_api_consumer.py index fab4be0..612947d 100644 --- a/scripts/chainlink_api_scripts/01_deploy_api_consumer.py +++ b/scripts/chainlink_api_scripts/01_deploy_api_consumer.py @@ -19,8 +19,12 @@ def deploy_api_consumer(): fee, link_token, {"from": account}, - publish_source=config["networks"][network.show_active()].get("verify", False), ) + if (config["networks"][network.show_active()].get("verify", False)): + api_consumer.tx.wait(BLOCK_CONFIRMATIONS_FOR_VERIFICATION) + APIConsumer.publish_source(api_consumer) + else: + api_consumer.tx.wait(1) print(f"API Consumer deployed to {api_consumer.address}") return api_consumer diff --git a/scripts/helpful_scripts.py b/scripts/helpful_scripts.py index 84ada03..aba68e9 100644 --- a/scripts/helpful_scripts.py +++ b/scripts/helpful_scripts.py @@ -7,8 +7,9 @@ MockOracle, VRFCoordinatorMock, Contract, + web3 ) -from web3 import Web3 +import time NON_FORKED_LOCAL_BLOCKCHAIN_ENVIRONMENTS = ["hardhat", "development", "ganache"] LOCAL_BLOCKCHAIN_ENVIRONMENTS = NON_FORKED_LOCAL_BLOCKCHAIN_ENVIRONMENTS + [ @@ -16,6 +17,8 @@ "binance-fork", "matic-fork", ] +# Etherscan usually takes a few blocks to register the contract has been deployed +BLOCK_CONFIRMATIONS_FOR_VERIFICATION = 6 contract_to_mock = { "link_token": LinkToken, @@ -25,7 +28,7 @@ } DECIMALS = 18 -INITIAL_VALUE = Web3.toWei(2000, "ether") +INITIAL_VALUE = web3.toWei(2000, "ether") def get_account(index=None, id=None): @@ -114,3 +117,36 @@ def deploy_mocks(decimals=DECIMALS, initial_value=INITIAL_VALUE): mock_oracle = MockOracle.deploy(link_token.address, {"from": account}) print(f"Deployed to {mock_oracle.address}") print("Mocks Deployed!") + + +def listen_for_event(brownie_contract, event, timeout=200, poll_interval=2): + """Listen asyncronously for an event to be fired from a contract. + We are waiting for the event to return, so this function is blocking. + + Args: + brownie_contract ([brownie.network.contract.ProjectContract]): + A brownie contract of some kind. + + event ([string]): The event you'd like to listen for. + + timeout (int, optional): The max amount in seconds you'd like to + wait for that event to fire. Defaults to 200 seconds. + + poll_interval ([int]): How often to call your node to check for events. + Defaults to 2 seconds. + """ + web3_contract = web3.eth.contract( + address=brownie_contract.address, abi=brownie_contract.abi + ) + start_time = time.time() + current_time = time.time() + event_filter = web3_contract.events[event].createFilter(fromBlock="latest") + while current_time - start_time < timeout: + for event_response in event_filter.get_new_entries(): + if event in event_response.event: + print("Found event!") + return event_response + time.sleep(poll_interval) + current_time = time.time() + print("Timeout reached, no event found.") + return { "event": None } diff --git a/scripts/price_feed_scripts/01_deploy_price_consumer_v3.py b/scripts/price_feed_scripts/01_deploy_price_consumer_v3.py index 40b351e..8fe7d8c 100644 --- a/scripts/price_feed_scripts/01_deploy_price_consumer_v3.py +++ b/scripts/price_feed_scripts/01_deploy_price_consumer_v3.py @@ -3,6 +3,7 @@ from scripts.helpful_scripts import ( get_account, get_contract, + BLOCK_CONFIRMATIONS_FOR_VERIFICATION ) @@ -12,8 +13,12 @@ def deploy_price_feed_consumer(): price_feed = PriceFeedConsumer.deploy( eth_usd_price_feed_address, {"from": account}, - publish_source=config["networks"][network.show_active()].get("verify", False), ) + if (config["networks"][network.show_active()].get("verify", False)): + price_feed.tx.wait(BLOCK_CONFIRMATIONS_FOR_VERIFICATION) + PriceFeedConsumer.publish_source(price_feed) + else: + price_feed.tx.wait(1) print(f"The current price of ETH is {price_feed.getLatestPrice()}") return price_feed diff --git a/scripts/vrf_scripts/01_deploy_vrf.py b/scripts/vrf_scripts/01_deploy_vrf.py index 839c912..5d07065 100644 --- a/scripts/vrf_scripts/01_deploy_vrf.py +++ b/scripts/vrf_scripts/01_deploy_vrf.py @@ -14,15 +14,20 @@ def depoly_vrf(): vrf_coordinator = get_contract("vrf_coordinator") link_token = get_contract("link_token") - return VRFConsumer.deploy( + vrf_consumer = VRFConsumer.deploy( keyhash, vrf_coordinator, link_token, fee, {"from": account}, - publish_source=config["networks"][network.show_active()].get("verify", False), ) + if (config["networks"][network.show_active()].get("verify", False)): + vrf_consumer.tx.wait(BLOCK_CONFIRMATIONS_FOR_VERIFICATION) + VRFConsumer.publish_source(vrf_consumer) + else: + vrf_consumer.tx.wait(1) + def main(): depoly_vrf() diff --git a/tests/conftest.py b/tests/conftest.py index d86652f..0e513fc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ from scripts.helpful_scripts import ( LOCAL_BLOCKCHAIN_ENVIRONMENTS, ) +from web3 import Web3 @pytest.fixture @@ -24,7 +25,7 @@ def get_job_id(): if network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS: return 0 if network.show_active() in config["networks"]: - return config["networks"][network.show_active()]["jobId"] + return Web3.toHex(text=config["networks"][network.show_active()]["jobId"]) else: pytest.skip("Invalid network/link token specified") @@ -46,7 +47,12 @@ def node_account(): @pytest.fixture def chainlink_fee(): - return 1000000000000000000 + if network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS: + return 1000000000000000000 + if network.show_active() in config["networks"]: + return config["networks"][network.show_active()]["fee"] + else: + pytest.skip("Invalid network/link token specified ") @pytest.fixture diff --git a/tests/test_api_consumer.py b/tests/test_api_consumer.py index 7d1e8df..de410ba 100644 --- a/tests/test_api_consumer.py +++ b/tests/test_api_consumer.py @@ -1,9 +1,14 @@ import time import pytest -from brownie import APIConsumer, network -from scripts.helpful_scripts import LOCAL_BLOCKCHAIN_ENVIRONMENTS, get_account -from scripts.helpful_scripts import get_contract +from brownie import APIConsumer, network, config +from scripts.helpful_scripts import ( + LOCAL_BLOCKCHAIN_ENVIRONMENTS, + get_account, + listen_for_event, + get_contract, + fund_with_link +) @pytest.fixture @@ -16,6 +21,10 @@ def deploy_api_contract(get_job_id, chainlink_fee): get_contract("link_token").address, {"from": get_account()}, ) + block_confirmations=6 + if network.show_active() in LOCAL_BLOCKCHAIN_ENVIRONMENTS: + block_confirmations=1 + api_consumer.tx.wait(block_confirmations) # Assert assert api_consumer is not None return api_consumer @@ -43,20 +52,25 @@ def test_send_api_request_local( assert isinstance(api_contract.volume(), int) assert api_contract.volume() > 0 - def test_send_api_request_testnet(deploy_api_contract, chainlink_fee): # Arrange if network.show_active() not in ["kovan", "rinkeby", "mainnet"]: pytest.skip("Only for local testing") api_contract = deploy_api_contract - get_contract("link_token").transfer( - api_contract.address, chainlink_fee * 2, {"from": get_account()} + + if (config["networks"][network.show_active()].get("verify", False)): + APIConsumer.publish_source(api_contract) + + tx = fund_with_link( + api_contract.address, amount=chainlink_fee ) + tx.wait(1) # Act transaction = api_contract.requestVolumeData({"from": get_account()}) + transaction.wait(1) + # Assert - assert transaction is not None - transaction.wait(2) - time.sleep(35) + event_response = listen_for_event(api_contract, "DataFullfilled") + assert event_response.event is not None assert isinstance(api_contract.volume(), int) assert api_contract.volume() > 0 diff --git a/tests/test_vrf.py b/tests/test_vrf.py index d888cfc..70e200a 100644 --- a/tests/test_vrf.py +++ b/tests/test_vrf.py @@ -1,10 +1,12 @@ import time import pytest -from brownie import VRFConsumer, convert, network +from brownie import VRFConsumer, convert, network, config from scripts.helpful_scripts import ( get_account, get_contract, LOCAL_BLOCKCHAIN_ENVIRONMENTS, + fund_with_link, + listen_for_event ) @@ -54,7 +56,6 @@ def test_returns_random_number_local(get_keyhash, chainlink_fee): def test_returns_random_number_testnet( get_keyhash, - chainlink_fee, ): # Arrange if network.show_active() not in ["kovan", "rinkeby", "ropsten"]: @@ -63,17 +64,20 @@ def test_returns_random_number_testnet( get_keyhash, get_contract("vrf_coordinator").address, get_contract("link_token").address, - chainlink_fee, + config["networks"][network.show_active()]["fee"], {"from": get_account()}, ) - get_contract("link_token").transfer( - vrf_consumer.address, chainlink_fee * 3, {"from": get_account()} + tx = fund_with_link( + vrf_consumer.address, amount=config["networks"][network.show_active()]["fee"] ) + tx.wait(1) # Act transaction_receipt = vrf_consumer.getRandomNumber({"from": get_account()}) assert isinstance(transaction_receipt.txid, str) transaction_receipt.wait(1) - time.sleep(90) + event_response = listen_for_event(vrf_consumer, "ReturnedRandomness") + # Assert + assert event_response.event is not None assert vrf_consumer.randomResult() > 0 assert isinstance(vrf_consumer.randomResult(), int)