diff --git a/README.md b/README.md
index 05f1caa..a9ffab4 100644
--- a/README.md
+++ b/README.md
@@ -1,77 +1,14 @@
-# Introduction to Starknet Contracts
+# Introduction to Testing Starknet Contract
+
+- Test suite, unit tests are provided under the each contract's implementations directly whereas full flow integration tests lies within this test suite. We use starknet-foundry testing framework in this class and test thoroughly for any edge cases in each of the contract.
+
+## Running Tests
+
1. Install starknet-foundry by running this command:
`curl -L https://raw.githubusercontent.com/foundry-rs/starknet-foundry/master/scripts/install.sh | sh`
restart your terminal
run `snfoundryup`
-2. Create an account on any of these RPC provides:
- - [Voyager](https://voyager.online/)
- - [BlastAPI](https://starknet-testnet.blastapi.io)
- - [Infura](https://www.infura.io/)
-
-Generate an RPC apikey to interact with the network
-
-3. Create a contract account by running this command on your terminal:
-`sncast -u account create -n --add-profile`
-
-4. Deploy the contract account:
-`sncast --url account deploy --name --max-fee 4323000047553`
-`NB`
-Running the above command should trigger an error:
-`error: Account balance is smaller than the transaction's max_fee.`
-That why your account must be funded; to fund your account, visit - https://faucet.goerli.starknet.io/
-
-5. Compile your contract by running: `scarb build`
-
-6. Declare your contract:
-`sncast --account test_deploy -u declare --contract-name `
-
-7. Deploy your contract:
-`sncast --account --url deploy --class-hash `
-
-`NB`
-While deploying, make sure you check the constructor argument of the contract you are trying to deploy. All arguments must be passed in appropriately; for such case, use this command:
-```sncast --account --url deploy --class-hash --constructor-calldata ```
-
-
-
-
----
-# Introduction to Dispatchers
-
-
-### Deployed Contracts
-
-#### Ownable Contract
-- [x] class hash - 0x421a3ad93deda96f863e26ab51a79f4cea384d71714a5b37ace35010872a088
-- [x] address - 0x4a742edef4df3d3fb09809535a322971ababb1f337ffcf5c297a941f54a76e1
-
-#### Counter Contract
-- [x] class hash - 0x71d83bb407cdd1a963bdcba92c82b3ff18e8e56fd3cfa9410b0dce069477511
-- [x] address - 0x14b32ec4783dabf825bb2ff4c82b20a81273455cf90ff263c85216b54b1f36d
-
-#### Caller Contract
-- [x] class hash - 0x6c9d24030d72669af3e857dc1f04981c5cf316e0c2efee443509bbf95530587
-- [x] address - 0x2ee3772f1ec48d45bd6280daf74bc35eacd8f5dd741daceaea04130bade808
-
-
-
----
-### Interacting with Deployed Contracts
-- Invoke: to execute the logic of a state-changing (writes) function within your deployed contracts from the terminal, run
-```
-sncast --url --account invoke --contract-address --function "" --calldata
-```
-
-
-- Call: to execute the logic of a non-state-changing (reads) function within your deployed contracts from the terminal, run:
-```
-sncast --url --account call --contract-address --function "()` is similar to address(0) in Solidity
- from: contract_address_const::<0>(), to: recipient, value: _initial_supply
+ Transfer { //Here, `contract_address_const::<0>()` is similar to address(0) in Solidity
+ from: contract_address_const::<0>(), to: recipient, value: 1000000
}
);
}
@@ -130,8 +131,12 @@ mod BWCERC20Token {
amount: u256
) {
let caller = get_caller_address();
- let my_allowance = self.allowances.read((sender, recipient));
- assert(my_allowance <= amount, 'Amount Not Allowed');
+ let my_allowance = self.allowances.read((sender, caller));
+
+ assert(my_allowance > 0, 'You have no token approved');
+ assert(amount <= my_allowance, 'Amount Not Allowed');
+ // assert(my_allowance <= amount, 'Amount Not Allowed');
+
self
.spend_allowance(
sender, caller, amount
@@ -205,7 +210,7 @@ mod BWCERC20Token {
// define a variable ONES_MASK of type u128
let ONES_MASK = 0xfffffffffffffffffffffffffffffff_u128;
- // to determine whether the authorization is unlimited,
+ // to determine whether the authorization is unlimited,
let is_unlimited_allowance = current_allowance.low == ONES_MASK
&& current_allowance
@@ -218,3 +223,270 @@ mod BWCERC20Token {
}
}
}
+
+
+// Annotation
+#[cfg(test)]
+mod test {
+ use core::serde::Serde;
+ use super::{IERC20, BWCERC20Token, IERC20Dispatcher, IERC20DispatcherTrait};
+ use starknet::ContractAddress;
+ use starknet::contract_address::contract_address_const;
+ use array::ArrayTrait;
+ use snforge_std::{declare, ContractClassTrait, fs::{FileTrait, read_txt}};
+ use snforge_std::{start_prank, stop_prank, CheatTarget};
+ use snforge_std::PrintTrait;
+ use traits::{Into, TryInto};
+
+ // We first have to deploy first via a helper function
+ fn deploy_contract() -> ContractAddress {
+ // Before deploying a starknet contract, we need a contract_class.
+ // Get it using the declare function from starknetFoundry
+ let erc20contract_class = declare('BWCERC20Token');
+
+ // Supply values the constructor arguements when deploying
+ // REMEMBER: It has to be in an array
+ let file = FileTrait::new('data/constructor_args.txt');
+ let constructor_args = read_txt(@file);
+ let contract_address = erc20contract_class.deploy(@constructor_args).unwrap();
+ contract_address
+ }
+
+ // Generate an address
+ mod Account {
+ use starknet::ContractAddress;
+ use traits::TryInto;
+
+ fn user1() -> ContractAddress {
+ 'joy'.try_into().unwrap()
+ }
+ fn user2() -> ContractAddress {
+ 'caleb'.try_into().unwrap()
+ }
+
+ fn admin() -> ContractAddress {
+ 'admin'.try_into().unwrap()
+ }
+ }
+
+
+ // --------------------- Now we start testing --------------------------------
+
+ // Test 1 - Test wether we can get the name
+ #[test]
+ fn test_constructor() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+ // let name = dispatcher.get_name();
+ let name = dispatcher.get_name();
+
+ assert(name == 'BlockheaderToken', 'name is not correct');
+ }
+
+ #[test]
+ fn test_decimal_is_correct() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+ let decimal = dispatcher.get_decimals();
+
+ assert(decimal == 18, 'Decimal is not correct');
+ }
+
+ #[test]
+ fn test_total_supply() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+ let total_supply = dispatcher.get_total_supply();
+
+ assert(total_supply == 1000000, 'Total supply is wrong');
+ }
+
+ #[test]
+ fn test_address_balance() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+ let balance = dispatcher.get_total_supply();
+ let admin_balance = dispatcher.balance_of(Account::admin());
+ assert(admin_balance == balance, Errors::INVALID_BALANCE);
+
+ start_prank(CheatTarget::One(contract_address), Account::admin());
+
+ dispatcher.transfer(Account::user1(), 10);
+ let new_admin_balance = dispatcher.balance_of(Account::admin());
+ assert(new_admin_balance == balance - 10, Errors::INVALID_BALANCE);
+ stop_prank(CheatTarget::One(contract_address));
+
+ let user1_balance = dispatcher.balance_of(Account::user1());
+ assert(user1_balance == 10, Errors::INVALID_BALANCE);
+ }
+
+ #[test]
+ fn test_allowance() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+
+ start_prank(CheatTarget::One(contract_address), Account::admin());
+ dispatcher.approve(contract_address, 20);
+
+ let currentAllowance = dispatcher.allowance(Account::admin(), contract_address);
+
+ assert(currentAllowance == 20, Errors::INVALID_ALLOWANCE_GIVEN);
+ stop_prank(CheatTarget::One(contract_address));
+ }
+
+ #[test]
+ fn test_transfer() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+
+ // Get original balances
+ let original_sender_balance = dispatcher.balance_of(Account::admin());
+ let original_recipient_balance = dispatcher.balance_of(Account::user1());
+
+ start_prank(CheatTarget::One(contract_address), Account::admin());
+
+ dispatcher.transfer(Account::user1(), 50);
+
+ // Confirm that the funds have been sent!
+ assert(
+ dispatcher.balance_of(Account::admin()) == original_sender_balance - 50,
+ Errors::FUNDS_NOT_SENT
+ );
+
+ // Confirm that the funds have been recieved!
+ assert(
+ dispatcher.balance_of(Account::user1()) == original_recipient_balance + 50,
+ Errors::FUNDS_NOT_RECIEVED
+ );
+
+ stop_prank(CheatTarget::One(contract_address));
+ }
+
+
+ #[test]
+ fn test_transfer_from() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+
+ start_prank(CheatTarget::One(contract_address), Account::admin());
+ dispatcher.approve(Account::user1(), 20);
+ stop_prank(CheatTarget::One(contract_address));
+
+ assert(
+ dispatcher.allowance(Account::admin(), Account::user1()) == 20,
+ Errors::INVALID_ALLOWANCE_GIVEN
+ );
+
+ start_prank(CheatTarget::One(contract_address), Account::user1());
+ dispatcher.transfer_from(Account::admin(), Account::user2(), 10);
+ assert(
+ dispatcher.allowance(Account::admin(), Account::user1()) == 10, Errors::FUNDS_NOT_SENT
+ );
+ stop_prank(CheatTarget::One(contract_address));
+ }
+
+ #[test]
+ fn test_transfer_from() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+ let user1 = Account::user1();
+ start_prank(CheatTarget::One(contract_address), Account::admin());
+ dispatcher.approve(user1, 10);
+ assert(dispatcher.allowance(Account::admin(), user1) == 10, Errors::NOT_ALLOWED);
+ stop_prank(CheatTarget::One(contract_address));
+
+ start_prank(CheatTarget::One(contract_address), user1);
+ dispatcher.transfer_from(Account::admin(), Account::user2(), 5);
+ assert(dispatcher.balance_of(Account::user2()) == 5, Errors::INVALID_BALANCE);
+ // dispatcher.transfer_from(Account::admin(), user1, 15);
+ // assert(dispatcher.balance_of(user1) == 5, Errors::INVALID_BALANCE);
+ stop_prank(CheatTarget::One(contract_address));
+ }
+
+ #[test]
+ #[should_panic(expected: ('Amount Not Allowed', ))]
+ fn test_transfer_from_should_fail() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher {contract_address};
+ start_prank(CheatTarget::One(contract_address), Account::admin());
+ dispatcher.approve(Account::user1(), 20);
+ stop_prank(CheatTarget::One(contract_address));
+
+ start_prank(CheatTarget::One(contract_address), Account::user1());
+ dispatcher.transfer_from(Account::admin(), Account::user2(), 40);
+ }
+
+ #[test]
+ #[should_panic(expected: ('You have no token approved', ))]
+ fn test_transfer_from_failed_when_not_approved() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+ start_prank(CheatTarget::One(contract_address), Account::user1());
+ dispatcher.transfer_from(Account::admin(), Account::user2(), 5);
+ }
+
+ #[test]
+ fn test_approve() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+
+ start_prank(CheatTarget::One(contract_address), Account::admin());
+ dispatcher.approve(Account::user1(), 50);
+ assert(
+ dispatcher.allowance(Account::admin(), Account::user1()) == 50,
+ Errors::INVALID_ALLOWANCE_GIVEN
+ );
+ }
+
+ #[test]
+ fn test_increase_allowance() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+
+ start_prank(CheatTarget::One(contract_address), Account::admin());
+ dispatcher.approve(Account::user1(), 30);
+ assert(
+ dispatcher.allowance(Account::admin(), Account::user1()) == 30,
+ Errors::INVALID_ALLOWANCE_GIVEN
+ );
+
+ dispatcher.increase_allowance(Account::user1(), 20);
+
+ assert(
+ dispatcher.allowance(Account::admin(), Account::user1()) == 50,
+ Errors::ERROR_INCREASING_ALLOWANCE
+ );
+ }
+
+ #[test]
+ fn test_decrease_allowance() {
+ let contract_address = deploy_contract();
+ let dispatcher = IERC20Dispatcher { contract_address };
+
+ start_prank(CheatTarget::One(contract_address), Account::admin());
+ dispatcher.approve(Account::user1(), 30);
+ assert(
+ dispatcher.allowance(Account::admin(), Account::user1()) == 30,
+ Errors::INVALID_ALLOWANCE_GIVEN
+ );
+
+ dispatcher.decrease_allowance(Account::user1(), 5);
+
+ assert(
+ dispatcher.allowance(Account::admin(), Account::user1()) == 25,
+ Errors::ERROR_DECREASING_ALLOWANCE
+ );
+ }
+
+ // Custom errors for error handling
+ mod Errors {
+ const INVALID_DECIMALS: felt252 = 'Invalid decimals!';
+ const UNMATCHED_SUPPLY: felt252 = 'Unmatched supply!';
+ const INVALID_BALANCE: felt252 = 'Invalid balance!';
+ const INVALID_ALLOWANCE_GIVEN: felt252 = 'Invalid allowance given';
+ const FUNDS_NOT_SENT: felt252 = 'Funds not sent!';
+ const FUNDS_NOT_RECIEVED: felt252 = 'Funds not recieved!';
+ const ERROR_INCREASING_ALLOWANCE: felt252 = 'Allowance not increased';
+ const ERROR_DECREASING_ALLOWANCE: felt252 = 'Allowance not decreased';
+ }
+}