├── hsck ├── __init__.py ├── test │ ├── __init__.py │ └── test_adapter.py ├── crypto_compare_ea │ ├── __init__.py │ ├── main.py │ ├── app.py │ ├── bridge.py │ └── adapter.py ├── ports.py └── job_id.py ├── .gitignore ├── geth_scripts ├── sendEth.sh ├── deploy_oracle.sh ├── deploy_operator.sh ├── deploy_link_token.sh ├── startGeth.sh └── js │ └── sendEth.js ├── avalanche_scripts ├── send_avax.sh ├── deploy_oracle.sh ├── deploy_operator.sh ├── deploy_link_token.sh ├── status.sh ├── start_avalanche.sh ├── check_c_chain_balance.sh ├── check_x_chain_balance.sh └── create_account.sh ├── ToDo ├── scripts ├── oracle_example │ ├── read_crypto_compare_ea_result.py │ ├── read_testnet_consumer_result.py │ ├── make_crypto_compare_ea_request.py │ ├── make_testnet_consumer_request.py │ ├── read_multi_word_consumer_result.py │ ├── deploy_testnet_consumer.py │ ├── make_multi_word_consumer_request.py │ └── deploy_multi_word_consumer.py └── infrastructure │ ├── deploy_oracle.py │ ├── deploy_operator.py │ ├── send_avax.py │ ├── fund_chainlink_account.py │ ├── print_contract_addresses.py │ └── deploy_link_token.py ├── brownie_scripts └── add_networks.sh ├── LICENSE ├── chainlink ├── TestnetConsumerJob.toml ├── CryptoCompareJob.toml ├── MultiWordConsumer.toml └── chainlink_dockerfile ├── contracts ├── MultiWordConsumer.sol ├── ATestnetConsumer.sol ├── Oracle.sol ├── linkContract.sol └── Operator.sol ├── setup.py ├── brownie-config.yaml └── README.md /hsck/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hsck/test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hsck/crypto_compare_ea/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /hsck/ports.py: -------------------------------------------------------------------------------- 1 | CRYPTO_COMPARE_EXTERNAL_ADAPTER_PORT = 8082 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /chainlink/local.env 2 | /build/ 3 | /hsck.egg-info/ 4 | .idea/ 5 | -------------------------------------------------------------------------------- /geth_scripts/sendEth.sh: -------------------------------------------------------------------------------- 1 | geth attach http://localhost:8545 --exec 'loadScript("js/sendEth.js")' -------------------------------------------------------------------------------- /avalanche_scripts/send_avax.sh: -------------------------------------------------------------------------------- 1 | brownie run scripts/infrastructure/send_avax.py --network avax-avash -------------------------------------------------------------------------------- /geth_scripts/deploy_oracle.sh: -------------------------------------------------------------------------------- 1 | brownie run scripts/infrastructure/deploy_oracle.py --network local -------------------------------------------------------------------------------- /geth_scripts/deploy_operator.sh: -------------------------------------------------------------------------------- 1 | brownie run scripts/infrastructure/deploy_operator.py --network local -------------------------------------------------------------------------------- /avalanche_scripts/deploy_oracle.sh: -------------------------------------------------------------------------------- 1 | brownie run scripts/infrastructure/deploy_oracle.py --network avax-avash -------------------------------------------------------------------------------- /geth_scripts/deploy_link_token.sh: -------------------------------------------------------------------------------- 1 | brownie run ../scripts/infrastructure/deploy_link_token.py --network local -------------------------------------------------------------------------------- /avalanche_scripts/deploy_operator.sh: -------------------------------------------------------------------------------- 1 | brownie run scripts/infrastructure/deploy_operator.py --network avax-avash -------------------------------------------------------------------------------- /avalanche_scripts/deploy_link_token.sh: -------------------------------------------------------------------------------- 1 | brownie run ../scripts/infrastructure/deploy_link_token.py --network avax-avash -------------------------------------------------------------------------------- /avalanche_scripts/status.sh: -------------------------------------------------------------------------------- 1 | curl -X POST --data '{ 2 | "jsonrpc":"2.0", 3 | "id" :1, 4 | "method" :"info.peers" 5 | }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/info -------------------------------------------------------------------------------- /avalanche_scripts/start_avalanche.sh: -------------------------------------------------------------------------------- 1 | avalanchego --http-port=9650 --public-ip=127.0.0.1 --db-dir $AVALANCHE_DEV_DATA_PATH --network-id=local --staking-enabled=false --snow-sample-size=1 --snow-quorum-size=1 2 | -------------------------------------------------------------------------------- /geth_scripts/startGeth.sh: -------------------------------------------------------------------------------- 1 | geth --datadir $GETH_DEV_DATA_PATH --networkid 1337 --verbosity 6 --ws --http --http.api personal,eth,net,web3 --http.rpcprefix '/' --dev --dev.period 1 --http.corsdomain "*" --rpc.allow-unprotected-txs -------------------------------------------------------------------------------- /ToDo: -------------------------------------------------------------------------------- 1 | - Use different port for Postgres on Chainlink container (and perhaps do not expose to the outside). 2 | - Switch from geth on host to geth in a docker container (and perhaps let the docker containers communicate on their own network). 3 | -------------------------------------------------------------------------------- /hsck/job_id.py: -------------------------------------------------------------------------------- 1 | CHAINLINK_API_EXAMPLE_JOB_ID = '45160287-79df-4f39-b69a-285630288687'.replace("-", "") 2 | CHAINLINK_CRYPTO_COMPARE_JOB_ID = 'ab207904-fb8a-4059-93eb-35c10aa38526'.replace("-", "") 3 | CHAINLINK_MULTI_WORD_JOB_ID = '0eec7e1d-d0d2-476c-a1a8-72dfb6633f47'.replace("-", "") 4 | -------------------------------------------------------------------------------- /hsck/crypto_compare_ea/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from hsck.crypto_compare_ea.adapter import Adapter 4 | 5 | 6 | def lambda_handler(event, context): 7 | adapter = Adapter(event) 8 | return adapter.result 9 | 10 | 11 | def gcs_handler(request): 12 | adapter = Adapter(request.json) 13 | return json.dumps(adapter.result) 14 | -------------------------------------------------------------------------------- /avalanche_scripts/check_c_chain_balance.sh: -------------------------------------------------------------------------------- 1 | curl --location --request POST 'localhost:9650/ext/bc/C/rpc' \ 2 | --header 'Content-Type: application/json' \ 3 | --data-raw '{ 4 | "jsonrpc": "2.0", 5 | "method": "eth_getBalance", 6 | "params": [ 7 | "0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC", 8 | "latest" 9 | ], 10 | "id": 1 11 | }' 12 | -------------------------------------------------------------------------------- /avalanche_scripts/check_x_chain_balance.sh: -------------------------------------------------------------------------------- 1 | curl --location --request POST '127.0.0.1:9650/ext/bc/X' \ 2 | --header 'Content-Type: application/json' \ 3 | --data-raw '{ 4 | "jsonrpc":"2.0", 5 | "id" : 1, 6 | "method" :"avm.getBalance", 7 | "params" :{ 8 | "address":"X-local18jma8ppw3nhx5r4ap8clazz0dps7rv5u00z96u", 9 | "assetID": "AVAX" 10 | } 11 | } ' -------------------------------------------------------------------------------- /scripts/oracle_example/read_crypto_compare_ea_result.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import ( 4 | ATestnetConsumer, 5 | accounts, 6 | network, 7 | config 8 | ) 9 | 10 | 11 | def main(): 12 | testnet_consumer_contract = ATestnetConsumer[-1] 13 | 14 | current_ether_price = testnet_consumer_contract.currentPriceViaExternalAdapter() 15 | 16 | print(f'Current Ether price returned by Chainlink node = {current_ether_price}') 17 | -------------------------------------------------------------------------------- /brownie_scripts/add_networks.sh: -------------------------------------------------------------------------------- 1 | brownie networks add Avalanche avax-avash host=http://127.0.0.1:9650/ext/bc/C/rpc chainid=43112 explorer=https://cchain.explorer.avax.network/ 2 | brownie networks add Ethereum binance-smart-chain host=https://bsc-dataseed1.binance.org chainid=56 3 | brownie networks add Ethereum mumbai host=https://rpc-mumbai.matic.today chainid=80001 explorer=https://mumbai-explorer.matic.today 4 | brownie networks add Ethereum local host=http://localhost:8545 chainid=1337 -------------------------------------------------------------------------------- /scripts/infrastructure/deploy_oracle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import LinkToken, Oracle, accounts, network, config 4 | 5 | 6 | def main(): 7 | dev = accounts.add(config['wallets']['dev_account_0']) 8 | 9 | link_token_address = LinkToken[-1].address 10 | print(f'Using LINK token contract at {link_token_address}') 11 | 12 | oracle_contract = Oracle.deploy(link_token_address, {'from': dev}) 13 | print(f'Oracle contract deployed at {oracle_contract.address}') 14 | -------------------------------------------------------------------------------- /scripts/infrastructure/deploy_operator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import LinkToken, Operator, accounts, network, config 4 | 5 | 6 | def main(): 7 | dev = accounts.add(config['wallets']['dev_account_0']) 8 | 9 | link_token_address = LinkToken[-1].address 10 | print(f'Using LINK token contract at {link_token_address}') 11 | 12 | oracle_contract = Operator.deploy(link_token_address, dev.address, {'from': dev}) 13 | print(f'Oracle contract deployed at {oracle_contract.address}') 14 | -------------------------------------------------------------------------------- /scripts/oracle_example/read_testnet_consumer_result.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import ( 4 | ATestnetConsumer, 5 | accounts, 6 | network, 7 | config 8 | ) 9 | 10 | 11 | def main(): 12 | dev = accounts.add(config['wallets']['dev_account_0']) 13 | testnet_consumer_contract = ATestnetConsumer[-1] 14 | 15 | # print(f'Making request to job id {job_id} from oracle contract deployed at {oracle_contract.address}') 16 | current_ether_price = testnet_consumer_contract.currentPrice() 17 | 18 | print(f'Current Ether price returned by Chainlink node = {current_ether_price}') 19 | -------------------------------------------------------------------------------- /geth_scripts/js/sendEth.js: -------------------------------------------------------------------------------- 1 | eth.sendTransaction({from:eth.coinbase, to:"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", value: web3.toWei(10000, "ether")}); 2 | eth.sendTransaction({from:eth.coinbase, to:"0x70997970c51812dc3a010c7d01b50e0d17dc79c8", value: web3.toWei(10000, "ether")}); 3 | eth.sendTransaction({from:eth.coinbase, to:"0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc", value: web3.toWei(10000, "ether")}); 4 | eth.sendTransaction({from:eth.coinbase, to:"0x90f79bf6eb2c4f870365e785982e1f101e93b906", value: web3.toWei(10000, "ether")}); 5 | eth.sendTransaction({from:eth.coinbase, to:"0x15d34aaf54267db7d7c367839aaf71a00a2c6a65", value: web3.toWei(10000, "ether")}); -------------------------------------------------------------------------------- /hsck/crypto_compare_ea/app.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | 3 | from hsck.crypto_compare_ea.adapter import Adapter 4 | from hsck.ports import CRYPTO_COMPARE_EXTERNAL_ADAPTER_PORT 5 | 6 | app = Flask(__name__) 7 | 8 | 9 | @app.before_request 10 | def log_request_info(): 11 | app.logger.debug('Headers: %s', request.headers) 12 | app.logger.debug('Body: %s', request.get_data()) 13 | 14 | 15 | @app.route('/', methods=['POST']) 16 | def call_adapter(): 17 | data = request.get_json() 18 | if data == '': 19 | data = {} 20 | adapter = Adapter(data) 21 | return jsonify(adapter.result) 22 | 23 | 24 | if __name__ == '__main__': 25 | app.run(debug=True, host='0.0.0.0', port=CRYPTO_COMPARE_EXTERNAL_ADAPTER_PORT, threaded=True) 26 | -------------------------------------------------------------------------------- /scripts/oracle_example/make_crypto_compare_ea_request.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import ( 4 | ATestnetConsumer, 5 | LinkToken, 6 | Oracle, 7 | accounts, 8 | network, 9 | config 10 | ) 11 | 12 | from hsck.job_id import CHAINLINK_CRYPTO_COMPARE_JOB_ID 13 | 14 | 15 | def main(): 16 | dev = accounts.add(config['wallets']['dev_account_0']) 17 | oracle_contract = Oracle[-1] 18 | job_id = CHAINLINK_CRYPTO_COMPARE_JOB_ID 19 | 20 | testnet_consumer_contract = ATestnetConsumer[-1] 21 | 22 | print(f'Making request to job id {job_id} from oracle contract deployed at {oracle_contract.address}') 23 | result = testnet_consumer_contract.requestEthereumPriceViaExternalAdapter(oracle_contract.address, job_id, {'from': dev}) 24 | 25 | print(result) 26 | -------------------------------------------------------------------------------- /scripts/oracle_example/make_testnet_consumer_request.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import ( 4 | ATestnetConsumer, 5 | LinkToken, 6 | Oracle, 7 | accounts, 8 | network, 9 | config 10 | ) 11 | 12 | from hsck.job_id import CHAINLINK_API_EXAMPLE_JOB_ID 13 | 14 | 15 | def main(): 16 | dev = accounts.add(config['wallets']['dev_account_0']) 17 | oracle_contract = Oracle[-1] 18 | job_id = CHAINLINK_API_EXAMPLE_JOB_ID 19 | 20 | testnet_consumer_contract = ATestnetConsumer[-1] 21 | 22 | print(f'Making request to job id {job_id} at oracle contract deployed at {oracle_contract.address} from a testnet consumer contract deployed at {testnet_consumer_contract.address}') 23 | result = testnet_consumer_contract.requestEthereumPrice(oracle_contract.address, job_id, {'from': dev}) 24 | 25 | print(result) 26 | -------------------------------------------------------------------------------- /scripts/oracle_example/read_multi_word_consumer_result.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import ( 4 | MultiWordConsumer, 5 | accounts, 6 | network, 7 | config 8 | ) 9 | 10 | 11 | def main(): 12 | dev = accounts.add(config['wallets']['dev_account_0']) 13 | multi_word_consumer_contract = MultiWordConsumer[-1] 14 | 15 | current_ether_price_in_usd = multi_word_consumer_contract.usd() 16 | current_ether_price_in_eur = multi_word_consumer_contract.eur() 17 | current_ether_price_in_jpy = multi_word_consumer_contract.jpy() 18 | 19 | print(f'Current Ether price in USD returned by Chainlink node = {current_ether_price_in_usd}') 20 | print(f'Current Ether price in EUR returned by Chainlink node = {current_ether_price_in_eur}') 21 | print(f'Current Ether price in JPY returned by Chainlink node = {current_ether_price_in_jpy}') 22 | -------------------------------------------------------------------------------- /scripts/oracle_example/deploy_testnet_consumer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import ( 4 | ATestnetConsumer, 5 | LinkToken, 6 | accounts, 7 | network, 8 | config 9 | ) 10 | 11 | 12 | def main(): 13 | dev = accounts.add(config['wallets']['dev_account_0']) 14 | link_token_contract = LinkToken[-1] 15 | 16 | gas_price = config['networks'][network.show_active()].get('gas_price') 17 | print(f'{gas_price=}') 18 | 19 | tx_dict = { 20 | 'from': dev, 21 | 'gas_price': gas_price 22 | } 23 | 24 | testnet_consumer_contract = ATestnetConsumer.deploy(link_token_contract.address, tx_dict) 25 | 26 | testnet_consumer_contract = ATestnetConsumer[-1] 27 | print(f'Transferring LINK to ATestnetConsumer contract at {testnet_consumer_contract.address}') 28 | link_token_contract.transfer(testnet_consumer_contract.address, 1 * 10 ** 24, tx_dict) 29 | -------------------------------------------------------------------------------- /scripts/oracle_example/make_multi_word_consumer_request.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import ( 4 | MultiWordConsumer, 5 | Operator, 6 | accounts, 7 | network, 8 | config 9 | ) 10 | 11 | from hsck.job_id import CHAINLINK_MULTI_WORD_JOB_ID 12 | 13 | 14 | def main(): 15 | dev = accounts.add(config['wallets']['dev_account_0']) 16 | operator_contract = Operator[-1] 17 | job_id = bytes(CHAINLINK_MULTI_WORD_JOB_ID, 'utf-8') 18 | 19 | print(f'{job_id=}') 20 | 21 | multi_word_consumer_contract = MultiWordConsumer[-1] 22 | 23 | payment = 1 * 10**18 24 | print(f'Making request to job id {job_id} at operator contract deployed at ' 25 | f'{operator_contract.address} from a multi word consumer contract deployed at ' 26 | f'{multi_word_consumer_contract.address}') 27 | result = multi_word_consumer_contract.requestMultipleParameters(job_id, payment, {'from': dev}) 28 | 29 | print(result) 30 | -------------------------------------------------------------------------------- /scripts/infrastructure/send_avax.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import accounts, network, config 4 | 5 | 6 | def main(): 7 | print(f'{network.is_connected()}') 8 | print(f'{network.show_active()=}') 9 | 10 | if network.show_active() != 'avax-avash': 11 | raise ValueError("Wrong network. Please run with option '--network avax-avash'!") 12 | 13 | 14 | dev = accounts.add(config['networks']['avax-avash']['ava_dev_account']) 15 | balance = dev.balance() / 10**18 16 | print(f'{balance=}') 17 | 18 | receiving_addresses = ( 19 | "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", 20 | "0x70997970c51812dc3a010c7d01b50e0d17dc79c8", 21 | "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc", 22 | "0x90f79bf6eb2c4f870365e785982e1f101e93b906", 23 | "0x15d34aaf54267db7d7c367839aaf71a00a2c6a65" 24 | ) 25 | 26 | for receiving_address in receiving_addresses: 27 | dev.transfer(receiving_address, "10000 ether") 28 | -------------------------------------------------------------------------------- /scripts/infrastructure/fund_chainlink_account.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import LinkToken, Oracle, accounts, network, config 4 | 5 | 6 | def main(): 7 | dev = accounts.add(config['wallets']['dev_account_0']) 8 | 9 | if network.show_active() == 'avax-avash': 10 | chainlink_node_account_address_env_name = 'AVALANCHE_CHAINLINK_NODE_ACCOUNT_ADDRESS' 11 | else: 12 | chainlink_node_account_address_env_name = 'GETH_CHAINLINK_NODE_ACCOUNT_ADDRESS' 13 | 14 | chainlink_node_account_address = os.getenv(chainlink_node_account_address_env_name) 15 | if chainlink_node_account_address is None: 16 | raise ValueError(f'Environment variable {chainlink_node_account_address_env_name} not set.') 17 | 18 | print(f'Funding Chainlink node account at {chainlink_node_account_address}') 19 | 20 | dev.transfer(chainlink_node_account_address, "1000 ether") 21 | 22 | # Set fulfillment permission 23 | oracle_contract = Oracle[-1] 24 | oracle_contract.setFulfillmentPermission(chainlink_node_account_address, True, {'from': dev}) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Michael Nowotny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/infrastructure/print_contract_addresses.py: -------------------------------------------------------------------------------- 1 | import os 2 | from brownie import LinkToken, Oracle, Operator, ATestnetConsumer, accounts, network, config 3 | 4 | 5 | def main(): 6 | if len(LinkToken) > 0: 7 | print(f'LINK token contract deployed at {LinkToken[-1].address}') 8 | else: 9 | print('LINK token contract has not been deployed yet') 10 | 11 | if len(Oracle) > 0: 12 | print(f'Oracle contract deployed at {Oracle[-1].address}') 13 | else: 14 | print('Oracle contract has not been deployed yet') 15 | 16 | if len(Operator) > 0: 17 | print(f'Operator contract deployed at {Operator[-1].address}') 18 | else: 19 | print('Operator contract has not been deployed yet') 20 | 21 | if len(ATestnetConsumer) > 0: 22 | print(f'ATestnetConsumer contract deployed at {ATestnetConsumer[-1].address}') 23 | 24 | if len(LinkToken) > 0: 25 | link_balance = LinkToken[-1].balanceOf(ATestnetConsumer[-1].address) 26 | print(f'ATestnetConsumer contract has {link_balance} LINK tokens') 27 | else: 28 | print('ATestnetConsumer contract has not been deployed yet') 29 | -------------------------------------------------------------------------------- /avalanche_scripts/create_account.sh: -------------------------------------------------------------------------------- 1 | curl --location --request POST '127.0.0.1:9650/ext/keystore' \ 2 | --header 'Content-Type: application/json' \ 3 | --data-raw '{ 4 | "jsonrpc":"2.0", 5 | "id" :1, 6 | "method" :"keystore.createUser", 7 | "params" :{ 8 | "username": "Avalanche", 9 | "password": "Ava-123;X5" 10 | } 11 | }' 12 | 13 | curl --location --request POST '127.0.0.1:9650/ext/bc/X' \ 14 | --header 'Content-Type: application/json' \ 15 | --data-raw '{ 16 | "jsonrpc":"2.0", 17 | "id" :1, 18 | "method" :"avm.importKey", 19 | "params" :{ 20 | "username": "Avalanche", 21 | "password": "Ava-123;X5", 22 | "privateKey":"PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN" 23 | } 24 | }' 25 | 26 | curl --location --request POST '127.0.0.1:9650/ext/bc/C/avax' \ 27 | --header 'Content-Type: application/json' \ 28 | --data-raw '{ 29 | "method": "avax.importKey", 30 | "params": { 31 | "username": "Avalanche", 32 | "password": "Ava-123;X5", 33 | "privateKey":"PrivateKey-ewoqjP7PxY4yr3iLTpLisriqt94hdyDFNgchSxGGztUrTXtNN" 34 | }, 35 | "jsonrpc": "2.0", 36 | "id": 1 37 | }' 38 | -------------------------------------------------------------------------------- /hsck/crypto_compare_ea/bridge.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests.adapters import HTTPAdapter 3 | from requests.packages.urllib3.util.retry import Retry 4 | 5 | 6 | class Bridge(object): 7 | 8 | def __init__( 9 | self, 10 | retries=3, 11 | backoff_factor=0.3, 12 | status_forcelist=(500, 502, 504), 13 | ): 14 | self.session = requests.Session() 15 | retry = Retry( 16 | total=retries, 17 | read=retries, 18 | connect=retries, 19 | backoff_factor=backoff_factor, 20 | status_forcelist=status_forcelist, 21 | ) 22 | adapter = HTTPAdapter(max_retries=retry) 23 | self.session.mount('http://', adapter) 24 | self.session.mount('https://', adapter) 25 | 26 | def request(self, url, params={}, headers={}, timeout=15): 27 | try: 28 | return self.session.get(url, 29 | params=params, 30 | headers=headers, 31 | timeout=timeout) 32 | except Exception as e: 33 | raise e 34 | 35 | def close(self): 36 | self.session.close() 37 | -------------------------------------------------------------------------------- /hsck/test/test_adapter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from hsck.crypto_compare_ea import adapter 3 | 4 | job_run_id = '1' 5 | 6 | 7 | def adapter_setup(test_data): 8 | a = adapter.Adapter(test_data) 9 | return a.result 10 | 11 | 12 | @pytest.mark.parametrize('test_data', [ 13 | {'id': job_run_id, 'data': {'base': 'ETH', 'quote': 'USD'}}, 14 | {'id': job_run_id, 'data': {'from': 'ETH', 'to': 'USD'}}, 15 | {'id': job_run_id, 'data': {'coin': 'ETH', 'market': 'USD'}}, 16 | ]) 17 | def test_create_request_success(test_data): 18 | result = adapter_setup(test_data) 19 | print(result) 20 | assert result['statusCode'] == 200 21 | assert result['jobRunID'] == job_run_id 22 | assert result['data'] is not None 23 | assert type(result['result']) is float 24 | assert type(result['data']['result']) is float 25 | 26 | 27 | @pytest.mark.parametrize('test_data', [ 28 | {'id': job_run_id, 'data': {}}, 29 | {'id': job_run_id, 'data': {'from': 'does_not_exist', 'to': 'USD'}}, 30 | {}, 31 | ]) 32 | def test_create_request_error(test_data): 33 | result = adapter_setup(test_data) 34 | print(result) 35 | assert result['statusCode'] == 500 36 | assert result['jobRunID'] == job_run_id 37 | assert result['status'] == 'errored' 38 | assert result['error'] is not None 39 | -------------------------------------------------------------------------------- /scripts/oracle_example/deploy_multi_word_consumer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import ( 4 | MultiWordConsumer, 5 | LinkToken, 6 | Operator, 7 | accounts, 8 | network, 9 | config 10 | ) 11 | 12 | 13 | def main(): 14 | dev = accounts.add(config['wallets']['dev_account_0']) 15 | operator_contract = Operator[-1] 16 | link_token_contract = LinkToken[-1] 17 | 18 | multi_word_consumer_contract = MultiWordConsumer.deploy( 19 | link_token_contract.address, 20 | operator_contract.address, 21 | {'from': dev} 22 | ) 23 | 24 | multi_word_consumer_contract = MultiWordConsumer[-1] 25 | print(f'Transferring LINK to MultiWordConsumer contract at {multi_word_consumer_contract.address}') 26 | link_token_contract.transfer(multi_word_consumer_contract.address, 1 * 10 ** 24, {'from': dev}) 27 | 28 | if network.show_active() == 'avax-avash': 29 | chainlink_node_account_address_env_name = 'AVALANCHE_CHAINLINK_NODE_ACCOUNT_ADDRESS' 30 | else: 31 | chainlink_node_account_address_env_name = 'GETH_CHAINLINK_NODE_ACCOUNT_ADDRESS' 32 | 33 | chainlink_node_account_address = os.getenv(chainlink_node_account_address_env_name) 34 | if chainlink_node_account_address is None: 35 | raise ValueError(f'Environment variable {chainlink_node_account_address_env_name} not set.') 36 | 37 | operator_contract.setAuthorizedSenders([chainlink_node_account_address], {'from': dev}) 38 | -------------------------------------------------------------------------------- /scripts/infrastructure/deploy_link_token.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | from brownie import LinkToken, accounts, network, config 4 | 5 | 6 | def main(): 7 | dev = accounts.add(config['wallets']['dev_account_0']) 8 | print(f'{network.show_active()=}') 9 | 10 | link_token_contract = LinkToken.deploy({'from': dev}) 11 | print(f'LINK token contract deployed at {link_token_contract.address}') 12 | 13 | chain_id = network.chain.id 14 | print(f'{chain_id=}') 15 | 16 | if network.show_active() == 'avax-avash': 17 | ws_url = 'ws://host.docker.internal:9650/ext/bc/C/ws' # see here: https://docs.avax.network/build/avalanchego-apis/contract-chain-c-chain-api 18 | else: 19 | ws_url = 'ws://host.docker.internal:8546' 20 | 21 | with open(os.path.join('chainlink', 'local.env'), "w") as f: 22 | f.write(f'ETH_URL={ws_url}\n') 23 | f.write('FEATURE_EXTERNAL_INITIATORS=TRUE\n') 24 | f.write('LOG_LEVEL=debug\n') 25 | f.write(f'ETH_CHAIN_ID={chain_id}\n') 26 | f.write('MIN_OUTGOING_CONFIRMATIONS=0\n') 27 | f.write(f'LINK_CONTRACT_ADDRESS={link_token_contract.address}\n') 28 | f.write('CHAINLINK_TLS_PORT=0\n') 29 | f.write('SECURE_COOKIES=false\n') 30 | f.write('ALLOW_ORIGINS=*\n') 31 | f.write('DATABASE_URL=postgresql://chainlink:crum-chum-hum@localhost:5432/chainlink?sslmode=disable\n') 32 | f.write('DATABASE_TIMEOUT=0\n') 33 | f.write('FEATURE_FLUX_MONITOR=true\n') 34 | f.write('MINIMUM_CONTRACT_PAYMENT_LINK_JUELS=100000000000000\n') 35 | f.write('CHAINLINK_DEV=true\n') 36 | f.write('GAS_UPDATER_ENABLED=true\n') 37 | -------------------------------------------------------------------------------- /chainlink/TestnetConsumerJob.toml: -------------------------------------------------------------------------------- 1 | type = "directrequest" 2 | schemaVersion = 1 3 | name = "Get > Uint256" 4 | contractAddress = "YOUR_ORACLE_CONTRACT_ADDRESS" 5 | maxTaskDuration = "0s" 6 | observationSource = """ 7 | decode_log [type=ethabidecodelog 8 | abi="OracleRequest(bytes32 indexed specId, address requester, bytes32 requestId, uint256 payment, address callbackAddr, bytes4 callbackFunctionId, uint256 cancelExpiration, uint256 dataVersion, bytes data)" 9 | data="$(jobRun.logData)" 10 | topics="$(jobRun.logTopics)"] 11 | 12 | decode_cbor [type=cborparse data="$(decode_log.data)"] 13 | fetch [type=http method=GET url="$(decode_cbor.get)"] 14 | parse [type=jsonparse path="$(decode_cbor.path)" data="$(fetch)"] 15 | multiply [type=multiply input="$(parse)" times=100] 16 | encode_data [type=ethabiencode abi="(uint256 value)" data="{ \\"value\\": $(multiply) }"] 17 | encode_tx [type=ethabiencode 18 | abi="fulfillOracleRequest(bytes32 requestId, uint256 payment, address callbackAddress, bytes4 callbackFunctionId, uint256 expiration, bytes32 data)" 19 | data="{\\"requestId\\": $(decode_log.requestId), \\"payment\\": $(decode_log.payment), \\"callbackAddress\\": $(decode_log.callbackAddr), \\"callbackFunctionId\\": $(decode_log.callbackFunctionId), \\"expiration\\": $(decode_log.cancelExpiration), \\"data\\": $(encode_data)}" 20 | ] 21 | submit_tx [type=ethtx to="YOUR_ORACLE_CONTRACT_ADDRESS" data="$(encode_tx)"] 22 | 23 | decode_log -> decode_cbor -> fetch -> parse -> multiply -> encode_data -> encode_tx -> submit_tx 24 | """ 25 | externalJobID = "45160287-79df-4f39-b69a-285630288687" -------------------------------------------------------------------------------- /chainlink/CryptoCompareJob.toml: -------------------------------------------------------------------------------- 1 | type = "directrequest" 2 | schemaVersion = 1 3 | name = "CryptoCompare" 4 | contractAddress = "YOUR_ORACLE_CONTRACT_ADDRESS" 5 | maxTaskDuration = "0s" 6 | observationSource = """ 7 | decode_log [type=ethabidecodelog 8 | abi="OracleRequest(bytes32 indexed specId, address requester, bytes32 requestId, uint256 payment, address callbackAddr, bytes4 callbackFunctionId, uint256 cancelExpiration, uint256 dataVersion, bytes data)" 9 | data="$(jobRun.logData)" 10 | topics="$(jobRun.logTopics)"] 11 | 12 | decode_cbor [type=cborparse data="$(decode_log.data)"] 13 | fetch [type="bridge" name="cryptocompare" requestData="{\\"data\\": { \\"from\\": \\"ETH\\", \\"to\\": \\"USD\\" }}"] 14 | parse [type=jsonparse path="$(decode_cbor.path)" data="$(fetch)"] 15 | multiply [type=multiply input="$(parse)" times=100] 16 | encode_data [type=ethabiencode abi="(uint256 value)" data="{ \\"value\\": $(multiply) }"] 17 | encode_tx [type=ethabiencode 18 | abi="fulfillOracleRequest(bytes32 requestId, uint256 payment, address callbackAddress, bytes4 callbackFunctionId, uint256 expiration, bytes32 data)" 19 | data="{\\"requestId\\": $(decode_log.requestId), \\"payment\\": $(decode_log.payment), \\"callbackAddress\\": $(decode_log.callbackAddr), \\"callbackFunctionId\\": $(decode_log.callbackFunctionId), \\"expiration\\": $(decode_log.cancelExpiration), \\"data\\": $(encode_data)}" 20 | ] 21 | submit_tx [type=ethtx to="YOUR_ORACLE_CONTRACT_ADDRESS" data="$(encode_tx)"] 22 | 23 | decode_log -> decode_cbor -> fetch -> parse -> multiply -> encode_data -> encode_tx -> submit_tx 24 | """ 25 | externalJobID = "ab207904-fb8a-4059-93eb-35c10aa38526" -------------------------------------------------------------------------------- /hsck/crypto_compare_ea/adapter.py: -------------------------------------------------------------------------------- 1 | from hsck.crypto_compare_ea.bridge import Bridge 2 | 3 | 4 | class Adapter: 5 | base_url = 'https://min-api.cryptocompare.com/data/price' 6 | from_params = ['base', 'from', 'coin'] 7 | to_params = ['quote', 'to', 'market'] 8 | 9 | def __init__(self, input): 10 | self.id = input.get('id', '1') 11 | self.request_data = input.get('data') 12 | if self.validate_request_data(): 13 | self.bridge = Bridge() 14 | self.set_params() 15 | self.create_request() 16 | else: 17 | self.result_error('No data provided') 18 | 19 | def validate_request_data(self): 20 | if self.request_data is None: 21 | return False 22 | if self.request_data == {}: 23 | return False 24 | return True 25 | 26 | def set_params(self): 27 | for param in self.from_params: 28 | self.from_param = self.request_data.get(param) 29 | if self.from_param is not None: 30 | break 31 | for param in self.to_params: 32 | self.to_param = self.request_data.get(param) 33 | if self.to_param is not None: 34 | break 35 | 36 | def create_request(self): 37 | try: 38 | params = { 39 | 'fsym': self.from_param, 40 | 'tsyms': self.to_param, 41 | } 42 | response = self.bridge.request(self.base_url, params) 43 | data = response.json() 44 | self.result = data[self.to_param] 45 | data['result'] = self.result 46 | self.result_success(data) 47 | except Exception as e: 48 | self.result_error(e) 49 | finally: 50 | self.bridge.close() 51 | 52 | def result_success(self, data): 53 | self.result = { 54 | 'jobRunID': self.id, 55 | 'data': data, 56 | 'result': self.result, 57 | 'statusCode': 200, 58 | } 59 | 60 | def result_error(self, error): 61 | self.result = { 62 | 'jobRunID': self.id, 63 | 'status': 'errored', 64 | 'error': f'There was an error: {error}', 65 | 'statusCode': 500, 66 | } 67 | -------------------------------------------------------------------------------- /chainlink/MultiWordConsumer.toml: -------------------------------------------------------------------------------- 1 | type = "directrequest" 2 | schemaVersion = 1 3 | name = "example eth request event spec" 4 | contractAddress = "YOUR_OPERATOR_CONTRACT_ADDRESS" 5 | externalJobID = "0eec7e1d-d0d2-476c-a1a8-72dfb6633f47" 6 | observationSource = """ 7 | decode_log [type="ethabidecodelog" 8 | abi="OracleRequest(bytes32 indexed specId, address requester, bytes32 requestId, uint256 payment, address callbackAddr, bytes4 callbackFunctionId, uint256 cancelExpiration, uint256 dataVersion, bytes data)" 9 | data="$(jobRun.logData)" 10 | topics="$(jobRun.logTopics)"] 11 | decode_cbor [type="cborparse" data="$(decode_log.data)"] 12 | 13 | decode_log -> decode_cbor 14 | 15 | decode_cbor -> usd 16 | decode_cbor -> eur 17 | decode_cbor -> jpy 18 | 19 | usd [type="http" method=GET url="$(decode_cbor.urlUSD)" allowunrestrictednetworkaccess="true"] 20 | usd_parse [type="jsonparse" path="$(decode_cbor.pathUSD)"] 21 | usd_multiply [type="multiply" value="$(usd_parse)", times="100"] 22 | usd -> usd_parse -> usd_multiply 23 | 24 | eur [type="http" method=GET url="$(decode_cbor.urlEUR)" allowunrestrictednetworkaccess="true"] 25 | eur_parse [type="jsonparse" path="$(decode_cbor.pathEUR)"] 26 | eur_multiply [type="multiply" value="$(eur_parse)", times="100"] 27 | eur -> eur_parse -> eur_multiply 28 | 29 | jpy [type="http" method=GET url="$(decode_cbor.urlJPY)" allowunrestrictednetworkaccess="true"] 30 | jpy_parse [type="jsonparse" path="$(decode_cbor.pathJPY)"] 31 | jpy_multiply [type="multiply" value="$(jpy_parse)", times="100"] 32 | jpy -> jpy_parse -> jpy_multiply 33 | 34 | usd_multiply -> encode_mwr 35 | eur_multiply -> encode_mwr 36 | jpy_multiply -> encode_mwr 37 | 38 | // MWR API does NOT auto populate the requestID. 39 | encode_mwr [type="ethabiencode" 40 | abi="(bytes32 requestId, uint256 usd, uint256 eur, uint256 jpy)" 41 | data="{\\"requestId\\": $(decode_log.requestId), \\"usd\\": $(usd_multiply), \\"eur\\": $(eur_multiply), \\"jpy\\": $(jpy_multiply)}" 42 | ] 43 | encode_tx [type="ethabiencode" 44 | abi="fulfillOracleRequest2(bytes32 requestId, uint256 payment, address callbackAddress, bytes4 callbackFunctionId, uint256 expiration, bytes calldata data)" 45 | data="{\\"requestId\\": $(decode_log.requestId), \\"payment\\": $(decode_log.payment), \\"callbackAddress\\": $(decode_log.callbackAddr), \\"callbackFunctionId\\": $(decode_log.callbackFunctionId), \\"expiration\\": $(decode_log.cancelExpiration), \\"data\\": $(encode_mwr)}" 46 | ] 47 | submit_tx [type="ethtx" to="YOUR_OPERATOR_CONTRACT_ADDRESS" data="$(encode_tx)" minConfirmations="2"] 48 | 49 | encode_mwr -> encode_tx -> submit_tx 50 | """ 51 | -------------------------------------------------------------------------------- /contracts/MultiWordConsumer.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; 5 | 6 | /** 7 | * Request testnet LINK and ETH here: https://faucets.chain.link/ 8 | * Find information on LINK Token Contracts and get the latest ETH and LINK faucets here: https://docs.chain.link/docs/link-token-contracts/ 9 | */ 10 | 11 | /** 12 | * @notice DO NOT USE THIS CODE IN PRODUCTION. This is an example contract. 13 | */ 14 | contract MultiWordConsumer is ChainlinkClient { 15 | using Chainlink for Chainlink.Request; 16 | 17 | // variable bytes returned in a signle oracle response 18 | bytes public data; 19 | 20 | // multiple params returned in a single oracle response 21 | uint256 public usd; 22 | uint256 public eur; 23 | uint256 public jpy; 24 | 25 | /** 26 | * @notice Initialize the link token and target oracle 27 | * @dev The oracle address must be an Operator contract for multiword response 28 | */ 29 | constructor( 30 | address link, 31 | address oracle 32 | ) { 33 | setChainlinkToken(link); 34 | setChainlinkOracle(oracle); 35 | } 36 | 37 | 38 | /** 39 | * @notice Request mutiple parameters from the oracle in a single transaction 40 | * @param specId bytes32 representation of the jobId in the Oracle 41 | * @param payment uint256 cost of request in LINK (JUELS) 42 | */ 43 | function requestMultipleParameters( 44 | bytes32 specId, 45 | uint256 payment 46 | ) 47 | public 48 | { 49 | Chainlink.Request memory req = buildChainlinkRequest(specId, address(this), this.fulfillMultipleParameters.selector); 50 | // https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD 51 | req.add("urlUSD", "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD"); 52 | req.add("pathUSD", "USD"); 53 | req.add("urlEUR", "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=EUR"); 54 | req.add("pathEUR", "EUR"); 55 | req.add("urlJPY", "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=JPY"); 56 | req.add("pathJPY", "JPY"); 57 | 58 | // req.add("urlUSD", "https://datafeed.xyz/ethusd"); 59 | // req.add("pathUSD", "data"); 60 | // req.add("urlEUR", "https://datafeed.xyz/etheur"); 61 | // req.add("pathEUR", "data"); 62 | // req.add("urlJPY", "https://datafeed.xyz/ethjpy"); 63 | // req.add("pathJPY", "data"); 64 | req.addUint("times", 10000); 65 | requestOracleData(req, payment); 66 | } 67 | 68 | event RequestMultipleFulfilled( 69 | bytes32 indexed requestId, 70 | uint256 indexed usd, 71 | uint256 indexed eur, 72 | uint256 jpy 73 | ); 74 | 75 | /** 76 | * @notice Fulfillment function for multiple parameters in a single request 77 | * @dev This is called by the oracle. recordChainlinkFulfillment must be used. 78 | */ 79 | function fulfillMultipleParameters( 80 | bytes32 requestId, 81 | uint256 usdResponse, 82 | uint256 eurResponse, 83 | uint256 jpyResponse 84 | ) 85 | public 86 | recordChainlinkFulfillment(requestId) 87 | { 88 | emit RequestMultipleFulfilled(requestId, usdResponse, eurResponse, jpyResponse); 89 | usd = usdResponse; 90 | eur = eurResponse; 91 | jpy = jpyResponse; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /chainlink/chainlink_dockerfile: -------------------------------------------------------------------------------- 1 | # Use Ubuntu 20.04 LTS 2 | FROM ubuntu:20.04 3 | 4 | # Silence PostgreSQL installer 5 | ARG DEBIAN_FRONTEND=noninteractive 6 | 7 | # Update the distribution to the latest patches and install some essentials 8 | RUN apt-get update && apt-get install -y \ 9 | build-essential 10 | 11 | RUN apt-get update && apt-get install -y \ 12 | xz-utils \ 13 | build-essential \ 14 | wget \ 15 | curl \ 16 | gnupg \ 17 | git \ 18 | vim 19 | 20 | # Add nodesource to the available sources 21 | RUN curl -fsSL https://deb.nodesource.com/setup_12.x | bash - 22 | 23 | # The actual installation 24 | RUN apt-get install -y nodejs 25 | 26 | # Install Go 27 | RUN wget -c https://golang.org/dl/go1.17.linux-amd64.tar.gz -O - | tar -xz -C /usr/local 28 | env PATH=$PATH:/usr/local/go/bin 29 | 30 | # Install Yarn 31 | RUN npm install -g yarn 32 | 33 | # Add the PostgreSQL PGP key to verify their Debian packages. 34 | # It should be the same key as https://www.postgresql.org/media/keys/ACCC4CF8.asc 35 | # RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 36 | RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 37 | 38 | # Add PostgreSQL's repository. It contains the most recent stable release 39 | # of PostgreSQL. 40 | RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ focal-pgdg main" > /etc/apt/sources.list.d/pgdg.list 41 | 42 | # Install ``python-software-properties``, ``software-properties-common`` and PostgreSQL 9.6 43 | # There are some warnings (in red) that show up during the build. You can hide 44 | # them by prefixing each apt-get statement with DEBIAN_FRONTEND=noninteractive 45 | # RUN apt-get update && apt-get install -y python-software-properties software-properties-common postgresql-11 postgresql-client-11 postgresql-contrib-11 46 | RUN apt-get update && apt-get install -y software-properties-common postgresql-11 postgresql-client-11 postgresql-contrib-11 47 | 48 | # Note: The official Debian and Ubuntu images automatically ``apt-get clean`` 49 | # after each ``apt-get`` 50 | 51 | # Run the rest of the commands as the ``postgres`` user created by the ``postgres-11`` package when it was ``apt-get installed`` 52 | USER postgres 53 | 54 | # Create a PostgreSQL role named ``docker`` with ``docker`` as the password and 55 | # then create a database `docker` owned by the ``docker`` role. 56 | # Note: here we use ``&&\`` to run commands one after the other - the ``\`` 57 | # allows the RUN command to span multiple lines. 58 | #RUN /etc/init.d/postgresql start &&\ 59 | # psql --command "CREATE USER chainlink WITH SUPERUSER PASSWORD 'crum-chum-hum';" &&\ 60 | # createdb -O chainlink chainlink 61 | 62 | RUN service postgresql start && \ 63 | psql --command "CREATE USER chainlink WITH SUPERUSER PASSWORD 'crum-chum-hum';" &&\ 64 | createdb -O chainlink chainlink 65 | 66 | # Adjust PostgreSQL configuration so that remote connections to the 67 | # database are possible. 68 | RUN echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/11/main/pg_hba.conf 69 | 70 | # And add ``listen_addresses`` to ``/etc/postgresql/11/main/postgresql.conf`` 71 | RUN echo "listen_addresses='*'" >> /etc/postgresql/11/main/postgresql.conf 72 | 73 | # Add VOLUMEs to allow backup of config, logs and databases 74 | VOLUME ["/etc/postgresql", "/var/log/postgresql", "/var/lib/postgresql"] 75 | 76 | # Run as root 77 | USER root 78 | 79 | # Expose the PostgreSQL port 80 | EXPOSE 5432 81 | 82 | # create API email and password files 83 | RUN mkdir /cla 84 | RUN echo "admin@example.com" > /cla/.api 85 | RUN echo "password" >> /cla/.api 86 | RUN echo "Crum-Chum-Hum-2000" > /cla/.password 87 | 88 | # RUN echo "cp /cla/.api /chainlink && cp /cla/.password /chainlink" > /cla/setup.sh 89 | # RUN chmod +x /cla/setup.sh 90 | 91 | # Start from a Bash promt 92 | ENTRYPOINT service postgresql start && bash 93 | # CMD service postgresql start && [ "/bin/bash" ] 94 | # CMD [ "/bin/bash" ] 95 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pip install twine 6 | 7 | import io 8 | import os 9 | import sys 10 | from shutil import rmtree 11 | 12 | from setuptools import ( 13 | find_packages, 14 | setup, 15 | Command 16 | ) 17 | 18 | # Package meta-data. 19 | NAME = 'hsck' 20 | DESCRIPTION = 'Hybrid Smart Contract Kit' 21 | URL = 'https://github.com/michaelnowotny/hybrid-smart-contract-kit' 22 | EMAIL = 'nowotnym@gmail.com' 23 | AUTHOR = 'Michael Christoph Nowotny' 24 | REQUIRES_PYTHON = '>=3.9.0' 25 | VERSION = "0.0.1" 26 | 27 | # What packages are required for this module to be executed? 28 | REQUIRED = [ 29 | "certifi", 30 | "chardet", 31 | "click", 32 | "eth-brownie", 33 | "flask", 34 | "idna", 35 | "itsdangerous", 36 | "jinja2", 37 | "markupsafe", 38 | "pytest", 39 | "requests", 40 | "urllib3", 41 | "web3", 42 | "werkzeug" 43 | ] 44 | 45 | # What packages are optional? 46 | EXTRAS = { 47 | # 'fancy feature': ['django'], 48 | } 49 | 50 | # The rest you shouldn't have to touch too much :) 51 | # ------------------------------------------------ 52 | # Except, perhaps the License and Trove Classifiers! 53 | # If you do change the License, remember to change the Trove Classifier for that! 54 | 55 | here = os.path.abspath(os.path.dirname(__file__)) 56 | 57 | # Import the README and use it as the long-description. 58 | # Note: this will only work if 'README.md' is present in your MANIFEST.in file! 59 | try: 60 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 61 | long_description = '\n' + f.read() 62 | except FileNotFoundError: 63 | long_description = DESCRIPTION 64 | 65 | # Load the package's __version__.py module as a dictionary. 66 | about = {} 67 | if not VERSION: 68 | project_slug = NAME.lower().replace("-", "_").replace(" ", "_") 69 | with open(os.path.join(here, project_slug, '__version__.py')) as f: 70 | exec(f.read(), about) 71 | else: 72 | about['__version__'] = VERSION 73 | 74 | 75 | class UploadCommand(Command): 76 | """Support setup.py upload.""" 77 | 78 | description = 'Build and publish the package.' 79 | user_options = [] 80 | 81 | @staticmethod 82 | def status(s): 83 | """Prints things in bold.""" 84 | print('\033[1m{0}\033[0m'.format(s)) 85 | 86 | def initialize_options(self): 87 | pass 88 | 89 | def finalize_options(self): 90 | pass 91 | 92 | def run(self): 93 | try: 94 | self.status('Removing previous builds…') 95 | rmtree(os.path.join(here, 'dist')) 96 | except OSError: 97 | pass 98 | 99 | self.status('Building Source and Wheel (universal) distribution…') 100 | os.system( 101 | '{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 102 | 103 | self.status('Uploading the package to PyPI via Twine…') 104 | os.system('twine upload dist/*') 105 | 106 | self.status('Pushing git tags…') 107 | os.system('git tag v{0}'.format(about['__version__'])) 108 | os.system('git push --tags') 109 | 110 | sys.exit() 111 | 112 | 113 | # Where the magic happens: 114 | setup( 115 | name=NAME, 116 | version=about['__version__'], 117 | description=DESCRIPTION, 118 | long_description=long_description, 119 | long_description_content_type='text/markdown', 120 | author=AUTHOR, 121 | author_email=EMAIL, 122 | python_requires=REQUIRES_PYTHON, 123 | url=URL, 124 | packages=find_packages(exclude=('examples', 'notebooks')), 125 | # If your package is a single module, use this instead of 'packages': 126 | # py_modules=['mypackage'], 127 | 128 | # entry_points={ 129 | # 'console_scripts': ['mycli=mymodule:cli'], 130 | # }, 131 | install_requires=REQUIRED, 132 | extras_require=EXTRAS, 133 | include_package_data=True, 134 | license='MIT', 135 | classifiers=[ 136 | # Trove classifiers 137 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 138 | 'License :: OSI Approved :: MIT License', 139 | 'Programming Language :: Python', 140 | 'Programming Language :: Python :: 3', 141 | 'Programming Language :: Python :: 3.6', 142 | 'Programming Language :: Python :: 3.7', 143 | 'Programming Language :: Python :: 3.8', 144 | 'Programming Language :: Python :: 3.9', 145 | 'Programming Language :: Python :: Implementation :: CPython', 146 | ], 147 | # $ setup.py publish support. 148 | cmdclass={ 149 | 'upload': UploadCommand, 150 | }, 151 | ) -------------------------------------------------------------------------------- /brownie-config.yaml: -------------------------------------------------------------------------------- 1 | # exclude SafeMath when calculating test coverage 2 | # https://eth-brownie.readthedocs.io/en/v1.10.3/config.html#exclude_paths 3 | reports: 4 | exclude_contracts: 5 | - SafeMath 6 | dependencies: 7 | - smartcontractkit/chainlink@1.0.0 8 | compiler: 9 | solc: 10 | remappings: 11 | - '@chainlink=smartcontractkit/chainlink@1.0.0' 12 | # automatically fetch contract sources from Etherscan 13 | autofetch_sources: True 14 | # set a custom mnemonic for the development network 15 | networks: 16 | default: local 17 | kovan: 18 | vrf_coordinator: '0xdD3782915140c8f3b190B5D67eAc6dc5760C46E9' 19 | link_token: '0xa36085F69e2889c224210F603D836748e7dC0088' 20 | keyhash: '0x6c3699283bda56ad74f6b855546325b68d482e983852a7a82979cc4807b641f4' 21 | fee: 100000000000000000 22 | eth_usd_price_feed: '0x9326BFA02ADD2366b30bacB125260Af641031331' 23 | rinkeby: 24 | vrf_coordinator: '0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B' 25 | link_token: '0x01be23585060835e02b77ef475b0cc51aa1e0709' 26 | keyhash: '0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311' 27 | fee: 100000000000000000 28 | eth_usd_price_feed: '0x8A753747A1Fa494EC906cE90E9f37563A8AF630e' 29 | avax-avash: 30 | gas_price: 225000000000 31 | ava_dev_account: '0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027' 32 | mumbai: 33 | vrf_coordinator: '0x8C7382F9D8f56b33781fE506E897a4F1e2d17255' 34 | link_token: '0x326C977E6efc84E512bB9C30f76E30c160eD06FB' 35 | keyhash: '0x6e75b569a01ef56d18cab6a8e71e6600d6ce853834d4a5748b720d06f878b3a4' 36 | fee: 100000000000000 37 | eth_usd_price_feed: '0x0715A7794a1dc8e42615F059dD6e406A6594651A' 38 | binance: 39 | # link_token: ?? 40 | eth_usd_price_feed: '0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e' 41 | binance-fork: 42 | eth_usd_price_feed: '0x9ef1B8c0E4F7dc8bF5719Ea496883DC6401d5b2e' 43 | mainnet-fork: 44 | eth_usd_price_feed: '0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419' 45 | matic-fork: 46 | eth_usd_price_feed: '0xF9680D99D6C9589e2a93a78A04A279e509205945' 47 | wallets: 48 | # Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 49 | dev_account_0: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' 50 | 51 | # Account #1: 0x70997970c51812dc3a010c7d01b50e0d17dc79c8 52 | dev_account_1: '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d' 53 | 54 | # Account #2: 0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc 55 | dev_account_2: '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a' 56 | 57 | # Account #3: 0x90f79bf6eb2c4f870365e785982e1f101e93b906 58 | dev_account_3: '0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6' 59 | 60 | # Account #4: 0x15d34aaf54267db7d7c367839aaf71a00a2c6a65 61 | dev_account_4: '0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a' 62 | 63 | # Account #5: 0x9965507d1a55bcc2695c58ba16fb37d819b0a4dc 64 | dev_account_5: '0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba' 65 | 66 | # Account #6: 0x976ea74026e726554db657fa54763abd0c3a0aa9 67 | dev_account_6: '0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e' 68 | 69 | # Account #7: 0x14dc79964da2c08b23698b3d3cc7ca32193d9955 70 | dev_account_7: '0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356' 71 | 72 | # Account #8: 0x23618e81e3f5cdf7f54c3d65f7fbc0abf5b21e8f 73 | dev_account_8: '0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97' 74 | 75 | # Account #9: 0xa0ee7a142d267c1f36714e4a8f75612f20a79720 76 | dev_account_9: '0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6' 77 | 78 | # Account #10: 0xbcd4042de499d14e55001ccbb24a551f3b954096 79 | dev_account_10: '0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897' 80 | 81 | # Account #11: 0x71be63f3384f5fb98995898a86b02fb2426c5788 82 | dev_account_11: '0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82' 83 | 84 | # Account #12: 0xfabb0ac9d68b0b445fb7357272ff202c5651694a 85 | dev_account_12: '0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1' 86 | 87 | # Account #13: 0x1cbd3b2770909d4e10f157cabc84c7264073c9ec 88 | dev_account_13: '0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd' 89 | 90 | # Account #14: 0xdf3e18d64bc6a983f673ab319ccae4f1a57c7097 91 | dev_account_14: '0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa' 92 | 93 | # Account #15: 0xcd3b766ccdd6ae721141f452c550ca635964ce71 94 | dev_account_15: '0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61' 95 | 96 | # Account #16: 0x2546bcd3c84621e976d8185a91a922ae77ecec30 97 | dev_account_16: '0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0' 98 | 99 | # Account #17: 0xbda5747bfd65f08deb54cb465eb87d40e51b197e 100 | dev_account_17: '0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd' 101 | 102 | # Account #18: 0xdd2fd4581271e230360230f9337d5c0430bf44c0 103 | dev_account_18: '0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0' 104 | 105 | # Account #19: 0x8626f6940e2eb28930efb4cef49b2d1f2c9c1199 106 | dev_account_19: '0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e' 107 | 108 | # could also do from_mnemonic, and you'd have to change the accounts.add to accounts.from_mnemonic 109 | -------------------------------------------------------------------------------- /contracts/ATestnetConsumer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.8.7; 3 | 4 | import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol"; 5 | import "@chainlink/contracts/src/v0.8/ConfirmedOwner.sol"; 6 | 7 | contract ATestnetConsumer is ChainlinkClient, ConfirmedOwner { 8 | using Chainlink for Chainlink.Request; 9 | 10 | uint256 constant private ORACLE_PAYMENT = 1 * LINK_DIVISIBILITY; 11 | uint256 public currentPrice; 12 | int256 public changeDay; 13 | bytes32 public lastMarket; 14 | uint256 public currentPriceViaExternalAdapter; 15 | 16 | event RequestEthereumPriceFulfilledViaExternalAdapter( 17 | bytes32 indexed requestId, 18 | uint256 indexed price 19 | ); 20 | 21 | event RequestEthereumPriceFulfilled( 22 | bytes32 indexed requestId, 23 | uint256 indexed price 24 | ); 25 | 26 | event RequestEthereumChangeFulfilled( 27 | bytes32 indexed requestId, 28 | int256 indexed change 29 | ); 30 | 31 | event RequestEthereumLastMarket( 32 | bytes32 indexed requestId, 33 | bytes32 indexed market 34 | ); 35 | 36 | constructor(address linkAddress) ConfirmedOwner(msg.sender){ 37 | setChainlinkToken(linkAddress); 38 | // setPublicChainlinkToken(); 39 | } 40 | 41 | function requestEthereumPriceViaExternalAdapter(address _oracle, string memory _jobId) 42 | public 43 | onlyOwner 44 | { 45 | Chainlink.Request memory req = buildChainlinkRequest(stringToBytes32(_jobId), address(this), this.fulfillEthereumPriceViaExternalAdapter.selector); 46 | req.add("path", "data,USD"); 47 | req.addInt("times", 100); 48 | sendChainlinkRequestTo(_oracle, req, ORACLE_PAYMENT); 49 | } 50 | 51 | function fulfillEthereumPriceViaExternalAdapter(bytes32 _requestId, uint256 _price) 52 | public 53 | recordChainlinkFulfillment(_requestId) 54 | { 55 | emit RequestEthereumPriceFulfilled(_requestId, _price); 56 | currentPriceViaExternalAdapter = _price; 57 | } 58 | 59 | function requestEthereumPrice(address _oracle, string memory _jobId) 60 | public 61 | onlyOwner 62 | { 63 | Chainlink.Request memory req = buildChainlinkRequest(stringToBytes32(_jobId), address(this), this.fulfillEthereumPrice.selector); 64 | req.add("get", "https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD"); 65 | req.add("path", "USD"); 66 | req.addInt("times", 100); 67 | sendChainlinkRequestTo(_oracle, req, ORACLE_PAYMENT); 68 | } 69 | 70 | function requestEthereumChange(address _oracle, string memory _jobId) 71 | public 72 | onlyOwner 73 | { 74 | Chainlink.Request memory req = buildChainlinkRequest(stringToBytes32(_jobId), address(this), this.fulfillEthereumChange.selector); 75 | req.add("get", "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD"); 76 | req.add("path", "RAW.ETH.USD.CHANGEPCTDAY"); 77 | req.addInt("times", 1000000000); 78 | sendChainlinkRequestTo(_oracle, req, ORACLE_PAYMENT); 79 | } 80 | 81 | function requestEthereumLastMarket(address _oracle, string memory _jobId) 82 | public 83 | onlyOwner 84 | { 85 | Chainlink.Request memory req = buildChainlinkRequest(stringToBytes32(_jobId), address(this), this.fulfillEthereumLastMarket.selector); 86 | req.add("get", "https://min-api.cryptocompare.com/data/pricemultifull?fsyms=ETH&tsyms=USD"); 87 | string[] memory path = new string[](4); 88 | path[0] = "RAW"; 89 | path[1] = "ETH"; 90 | path[2] = "USD"; 91 | path[3] = "LASTMARKET"; 92 | req.addStringArray("path", path); 93 | sendChainlinkRequestTo(_oracle, req, ORACLE_PAYMENT); 94 | } 95 | 96 | function fulfillEthereumPrice(bytes32 _requestId, uint256 _price) 97 | public 98 | recordChainlinkFulfillment(_requestId) 99 | { 100 | emit RequestEthereumPriceFulfilled(_requestId, _price); 101 | currentPrice = _price; 102 | } 103 | 104 | function fulfillEthereumChange(bytes32 _requestId, int256 _change) 105 | public 106 | recordChainlinkFulfillment(_requestId) 107 | { 108 | emit RequestEthereumChangeFulfilled(_requestId, _change); 109 | changeDay = _change; 110 | } 111 | 112 | function fulfillEthereumLastMarket(bytes32 _requestId, bytes32 _market) 113 | public 114 | recordChainlinkFulfillment(_requestId) 115 | { 116 | emit RequestEthereumLastMarket(_requestId, _market); 117 | lastMarket = _market; 118 | } 119 | 120 | function getChainlinkToken() public view returns (address) { 121 | return chainlinkTokenAddress(); 122 | } 123 | 124 | function withdrawLink() public onlyOwner { 125 | LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress()); 126 | require(link.transfer(msg.sender, link.balanceOf(address(this))), "Unable to transfer"); 127 | } 128 | 129 | function cancelRequest( 130 | bytes32 _requestId, 131 | uint256 _payment, 132 | bytes4 _callbackFunctionId, 133 | uint256 _expiration 134 | ) 135 | public 136 | onlyOwner 137 | { 138 | cancelChainlinkRequest(_requestId, _payment, _callbackFunctionId, _expiration); 139 | } 140 | 141 | function stringToBytes32(string memory source) private pure returns (bytes32 result) { 142 | bytes memory tempEmptyStringTest = bytes(source); 143 | if (tempEmptyStringTest.length == 0) { 144 | return 0x0; 145 | } 146 | 147 | assembly { // solhint-disable-line no-inline-assembly 148 | result := mload(add(source, 32)) 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /contracts/Oracle.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.6; 3 | 4 | import "@chainlink/contracts/src/v0.6/LinkTokenReceiver.sol"; 5 | import "@chainlink/contracts/src/v0.6/vendor/Ownable.sol"; 6 | import "@chainlink/contracts/src/v0.6/vendor/SafeMathChainlink.sol"; 7 | import "@chainlink/contracts/src/v0.6/interfaces/ChainlinkRequestInterface.sol"; 8 | import "@chainlink/contracts/src/v0.6/interfaces/OracleInterface.sol"; 9 | import "@chainlink/contracts/src/v0.6/interfaces/LinkTokenInterface.sol"; 10 | import "@chainlink/contracts/src/v0.6/interfaces/WithdrawalInterface.sol"; 11 | 12 | /** 13 | * @title The Chainlink Oracle contract 14 | * @notice Node operators can deploy this contract to fulfill requests sent to them 15 | */ 16 | contract Oracle is ChainlinkRequestInterface, OracleInterface, Ownable, LinkTokenReceiver, WithdrawalInterface { 17 | using SafeMathChainlink for uint256; 18 | 19 | uint256 constant public EXPIRY_TIME = 5 minutes; 20 | uint256 constant private MINIMUM_CONSUMER_GAS_LIMIT = 400000; 21 | // We initialize fields to 1 instead of 0 so that the first invocation 22 | // does not cost more gas. 23 | uint256 constant private ONE_FOR_CONSISTENT_GAS_COST = 1; 24 | 25 | LinkTokenInterface internal LinkToken; 26 | mapping(bytes32 => bytes32) private commitments; 27 | mapping(address => bool) private authorizedNodes; 28 | uint256 private withdrawableTokens = ONE_FOR_CONSISTENT_GAS_COST; 29 | 30 | event OracleRequest( 31 | bytes32 indexed specId, 32 | address requester, 33 | bytes32 requestId, 34 | uint256 payment, 35 | address callbackAddr, 36 | bytes4 callbackFunctionId, 37 | uint256 cancelExpiration, 38 | uint256 dataVersion, 39 | bytes data 40 | ); 41 | 42 | event CancelOracleRequest( 43 | bytes32 indexed requestId 44 | ); 45 | 46 | /** 47 | * @notice Deploy with the address of the LINK token 48 | * @dev Sets the LinkToken address for the imported LinkTokenInterface 49 | * @param _link The address of the LINK token 50 | */ 51 | constructor(address _link) 52 | public 53 | Ownable() 54 | { 55 | LinkToken = LinkTokenInterface(_link); // external but already deployed and unalterable 56 | } 57 | 58 | /** 59 | * @notice Creates the Chainlink request 60 | * @dev Stores the hash of the params as the on-chain commitment for the request. 61 | * Emits OracleRequest event for the Chainlink node to detect. 62 | * @param _sender The sender of the request 63 | * @param _payment The amount of payment given (specified in wei) 64 | * @param _specId The Job Specification ID 65 | * @param _callbackAddress The callback address for the response 66 | * @param _callbackFunctionId The callback function ID for the response 67 | * @param _nonce The nonce sent by the requester 68 | * @param _dataVersion The specified data version 69 | * @param _data The CBOR payload of the request 70 | */ 71 | function oracleRequest( 72 | address _sender, 73 | uint256 _payment, 74 | bytes32 _specId, 75 | address _callbackAddress, 76 | bytes4 _callbackFunctionId, 77 | uint256 _nonce, 78 | uint256 _dataVersion, 79 | bytes calldata _data 80 | ) 81 | external 82 | override 83 | onlyLINK() 84 | checkCallbackAddress(_callbackAddress) 85 | { 86 | bytes32 requestId = keccak256(abi.encodePacked(_sender, _nonce)); 87 | require(commitments[requestId] == 0, "Must use a unique ID"); 88 | // solhint-disable-next-line not-rely-on-time 89 | uint256 expiration = now.add(EXPIRY_TIME); 90 | 91 | commitments[requestId] = keccak256( 92 | abi.encodePacked( 93 | _payment, 94 | _callbackAddress, 95 | _callbackFunctionId, 96 | expiration 97 | ) 98 | ); 99 | 100 | emit OracleRequest( 101 | _specId, 102 | _sender, 103 | requestId, 104 | _payment, 105 | _callbackAddress, 106 | _callbackFunctionId, 107 | expiration, 108 | _dataVersion, 109 | _data); 110 | } 111 | 112 | /** 113 | * @notice Called by the Chainlink node to fulfill requests 114 | * @dev Given params must hash back to the commitment stored from `oracleRequest`. 115 | * Will call the callback address' callback function without bubbling up error 116 | * checking in a `require` so that the node can get paid. 117 | * @param _requestId The fulfillment request ID that must match the requester's 118 | * @param _payment The payment amount that will be released for the oracle (specified in wei) 119 | * @param _callbackAddress The callback address to call for fulfillment 120 | * @param _callbackFunctionId The callback function ID to use for fulfillment 121 | * @param _expiration The expiration that the node should respond by before the requester can cancel 122 | * @param _data The data to return to the consuming contract 123 | * @return Status if the external call was successful 124 | */ 125 | function fulfillOracleRequest( 126 | bytes32 _requestId, 127 | uint256 _payment, 128 | address _callbackAddress, 129 | bytes4 _callbackFunctionId, 130 | uint256 _expiration, 131 | bytes32 _data 132 | ) 133 | external 134 | onlyAuthorizedNode 135 | override 136 | isValidRequest(_requestId) 137 | returns (bool) 138 | { 139 | bytes32 paramsHash = keccak256( 140 | abi.encodePacked( 141 | _payment, 142 | _callbackAddress, 143 | _callbackFunctionId, 144 | _expiration 145 | ) 146 | ); 147 | require(commitments[_requestId] == paramsHash, "Params do not match request ID"); 148 | withdrawableTokens = withdrawableTokens.add(_payment); 149 | delete commitments[_requestId]; 150 | require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, "Must provide consumer enough gas"); 151 | // All updates to the oracle's fulfillment should come before calling the 152 | // callback(addr+functionId) as it is untrusted. 153 | // See: https://solidity.readthedocs.io/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern 154 | (bool success, ) = _callbackAddress.call(abi.encodeWithSelector(_callbackFunctionId, _requestId, _data)); // solhint-disable-line avoid-low-level-calls 155 | return success; 156 | } 157 | 158 | /** 159 | * @notice Use this to check if a node is authorized for fulfilling requests 160 | * @param _node The address of the Chainlink node 161 | * @return The authorization status of the node 162 | */ 163 | function getAuthorizationStatus(address _node) 164 | external 165 | view 166 | override 167 | returns (bool) 168 | { 169 | return authorizedNodes[_node]; 170 | } 171 | 172 | /** 173 | * @notice Sets the fulfillment permission for a given node. Use `true` to allow, `false` to disallow. 174 | * @param _node The address of the Chainlink node 175 | * @param _allowed Bool value to determine if the node can fulfill requests 176 | */ 177 | function setFulfillmentPermission(address _node, bool _allowed) 178 | external 179 | override 180 | onlyOwner() 181 | { 182 | authorizedNodes[_node] = _allowed; 183 | } 184 | 185 | /** 186 | * @notice Allows the node operator to withdraw earned LINK to a given address 187 | * @dev The owner of the contract can be another wallet and does not have to be a Chainlink node 188 | * @param _recipient The address to send the LINK token to 189 | * @param _amount The amount to send (specified in wei) 190 | */ 191 | function withdraw(address _recipient, uint256 _amount) 192 | external 193 | override(OracleInterface, WithdrawalInterface) 194 | onlyOwner 195 | hasAvailableFunds(_amount) 196 | { 197 | withdrawableTokens = withdrawableTokens.sub(_amount); 198 | assert(LinkToken.transfer(_recipient, _amount)); 199 | } 200 | 201 | /** 202 | * @notice Displays the amount of LINK that is available for the node operator to withdraw 203 | * @dev We use `ONE_FOR_CONSISTENT_GAS_COST` in place of 0 in storage 204 | * @return The amount of withdrawable LINK on the contract 205 | */ 206 | function withdrawable() 207 | external 208 | view 209 | override(OracleInterface, WithdrawalInterface) 210 | onlyOwner() 211 | returns (uint256) 212 | { 213 | return withdrawableTokens.sub(ONE_FOR_CONSISTENT_GAS_COST); 214 | } 215 | 216 | /** 217 | * @notice Allows requesters to cancel requests sent to this oracle contract. Will transfer the LINK 218 | * sent for the request back to the requester's address. 219 | * @dev Given params must hash to a commitment stored on the contract in order for the request to be valid 220 | * Emits CancelOracleRequest event. 221 | * @param _requestId The request ID 222 | * @param _payment The amount of payment given (specified in wei) 223 | * @param _callbackFunc The requester's specified callback address 224 | * @param _expiration The time of the expiration for the request 225 | */ 226 | function cancelOracleRequest( 227 | bytes32 _requestId, 228 | uint256 _payment, 229 | bytes4 _callbackFunc, 230 | uint256 _expiration 231 | ) 232 | external 233 | override 234 | { 235 | bytes32 paramsHash = keccak256( 236 | abi.encodePacked( 237 | _payment, 238 | msg.sender, 239 | _callbackFunc, 240 | _expiration) 241 | ); 242 | require(paramsHash == commitments[_requestId], "Params do not match request ID"); 243 | // solhint-disable-next-line not-rely-on-time 244 | require(_expiration <= now, "Request is not expired"); 245 | 246 | delete commitments[_requestId]; 247 | emit CancelOracleRequest(_requestId); 248 | 249 | assert(LinkToken.transfer(msg.sender, _payment)); 250 | } 251 | 252 | /** 253 | * @notice Returns the address of the LINK token 254 | * @dev This is the public implementation for chainlinkTokenAddress, which is 255 | * an internal method of the ChainlinkClient contract 256 | */ 257 | function getChainlinkToken() 258 | public 259 | view 260 | override 261 | returns (address) 262 | { 263 | return address(LinkToken); 264 | } 265 | 266 | // MODIFIERS 267 | 268 | /** 269 | * @dev Reverts if amount requested is greater than withdrawable balance 270 | * @param _amount The given amount to compare to `withdrawableTokens` 271 | */ 272 | modifier hasAvailableFunds(uint256 _amount) { 273 | require(withdrawableTokens >= _amount.add(ONE_FOR_CONSISTENT_GAS_COST), "Amount requested is greater than withdrawable balance"); 274 | _; 275 | } 276 | 277 | /** 278 | * @dev Reverts if request ID does not exist 279 | * @param _requestId The given request ID to check in stored `commitments` 280 | */ 281 | modifier isValidRequest(bytes32 _requestId) { 282 | require(commitments[_requestId] != 0, "Must have a valid requestId"); 283 | _; 284 | } 285 | 286 | /** 287 | * @dev Reverts if `msg.sender` is not authorized to fulfill requests 288 | */ 289 | modifier onlyAuthorizedNode() { 290 | require(authorizedNodes[msg.sender] || msg.sender == owner(), "Not an authorized node to fulfill requests"); 291 | _; 292 | } 293 | 294 | /** 295 | * @dev Reverts if the callback address is the LINK token 296 | * @param _to The callback address 297 | */ 298 | modifier checkCallbackAddress(address _to) { 299 | require(_to != address(LinkToken), "Cannot callback to LINK"); 300 | _; 301 | } 302 | 303 | } 304 | -------------------------------------------------------------------------------- /contracts/linkContract.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at polygonscan.com on 2021-07-12 3 | */ 4 | 5 | // File: https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.4/vendor/SafeMath.sol 6 | 7 | pragma solidity ^0.4.11; 8 | 9 | 10 | /** 11 | * @title SafeMath 12 | * @dev Math operations with safety checks that throw on error 13 | */ 14 | library SafeMath { 15 | 16 | /** 17 | * @dev Multiplies two numbers, throws on overflow. 18 | */ 19 | function mul(uint256 _a, uint256 _b) internal pure returns (uint256 c) { 20 | // Gas optimization: this is cheaper than asserting 'a' not being zero, but the 21 | // benefit is lost if 'b' is also tested. 22 | // See: https://github.com/OpenZeppelin/openzeppelin-solidity/pull/522 23 | if (_a == 0) { 24 | return 0; 25 | } 26 | 27 | c = _a * _b; 28 | assert(c / _a == _b); 29 | return c; 30 | } 31 | 32 | /** 33 | * @dev Integer division of two numbers, truncating the quotient. 34 | */ 35 | function div(uint256 _a, uint256 _b) internal pure returns (uint256) { 36 | // assert(_b > 0); // Solidity automatically throws when dividing by 0 37 | // uint256 c = _a / _b; 38 | // assert(_a == _b * c + _a % _b); // There is no case in which this doesn't hold 39 | return _a / _b; 40 | } 41 | 42 | /** 43 | * @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). 44 | */ 45 | function sub(uint256 _a, uint256 _b) internal pure returns (uint256) { 46 | assert(_b <= _a); 47 | return _a - _b; 48 | } 49 | 50 | /** 51 | * @dev Adds two numbers, throws on overflow. 52 | */ 53 | function add(uint256 _a, uint256 _b) internal pure returns (uint256 c) { 54 | c = _a + _b; 55 | assert(c >= _a); 56 | return c; 57 | } 58 | } 59 | 60 | // File: https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.4/vendor/BasicToken.sol 61 | 62 | 63 | /** 64 | * @title ERC20Basic 65 | * @dev Simpler version of ERC20 interface 66 | * @dev see https://github.com/ethereum/EIPs/issues/179 67 | */ 68 | contract ERC20Basic { 69 | uint256 public totalSupply; 70 | function balanceOf(address who) constant returns (uint256); 71 | function transfer(address to, uint256 value) returns (bool); 72 | event Transfer(address indexed from, address indexed to, uint256 value); 73 | } 74 | 75 | 76 | /** 77 | * @title Basic token 78 | * @dev Basic version of StandardToken, with no allowances. 79 | */ 80 | contract BasicToken is ERC20Basic { 81 | using SafeMath for uint256; 82 | 83 | mapping(address => uint256) balances; 84 | 85 | /** 86 | * @dev transfer token for a specified address 87 | * @param _to The address to transfer to. 88 | * @param _value The amount to be transferred. 89 | */ 90 | function transfer(address _to, uint256 _value) returns (bool) { 91 | balances[msg.sender] = balances[msg.sender].sub(_value); 92 | balances[_to] = balances[_to].add(_value); 93 | Transfer(msg.sender, _to, _value); 94 | return true; 95 | } 96 | 97 | /** 98 | * @dev Gets the balance of the specified address. 99 | * @param _owner The address to query the the balance of. 100 | * @return An uint256 representing the amount owned by the passed address. 101 | */ 102 | function balanceOf(address _owner) constant returns (uint256 balance) { 103 | return balances[_owner]; 104 | } 105 | 106 | } 107 | 108 | // File: https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.4/vendor/StandardToken.sol 109 | 110 | 111 | /** 112 | * @title ERC20 interface 113 | * @dev see https://github.com/ethereum/EIPs/issues/20 114 | */ 115 | contract ERC20 is ERC20Basic { 116 | function allowance(address owner, address spender) constant returns (uint256); 117 | function transferFrom(address from, address to, uint256 value) returns (bool); 118 | function approve(address spender, uint256 value) returns (bool); 119 | event Approval(address indexed owner, address indexed spender, uint256 value); 120 | } 121 | 122 | 123 | /** 124 | * @title Standard ERC20 token 125 | * 126 | * @dev Implementation of the basic standard token. 127 | * @dev https://github.com/ethereum/EIPs/issues/20 128 | * @dev Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol 129 | */ 130 | contract StandardToken is ERC20, BasicToken { 131 | 132 | mapping (address => mapping (address => uint256)) allowed; 133 | 134 | 135 | /** 136 | * @dev Transfer tokens from one address to another 137 | * @param _from address The address which you want to send tokens from 138 | * @param _to address The address which you want to transfer to 139 | * @param _value uint256 the amount of tokens to be transferred 140 | */ 141 | function transferFrom(address _from, address _to, uint256 _value) returns (bool) { 142 | var _allowance = allowed[_from][msg.sender]; 143 | 144 | // Check is not needed because sub(_allowance, _value) will already throw if this condition is not met 145 | // require (_value <= _allowance); 146 | 147 | balances[_from] = balances[_from].sub(_value); 148 | balances[_to] = balances[_to].add(_value); 149 | allowed[_from][msg.sender] = _allowance.sub(_value); 150 | Transfer(_from, _to, _value); 151 | return true; 152 | } 153 | 154 | /** 155 | * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 156 | * @param _spender The address which will spend the funds. 157 | * @param _value The amount of tokens to be spent. 158 | */ 159 | function approve(address _spender, uint256 _value) returns (bool) { 160 | allowed[msg.sender][_spender] = _value; 161 | Approval(msg.sender, _spender, _value); 162 | return true; 163 | } 164 | 165 | /** 166 | * @dev Function to check the amount of tokens that an owner allowed to a spender. 167 | * @param _owner address The address which owns the funds. 168 | * @param _spender address The address which will spend the funds. 169 | * @return A uint256 specifying the amount of tokens still available for the spender. 170 | */ 171 | function allowance(address _owner, address _spender) constant returns (uint256 remaining) { 172 | return allowed[_owner][_spender]; 173 | } 174 | 175 | /* 176 | * approve should be called when allowed[_spender] == 0. To increment 177 | * allowed value is better to use this function to avoid 2 calls (and wait until 178 | * the first transaction is mined) 179 | * From MonolithDAO Token.sol 180 | */ 181 | function increaseApproval (address _spender, uint _addedValue) 182 | returns (bool success) { 183 | allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue); 184 | Approval(msg.sender, _spender, allowed[msg.sender][_spender]); 185 | return true; 186 | } 187 | 188 | function decreaseApproval (address _spender, uint _subtractedValue) 189 | returns (bool success) { 190 | uint oldValue = allowed[msg.sender][_spender]; 191 | if (_subtractedValue > oldValue) { 192 | allowed[msg.sender][_spender] = 0; 193 | } else { 194 | allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue); 195 | } 196 | Approval(msg.sender, _spender, allowed[msg.sender][_spender]); 197 | return true; 198 | } 199 | 200 | } 201 | 202 | // File: https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.4/interfaces/ERC677Receiver.sol 203 | 204 | 205 | 206 | contract ERC677Receiver { 207 | function onTokenTransfer(address _sender, uint _value, bytes _data); 208 | } 209 | 210 | // File: https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.4/interfaces/ERC20Basic.sol 211 | 212 | // File: https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.4/interfaces/ERC20.sol 213 | 214 | 215 | // File: https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.4/interfaces/ERC677.sol 216 | 217 | 218 | 219 | contract ERC677 is ERC20 { 220 | function transferAndCall(address to, uint value, bytes data) returns (bool success); 221 | 222 | event Transfer(address indexed from, address indexed to, uint value, bytes data); 223 | } 224 | 225 | // File: https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.4/ERC677Token.sol 226 | 227 | 228 | 229 | 230 | 231 | contract ERC677Token is ERC677 { 232 | 233 | /** 234 | * @dev transfer token to a contract address with additional data if the recipient is a contact. 235 | * @param _to The address to transfer to. 236 | * @param _value The amount to be transferred. 237 | * @param _data The extra data to be passed to the receiving contract. 238 | */ 239 | function transferAndCall(address _to, uint _value, bytes _data) 240 | public 241 | returns (bool success) 242 | { 243 | super.transfer(_to, _value); 244 | Transfer(msg.sender, _to, _value, _data); 245 | if (isContract(_to)) { 246 | contractFallback(_to, _value, _data); 247 | } 248 | return true; 249 | } 250 | 251 | 252 | // PRIVATE 253 | 254 | function contractFallback(address _to, uint _value, bytes _data) 255 | private 256 | { 257 | ERC677Receiver receiver = ERC677Receiver(_to); 258 | receiver.onTokenTransfer(msg.sender, _value, _data); 259 | } 260 | 261 | function isContract(address _addr) 262 | private 263 | returns (bool hasCode) 264 | { 265 | uint length; 266 | assembly { length := extcodesize(_addr) } 267 | return length > 0; 268 | } 269 | 270 | } 271 | 272 | // File: https://github.com/smartcontractkit/chainlink/blob/develop/evm-contracts/src/v0.4/LinkToken.sol 273 | 274 | 275 | 276 | 277 | 278 | contract LinkToken is StandardToken, ERC677Token { 279 | 280 | uint public constant totalSupply = 10**27; 281 | string public constant name = 'ChainLink Token'; 282 | uint8 public constant decimals = 18; 283 | string public constant symbol = 'LINK'; 284 | 285 | function LinkToken() 286 | public 287 | { 288 | balances[msg.sender] = totalSupply; 289 | } 290 | 291 | /** 292 | * @dev transfer token to a specified address with additional data if the recipient is a contract. 293 | * @param _to The address to transfer to. 294 | * @param _value The amount to be transferred. 295 | * @param _data The extra data to be passed to the receiving contract. 296 | */ 297 | function transferAndCall(address _to, uint _value, bytes _data) 298 | public 299 | validRecipient(_to) 300 | returns (bool success) 301 | { 302 | return super.transferAndCall(_to, _value, _data); 303 | } 304 | 305 | /** 306 | * @dev transfer token to a specified address. 307 | * @param _to The address to transfer to. 308 | * @param _value The amount to be transferred. 309 | */ 310 | function transfer(address _to, uint _value) 311 | public 312 | validRecipient(_to) 313 | returns (bool success) 314 | { 315 | return super.transfer(_to, _value); 316 | } 317 | 318 | /** 319 | * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. 320 | * @param _spender The address which will spend the funds. 321 | * @param _value The amount of tokens to be spent. 322 | */ 323 | function approve(address _spender, uint256 _value) 324 | public 325 | validRecipient(_spender) 326 | returns (bool) 327 | { 328 | return super.approve(_spender, _value); 329 | } 330 | 331 | /** 332 | * @dev Transfer tokens from one address to another 333 | * @param _from address The address which you want to send tokens from 334 | * @param _to address The address which you want to transfer to 335 | * @param _value uint256 the amount of tokens to be transferred 336 | */ 337 | function transferFrom(address _from, address _to, uint256 _value) 338 | public 339 | validRecipient(_to) 340 | returns (bool) 341 | { 342 | return super.transferFrom(_from, _to, _value); 343 | } 344 | 345 | 346 | // MODIFIERS 347 | 348 | modifier validRecipient(address _recipient) { 349 | require(_recipient != address(0) && _recipient != address(this)); 350 | _; 351 | } 352 | 353 | } 354 | 355 | // File: browser/LinkToken.sol -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hybrid Smart Contract Kit 2 | A hybrid smart contract consists of on-chain and an off-chain component. 3 | The on-chain component is implemented in a smart contract language such as Solidity. 4 | The off-chain component can be a data oracle or a computation. 5 | The Chainlink framework enables both off-chain data acquisition and off-chain computation in a decentralized way. 6 | 7 | This repository demonstrates how to set up a local hybrid smart contract development environment and provides brief, working, illustrative examples from Chainlink's documentation to help developers get started developing hybrid smart contract without the quirks. 8 | Unless otherwise indicated, all commands are understood to be run from the project root directory. 9 | The instructions have been tested on macOS 11.6, Linux Mint 20.2, and Zorin OS 16. Windows is not supported. 10 | 11 | ## Clone Chainlink Repository 12 | - Define an environment variable `CHAINLINK_PATH` pointing to the directory the Chainlink repository will be cloned to. Ideally, this lies outside the directory to which `hybrid-smart-contract-kit` has been cloned. 13 | - Navigate to the parent directory of `CHAINLINK_PATH`. 14 | - Remember to restart your terminal or resource your environment file. 15 | - Run `git clone https://github.com/smartcontractkit/chainlink`. Here are official instructions for reference: `https://github.com/smartcontractkit/chainlink#install`. 16 | - Change into `chainlink` directory and run `git checkout master`. 17 | - Remember to navigate back to the `hybrid-smart-contract-kit` root directory. 18 | 19 | ## Define Local Avalanche Blockchain Storage Directory 20 | - Create an empty directory into which avalanchego should save the blockchain. 21 | - Store this directory in an environment variable called `AVALANCHE_DEV_DATA_PATH`. 22 | - Remember to restart your terminal or resource your environment file. 23 | 24 | ## Define Local Ethereum Blockchain Storage Directory 25 | - Create an empty directory into which geth should save the blockchain. 26 | - Store this directory in an environment variable called `GETH_DEV_DATA_PATH`. 27 | - Remember to restart your terminal or resource your environment file. 28 | 29 | ## Install Python Package in Editable Mode 30 | - Run `pip install -e .`. 31 | 32 | ## Add Networks to Brownie 33 | ### Avalanche 34 | - Run `brownie networks add Avalanche avax-avash host=http://127.0.0.1:9650/ext/bc/C/rpc chainid=43112 explorer=https://cchain.explorer.avax.network/`. 35 | 36 | ### Binance Smart Chain 37 | - Run `brownie networks add Ethereum binance-smart-chain host=https://bsc-dataseed1.binance.org chainid=56`. 38 | 39 | ### Polygon Mumbai Matic Testnet 40 | - Run `brownie networks add Ethereum mumbai host=https://rpc-mumbai.maticvigil.com/ chainid=80001 explorer=https://mumbai-explorer.matic.today`. 41 | 42 | ### Local Ethereum Blockchain Node 43 | - Run `brownie networks add Ethereum local host=http://localhost:8545 chainid=1337`. 44 | 45 | ## Install Avalanche Client Software 46 | - On Linux, follow the instructions at `https://github.com/ava-labs/avalanchego`, i.e. 47 | `sudo su -` 48 | `wget -O - https://downloads.avax.network/avalanchego.gpg.key | apt-key add -` 49 | `echo "deb https://downloads.avax.network/apt bionic main" > /etc/apt/sources.list.d/avalanche.list` 50 | `sudo apt update` 51 | `sudo apt install avalanchego` 52 | - On MacOs, download the binary from `https://github.com/ava-labs/avalanchego/releases/tag/v1.7.0` and install 53 | 54 | ## Install Ethereum Client Software 55 | - On Mac, via homebrew, run `brew install geth`. 56 | - On Linux, follow the instructions at `https://geth.ethereum.org/docs/install-and-build/installing-geth#install-on-ubuntu-via-ppas`. 57 | 58 | ## Start Local Ethereum Node 59 | - From the project's root directory, navigate to `geth_scripts` and run `./startGeth.sh`. 60 | 61 | ## Start Local AvalancheGo Node 62 | - From the project's root directory, navigate to `avalanche_scripts` and run `./start_avalanche.sh`. 63 | - To make sure that the node is running, execute `./status.sh`. 64 | 65 | ## Set Up Local Avalanche Test Account (Only Required the First Time on the Local Testnet) 66 | - The following steps are taken from these instructions: `https://docs.avax.network/build/tutorials/platform/fund-a-local-test-network` 67 | - Navigate to `avalanche_scripts`. 68 | - Run `./create_account.sh` to create a user `Avalanche` with password `Ava-123;X5`. 69 | - Run './check_x_chain_balance.sh' to check the balance on the X-Chain 70 | - Run './check_c_chain_balance.sh' to check the balance on the C-Chain 71 | 72 | ## Set Up MetaMask 73 | ### Connect MetaMask Wallet to Polygon Mumbai Matic Testnet ### 74 | - Network Name: "Mumbai Matic" 75 | - New RPC URL: "https://rpc-mumbai.maticvigil.com/" 76 | - Chain ID: "80001" 77 | - Currency Symbol (optional): "ETH" 78 | - Block Explorer URL (optional): "https://explorer-mumbai.maticvigil.com/" 79 | 80 | ### Connect MetaMask Wallet to Local AvalancheGo Node ### 81 | - Network Name: "Avalanche Local" 82 | - New RPC URL: "http://localhost:9650/ext/bc/C/rpc" 83 | - Chain ID: "43112" 84 | - Currency Symbol (optional): "AVAX" 85 | - Block Explorer URL (optional): "" 86 | 87 | ### Connect MetaMask Wallet to Local GETH Node ### 88 | - Network Name: "Localhost 8545" 89 | - New RPC URL: "http://localhost:8545" 90 | - Chain ID: "1337" 91 | - Currency Symbol (optional): "ETH" 92 | - Block Explorer URL (optional): "" 93 | 94 | ### Add Dev Account for Local GETH Node 95 | - In MetaMask select `Import Account` and paste the private key `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`. 96 | - Name the account `Local GETH Development Account`. 97 | 98 | ### Add Dev Account for Local AvalancheGo Node 99 | - In MetaMask select `Import Account` and paste the private key `0x56289e99c94b6912bfc12adc093c9b51124f0dc54ac7a766b2bc5ccf558d8027`. 100 | - Name the account `Local Avalanche Development Account` 101 | 102 | ## Transfer 10000 ETH on Local Ethereum Node From Coinbase To Development Account 103 | - Navigate to `geth_scripts` and run `./sendEth.sh`. - Remember to navigate back to the `hybrid-smart-contract-kit` root directory. 104 | 105 | ## Transfer 10000 AVAX Each on Local Avalanche Node from Pre-Funded Account to Dev Accounts 106 | - Run `avalanche_scripts/send_avax.sh`. 107 | 108 | ## !!! IMPORTANT !!! 109 | - In the following, replace `` with `local` or `avax-avash`, depending on the blockchain. 110 | 111 | ## Deploy LINK Token Contract (Only Needs to be Done on Local Networks, not Public Ones) and add to MetaMask 112 | - Either run `brownie run scripts/infrastructure/deploy_link_token.py --network ` or 113 | - On GETH, run `./geth_scripts/deploy_link_token.sh` . 114 | - On AvalancheGo, run `./avalanche_scripts/deploy_link_token.sh`. 115 | - Copy the address at which the LINK token has been deployed, click `Import tokens` in MetaMask below the list of all coins and tokens, and paste into the field `Token Contract Address`. Finally, click `Add Custom Token`. 116 | 117 | ## Deploy Oracle Contract (Needs to be Done on both Local and Public Networks) 118 | - Either run `brownie run scripts/infrastructure/deploy_oracle.py --network ` or 119 | - On GETH, run `./geth_scripts/deploy_oracle.sh`. 120 | - On AvalancheGo, run `./avalanche_scripts/deploy_oracle.sh`. 121 | 122 | ## Deploy Operator Contract (Needs to be Done on both Local and Public Networks) 123 | - Either run `brownie run scripts/infrastructure/deploy_operator.py --network ` or 124 | - On GETH, run `./geth_scripts/deploy_operator.sh`. 125 | - On AvalancheGo, run `./avalanche_scripts/deploy_operator.sh`. 126 | 127 | ## Modify `/etc/hosts` on Linux 128 | - On Linux only, associate `host.docker.internal` with `127.0.0.1` via this line in `/etc/hosts`: `127.0.0.1 localhost host.docker.internal`. 129 | 130 | ## Important: Stop a Postgres Service on the Host Machine on Port 5432 Before Running the Docker Container 131 | - Run `systemctl stop postgresql`. 132 | 133 | ## Build Chainlink Node (This only needs to be done the first time!) 134 | - On Mac, from the project root directory type `docker build -f chainlink/chainlink_dockerfile --tag chainlink_node .` to build a docker image for the chainlink node. 135 | - On Linux, add `sudo` before the docker command. 136 | - For GETH: 137 | - On Mac: Create container from docker image: `docker run --name chainlink_geth -p 6688:6688 -p 5432:5432 --add-host=host.docker.internal:host-gateway -it --env-file=chainlink/local.env -v $CHAINLINK_PATH:/chainlink chainlink_node`. 138 | - On Linux: Create container from docker image: `sudo docker run --name chainlink_geth -p 6688:6688 -p 5432:5432 --network=host -it --env-file=chainlink/local.env -v $CHAINLINK_PATH:/chainlink chainlink_node`. 139 | - For Avalanche 140 | - On Mac: Create container from docker image: `docker run --name chainlink_avalanche -p 6688:6688 -p 5432:5432 --add-host=host.docker.internal:host-gateway -it --env-file=chainlink/local.env -v $CHAINLINK_PATH:/chainlink chainlink_node`. 141 | - On Linux: Create container from docker image: `sudo docker run --name chainlink_avalanche -p 6688:6688 -p 5432:5432 --network=host -it --env-file=chainlink/local.env -v $CHAINLINK_PATH:/chainlink chainlink_node`. 142 | - Compile chainlink: 143 | - Change into `chainlink` folder. 144 | - Run `yarn install`. 145 | - Run `make install`. 146 | - In `chainlink` folder run `./chainlink node start -p /cla/.password -a /cla/.api`. 147 | - In a web browser navigate to `http://localhost:6688`. 148 | - Login with username `admin@example.com` and password `password`. 149 | - Navigate to gear icon, select `Key Management`, and copy the regular account address. 150 | - For Geth, store the copied account address in an environment variable `GETH_CHAINLINK_NODE_ACCOUNT_ADDRESS`. 151 | - For Avalanche, store the copied account address in an environment variable `AVALANCHE_CHAINLINK_NODE_ACCOUNT_ADDRESS`. 152 | 153 | ## To Start the Chainlink Docker Container After the First Setup 154 | - Start a new terminal or resource your environment file. 155 | - On Linux, remember to add `sudo` before the docker command. 156 | - In the following, the placeholder `` refers to `chainlink_avalanche` for Avalanche and `chainlink_geth` for GETH. 157 | - To start a stopped container, type `docker start `. 158 | - To attach a terminal, type `docker attach $CHAINLINK_DOCKER_CONTAINER_NAME`. 159 | - Navigate to `chainlink` folder via `cd chainlink`. 160 | - In `chainlink` folder run `./chainlink node start -p /cla/.password -a /cla/.api`. 161 | 162 | ## Transfer 1000 ETH from Local Development Account to Chainlink Node Account and Set Fulfillment Permission on Oracle Contract 163 | - Run `brownie run scripts/infrastructure/fund_chainlink_account --network `. 164 | 165 | ## Testnet Consumer 166 | Adapted from `https://github.com/sourabhrajsingh/chainlink-remix-workshop/blob/master/ATestnetConsumer.sol` 167 | 168 | ### Add Get > Uint256 Job on Chainlink Node 169 | - In a browser, navigate to the Chainlink management console at `localhost:6688`. 170 | - Select `Jobs` and click `New Job`. 171 | - To display the oracle contract address, run `brownie run scripts/infrastructure/print_contract_addresses.py --network `. 172 | - Paste the contents of the file `chainlink/TestnetConsumerJob.toml` into the job description and replace `YOUR_ORACLE_CONTRACT_ADDRESS` with the address at which the oracle contract has been deployed. 173 | - Click `Create Job`. 174 | 175 | ### Deploy A Testnet Consumer Contract and Fund With LINK Tokens 176 | - Run `brownie run scripts/oracle_example/deploy_testnet_consumer.py --network `. 177 | 178 | ### Initiate Request of Ether Price from Oracle (i.e. call ATestnetConsumer contract which calls the Oracle contract) 179 | - Run `brownie run scripts/oracle_example/make_testnet_consumer_request.py --network `. 180 | 181 | ### Read Data Returned By Chainlink from A Testnet Consumer Contract 182 | - Run `brownie run scripts/oracle_example/read_testnet_consumer_result.py --network `. 183 | 184 | ## Crypto Compare External Adapter 185 | Originally created by Thomas Hodges and published on GitHub at `https://github.com/thodges-gh/CL-EA-Python-Template`. 186 | 187 | ### Start and Test the Web Service Running the External Adapter 188 | - To start the web service, run `python -m hsck.crypto_compare_ea.app`. 189 | - To test the web service, run `pytest hsck`. 190 | - To see the output from a post request, run `curl -X POST -H "content-type:application/json" "http://localhost:8082/" --data '{ "id": 0, "data": { "from": "ETH", "to": "USD" } }'`. 191 | 192 | ### Add Bridge to Crypto Compare External Adapter on Chainlink Node 193 | - Log in to the Chainlink web management console. 194 | - Navigate to `Bridges` and click `New Bridge`. 195 | - Enter `CryptoCompare` under `Bridge Name`. 196 | - Enter `http://host.docker.internal:8082` under `Bridge URL`. 197 | 198 | ### Add CryptoCompare Job on Chainlink Node 199 | - In a browser, navigate to the Chainlink management console at `localhost:6688`. 200 | - Select `Jobs` and click `New Job`. 201 | - To display the oracle contract address, run `brownie run scripts/infrastructure/print_contract_addresses.py --network `. 202 | - Paste the contents of the file `chainlink/CryptoCompareJob.toml` into the job description and replace `YOUR_ORACLE_CONTRACT_ADDRESS` with the address at which the oracle contract has been deployed. 203 | - Click `Create Job`. 204 | 205 | ### Initiate Request To Crypto Compare External Adapter 206 | - Run `brownie run scripts/oracle_example/make_crypto_compare_ea_request.py --network `. 207 | 208 | ### Read Data Returned By Chainlink Node via Crypto Compare External Adapter 209 | - Run `brownie run scripts/oracle_example/read_crypto_compare_ea_result.py --network `. 210 | 211 | ## Multi Word Consumer Example 212 | See the Chainlink documentation for background information at `https://docs.chain.link/docs/multi-variable-responses/`. 213 | 214 | ### Add Multi Word Consumer Job on Chainlink Node 215 | - In a browser, navigate to the Chainlink management console at `localhost:6688`. 216 | - Select `Jobs` and click `New Job`. 217 | - To display the operator contract address, run `brownie run scripts/infrastructure/print_contract_addresses.py --network `. 218 | - Paste the contents of the file `chainlink/MultiWordConsumer.toml` into the job description and replace `YOUR_OPERATOR_CONTRACT_ADDRESS` with the address at which the operator contract has been deployed (not the oracle address!). 219 | - Click `Create Job`. 220 | 221 | ### Deploy Multi Word Consumer Contract and Fund With LINK Tokens 222 | - Run `brownie run scripts/oracle_example/deploy_multi_word_consumer.py --network `. 223 | 224 | ### Initiate Request of Ether Price in USD, EUR, and JPY from Operator 225 | - Run `brownie run scripts/oracle_example/make_multi_word_consumer_request.py --network `. 226 | 227 | ### Read Data Returned By Chainlink from Multi Word Consumer Contract 228 | - Run `brownie run scripts/oracle_example/read_multi_word_consumer_result.py --network `. 229 | -------------------------------------------------------------------------------- /contracts/Operator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity ^0.7.0; 3 | 4 | import "@chainlink/contracts/src/v0.7/AuthorizedReceiver.sol"; 5 | import "@chainlink/contracts/src/v0.7/LinkTokenReceiver.sol"; 6 | import "@chainlink/contracts/src/v0.7/ConfirmedOwner.sol"; 7 | import "@chainlink/contracts/src/v0.7/interfaces/LinkTokenInterface.sol"; 8 | import "@chainlink/contracts/src/v0.7/interfaces/OperatorInterface.sol"; 9 | import "@chainlink/contracts/src/v0.7/interfaces/OwnableInterface.sol"; 10 | import "@chainlink/contracts/src/v0.7/interfaces/WithdrawalInterface.sol"; 11 | import "@chainlink/contracts/src/v0.7/vendor/Address.sol"; 12 | import "@chainlink/contracts/src/v0.7/vendor/SafeMathChainlink.sol"; 13 | 14 | /** 15 | * @title The Chainlink Operator contract 16 | * @notice Node operators can deploy this contract to fulfill requests sent to them 17 | */ 18 | contract Operator is 19 | AuthorizedReceiver, 20 | ConfirmedOwner, 21 | LinkTokenReceiver, 22 | OperatorInterface, 23 | WithdrawalInterface 24 | { 25 | using Address for address; 26 | using SafeMathChainlink for uint256; 27 | 28 | struct Commitment { 29 | bytes31 paramsHash; 30 | uint8 dataVersion; 31 | } 32 | 33 | uint256 constant public getExpiryTime = 5 minutes; 34 | uint256 constant private MAXIMUM_DATA_VERSION = 256; 35 | uint256 constant private MINIMUM_CONSUMER_GAS_LIMIT = 400000; 36 | uint256 constant private SELECTOR_LENGTH = 4; 37 | uint256 constant private EXPECTED_REQUEST_WORDS = 2; 38 | uint256 constant private MINIMUM_REQUEST_LENGTH = SELECTOR_LENGTH + (32 * EXPECTED_REQUEST_WORDS); 39 | // We initialize fields to 1 instead of 0 so that the first invocation 40 | // does not cost more gas. 41 | uint256 constant private ONE_FOR_CONSISTENT_GAS_COST = 1; 42 | // oracleRequest is version 1, enabling single word responses 43 | bytes4 constant private ORACLE_REQUEST_SELECTOR = this.oracleRequest.selector; 44 | // requestOracleData is version 2, enabling multi-word responses 45 | bytes4 constant private OPERATOR_REQUEST_SELECTOR = this.requestOracleData.selector; 46 | 47 | LinkTokenInterface internal immutable linkToken; 48 | mapping(bytes32 => Commitment) private s_commitments; 49 | // Tokens sent for requests that have not been fulfilled yet 50 | uint256 private s_tokensInEscrow = ONE_FOR_CONSISTENT_GAS_COST; 51 | 52 | event OracleRequest( 53 | bytes32 indexed specId, 54 | address requester, 55 | bytes32 requestId, 56 | uint256 payment, 57 | address callbackAddr, 58 | bytes4 callbackFunctionId, 59 | uint256 cancelExpiration, 60 | uint256 dataVersion, 61 | bytes data 62 | ); 63 | 64 | event CancelOracleRequest( 65 | bytes32 indexed requestId 66 | ); 67 | 68 | event OracleResponse( 69 | bytes32 indexed requestId 70 | ); 71 | 72 | event OwnableContractAccepted( 73 | address indexed accpetedContract 74 | ); 75 | 76 | event TargetsUpdatedAuthorizedSenders( 77 | address[] targets, 78 | address[] senders, 79 | address changedBy 80 | ); 81 | 82 | /** 83 | * @notice Deploy with the address of the LINK token 84 | * @dev Sets the LinkToken address for the imported LinkTokenInterface 85 | * @param link The address of the LINK token 86 | * @param owner The address of the owner 87 | */ 88 | constructor( 89 | address link, 90 | address owner 91 | ) 92 | ConfirmedOwner(owner) 93 | { 94 | linkToken = LinkTokenInterface(link); // external but already deployed and unalterable 95 | } 96 | 97 | function oracleRequest( 98 | address sender, 99 | uint256 payment, 100 | bytes32 specId, 101 | address callbackAddress, 102 | bytes4 callbackFunctionId, 103 | uint256 nonce, 104 | uint256 dataVersion, 105 | bytes calldata data 106 | ) 107 | external 108 | override 109 | { 110 | requestOracleData( 111 | sender, 112 | payment, 113 | specId, 114 | callbackAddress, 115 | callbackFunctionId, 116 | nonce, 117 | dataVersion, 118 | data 119 | ); 120 | } 121 | 122 | /** 123 | * @notice Creates the Chainlink request 124 | * @dev Stores the hash of the params as the on-chain commitment for the request. 125 | * Emits OracleRequest event for the Chainlink node to detect. 126 | * @param sender The sender of the request 127 | * @param payment The amount of payment given (specified in wei) 128 | * @param specId The Job Specification ID 129 | * @param callbackAddress The callback address for the response 130 | * @param callbackFunctionId The callback function ID for the response 131 | * @param nonce The nonce sent by the requester 132 | * @param dataVersion The specified data version 133 | * @param data The CBOR payload of the request 134 | */ 135 | function requestOracleData( 136 | address sender, 137 | uint256 payment, 138 | bytes32 specId, 139 | address callbackAddress, 140 | bytes4 callbackFunctionId, 141 | uint256 nonce, 142 | uint256 dataVersion, 143 | bytes calldata data 144 | ) 145 | public 146 | override 147 | validateFromLINK() 148 | validateNotToLINK(callbackAddress) 149 | { 150 | (bytes32 requestId, uint256 expiration) = _verifyOracleRequest( 151 | sender, 152 | payment, 153 | callbackAddress, 154 | callbackFunctionId, 155 | nonce, 156 | dataVersion 157 | ); 158 | emit OracleRequest( 159 | specId, 160 | sender, 161 | requestId, 162 | payment, 163 | callbackAddress, 164 | callbackFunctionId, 165 | expiration, 166 | dataVersion, 167 | data); 168 | } 169 | 170 | /** 171 | * @notice Called by the Chainlink node to fulfill requests 172 | * @dev Given params must hash back to the commitment stored from `oracleRequest`. 173 | * Will call the callback address' callback function without bubbling up error 174 | * checking in a `require` so that the node can get paid. 175 | * @param requestId The fulfillment request ID that must match the requester's 176 | * @param payment The payment amount that will be released for the oracle (specified in wei) 177 | * @param callbackAddress The callback address to call for fulfillment 178 | * @param callbackFunctionId The callback function ID to use for fulfillment 179 | * @param expiration The expiration that the node should respond by before the requester can cancel 180 | * @param data The data to return to the consuming contract 181 | * @return Status if the external call was successful 182 | */ 183 | function fulfillOracleRequest( 184 | bytes32 requestId, 185 | uint256 payment, 186 | address callbackAddress, 187 | bytes4 callbackFunctionId, 188 | uint256 expiration, 189 | bytes32 data 190 | ) 191 | external 192 | override 193 | validateAuthorizedSender() 194 | validateRequestId(requestId) 195 | returns ( 196 | bool 197 | ) 198 | { 199 | _verifyOracleResponse( 200 | requestId, 201 | payment, 202 | callbackAddress, 203 | callbackFunctionId, 204 | expiration, 205 | 1 206 | ); 207 | emit OracleResponse(requestId); 208 | require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, "Must provide consumer enough gas"); 209 | // All updates to the oracle's fulfillment should come before calling the 210 | // callback(addr+functionId) as it is untrusted. 211 | // See: https://solidity.readthedocs.io/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern 212 | (bool success, ) = callbackAddress.call(abi.encodeWithSelector(callbackFunctionId, requestId, data)); // solhint-disable-line avoid-low-level-calls 213 | return success; 214 | } 215 | 216 | /** 217 | * @notice Called by the Chainlink node to fulfill requests with multi-word support 218 | * @dev Given params must hash back to the commitment stored from `oracleRequest`. 219 | * Will call the callback address' callback function without bubbling up error 220 | * checking in a `require` so that the node can get paid. 221 | * @param requestId The fulfillment request ID that must match the requester's 222 | * @param payment The payment amount that will be released for the oracle (specified in wei) 223 | * @param callbackAddress The callback address to call for fulfillment 224 | * @param callbackFunctionId The callback function ID to use for fulfillment 225 | * @param expiration The expiration that the node should respond by before the requester can cancel 226 | * @param data The data to return to the consuming contract 227 | * @return Status if the external call was successful 228 | */ 229 | function fulfillOracleRequest2( 230 | bytes32 requestId, 231 | uint256 payment, 232 | address callbackAddress, 233 | bytes4 callbackFunctionId, 234 | uint256 expiration, 235 | bytes calldata data 236 | ) 237 | external 238 | override 239 | validateAuthorizedSender() 240 | validateRequestId(requestId) 241 | validateMultiWordResponseId(requestId, data) 242 | returns ( 243 | bool 244 | ) 245 | { 246 | _verifyOracleResponse( 247 | requestId, 248 | payment, 249 | callbackAddress, 250 | callbackFunctionId, 251 | expiration, 252 | 2 253 | ); 254 | emit OracleResponse(requestId); 255 | require(gasleft() >= MINIMUM_CONSUMER_GAS_LIMIT, "Must provide consumer enough gas"); 256 | // All updates to the oracle's fulfillment should come before calling the 257 | // callback(addr+functionId) as it is untrusted. 258 | // See: https://solidity.readthedocs.io/en/develop/security-considerations.html#use-the-checks-effects-interactions-pattern 259 | (bool success, ) = callbackAddress.call(abi.encodePacked(callbackFunctionId, data)); // solhint-disable-line avoid-low-level-calls 260 | return success; 261 | } 262 | 263 | /** 264 | * @notice Transfer the ownership of ownable contracts 265 | * @param ownable list of addresses to transfer 266 | * @param newOwner address to transfer ownership to 267 | */ 268 | function transferOwnableContracts( 269 | address[] calldata ownable, 270 | address newOwner 271 | ) 272 | external 273 | onlyOwner() 274 | { 275 | for (uint256 i = 0; i < ownable.length; i++) { 276 | OwnableInterface(ownable[i]).transferOwnership(newOwner); 277 | } 278 | } 279 | 280 | /** 281 | * @notice Accept the ownership of an ownable contract 282 | * @dev Must be the pending owner on the contract 283 | * @param ownable list of addresses of Ownable contracts to accept 284 | */ 285 | function acceptOwnableContracts( 286 | address[] calldata ownable 287 | ) 288 | public 289 | validateAuthorizedSenderSetter() 290 | { 291 | for (uint256 i = 0; i < ownable.length; i++) { 292 | OwnableInterface(ownable[i]).acceptOwnership(); 293 | emit OwnableContractAccepted(ownable[i]); 294 | } 295 | } 296 | 297 | /** 298 | * @notice Sets the fulfillment permission for 299 | * @param targets The addresses to set permissions on 300 | * @param senders The addresses that are allowed to send updates 301 | */ 302 | function setAuthorizedSendersOn( 303 | address[] calldata targets, 304 | address[] calldata senders 305 | ) 306 | public 307 | validateAuthorizedSenderSetter() 308 | { 309 | TargetsUpdatedAuthorizedSenders(targets, senders, msg.sender); 310 | 311 | for (uint256 i = 0; i < targets.length; i++) { 312 | AuthorizedReceiverInterface(targets[i]).setAuthorizedSenders(senders); 313 | } 314 | } 315 | 316 | /** 317 | * @notice Sets the fulfillment permission for 318 | * @param targets The addresses to set permissions on 319 | * @param senders The addresses that are allowed to send updates 320 | */ 321 | function acceptAuthorizedReceivers( 322 | address[] calldata targets, 323 | address[] calldata senders 324 | ) 325 | external 326 | validateAuthorizedSenderSetter() 327 | { 328 | acceptOwnableContracts(targets); 329 | setAuthorizedSendersOn(targets, senders); 330 | } 331 | 332 | /** 333 | * @notice Allows the node operator to withdraw earned LINK to a given address 334 | * @dev The owner of the contract can be another wallet and does not have to be a Chainlink node 335 | * @param recipient The address to send the LINK token to 336 | * @param amount The amount to send (specified in wei) 337 | */ 338 | function withdraw( 339 | address recipient, 340 | uint256 amount 341 | ) 342 | external 343 | override(OracleInterface, WithdrawalInterface) 344 | onlyOwner() 345 | validateAvailableFunds(amount) 346 | { 347 | assert(linkToken.transfer(recipient, amount)); 348 | } 349 | 350 | /** 351 | * @notice Displays the amount of LINK that is available for the node operator to withdraw 352 | * @dev We use `ONE_FOR_CONSISTENT_GAS_COST` in place of 0 in storage 353 | * @return The amount of withdrawable LINK on the contract 354 | */ 355 | function withdrawable() 356 | external 357 | view 358 | override(OracleInterface, WithdrawalInterface) 359 | returns (uint256) 360 | { 361 | return _fundsAvailable(); 362 | } 363 | 364 | /** 365 | * @notice Forward a call to another contract 366 | * @dev Only callable by the owner 367 | * @param to address 368 | * @param data to forward 369 | */ 370 | function ownerForward( 371 | address to, 372 | bytes calldata data 373 | ) 374 | external 375 | onlyOwner() 376 | validateNotToLINK(to) 377 | { 378 | require(to.isContract(), "Must forward to a contract"); 379 | (bool status,) = to.call(data); 380 | require(status, "Forwarded call failed"); 381 | } 382 | 383 | /** 384 | * @notice Interact with other LinkTokenReceiver contracts by calling transferAndCall 385 | * @param to The address to transfer to. 386 | * @param value The amount to be transferred. 387 | * @param data The extra data to be passed to the receiving contract. 388 | * @return success bool 389 | */ 390 | function ownerTransferAndCall( 391 | address to, 392 | uint256 value, 393 | bytes calldata data 394 | ) 395 | external 396 | override 397 | onlyOwner() 398 | validateAvailableFunds(value) 399 | returns ( 400 | bool success 401 | ) 402 | { 403 | return linkToken.transferAndCall(to, value, data); 404 | } 405 | 406 | /** 407 | * @notice Distribute funds to multiple addresses using ETH send 408 | * to this payable function. 409 | * @dev Array length must be equal, ETH sent must equal the sum of amounts. 410 | * @param receivers list of addresses 411 | * @param amounts list of amounts 412 | */ 413 | function distributeFunds( 414 | address payable[] calldata receivers, 415 | uint[] calldata amounts 416 | ) 417 | external 418 | payable 419 | { 420 | require(receivers.length > 0 && receivers.length == amounts.length, "Invalid array length(s)"); 421 | uint256 valueRemaining = msg.value; 422 | for (uint256 i = 0; i < receivers.length; i++) { 423 | uint256 sendAmount = amounts[i]; 424 | valueRemaining = valueRemaining.sub(sendAmount); 425 | receivers[i].transfer(sendAmount); 426 | } 427 | require(valueRemaining == 0, "Too much ETH sent"); 428 | } 429 | 430 | /** 431 | * @notice Allows requesters to cancel requests sent to this oracle contract. Will transfer the LINK 432 | * sent for the request back to the requester's address. 433 | * @dev Given params must hash to a commitment stored on the contract in order for the request to be valid 434 | * Emits CancelOracleRequest event. 435 | * @param requestId The request ID 436 | * @param payment The amount of payment given (specified in wei) 437 | * @param callbackFunc The requester's specified callback address 438 | * @param expiration The time of the expiration for the request 439 | */ 440 | function cancelOracleRequest( 441 | bytes32 requestId, 442 | uint256 payment, 443 | bytes4 callbackFunc, 444 | uint256 expiration 445 | ) 446 | external 447 | override 448 | { 449 | bytes31 paramsHash = _buildFunctionHash(payment, msg.sender, callbackFunc, expiration); 450 | require(s_commitments[requestId].paramsHash == paramsHash, "Params do not match request ID"); 451 | // solhint-disable-next-line not-rely-on-time 452 | require(expiration <= block.timestamp, "Request is not expired"); 453 | 454 | delete s_commitments[requestId]; 455 | emit CancelOracleRequest(requestId); 456 | 457 | assert(linkToken.transfer(msg.sender, payment)); 458 | } 459 | 460 | /** 461 | * @notice Returns the address of the LINK token 462 | * @dev This is the public implementation for chainlinkTokenAddress, which is 463 | * an internal method of the ChainlinkClient contract 464 | */ 465 | function getChainlinkToken() 466 | public 467 | view 468 | override 469 | returns ( 470 | address 471 | ) 472 | { 473 | return address(linkToken); 474 | } 475 | 476 | 477 | /** 478 | * @notice Require that the token transfer action is valid 479 | * @dev OPERATOR_REQUEST_SELECTOR = multiword, ORACLE_REQUEST_SELECTOR = singleword 480 | */ 481 | function _validateTokenTransferAction( 482 | bytes4 funcSelector, 483 | bytes memory data 484 | ) 485 | internal 486 | override 487 | pure 488 | { 489 | require(data.length >= MINIMUM_REQUEST_LENGTH, "Invalid request length"); 490 | require(funcSelector == OPERATOR_REQUEST_SELECTOR || funcSelector == ORACLE_REQUEST_SELECTOR, "Must use whitelisted functions"); 491 | } 492 | 493 | /** 494 | * @notice Verify the Oracle Request 495 | * @param sender The sender of the request 496 | * @param payment The amount of payment given (specified in wei) 497 | * @param callbackAddress The callback address for the response 498 | * @param callbackFunctionId The callback function ID for the response 499 | * @param nonce The nonce sent by the requester 500 | */ 501 | function _verifyOracleRequest( 502 | address sender, 503 | uint256 payment, 504 | address callbackAddress, 505 | bytes4 callbackFunctionId, 506 | uint256 nonce, 507 | uint256 dataVersion 508 | ) 509 | private 510 | returns ( 511 | bytes32 requestId, 512 | uint256 expiration 513 | ) 514 | { 515 | requestId = keccak256(abi.encodePacked(sender, nonce)); 516 | require(s_commitments[requestId].paramsHash == 0, "Must use a unique ID"); 517 | // solhint-disable-next-line not-rely-on-time 518 | expiration = block.timestamp.add(getExpiryTime); 519 | bytes31 paramsHash = _buildFunctionHash(payment, callbackAddress, callbackFunctionId, expiration); 520 | s_commitments[requestId] = Commitment(paramsHash, _safeCastToUint8(dataVersion)); 521 | s_tokensInEscrow = s_tokensInEscrow.add(payment); 522 | return (requestId, expiration); 523 | } 524 | 525 | /** 526 | * @notice Verify the Oracle Response 527 | * @param requestId The fulfillment request ID that must match the requester's 528 | * @param payment The payment amount that will be released for the oracle (specified in wei) 529 | * @param callbackAddress The callback address to call for fulfillment 530 | * @param callbackFunctionId The callback function ID to use for fulfillment 531 | * @param expiration The expiration that the node should respond by before the requester can cancel 532 | */ 533 | function _verifyOracleResponse( 534 | bytes32 requestId, 535 | uint256 payment, 536 | address callbackAddress, 537 | bytes4 callbackFunctionId, 538 | uint256 expiration, 539 | uint256 dataVersion 540 | ) 541 | internal 542 | { 543 | bytes31 paramsHash = _buildFunctionHash(payment, callbackAddress, callbackFunctionId, expiration); 544 | require(s_commitments[requestId].paramsHash == paramsHash, "Params do not match request ID"); 545 | require(s_commitments[requestId].dataVersion <= _safeCastToUint8(dataVersion), "Data versions must match"); 546 | s_tokensInEscrow = s_tokensInEscrow.sub(payment); 547 | delete s_commitments[requestId]; 548 | } 549 | 550 | /** 551 | * @notice Build the bytes31 function hash from the payment, callback and expiration. 552 | * @param payment The payment amount that will be released for the oracle (specified in wei) 553 | * @param callbackAddress The callback address to call for fulfillment 554 | * @param callbackFunctionId The callback function ID to use for fulfillment 555 | * @param expiration The expiration that the node should respond by before the requester can cancel 556 | * @return hash bytes31 557 | */ 558 | function _buildFunctionHash( 559 | uint256 payment, 560 | address callbackAddress, 561 | bytes4 callbackFunctionId, 562 | uint256 expiration 563 | ) 564 | internal 565 | pure 566 | returns ( 567 | bytes31 568 | ) 569 | { 570 | return bytes31(keccak256( 571 | abi.encodePacked( 572 | payment, 573 | callbackAddress, 574 | callbackFunctionId, 575 | expiration 576 | ) 577 | )); 578 | } 579 | 580 | /** 581 | * @notice Safely cast uint256 to uint8 582 | * @param number uint256 583 | * @return uint8 number 584 | */ 585 | function _safeCastToUint8( 586 | uint256 number 587 | ) 588 | internal 589 | pure 590 | returns ( 591 | uint8 592 | ) 593 | { 594 | require(number < MAXIMUM_DATA_VERSION, "number too big to cast"); 595 | return uint8(number); 596 | } 597 | 598 | /** 599 | * @notice Returns the LINK available in this contract, not locked in escrow 600 | * @return uint256 LINK tokens available 601 | */ 602 | function _fundsAvailable() 603 | private 604 | view 605 | returns ( 606 | uint256 607 | ) 608 | { 609 | uint256 inEscrow = s_tokensInEscrow.sub(ONE_FOR_CONSISTENT_GAS_COST); 610 | return linkToken.balanceOf(address(this)).sub(inEscrow); 611 | } 612 | 613 | /** 614 | * @notice concrete implementation of AuthorizedReceiver 615 | * @return bool of whether sender is authorized 616 | */ 617 | function _canSetAuthorizedSenders() 618 | internal 619 | view 620 | override 621 | returns (bool) 622 | { 623 | return isAuthorizedSender(msg.sender) || owner() == msg.sender; 624 | } 625 | 626 | 627 | // MODIFIERS 628 | 629 | /** 630 | * @dev Reverts if the first 32 bytes of the bytes array is not equal to requestId 631 | * @param requestId bytes32 632 | * @param data bytes 633 | */ 634 | modifier validateMultiWordResponseId( 635 | bytes32 requestId, 636 | bytes memory data 637 | ) { 638 | bytes32 firstWord; 639 | assembly{ 640 | firstWord := mload(add(data, 0x20)) 641 | } 642 | require(requestId == firstWord, "First word must be requestId"); 643 | _; 644 | } 645 | 646 | /** 647 | * @dev Reverts if amount requested is greater than withdrawable balance 648 | * @param amount The given amount to compare to `s_withdrawableTokens` 649 | */ 650 | modifier validateAvailableFunds( 651 | uint256 amount 652 | ) { 653 | require(_fundsAvailable() >= amount, "Amount requested is greater than withdrawable balance"); 654 | _; 655 | } 656 | 657 | /** 658 | * @dev Reverts if request ID does not exist 659 | * @param requestId The given request ID to check in stored `commitments` 660 | */ 661 | modifier validateRequestId( 662 | bytes32 requestId 663 | ) { 664 | require(s_commitments[requestId].paramsHash != 0, "Must have a valid requestId"); 665 | _; 666 | } 667 | 668 | /** 669 | * @dev Reverts if the callback address is the LINK token 670 | * @param to The callback address 671 | */ 672 | modifier validateNotToLINK( 673 | address to 674 | ) { 675 | require(to != address(linkToken), "Cannot call to LINK"); 676 | _; 677 | } 678 | 679 | } 680 | --------------------------------------------------------------------------------