From 7717098b98b4836aa5a2b6c11ecd62c36d1b73f0 Mon Sep 17 00:00:00 2001 From: Chris De Leon Date: Tue, 27 Feb 2024 20:20:24 -0800 Subject: [PATCH 1/8] updates examples directory and removes proxy-consumer --- .../contracts/aggregator_consumer/.gitignore | 2 + .../contracts/aggregator_consumer/Makefile | 12 + .../contracts/aggregator_consumer/README.md | 27 ++ .../contracts/aggregator_consumer/Scarb.lock | 28 ++ .../contracts/aggregator_consumer/Scarb.toml | 20 ++ .../aggregator_consumer/scripts/Scarb.lock | 14 + .../aggregator_consumer/scripts/Scarb.toml | 10 + .../scripts/src/deploy.cairo | 5 + .../scripts/src/example.cairo | 9 + .../aggregator_consumer/scripts/src/lib.cairo | 2 + .../aggregator_consumer/snfoundry.toml | 8 + .../aggregator_consumer/src/emergency.cairo | 1 + .../src/emergency/sequencer_uptime_feed.cairo | 287 ++++++++++++++++++ .../aggregator_consumer/src/lib.cairo | 5 + .../aggregator_consumer/src/libraries.cairo | 1 + .../src/libraries/access_control.cairo | 152 ++++++++++ .../aggregator_consumer/src/mocks.cairo | 1 + .../src/mocks/mock_aggregator.cairo | 121 ++++++++ .../aggregator_consumer/src/ocr2.cairo | 2 + .../src/ocr2/consumer.cairo | 35 +++ .../src/ocr2/price_consumer.cairo | 66 ++++ .../tests/test_consumer.cairo | 66 ++++ .../test_price_consumer_with_sequencer.cairo | 88 ++++++ examples/contracts/proxy-consumer/.gitignore | 1 - examples/contracts/proxy-consumer/README.md | 65 ---- examples/contracts/proxy-consumer/Scarb.toml | 16 - .../contracts/proxy-consumer/package.json | 15 - .../proxy-consumer/scripts/deployConsumer.ts | 64 ---- .../proxy-consumer/scripts/readLatestRound.ts | 80 ----- .../scripts/readLatestRoundOffChain.ts | 39 --- .../contracts/proxy-consumer/src/lib.cairo | 1 - .../proxy-consumer/src/proxy_consumer.cairo | 52 ---- examples/contracts/proxy-consumer/yarn.lock | 95 ------ 33 files changed, 962 insertions(+), 428 deletions(-) create mode 100644 examples/contracts/aggregator_consumer/.gitignore create mode 100644 examples/contracts/aggregator_consumer/Makefile create mode 100644 examples/contracts/aggregator_consumer/README.md create mode 100644 examples/contracts/aggregator_consumer/Scarb.lock create mode 100644 examples/contracts/aggregator_consumer/Scarb.toml create mode 100644 examples/contracts/aggregator_consumer/scripts/Scarb.lock create mode 100644 examples/contracts/aggregator_consumer/scripts/Scarb.toml create mode 100644 examples/contracts/aggregator_consumer/scripts/src/deploy.cairo create mode 100644 examples/contracts/aggregator_consumer/scripts/src/example.cairo create mode 100644 examples/contracts/aggregator_consumer/scripts/src/lib.cairo create mode 100644 examples/contracts/aggregator_consumer/snfoundry.toml create mode 100644 examples/contracts/aggregator_consumer/src/emergency.cairo create mode 100644 examples/contracts/aggregator_consumer/src/emergency/sequencer_uptime_feed.cairo create mode 100644 examples/contracts/aggregator_consumer/src/lib.cairo create mode 100644 examples/contracts/aggregator_consumer/src/libraries.cairo create mode 100644 examples/contracts/aggregator_consumer/src/libraries/access_control.cairo create mode 100644 examples/contracts/aggregator_consumer/src/mocks.cairo create mode 100644 examples/contracts/aggregator_consumer/src/mocks/mock_aggregator.cairo create mode 100644 examples/contracts/aggregator_consumer/src/ocr2.cairo create mode 100644 examples/contracts/aggregator_consumer/src/ocr2/consumer.cairo create mode 100644 examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo create mode 100644 examples/contracts/aggregator_consumer/tests/test_consumer.cairo create mode 100644 examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo delete mode 100644 examples/contracts/proxy-consumer/.gitignore delete mode 100644 examples/contracts/proxy-consumer/README.md delete mode 100644 examples/contracts/proxy-consumer/Scarb.toml delete mode 100644 examples/contracts/proxy-consumer/package.json delete mode 100644 examples/contracts/proxy-consumer/scripts/deployConsumer.ts delete mode 100644 examples/contracts/proxy-consumer/scripts/readLatestRound.ts delete mode 100644 examples/contracts/proxy-consumer/scripts/readLatestRoundOffChain.ts delete mode 100644 examples/contracts/proxy-consumer/src/lib.cairo delete mode 100644 examples/contracts/proxy-consumer/src/proxy_consumer.cairo delete mode 100644 examples/contracts/proxy-consumer/yarn.lock diff --git a/examples/contracts/aggregator_consumer/.gitignore b/examples/contracts/aggregator_consumer/.gitignore new file mode 100644 index 000000000..73aa31e60 --- /dev/null +++ b/examples/contracts/aggregator_consumer/.gitignore @@ -0,0 +1,2 @@ +target +.snfoundry_cache/ diff --git a/examples/contracts/aggregator_consumer/Makefile b/examples/contracts/aggregator_consumer/Makefile new file mode 100644 index 000000000..dccb98269 --- /dev/null +++ b/examples/contracts/aggregator_consumer/Makefile @@ -0,0 +1,12 @@ +# TODO: these helper commands may be useful for the deploy script(s) +# +# ACCOUNT_NAME="test-acct" +# +# create-account: +# @sncast account create --name $(ACCOUNT_NAME) +# +# deploy-account: +# @sncast account deploy --name $(ACCOUNT_NAME) + +run-script: + @cd ./scripts && sncast script run $(NAME) diff --git a/examples/contracts/aggregator_consumer/README.md b/examples/contracts/aggregator_consumer/README.md new file mode 100644 index 000000000..bea900f50 --- /dev/null +++ b/examples/contracts/aggregator_consumer/README.md @@ -0,0 +1,27 @@ +# Examples + +## Overview + +## Prerequisites + + +To get started, ensure that you have the following tools installed on your machine: + +- [starknet-foundry (v0.18.0)](https://github.com/foundry-rs/starknet-foundry/releases/tag/v0.18.0) +- [scarb (v2.5.4)](https://github.com/software-mansion/scarb/releases/tag/v2.5.4) + +## Running Test Cases + +To run all test cases, you can use the following command: + +```sh +snforge test +``` + +## Running Scripts + +To run a script, you can use the following command replacing the `NAME` argument with the name of the script to run: + +```sh +make run-script NAME=example +``` diff --git a/examples/contracts/aggregator_consumer/Scarb.lock b/examples/contracts/aggregator_consumer/Scarb.lock new file mode 100644 index 000000000..80cccaf0d --- /dev/null +++ b/examples/contracts/aggregator_consumer/Scarb.lock @@ -0,0 +1,28 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "aggregator_consumer" +version = "0.1.0" +dependencies = [ + "chainlink", + "openzeppelin", + "snforge_std", +] + +[[package]] +name = "chainlink" +version = "0.1.0" +dependencies = [ + "openzeppelin", +] + +[[package]] +name = "openzeppelin" +version = "0.9.0" +source = "git+https://github.com/OpenZeppelin/cairo-contracts.git?tag=v0.9.0#861fc416f87addbe23a3b47f9d19ab27c10d5dc8" + +[[package]] +name = "snforge_std" +version = "0.18.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.18.0#48f909a56b08cbdc5ca6a21a836b0fbc6c36d55b" diff --git a/examples/contracts/aggregator_consumer/Scarb.toml b/examples/contracts/aggregator_consumer/Scarb.toml new file mode 100644 index 000000000..233211981 --- /dev/null +++ b/examples/contracts/aggregator_consumer/Scarb.toml @@ -0,0 +1,20 @@ +# This project was generated using snforge init +# +# https://foundry-rs.github.io/starknet-foundry/appendix/snforge/init.html +# + +[package] +name = "aggregator_consumer" +version = "0.1.0" +edition = "2023_11" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.9.0" } +snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.18.0" } +chainlink = { path = "../../../contracts" } +starknet = "2.5.4" + +[[target.starknet-contract]] +casm = true diff --git a/examples/contracts/aggregator_consumer/scripts/Scarb.lock b/examples/contracts/aggregator_consumer/scripts/Scarb.lock new file mode 100644 index 000000000..4dca6a9b5 --- /dev/null +++ b/examples/contracts/aggregator_consumer/scripts/Scarb.lock @@ -0,0 +1,14 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "sncast_std" +version = "0.18.0" +source = "git+https://github.com/foundry-rs/starknet-foundry?tag=v0.18.0#48f909a56b08cbdc5ca6a21a836b0fbc6c36d55b" + +[[package]] +name = "src" +version = "0.1.0" +dependencies = [ + "sncast_std", +] diff --git a/examples/contracts/aggregator_consumer/scripts/Scarb.toml b/examples/contracts/aggregator_consumer/scripts/Scarb.toml new file mode 100644 index 000000000..d912a6217 --- /dev/null +++ b/examples/contracts/aggregator_consumer/scripts/Scarb.toml @@ -0,0 +1,10 @@ +[package] +name = "src" +version = "0.1.0" +edition = "2023_11" + +# See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html + +[dependencies] +sncast_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.18.0" } +starknet = ">=2.5.4" diff --git a/examples/contracts/aggregator_consumer/scripts/src/deploy.cairo b/examples/contracts/aggregator_consumer/scripts/src/deploy.cairo new file mode 100644 index 000000000..13218da2f --- /dev/null +++ b/examples/contracts/aggregator_consumer/scripts/src/deploy.cairo @@ -0,0 +1,5 @@ +use sncast_std::{call, CallResult}; + +fn main() { + println!("replace this with a deploy script!"); +} diff --git a/examples/contracts/aggregator_consumer/scripts/src/example.cairo b/examples/contracts/aggregator_consumer/scripts/src/example.cairo new file mode 100644 index 000000000..b995a7004 --- /dev/null +++ b/examples/contracts/aggregator_consumer/scripts/src/example.cairo @@ -0,0 +1,9 @@ +use sncast_std::{call, CallResult}; + +// The example below uses a contract deployed to the Goerli testnet +fn main() { + let contract_address = 0x7ad10abd2cc24c2e066a2fee1e435cd5fa60a37f9268bfbaf2e98ce5ca3c436; + let call_result = call(contract_address.try_into().unwrap(), 'get_greeting', array![]); + assert(*call_result.data[0] == 'Hello, Starknet!', *call_result.data[0]); + println!("{:?}", call_result); +} diff --git a/examples/contracts/aggregator_consumer/scripts/src/lib.cairo b/examples/contracts/aggregator_consumer/scripts/src/lib.cairo new file mode 100644 index 000000000..a58158aab --- /dev/null +++ b/examples/contracts/aggregator_consumer/scripts/src/lib.cairo @@ -0,0 +1,2 @@ +mod example; +mod deploy; diff --git a/examples/contracts/aggregator_consumer/snfoundry.toml b/examples/contracts/aggregator_consumer/snfoundry.toml new file mode 100644 index 000000000..253c2f3c5 --- /dev/null +++ b/examples/contracts/aggregator_consumer/snfoundry.toml @@ -0,0 +1,8 @@ +# A full list of RPC endpoints can be found here: +# +# https://blastapi.io/public-api/starknet +# + +[sncast.default] +url = "https://starknet-testnet.public.blastapi.io/rpc/v0_6" + diff --git a/examples/contracts/aggregator_consumer/src/emergency.cairo b/examples/contracts/aggregator_consumer/src/emergency.cairo new file mode 100644 index 000000000..870a7f708 --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/emergency.cairo @@ -0,0 +1 @@ +pub mod sequencer_uptime_feed; diff --git a/examples/contracts/aggregator_consumer/src/emergency/sequencer_uptime_feed.cairo b/examples/contracts/aggregator_consumer/src/emergency/sequencer_uptime_feed.cairo new file mode 100644 index 000000000..7aebf16fc --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/emergency/sequencer_uptime_feed.cairo @@ -0,0 +1,287 @@ +use starknet::EthAddress; +#[starknet::interface] +pub trait ISequencerUptimeFeed { + fn l1_sender(self: @TContractState) -> EthAddress; + fn set_l1_sender(ref self: TContractState, address: EthAddress); +} + +#[starknet::contract] +mod SequencerUptimeFeed { + use starknet::EthAddress; + use starknet::ContractAddress; + use starknet::SyscallResult; + use starknet::syscalls::storage_read_syscall; + use starknet::syscalls::storage_write_syscall; + use starknet::storage_access::storage_address_from_base_and_offset; + use starknet::class_hash::ClassHash; + + use core::option::OptionTrait; + use core::traits::TryInto; + + use openzeppelin::access::ownable::ownable::OwnableComponent; + + use aggregator_consumer::libraries::access_control::{AccessControlComponent, IAccessController}; + use aggregator_consumer::libraries::access_control::AccessControlComponent::InternalTrait as AccessControlInternalTrait; + use chainlink::libraries::type_and_version::ITypeAndVersion; + use chainlink::ocr2::aggregator::Round; + use chainlink::ocr2::aggregator::IAggregator; + use chainlink::ocr2::aggregator::{Transmission}; + use chainlink::libraries::upgradeable::Upgradeable; + + component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); + component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent); + + #[abi(embed_v0)] + impl OwnableImpl = OwnableComponent::OwnableTwoStepImpl; + impl OwnableInternalImpl = OwnableComponent::InternalImpl; + + #[abi(embed_v0)] + impl AccessControlImpl = + AccessControlComponent::AccessControlImpl; + impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; + + #[storage] + struct Storage { + #[substorage(v0)] + ownable: OwnableComponent::Storage, + #[substorage(v0)] + access_control: AccessControlComponent::Storage, + // l1 sender is an starknet validator ethereum address + _l1_sender: felt252, + // maps round id to round transmission + _round_transmissions: LegacyMap, + _latest_round_id: u128, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + #[flat] + OwnableEvent: OwnableComponent::Event, + #[flat] + AccessControlEvent: AccessControlComponent::Event, + RoundUpdated: RoundUpdated, + NewRound: NewRound, + AnswerUpdated: AnswerUpdated, + UpdateIgnored: UpdateIgnored, + L1SenderTransferred: L1SenderTransferred, + } + + #[derive(Drop, starknet::Event)] + struct RoundUpdated { + status: u128, + updated_at: u64 + } + + #[derive(Drop, starknet::Event)] + struct NewRound { + round_id: u128, + started_by: ContractAddress, + started_at: u64 + } + + #[derive(Drop, starknet::Event)] + struct AnswerUpdated { + current: u128, + round_id: u128, + timestamp: u64 + } + + #[derive(Drop, starknet::Event)] + struct UpdateIgnored { + latest_status: u128, + latest_timestamp: u64, + incoming_status: u128, + incoming_timestamp: u64 + } + + #[derive(Drop, starknet::Event)] + struct L1SenderTransferred { + from_address: EthAddress, + to_address: EthAddress + } + + #[abi(embed_v0)] + impl TypeAndVersion of ITypeAndVersion { + fn type_and_version(self: @ContractState) -> felt252 { + 'SequencerUptimeFeed 1.0.0' + } + } + + #[abi(embed_v0)] + impl AggregatorImpl of IAggregator { + fn latest_round_data(self: @ContractState) -> Round { + self._require_read_access(); + let latest_round_id = self._latest_round_id.read(); + let round_transmission = self._round_transmissions.read(latest_round_id); + Round { + round_id: latest_round_id.into(), + answer: round_transmission.answer, + block_num: round_transmission.block_num, + started_at: round_transmission.observation_timestamp, + updated_at: round_transmission.transmission_timestamp, + } + } + + fn round_data(self: @ContractState, round_id: u128) -> Round { + self._require_read_access(); + assert(round_id < self._latest_round_id.read(), 'invalid round id'); + let round_transmission = self._round_transmissions.read(round_id); + Round { + round_id: round_id.into(), + answer: round_transmission.answer, + block_num: round_transmission.block_num, + started_at: round_transmission.observation_timestamp, + updated_at: round_transmission.transmission_timestamp, + } + } + + fn description(self: @ContractState) -> felt252 { + 'L2 Sequencer Uptime Status Feed' + } + + fn decimals(self: @ContractState) -> u8 { + 0_u8 + } + } + + #[constructor] + fn constructor(ref self: ContractState, initial_status: u128, owner_address: ContractAddress) { + self._initializer(initial_status, owner_address); + } + + #[l1_handler] + fn update_status(ref self: ContractState, from_address: felt252, status: u128, timestamp: u64) { + assert(self._l1_sender.read() == from_address, 'EXPECTED_FROM_BRIDGE_ONLY'); + + let latest_round_id = self._latest_round_id.read(); + let latest_round = self._round_transmissions.read(latest_round_id); + + if timestamp <= latest_round.observation_timestamp { + self + .emit( + Event::UpdateIgnored( + UpdateIgnored { + latest_status: latest_round.answer, + latest_timestamp: latest_round.transmission_timestamp, + incoming_status: status, + incoming_timestamp: timestamp + } + ) + ); + return (); + } + + if latest_round.answer == status { + self._update_round(latest_round_id, latest_round); + } else { + // only increment round when status flips + let round_id = latest_round_id + 1_u128; + self._record_round(round_id, status, timestamp); + } + } + + #[abi(embed_v0)] + impl SequencerUptimeFeedImpl of super::ISequencerUptimeFeed { + fn set_l1_sender(ref self: ContractState, address: EthAddress) { + self.ownable.assert_only_owner(); + + // TODO: + // assert(!address.is_zero(), '0 address not allowed'); + + let old_address = self._l1_sender.read(); + + if old_address != address.into() { + self._l1_sender.write(address.into()); + self + .emit( + Event::L1SenderTransferred( + L1SenderTransferred { + from_address: old_address.try_into().unwrap(), to_address: address + } + ) + ); + } + } + + fn l1_sender(self: @ContractState) -> EthAddress { + self._l1_sender.read().try_into().unwrap() + } + } + + /// + /// Upgradeable + /// + + #[abi(embed_v0)] + fn upgrade(ref self: ContractState, new_impl: ClassHash) { + self.ownable.assert_only_owner(); + Upgradeable::upgrade(new_impl) + } + + /// + /// Internals + /// + + #[generate_trait] + impl Internals of InternalTrait { + fn _require_read_access(self: @ContractState) { + let sender = starknet::get_caller_address(); + self.access_control.check_read_access(sender); + } + + fn _initializer( + ref self: ContractState, initial_status: u128, owner_address: ContractAddress + ) { + self.ownable.initializer(owner_address); + self.access_control.initializer(); + let round_id = 1_u128; + let timestamp = starknet::get_block_timestamp(); + self._record_round(round_id, initial_status, timestamp); + } + + fn _record_round(ref self: ContractState, round_id: u128, status: u128, timestamp: u64) { + self._latest_round_id.write(round_id); + let block_info = starknet::get_block_info().unbox(); + let block_number = block_info.block_number; + let block_timestamp = block_info.block_timestamp; + + let round = Transmission { + answer: status, + block_num: block_number, + observation_timestamp: timestamp, + transmission_timestamp: block_timestamp, + }; + self._round_transmissions.write(round_id, round); + + let sender = starknet::get_caller_address(); + + self + .emit( + Event::NewRound( + NewRound { round_id: round_id, started_by: sender, started_at: timestamp } + ) + ); + self + .emit( + Event::AnswerUpdated( + AnswerUpdated { current: status, round_id: round_id, timestamp: timestamp } + ) + ); + } + + fn _update_round(ref self: ContractState, round_id: u128, mut round: Transmission) { + round.transmission_timestamp = starknet::get_block_timestamp(); + self._round_transmissions.write(round_id, round); + + self + .emit( + Event::RoundUpdated( + RoundUpdated { + status: round.answer, updated_at: round.transmission_timestamp + } + ) + ); + } + } +} diff --git a/examples/contracts/aggregator_consumer/src/lib.cairo b/examples/contracts/aggregator_consumer/src/lib.cairo new file mode 100644 index 000000000..cc02a54bd --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/lib.cairo @@ -0,0 +1,5 @@ +pub mod libraries; +pub mod emergency; +pub mod mocks; +pub mod ocr2; + diff --git a/examples/contracts/aggregator_consumer/src/libraries.cairo b/examples/contracts/aggregator_consumer/src/libraries.cairo new file mode 100644 index 000000000..18e237199 --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/libraries.cairo @@ -0,0 +1 @@ +pub mod access_control; diff --git a/examples/contracts/aggregator_consumer/src/libraries/access_control.cairo b/examples/contracts/aggregator_consumer/src/libraries/access_control.cairo new file mode 100644 index 000000000..a44fee769 --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/libraries/access_control.cairo @@ -0,0 +1,152 @@ +use starknet::ContractAddress; +#[starknet::interface] +pub trait IAccessController { + fn has_access(self: @TContractState, user: ContractAddress, data: Array) -> bool; + fn has_read_access(self: @TContractState, user: ContractAddress, data: Array) -> bool; + fn add_access(ref self: TContractState, user: ContractAddress); + fn remove_access(ref self: TContractState, user: ContractAddress); + fn enable_access_check(ref self: TContractState); + fn disable_access_check(ref self: TContractState); +} + +// Requires Ownable subcomponent. +#[starknet::component] +pub mod AccessControlComponent { + use starknet::ContractAddress; + use starknet::class_hash::ClassHash; + + use openzeppelin::access::ownable::ownable::OwnableComponent; + + use OwnableComponent::InternalImpl as OwnableInternalImpl; + + #[storage] + struct Storage { + _check_enabled: bool, + _access_list: LegacyMap, + } + + #[event] + #[derive(Drop, starknet::Event)] + pub enum Event { + AddedAccess: AddedAccess, + RemovedAccess: RemovedAccess, + AccessControlEnabled: AccessControlEnabled, + AccessControlDisabled: AccessControlDisabled, + } + + #[derive(Drop, starknet::Event)] + struct AddedAccess { + user: ContractAddress + } + + #[derive(Drop, starknet::Event)] + struct RemovedAccess { + user: ContractAddress + } + + #[derive(Drop, starknet::Event)] + struct AccessControlEnabled {} + + #[derive(Drop, starknet::Event)] + struct AccessControlDisabled {} + + #[embeddable_as(AccessControlImpl)] + pub impl AccessControl< + TContractState, + +HasComponent, + impl Ownable: OwnableComponent::HasComponent, + +Drop, + > of super::IAccessController> { + fn has_access( + self: @ComponentState, user: ContractAddress, data: Array + ) -> bool { + let has_access = self._access_list.read(user); + if has_access { + return true; + } + + let check_enabled = self._check_enabled.read(); + if !check_enabled { + return true; + } + + false + } + + fn has_read_access( + self: @ComponentState, user: ContractAddress, data: Array + ) -> bool { + let _has_access = self.has_access(user, data); + if _has_access { + return true; + } + + // TODO: + // NOTICE: read access is granted to direct calls, to enable off-chain reads. + // if user.is_zero() { + // return true; + //} + + false + } + + fn add_access(ref self: ComponentState, user: ContractAddress) { + get_dep_component!(@self, Ownable).assert_only_owner(); + let has_access = self._access_list.read(user); + if !has_access { + self._access_list.write(user, true); + self.emit(Event::AddedAccess(AddedAccess { user: user })); + } + } + + fn remove_access(ref self: ComponentState, user: ContractAddress) { + get_dep_component!(@self, Ownable).assert_only_owner(); + let has_access = self._access_list.read(user); + if has_access { + self._access_list.write(user, false); + self.emit(Event::RemovedAccess(RemovedAccess { user: user })); + } + } + + fn enable_access_check(ref self: ComponentState) { + get_dep_component!(@self, Ownable).assert_only_owner(); + let check_enabled = self._check_enabled.read(); + if !check_enabled { + self._check_enabled.write(true); + self.emit(Event::AccessControlEnabled(AccessControlEnabled {})); + } + } + + fn disable_access_check(ref self: ComponentState) { + get_dep_component!(@self, Ownable).assert_only_owner(); + let check_enabled = self._check_enabled.read(); + if check_enabled { + self._check_enabled.write(false); + self.emit(Event::AccessControlDisabled(AccessControlDisabled {})); + } + } + } + + #[generate_trait] + pub impl InternalImpl< + TContractState, + +HasComponent, + impl Ownable: OwnableComponent::HasComponent, + +Drop, + > of InternalTrait { + fn initializer(ref self: ComponentState) { + self._check_enabled.write(true); + self.emit(Event::AccessControlEnabled(AccessControlEnabled {})); + } + + fn check_access(self: @ComponentState, user: ContractAddress) { + let allowed = AccessControl::has_access(self, user, ArrayTrait::new()); + assert(allowed, 'user does not have access'); + } + + fn check_read_access(self: @ComponentState, user: ContractAddress) { + let allowed = AccessControl::has_read_access(self, user, ArrayTrait::new()); + assert(allowed, 'user does not have read access'); + } + } +} diff --git a/examples/contracts/aggregator_consumer/src/mocks.cairo b/examples/contracts/aggregator_consumer/src/mocks.cairo new file mode 100644 index 000000000..b42bbb0d5 --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/mocks.cairo @@ -0,0 +1 @@ +pub mod mock_aggregator; diff --git a/examples/contracts/aggregator_consumer/src/mocks/mock_aggregator.cairo b/examples/contracts/aggregator_consumer/src/mocks/mock_aggregator.cairo new file mode 100644 index 000000000..36b220181 --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/mocks/mock_aggregator.cairo @@ -0,0 +1,121 @@ +#[starknet::interface] +pub trait IMockAggregator { + fn set_latest_round_data( + ref self: TContractState, + answer: u128, + block_num: u64, + observation_timestamp: u64, + transmission_timestamp: u64 + ); +} + +#[starknet::contract] +mod MockAggregator { + use starknet::contract_address_const; + use core::panic_with_felt252; + + use chainlink::libraries::type_and_version::ITypeAndVersion; + + use chainlink::ocr2::aggregator::Aggregator::{Transmission, NewTransmission}; + use chainlink::ocr2::aggregator::IAggregator; + use chainlink::ocr2::aggregator::Round; + + #[event] + use chainlink::ocr2::aggregator::Aggregator::Event; + + #[storage] + struct Storage { + _transmissions: LegacyMap, + _latest_aggregator_round_id: u128, + _decimals: u8 + } + + #[constructor] + fn constructor(ref self: ContractState, decimals: u8) { + self._decimals.write(decimals); + } + + #[abi(embed_v0)] + impl MockImpl of super::IMockAggregator { + fn set_latest_round_data( + ref self: ContractState, + answer: u128, + block_num: u64, + observation_timestamp: u64, + transmission_timestamp: u64 + ) { + let new_round_id = self._latest_aggregator_round_id.read() + 1_u128; + self + ._transmissions + .write( + new_round_id, + Transmission { + answer: answer, + block_num: block_num, + observation_timestamp: observation_timestamp, + transmission_timestamp: transmission_timestamp + } + ); + + let mut observations = ArrayTrait::new(); + observations.append(2_u128); + observations.append(3_u128); + + self._latest_aggregator_round_id.write(new_round_id); + + self + .emit( + Event::NewTransmission( + NewTransmission { + round_id: new_round_id, + answer: answer, + transmitter: contract_address_const::<42>(), + observation_timestamp: observation_timestamp, + observers: 3, + observations: observations, + juels_per_fee_coin: 18_u128, + gas_price: 1_u128, + config_digest: 777, + epoch_and_round: 20_u64, + reimbursement: 100_u128 + } + ) + ); + } + } + + #[abi(embed_v0)] + impl TypeAndVersionImpl of ITypeAndVersion { + fn type_and_version(self: @ContractState) -> felt252 { + 'mock_aggregator.cairo 1.0.0' + } + } + + #[abi(embed_v0)] + impl Aggregator of IAggregator { + fn round_data(self: @ContractState, round_id: u128) -> Round { + panic_with_felt252('unimplemented') + } + + fn latest_round_data(self: @ContractState) -> Round { + let latest_round_id = self._latest_aggregator_round_id.read(); + let transmission = self._transmissions.read(latest_round_id); + + Round { + round_id: latest_round_id.into(), + answer: transmission.answer, + block_num: transmission.block_num, + started_at: transmission.observation_timestamp, + updated_at: transmission.transmission_timestamp + } + } + + fn decimals(self: @ContractState) -> u8 { + self._decimals.read() + } + + fn description(self: @ContractState) -> felt252 { + 'mock' + } + } +} diff --git a/examples/contracts/aggregator_consumer/src/ocr2.cairo b/examples/contracts/aggregator_consumer/src/ocr2.cairo new file mode 100644 index 000000000..8947aa72c --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/ocr2.cairo @@ -0,0 +1,2 @@ +pub mod price_consumer; +pub mod consumer; 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..f0448b309 --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/ocr2/consumer.cairo @@ -0,0 +1,35 @@ +#[starknet::interface] +pub trait IAggregatorConsumer { + fn read_latest_round(self: @TContractState) -> chainlink::ocr2::aggregator::Round; + fn read_decimals(self: @TContractState) -> u8; +} + +#[starknet::contract] +mod AggregatorConsumer { + use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcherTrait; + use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcher; + use chainlink::ocr2::aggregator_proxy::IAggregator; + use chainlink::ocr2::aggregator::Round; + use starknet::ContractAddress; + + #[storage] + struct Storage { + _ocr_address: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, ocr_address: ContractAddress) { + self._ocr_address.write(ocr_address); + } + + #[abi(embed_v0)] + impl AggregatorConsumerImpl of super::IAggregatorConsumer { + fn read_latest_round(self: @ContractState) -> Round { + IAggregatorDispatcher { contract_address: self._ocr_address.read() }.latest_round_data() + } + + 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.cairo b/examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo new file mode 100644 index 000000000..ca8c5cec5 --- /dev/null +++ b/examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo @@ -0,0 +1,66 @@ +#[starknet::interface] +pub trait IAggregatorPriceConsumer { + fn get_latest_price(self: @TContractState) -> u128; +} + +#[starknet::contract] +mod AggregatorPriceConsumer { + use core::starknet::ContractAddress; + use core::starknet::get_block_info; + use core::box::BoxTrait; + use core::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 + ) { + self._uptime_feed_address.write(uptime_feed_address); + self._aggregator_address.write(aggregator_address); + } + + + #[abi(embed_v0)] + impl AggregatorPriceConsumerImpl of super::IAggregatorPriceConsumer { + 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 = 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/tests/test_consumer.cairo b/examples/contracts/aggregator_consumer/tests/test_consumer.cairo new file mode 100644 index 000000000..47d48b9fa --- /dev/null +++ b/examples/contracts/aggregator_consumer/tests/test_consumer.cairo @@ -0,0 +1,66 @@ +use snforge_std::{declare, ContractClassTrait}; + +use aggregator_consumer::mocks::mock_aggregator::IMockAggregatorDispatcherTrait; +use aggregator_consumer::mocks::mock_aggregator::IMockAggregatorDispatcher; +use aggregator_consumer::ocr2::consumer::IAggregatorConsumerDispatcherTrait; +use aggregator_consumer::ocr2::consumer::IAggregatorConsumerDispatcher; + +use starknet::ContractAddress; + +fn deploy_mock_aggregator(decimals: u8) -> ContractAddress { + let mut calldata = ArrayTrait::new(); + calldata.append(decimals.into()); + return declare('MockAggregator').deploy(@calldata).unwrap(); +} + +fn deploy_consumer(aggregator_address: ContractAddress) -> ContractAddress { + let mut calldata = ArrayTrait::new(); + calldata.append(aggregator_address.into()); + return declare('AggregatorConsumer').deploy(@calldata).unwrap(); +} + +#[test] +fn test_read_decimals() { + let decimals = 16; + let mock_aggregator_address = deploy_mock_aggregator(decimals); + let consumer_address = deploy_consumer(mock_aggregator_address); + let consumer_dispatcher = IAggregatorConsumerDispatcher { contract_address: consumer_address }; + assert(decimals == consumer_dispatcher.read_decimals(), 'Invalid decimals'); +} + +#[test] +fn test_read_latest_round() { + // Deploys the mock aggregator + let mock_aggregator_address = deploy_mock_aggregator(16); + let mock_aggregator_dispatcher = IMockAggregatorDispatcher { + contract_address: mock_aggregator_address + }; + + // Deploys the consumer + let consumer_address = deploy_consumer(mock_aggregator_address); + let consumer_dispatcher = IAggregatorConsumerDispatcher { contract_address: consumer_address }; + + // No round data has been initialized, so reading the latest round should return no data + let empty_latest_round = consumer_dispatcher.read_latest_round(); + assert(empty_latest_round.round_id == 0, 'round_id != 0'); + assert(empty_latest_round.answer == 0, 'answer != 0'); + assert(empty_latest_round.block_num == 0, 'block_num != 0'); + assert(empty_latest_round.started_at == 0, 'started_at != 0'); + assert(empty_latest_round.updated_at == 0, 'updated_at != 0'); + + // Now let's set the latest round data to some random values + let answer = 1; + let block_num = 12345; + let observation_timestamp = 100000; + let transmission_timestamp = 200000; + mock_aggregator_dispatcher + .set_latest_round_data(answer, block_num, observation_timestamp, transmission_timestamp); + + // The latest round should now have some data + let latest_round = consumer_dispatcher.read_latest_round(); + assert(latest_round.round_id == 1, 'round_id != 1'); + assert(latest_round.answer == answer, 'bad answer'); + assert(latest_round.block_num == block_num, 'bad block_num'); + assert(latest_round.started_at == observation_timestamp, 'bad started_at'); + assert(latest_round.updated_at == transmission_timestamp, 'bad updated_at'); +} diff --git a/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo b/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo new file mode 100644 index 000000000..17de9dd35 --- /dev/null +++ b/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo @@ -0,0 +1,88 @@ +use snforge_std::{declare, ContractClassTrait, start_prank, stop_prank, CheatTarget}; + +use aggregator_consumer::emergency::sequencer_uptime_feed::ISequencerUptimeFeedDispatcherTrait; +use aggregator_consumer::emergency::sequencer_uptime_feed::ISequencerUptimeFeedDispatcher; +use aggregator_consumer::ocr2::price_consumer::IAggregatorPriceConsumerDispatcherTrait; +use aggregator_consumer::ocr2::price_consumer::IAggregatorPriceConsumerDispatcher; +use aggregator_consumer::libraries::access_control::IAccessControllerDispatcherTrait; +use aggregator_consumer::libraries::access_control::IAccessControllerDispatcher; +use aggregator_consumer::mocks::mock_aggregator::IMockAggregatorDispatcherTrait; +use aggregator_consumer::mocks::mock_aggregator::IMockAggregatorDispatcher; + +use starknet::contract_address_const; +use starknet::get_caller_address; +use starknet::ContractAddress; + +fn deploy_mock_aggregator(decimals: u8) -> ContractAddress { + let mut calldata = ArrayTrait::new(); + calldata.append(decimals.into()); + return declare('MockAggregator').deploy(@calldata).unwrap(); +} + +fn deploy_uptime_feed(initial_status: u128, owner_address: ContractAddress) -> ContractAddress { + let mut calldata = ArrayTrait::new(); + calldata.append(initial_status.into()); + calldata.append(owner_address.into()); + return declare('SequencerUptimeFeed').deploy(@calldata).unwrap(); +} + +fn deploy_price_consumer( + uptime_feed_address: ContractAddress, aggregator_address: ContractAddress +) -> ContractAddress { + let mut calldata = ArrayTrait::new(); + calldata.append(uptime_feed_address.into()); + calldata.append(aggregator_address.into()); + return declare('AggregatorPriceConsumer').deploy(@calldata).unwrap(); +} + +#[test] +fn test_get_latest_price() { + // Defines helper variables + let owner = contract_address_const::<1>(); + let init_status = 0; + let decimals = 18; + + // Deploys contracts + let mock_aggregator_address = deploy_mock_aggregator(decimals); + let uptime_feed_address = deploy_uptime_feed(init_status, owner); + let price_consumer_address = deploy_price_consumer( + uptime_feed_address, mock_aggregator_address + ); + + // Adds the price consumer contract to the sequencer uptime feed access control list + // which allows the price consumer to call the get_latest_price function + start_prank(CheatTarget::All, owner); + IAccessControllerDispatcher { contract_address: uptime_feed_address } + .add_access(price_consumer_address); + + // The get_latest_price function returns the mock aggregator's latest round answer. At + // this point in the test, there is only one round that is initialized and that is the + // one that the sequencer uptime feed creates when it is deployed. In its constructor, + // a new round is initialized using its initial status as the round's answer, so the + // latest price should be the initial status that was passed into the sequencer uptime + // feed's constructor. + start_prank(CheatTarget::All, price_consumer_address); + let latest_price = IAggregatorPriceConsumerDispatcher { + contract_address: price_consumer_address + } + .get_latest_price(); + assert(latest_price == init_status, 'latest price is incorrect'); + + // Now let's update the round + stop_prank(CheatTarget::All); + let answer = 1; + let block_num = 12345; + let observation_timestamp = 100000; + let transmission_timestamp = 200000; + IMockAggregatorDispatcher { contract_address: mock_aggregator_address } + .set_latest_round_data(answer, block_num, observation_timestamp, transmission_timestamp); + + // This should now return the updated answer + start_prank(CheatTarget::All, price_consumer_address); + let updated_latest_price = IAggregatorPriceConsumerDispatcher { + contract_address: price_consumer_address + } + .get_latest_price(); + assert(updated_latest_price == answer, 'updlatest price is incorrect'); +} + diff --git a/examples/contracts/proxy-consumer/.gitignore b/examples/contracts/proxy-consumer/.gitignore deleted file mode 100644 index eb5a316cb..000000000 --- a/examples/contracts/proxy-consumer/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target diff --git a/examples/contracts/proxy-consumer/README.md b/examples/contracts/proxy-consumer/README.md deleted file mode 100644 index 4cce785e9..000000000 --- a/examples/contracts/proxy-consumer/README.md +++ /dev/null @@ -1,65 +0,0 @@ -This is a simple example for how to read Chainlink data feeds on Starknet. - -### Requirements - -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. [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. 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 - -1. Find the your account address and private key for your funded Starknet testnet account. By default, the OpenZeppelin wallet contains these values at `~/.starknet_accounts/starknet_open_zeppelin_accounts.json`. -1. Export your address to the `DEPLOYER_ACCOUNT_ADDRESS` environment variable and your private key to the `DEPLOYER_PRIVATE_KEY` environment variable. - - ```shell - export DEPLOYER_ACCOUNT_ADDRESS= - ``` - - ```shell - export DEPLOYER_PRIVATE_KEY= - ``` -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. - -### Running the off-chain example - -This example simply reads the proxy contract to get the latest values with no account or contract compiling steps required. - -1. Run `yarn readLatestRoundOffChain`. diff --git a/examples/contracts/proxy-consumer/Scarb.toml b/examples/contracts/proxy-consumer/Scarb.toml deleted file mode 100644 index b497f3adb..000000000 --- a/examples/contracts/proxy-consumer/Scarb.toml +++ /dev/null @@ -1,16 +0,0 @@ -[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/package.json b/examples/contracts/proxy-consumer/package.json deleted file mode 100644 index 676bed3ef..000000000 --- a/examples/contracts/proxy-consumer/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "proxy-consumer", - "version": "1.0.0", - "description": "Simple contracts to read Chainlink data feeds on-chain and off-chain", - "scripts": { - "build": "scarb --profile release build", - "deploy": "yarn ts-node ./scripts/deployConsumer.ts", - "readLatestRound": "yarn ts-node ./scripts/readLatestRound.ts", - "readLatestRoundOffChain": "yarn ts-node ./scripts/readLatestRoundOffChain.ts" - }, - "license": "MIT", - "dependencies": { - "starknet": "^6.0.0-beta.11" - } -} diff --git a/examples/contracts/proxy-consumer/scripts/deployConsumer.ts b/examples/contracts/proxy-consumer/scripts/deployConsumer.ts deleted file mode 100644 index 4326c4a98..000000000 --- a/examples/contracts/proxy-consumer/scripts/deployConsumer.ts +++ /dev/null @@ -1,64 +0,0 @@ -import fs from 'fs' -import { Account, Provider, Contract, json, ec, constants } from 'starknet' - -// The Cairo contract that is compiled and ready to declare and deploy -const consumerContractName = 'ProxyConsumer' - -/** - * Network: Starknet Goerli testnet - * Aggregator: LINK/USD - * Address: 0x2579940ca3c41e7119283ceb82cd851c906cbb1510908a913d434861fdcb245 - * Find more feed address at: - * https://docs.chain.link/data-feeds/price-feeds/addresses?network=starknet - */ -const dataFeedAddress = '0x2579940ca3c41e7119283ceb82cd851c906cbb1510908a913d434861fdcb245' - -/** 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 accountPrivateKey = process.env.DEPLOYER_PRIVATE_KEY as string -const starkKeyPub = ec.starkCurve.getStarkKey(accountPrivateKey) - -export async function deployContract() { - const provider = new Provider({ - sequencer: { - // Starknet network: Either goerli-alpha or mainnet-alpha - network: constants.NetworkName.SN_GOERLI, - }, - }) - - const account = new Account(provider, accountAddress, accountPrivateKey) - - const consumerContract = json.parse( - fs - .readFileSync( - `${__dirname}/../target/release/proxy_consumer_${consumerContractName}.sierra.json`, - ) - .toString('ascii'), - ) - - const declareDeployConsumer = await account.declareAndDeploy({ - contract: consumerContract, - casm: json.parse( - fs - .readFileSync( - `${__dirname}/../target/release/proxy_consumer_${consumerContractName}.casm.json`, - ) - .toString('ascii'), - ), - constructorCalldata: [dataFeedAddress as string], - }) - - const consumerDeploy = new Contract( - consumerContract.abi, - declareDeployConsumer.deploy.contract_address, - provider, - ) - - console.log('Contract address: ' + consumerDeploy.address) - console.log('Transaction hash: ' + declareDeployConsumer.deploy.transaction_hash) -} - -deployContract() diff --git a/examples/contracts/proxy-consumer/scripts/readLatestRound.ts b/examples/contracts/proxy-consumer/scripts/readLatestRound.ts deleted file mode 100644 index 4516e86fd..000000000 --- a/examples/contracts/proxy-consumer/scripts/readLatestRound.ts +++ /dev/null @@ -1,80 +0,0 @@ -import fs from 'fs' -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 privateKey = process.env.DEPLOYER_PRIVATE_KEY as string -const starkKeyPub = ec.starkCurve.getStarkKey(privateKey) - -const consumerContractName = 'ProxyConsumer' - -const contractAddress = process.argv.at(2) as string - -const provider = new Provider({ - sequencer: { - // Starknet network: Either goerli-alpha or mainnet-alpha - network: constants.NetworkName.SN_GOERLI, - }, -}) - -const account = new Account(provider, accountAddress, privateKey) - -export async function updateStoredRound(account: Account, contractAddress: string) { - const consumerContract = json.parse( - fs - .readFileSync( - `${__dirname}/../target/release/proxy_consumer_${consumerContractName}.sierra.json`, - ) - .toString('ascii'), - ) - - const targetContract = new Contract(consumerContract.abi, contractAddress, account) - - const response = await targetContract.invoke('get_latest_round_data') - - console.log('\nInvoking the get_latest_round_data function.') - console.log('Transaction hash: ' + response.transaction_hash) - - console.log('Waiting for transaction...') - let transactionStatus = (await provider.getTransactionReceipt(response.transaction_hash)).status - while (transactionStatus !== 'REJECTED' && transactionStatus !== 'ACCEPTED_ON_L2') { - console.log('Transaction status is: ' + transactionStatus) - await new Promise((f) => setTimeout(f, 10000)) - transactionStatus = (await provider.getTransactionReceipt(response.transaction_hash)).status - } - console.log('Transaction is: ' + transactionStatus) - readStoredRound(account, contractAddress) -} - -export async function readStoredRound(account: Account, contractAddress: string) { - const round = await account.callContract({ - contractAddress: contractAddress, - entrypoint: 'get_stored_round', - }) - - console.log('\nStored values are:') - printResult(round) - return round -} - -export async function readStoredProxy(account: Account, contractAddress: string) { - const feed = await account.callContract({ - contractAddress: contractAddress, - entrypoint: 'get_stored_feed_address', - }) - - return feed -} - -function printResult(latestRound: 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('observation_timestamp =', parseInt(latestRound.result[3], 16)) - console.log('transmission_timestamp =', parseInt(latestRound.result[4], 16)) -} - -updateStoredRound(account, contractAddress) diff --git a/examples/contracts/proxy-consumer/scripts/readLatestRoundOffChain.ts b/examples/contracts/proxy-consumer/scripts/readLatestRoundOffChain.ts deleted file mode 100644 index 4fd27ebc7..000000000 --- a/examples/contracts/proxy-consumer/scripts/readLatestRoundOffChain.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Provider, CallContractResponse, constants } from 'starknet' - -// Starknet network: Either goerli-alpha or mainnet-alpha -const network = 'goerli-alpha' - -/** - * Network: Starknet Goerli testnet - * Aggregator: LINK/USD - * Address: 0x2579940ca3c41e7119283ceb82cd851c906cbb1510908a913d434861fdcb245 - * Find more proxy address at: - * https://docs.chain.link/data-feeds/price-feeds/addresses?network=starknet - */ -const dataFeedAddress = '0x2579940ca3c41e7119283ceb82cd851c906cbb1510908a913d434861fdcb245' - -export async function readLatestRoundOffChain() { - const provider = new Provider({ - sequencer: { - network: constants.NetworkName.SN_GOERLI, - }, - }) - - const latestRound = await provider.callContract({ - contractAddress: dataFeedAddress, - entrypoint: 'latest_round_data', - }) - - printResult(latestRound) - return latestRound -} - -function printResult(latestRound: 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('observation_timestamp =', parseInt(latestRound.result[3], 16)) - console.log('transmission_timestamp =', parseInt(latestRound.result[4], 16)) -} - -readLatestRoundOffChain() diff --git a/examples/contracts/proxy-consumer/src/lib.cairo b/examples/contracts/proxy-consumer/src/lib.cairo deleted file mode 100644 index bf37de90f..000000000 --- a/examples/contracts/proxy-consumer/src/lib.cairo +++ /dev/null @@ -1 +0,0 @@ -mod proxy_consumer; diff --git a/examples/contracts/proxy-consumer/src/proxy_consumer.cairo b/examples/contracts/proxy-consumer/src/proxy_consumer.cairo deleted file mode 100644 index 5be522cd5..000000000 --- a/examples/contracts/proxy-consumer/src/proxy_consumer.cairo +++ /dev/null @@ -1,52 +0,0 @@ -#[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 deleted file mode 100644 index 818935665..000000000 --- a/examples/contracts/proxy-consumer/yarn.lock +++ /dev/null @@ -1,95 +0,0 @@ -# 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" From 718003ca75020c9b4d1531659f6b8e11863e45c9 Mon Sep 17 00:00:00 2001 From: Chris De Leon Date: Thu, 29 Feb 2024 12:38:49 -0800 Subject: [PATCH 2/8] removes duplicate mocks and contracts, fixes bug with zeroable module import, updates test cases --- Makefile | 5 + .../contracts/aggregator_consumer/Scarb.lock | 1 - .../contracts/aggregator_consumer/Scarb.toml | 7 +- .../aggregator_consumer/src/emergency.cairo | 1 - .../src/emergency/sequencer_uptime_feed.cairo | 287 ------------------ .../aggregator_consumer/src/lib.cairo | 3 - .../aggregator_consumer/src/libraries.cairo | 1 - .../src/libraries/access_control.cairo | 152 ---------- .../aggregator_consumer/src/mocks.cairo | 1 - .../src/mocks/mock_aggregator.cairo | 121 -------- .../src/ocr2/consumer.cairo | 10 +- .../src/ocr2/price_consumer.cairo | 12 +- .../tests/test_consumer.cairo | 5 +- .../test_price_consumer_with_sequencer.cairo | 13 +- 14 files changed, 32 insertions(+), 587 deletions(-) delete mode 100644 examples/contracts/aggregator_consumer/src/emergency.cairo delete mode 100644 examples/contracts/aggregator_consumer/src/emergency/sequencer_uptime_feed.cairo delete mode 100644 examples/contracts/aggregator_consumer/src/libraries.cairo delete mode 100644 examples/contracts/aggregator_consumer/src/libraries/access_control.cairo delete mode 100644 examples/contracts/aggregator_consumer/src/mocks.cairo delete mode 100644 examples/contracts/aggregator_consumer/src/mocks/mock_aggregator.cairo diff --git a/Makefile b/Makefile index 22937f60b..1497e7874 100644 --- a/Makefile +++ b/Makefile @@ -224,6 +224,11 @@ test-integration-contracts: build-ts env-devnet-hardhat cd packages-ts/starknet/ && \ yarn test +.PHONY test-examples +test-examples: + cd ./examples/contracts/aggregator_consumer && \ + snforge test + .PHONY: test-integration-gauntlet # TODO: fix example # cd packages-ts/starknet-gauntlet-example/ && \ diff --git a/examples/contracts/aggregator_consumer/Scarb.lock b/examples/contracts/aggregator_consumer/Scarb.lock index 80cccaf0d..cd4104701 100644 --- a/examples/contracts/aggregator_consumer/Scarb.lock +++ b/examples/contracts/aggregator_consumer/Scarb.lock @@ -6,7 +6,6 @@ name = "aggregator_consumer" version = "0.1.0" dependencies = [ "chainlink", - "openzeppelin", "snforge_std", ] diff --git a/examples/contracts/aggregator_consumer/Scarb.toml b/examples/contracts/aggregator_consumer/Scarb.toml index 233211981..3e4b516d4 100644 --- a/examples/contracts/aggregator_consumer/Scarb.toml +++ b/examples/contracts/aggregator_consumer/Scarb.toml @@ -6,15 +6,16 @@ [package] name = "aggregator_consumer" version = "0.1.0" -edition = "2023_11" # See more keys and their definitions at https://docs.swmansion.com/scarb/docs/reference/manifest.html [dependencies] -openzeppelin = { git = "https://github.com/OpenZeppelin/cairo-contracts.git", tag = "v0.9.0" } snforge_std = { git = "https://github.com/foundry-rs/starknet-foundry", tag = "v0.18.0" } chainlink = { path = "../../../contracts" } -starknet = "2.5.4" [[target.starknet-contract]] casm = true +build-external-contracts = [ + "chainlink::emergency::sequencer_uptime_feed::SequencerUptimeFeed", + "chainlink::ocr2::mocks::mock_aggregator::MockAggregator", +] diff --git a/examples/contracts/aggregator_consumer/src/emergency.cairo b/examples/contracts/aggregator_consumer/src/emergency.cairo deleted file mode 100644 index 870a7f708..000000000 --- a/examples/contracts/aggregator_consumer/src/emergency.cairo +++ /dev/null @@ -1 +0,0 @@ -pub mod sequencer_uptime_feed; diff --git a/examples/contracts/aggregator_consumer/src/emergency/sequencer_uptime_feed.cairo b/examples/contracts/aggregator_consumer/src/emergency/sequencer_uptime_feed.cairo deleted file mode 100644 index 7aebf16fc..000000000 --- a/examples/contracts/aggregator_consumer/src/emergency/sequencer_uptime_feed.cairo +++ /dev/null @@ -1,287 +0,0 @@ -use starknet::EthAddress; -#[starknet::interface] -pub trait ISequencerUptimeFeed { - fn l1_sender(self: @TContractState) -> EthAddress; - fn set_l1_sender(ref self: TContractState, address: EthAddress); -} - -#[starknet::contract] -mod SequencerUptimeFeed { - use starknet::EthAddress; - use starknet::ContractAddress; - use starknet::SyscallResult; - use starknet::syscalls::storage_read_syscall; - use starknet::syscalls::storage_write_syscall; - use starknet::storage_access::storage_address_from_base_and_offset; - use starknet::class_hash::ClassHash; - - use core::option::OptionTrait; - use core::traits::TryInto; - - use openzeppelin::access::ownable::ownable::OwnableComponent; - - use aggregator_consumer::libraries::access_control::{AccessControlComponent, IAccessController}; - use aggregator_consumer::libraries::access_control::AccessControlComponent::InternalTrait as AccessControlInternalTrait; - use chainlink::libraries::type_and_version::ITypeAndVersion; - use chainlink::ocr2::aggregator::Round; - use chainlink::ocr2::aggregator::IAggregator; - use chainlink::ocr2::aggregator::{Transmission}; - use chainlink::libraries::upgradeable::Upgradeable; - - component!(path: OwnableComponent, storage: ownable, event: OwnableEvent); - component!(path: AccessControlComponent, storage: access_control, event: AccessControlEvent); - - #[abi(embed_v0)] - impl OwnableImpl = OwnableComponent::OwnableTwoStepImpl; - impl OwnableInternalImpl = OwnableComponent::InternalImpl; - - #[abi(embed_v0)] - impl AccessControlImpl = - AccessControlComponent::AccessControlImpl; - impl AccessControlInternalImpl = AccessControlComponent::InternalImpl; - - #[storage] - struct Storage { - #[substorage(v0)] - ownable: OwnableComponent::Storage, - #[substorage(v0)] - access_control: AccessControlComponent::Storage, - // l1 sender is an starknet validator ethereum address - _l1_sender: felt252, - // maps round id to round transmission - _round_transmissions: LegacyMap, - _latest_round_id: u128, - } - - #[event] - #[derive(Drop, starknet::Event)] - enum Event { - #[flat] - OwnableEvent: OwnableComponent::Event, - #[flat] - AccessControlEvent: AccessControlComponent::Event, - RoundUpdated: RoundUpdated, - NewRound: NewRound, - AnswerUpdated: AnswerUpdated, - UpdateIgnored: UpdateIgnored, - L1SenderTransferred: L1SenderTransferred, - } - - #[derive(Drop, starknet::Event)] - struct RoundUpdated { - status: u128, - updated_at: u64 - } - - #[derive(Drop, starknet::Event)] - struct NewRound { - round_id: u128, - started_by: ContractAddress, - started_at: u64 - } - - #[derive(Drop, starknet::Event)] - struct AnswerUpdated { - current: u128, - round_id: u128, - timestamp: u64 - } - - #[derive(Drop, starknet::Event)] - struct UpdateIgnored { - latest_status: u128, - latest_timestamp: u64, - incoming_status: u128, - incoming_timestamp: u64 - } - - #[derive(Drop, starknet::Event)] - struct L1SenderTransferred { - from_address: EthAddress, - to_address: EthAddress - } - - #[abi(embed_v0)] - impl TypeAndVersion of ITypeAndVersion { - fn type_and_version(self: @ContractState) -> felt252 { - 'SequencerUptimeFeed 1.0.0' - } - } - - #[abi(embed_v0)] - impl AggregatorImpl of IAggregator { - fn latest_round_data(self: @ContractState) -> Round { - self._require_read_access(); - let latest_round_id = self._latest_round_id.read(); - let round_transmission = self._round_transmissions.read(latest_round_id); - Round { - round_id: latest_round_id.into(), - answer: round_transmission.answer, - block_num: round_transmission.block_num, - started_at: round_transmission.observation_timestamp, - updated_at: round_transmission.transmission_timestamp, - } - } - - fn round_data(self: @ContractState, round_id: u128) -> Round { - self._require_read_access(); - assert(round_id < self._latest_round_id.read(), 'invalid round id'); - let round_transmission = self._round_transmissions.read(round_id); - Round { - round_id: round_id.into(), - answer: round_transmission.answer, - block_num: round_transmission.block_num, - started_at: round_transmission.observation_timestamp, - updated_at: round_transmission.transmission_timestamp, - } - } - - fn description(self: @ContractState) -> felt252 { - 'L2 Sequencer Uptime Status Feed' - } - - fn decimals(self: @ContractState) -> u8 { - 0_u8 - } - } - - #[constructor] - fn constructor(ref self: ContractState, initial_status: u128, owner_address: ContractAddress) { - self._initializer(initial_status, owner_address); - } - - #[l1_handler] - fn update_status(ref self: ContractState, from_address: felt252, status: u128, timestamp: u64) { - assert(self._l1_sender.read() == from_address, 'EXPECTED_FROM_BRIDGE_ONLY'); - - let latest_round_id = self._latest_round_id.read(); - let latest_round = self._round_transmissions.read(latest_round_id); - - if timestamp <= latest_round.observation_timestamp { - self - .emit( - Event::UpdateIgnored( - UpdateIgnored { - latest_status: latest_round.answer, - latest_timestamp: latest_round.transmission_timestamp, - incoming_status: status, - incoming_timestamp: timestamp - } - ) - ); - return (); - } - - if latest_round.answer == status { - self._update_round(latest_round_id, latest_round); - } else { - // only increment round when status flips - let round_id = latest_round_id + 1_u128; - self._record_round(round_id, status, timestamp); - } - } - - #[abi(embed_v0)] - impl SequencerUptimeFeedImpl of super::ISequencerUptimeFeed { - fn set_l1_sender(ref self: ContractState, address: EthAddress) { - self.ownable.assert_only_owner(); - - // TODO: - // assert(!address.is_zero(), '0 address not allowed'); - - let old_address = self._l1_sender.read(); - - if old_address != address.into() { - self._l1_sender.write(address.into()); - self - .emit( - Event::L1SenderTransferred( - L1SenderTransferred { - from_address: old_address.try_into().unwrap(), to_address: address - } - ) - ); - } - } - - fn l1_sender(self: @ContractState) -> EthAddress { - self._l1_sender.read().try_into().unwrap() - } - } - - /// - /// Upgradeable - /// - - #[abi(embed_v0)] - fn upgrade(ref self: ContractState, new_impl: ClassHash) { - self.ownable.assert_only_owner(); - Upgradeable::upgrade(new_impl) - } - - /// - /// Internals - /// - - #[generate_trait] - impl Internals of InternalTrait { - fn _require_read_access(self: @ContractState) { - let sender = starknet::get_caller_address(); - self.access_control.check_read_access(sender); - } - - fn _initializer( - ref self: ContractState, initial_status: u128, owner_address: ContractAddress - ) { - self.ownable.initializer(owner_address); - self.access_control.initializer(); - let round_id = 1_u128; - let timestamp = starknet::get_block_timestamp(); - self._record_round(round_id, initial_status, timestamp); - } - - fn _record_round(ref self: ContractState, round_id: u128, status: u128, timestamp: u64) { - self._latest_round_id.write(round_id); - let block_info = starknet::get_block_info().unbox(); - let block_number = block_info.block_number; - let block_timestamp = block_info.block_timestamp; - - let round = Transmission { - answer: status, - block_num: block_number, - observation_timestamp: timestamp, - transmission_timestamp: block_timestamp, - }; - self._round_transmissions.write(round_id, round); - - let sender = starknet::get_caller_address(); - - self - .emit( - Event::NewRound( - NewRound { round_id: round_id, started_by: sender, started_at: timestamp } - ) - ); - self - .emit( - Event::AnswerUpdated( - AnswerUpdated { current: status, round_id: round_id, timestamp: timestamp } - ) - ); - } - - fn _update_round(ref self: ContractState, round_id: u128, mut round: Transmission) { - round.transmission_timestamp = starknet::get_block_timestamp(); - self._round_transmissions.write(round_id, round); - - self - .emit( - Event::RoundUpdated( - RoundUpdated { - status: round.answer, updated_at: round.transmission_timestamp - } - ) - ); - } - } -} diff --git a/examples/contracts/aggregator_consumer/src/lib.cairo b/examples/contracts/aggregator_consumer/src/lib.cairo index cc02a54bd..ea174388f 100644 --- a/examples/contracts/aggregator_consumer/src/lib.cairo +++ b/examples/contracts/aggregator_consumer/src/lib.cairo @@ -1,5 +1,2 @@ -pub mod libraries; -pub mod emergency; -pub mod mocks; pub mod ocr2; diff --git a/examples/contracts/aggregator_consumer/src/libraries.cairo b/examples/contracts/aggregator_consumer/src/libraries.cairo deleted file mode 100644 index 18e237199..000000000 --- a/examples/contracts/aggregator_consumer/src/libraries.cairo +++ /dev/null @@ -1 +0,0 @@ -pub mod access_control; diff --git a/examples/contracts/aggregator_consumer/src/libraries/access_control.cairo b/examples/contracts/aggregator_consumer/src/libraries/access_control.cairo deleted file mode 100644 index a44fee769..000000000 --- a/examples/contracts/aggregator_consumer/src/libraries/access_control.cairo +++ /dev/null @@ -1,152 +0,0 @@ -use starknet::ContractAddress; -#[starknet::interface] -pub trait IAccessController { - fn has_access(self: @TContractState, user: ContractAddress, data: Array) -> bool; - fn has_read_access(self: @TContractState, user: ContractAddress, data: Array) -> bool; - fn add_access(ref self: TContractState, user: ContractAddress); - fn remove_access(ref self: TContractState, user: ContractAddress); - fn enable_access_check(ref self: TContractState); - fn disable_access_check(ref self: TContractState); -} - -// Requires Ownable subcomponent. -#[starknet::component] -pub mod AccessControlComponent { - use starknet::ContractAddress; - use starknet::class_hash::ClassHash; - - use openzeppelin::access::ownable::ownable::OwnableComponent; - - use OwnableComponent::InternalImpl as OwnableInternalImpl; - - #[storage] - struct Storage { - _check_enabled: bool, - _access_list: LegacyMap, - } - - #[event] - #[derive(Drop, starknet::Event)] - pub enum Event { - AddedAccess: AddedAccess, - RemovedAccess: RemovedAccess, - AccessControlEnabled: AccessControlEnabled, - AccessControlDisabled: AccessControlDisabled, - } - - #[derive(Drop, starknet::Event)] - struct AddedAccess { - user: ContractAddress - } - - #[derive(Drop, starknet::Event)] - struct RemovedAccess { - user: ContractAddress - } - - #[derive(Drop, starknet::Event)] - struct AccessControlEnabled {} - - #[derive(Drop, starknet::Event)] - struct AccessControlDisabled {} - - #[embeddable_as(AccessControlImpl)] - pub impl AccessControl< - TContractState, - +HasComponent, - impl Ownable: OwnableComponent::HasComponent, - +Drop, - > of super::IAccessController> { - fn has_access( - self: @ComponentState, user: ContractAddress, data: Array - ) -> bool { - let has_access = self._access_list.read(user); - if has_access { - return true; - } - - let check_enabled = self._check_enabled.read(); - if !check_enabled { - return true; - } - - false - } - - fn has_read_access( - self: @ComponentState, user: ContractAddress, data: Array - ) -> bool { - let _has_access = self.has_access(user, data); - if _has_access { - return true; - } - - // TODO: - // NOTICE: read access is granted to direct calls, to enable off-chain reads. - // if user.is_zero() { - // return true; - //} - - false - } - - fn add_access(ref self: ComponentState, user: ContractAddress) { - get_dep_component!(@self, Ownable).assert_only_owner(); - let has_access = self._access_list.read(user); - if !has_access { - self._access_list.write(user, true); - self.emit(Event::AddedAccess(AddedAccess { user: user })); - } - } - - fn remove_access(ref self: ComponentState, user: ContractAddress) { - get_dep_component!(@self, Ownable).assert_only_owner(); - let has_access = self._access_list.read(user); - if has_access { - self._access_list.write(user, false); - self.emit(Event::RemovedAccess(RemovedAccess { user: user })); - } - } - - fn enable_access_check(ref self: ComponentState) { - get_dep_component!(@self, Ownable).assert_only_owner(); - let check_enabled = self._check_enabled.read(); - if !check_enabled { - self._check_enabled.write(true); - self.emit(Event::AccessControlEnabled(AccessControlEnabled {})); - } - } - - fn disable_access_check(ref self: ComponentState) { - get_dep_component!(@self, Ownable).assert_only_owner(); - let check_enabled = self._check_enabled.read(); - if check_enabled { - self._check_enabled.write(false); - self.emit(Event::AccessControlDisabled(AccessControlDisabled {})); - } - } - } - - #[generate_trait] - pub impl InternalImpl< - TContractState, - +HasComponent, - impl Ownable: OwnableComponent::HasComponent, - +Drop, - > of InternalTrait { - fn initializer(ref self: ComponentState) { - self._check_enabled.write(true); - self.emit(Event::AccessControlEnabled(AccessControlEnabled {})); - } - - fn check_access(self: @ComponentState, user: ContractAddress) { - let allowed = AccessControl::has_access(self, user, ArrayTrait::new()); - assert(allowed, 'user does not have access'); - } - - fn check_read_access(self: @ComponentState, user: ContractAddress) { - let allowed = AccessControl::has_read_access(self, user, ArrayTrait::new()); - assert(allowed, 'user does not have read access'); - } - } -} diff --git a/examples/contracts/aggregator_consumer/src/mocks.cairo b/examples/contracts/aggregator_consumer/src/mocks.cairo deleted file mode 100644 index b42bbb0d5..000000000 --- a/examples/contracts/aggregator_consumer/src/mocks.cairo +++ /dev/null @@ -1 +0,0 @@ -pub mod mock_aggregator; diff --git a/examples/contracts/aggregator_consumer/src/mocks/mock_aggregator.cairo b/examples/contracts/aggregator_consumer/src/mocks/mock_aggregator.cairo deleted file mode 100644 index 36b220181..000000000 --- a/examples/contracts/aggregator_consumer/src/mocks/mock_aggregator.cairo +++ /dev/null @@ -1,121 +0,0 @@ -#[starknet::interface] -pub trait IMockAggregator { - fn set_latest_round_data( - ref self: TContractState, - answer: u128, - block_num: u64, - observation_timestamp: u64, - transmission_timestamp: u64 - ); -} - -#[starknet::contract] -mod MockAggregator { - use starknet::contract_address_const; - use core::panic_with_felt252; - - use chainlink::libraries::type_and_version::ITypeAndVersion; - - use chainlink::ocr2::aggregator::Aggregator::{Transmission, NewTransmission}; - use chainlink::ocr2::aggregator::IAggregator; - use chainlink::ocr2::aggregator::Round; - - #[event] - use chainlink::ocr2::aggregator::Aggregator::Event; - - #[storage] - struct Storage { - _transmissions: LegacyMap, - _latest_aggregator_round_id: u128, - _decimals: u8 - } - - #[constructor] - fn constructor(ref self: ContractState, decimals: u8) { - self._decimals.write(decimals); - } - - #[abi(embed_v0)] - impl MockImpl of super::IMockAggregator { - fn set_latest_round_data( - ref self: ContractState, - answer: u128, - block_num: u64, - observation_timestamp: u64, - transmission_timestamp: u64 - ) { - let new_round_id = self._latest_aggregator_round_id.read() + 1_u128; - self - ._transmissions - .write( - new_round_id, - Transmission { - answer: answer, - block_num: block_num, - observation_timestamp: observation_timestamp, - transmission_timestamp: transmission_timestamp - } - ); - - let mut observations = ArrayTrait::new(); - observations.append(2_u128); - observations.append(3_u128); - - self._latest_aggregator_round_id.write(new_round_id); - - self - .emit( - Event::NewTransmission( - NewTransmission { - round_id: new_round_id, - answer: answer, - transmitter: contract_address_const::<42>(), - observation_timestamp: observation_timestamp, - observers: 3, - observations: observations, - juels_per_fee_coin: 18_u128, - gas_price: 1_u128, - config_digest: 777, - epoch_and_round: 20_u64, - reimbursement: 100_u128 - } - ) - ); - } - } - - #[abi(embed_v0)] - impl TypeAndVersionImpl of ITypeAndVersion { - fn type_and_version(self: @ContractState) -> felt252 { - 'mock_aggregator.cairo 1.0.0' - } - } - - #[abi(embed_v0)] - impl Aggregator of IAggregator { - fn round_data(self: @ContractState, round_id: u128) -> Round { - panic_with_felt252('unimplemented') - } - - fn latest_round_data(self: @ContractState) -> Round { - let latest_round_id = self._latest_aggregator_round_id.read(); - let transmission = self._transmissions.read(latest_round_id); - - Round { - round_id: latest_round_id.into(), - answer: transmission.answer, - block_num: transmission.block_num, - started_at: transmission.observation_timestamp, - updated_at: transmission.transmission_timestamp - } - } - - fn decimals(self: @ContractState) -> u8 { - self._decimals.read() - } - - fn description(self: @ContractState) -> felt252 { - 'mock' - } - } -} diff --git a/examples/contracts/aggregator_consumer/src/ocr2/consumer.cairo b/examples/contracts/aggregator_consumer/src/ocr2/consumer.cairo index f0448b309..bf67f2a3c 100644 --- a/examples/contracts/aggregator_consumer/src/ocr2/consumer.cairo +++ b/examples/contracts/aggregator_consumer/src/ocr2/consumer.cairo @@ -6,12 +6,14 @@ pub trait IAggregatorConsumer { #[starknet::contract] mod AggregatorConsumer { - use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcherTrait; - use chainlink::ocr2::aggregator_proxy::IAggregatorDispatcher; - use chainlink::ocr2::aggregator_proxy::IAggregator; - use chainlink::ocr2::aggregator::Round; 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, diff --git a/examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo b/examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo index ca8c5cec5..9f2d2e381 100644 --- a/examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo +++ b/examples/contracts/aggregator_consumer/src/ocr2/price_consumer.cairo @@ -5,10 +5,10 @@ pub trait IAggregatorPriceConsumer { #[starknet::contract] mod AggregatorPriceConsumer { - use core::starknet::ContractAddress; - use core::starknet::get_block_info; - use core::box::BoxTrait; - use core::traits::Into; + use box::BoxTrait; + use starknet::ContractAddress; + use zeroable::Zeroable; + use traits::Into; use chainlink::ocr2::aggregator::Round; use chainlink::ocr2::aggregator_proxy::IAggregator; @@ -30,6 +30,8 @@ mod AggregatorPriceConsumer { 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); } @@ -48,7 +50,7 @@ mod AggregatorPriceConsumer { fn assert_sequencer_healthy(self: @ContractState) { let round = IAggregatorDispatcher { contract_address: self._uptime_feed_address.read() } .latest_round_data(); - let timestamp = get_block_info().unbox().block_timestamp; + let timestamp = starknet::get_block_info().unbox().block_timestamp; // After 60 sec the report is considered stale let report_stale = timestamp - round.updated_at > 60_u64; diff --git a/examples/contracts/aggregator_consumer/tests/test_consumer.cairo b/examples/contracts/aggregator_consumer/tests/test_consumer.cairo index 47d48b9fa..1bb3b4fe4 100644 --- a/examples/contracts/aggregator_consumer/tests/test_consumer.cairo +++ b/examples/contracts/aggregator_consumer/tests/test_consumer.cairo @@ -1,7 +1,8 @@ use snforge_std::{declare, ContractClassTrait}; -use aggregator_consumer::mocks::mock_aggregator::IMockAggregatorDispatcherTrait; -use aggregator_consumer::mocks::mock_aggregator::IMockAggregatorDispatcher; +use chainlink::ocr2::mocks::mock_aggregator::IMockAggregatorDispatcherTrait; +use chainlink::ocr2::mocks::mock_aggregator::IMockAggregatorDispatcher; + use aggregator_consumer::ocr2::consumer::IAggregatorConsumerDispatcherTrait; use aggregator_consumer::ocr2::consumer::IAggregatorConsumerDispatcher; diff --git a/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo b/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo index 17de9dd35..cd61f77fc 100644 --- a/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo +++ b/examples/contracts/aggregator_consumer/tests/test_price_consumer_with_sequencer.cairo @@ -1,13 +1,14 @@ use snforge_std::{declare, ContractClassTrait, start_prank, stop_prank, CheatTarget}; -use aggregator_consumer::emergency::sequencer_uptime_feed::ISequencerUptimeFeedDispatcherTrait; -use aggregator_consumer::emergency::sequencer_uptime_feed::ISequencerUptimeFeedDispatcher; +use chainlink::emergency::sequencer_uptime_feed::ISequencerUptimeFeedDispatcherTrait; +use chainlink::emergency::sequencer_uptime_feed::ISequencerUptimeFeedDispatcher; +use chainlink::libraries::access_control::IAccessControllerDispatcherTrait; +use chainlink::libraries::access_control::IAccessControllerDispatcher; +use chainlink::ocr2::mocks::mock_aggregator::IMockAggregatorDispatcherTrait; +use chainlink::ocr2::mocks::mock_aggregator::IMockAggregatorDispatcher; + use aggregator_consumer::ocr2::price_consumer::IAggregatorPriceConsumerDispatcherTrait; use aggregator_consumer::ocr2::price_consumer::IAggregatorPriceConsumerDispatcher; -use aggregator_consumer::libraries::access_control::IAccessControllerDispatcherTrait; -use aggregator_consumer::libraries::access_control::IAccessControllerDispatcher; -use aggregator_consumer::mocks::mock_aggregator::IMockAggregatorDispatcherTrait; -use aggregator_consumer::mocks::mock_aggregator::IMockAggregatorDispatcher; use starknet::contract_address_const; use starknet::get_caller_address; From 073bc82e58cc416d32d8db024d4520765fbd19bc Mon Sep 17 00:00:00 2001 From: Chris De Leon Date: Thu, 29 Feb 2024 13:18:47 -0800 Subject: [PATCH 3/8] removes the old aggregator-consumer examples --- .../contracts/aggregator-consumer/.gitignore | 3 - .../contracts/aggregator-consumer/README.md | 70 ---------- .../contracts/aggregator-consumer/Scarb.toml | 14 -- .../aggregator-consumer/cairo_project.toml | 2 - .../contracts/aggregator-consumer/example.env | 11 -- .../aggregator-consumer/hardhat.config.ts | 35 ----- .../aggregator-consumer/package.json | 33 ----- .../scripts/consumerValidator.ts | 123 ------------------ .../scripts/deploy_accounts.ts | 80 ------------ .../scripts/deploy_contracts.ts | 105 --------------- .../scripts/getLatestPriceSeqCheck.ts | 79 ----------- .../scripts/readContinuously.ts | 40 ------ .../scripts/readDecimals.ts | 26 ---- .../scripts/readLatestRound.ts | 35 ----- .../scripts/updateLatestRound.ts | 85 ------------ .../aggregator-consumer/scripts/utils.ts | 85 ------------ .../aggregator-consumer/src/lib.cairo | 1 - .../aggregator-consumer/src/ocr2.cairo | 2 - .../src/ocr2/consumer.cairo | 30 ----- .../ocr2/price_consumer_with_sequencer.cairo | 63 --------- .../aggregator-consumer/tsconfig.json | 17 --- 21 files changed, 939 deletions(-) delete mode 100644 examples/contracts/aggregator-consumer/.gitignore delete mode 100644 examples/contracts/aggregator-consumer/README.md delete mode 100644 examples/contracts/aggregator-consumer/Scarb.toml delete mode 100644 examples/contracts/aggregator-consumer/cairo_project.toml delete mode 100644 examples/contracts/aggregator-consumer/example.env delete mode 100644 examples/contracts/aggregator-consumer/hardhat.config.ts delete mode 100644 examples/contracts/aggregator-consumer/package.json delete mode 100644 examples/contracts/aggregator-consumer/scripts/consumerValidator.ts delete mode 100644 examples/contracts/aggregator-consumer/scripts/deploy_accounts.ts delete mode 100644 examples/contracts/aggregator-consumer/scripts/deploy_contracts.ts delete mode 100644 examples/contracts/aggregator-consumer/scripts/getLatestPriceSeqCheck.ts delete mode 100644 examples/contracts/aggregator-consumer/scripts/readContinuously.ts delete mode 100644 examples/contracts/aggregator-consumer/scripts/readDecimals.ts delete mode 100644 examples/contracts/aggregator-consumer/scripts/readLatestRound.ts delete mode 100644 examples/contracts/aggregator-consumer/scripts/updateLatestRound.ts delete mode 100644 examples/contracts/aggregator-consumer/scripts/utils.ts delete mode 100644 examples/contracts/aggregator-consumer/src/lib.cairo delete mode 100644 examples/contracts/aggregator-consumer/src/ocr2.cairo delete mode 100644 examples/contracts/aggregator-consumer/src/ocr2/consumer.cairo delete mode 100644 examples/contracts/aggregator-consumer/src/ocr2/price_consumer_with_sequencer.cairo delete mode 100644 examples/contracts/aggregator-consumer/tsconfig.json diff --git a/examples/contracts/aggregator-consumer/.gitignore b/examples/contracts/aggregator-consumer/.gitignore deleted file mode 100644 index 96611b42c..000000000 --- a/examples/contracts/aggregator-consumer/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -target -cache/ -starknet-artifacts/ diff --git a/examples/contracts/aggregator-consumer/README.md b/examples/contracts/aggregator-consumer/README.md deleted file mode 100644 index e390b44ef..000000000 --- a/examples/contracts/aggregator-consumer/README.md +++ /dev/null @@ -1,70 +0,0 @@ -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 deleted file mode 100644 index 18ac52c35..000000000 --- a/examples/contracts/aggregator-consumer/Scarb.toml +++ /dev/null @@ -1,14 +0,0 @@ -[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 deleted file mode 100644 index e2ab1041e..000000000 --- a/examples/contracts/aggregator-consumer/cairo_project.toml +++ /dev/null @@ -1,2 +0,0 @@ -[crate_roots] -chainlink_examples = "src" diff --git a/examples/contracts/aggregator-consumer/example.env b/examples/contracts/aggregator-consumer/example.env deleted file mode 100644 index 7d29ecc93..000000000 --- a/examples/contracts/aggregator-consumer/example.env +++ /dev/null @@ -1,11 +0,0 @@ -# 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/hardhat.config.ts b/examples/contracts/aggregator-consumer/hardhat.config.ts deleted file mode 100644 index 965b87a03..000000000 --- a/examples/contracts/aggregator-consumer/hardhat.config.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { HardhatUserConfig } from 'hardhat/types' -import '@shardlabs/starknet-hardhat-plugin' -import '@nomiclabs/hardhat-ethers' - -const config: HardhatUserConfig = { - solidity: '0.8.14', - starknet: { - venv: 'active', - network: 'devnet', - wallets: { - OpenZeppelin: { - accountName: 'OpenZeppelin', - modulePath: 'starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount', - accountPath: '~/.starknet_accounts', - }, - }, - }, - networks: { - devnet: { - url: 'http://127.0.0.1:5050', - args: ['--cairo-compiler-manifest', '../../../vendor/cairo/Cargo.toml'], - }, - integratedDevnet: { - url: 'http://127.0.0.1:5050', - venv: 'active', - args: ['--cairo-compiler-manifest', '../../../vendor/cairo/Cargo.toml', '--lite-mode'], - // dockerizedVersion: "0.2.0" - }, - }, - paths: { - cairoPaths: ['../../contracts/src'], - }, -} - -export default config diff --git a/examples/contracts/aggregator-consumer/package.json b/examples/contracts/aggregator-consumer/package.json deleted file mode 100644 index 2aff8abbe..000000000 --- a/examples/contracts/aggregator-consumer/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "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 deleted file mode 100644 index 6441c3ff2..000000000 --- a/examples/contracts/aggregator-consumer/scripts/consumerValidator.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { ethers, starknet, network } from 'hardhat' -import { Contract, ContractFactory } from 'ethers' -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, CompiledContract, Contract as StarknetContract } from 'starknet' -import { - createDeployerAccount, - loadContractPath, - loadContract_Solidity, - loadContract_Solidity_V8, - makeProvider, -} from './utils' - -dotenv.config({ path: __dirname + '/../.env' }) -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 -let mockStarknetMessenger: Contract -let deployer: SignerWithAddress -let eoaValidator: SignerWithAddress -let networkUrl: string -let account: Account - -let mockGasPriceFeed: MockContract -let mockAccessController: MockContract -let mockAggregator: MockContract - -export async function consumerValidator() { - const provider = makeProvider() - - account = createDeployerAccount(provider) - - networkUrl = (network.config as HttpNetworkConfig).url - const accounts = await ethers.getSigners() - deployer = accounts[0] - eoaValidator = accounts[1] - - const aggregatorAbi = loadContract_Solidity_V8('AggregatorV3Interface') - const accessControllerAbi = loadContract_Solidity_V8('AccessControllerInterface') - - // Deploy the mock feed - mockGasPriceFeed = await deployMockContract(deployer, aggregatorAbi.abi) - await mockGasPriceFeed.mock.latestRoundData.returns( - '73786976294838220258' /** roundId */, - '96800000000' /** answer */, - '163826896' /** startedAt */, - '1638268960' /** updatedAt */, - '73786976294838220258' /** answeredInRound */, - ) - - // Deploy the mock access controller - mockAccessController = await deployMockContract(deployer, accessControllerAbi.abi) - - // Deploy the mock aggregator - mockAggregator = await deployMockContract(deployer, aggregatorAbi.abi) - await mockAggregator.mock.latestRoundData.returns( - '73786976294838220258' /** roundId */, - 1 /** answer */, - '163826896' /** startedAt */, - '1638268960' /** updatedAt */, - '73786976294838220258' /** answeredInRound */, - ) - - const validatorArtifact = await loadContract_Solidity('emergency', 'StarknetValidator') - const validatorFactory = await ethers.getContractFactoryFromArtifact(validatorArtifact, deployer) - - const mockStarknetMessagingArtifact = await loadContract_Solidity( - 'mocks', - 'MockStarknetMessaging', - ) - mockStarknetMessengerFactory = await ethers.getContractFactoryFromArtifact( - mockStarknetMessagingArtifact, - deployer, - ) - - const messageCancellationDelay = 5 * 60 // seconds - mockStarknetMessenger = await mockStarknetMessengerFactory.deploy(messageCancellationDelay) - await mockStarknetMessenger.deployed() - - const UptimeFeedArtifact = loadContractPath(UPTIME_FEED_PATH) as CompiledContract - - const mockUptimeFeedDeploy = new StarknetContract( - UptimeFeedArtifact.abi, - process.env.UPTIME_FEED as string, - provider, - ) - - mockUptimeFeedDeploy.connect(account) - - validator = await validatorFactory.deploy( - mockStarknetMessenger.address, - mockAccessController.address, - mockGasPriceFeed.address, - mockAggregator.address, - mockUptimeFeedDeploy.address, - 0, - ) - - console.log('Validator address: ', validator.address) - - 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) -} - -async function callFunction() { - await starknet.devnet.loadL1MessagingContract(networkUrl, mockStarknetMessenger.address) - - await validator.connect(eoaValidator).validate(0, 0, 1, 1) - - const flushL1Response = await starknet.devnet.flush() - flushL1Response.consumed_messages.from_l1 -} -consumerValidator() diff --git a/examples/contracts/aggregator-consumer/scripts/deploy_accounts.ts b/examples/contracts/aggregator-consumer/scripts/deploy_accounts.ts deleted file mode 100644 index e031c63fa..000000000 --- a/examples/contracts/aggregator-consumer/scripts/deploy_accounts.ts +++ /dev/null @@ -1,80 +0,0 @@ -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' - -const ENV_PATH = __dirname + '/../.env' - -dotenv.config({ path: ENV_PATH }) - -interface UserAccount { - address: string - privateKey: string -} - -export async function deployAccount() { - const account = await createAccount() - - 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) - - // 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 }) - - // declare OZ account - const declareTx = await fundedAccount.declare({ - classHash: OZaccountClassHash, - contract: compiledAccount, - }) - console.log('Declare new Account...') - await provider.waitForTransaction(declareTx.transaction_hash) - - // 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, - addressSalt: starkKeyPub, - }) - - console.log('Waiting for Tx to be Accepted on Starknet - OZ Account Deployment...') - await provider.waitForTransaction(transaction_hash) - - console.log('actual contract address', contract_address) - - 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 deleted file mode 100644 index ece2fed60..000000000 --- a/examples/contracts/aggregator-consumer/scripts/deploy_contracts.ts +++ /dev/null @@ -1,105 +0,0 @@ -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 AGGREGATOR = 'MockAggregator' - -const AGGREGATOR_PATH = '../../../../contracts/target/release/chainlink_MockAggregator' - -const CONSUMER = 'AggregatorConsumer' - -const UPTIME_FEED_PATH = '../../../../contracts/target/release/chainlink_SequencerUptimeFeed' - -const PRICE_CONSUMER = 'AggregatorPriceConsumerWithSequencer' - -const DECIMALS = '18' - -dotenv.config({ path: __dirname + '/../.env' }) - -export async function deployContract() { - const provider = makeProvider() - 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) - - 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 aggregatorDeploy = new Contract( - AggregatorArtifact.abi, - declareDeployAggregator.deploy.contract_address, - provider, - ) - - const declareDeployConsumer = await account.declareAndDeploy({ - casm: loadCasmContract(CONSUMER), - contract: ConsumerArtifact, - constructorCalldata: [aggregatorDeploy.address as string], - }) - - const consumerDeploy = new Contract( - 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], - }) - - const uptimeFeedDeploy = new Contract( - UptimeFeedArtifact.abi, - declareDeployUptimeFeed.deploy.contract_address, - provider, - ) - - const declareDeployPriceConsumer = await account.declareAndDeploy({ - casm: loadCasmContract(PRICE_CONSUMER), - contract: PriceConsumerArtifact, - constructorCalldata: [uptimeFeedDeploy.address as string, aggregatorDeploy.address as string], - }) - - const priceConsumerDeploy = new Contract( - PriceConsumerArtifact.abi, - declareDeployPriceConsumer.deploy.contract_address, - provider, - ) - - fs.appendFile(__dirname + '/../.env', '\nCONSUMER=' + consumerDeploy.address, function (err) { - if (err) throw err - }) - fs.appendFile(__dirname + '/../.env', '\nMOCK=' + aggregatorDeploy.address, function (err) { - if (err) throw err - }) - fs.appendFile( - __dirname + '/../.env', - '\nPRICE_CONSUMER=' + priceConsumerDeploy.address, - function (err) { - if (err) throw err - }, - ) - fs.appendFile(__dirname + '/../.env', '\nUPTIME_FEED=' + uptimeFeedDeploy.address, function ( - err, - ) { - if (err) throw err - }) -} - -deployContract() diff --git a/examples/contracts/aggregator-consumer/scripts/getLatestPriceSeqCheck.ts b/examples/contracts/aggregator-consumer/scripts/getLatestPriceSeqCheck.ts deleted file mode 100644 index d58cb9c27..000000000 --- a/examples/contracts/aggregator-consumer/scripts/getLatestPriceSeqCheck.ts +++ /dev/null @@ -1,79 +0,0 @@ -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 deleted file mode 100644 index 442c732f9..000000000 --- a/examples/contracts/aggregator-consumer/scripts/readContinuously.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Contract, Account, CallContractResponse, Result } from 'starknet' - -import { createDeployerAccount, loadContract, makeProvider } from './utils' -import dotenv from 'dotenv' - -const CONTRACT_NAME = 'AggregatorConsumer' -let account: Account -let consumer: Contract - -dotenv.config({ path: __dirname + '/../.env' }) - -async function readContinuously() { - const provider = makeProvider() - - account = createDeployerAccount(provider) - - const AggregatorArtifact = loadContract(CONTRACT_NAME) - - consumer = new Contract(AggregatorArtifact.abi, process.env.CONSUMER as string) - consumer.connect(account) - setInterval(callFunction, 3000) -} - -async function callFunction() { - const latestRound = await consumer.call('read_latest_round') - const decimals = await consumer.call('read_decimals') - printResult(latestRound, decimals) -} - -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 deleted file mode 100644 index 7d76b80bb..000000000 --- a/examples/contracts/aggregator-consumer/scripts/readDecimals.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Account, Contract } from 'starknet' -import { createDeployerAccount, loadContract, makeProvider } from './utils' -import dotenv from 'dotenv' - -const CONSUMER_NAME = 'AggregatorConsumer' -let account: Account -let consumer: Contract - -dotenv.config({ path: __dirname + '/../.env' }) - -export async function readDecimals() { - const provider = makeProvider() - account = createDeployerAccount(provider) - - const AggregatorArtifact = loadContract(CONSUMER_NAME) - consumer = new Contract(AggregatorArtifact.abi, process.env.CONSUMER as string) - - consumer.connect(account) - - 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 deleted file mode 100644 index 762144a1b..000000000 --- a/examples/contracts/aggregator-consumer/scripts/readLatestRound.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Account, Contract, Result } from 'starknet' - -import { createDeployerAccount, loadContract, makeProvider } from './utils' -import dotenv from 'dotenv' - -const CONSUMER_NAME = 'AggregatorConsumer' -let account: Account -let consumer: Contract - -dotenv.config({ path: __dirname + '/../.env' }) - -export async function readLatestRound() { - const provider = makeProvider() - account = createDeployerAccount(provider) - - const AggregatorArtifact = loadContract(CONSUMER_NAME) - consumer = new Contract(AggregatorArtifact.abi, process.env.CONSUMER as string) - - consumer.connect(account) - - const latestRound = await consumer.call('read_latest_round') - - printResult(latestRound) - return latestRound -} - -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 deleted file mode 100644 index 5d0b1bcd4..000000000 --- a/examples/contracts/aggregator-consumer/scripts/updateLatestRound.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Account, CompiledContract, Contract, Provider, ec, number } from 'starknet' -import { loadContract, loadContractPath, makeProvider } from './utils' -import dotenv from 'dotenv' - -interface Transmission { - answer: number - block_num: number - observation_timestamp: number - transmission_timestamp: number -} - -const CONTRACT_NAME = 'MockAggregator' -const CONTRACT_PATH = '../../../../contracts/target/release/chainlink_MockAggregator' -let account: Account -let mock: Contract -let transmission: Transmission -let provider: Provider - -dotenv.config({ path: __dirname + '/../.env' }) - -const rl = require('readline').createInterface({ - input: process.stdin, - output: process.stdout, -}) - -async function updateLatestRound() { - provider = makeProvider() - - transmission = { - answer: 0, - block_num: 0, - observation_timestamp: 0, - transmission_timestamp: 0, - } - - const privateKey = process.env.DEPLOYER_PRIVATE_KEY as string - account = new Account(provider, process.env.DEPLOYER_ACCOUNT_ADDRESS as string, privateKey) - - 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( - await input('Enter a number for new observation_timestamp: '), - ) - transmission.transmission_timestamp = Number( - await input('Enter a number for new transmission_timestamp: '), - ) - rl.close() - - await callFunction(transmission) -} - -async function callFunction(transmission: Transmission) { - 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) { - return new Promise((callbackFn, errorFn) => { - rl.question(prompt, (uinput: string) => { - switch (isNaN(Number(uinput))) { - case true: - console.log('input is not a number we will use the default value of 1') - uinput = '1' - break - default: - break - } - callbackFn(uinput) - }) - }) -} - -updateLatestRound() diff --git a/examples/contracts/aggregator-consumer/scripts/utils.ts b/examples/contracts/aggregator-consumer/scripts/utils.ts deleted file mode 100644 index ef4e8cf54..000000000 --- a/examples/contracts/aggregator-consumer/scripts/utils.ts +++ /dev/null @@ -1,85 +0,0 @@ -import fs from 'fs' -import dotenv from 'dotenv' -import { CompiledContract, json, ec, Account, Provider, constants, CairoAssembly } from 'starknet' - -const DEVNET_NAME = 'devnet' - -export const loadContract_Account = (name: string): CompiledContract => { - return json.parse( - fs - .readFileSync( - `${__dirname}/../../../../node_modules/@shardlabs/starknet-hardhat-plugin/dist/contract-artifacts/OpenZeppelinAccount/0.5.1/${name}.cairo/${name}.json`, - ) - .toString('ascii'), - ) -} - -export const loadContract = (name: string): CompiledContract => { - return json.parse( - fs - .readFileSync(`${__dirname}/../target/release/chainlink_examples_${name}.sierra.json`) - .toString('ascii'), - ) -} - -export const loadCasmContract = (name: string): CairoAssembly => { - return json.parse( - fs - .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/src/chainlink/solidity/${path}/${name}.sol/${name}.json`, - ) - .toString('ascii'), - ) -} -export const loadContract_Solidity_V8 = (name: string): any => { - return json.parse( - fs - .readFileSync( - `${__dirname}/../../../../contracts/artifacts/@chainlink/contracts/src/v0.8/interfaces/${name}.sol/${name}.json`, - ) - .toString('ascii'), - ) -} - -export function createDeployerAccount(provider: Provider): Account { - dotenv.config({ path: __dirname + '/../.env' }) - - const privateKey: string = process.env.DEPLOYER_PRIVATE_KEY as string - const accountAddress: string = process.env.DEPLOYER_ACCOUNT_ADDRESS as string - if (!privateKey || !accountAddress) { - throw new Error('Deployer account address or private key is undefined!') - } - - return new Account(provider, accountAddress, privateKey) -} - -export const makeProvider = () => { - const network = process.env.NETWORK || DEVNET_NAME - if (network === DEVNET_NAME) { - return new Provider({ - sequencer: { - baseUrl: 'http://127.0.0.1:5050/', - feederGatewayUrl: 'feeder_gateway', - gatewayUrl: 'gateway', - }, - }) - } else { - return new Provider({ - sequencer: { - network: constants.NetworkName.SN_GOERLI, - }, - }) - } -} diff --git a/examples/contracts/aggregator-consumer/src/lib.cairo b/examples/contracts/aggregator-consumer/src/lib.cairo deleted file mode 100644 index f14ff3b83..000000000 --- a/examples/contracts/aggregator-consumer/src/lib.cairo +++ /dev/null @@ -1 +0,0 @@ -mod ocr2; diff --git a/examples/contracts/aggregator-consumer/src/ocr2.cairo b/examples/contracts/aggregator-consumer/src/ocr2.cairo deleted file mode 100644 index 6c9dcdfe1..000000000 --- a/examples/contracts/aggregator-consumer/src/ocr2.cairo +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 3b3916316..000000000 --- a/examples/contracts/aggregator-consumer/src/ocr2/consumer.cairo +++ /dev/null @@ -1,30 +0,0 @@ -#[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 deleted file mode 100644 index 06ddd331a..000000000 --- a/examples/contracts/aggregator-consumer/src/ocr2/price_consumer_with_sequencer.cairo +++ /dev/null @@ -1,63 +0,0 @@ -#[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'); - }, - 1 | _ => { - assert(!report_stale, 'L2 seq down & report stale'); - assert(false, 'L2 seq down & report ok'); - } - } - } -} - diff --git a/examples/contracts/aggregator-consumer/tsconfig.json b/examples/contracts/aggregator-consumer/tsconfig.json deleted file mode 100644 index 445119cd5..000000000 --- a/examples/contracts/aggregator-consumer/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - }, - "include": [ - "scripts/**/*" - ], - "exclude": [ - "dist", - "**/*.spec.ts", - "**/*.test.ts" - ], - "files": [ - "./hardhat.config.ts" - ] -} From efe80322a945f0fb82b1edeac538bf1bffaa9245 Mon Sep 17 00:00:00 2001 From: Chris De Leon Date: Fri, 1 Mar 2024 14:08:45 -0800 Subject: [PATCH 4/8] adding colon --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1497e7874..906d53fa6 100644 --- a/Makefile +++ b/Makefile @@ -224,7 +224,7 @@ test-integration-contracts: build-ts env-devnet-hardhat cd packages-ts/starknet/ && \ yarn test -.PHONY test-examples +.PHONY: test-examples test-examples: cd ./examples/contracts/aggregator_consumer && \ snforge test From 368e31bd0a868508bf413505a5860fd11df52168 Mon Sep 17 00:00:00 2001 From: Chris De Leon Date: Fri, 1 Mar 2024 14:25:28 -0800 Subject: [PATCH 5/8] adds github action for installing starknet foundry, adds github action for running example test cases with starknet foundry, removes test-integration-contracts --- .../install-starknet-foundry/action.yml | 19 ++++ ...integration_contracts.yml => examples.yml} | 9 +- Makefile | 11 -- packages-ts/starknet/.gitignore | 3 - packages-ts/starknet/hardhat.config.ts | 26 ----- packages-ts/starknet/package.json | 29 ----- packages-ts/starknet/src/account.ts | 100 ------------------ packages-ts/starknet/src/index.ts | 2 - packages-ts/starknet/src/utils.ts | 81 -------------- packages-ts/starknet/test/fundAccount.test.ts | 73 ------------- packages-ts/starknet/tsconfig.json | 15 --- 11 files changed, 25 insertions(+), 343 deletions(-) create mode 100644 .github/actions/install-starknet-foundry/action.yml rename .github/workflows/{integration_contracts.yml => examples.yml} (73%) delete mode 100644 packages-ts/starknet/.gitignore delete mode 100644 packages-ts/starknet/hardhat.config.ts delete mode 100644 packages-ts/starknet/package.json delete mode 100644 packages-ts/starknet/src/account.ts delete mode 100644 packages-ts/starknet/src/index.ts delete mode 100644 packages-ts/starknet/src/utils.ts delete mode 100644 packages-ts/starknet/test/fundAccount.test.ts delete mode 100644 packages-ts/starknet/tsconfig.json diff --git a/.github/actions/install-starknet-foundry/action.yml b/.github/actions/install-starknet-foundry/action.yml new file mode 100644 index 000000000..f31b71c9a --- /dev/null +++ b/.github/actions/install-starknet-foundry/action.yml @@ -0,0 +1,19 @@ +name: Install Starknet Foundry (snforge and sncast) +description: A composite action that installs the snforge and sncast binaries + +inputs: + starknet_foundry_version: + description: Starknet Foundry release version + default: "v0.18.0" + required: false + +runs: + using: composite + steps: + - name: Setup Starknet Foundry for Linux + id: install-starknet-foundry + shell: bash + run: | + curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh + snfoundryup -v ${{ inputs.starknet_foundry_version }} + diff --git a/.github/workflows/integration_contracts.yml b/.github/workflows/examples.yml similarity index 73% rename from .github/workflows/integration_contracts.yml rename to .github/workflows/examples.yml index fa421a134..28572831c 100644 --- a/.github/workflows/integration_contracts.yml +++ b/.github/workflows/examples.yml @@ -1,4 +1,4 @@ -name: Integration Contracts (Vendor, Examples) +name: Example Contracts on: push: @@ -8,7 +8,7 @@ on: pull_request: jobs: - integration_contracts_run_tests: + run_examples_tests: name: Run Tests runs-on: ubuntu-latest steps: @@ -23,5 +23,8 @@ jobs: - name: Install Cairo uses: ./.github/actions/install-cairo + - name: Install Starknet Foundry + uses: ./.github/actions/install-starknet-foundry + - name: Test - run: nix develop -c make test-integration-contracts + run: nix develop -c make test-examples diff --git a/Makefile b/Makefile index 906d53fa6..81eafba7d 100644 --- a/Makefile +++ b/Makefile @@ -213,17 +213,6 @@ test-integration-soak-ci: cd integration-tests/ && \ go test --timeout=1h -v -count=1 -json ./soak -.PHONY: test-integration-contracts -# TODO: better network lifecycle setup - requires external network (L1 + L2) -# TODO: readd test examples -# cd examples/contracts/aggregator-consumer/ && \ -# yarn test -test-integration-contracts: build-ts env-devnet-hardhat - echo "Tests currently broken because of starknet-hardhat-plugin" - exit 1 - cd packages-ts/starknet/ && \ - yarn test - .PHONY: test-examples test-examples: cd ./examples/contracts/aggregator_consumer && \ diff --git a/packages-ts/starknet/.gitignore b/packages-ts/starknet/.gitignore deleted file mode 100644 index 6cb6d202b..000000000 --- a/packages-ts/starknet/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -cache/ -node_modules/ -starknet-artifacts/ diff --git a/packages-ts/starknet/hardhat.config.ts b/packages-ts/starknet/hardhat.config.ts deleted file mode 100644 index 7e851a738..000000000 --- a/packages-ts/starknet/hardhat.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { HardhatUserConfig } from 'hardhat/types' -import '@shardlabs/starknet-hardhat-plugin' - -/** - * @type import('hardhat/config').HardhatUserConfig - */ -const config: HardhatUserConfig = { - starknet: { - venv: 'active', - network: 'devnet', - wallets: { - OpenZeppelin: { - accountName: 'OpenZeppelin', - modulePath: 'starkware.starknet.wallets.open_zeppelin.OpenZeppelinAccount', - accountPath: '~/.starknet_accounts', - }, - }, - }, - networks: { - devnet: { - url: 'http://127.0.0.1:5050/', - }, - }, -} - -export default config diff --git a/packages-ts/starknet/package.json b/packages-ts/starknet/package.json deleted file mode 100644 index f298931e7..000000000 --- a/packages-ts/starknet/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@chainlink/starknet", - "version": "1.0.0", - "description": "Starknet test utilities", - "keywords": [ - "typescript", - "cli" - ], - "main": "./dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist/**/*", - "!dist/**/*.test.js" - ], - "scripts": { - "lint": "tsc", - "test": "npx hardhat --network localhost test", - "gauntlet": "ts-node ./src/index.ts", - "format": "yarn prettier --write ./src", - "format:check": "yarn prettier --check ./src", - "clean": "rm -rf ./dist/ ./bin/", - "build": "yarn clean && tsc -b", - "bundle": "yarn build && pkg ." - }, - "devDependencies": { - "@shardlabs/starknet-hardhat-plugin": "^0.8.0-alpha.2", - "hardhat": "^2.16.1" - } -} diff --git a/packages-ts/starknet/src/account.ts b/packages-ts/starknet/src/account.ts deleted file mode 100644 index 5da60b364..000000000 --- a/packages-ts/starknet/src/account.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { Account, RpcProvider, ec, uint256, constants } from 'starknet' - -export const ERC20_ADDRESS = '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7' - -export const DEVNET_URL = 'http://127.0.0.1:5050' -const DEVNET_NAME = 'devnet' -// This function loads options from the environment. -// It returns options for Devnet as default when nothing is configured in the environment. -export const makeFunderOptsFromEnv = () => { - const network = process.env.NETWORK || DEVNET_NAME - const gateway = process.env.NODE_URL || DEVNET_URL - const accountAddr = process.env.ACCOUNT?.toLowerCase() - const keyPair = ec.starkCurve.utils.randomPrivateKey() - - return { network, gateway, accountAddr, keyPair } -} - -interface FundAccounts { - account: string - amount: number -} - -interface FunderOptions { - network?: string - gateway?: string - accountAddr?: string - keyPair?: Uint8Array -} - -// Define the Strategy to use depending on the network. -export class Funder { - private opts: FunderOptions - private strategy: IFundingStrategy - - constructor(opts: FunderOptions) { - this.opts = opts - if (this.opts.network === DEVNET_NAME) { - this.strategy = new DevnetFundingStrategy() - return - } - this.strategy = new AllowanceFundingStrategy() - } - - // This function adds some funds to pre-deployed account that we are using in our test. - public async fund(accounts: FundAccounts[]) { - await this.strategy.fund(accounts, this.opts) - } -} - -interface IFundingStrategy { - fund(accounts: FundAccounts[], opts: FunderOptions): Promise -} - -// Fund the Account on Devnet -class DevnetFundingStrategy implements IFundingStrategy { - public async fund(accounts: FundAccounts[], opts: FunderOptions) { - accounts.forEach(async (account) => { - const body = { - address: account.account, - amount: account.amount, - lite: true, - } - await fetch(`${opts.gateway}/mint`, { - method: 'post', - body: JSON.stringify(body), - headers: { 'Content-Type': 'application/json' }, - }) - }) - } -} - -// Fund the Account on Testnet -class AllowanceFundingStrategy implements IFundingStrategy { - public async fund(accounts: FundAccounts[], opts: FunderOptions) { - const provider = new RpcProvider({ - nodeUrl: constants.NetworkName.SN_GOERLI, - }) - - const operator = new Account(provider, opts.accountAddr, opts.keyPair) - - for (const account of accounts) { - const data = [ - account.account, - uint256.bnToUint256(account.amount).low.toString(), - uint256.bnToUint256(account.amount).high.toString(), - ] - const nonce = await operator.getNonce() - const hash = await operator.execute( - { - contractAddress: ERC20_ADDRESS, - entrypoint: 'transfer', - calldata: data, - }, - undefined, - { nonce }, - ) - await provider.waitForTransaction(hash.transaction_hash) - } - } -} diff --git a/packages-ts/starknet/src/index.ts b/packages-ts/starknet/src/index.ts deleted file mode 100644 index c90169921..000000000 --- a/packages-ts/starknet/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './utils' -export * as account from './account' diff --git a/packages-ts/starknet/src/utils.ts b/packages-ts/starknet/src/utils.ts deleted file mode 100644 index 6bf736f85..000000000 --- a/packages-ts/starknet/src/utils.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { expect } from 'chai' -import { artifacts, network } from 'hardhat' - -// This function adds the build info to the test network so that the network knows -// how to handle custom errors. It is automatically done when testing -// against the default hardhat network. -export const addCompilationToNetwork = async (fullyQualifiedName: string) => { - if (network.name !== 'hardhat') { - // This is so that the network can know about custom errors. - // Running against the provided hardhat node does this automatically. - - const buildInfo = await artifacts.getBuildInfo(fullyQualifiedName) - if (!buildInfo) { - throw Error('Cannot find build info') - } - const { solcVersion, input, output } = buildInfo - console.log('Sending compilation result for StarknetValidator test') - await network.provider.request({ - method: 'hardhat_addCompilationResult', - params: [solcVersion, input, output], - }) - console.log('Successfully sent compilation result for StarknetValidator test') - } -} - -export const expectInvokeError = async (invoke: Promise, expected?: string) => { - try { - await invoke - } catch (err: any) { - expectInvokeErrorMsg(err?.message, expected) - return // force - } - expect.fail("Unexpected! Invoke didn't error!?") -} - -export const expectInvokeErrorMsg = (actual: string, expected?: string) => { - // Match transaction error - expect(actual).to.deep.contain('TRANSACTION_FAILED') - // Match specific error - if (expected) expectSpecificMsg(actual, expected) -} - -export const expectCallError = async (call: Promise, expected?: string) => { - try { - await call - } catch (err: any) { - expectCallErrorMsg(err?.message, expected) - return // force - } - expect.fail("Unexpected! Call didn't error!?") -} - -export const expectCallErrorMsg = (actual: string, expected?: string) => { - // Match call error - expect(actual).to.deep.contain('Could not perform call') - // Match specific error - if (expected) expectSpecificMsg(actual, expected) -} - -export const expectSpecificMsg = (actual: string, expected: string) => { - // The error message is displayed as a felt hex string, so we need to convert the text. - // ref: https://github.com/starkware-libs/cairo-lang/blob/c954f154bbab04c3fb27f7598b015a9475fc628e/src/starkware/starknet/business_logic/execution/execute_entry_point.py#L223 - const expectedHex = '0x' + Buffer.from(expected, 'utf8').toString('hex') - const errorMessage = `Execution was reverted; failure reason: [${expectedHex}]` - if (!actual.includes(errorMessage)) { - expect.fail(`\nActual: ${actual}\n\nExpected:\n\tFelt hex: ${expectedHex}\n\tText: ${expected}`) - } -} - -// Starknet v0.11.0 and higher only allow declaring a class once: -// https://github.com/starkware-libs/starknet-specs/pull/85 -export const expectSuccessOrDeclared = async (declareContractPromise: Promise) => { - try { - await declareContractPromise - } catch (err: any) { - if (/Class with hash 0x[0-9a-f]+ is already declared\./.test(err?.message)) { - return // force - } - expect.fail(err) - } -} diff --git a/packages-ts/starknet/test/fundAccount.test.ts b/packages-ts/starknet/test/fundAccount.test.ts deleted file mode 100644 index b9aff5eb0..000000000 --- a/packages-ts/starknet/test/fundAccount.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { assert } from 'chai' -import { account } from '@chainlink/starknet' -import { Account, ec, RpcProvider, stark } from 'starknet' -import { DEVNET_URL, ERC20_ADDRESS } from '../src/account' - -describe('fundAccount', function () { - this.timeout(900_000) - let alice: Account - let bob: Account - let provider: RpcProvider - - const opts = account.makeFunderOptsFromEnv() - const funder = new account.Funder(opts) - - before(async function () { - const gateway_url = process.env.NODE_URL || DEVNET_URL - provider = new RpcProvider({ nodeUrl: gateway_url }) - - const aliceStarkKeyPair = ec.starkCurve.utils.randomPrivateKey() - const bobStarkKeyPair = ec.starkCurve.utils.randomPrivateKey() - - const default_alice_address = stark.randomAddress() - const default_bob_address = stark.randomAddress() - - alice = new Account(provider, default_alice_address, aliceStarkKeyPair) - bob = new Account(provider, default_bob_address, bobStarkKeyPair) - - await funder.fund([ - { account: alice.address, amount: 5000 }, - { account: bob.address, amount: 8000 }, - ]) - }) - - it('should have fund alice', async () => { - const balance = await alice.callContract({ - contractAddress: ERC20_ADDRESS, - entrypoint: 'balanceOf', - calldata: [BigInt(alice.address).toString(10)], - }) - assert.deepEqual(balance, ['0x1388', '0x0']) - }) - - it('should have fund bob', async () => { - const balance = await bob.callContract({ - contractAddress: ERC20_ADDRESS, - entrypoint: 'balanceOf', - calldata: [BigInt(bob.address).toString(10)], - }) - assert.deepEqual(balance, ['0x1f40', '0x0']) - }) - - it("should increament alice's fees", async () => { - await funder.fund([{ account: alice.address, amount: 100 }]) - - const balance = await alice.callContract({ - contractAddress: ERC20_ADDRESS, - entrypoint: 'balanceOf', - calldata: [BigInt(alice.address).toString(10)], - }) - assert.deepEqual(balance, ['0x13ec', '0x0']) - }) - - it("should increament bob's fees", async () => { - await funder.fund([{ account: bob.address, amount: 1000 }]) - - const balance = await bob.callContract({ - contractAddress: ERC20_ADDRESS, - entrypoint: 'balanceOf', - calldata: [BigInt(bob.address).toString(10)], - }) - assert.deepEqual(balance, ['0x2328', '0x0']) - }) -}) diff --git a/packages-ts/starknet/tsconfig.json b/packages-ts/starknet/tsconfig.json deleted file mode 100644 index 2b6853d59..000000000 --- a/packages-ts/starknet/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src" - }, - "include": [ - "src/**/*" - ], - "exclude": [ - "dist", - "**/*.spec.ts", - "**/*.test.ts" - ], -} From 5c8de0f98049c8f675062668e94bc3cbec7b996e Mon Sep 17 00:00:00 2001 From: Chris De Leon Date: Fri, 1 Mar 2024 14:27:14 -0800 Subject: [PATCH 6/8] removes unnecessary v character --- .github/actions/install-starknet-foundry/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/install-starknet-foundry/action.yml b/.github/actions/install-starknet-foundry/action.yml index f31b71c9a..060ea6298 100644 --- a/.github/actions/install-starknet-foundry/action.yml +++ b/.github/actions/install-starknet-foundry/action.yml @@ -4,7 +4,7 @@ description: A composite action that installs the snforge and sncast binaries inputs: starknet_foundry_version: description: Starknet Foundry release version - default: "v0.18.0" + default: "0.18.0" required: false runs: From 1a4a1062d910d3cf0bd69aeee9bffc4232f68122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Mon, 4 Mar 2024 13:32:20 +0900 Subject: [PATCH 7/8] Fix yarn error, move utils over to contracts/ --- contracts/package.json | 1 - contracts/test/account.ts | 100 ++++++++++++++++++ .../test/emergency/StarknetValidator.test.ts | 5 +- contracts/test/ocr2/aggregator.test.ts | 3 +- contracts/test/utils.ts | 81 ++++++++++++++ 5 files changed, 186 insertions(+), 4 deletions(-) create mode 100644 contracts/test/account.ts create mode 100644 contracts/test/utils.ts diff --git a/contracts/package.json b/contracts/package.json index 131fc7d74..8bad5cc62 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -24,7 +24,6 @@ }, "dependencies": { "@chainlink/contracts": "^0.4.2", - "@chainlink/starknet": "^1.0.0", "@openzeppelin/contracts": "^4.7.3", "axios": "^0.24.0" } diff --git a/contracts/test/account.ts b/contracts/test/account.ts new file mode 100644 index 000000000..5da60b364 --- /dev/null +++ b/contracts/test/account.ts @@ -0,0 +1,100 @@ +import { Account, RpcProvider, ec, uint256, constants } from 'starknet' + +export const ERC20_ADDRESS = '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7' + +export const DEVNET_URL = 'http://127.0.0.1:5050' +const DEVNET_NAME = 'devnet' +// This function loads options from the environment. +// It returns options for Devnet as default when nothing is configured in the environment. +export const makeFunderOptsFromEnv = () => { + const network = process.env.NETWORK || DEVNET_NAME + const gateway = process.env.NODE_URL || DEVNET_URL + const accountAddr = process.env.ACCOUNT?.toLowerCase() + const keyPair = ec.starkCurve.utils.randomPrivateKey() + + return { network, gateway, accountAddr, keyPair } +} + +interface FundAccounts { + account: string + amount: number +} + +interface FunderOptions { + network?: string + gateway?: string + accountAddr?: string + keyPair?: Uint8Array +} + +// Define the Strategy to use depending on the network. +export class Funder { + private opts: FunderOptions + private strategy: IFundingStrategy + + constructor(opts: FunderOptions) { + this.opts = opts + if (this.opts.network === DEVNET_NAME) { + this.strategy = new DevnetFundingStrategy() + return + } + this.strategy = new AllowanceFundingStrategy() + } + + // This function adds some funds to pre-deployed account that we are using in our test. + public async fund(accounts: FundAccounts[]) { + await this.strategy.fund(accounts, this.opts) + } +} + +interface IFundingStrategy { + fund(accounts: FundAccounts[], opts: FunderOptions): Promise +} + +// Fund the Account on Devnet +class DevnetFundingStrategy implements IFundingStrategy { + public async fund(accounts: FundAccounts[], opts: FunderOptions) { + accounts.forEach(async (account) => { + const body = { + address: account.account, + amount: account.amount, + lite: true, + } + await fetch(`${opts.gateway}/mint`, { + method: 'post', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' }, + }) + }) + } +} + +// Fund the Account on Testnet +class AllowanceFundingStrategy implements IFundingStrategy { + public async fund(accounts: FundAccounts[], opts: FunderOptions) { + const provider = new RpcProvider({ + nodeUrl: constants.NetworkName.SN_GOERLI, + }) + + const operator = new Account(provider, opts.accountAddr, opts.keyPair) + + for (const account of accounts) { + const data = [ + account.account, + uint256.bnToUint256(account.amount).low.toString(), + uint256.bnToUint256(account.amount).high.toString(), + ] + const nonce = await operator.getNonce() + const hash = await operator.execute( + { + contractAddress: ERC20_ADDRESS, + entrypoint: 'transfer', + calldata: data, + }, + undefined, + { nonce }, + ) + await provider.waitForTransaction(hash.transaction_hash) + } + } +} diff --git a/contracts/test/emergency/StarknetValidator.test.ts b/contracts/test/emergency/StarknetValidator.test.ts index 54385109f..5eedc51fa 100644 --- a/contracts/test/emergency/StarknetValidator.test.ts +++ b/contracts/test/emergency/StarknetValidator.test.ts @@ -1,6 +1,6 @@ import { ethers, starknet, network } from 'hardhat' import { BigNumber, Contract, ContractFactory } from 'ethers' -import { hash, number } from 'starknet' +import { hash } from 'starknet' import { Account, StarknetContractFactory, @@ -13,7 +13,8 @@ import { abi as aggregatorAbi } from '../../artifacts/@chainlink/contracts/src/v import { abi as accessControllerAbi } from '../../artifacts/@chainlink/contracts/src/v0.8/interfaces/AccessControllerInterface.sol/AccessControllerInterface.json' import { abi as starknetMessagingAbi } from '../../artifacts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessaging.sol/IStarknetMessaging.json' import { deployMockContract, MockContract } from '@ethereum-waffle/mock-contract' -import { account, addCompilationToNetwork, expectSuccessOrDeclared } from '@chainlink/starknet' +import * as account from '../account' +import { addCompilationToNetwork, expectSuccessOrDeclared } from '../utils' describe('StarknetValidator', () => { /** Fake L2 target */ diff --git a/contracts/test/ocr2/aggregator.test.ts b/contracts/test/ocr2/aggregator.test.ts index 3d53e7ccd..2b07e55d0 100644 --- a/contracts/test/ocr2/aggregator.test.ts +++ b/contracts/test/ocr2/aggregator.test.ts @@ -3,7 +3,8 @@ import { starknet } from 'hardhat' import { ec, hash, num } from 'starknet' import { Account, StarknetContract, StarknetContractFactory } from 'hardhat/types/runtime' import { TIMEOUT } from '../constants' -import { account, expectInvokeError, expectSuccessOrDeclared } from '@chainlink/starknet' +import { expectInvokeError, expectSuccessOrDeclared } from '../utils' +import * as account from '../account' import { bytesToFelts } from '@chainlink/starknet-gauntlet' interface Oracle { diff --git a/contracts/test/utils.ts b/contracts/test/utils.ts new file mode 100644 index 000000000..6bf736f85 --- /dev/null +++ b/contracts/test/utils.ts @@ -0,0 +1,81 @@ +import { expect } from 'chai' +import { artifacts, network } from 'hardhat' + +// This function adds the build info to the test network so that the network knows +// how to handle custom errors. It is automatically done when testing +// against the default hardhat network. +export const addCompilationToNetwork = async (fullyQualifiedName: string) => { + if (network.name !== 'hardhat') { + // This is so that the network can know about custom errors. + // Running against the provided hardhat node does this automatically. + + const buildInfo = await artifacts.getBuildInfo(fullyQualifiedName) + if (!buildInfo) { + throw Error('Cannot find build info') + } + const { solcVersion, input, output } = buildInfo + console.log('Sending compilation result for StarknetValidator test') + await network.provider.request({ + method: 'hardhat_addCompilationResult', + params: [solcVersion, input, output], + }) + console.log('Successfully sent compilation result for StarknetValidator test') + } +} + +export const expectInvokeError = async (invoke: Promise, expected?: string) => { + try { + await invoke + } catch (err: any) { + expectInvokeErrorMsg(err?.message, expected) + return // force + } + expect.fail("Unexpected! Invoke didn't error!?") +} + +export const expectInvokeErrorMsg = (actual: string, expected?: string) => { + // Match transaction error + expect(actual).to.deep.contain('TRANSACTION_FAILED') + // Match specific error + if (expected) expectSpecificMsg(actual, expected) +} + +export const expectCallError = async (call: Promise, expected?: string) => { + try { + await call + } catch (err: any) { + expectCallErrorMsg(err?.message, expected) + return // force + } + expect.fail("Unexpected! Call didn't error!?") +} + +export const expectCallErrorMsg = (actual: string, expected?: string) => { + // Match call error + expect(actual).to.deep.contain('Could not perform call') + // Match specific error + if (expected) expectSpecificMsg(actual, expected) +} + +export const expectSpecificMsg = (actual: string, expected: string) => { + // The error message is displayed as a felt hex string, so we need to convert the text. + // ref: https://github.com/starkware-libs/cairo-lang/blob/c954f154bbab04c3fb27f7598b015a9475fc628e/src/starkware/starknet/business_logic/execution/execute_entry_point.py#L223 + const expectedHex = '0x' + Buffer.from(expected, 'utf8').toString('hex') + const errorMessage = `Execution was reverted; failure reason: [${expectedHex}]` + if (!actual.includes(errorMessage)) { + expect.fail(`\nActual: ${actual}\n\nExpected:\n\tFelt hex: ${expectedHex}\n\tText: ${expected}`) + } +} + +// Starknet v0.11.0 and higher only allow declaring a class once: +// https://github.com/starkware-libs/starknet-specs/pull/85 +export const expectSuccessOrDeclared = async (declareContractPromise: Promise) => { + try { + await declareContractPromise + } catch (err: any) { + if (/Class with hash 0x[0-9a-f]+ is already declared\./.test(err?.message)) { + return // force + } + expect.fail(err) + } +} From 67c1962edb50c05a82e7c61674b440d450e6a56f Mon Sep 17 00:00:00 2001 From: Chris De Leon Date: Tue, 5 Mar 2024 14:33:58 -0800 Subject: [PATCH 8/8] removes packages-ts/starknet reference from root tsconfig --- tsconfig.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index cb4dd770b..0c5132c51 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,9 +3,6 @@ "files": [], "include": [], "references": [ - { - "path": "./packages-ts/starknet" - }, { "path": "./packages-ts/starknet-gauntlet" },