diff --git a/Makefile b/Makefile index 31b53247a..08b70f0d2 100644 --- a/Makefile +++ b/Makefile @@ -145,8 +145,7 @@ format-cairo: .PHONY: format-cairo-check format-cairo-check: cairo-format -c ./contracts/src/**/*.cairo - # TODO: re-enable once examples are rewritten - # cairo-format -c ./examples/**/*.cairo + cairo-format -c ./examples/**/*.cairo .PHONY: format-ts format-ts: diff --git a/contracts/Scarb.toml b/contracts/Scarb.toml index 663c85447..2d2a6323c 100644 --- a/contracts/Scarb.toml +++ b/contracts/Scarb.toml @@ -14,6 +14,8 @@ sierra = "cairo-compile . -r" starknet = ">=1.0.0" openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", rev = "495ed8a" } # latest cairo-2 branch +[lib] + [[target.starknet-contract]] sierra = true casm = true @@ -21,9 +23,7 @@ casm = true # Unsupported compiled class format. Cairo 1.0 compiled class must contain the attribute `pythonic_hints`. casm-add-pythonic-hints = true -# this elevants the severity of disallowed libfuncs to compilation errors +# this elevates the severity of disallowed libfuncs to compilation errors # https://docs.swmansion.com/scarb/docs/starknet/contract-target#allowed-libfuncs-validation allowed-libfuncs-deny = true - -# TODO: change the allowlist to 'audited' when cairo v2 is released allowed-libfuncs-list.name = "audited" diff --git a/contracts/cairo_project.toml b/contracts/cairo_project.toml index f36ac6cf8..df050dd0c 100644 --- a/contracts/cairo_project.toml +++ b/contracts/cairo_project.toml @@ -1,2 +1,2 @@ [crate_roots] -chainlink = "src" \ No newline at end of file +chainlink = "src" diff --git a/contracts/src/ocr2/aggregator.cairo b/contracts/src/ocr2/aggregator.cairo index 650805abf..64a677f29 100644 --- a/contracts/src/ocr2/aggregator.cairo +++ b/contracts/src/ocr2/aggregator.cairo @@ -1,6 +1,6 @@ use starknet::ContractAddress; -#[derive(Copy, Drop, Serde, PartialEq)] +#[derive(Copy, Drop, Serde, PartialEq, starknet::Store)] struct Round { // used as u128 internally, but necessary for phase-prefixed round ids as returned by proxy round_id: felt252, diff --git a/contracts/src/tests.cairo b/contracts/src/tests.cairo index 8b45469a8..4d8104499 100644 --- a/contracts/src/tests.cairo +++ b/contracts/src/tests.cairo @@ -8,4 +8,5 @@ mod test_erc677; mod test_link_token; mod test_upgradeable; mod test_access_controller; +mod test_mock_aggregator; mod test_sequencer_uptime_feed; diff --git a/contracts/src/tests/test_mock_aggregator.cairo b/contracts/src/tests/test_mock_aggregator.cairo new file mode 100644 index 000000000..70ee6bad3 --- /dev/null +++ b/contracts/src/tests/test_mock_aggregator.cairo @@ -0,0 +1,62 @@ +use starknet::ContractAddress; +use starknet::testing::set_caller_address; +use chainlink::ocr2::mocks::mock_aggregator::MockAggregator; +use starknet::contract_address_const; +use chainlink::ocr2::aggregator::Round; + +fn STATE() -> MockAggregator::ContractState { + MockAggregator::contract_state_for_testing() +} + +fn setup() -> ContractAddress { + let account: ContractAddress = contract_address_const::<777>(); + // Set account as default caller + set_caller_address(account); + account +} + +#[test] +#[available_gas(2000000)] +fn test_deploy() { + setup(); + + let mut state = STATE(); + + MockAggregator::constructor(ref state, 18_u8); + + assert(MockAggregator::Aggregator::decimals(@state) == 18_u8, 'decimals'); + + let latest_round = MockAggregator::Aggregator::latest_round_data(@state); + + let zeroed_round = Round { + round_id: 0, answer: 0_u128, block_num: 0_u64, started_at: 0_u64, updated_at: 0_u64 + }; + + assert( + latest_round == Round { + round_id: 0, answer: 0_u128, block_num: 0_u64, started_at: 0_u64, updated_at: 0_u64 + }, + 'rounds' + ); +} + +#[test] +#[available_gas(2000000)] +fn test_set_latest_round() { + setup(); + + let mut state = STATE(); + + MockAggregator::constructor(ref state, 18_u8); + + MockAggregator::MockImpl::set_latest_round_data(ref state, 777_u128, 777_u64, 777_u64, 777_u64); + + let expected_round = Round { + round_id: 1, answer: 777_u128, block_num: 777_u64, started_at: 777_u64, updated_at: 777_u64 + }; + + assert( + MockAggregator::Aggregator::latest_round_data(@state) == expected_round, 'round not equal' + ); +} + diff --git a/examples/contracts/aggregator-consumer/.gitignore b/examples/contracts/aggregator-consumer/.gitignore index 793bd7c54..96611b42c 100644 --- a/examples/contracts/aggregator-consumer/.gitignore +++ b/examples/contracts/aggregator-consumer/.gitignore @@ -1,2 +1,3 @@ +target cache/ -starknet-artifacts/ \ No newline at end of file +starknet-artifacts/ diff --git a/examples/contracts/aggregator-consumer/README.md b/examples/contracts/aggregator-consumer/README.md new file mode 100644 index 000000000..e390b44ef --- /dev/null +++ b/examples/contracts/aggregator-consumer/README.md @@ -0,0 +1,70 @@ +This example demonstrates how consumer contracts can read from the aggregator, with and without consideration of the uptime feed address. Recall that the purpose of the uptime feed is to let consumer contracts know if the L2 layer is up and healthy. + +We demonstrate these examples by mocking an aggregator to return stubbed values. + +You can run this example on your local devnet or on goerli (up to you). NOTE: at the time of writing, the examples only work against devnet due to Starknet version incompatability. + +Note: `.env` will contain account details, such as DEPLOYER_ACCOUNT_ADDRESS and DEPLOYER_PRIVATE_KEY + +## Deploy on Devnet + +### 1. Install Local Dev Environment + +Follow [steps 1 and 2 here](../proxy_consumer/README.md) + +At this point, you should have starknet-devnet, cairo, and scarb installed. Your virtualenv will also have the starknet cli tool but we won't be using that to test against devnet + +All instructions begin at the root of this folder +### 2. Compile cairo 1 contracts + +``` + yarn compile:cairo +``` + + +### 3. Setup Starknet Devnet + +In a seperate terminal, run: +``` + starknet-devnet --cairo-compiler-manifest ../../vendor/cairo/Cargo.toml --seed 0 --lite-mode ../../../vendor/cairo/Cargo.toml +``` +This will start up the devnet and enable you to deploy and run cairo 1 contracts against it + +### 4. Deploy Devnet Account + +This command will deploy a devnet account onto devnet and write the DEPLOYER_ACCOUNT_ADDRESS and DEPLOYER_PRIVATE_KEY into a .env file in the current directory (it will create .env if it doesn't exist). This account will be utilized for deploying and interacting with the rest of the contracts in this section. + +``` + yarn deployAccount +``` + +## 5. Deploy Contracts + +This will deploy the following contracts: +* SequencerUptimeFeed: Displays the status of the L2 layer. If it is up, then it is safe to read from the aggregator contract. For more information please see this [document](../../../docs/emergency-protocol/README.md). +* MockAggregator: A mocked version of the aggregator with limited functionality. It gives the reader the ability to set and view the latest round data in order so the reader can familiarize themselves for testing purposes. +* AggregatorConsumer: Simply reads the mocked aggregator's latest values +* AggregatorPriceConsumerWithSequencer: Reads the mocked aggregator's values but also queries the SequencerUptimeFeed to determine if the value should be used or not. If the SequencerUptimeFeed is too stale, then that means the L2 layer is down. We've arbitrarily chosen the threshold of 60 seconds. + + +``` + yarn deployContracts +``` + +## 6. Interact with Contracts + +``` +# deployer calls AggregatorConsumer to read the decimals method of the MockAggregator +yarn readDecimals + +# deployer calls AggregatorConsumer to read the latest round of the MockAggregator +yarn readLatestRound + +# deployer calls Aggregator Consumer to poll decimals AND latest round of MockAggregator +yarn readContinuously + +# deployer calls MockAggregator to manually set the new round's data +yarn updateLatestRound + +# deployer calls AggregatorPriceConsumerWithSequencer to read latest round or revert if uptime feed is stale +yarn getLatestPriceSeqCheck diff --git a/examples/contracts/aggregator-consumer/Scarb.toml b/examples/contracts/aggregator-consumer/Scarb.toml new file mode 100644 index 000000000..18ac52c35 --- /dev/null +++ b/examples/contracts/aggregator-consumer/Scarb.toml @@ -0,0 +1,14 @@ +[package] +name = "chainlink_examples" +version = "0.1.0" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest + +[dependencies] + chainlink = { path = "../../../contracts" } + +[[target.starknet-contract]] +# note these two options only work on scarb 0.2.0 and forward +casm = true +# Emit Python-powered hints in order to run compiled CASM class with legacy Cairo VM. +casm-add-pythonic-hints = true diff --git a/examples/contracts/aggregator-consumer/cairo_project.toml b/examples/contracts/aggregator-consumer/cairo_project.toml new file mode 100644 index 000000000..e2ab1041e --- /dev/null +++ b/examples/contracts/aggregator-consumer/cairo_project.toml @@ -0,0 +1,2 @@ +[crate_roots] +chainlink_examples = "src" diff --git a/examples/contracts/aggregator-consumer/contracts/Aggregator_consumer.cairo b/examples/contracts/aggregator-consumer/contracts/Aggregator_consumer.cairo deleted file mode 100644 index 040dd960c..000000000 --- a/examples/contracts/aggregator-consumer/contracts/Aggregator_consumer.cairo +++ /dev/null @@ -1,32 +0,0 @@ -%lang starknet - -from starkware.cairo.common.cairo_builtins import HashBuiltin -from chainlink.cairo.ocr2.IAggregator import IAggregator, Round - -@storage_var -func AggregatorConsumer_ocr_address() -> (address: felt) { -} - -@constructor -func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}(address: felt) { - AggregatorConsumer_ocr_address.write(address); - return (); -} - -@view -func readLatestRound{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> ( - round: Round -) { - let (address) = AggregatorConsumer_ocr_address.read(); - let (round: Round) = IAggregator.latest_round_data(contract_address=address); - return (round,); -} - -@view -func readDecimals{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> ( - decimals: felt -) { - let (address) = AggregatorConsumer_ocr_address.read(); - let (decimals) = IAggregator.decimals(contract_address=address); - return (decimals,); -} diff --git a/examples/contracts/aggregator-consumer/contracts/MockAggregator.cairo b/examples/contracts/aggregator-consumer/contracts/MockAggregator.cairo deleted file mode 100644 index 49cf214b4..000000000 --- a/examples/contracts/aggregator-consumer/contracts/MockAggregator.cairo +++ /dev/null @@ -1,8 +0,0 @@ -%lang starknet - -from chainlink.cairo.ocr2.mocks.MockAggregator import ( - constructor, - set_latest_round_data, - latest_round_data, - decimals, -) diff --git a/examples/contracts/aggregator-consumer/contracts/Price_Consumer_With_Sequencer_Check.cairo b/examples/contracts/aggregator-consumer/contracts/Price_Consumer_With_Sequencer_Check.cairo deleted file mode 100644 index 342e73072..000000000 --- a/examples/contracts/aggregator-consumer/contracts/Price_Consumer_With_Sequencer_Check.cairo +++ /dev/null @@ -1,74 +0,0 @@ -%lang starknet - -from starkware.cairo.common.alloc import alloc -from starkware.cairo.common.cairo_builtins import HashBuiltin -from starkware.cairo.common.math import assert_not_zero -from starkware.cairo.common.math_cmp import is_nn -from starkware.cairo.common.bool import TRUE, FALSE -from starkware.starknet.common.syscalls import get_block_timestamp - -from chainlink.cairo.ocr2.IAggregator import IAggregator, Round - -@storage_var -func PriceConsumerWithSequencerCheck_uptime_feed_address() -> (address: felt) { -} - -@storage_var -func PriceConsumerWithSequencerCheck_aggregator_address() -> (address: felt) { -} - -// If the sequencer is up and that 60 sec has passed, -// The function retrieves the latest price from the data feed using the priceFeed object. -@constructor -func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( - uptime_feed_address: felt, aggregator_address: felt -) { - assert_not_zero(uptime_feed_address); - assert_not_zero(aggregator_address); - - PriceConsumerWithSequencerCheck_uptime_feed_address.write(uptime_feed_address); - PriceConsumerWithSequencerCheck_aggregator_address.write(aggregator_address); - return (); -} - -// If the sequencer is up and report is OK then we can get the latest price. -@view -func get_latest_price{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> ( - round: felt -) { - assert_sequencer_healthy(); - let (aggregator_address) = PriceConsumerWithSequencerCheck_aggregator_address.read(); - let (round: Round) = IAggregator.latest_round_data(contract_address=aggregator_address); - return (round=round.answer); -} - -// Errors if the report is stale, or it's reported that Sequencer node is down -@external -func assert_sequencer_healthy{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { - alloc_locals; - - let (uptime_feed_address) = PriceConsumerWithSequencerCheck_uptime_feed_address.read(); - - // Get latest_round_data from sequencer contract which should be update by a message send from L1 - let (round: Round) = IAggregator.latest_round_data(contract_address=uptime_feed_address); - let (local block_timestemp) = get_block_timestamp(); - let time = block_timestemp - round.updated_at; - - // After 60 sec the report is considered stale - let is_ls = is_nn(time - (60 + 1)); - - // 0 if the sequencer is up and 1 if it is down - if (round.answer == 0) { - with_attr error_message("PriceConsumer: L2 Sequencer is up, report stale") { - assert is_ls = 1; - } - return (); - } - with_attr error_message("PriceConsumer: L2 Sequencer is down, report stale") { - assert is_ls = 1; - } - with_attr error_message("PriceConsumer: L2 Sequencer is down, report ok") { - assert round.answer = 0; - } - return (); -} diff --git a/examples/contracts/aggregator-consumer/example.env b/examples/contracts/aggregator-consumer/example.env new file mode 100644 index 000000000..7d29ecc93 --- /dev/null +++ b/examples/contracts/aggregator-consumer/example.env @@ -0,0 +1,11 @@ +# This is a an example .env file that contains pre-deployed and pre-funded account that come with starknet-devnet +# To see a complete list of pre-deployed starknet-devnet accounts just run `starknet-devnet` + +# hypothetical devnet account +DEPLOYER_ACCOUNT_ADDRESS= +DEPLOYER_PRIVATE_KEY= + +# Below is what you'd set to run these examples on GOERLI +# DEPLOYER_ACCOUNT_ADDRESS= +# DEPLOYER_PRIVATE_KEY= +# NETWORK=GOERLI diff --git a/examples/contracts/aggregator-consumer/getting_start.md b/examples/contracts/aggregator-consumer/getting_start.md deleted file mode 100644 index 70775c2e0..000000000 --- a/examples/contracts/aggregator-consumer/getting_start.md +++ /dev/null @@ -1,25 +0,0 @@ -0. `.env` contains predeploy and funded account details, such as DEPLOYER_ACCOUNT_ADDRESS and DEPLOYER_PRIVATE_KEY - -1. Set up the network `export STARKNET_NETWORK=alpha-goerli` - -2. Choose a wallet provider `export STARKNET_WALLET=starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount` - -3. If you don't have an account create it with the command `starknet new_account` - -4. Open that link https://faucet.goerli.starknet.io/ and past the address to get some faucet. If you can not get faucet you can transfer some eth from one of you L1 accounts to starknet https://goerli.starkgate.starknet.io/. - -5. When you have be able to get some faucet you can deploy your account `starknet deploy_account` (for more information take a look at this documentation https://starknet.io/docs/hello_starknet/account_setup.html). - -6. Create a `.env` in `exemples/contracts/aggregator-consumer`and copy past your account address under `DEPLOYER_ACCOUNT_ADDRESS` and you private key under `DEPLOYER_PRIVATE_KEY`. You can find the private key in your home directory under `.starknet_accounts` folder in `starknet_open_zeppelin_accounts.json`. - -7. Start by deploying accounts with `npx ts-node ./scripts/deploy_accounts.ts` - -8. It'll write in previously created `.env`. - -9. Open that link https://faucet.goerli.starknet.io/ and past the address of the new accounts to get some faucet. - -10. Deploy the contracts by running `npx ts-node ./scripts/deploy_contracts.ts.` - -11. You can now read decimals and latest round data by running `yarn readDecimals` and `yarn readLatestRound`. - -12. You can update value and see continuously the data by openning another terminal and run `npx ts-node ./scripts/updateLatestRound.ts` in one terminal and `npx ts-node ./scripts/readContinuously` into another one. diff --git a/examples/contracts/aggregator-consumer/hardhat.config.ts b/examples/contracts/aggregator-consumer/hardhat.config.ts index 970a9b452..965b87a03 100644 --- a/examples/contracts/aggregator-consumer/hardhat.config.ts +++ b/examples/contracts/aggregator-consumer/hardhat.config.ts @@ -28,7 +28,7 @@ const config: HardhatUserConfig = { }, }, paths: { - cairoPaths: ['../../../contracts/src'], + cairoPaths: ['../../contracts/src'], }, } diff --git a/examples/contracts/aggregator-consumer/package.json b/examples/contracts/aggregator-consumer/package.json index 269007eee..2aff8abbe 100644 --- a/examples/contracts/aggregator-consumer/package.json +++ b/examples/contracts/aggregator-consumer/package.json @@ -1,26 +1,33 @@ { - "name": "@chainlink/starknet-ocr2-consumer", - "version": "0.1.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "npx hardhat --network localhost test", - "readDecimals": "npx ts-node ./scripts/readDecimals.ts", - "readLatestRound": "npx ts-node ./scripts/readLatestRound.ts" - }, - "keywords": [], - "author": "", - "license": "MIT", - "devDependencies": { - "@nomiclabs/hardhat-ethers": "^2.1.0", - "@shardlabs/starknet-hardhat-plugin": "^0.8.0-alpha.2", - "@types/chai": "^4.3.3", - "@types/mocha": "^9.1.1", - "chai": "^4.3.6", - "hardhat": "^*" - }, - "dependencies": { - "@chainlink/starknet": "^1.0.0", - "dotenv": "^16.0.1" - } + "name": "@chainlink/latest-starknet-ocr2-consumer", + "version": "0.1.0", + "description": "", + "main": "index.js", + "scripts": { + "compile:cairo": "scarb --profile release build", + "compile": "yarn compile:cairo", + "test": "yarn hardhat --network localhost test", + "deployAccount": "yarn ts-node ./scripts/deploy_accounts.ts", + "deployContracts": "yarn ts-node ./scripts/deploy_contracts.ts", + "readDecimals": "yarn ts-node ./scripts/readDecimals.ts", + "readLatestRound": "yarn ts-node ./scripts/readLatestRound.ts", + "readContinuously": "yarn ts-node ./scripts/readContinuously.ts", + "updateLatestRound": "yarn ts-node ./scripts/updateLatestRound.ts", + "getLatestPriceSeqCheck": "yarn ts-node ./scripts/getLatestPriceSeqCheck.ts" + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.1.0", + "@shardlabs/starknet-hardhat-plugin": "^0.8.0-alpha.0", + "@types/chai": "^4.3.3", + "@types/mocha": "^9.1.1", + "chai": "^4.3.6", + "hardhat": "^*" + }, + "dependencies": { + "@chainlink/starknet": "^1.0.0", + "dotenv": "^16.0.1" + } } diff --git a/examples/contracts/aggregator-consumer/scripts/consumerValidator.ts b/examples/contracts/aggregator-consumer/scripts/consumerValidator.ts index dd7c768db..6441c3ff2 100644 --- a/examples/contracts/aggregator-consumer/scripts/consumerValidator.ts +++ b/examples/contracts/aggregator-consumer/scripts/consumerValidator.ts @@ -5,7 +5,7 @@ import { HttpNetworkConfig } from 'hardhat/types' import dotenv from 'dotenv' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' import { deployMockContract, MockContract } from '@ethereum-waffle/mock-contract' -import { Account, Contract as StarknetContract } from 'starknet' +import { Account, CompiledContract, Contract as StarknetContract } from 'starknet' import { createDeployerAccount, loadContractPath, @@ -15,9 +15,9 @@ import { } from './utils' dotenv.config({ path: __dirname + '/../.env' }) -const UPTIME_FEED_PATH = - '../../../../contracts/starknet-artifacts/src/chainlink/cairo/emergency/SequencerUptimeFeed' -const UPTIME_FEED_NAME = 'sequencer_uptime_feed' +const UPTIME_FEED_PATH = '../../../../contracts/target/release/chainlink_SequencerUptimeFeed' + +// TODO: need to modify when the cairo-1.0 branch is rebased to include StarknetValidator changes let validator: Contract let mockStarknetMessengerFactory: ContractFactory @@ -83,7 +83,7 @@ export async function consumerValidator() { mockStarknetMessenger = await mockStarknetMessengerFactory.deploy(messageCancellationDelay) await mockStarknetMessenger.deployed() - const UptimeFeedArtifact = loadContractPath(UPTIME_FEED_PATH, UPTIME_FEED_NAME) + const UptimeFeedArtifact = loadContractPath(UPTIME_FEED_PATH) as CompiledContract const mockUptimeFeedDeploy = new StarknetContract( UptimeFeedArtifact.abi, @@ -91,6 +91,8 @@ export async function consumerValidator() { provider, ) + mockUptimeFeedDeploy.connect(account) + validator = await validatorFactory.deploy( mockStarknetMessenger.address, mockAccessController.address, @@ -101,16 +103,10 @@ export async function consumerValidator() { ) console.log('Validator address: ', validator.address) - const transaction = await account.execute( - { - contractAddress: mockUptimeFeedDeploy.address, - entrypoint: 'set_l1_sender', - calldata: [validator.address], - }, - [UptimeFeedArtifact.abi], - ) - await provider.waitForTransaction(transaction.transaction_hash) + const tx = await mockUptimeFeedDeploy.invoke('set_l1_sender', [validator.address]) + + await provider.waitForTransaction(tx.transaction_hash) await validator.addAccess(eoaValidator.address) setInterval(callFunction, 60_000) diff --git a/examples/contracts/aggregator-consumer/scripts/deploy_accounts.ts b/examples/contracts/aggregator-consumer/scripts/deploy_accounts.ts index f89a93e89..e031c63fa 100644 --- a/examples/contracts/aggregator-consumer/scripts/deploy_accounts.ts +++ b/examples/contracts/aggregator-consumer/scripts/deploy_accounts.ts @@ -1,60 +1,80 @@ -import fs from 'fs' -import dotenv from 'dotenv' -import { ec, CallData } from 'starknet' -import { loadContract_Account, createDeployerAccount, makeProvider } from './utils' +import * as fs from 'fs' +import * as dotenv from 'dotenv' +import { starknet } from 'hardhat' +import { exit } from 'node:process' +import { loadContract_Account, makeProvider } from './utils' +import { Account, CallData, ec, hash } from 'starknet' -dotenv.config({ path: __dirname + '/../.env' }) +const ENV_PATH = __dirname + '/../.env' + +dotenv.config({ path: ENV_PATH }) -const ACCOUNT_NAME = 'Account' interface UserAccount { - account: string - privateKey: Uint8Array + address: string + privateKey: string } -let firstAccount: UserAccount export async function deployAccount() { - firstAccount = await createAccount() + const account = await createAccount() - fs.appendFile(__dirname + '/../.env', '\nACCOUNT_ADDRESS=' + firstAccount.account, function ( - err, - ) { - if (err) throw err - }) - fs.appendFile(__dirname + '/../.env', '\nPRIVATE_KEY=' + firstAccount.privateKey, function (err) { - if (err) throw err - }) + try { + fs.appendFileSync(ENV_PATH, '\n# Autogenerated Dev Account from ./scripts/deploy_account.ts') + fs.appendFileSync(ENV_PATH, '\nDEPLOYER_ACCOUNT_ADDRESS=' + account.address) + fs.appendFileSync(ENV_PATH, '\nDEPLOYER_PRIVATE_KEY=' + account.privateKey) + } catch (err) { + throw err + } + + exit(0) // must manually exit due to ts-node weirdness } async function createAccount(): Promise { + // use pre-deployed accounts to deploy new OZ account + const accounts = await starknet.devnet.getPredeployedAccounts() + const account = accounts[0] const provider = makeProvider() + const fundedAccount = new Account(provider, account.address, account.private_key) - const predeployedAccount = createDeployerAccount(provider) - - const compiledAccount = loadContract_Account(ACCOUNT_NAME) + // define account parameters + const OZaccountClassHash = '0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f' + const compiledAccount = loadContract_Account('Account') const privateKey = ec.starkCurve.utils.randomPrivateKey() - const starkKeyPub = ec.starkCurve.getStarkKey(privateKey) const OZaccountConstructorCallData = CallData.compile({ publicKey: starkKeyPub }) - const declareTx = await predeployedAccount.declare({ - classHash: '0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f', + // declare OZ account + const declareTx = await fundedAccount.declare({ + classHash: OZaccountClassHash, contract: compiledAccount, }) - console.log('Declare new Account...') await provider.waitForTransaction(declareTx.transaction_hash) - const salt = '900080545022' - const accountResponse = await predeployedAccount.deploy({ - classHash: '0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f', + // fund OZ account + const OZcontractAddress = hash.calculateContractAddressFromHash( + starkKeyPub, + OZaccountClassHash, + OZaccountConstructorCallData, + 0, + ) + console.log('hash calculation contract address', OZcontractAddress) + await starknet.devnet.mint(OZcontractAddress, 1e22, true) + + // // deploy + const OZaccount = new Account(provider, OZcontractAddress, privateKey) + + const { transaction_hash, contract_address } = await OZaccount.deployAccount({ + classHash: OZaccountClassHash, constructorCalldata: OZaccountConstructorCallData, - salt, + addressSalt: starkKeyPub, }) console.log('Waiting for Tx to be Accepted on Starknet - OZ Account Deployment...') - await provider.waitForTransaction(accountResponse.transaction_hash) + await provider.waitForTransaction(transaction_hash) + + console.log('actual contract address', contract_address) - return { account: accountResponse.contract_address[0], privateKey: privateKey } + return { address: contract_address, privateKey: '0x' + Buffer.from(privateKey).toString('hex') } } deployAccount() diff --git a/examples/contracts/aggregator-consumer/scripts/deploy_contracts.ts b/examples/contracts/aggregator-consumer/scripts/deploy_contracts.ts index 6a778553a..ece2fed60 100644 --- a/examples/contracts/aggregator-consumer/scripts/deploy_contracts.ts +++ b/examples/contracts/aggregator-consumer/scripts/deploy_contracts.ts @@ -1,17 +1,23 @@ -import { Contract } from 'starknet' -import { loadContract, createDeployerAccount, loadContractPath, makeProvider } from './utils' -import fs from 'fs' -import dotenv from 'dotenv' +import { CairoAssembly, CallData, CompiledContract, Contract } from 'starknet' +import { + loadContract, + createDeployerAccount, + loadContractPath, + makeProvider, + loadCasmContract, +} from './utils' +import * as fs from 'fs' +import * as dotenv from 'dotenv' -const CONSUMER_NAME = 'Aggregator_consumer' +const AGGREGATOR = 'MockAggregator' -const MOCK_NAME = 'MockAggregator' +const AGGREGATOR_PATH = '../../../../contracts/target/release/chainlink_MockAggregator' -const PRICE_CONSUMER_NAME = 'Price_Consumer_With_Sequencer_Check' +const CONSUMER = 'AggregatorConsumer' -const UPTIME_FEED_PATH = - '../../../../contracts/starknet-artifacts/src/chainlink/cairo/emergency/SequencerUptimeFeed' -const UPTIME_FEED_NAME = 'sequencer_uptime_feed' +const UPTIME_FEED_PATH = '../../../../contracts/target/release/chainlink_SequencerUptimeFeed' + +const PRICE_CONSUMER = 'AggregatorPriceConsumerWithSequencer' const DECIMALS = '18' @@ -19,36 +25,41 @@ dotenv.config({ path: __dirname + '/../.env' }) export async function deployContract() { const provider = makeProvider() - const MockArtifact = loadContract(MOCK_NAME) - const AggregatorArtifact = loadContract(CONSUMER_NAME) - const priceConsumerArtifact = loadContract(PRICE_CONSUMER_NAME) - const UptimeFeedArtifact = loadContractPath(UPTIME_FEED_PATH, UPTIME_FEED_NAME) + const AggregatorArtifact = loadContractPath(`${AGGREGATOR_PATH}.sierra`) as CompiledContract + const ConsumerArtifact = loadContract(CONSUMER) + const PriceConsumerArtifact = loadContract(PRICE_CONSUMER) + const UptimeFeedArtifact = loadContractPath(`${UPTIME_FEED_PATH}.sierra`) as CompiledContract const account = createDeployerAccount(provider) - const declareDeployMock = await account.declareAndDeploy({ - contract: MockArtifact, + console.log('Deploying Contracts...(this may take 3-5 minutes)') + + const declareDeployAggregator = await account.declareAndDeploy({ + casm: loadContractPath(`${AGGREGATOR_PATH}.casm`) as CairoAssembly, + contract: AggregatorArtifact, constructorCalldata: [DECIMALS], }) - const mockDeploy = new Contract( - MockArtifact.abi, - declareDeployMock.deploy.contract_address, + const aggregatorDeploy = new Contract( + AggregatorArtifact.abi, + declareDeployAggregator.deploy.contract_address, provider, ) - const declareDeployAggregator = await account.declareAndDeploy({ - contract: AggregatorArtifact, - constructorCalldata: [mockDeploy.address as string], + const declareDeployConsumer = await account.declareAndDeploy({ + casm: loadCasmContract(CONSUMER), + contract: ConsumerArtifact, + constructorCalldata: [aggregatorDeploy.address as string], }) const consumerDeploy = new Contract( - AggregatorArtifact.abi, - declareDeployAggregator.deploy.contract_address, + ConsumerArtifact.abi, + declareDeployConsumer.deploy.contract_address, provider, ) const declareDeployUptimeFeed = await account.declareAndDeploy({ + casm: loadContractPath(`${UPTIME_FEED_PATH}.casm`) as CairoAssembly, contract: UptimeFeedArtifact, constructorCalldata: ['0', account.address], }) @@ -60,12 +71,13 @@ export async function deployContract() { ) const declareDeployPriceConsumer = await account.declareAndDeploy({ - contract: priceConsumerArtifact, - constructorCalldata: [uptimeFeedDeploy.address as string, mockDeploy.address as string], + casm: loadCasmContract(PRICE_CONSUMER), + contract: PriceConsumerArtifact, + constructorCalldata: [uptimeFeedDeploy.address as string, aggregatorDeploy.address as string], }) const priceConsumerDeploy = new Contract( - priceConsumerArtifact.abi, + PriceConsumerArtifact.abi, declareDeployPriceConsumer.deploy.contract_address, provider, ) @@ -73,7 +85,7 @@ export async function deployContract() { fs.appendFile(__dirname + '/../.env', '\nCONSUMER=' + consumerDeploy.address, function (err) { if (err) throw err }) - fs.appendFile(__dirname + '/../.env', '\nMOCK=' + mockDeploy.address, function (err) { + fs.appendFile(__dirname + '/../.env', '\nMOCK=' + aggregatorDeploy.address, function (err) { if (err) throw err }) fs.appendFile( diff --git a/examples/contracts/aggregator-consumer/scripts/getLatestPrice.ts b/examples/contracts/aggregator-consumer/scripts/getLatestPrice.ts deleted file mode 100644 index 66574815c..000000000 --- a/examples/contracts/aggregator-consumer/scripts/getLatestPrice.ts +++ /dev/null @@ -1,54 +0,0 @@ -import dotenv from 'dotenv' -import { createDeployerAccount, loadContract, loadContractPath, makeProvider } from './utils' -import { Contract } from 'starknet' - -const PRICE_CONSUMER_NAME = 'Price_Consumer_With_Sequencer_Check' -const UPTIME_FEED_PATH = - '../../../../contracts/starknet-artifacts/src/chainlink/cairo/emergency/SequencerUptimeFeed' -const UPTIME_FEED_NAME = 'sequencer_uptime_feed' - -dotenv.config({ path: __dirname + '/../.env' }) - -export async function getLatestPrice() { - const provider = makeProvider() - - const account = createDeployerAccount(provider) - - const priceConsumerArtifact = loadContract(PRICE_CONSUMER_NAME) - const UptimeFeedArtifact = loadContractPath(UPTIME_FEED_PATH, UPTIME_FEED_NAME) - - const priceConsumer = new Contract( - priceConsumerArtifact.abi, - process.env.PRICE_CONSUMER as string, - provider, - ) - const uptimeFeed = new Contract( - UptimeFeedArtifact.abi, - process.env.UPTIME_FEED as string, - provider, - ) - - const transaction = await account.execute( - { - contractAddress: uptimeFeed.address, - entrypoint: 'add_access', - calldata: [priceConsumer.address], - }, - [UptimeFeedArtifact.abi], - ) - - console.log('Waiting for Tx to be Accepted on Starknet...') - await provider.waitForTransaction(transaction.transaction_hash) - - const lat = await uptimeFeed.call('latest_round_data') - const latestPrice = await account.callContract({ - contractAddress: priceConsumer.address, - entrypoint: 'get_latest_price', - calldata: [], - }) - - console.log('answer= ', latestPrice) - return latestPrice -} - -getLatestPrice() diff --git a/examples/contracts/aggregator-consumer/scripts/getLatestPriceSeqCheck.ts b/examples/contracts/aggregator-consumer/scripts/getLatestPriceSeqCheck.ts new file mode 100644 index 000000000..d58cb9c27 --- /dev/null +++ b/examples/contracts/aggregator-consumer/scripts/getLatestPriceSeqCheck.ts @@ -0,0 +1,79 @@ +import dotenv from 'dotenv' +import { createDeployerAccount, loadContract, loadContractPath, makeProvider } from './utils' +import { CompiledContract, Contract, GatewayError } from 'starknet' + +const PRICE_CONSUMER_NAME = 'AggregatorPriceConsumerWithSequencer' +const UPTIME_FEED_PATH = '../../../../contracts/target/release/chainlink_SequencerUptimeFeed.sierra' + +dotenv.config({ path: __dirname + '/../.env' }) + +const SEQ_UP_REPORT_STALE = 'L2 seq up & report stale' +const SEQ_DOWN_REPORT_STALE = 'L2 seq down & report stale' +const SEQ_DOWN_REPORT_OK = 'L2 seq down & report ok' + +const HEX_SEQ_UP_REPORT_STALE = revertMessageHex(SEQ_UP_REPORT_STALE) +const HEX_SEQ_DOWN_REPORT_STALE = revertMessageHex(SEQ_DOWN_REPORT_STALE) +const HEX_SEQ_DOWN_REPORT_OK = revertMessageHex(SEQ_DOWN_REPORT_OK) + +function revertMessageHex(msg: string): string { + return Buffer.from(msg, 'utf8').toString('hex') +} + +export async function getLatestPrice() { + const provider = makeProvider() + + const account = createDeployerAccount(provider) + + const priceConsumerArtifact = loadContract(PRICE_CONSUMER_NAME) + const UptimeFeedArtifact = loadContractPath(UPTIME_FEED_PATH) as CompiledContract + + const priceConsumer = new Contract( + priceConsumerArtifact.abi, + process.env.PRICE_CONSUMER as string, + provider, + ) + const uptimeFeed = new Contract( + UptimeFeedArtifact.abi, + process.env.UPTIME_FEED as string, + provider, + ) + + priceConsumer.connect(account) + uptimeFeed.connect(account) + + let res = await uptimeFeed.invoke('add_access', [priceConsumer.address]) + console.log('Waiting for add_access Tx to be Accepted on Starknet...') + await provider.waitForTransaction(res.transaction_hash) + + try { + console.log('Waiting to get latest price') + const latestPrice = await priceConsumer.call('get_latest_price', []) + console.log('answer= ', latestPrice) + } catch (e) { + // transaction reverted because sequencer is down or report is stale + console.log('Getting latest price not possible (reason below)') + if (e instanceof GatewayError) { + switch (true) { + case e.message.includes(HEX_SEQ_UP_REPORT_STALE): { + console.log(SEQ_UP_REPORT_STALE) + break + } + case e.message.includes(HEX_SEQ_DOWN_REPORT_STALE): { + console.log(SEQ_DOWN_REPORT_STALE) + break + } + case e.message.includes(HEX_SEQ_DOWN_REPORT_OK): { + console.log(SEQ_DOWN_REPORT_OK) + break + } + default: + console.log(e) + break + } + } else { + console.log(e) + } + } +} + +getLatestPrice() diff --git a/examples/contracts/aggregator-consumer/scripts/readContinuously.ts b/examples/contracts/aggregator-consumer/scripts/readContinuously.ts index 17a11602e..442c732f9 100644 --- a/examples/contracts/aggregator-consumer/scripts/readContinuously.ts +++ b/examples/contracts/aggregator-consumer/scripts/readContinuously.ts @@ -1,9 +1,9 @@ -import { Contract, Account, CallContractResponse } from 'starknet' +import { Contract, Account, CallContractResponse, Result } from 'starknet' import { createDeployerAccount, loadContract, makeProvider } from './utils' import dotenv from 'dotenv' -const CONTRACT_NAME = 'Aggregator_consumer' +const CONTRACT_NAME = 'AggregatorConsumer' let account: Account let consumer: Contract @@ -17,31 +17,24 @@ async function readContinuously() { const AggregatorArtifact = loadContract(CONTRACT_NAME) consumer = new Contract(AggregatorArtifact.abi, process.env.CONSUMER as string) - setInterval(callFunction, 30000) + consumer.connect(account) + setInterval(callFunction, 3000) } async function callFunction() { - const latestRound = await account.callContract({ - contractAddress: consumer.address, - entrypoint: 'readLatestRound', - calldata: [], - }) - - const decimals = await account.callContract({ - contractAddress: consumer.address, - entrypoint: 'readDecimals', - calldata: [], - }) + const latestRound = await consumer.call('read_latest_round') + const decimals = await consumer.call('read_decimals') printResult(latestRound, decimals) } -function printResult(latestRound: CallContractResponse, decimals: CallContractResponse) { - console.log('round_id= ', parseInt(latestRound.result[0], 16)) - console.log('answer= ', parseInt(latestRound.result[1], 16)) - console.log('block_num= ', parseInt(latestRound.result[2], 16)) - console.log('staerted_at= ', parseInt(latestRound.result[3], 16)) - console.log('updated_at= ', parseInt(latestRound.result[4], 16)) - console.log('decimals= ', parseInt(decimals.result[0], 16)) +function printResult(latestRound: Result, decimals: Result) { + console.log('---------------') + console.log('round_id= ', latestRound['round_id']) + console.log('answer= ', latestRound['answer']) + console.log('block_num= ', latestRound['block_num']) + console.log('started_at= ', latestRound['started_at']) + console.log('updated_at= ', latestRound['updated_at']) + console.log('decimals= ', decimals.toString()) } readContinuously() diff --git a/examples/contracts/aggregator-consumer/scripts/readDecimals.ts b/examples/contracts/aggregator-consumer/scripts/readDecimals.ts index 4258cfdce..7d76b80bb 100644 --- a/examples/contracts/aggregator-consumer/scripts/readDecimals.ts +++ b/examples/contracts/aggregator-consumer/scripts/readDecimals.ts @@ -2,7 +2,7 @@ import { Account, Contract } from 'starknet' import { createDeployerAccount, loadContract, makeProvider } from './utils' import dotenv from 'dotenv' -const CONSUMER_NAME = 'Aggregator_consumer' +const CONSUMER_NAME = 'AggregatorConsumer' let account: Account let consumer: Contract @@ -15,14 +15,12 @@ export async function readDecimals() { const AggregatorArtifact = loadContract(CONSUMER_NAME) consumer = new Contract(AggregatorArtifact.abi, process.env.CONSUMER as string) - const decimals = await account.callContract({ - contractAddress: consumer.address, - entrypoint: 'readDecimals', - calldata: [], - }) + consumer.connect(account) - console.log('decimals= ', parseInt(decimals.result[0], 16)) - return parseInt(decimals.result[0], 16) + const decimals = await consumer.call('read_decimals') + + console.log('decimals= ', decimals.toString()) + return decimals } readDecimals() diff --git a/examples/contracts/aggregator-consumer/scripts/readLatestRound.ts b/examples/contracts/aggregator-consumer/scripts/readLatestRound.ts index e9c82c36d..762144a1b 100644 --- a/examples/contracts/aggregator-consumer/scripts/readLatestRound.ts +++ b/examples/contracts/aggregator-consumer/scripts/readLatestRound.ts @@ -1,9 +1,9 @@ -import { Account, Contract, CallContractResponse } from 'starknet' +import { Account, Contract, Result } from 'starknet' import { createDeployerAccount, loadContract, makeProvider } from './utils' import dotenv from 'dotenv' -const CONSUMER_NAME = 'Aggregator_consumer' +const CONSUMER_NAME = 'AggregatorConsumer' let account: Account let consumer: Contract @@ -16,21 +16,20 @@ export async function readLatestRound() { const AggregatorArtifact = loadContract(CONSUMER_NAME) consumer = new Contract(AggregatorArtifact.abi, process.env.CONSUMER as string) - const latestRound = await account.callContract({ - contractAddress: consumer.address, - entrypoint: 'readLatestRound', - calldata: [], - }) + consumer.connect(account) + + const latestRound = await consumer.call('read_latest_round') + printResult(latestRound) return latestRound } -function printResult(latestRound: CallContractResponse) { - console.log('\nround_id= ', parseInt(latestRound.result[0], 16)) - console.log('answer= ', parseInt(latestRound.result[1], 16)) - console.log('block_num= ', parseInt(latestRound.result[2], 16)) - console.log('observation_timestamp= ', parseInt(latestRound.result[3], 16)) - console.log('transmission_timestamp= ', parseInt(latestRound.result[4], 16)) +function printResult(latestRound: Result) { + console.log('round_id= ', latestRound['round_id']) + console.log('answer= ', latestRound['answer']) + console.log('block_num= ', latestRound['block_num']) + console.log('started_at= ', latestRound['started_at']) + console.log('updated_at= ', latestRound['updated_at']) } readLatestRound() diff --git a/examples/contracts/aggregator-consumer/scripts/updateLatestRound.ts b/examples/contracts/aggregator-consumer/scripts/updateLatestRound.ts index 95b0c0515..5d0b1bcd4 100644 --- a/examples/contracts/aggregator-consumer/scripts/updateLatestRound.ts +++ b/examples/contracts/aggregator-consumer/scripts/updateLatestRound.ts @@ -1,5 +1,5 @@ -import { Account, Contract, Provider, ec, number } from 'starknet' -import { loadContract, makeProvider } from './utils' +import { Account, CompiledContract, Contract, Provider, ec, number } from 'starknet' +import { loadContract, loadContractPath, makeProvider } from './utils' import dotenv from 'dotenv' interface Transmission { @@ -10,6 +10,7 @@ interface Transmission { } const CONTRACT_NAME = 'MockAggregator' +const CONTRACT_PATH = '../../../../contracts/target/release/chainlink_MockAggregator' let account: Account let mock: Contract let transmission: Transmission @@ -32,12 +33,14 @@ async function updateLatestRound() { transmission_timestamp: 0, } - const keyPair = ec.getKeyPair(process.env.PRIVATE_KEY as string) - account = new Account(provider, process.env.ACCOUNT_ADDRESS as string, keyPair) + const privateKey = process.env.DEPLOYER_PRIVATE_KEY as string + account = new Account(provider, process.env.DEPLOYER_ACCOUNT_ADDRESS as string, privateKey) - const MockArtifact = loadContract(CONTRACT_NAME) + const MockArtifact = loadContractPath(`${CONTRACT_PATH}.sierra`) as CompiledContract mock = new Contract(MockArtifact.abi, process.env.MOCK as string) + mock.connect(account) + transmission.answer = Number(await input('Enter a number for new answer: ')) transmission.block_num = Number(await input('Enter a number for new block_num: ')) transmission.observation_timestamp = Number( @@ -48,26 +51,19 @@ async function updateLatestRound() { ) rl.close() - callFunction(transmission) + await callFunction(transmission) } async function callFunction(transmission: Transmission) { - const transaction = await account.execute( - { - contractAddress: mock.address, - entrypoint: 'set_latest_round_data', - calldata: [ - transmission.answer, - transmission.block_num, - transmission.observation_timestamp, - transmission.transmission_timestamp, - ], - }, - [mock.abi], - { maxFee: 1e18 }, - ) - console.log('Waiting for Tx to be Accepted on Starknet - Aggregator consumer Deployment...') - await provider.waitForTransaction(transaction.transaction_hash) + const tx = await mock.invoke('set_latest_round_data', [ + transmission.answer, + transmission.block_num, + transmission.observation_timestamp, + transmission.transmission_timestamp, + ]) + + console.log('Waiting for Tx to be Accepted on Starknet: Updating Latest Round') + await provider.waitForTransaction(tx.transaction_hash) } function input(prompt: string) { diff --git a/examples/contracts/aggregator-consumer/scripts/utils.ts b/examples/contracts/aggregator-consumer/scripts/utils.ts index 64986325b..ef4e8cf54 100644 --- a/examples/contracts/aggregator-consumer/scripts/utils.ts +++ b/examples/contracts/aggregator-consumer/scripts/utils.ts @@ -1,38 +1,44 @@ import fs from 'fs' import dotenv from 'dotenv' -import { CompiledContract, json, ec, Account, Provider, constants } from 'starknet' +import { CompiledContract, json, ec, Account, Provider, constants, CairoAssembly } from 'starknet' const DEVNET_NAME = 'devnet' -export const loadContract = (name: string): CompiledContract => { +export const loadContract_Account = (name: string): CompiledContract => { return json.parse( fs - .readFileSync(`${__dirname}/../starknet-artifacts/contracts/${name}.cairo/${name}.json`) + .readFileSync( + `${__dirname}/../../../../node_modules/@shardlabs/starknet-hardhat-plugin/dist/contract-artifacts/OpenZeppelinAccount/0.5.1/${name}.cairo/${name}.json`, + ) .toString('ascii'), ) } -export const loadContractPath = (path: string, name: string): CompiledContract => { +export const loadContract = (name: string): CompiledContract => { return json.parse( - fs.readFileSync(`${__dirname}/${path}/${name}.cairo/${name}.json`).toString('ascii'), + fs + .readFileSync(`${__dirname}/../target/release/chainlink_examples_${name}.sierra.json`) + .toString('ascii'), ) } -export const loadContract_Account = (name: string): CompiledContract => { +export const loadCasmContract = (name: string): CairoAssembly => { return json.parse( fs - .readFileSync( - `${__dirname}/../../../../node_modules/@shardlabs/starknet-hardhat-plugin/dist/contract-artifacts/OpenZeppelinAccount/0.5.1/${name}.cairo/${name}.json`, - ) + .readFileSync(`${__dirname}/../target/release/chainlink_examples_${name}.casm.json`) .toString('ascii'), ) } +export const loadContractPath = (path: string): CompiledContract | CairoAssembly => { + return json.parse(fs.readFileSync(`${__dirname}/${path}.json`).toString('ascii')) +} + export const loadContract_Solidity = (path: string, name: string): any => { return json.parse( fs .readFileSync( - `${__dirname}/../../../../contracts/artifacts/solidity/${path}/${name}.sol/${name}.json`, + `${__dirname}/../../../../contracts/artifacts/src/chainlink/solidity/${path}/${name}.sol/${name}.json`, ) .toString('ascii'), ) diff --git a/examples/contracts/aggregator-consumer/src/lib.cairo b/examples/contracts/aggregator-consumer/src/lib.cairo new file mode 100644 index 000000000..f14ff3b83 --- /dev/null +++ b/examples/contracts/aggregator-consumer/src/lib.cairo @@ -0,0 +1 @@ +mod ocr2; diff --git a/examples/contracts/aggregator-consumer/src/ocr2.cairo b/examples/contracts/aggregator-consumer/src/ocr2.cairo new file mode 100644 index 000000000..6c9dcdfe1 --- /dev/null +++ b/examples/contracts/aggregator-consumer/src/ocr2.cairo @@ -0,0 +1,2 @@ +mod consumer; +mod price_consumer_with_sequencer; diff --git a/examples/contracts/aggregator-consumer/src/ocr2/consumer.cairo b/examples/contracts/aggregator-consumer/src/ocr2/consumer.cairo new file mode 100644 index 000000000..3da49258e --- /dev/null +++ b/examples/contracts/aggregator-consumer/src/ocr2/consumer.cairo @@ -0,0 +1,30 @@ +#[starknet::contract] +mod AggregatorConsumer { + use starknet::ContractAddress; + + use chainlink::ocr2::aggregator::Round; + + use chainlink::ocr2::aggregator_proxy::IAggregator; + use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcher; + use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcherTrait; + + #[storage] + struct Storage { + _ocr_address: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, ocr_address: ContractAddress) { + self._ocr_address.write(ocr_address); + } + + #[external(v0)] + fn read_latest_round(self: @ContractState) -> Round { + IAggregatorDispatcher { contract_address: self._ocr_address.read() }.latest_round_data() + } + + #[external(v0)] + fn read_decimals(self: @ContractState) -> u8 { + IAggregatorDispatcher { contract_address: self._ocr_address.read() }.decimals() + } +} diff --git a/examples/contracts/aggregator-consumer/src/ocr2/price_consumer_with_sequencer.cairo b/examples/contracts/aggregator-consumer/src/ocr2/price_consumer_with_sequencer.cairo new file mode 100644 index 000000000..2e4536cf7 --- /dev/null +++ b/examples/contracts/aggregator-consumer/src/ocr2/price_consumer_with_sequencer.cairo @@ -0,0 +1,65 @@ +#[starknet::contract] +mod AggregatorPriceConsumerWithSequencer { + use box::BoxTrait; + use starknet::ContractAddress; + use zeroable::Zeroable; + use traits::Into; + + use chainlink::ocr2::aggregator::Round; + use chainlink::ocr2::aggregator_proxy::IAggregator; + use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcher; + use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcherTrait; + + #[storage] + struct Storage { + _uptime_feed_address: ContractAddress, + _aggregator_address: ContractAddress, + } + + // Sequencer-aware aggregator consumer + // retrieves the latest price from the data feed only if + // the uptime feed is not stale (stale = older than 60 seconds) + #[constructor] + fn constructor( + ref self: ContractState, + uptime_feed_address: ContractAddress, + aggregator_address: ContractAddress + ) { + assert(!uptime_feed_address.is_zero(), 'uptime feed is 0'); + assert(!aggregator_address.is_zero(), 'aggregator is 0'); + self._uptime_feed_address.write(uptime_feed_address); + self._aggregator_address.write(aggregator_address); + } + + + #[extrnal(v0)] + fn get_latest_price(self: @ContractState) -> u128 { + assert_sequencer_healthy(self); + let round = IAggregatorDispatcher { + contract_address: self._aggregator_address.read() + }.latest_round_data(); + round.answer + } + + fn assert_sequencer_healthy(self: @ContractState) { + let round = IAggregatorDispatcher { + contract_address: self._uptime_feed_address.read() + }.latest_round_data(); + let timestamp = starknet::info::get_block_info().unbox().block_timestamp; + + // After 60 sec the report is considered stale + let report_stale = timestamp - round.updated_at > 60_u64; + + // 0 if the sequencer is up and 1 if it is down. No other options besides 1 and 0 + match round.answer.into() { + 0 => { + assert(!report_stale, 'L2 seq up & report stale'); + }, + _ => { + assert(!report_stale, 'L2 seq down & report stale'); + assert(false, 'L2 seq down & report ok'); + } + } + } +} + diff --git a/examples/contracts/aggregator-consumer/test/mock/Aggregator_consumer.test.ts b/examples/contracts/aggregator-consumer/test/mock/Aggregator_consumer.test.ts deleted file mode 100644 index b76bd69a4..000000000 --- a/examples/contracts/aggregator-consumer/test/mock/Aggregator_consumer.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { starknet } from 'hardhat' -import { assert } from 'chai' -import { StarknetContract, Account } from 'hardhat/types/runtime' -import { account } from '@chainlink/starknet' - -describe('ContractTestsMock', function () { - this.timeout(600_000) - const opts = account.makeFunderOptsFromEnv() - const funder = new account.Funder(opts) - - let alice: Account - let MockContract: StarknetContract - let ConsumerContract: StarknetContract - - before(async () => { - alice = await starknet.OpenZeppelinAccount.createAccount() - - await funder.fund([{ account: alice.address, amount: 1e21 }]) - await alice.deployAccount() - - const decimals = 18 - const MockFactory = await starknet.getContractFactory('MockAggregator.cairo') - await alice.declare(MockFactory) - - MockContract = await alice.deploy(MockFactory, { decimals: decimals }) - - const ConsumerFactory = await starknet.getContractFactory('Aggregator_consumer.cairo') - await alice.declare(ConsumerFactory) - - ConsumerContract = await alice.deploy(ConsumerFactory, { - address: MockContract.address, - }) - }) - - it('should set and read latest round data successfully', async () => { - await alice.invoke(MockContract, 'set_latest_round_data', { - answer: 12, - block_num: 1, - observation_timestamp: 14325, - transmission_timestamp: 87654, - }) - - const { round: round } = await ConsumerContract.call('readLatestRound', {}) - assert.equal(round.answer, 12) - assert.equal(round.block_num, 1) - assert.equal(round.started_at, 14325) - assert.equal(round.updated_at, 87654) - }) - - it('should set and read latest round data successfully for the second time', async () => { - await alice.invoke(MockContract, 'set_latest_round_data', { - answer: 19, - block_num: 2, - observation_timestamp: 14345, - transmission_timestamp: 62543, - }) - - const { round: round } = await ConsumerContract.call('readLatestRound', {}) - assert.equal(round.answer, 19) - assert.equal(round.block_num, 2) - assert.equal(round.started_at, 14345) - assert.equal(round.updated_at, 62543) - }) - - it('should set and read latest round data successfully for the third time', async () => { - await alice.invoke(MockContract, 'set_latest_round_data', { - answer: 42, - block_num: 3, - observation_timestamp: 9876, - transmission_timestamp: 27839, - }) - const { round: round } = await ConsumerContract.call('readLatestRound', {}) - assert.equal(round.answer, 42) - assert.equal(round.block_num, 3) - assert.equal(round.started_at, 9876) - assert.equal(round.updated_at, 27839) - }) - - it('should read Decimals successfully', async () => { - const decimals = await ConsumerContract.call('readDecimals', {}) - assert.equal(decimals.decimals, 18) - }) -}) diff --git a/examples/contracts/aggregator-consumer/test/mock/Examples_test.test.ts b/examples/contracts/aggregator-consumer/test/mock/Examples_test.test.ts deleted file mode 100644 index 562648fec..000000000 --- a/examples/contracts/aggregator-consumer/test/mock/Examples_test.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { starknet, type } from 'hardhat' -import { assert } from 'chai' -import { account } from '@chainlink/starknet' -import fs from 'fs' -import { readDecimals } from '../../scripts/readDecimals' -import { readLatestRound } from '../../scripts/readLatestRound' -import { createDeployerAccount, loadContract, makeProvider } from '../../scripts/utils' -import { Contract, Provider, number } from 'starknet' -import dotenv from 'dotenv' -import { deployContract } from '../../scripts/deploy_contracts' -import { getLatestPrice } from '../../scripts/getLatestPrice' -import { deployAccount } from '../../scripts/deploy_accounts' -import { consumerValidator } from '../../scripts/consumerValidator' - -describe('ExamplesTests', function () { - dotenv.config({ path: __dirname + '/../../.env' }) - - this.timeout(600_000) - const opts = account.makeFunderOptsFromEnv() - const funder = new account.Funder(opts) - - let alice: type.Account - let provider: Provider - - before(async () => { - provider = makeProvider() - - alice = await starknet.OpenZeppelinAccount.createAccount() - - await funder.fund([{ account: alice.address, amount: 1e21 }]) - await alice.deployAccount() - fs.appendFile( - __dirname + '/../../.env', - '\nDEPLOYER_ACCOUNT_ADDRESS=' + alice.address, - function (err) { - if (err) throw err - }, - ) - fs.appendFile( - __dirname + '/../../.env', - '\nDEPLOYER_PRIVATE_KEY=' + alice.privateKey, - function (err) { - if (err) throw err - }, - ) - }) - - it('should deploy contract', async () => { - await deployContract() - }) - - it('should deploy account', async () => { - await deployAccount() - }) - - it('should set and read latest round data successfully', async () => { - const MockArtifact = loadContract('MockAggregator') - const mock = new Contract(MockArtifact.abi, process.env.MOCK as string, provider) - - const bob = createDeployerAccount(provider) - await funder.fund([{ account: bob.address, amount: 1e21 }]) - const transaction = await bob.execute( - { - contractAddress: mock.address, - entrypoint: 'set_latest_round_data', - calldata: [42, 3, 9876, 27839], - }, - [mock.abi], - ) - console.log('Waiting for Tx to be Accepted on Starknet - Aggregator consumer Deployment...') - await provider.waitForTransaction(transaction.transaction_hash) - - const latestRound = await readLatestRound() - assert.equal(parseInt(latestRound.result[1], 16), 42) - assert.equal(parseInt(latestRound.result[2], 16), 3) - assert.equal(parseInt(latestRound.result[3], 16), 9876) - assert.equal(parseInt(latestRound.result[4], 16), 27839) - }) - - it('should read Decimals successfully', async () => { - const decimals = await readDecimals() - assert.equal(decimals, 18) - }) - - it('should test consumer validator', async () => { - await consumerValidator() - }) - - it('should get latest price', async () => { - const latestPrice = await getLatestPrice() - assert.equal(parseInt(latestPrice.result[0], 16), 42) - }) -}) diff --git a/examples/contracts/aggregator-consumer/tsconfig.json b/examples/contracts/aggregator-consumer/tsconfig.json index 05f2b5dc7..445119cd5 100644 --- a/examples/contracts/aggregator-consumer/tsconfig.json +++ b/examples/contracts/aggregator-consumer/tsconfig.json @@ -4,12 +4,14 @@ "outDir": "dist", }, "include": [ - "./hardhat.config.ts", "scripts/**/*" ], "exclude": [ "dist", "**/*.spec.ts", "**/*.test.ts" + ], + "files": [ + "./hardhat.config.ts" ] } diff --git a/examples/contracts/proxy-consumer/.gitignore b/examples/contracts/proxy-consumer/.gitignore index 80b62961b..eb5a316cb 100644 --- a/examples/contracts/proxy-consumer/.gitignore +++ b/examples/contracts/proxy-consumer/.gitignore @@ -1,2 +1 @@ -cache/ -starknet-artifacts/ +target diff --git a/examples/contracts/proxy-consumer/README.md b/examples/contracts/proxy-consumer/README.md index 5cf3a4242..4cce785e9 100644 --- a/examples/contracts/proxy-consumer/README.md +++ b/examples/contracts/proxy-consumer/README.md @@ -2,15 +2,45 @@ This is a simple example for how to read Chainlink data feeds on Starknet. ### Requirements -Set up your environment to run the examples. +Set up your environment to run the examples. Make sure to clone this repo before you start these instructions + +1. Clone the [smartcontractkit/chainlink-starknet](https://github.com/smartcontractkit/chainlink-starknet) repository, which includes the example contracts for this guide: + ``` + git clone https://github.com/smartcontractkit chainlink-starknet.git + git submodule update --init --recursive + ``` + We use git submodules to pin specific versions of cairo and scarb (you'll see this come into play later). + +1. Setup your local Starknet environment. We will install starknet cli tools, the rust cairo compiler, and scarb which is a framework and dependency manager for cairo. If you already have them installed, feel free to skip this step (if you later find that your versions are not working, follow the steps below because they are pinned to specific versions). + ``` + # Part 1: Install starknet cli tools via virtualenv + + cd chainlink-starknet + # tested on python 3.9 and onwards + python -m venv venv + source ./venv/bin/activate + pip install -r contracts/requirements.txt + ``` + Next we'll install cairo. If you've already installed cairo, make sure to disable that path first. + ``` + # Part 2: Install cairo + cd vendor/cairo && cargo build --all --release + # Add cairo executable to your path + export PATH="$HOME/path/to/chainlink-starknet/vendor/cairo/target/release:$PATH" + ``` + Lastly, we'll install scarb. You should be able to install the scarb 0.2.0-alpha.2 binary from [here](https://github.com/software-mansion/scarb/releases/tag/v0.2.0-alpha.2) for your operating system. Install it in your `$HOME` directory and add it to your path. + ``` + # assuming you've downloaded the scarb artifact already to your $HOME directory + export PATH="$HOME/scarb/bin:$PATH" + ``` + Awesome, that was a lot of work, but now we're ready to start! + +1. Set up a Starknet account. Follow instructions to [set up environment variables](https://docs.starknet.io/documentation/getting_started/deploying_contracts/#setting_up_environment_variables) and [deploy an account](https://docs.starknet.io/documentation/getting_started/deploying_contracts/#setting_up_an_account). This deploys an account on Starknet's `alpha-goerli` network and funds it with [testnet ETH](https://faucet.goerli.starknet.io/). These examples expect the OpenZeppelin wallet, which stores your addresses and private keys at `~/.starknet_accounts/` by default. -1. [Setup your local Starknet environment](https://starknet.io/docs/quickstart.html). Note that a Python version in the `>=3.6 <=3.9` range is required for compiling and deploying contracts on-chain. The [`cairo-lang` Python package](https://pypi.org/project/cairo-lang/) is not compatible with newer versions of Python as of the [`cairo-lang` 0.10.3](https://pypi.org/project/cairo-lang/0.10.3/) package. Check [starknet.io](https://starknet.io/docs/quickstart.html) for the latest requirements. -1. [Set up a Starknet account](https://starknet.io/docs/hello_starknet/account_setup.html) on Starknet's `alpha-goerli` network and fund it with [testnet ETH](https://faucet.goerli.starknet.io/). These examples expect the OpenZeppelin wallet, which stores your addresses and private keys at `~/.starknet_accounts/starknet_open_zeppelin_accounts.json` by default. 1. [Install NodeJS](https://nodejs.org/en/download/) in the version in the `>=14 <=18` version range. 1. [Install Yarn](https://classic.yarnpkg.com/lang/en/docs/install/). -1. Clone the [smartcontractkit/chainlink-starknet](https://github.com/smartcontractkit/chainlink-starknet) repository, which includes the example contracts for this guide: `git clone https://github.com/smartcontractkit/chainlink-starknet.git` -1. In your clone of the [chainlink-starknet](https://github.com/smartcontractkit/chainlink-starknet) repository, change directories to the proxy consumer example: `cd ./chainlink-starknet/examples/contracts/proxy-consumer/` -1. Run `yarn install` to install the required packages including [Starknet.js](https://www.starknetjs.com/), [HardHat](https://hardhat.org/), and the [Starknet Hardhat Plugin](https://shard-labs.github.io/starknet-hardhat-plugin/). +1. Change directories to the proxy consumer example: `cd ./chainlink-starknet/examples/new_contracts/proxy_consumer/` +1. Run `yarn install` to install the required packages including [Starknet.js](https://www.starknetjs.com/) ### Running the on-chain example @@ -24,8 +54,7 @@ Set up your environment to run the examples. ```shell export DEPLOYER_PRIVATE_KEY= ``` - -1. Run `yarn build` to run Hardhat and create `./starknet-artifacts/` with the compiled contracts. Hardhat uses the [`@shardlabs/starknet-hardhat-plugin` package](https://www.npmjs.com/package/@shardlabs/starknet-hardhat-plugin) for this step. +1. Run `yarn build` to build the cairo artifacts via scarb. These will be put in the target/ directory 1. Run `yarn deploy` to deploy the example consumer contract to the Starknet Goerli testnet. The console prints the contract address and transaction hash. 1. Run `yarn readLatestRound ` to send an invoke transaction to the deployed contract. Specify the contract address printed by the deploy step. The deployed contract reads the latest round data from the proxy, stores the values, and prints the resulting values. diff --git a/examples/contracts/proxy-consumer/Scarb.toml b/examples/contracts/proxy-consumer/Scarb.toml new file mode 100644 index 000000000..b497f3adb --- /dev/null +++ b/examples/contracts/proxy-consumer/Scarb.toml @@ -0,0 +1,16 @@ +[package] +name = "proxy_consumer" +version = "0.1.0" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest + +[dependencies] + chainlink = { path = "../../../contracts" } + + +[[target.starknet-contract]] +# note these two options only work on scarb 0.2.0 and forward +casm = true +# Emit Python-powered hints in order to run compiled CASM class with legacy Cairo VM. +casm-add-pythonic-hints = true + diff --git a/examples/contracts/proxy-consumer/contracts/Proxy_consumer.cairo b/examples/contracts/proxy-consumer/contracts/Proxy_consumer.cairo deleted file mode 100644 index af7882032..000000000 --- a/examples/contracts/proxy-consumer/contracts/Proxy_consumer.cairo +++ /dev/null @@ -1,50 +0,0 @@ -%lang starknet - -from starkware.cairo.common.cairo_builtins import HashBuiltin -from starkware.cairo.common.math import assert_not_zero -from chainlink.cairo.ocr2.IAggregator import IAggregator, Round - -@storage_var -func consumer_proxy_address() -> (address: felt) { -} - -@storage_var -func feed_data() -> (data: Round) { -} - -@constructor -func constructor{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( - proxy_address: felt -) { - assert_not_zero(proxy_address); - consumer_proxy_address.write(proxy_address); - get_latest_round_data(); - return (); -} - -// Get the latest data and store it. -@external -func get_latest_round_data{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> ( - round: Round -) { - let (proxy: felt) = consumer_proxy_address.read(); - let (round: Round) = IAggregator.latest_round_data(contract_address=proxy); - feed_data.write(round); - return (round=round); -} - -@view -func get_stored_round{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> ( - res: Round -) { - let (res) = feed_data.read(); - return (res=res); -} - -@view -func get_stored_feed_address{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() -> ( - res: felt -) { - let (res) = consumer_proxy_address.read(); - return (res=res); -} diff --git a/examples/contracts/proxy-consumer/contracts/aggregator_proxy_abi.json b/examples/contracts/proxy-consumer/contracts/aggregator_proxy_abi.json deleted file mode 100644 index 227ae8513..000000000 --- a/examples/contracts/proxy-consumer/contracts/aggregator_proxy_abi.json +++ /dev/null @@ -1,451 +0,0 @@ -[ - { - "members": [ - { - "name": "round_id", - "offset": 0, - "type": "felt" - }, - { - "name": "answer", - "offset": 1, - "type": "felt" - }, - { - "name": "block_num", - "offset": 2, - "type": "felt" - }, - { - "name": "started_at", - "offset": 3, - "type": "felt" - }, - { - "name": "updated_at", - "offset": 4, - "type": "felt" - } - ], - "name": "Round", - "size": 5, - "type": "struct" - }, - { - "data": [ - { - "name": "round_id", - "type": "felt" - }, - { - "name": "answer", - "type": "felt" - }, - { - "name": "transmitter", - "type": "felt" - }, - { - "name": "observation_timestamp", - "type": "felt" - }, - { - "name": "observers", - "type": "felt" - }, - { - "name": "observations_len", - "type": "felt" - }, - { - "name": "observations", - "type": "felt*" - }, - { - "name": "juels_per_fee_coin", - "type": "felt" - }, - { - "name": "gas_price", - "type": "felt" - }, - { - "name": "config_digest", - "type": "felt" - }, - { - "name": "epoch_and_round", - "type": "felt" - }, - { - "name": "reimbursement", - "type": "felt" - } - ], - "keys": [], - "name": "NewTransmission", - "type": "event" - }, - { - "data": [ - { - "name": "current", - "type": "felt" - }, - { - "name": "round_id", - "type": "felt" - }, - { - "name": "timestamp", - "type": "felt" - } - ], - "keys": [], - "name": "AnswerUpdated", - "type": "event" - }, - { - "data": [ - { - "name": "round_id", - "type": "felt" - }, - { - "name": "started_by", - "type": "felt" - }, - { - "name": "started_at", - "type": "felt" - } - ], - "keys": [], - "name": "NewRound", - "type": "event" - }, - { - "data": [ - { - "name": "previousOwner", - "type": "felt" - }, - { - "name": "newOwner", - "type": "felt" - } - ], - "keys": [], - "name": "OwnershipTransferred", - "type": "event" - }, - { - "data": [ - { - "name": "from_address", - "type": "felt" - }, - { - "name": "to", - "type": "felt" - } - ], - "keys": [], - "name": "OwnershipTransferRequested", - "type": "event" - }, - { - "data": [ - { - "name": "user", - "type": "felt" - } - ], - "keys": [], - "name": "AddedAccess", - "type": "event" - }, - { - "data": [ - { - "name": "user", - "type": "felt" - } - ], - "keys": [], - "name": "RemovedAccess", - "type": "event" - }, - { - "data": [], - "keys": [], - "name": "CheckAccessEnabled", - "type": "event" - }, - { - "data": [], - "keys": [], - "name": "CheckAccessDisabled", - "type": "event" - }, - { - "inputs": [], - "name": "owner", - "outputs": [ - { - "name": "owner", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "proposed_owner", - "outputs": [ - { - "name": "proposed_owner", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "new_owner", - "type": "felt" - } - ], - "name": "transfer_ownership", - "outputs": [], - "type": "function" - }, - { - "inputs": [], - "name": "accept_ownership", - "outputs": [], - "type": "function" - }, - { - "inputs": [ - { - "name": "user", - "type": "felt" - } - ], - "name": "add_access", - "outputs": [], - "type": "function" - }, - { - "inputs": [ - { - "name": "user", - "type": "felt" - } - ], - "name": "remove_access", - "outputs": [], - "type": "function" - }, - { - "inputs": [], - "name": "enable_access_check", - "outputs": [], - "type": "function" - }, - { - "inputs": [], - "name": "disable_access_check", - "outputs": [], - "type": "function" - }, - { - "data": [ - { - "name": "current", - "type": "felt" - }, - { - "name": "proposed", - "type": "felt" - } - ], - "keys": [], - "name": "AggregatorProposed", - "type": "event" - }, - { - "data": [ - { - "name": "previous", - "type": "felt" - }, - { - "name": "latest", - "type": "felt" - } - ], - "keys": [], - "name": "AggregatorConfirmed", - "type": "event" - }, - { - "inputs": [ - { - "name": "owner", - "type": "felt" - }, - { - "name": "address", - "type": "felt" - } - ], - "name": "constructor", - "outputs": [], - "type": "constructor" - }, - { - "inputs": [ - { - "name": "address", - "type": "felt" - } - ], - "name": "propose_aggregator", - "outputs": [], - "type": "function" - }, - { - "inputs": [ - { - "name": "address", - "type": "felt" - } - ], - "name": "confirm_aggregator", - "outputs": [], - "type": "function" - }, - { - "inputs": [], - "name": "latest_round_data", - "outputs": [ - { - "name": "round", - "type": "Round" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "round_id", - "type": "felt" - } - ], - "name": "round_data", - "outputs": [ - { - "name": "round", - "type": "Round" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "proposed_latest_round_data", - "outputs": [ - { - "name": "round", - "type": "Round" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "name": "round_id", - "type": "felt" - } - ], - "name": "proposed_round_data", - "outputs": [ - { - "name": "round", - "type": "Round" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "aggregator", - "outputs": [ - { - "name": "aggregator", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "phase_id", - "outputs": [ - { - "name": "phase_id", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "description", - "outputs": [ - { - "name": "description", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [ - { - "name": "decimals", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "type_and_version", - "outputs": [ - { - "name": "meta", - "type": "felt" - } - ], - "stateMutability": "view", - "type": "function" - } -] diff --git a/examples/contracts/proxy-consumer/hardhat.config.ts b/examples/contracts/proxy-consumer/hardhat.config.ts deleted file mode 100644 index 0aea8ea37..000000000 --- a/examples/contracts/proxy-consumer/hardhat.config.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { HardhatUserConfig } from 'hardhat/types' -import '@shardlabs/starknet-hardhat-plugin' - -const config: HardhatUserConfig = { - solidity: '0.8.14', - starknet: { - venv: 'active', - network: 'alphaGoerli', - wallets: { - OpenZeppelin: { - accountName: 'OpenZeppelin', - modulePath: 'starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount', - accountPath: '~/.starknet_accounts', - }, - }, - }, - paths: { - cairoPaths: ['../../../contracts/src'], - }, -} - -export default config diff --git a/examples/contracts/proxy-consumer/package.json b/examples/contracts/proxy-consumer/package.json index 48451dac6..431231eff 100644 --- a/examples/contracts/proxy-consumer/package.json +++ b/examples/contracts/proxy-consumer/package.json @@ -9,10 +9,7 @@ "readLatestRoundOffChain": "yarn ts-node ./scripts/readLatestRoundOffChain.ts" }, "license": "MIT", - "devDependencies": { - "@shardlabs/starknet-hardhat-plugin": "^0.8.0-alpha.2" - }, "dependencies": { - "starknet": "^4.17.1" + "starknet": "^5.2.0" } } diff --git a/examples/contracts/proxy-consumer/scripts/deployConsumer.ts b/examples/contracts/proxy-consumer/scripts/deployConsumer.ts index 888e3f6aa..4326c4a98 100644 --- a/examples/contracts/proxy-consumer/scripts/deployConsumer.ts +++ b/examples/contracts/proxy-consumer/scripts/deployConsumer.ts @@ -1,12 +1,8 @@ import fs from 'fs' -import { Account, Provider, Contract, json, ec } from 'starknet' - -// Starknet network: Either goerli-alpha or mainnet-alpha -const network = 'goerli-alpha' +import { Account, Provider, Contract, json, ec, constants } from 'starknet' // The Cairo contract that is compiled and ready to declare and deploy -const consumerContractName = 'Proxy_consumer' -const consumerClassHash = '0x75f25b359402fa046e2b9c17d00138772b51c647c0352eb16954e9e39df4ca6' +const consumerContractName = 'ProxyConsumer' /** * Network: Starknet Goerli testnet @@ -22,28 +18,36 @@ const dataFeedAddress = '0x2579940ca3c41e7119283ceb82cd851c906cbb1510908a913d434 * ~/.starknet_accounts/starknet_open_zeppelin_accounts.json */ const accountAddress = process.env.DEPLOYER_ACCOUNT_ADDRESS as string -const accountKeyPair = ec.getKeyPair(process.env.DEPLOYER_PRIVATE_KEY as string) +const accountPrivateKey = process.env.DEPLOYER_PRIVATE_KEY as string +const starkKeyPub = ec.starkCurve.getStarkKey(accountPrivateKey) export async function deployContract() { const provider = new Provider({ sequencer: { - network: network, + // Starknet network: Either goerli-alpha or mainnet-alpha + network: constants.NetworkName.SN_GOERLI, }, }) - const account = new Account(provider, accountAddress, accountKeyPair) + const account = new Account(provider, accountAddress, accountPrivateKey) const consumerContract = json.parse( fs .readFileSync( - `${__dirname}/../starknet-artifacts/contracts/${consumerContractName}.cairo/${consumerContractName}.json`, + `${__dirname}/../target/release/proxy_consumer_${consumerContractName}.sierra.json`, ) .toString('ascii'), ) - const declareDeployConsumer = await account.declareDeploy({ + const declareDeployConsumer = await account.declareAndDeploy({ contract: consumerContract, - classHash: consumerClassHash, + casm: json.parse( + fs + .readFileSync( + `${__dirname}/../target/release/proxy_consumer_${consumerContractName}.casm.json`, + ) + .toString('ascii'), + ), constructorCalldata: [dataFeedAddress as string], }) diff --git a/examples/contracts/proxy-consumer/scripts/readLatestRound.ts b/examples/contracts/proxy-consumer/scripts/readLatestRound.ts index 8eff53d61..4516e86fd 100644 --- a/examples/contracts/proxy-consumer/scripts/readLatestRound.ts +++ b/examples/contracts/proxy-consumer/scripts/readLatestRound.ts @@ -1,33 +1,32 @@ import fs from 'fs' -import { Account, Provider, Contract, CallContractResponse, json, ec } from 'starknet' - -// Starknet network: Either goerli-alpha or mainnet-alpha -const network = 'goerli-alpha' +import { Account, Provider, Contract, CallContractResponse, json, ec, constants } from 'starknet' /** Environment variables for a deployed and funded account to use for deploying contracts * Find your OpenZeppelin account address and private key at: * ~/.starknet_accounts/starknet_open_zeppelin_accounts.json */ const accountAddress = process.env.DEPLOYER_ACCOUNT_ADDRESS as string -const accountKeyPair = ec.getKeyPair(process.env.DEPLOYER_PRIVATE_KEY as string) +const privateKey = process.env.DEPLOYER_PRIVATE_KEY as string +const starkKeyPub = ec.starkCurve.getStarkKey(privateKey) -const consumerContractName = 'Proxy_consumer' +const consumerContractName = 'ProxyConsumer' const contractAddress = process.argv.at(2) as string const provider = new Provider({ sequencer: { - network: network, + // Starknet network: Either goerli-alpha or mainnet-alpha + network: constants.NetworkName.SN_GOERLI, }, }) -const account = new Account(provider, accountAddress, accountKeyPair) +const account = new Account(provider, accountAddress, privateKey) export async function updateStoredRound(account: Account, contractAddress: string) { const consumerContract = json.parse( fs .readFileSync( - `${__dirname}/../starknet-artifacts/contracts/${consumerContractName}.cairo/${consumerContractName}.json`, + `${__dirname}/../target/release/proxy_consumer_${consumerContractName}.sierra.json`, ) .toString('ascii'), ) diff --git a/examples/contracts/proxy-consumer/scripts/readLatestRoundOffChain.ts b/examples/contracts/proxy-consumer/scripts/readLatestRoundOffChain.ts index a120df024..4fd27ebc7 100644 --- a/examples/contracts/proxy-consumer/scripts/readLatestRoundOffChain.ts +++ b/examples/contracts/proxy-consumer/scripts/readLatestRoundOffChain.ts @@ -1,4 +1,4 @@ -import { Provider, CallContractResponse } from 'starknet' +import { Provider, CallContractResponse, constants } from 'starknet' // Starknet network: Either goerli-alpha or mainnet-alpha const network = 'goerli-alpha' @@ -15,7 +15,7 @@ const dataFeedAddress = '0x2579940ca3c41e7119283ceb82cd851c906cbb1510908a913d434 export async function readLatestRoundOffChain() { const provider = new Provider({ sequencer: { - network: network, + network: constants.NetworkName.SN_GOERLI, }, }) diff --git a/examples/contracts/proxy-consumer/src/lib.cairo b/examples/contracts/proxy-consumer/src/lib.cairo new file mode 100644 index 000000000..bf37de90f --- /dev/null +++ b/examples/contracts/proxy-consumer/src/lib.cairo @@ -0,0 +1 @@ +mod proxy_consumer; diff --git a/examples/contracts/proxy-consumer/src/proxy_consumer.cairo b/examples/contracts/proxy-consumer/src/proxy_consumer.cairo new file mode 100644 index 000000000..1ea30585b --- /dev/null +++ b/examples/contracts/proxy-consumer/src/proxy_consumer.cairo @@ -0,0 +1,53 @@ +#[starknet::contract] +mod ProxyConsumer { + use zeroable::Zeroable; + use traits::Into; + use traits::TryInto; + use option::OptionTrait; + + use starknet::ContractAddress; + use starknet::StorageBaseAddress; + use starknet::SyscallResult; + use starknet::storage_read_syscall; + use starknet::storage_write_syscall; + use starknet::storage_address_from_base_and_offset; + + use chainlink::ocr2::aggregator::Round; + + use chainlink::ocr2::aggregator_proxy::IAggregator; + use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcher; + use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcherTrait; + + + #[storage] + struct Storage { + _proxy_address: ContractAddress, + _feed_data: Round, + } + + #[constructor] + fn constructor(ref self: ContractState, proxy_address: ContractAddress) { + assert(!proxy_address.is_zero(), 'proxy address 0'); + self._proxy_address.write(proxy_address); + get_latest_round_data(ref self); + } + + #[external(v0)] + fn get_latest_round_data(ref self: ContractState) -> Round { + let round = IAggregatorDispatcher { + contract_address: self._proxy_address.read() + }.latest_round_data(); + self._feed_data.write(round); + round + } + + #[external(v0)] + fn get_stored_round(self: @ContractState) -> Round { + self._feed_data.read() + } + + #[external(v0)] + fn get_stored_feed_address(self: @ContractState) -> ContractAddress { + self._proxy_address.read() + } +} diff --git a/examples/contracts/proxy-consumer/yarn.lock b/examples/contracts/proxy-consumer/yarn.lock new file mode 100644 index 000000000..818935665 --- /dev/null +++ b/examples/contracts/proxy-consumer/yarn.lock @@ -0,0 +1,95 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@noble/curves@^0.8.2": + version "0.8.3" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-0.8.3.tgz#ad6d48baf2599cf1d58dcb734c14d5225c8996e0" + integrity sha512-OqaOf4RWDaCRuBKJLDURrgVxjLmneGsiCXGuzYB5y95YithZMA6w4uk34DHSm0rKMrrYiaeZj48/81EvaAScLQ== + dependencies: + "@noble/hashes" "1.3.0" + +"@noble/curves@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" + integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== + dependencies: + "@noble/hashes" "1.3.0" + +"@noble/hashes@1.3.0", "@noble/hashes@~1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" + integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== + +isomorphic-fetch@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-3.0.0.tgz#0267b005049046d2421207215d45d6a262b8b8b4" + integrity sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== + dependencies: + node-fetch "^2.6.1" + whatwg-fetch "^3.4.1" + +lossless-json@^2.0.8: + version "2.0.9" + resolved "https://registry.yarnpkg.com/lossless-json/-/lossless-json-2.0.9.tgz#2e9a71a3dcbc6c59dee565e537b9084107b7fe37" + integrity sha512-PUfJ5foxULG1x/dXpSckmt0woBDqyq/WFoI885vEqjGwuP41K2EBYh2IT3zYx9dWqcTLIfXiCE5AjhF1jk9Sbg== + +micro-starknet@^0.2.1: + version "0.2.3" + resolved "https://registry.yarnpkg.com/micro-starknet/-/micro-starknet-0.2.3.tgz#ff4e7caf599255d2110e9c57bb483dfaf493ccb3" + integrity sha512-6XBcC+GerlwJSR4iA0VaeXtS2wrayWFcA4PEzrJPMuFmWCaUtuGIq5K/DB5F/XgnL54/zl2Bxo690Lj7mYVA8A== + dependencies: + "@noble/curves" "~1.0.0" + "@noble/hashes" "~1.3.0" + +node-fetch@^2.6.1: + version "2.6.11" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" + integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== + dependencies: + whatwg-url "^5.0.0" + +pako@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" + integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== + +starknet@^5.2.0: + version "5.9.2" + resolved "https://registry.yarnpkg.com/starknet/-/starknet-5.9.2.tgz#2f82f2eb5e24912468e00df64cb349fd0f6fdc9c" + integrity sha512-zCoMQOlmaNeYlNjvVjYevaYcZv+6Uvivtn1n9IuF8cGtZKHVYf/7wqha8rjpFgGGCgDthJjqj+B7Zxu9oh0GFg== + dependencies: + "@noble/curves" "^0.8.2" + isomorphic-fetch "^3.0.0" + lossless-json "^2.0.8" + micro-starknet "^0.2.1" + pako "^2.0.4" + url-join "^4.0.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +url-join@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-fetch@^3.4.1: + version "3.6.2" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0"