diff --git a/src/base/constants/errors.cairo b/src/base/constants/errors.cairo index 62e29a9..3290474 100644 --- a/src/base/constants/errors.cairo +++ b/src/base/constants/errors.cairo @@ -22,6 +22,7 @@ pub mod Errors { pub const SELF_TIPPING: felt252 = 'Karst: self-tip forbidden!'; pub const SELF_TRANSFER: felt252 = 'Karst: self-transfer forbidden!'; pub const SELF_REQUEST: felt252 = 'Karst: self-request forbidden!'; + pub const INVALID_EXPIRATION_STAMP: felt252 = 'Karst: invalid expiration stamp'; pub const INSUFFICIENT_ALLOWANCE: felt252 = 'Karst: insufficient allowance!'; pub const AUTO_RENEW_DURATION_ENDED: felt252 = 'Karst: auto renew ended!'; pub const INVALID_JOLT: felt252 = 'Karst: invalid jolt!'; diff --git a/src/interfaces/IJolt.cairo b/src/interfaces/IJolt.cairo index 7960f4a..9b76dfd 100644 --- a/src/interfaces/IJolt.cairo +++ b/src/interfaces/IJolt.cairo @@ -9,7 +9,7 @@ pub trait IJolt { fn jolt(ref self: TState, jolt_params: JoltParams) -> u256; fn set_fee_address(ref self: TState, _fee_address: ContractAddress); fn auto_renew(ref self: TState, profile: ContractAddress, renewal_id: u256) -> bool; - fn fullfill_request(ref self: TState, jolt_id: u256, sender: ContractAddress) -> bool; + fn fulfill_request(ref self: TState, jolt_id: u256) -> bool; // ************************************************************************* // GETTERS // ************************************************************************* diff --git a/src/jolt/jolt.cairo b/src/jolt/jolt.cairo index cf3cc5b..ea93216 100644 --- a/src/jolt/jolt.cairo +++ b/src/jolt/jolt.cairo @@ -134,7 +134,7 @@ pub mod Jolt { let jolt_type = @jolt_params.jolt_type; match jolt_type { JoltType::Tip => { - let _jolt_status = self + let _jolt_status = self ._tip( jolt_id, sender, @@ -203,13 +203,13 @@ pub mod Jolt { self.fee_address.write(_fee_address); } - fn fullfill_request( - ref self: ContractState, jolt_id: u256, sender: ContractAddress - ) -> bool { + fn fulfill_request(ref self: ContractState, jolt_id: u256) -> bool { // get jolt details let mut jolt_details = self.jolt.read(jolt_id); + let sender = get_caller_address(); // validate request + assert(jolt_details.jolt_type == JoltType::Request, Errors::INVALID_JOLT); assert(jolt_details.status == JoltStatus::PENDING, Errors::INVALID_JOLT); assert(sender == jolt_details.recipient, Errors::INVALID_JOLT_RECIPIENT); @@ -334,9 +334,7 @@ pub mod Jolt { if (renewal_status == true) { // check allowances match auto-renew duration let allowance = dispatcher.allowance(sender, this_contract); - assert( - allowance >= renewal_duration * amount, Errors::INSUFFICIENT_ALLOWANCE - ); + assert(allowance >= renewal_duration * amount, Errors::INSUFFICIENT_ALLOWANCE); // generate renewal ID let renewal_hash = PedersenTrait::new(0) @@ -387,9 +385,11 @@ pub mod Jolt { expiration_timestamp: u64, erc20_contract_address: ContractAddress ) -> JoltStatus { - // check that user is not requesting to self or to a non-existent address + // check that user is not requesting to self or to a non-existent address and expiration + // time exceeds current time assert(sender != recipient, Errors::SELF_REQUEST); assert(recipient.is_non_zero(), Errors::INVALID_PROFILE_ADDRESS); + assert(expiration_timestamp > get_block_timestamp(), Errors::INVALID_EXPIRATION_STAMP); // emit event self @@ -412,12 +412,13 @@ pub mod Jolt { ref self: ContractState, jolt_id: u256, sender: ContractAddress, jolt_details: JoltData ) -> bool { // transfer request amount - self._transfer_helper( - jolt_details.erc20_contract_address, - jolt_details.sender, - jolt_details.recipient, - jolt_details.amount - ); + self + ._transfer_helper( + jolt_details.erc20_contract_address, + sender, + jolt_details.sender, + jolt_details.amount + ); // update jolt details let jolt_data = JoltData { status: JoltStatus::SUCCESSFUL, ..jolt_details }; @@ -428,8 +429,8 @@ pub mod Jolt { .emit( JoltRequestFullfilled { jolt_id, - jolt_type: 'REQUEST', - sender, + jolt_type: 'REQUEST FULFILLMENT', + sender: jolt_details.recipient, recipient: jolt_details.sender, expiration_timestamp: jolt_details.expiration_stamp, block_timestamp: get_block_timestamp(), @@ -506,10 +507,10 @@ pub mod Jolt { } fn _transfer_helper( - ref self: ContractState, - erc20_contract_address: ContractAddress, - sender: ContractAddress, - recipient: ContractAddress, + ref self: ContractState, + erc20_contract_address: ContractAddress, + sender: ContractAddress, + recipient: ContractAddress, amount: u256 ) { let dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; @@ -522,4 +523,4 @@ pub mod Jolt { dispatcher.transfer_from(sender, recipient, amount); } } -} \ No newline at end of file +} diff --git a/src/mocks.cairo b/src/mocks.cairo index 1541e6f..231805c 100644 --- a/src/mocks.cairo +++ b/src/mocks.cairo @@ -1,3 +1,3 @@ pub mod registry; pub mod interfaces; -pub mod ERC20; \ No newline at end of file +pub mod ERC20; diff --git a/src/mocks/ERC20.cairo b/src/mocks/ERC20.cairo index 17076b9..c184133 100644 --- a/src/mocks/ERC20.cairo +++ b/src/mocks/ERC20.cairo @@ -24,15 +24,11 @@ mod USDT { } #[constructor] - fn constructor( - ref self: ContractState, - initial_supply: u256, - recipient: ContractAddress - ) { + fn constructor(ref self: ContractState, initial_supply: u256, recipient: ContractAddress) { let name = "USDT"; let symbol = "USDT"; self.erc20.initializer(name, symbol); self.erc20.mint(recipient, initial_supply); } -} \ No newline at end of file +} diff --git a/tests/test_jolt.cairo b/tests/test_jolt.cairo index 5c20c2e..2b59287 100644 --- a/tests/test_jolt.cairo +++ b/tests/test_jolt.cairo @@ -3,14 +3,16 @@ use core::hash::HashStateTrait; use core::pedersen::PedersenTrait; use starknet::{ContractAddress, contract_address_const}; use snforge_std::{ - declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, stop_cheat_caller_address, start_cheat_nonce, stop_cheat_nonce, start_cheat_block_timestamp, stop_cheat_block_timestamp, spy_events, EventSpyAssertionsTrait + declare, DeclareResultTrait, ContractClassTrait, start_cheat_caller_address, + stop_cheat_caller_address, start_cheat_nonce, stop_cheat_nonce, start_cheat_block_timestamp, + stop_cheat_block_timestamp, spy_events, EventSpyAssertionsTrait }; use karst::interfaces::IJolt::{IJoltDispatcher, IJoltDispatcherTrait}; use karst::interfaces::IERC20::{IERC20Dispatcher, IERC20DispatcherTrait}; use karst::jolt::jolt::Jolt::{Event as JoltEvent, Jolted}; -use karst::base::{ - constants::types::{JoltParams, JoltType, JoltStatus} -}; +use karst::jolt::jolt::Jolt::{Event as JoltRequestEvent, JoltRequested}; +use karst::jolt::jolt::Jolt::{Event as JoltRequestFulfillEvent, JoltRequestFullfilled}; +use karst::base::{constants::types::{JoltParams, JoltType, JoltStatus}}; const ADMIN: felt252 = 5382942; const ADDRESS1: felt252 = 254290; @@ -26,7 +28,9 @@ fn __setup__() -> (ContractAddress, ContractAddress) { // deploy mock USDT let usdt_contract = declare("USDT").unwrap().contract_class(); - let (usdt_contract_address, _) = usdt_contract.deploy(@array![1000000000000000000000, 0, ADDRESS1]).unwrap(); + let (usdt_contract_address, _) = usdt_contract + .deploy(@array![1000000000000000000000, 0, ADDRESS1]) + .unwrap(); return (jolt_contract_address, usdt_contract_address); } @@ -37,7 +41,7 @@ fn __setup__() -> (ContractAddress, ContractAddress) { #[test] fn test_jolt_tipping() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -71,7 +75,7 @@ fn test_jolt_tipping() { .update(4) .finalize(); let expected_jolt_id: u256 = jolt_hash.try_into().unwrap(); - + // check jolt data let jolt_data = dispatcher.get_jolt(jolt_id); assert(jolt_data.jolt_id == expected_jolt_id, 'invalid jolt ID'); @@ -99,7 +103,7 @@ fn test_jolting_with_same_params_have_different_jolt_ids() { let (jolt_contract_address, erc20_contract_address) = __setup__(); let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; - + let jolt_params_1 = JoltParams { jolt_type: JoltType::Tip, recipient: ADDRESS2.try_into().unwrap(), @@ -147,7 +151,7 @@ fn test_jolting_with_same_params_have_different_jolt_ids() { #[should_panic(expected: ('Karst: self-tip forbidden!',))] fn test_tipper_cant_self_tip() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -176,7 +180,7 @@ fn test_tipper_cant_self_tip() { #[should_panic(expected: ('Karst: invalid profile address!',))] fn test_tipper_cant_tip_a_zero_address() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -204,7 +208,7 @@ fn test_tipper_cant_tip_a_zero_address() { #[test] fn test_jolt_event_is_emitted_on_tipping() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -251,7 +255,7 @@ fn test_jolt_event_is_emitted_on_tipping() { #[test] fn test_jolt_transfer() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -285,7 +289,7 @@ fn test_jolt_transfer() { .update(4) .finalize(); let expected_jolt_id: u256 = jolt_hash.try_into().unwrap(); - + // check jolt data let jolt_data = dispatcher.get_jolt(jolt_id); assert(jolt_data.jolt_id == expected_jolt_id, 'invalid jolt ID'); @@ -312,7 +316,7 @@ fn test_jolt_transfer() { #[should_panic(expected: ('Karst: invalid profile address!',))] fn test_sender_cant_transfer_to_a_zero_address() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -341,7 +345,7 @@ fn test_sender_cant_transfer_to_a_zero_address() { #[should_panic(expected: ('Karst: self-transfer forbidden!',))] fn test_sender_cant_self_transfer() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -369,7 +373,7 @@ fn test_sender_cant_self_transfer() { #[test] fn test_jolt_event_is_emitted_on_transfer() { let (jolt_contract_address, erc20_contract_address) = __setup__(); - let dispatcher = IJoltDispatcher{ contract_address: jolt_contract_address }; + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; let jolt_params = JoltParams { @@ -412,4 +416,436 @@ fn test_jolt_event_is_emitted_on_transfer() { // ************************************************************************* // TEST - REQUEST -// ************************************************************************* \ No newline at end of file +// ************************************************************************* +#[test] +fn test_jolt_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 15640, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + start_cheat_nonce(jolt_contract_address, 23); + + let jolt_id = dispatcher.jolt(jolt_params); + + // calculate expected jolt ID + let jolt_hash = PedersenTrait::new(0) + .update(ADDRESS1) + .update(2000000000000000000) + .update(0) + .update(23) + .update(4) + .finalize(); + let expected_jolt_id: u256 = jolt_hash.try_into().unwrap(); + + // check jolt data + let jolt_data = dispatcher.get_jolt(jolt_id); + assert(jolt_data.jolt_id == expected_jolt_id, 'invalid jolt ID'); + assert(jolt_data.jolt_type == JoltType::Request, 'invalid jolt type'); + assert(jolt_data.sender == ADDRESS2.try_into().unwrap(), 'invalid sender'); + assert(jolt_data.recipient == ADDRESS1.try_into().unwrap(), 'invalid recipient'); + assert(jolt_data.memo == "hey first request ever!", 'invalid memo'); + assert(jolt_data.amount == 2000000000000000000, 'invalid amount'); + assert(jolt_data.status == JoltStatus::PENDING, 'invalid status'); + assert(jolt_data.expiration_stamp == 15640, 'invalid expiration stamp'); + assert(jolt_data.block_timestamp == 5640, 'invalid block stamp'); + assert(jolt_data.erc20_contract_address == erc20_contract_address, 'invalid address'); + + stop_cheat_nonce(jolt_contract_address); + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid profile address!',))] +fn test_requester_cant_request_to_a_zero_address() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: contract_address_const::<0>(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 15460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.jolt(jolt_params); + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: self-request forbidden!',))] +fn test_requester_cant_self_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 15460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_jolt_event_is_emitted_on_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 154600, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 36000); + + let mut spy = spy_events(); + let jolt_id = dispatcher.jolt(jolt_params); + + // check for events + let expected_event = JoltRequestEvent::JoltRequested( + JoltRequested { + jolt_id: jolt_id, + jolt_type: 'REQUEST', + sender: ADDRESS1.try_into().unwrap(), + recipient: ADDRESS2.try_into().unwrap(), + expiration_timestamp: 154600, + block_timestamp: 36000, + } + ); + spy.assert_emitted(@array![(jolt_contract_address, expected_event)]); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid expiration stamp',))] +fn test_request_expiration_time_must_be_greater_than_current_time() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid jolt!',))] +fn test_cant_fulfill_request_if_jolt_type_is_not_a_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Transfer, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 12460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + // approve contract to spend amount + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // try to fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.fulfill_request(jolt_id); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: invalid jolt!',))] +fn test_cant_fulfill_request_if_jolt_status_is_not_pending() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 12460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 5000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // try to fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.fulfill_request(jolt_id); + dispatcher.fulfill_request(jolt_id); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +#[should_panic(expected: ('Karst: not request recipient!',))] +fn test_cant_fulfill_request_if_sender_is_not_initial_recipient() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 12460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // try to fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + dispatcher.fulfill_request(jolt_id); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_if_expiration_time_has_exceeded_jolt_fails_with_expired_status() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS2.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 8460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // try to fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 95640); + + let status = dispatcher.fulfill_request(jolt_id); + + let jolt_data = dispatcher.get_jolt(jolt_id); + assert(jolt_data.status == JoltStatus::EXPIRED, 'invalid jolt status'); + assert(status == false, 'invalid return status'); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_fulfill_request() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 8460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5840); + + let status = dispatcher.fulfill_request(jolt_id); + + // check jolt data + let jolt_data = dispatcher.get_jolt(jolt_id); + assert(jolt_data.jolt_type == JoltType::Request, 'invalid jolt type'); + assert(jolt_data.sender == ADDRESS2.try_into().unwrap(), 'invalid sender'); + assert(jolt_data.recipient == ADDRESS1.try_into().unwrap(), 'invalid recipient'); + assert(jolt_data.memo == "hey first request ever!", 'invalid memo'); + assert(jolt_data.amount == 2000000000000000000, 'invalid amount'); + assert(jolt_data.status == JoltStatus::SUCCESSFUL, 'invalid status'); + assert(jolt_data.expiration_stamp == 8460, 'invalid expiration stamp'); + assert(jolt_data.block_timestamp == 5640, 'invalid block stamp'); + assert(jolt_data.erc20_contract_address == erc20_contract_address, 'invalid address'); + assert(status == true, 'invalid return status'); + + // check that recipient received his tip + let balance = erc20_dispatcher.balance_of(ADDRESS2.try_into().unwrap()); + assert(balance == 2000000000000000000, 'incorrect balance'); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +} + +#[test] +fn test_jolt_event_is_emitted_on_request_fulfillment() { + let (jolt_contract_address, erc20_contract_address) = __setup__(); + let dispatcher = IJoltDispatcher { contract_address: jolt_contract_address }; + let erc20_dispatcher = IERC20Dispatcher { contract_address: erc20_contract_address }; + + let jolt_params = JoltParams { + jolt_type: JoltType::Request, + recipient: ADDRESS1.try_into().unwrap(), + memo: "hey first request ever!", + amount: 2000000000000000000, + expiration_stamp: 8460, + auto_renewal: (false, 0), + erc20_contract_address: erc20_contract_address + }; + + // jolt + start_cheat_caller_address(jolt_contract_address, ADDRESS2.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5640); + + let jolt_id = dispatcher.jolt(jolt_params); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); + + // approve contract to spend amount + start_cheat_caller_address(erc20_contract_address, ADDRESS1.try_into().unwrap()); + erc20_dispatcher.approve(jolt_contract_address, 2000000000000000000); + stop_cheat_caller_address(erc20_contract_address); + + // fulfill request + start_cheat_caller_address(jolt_contract_address, ADDRESS1.try_into().unwrap()); + start_cheat_block_timestamp(jolt_contract_address, 5840); + + let mut spy = spy_events(); + dispatcher.fulfill_request(jolt_id); + + // check for events + let expected_event = JoltRequestFulfillEvent::JoltRequestFullfilled( + JoltRequestFullfilled { + jolt_id: jolt_id, + jolt_type: 'REQUEST FULFILLMENT', + sender: ADDRESS1.try_into().unwrap(), + recipient: ADDRESS2.try_into().unwrap(), + expiration_timestamp: 8460, + block_timestamp: 5840, + } + ); + spy.assert_emitted(@array![(jolt_contract_address, expected_event)]); + + stop_cheat_block_timestamp(jolt_contract_address); + stop_cheat_caller_address(jolt_contract_address); +}