From 98e3e54501dae7b061685098295c97d6db87a062 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Thu, 24 Aug 2023 22:10:55 -0700 Subject: [PATCH 001/133] add prev_id to job struct so we don't need to rely on indexer to get previous id for recurring jobs --- contracts/warp-controller/src/contract.rs | 4 ++++ contracts/warp-controller/src/execute/job.rs | 6 ++++++ packages/controller/src/job.rs | 2 ++ 3 files changed, 12 insertions(+) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 456368dd..590a15bd 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -314,6 +314,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result Result Ok(Job { id: job.id, + prev_id: job.prev_id, owner: job.owner, last_update_time: job.last_update_time, name: job.name, @@ -666,6 +669,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Ok(Job { id: state.current_job_id, + prev_id: Some(finished_job.id), owner: finished_job.owner.clone(), last_update_time: Uint64::from(env.block.time.seconds()), name: finished_job.name.clone(), diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index fe104dda..5e3dbaae 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -60,6 +60,7 @@ pub fn create_job( let job = PENDING_JOBS().update(deps.storage, state.current_job_id.u64(), |s| match s { None => Ok(Job { id: state.current_job_id, + prev_id: None, owner: account.owner, last_update_time: Uint64::from(env.block.time.seconds()), name: data.name, @@ -151,6 +152,7 @@ pub fn delete_job( let _new_job = FINISHED_JOBS().update(deps.storage, data.id.u64(), |h| match h { None => Ok(Job { id: job.id, + prev_id: job.prev_id, owner: job.owner, last_update_time: job.last_update_time, name: job.name, @@ -231,6 +233,7 @@ pub fn update_job( None => Err(ContractError::JobDoesNotExist {}), Some(job) => Ok(Job { id: job.id, + prev_id: job.prev_id, owner: job.owner, last_update_time: if added_reward > config.minimum_reward { Uint64::new(env.block.time.seconds()) @@ -353,6 +356,7 @@ pub fn execute_job( data.id.u64(), &Job { id: job.id, + prev_id: job.prev_id, owner: job.owner, last_update_time: job.last_update_time, name: job.name, @@ -481,6 +485,7 @@ pub fn evict_job( None => Err(ContractError::JobDoesNotExist {}), Some(job) => Ok(Job { id: job.id, + prev_id: job.prev_id, owner: job.owner, last_update_time: Uint64::new(env.block.time.seconds()), name: job.name, @@ -504,6 +509,7 @@ pub fn evict_job( .update(deps.storage, data.id.u64(), |j| match j { None => Ok(Job { id: job.id, + prev_id: job.prev_id, owner: job.owner, last_update_time: Uint64::new(env.block.time.seconds()), name: job.name, diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 37939f30..59ca4c27 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -22,6 +22,8 @@ use strum_macros::Display; #[cw_serde] pub struct Job { pub id: Uint64, + // Exist if job is the follow up job of a recurring job + pub prev_id: Option, pub owner: Addr, pub last_update_time: Uint64, pub name: String, From 83172329c36f119ce4f32f747b540d478f7df84c Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Thu, 24 Aug 2023 22:15:17 -0700 Subject: [PATCH 002/133] send execution reward directly to executor instead of executor's warp account, so anyone can run the keeper (to evict or execute job) without creating a warp account --- contracts/warp-controller/src/execute/job.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 5e3dbaae..24c27be5 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -318,12 +318,6 @@ pub fn execute_job( let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; let account = ACCOUNTS().load(deps.storage, job.owner.clone())?; - if !ACCOUNTS().has(deps.storage, info.sender.clone()) { - return Err(ContractError::AccountDoesNotExist {}); - } - - let keeper_account = ACCOUNTS().load(deps.storage, info.sender.clone())?; - if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); } @@ -407,8 +401,9 @@ pub fn execute_job( }); } + //send reward to executor let reward_msg = BankMsg::Send { - to_address: keeper_account.account.to_string(), + to_address: info.sender.to_string(), amount: vec![Coin::new(job.reward.u128(), config.fee_denom)], }; From d8a83de24663be39172c45d28cebf095d2b6406a Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 11 Sep 2023 11:57:52 -0400 Subject: [PATCH 003/133] fmt, clippy --- contracts/warp-controller/src/execute/job.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index c009ad1b..12bc6629 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -381,10 +381,9 @@ pub fn execute_job( attrs.push(Attribute::new("job_condition_status", "valid")); if !resolution? { return Ok(Response::new() - .add_attribute("action", "execute_job", ) + .add_attribute("action", "execute_job") .add_attribute("condition", "false") - .add_attribute("job_id", job.id) - ); + .add_attribute("job_id", job.id)); } submsgs.push(SubMsg { From 5ccf80cc8650d5ee3b72d1988c6052bfbd95c5b5 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Sep 2023 18:49:22 +0200 Subject: [PATCH 004/133] add job queue abstraction --- contracts/warp-controller/src/state.rs | 133 ++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index edacdad2..722d9c72 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -1,10 +1,12 @@ use controller::account::Account; -use cosmwasm_std::Addr; +use cosmwasm_std::{Addr, DepsMut, Env, Uint128, Uint64}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; -use controller::job::Job; +use controller::job::{Job, JobStatus, UpdateJobMsg}; use controller::{Config, State}; +use crate::ContractError; + pub struct JobIndexes<'a> { pub reward: UniqueIndex<'a, (u128, u64), Job>, pub publish_time: MultiIndex<'a, u64, Job, u64>, @@ -71,3 +73,130 @@ pub fn ACCOUNTS<'a>() -> IndexedMap<'a, Addr, Account, AccountIndexes<'a>> { pub const QUERY_PAGE_SIZE: u32 = 50; pub const CONFIG: Item = Item::new("config"); pub const STATE: Item = Item::new("state"); + +pub trait JobQueue { + fn add(deps: &mut DepsMut, job: Job) -> Result; + fn get(deps: &DepsMut, job_id: u64) -> Result; + fn update(deps: &mut DepsMut, env: Env, data: UpdateJobMsg) -> Result; + fn finalize(deps: &mut DepsMut, job_id: u64, status: JobStatus) -> Result; + fn remove(deps: &mut DepsMut, job_id: u64) -> Result<(), ContractError>; +} + +pub struct JobQueueInstance; + +impl JobQueue for JobQueueInstance { + fn add(deps: &mut DepsMut, job: Job) -> Result { + let state = STATE.load(deps.storage)?; + + let job = PENDING_JOBS().update(deps.storage, state.current_job_id.u64(), |s| match s { + None => Ok(job), + Some(_) => Err(ContractError::JobAlreadyExists {}), + })?; + + STATE.save( + deps.storage, + &State { + current_job_id: state.current_job_id.checked_add(Uint64::new(1))?, + q: state.q.checked_add(Uint64::new(1))?, + }, + )?; + + Ok(job) + } + + fn get(deps: &DepsMut, job_id: u64) -> Result { + let job = PENDING_JOBS().load(deps.storage, job_id)?; + + Ok(job) + } + + fn update(deps: &mut DepsMut, env: Env, data: UpdateJobMsg) -> Result { + let config = CONFIG.load(deps.storage)?; + let added_reward: Uint128 = data.added_reward.unwrap_or(Uint128::new(0)); + + PENDING_JOBS().update(deps.storage, data.id.u64(), |h| match h { + None => Err(ContractError::JobDoesNotExist {}), + Some(job) => Ok(Job { + id: job.id, + owner: job.owner, + last_update_time: if added_reward > config.minimum_reward { + Uint64::new(env.block.time.seconds()) + } else { + job.last_update_time + }, + name: data.name.unwrap_or(job.name), + description: data.description.unwrap_or(job.description), + labels: data.labels.unwrap_or(job.labels), + status: job.status, + condition: job.condition, + terminate_condition: job.terminate_condition, + msgs: job.msgs, + vars: job.vars, + recurring: job.recurring, + requeue_on_evict: job.requeue_on_evict, + reward: job.reward + added_reward, + assets_to_withdraw: job.assets_to_withdraw, + }), + }) + } + + fn finalize(deps: &mut DepsMut, job_id: u64, status: JobStatus) -> Result { + if status == JobStatus::Pending { + return Err(ContractError::Unauthorized {}); + } + + let job = PENDING_JOBS().load(deps.storage, job_id)?; + + let new_job = Job { + id: job.id, + owner: job.owner, + last_update_time: job.last_update_time, + name: job.name, + description: job.description, + labels: job.labels, + status, + condition: job.condition, + terminate_condition: job.terminate_condition, + msgs: job.msgs, + vars: job.vars, + recurring: job.recurring, + requeue_on_evict: job.requeue_on_evict, + reward: job.reward, + assets_to_withdraw: job.assets_to_withdraw, + }; + + FINISHED_JOBS().update(deps.storage, job_id, |j| match j { + None => Ok(new_job), + Some(_) => Err(ContractError::JobAlreadyFinished {}), + })?; + + PENDING_JOBS().remove(deps.storage, job_id)?; + + let state = STATE.load(deps.storage)?; + STATE.save( + deps.storage, + &State { + current_job_id: state.current_job_id, + q: state.q.checked_sub(Uint64::new(1))?, + }, + )?; + + Ok(new_job) + } + + fn remove(deps: &mut DepsMut, job_id: u64) -> Result<(), ContractError> { + PENDING_JOBS().remove(deps.storage, job_id)?; + + let state = STATE.load(deps.storage)?; + + STATE.save( + deps.storage, + &State { + current_job_id: state.current_job_id, + q: state.q.checked_sub(Uint64::new(1))?, + }, + )?; + + Ok(()) + } +} From 316fc665b235e2dc32b91bcc35954a13bc4559a8 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Sep 2023 18:49:31 +0200 Subject: [PATCH 005/133] refactor reply --- contracts/warp-controller/src/contract.rs | 77 +++++++---------------- 1 file changed, 23 insertions(+), 54 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 84f1d691..78cb97a8 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -1,5 +1,5 @@ use crate::error::map_contract_error; -use crate::state::{ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS}; +use crate::state::{JobQueue, JobQueueInstance, ACCOUNTS, CONFIG}; use crate::{execute, query, state::STATE, ContractError}; use account::{GenericMsg, WithdrawAssetsMsg}; use controller::account::{Account, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; @@ -189,7 +189,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { +pub fn reply(mut deps: DepsMut, env: Env, msg: Reply) -> Result { match msg.id { //account creation 0 => { @@ -301,36 +301,14 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { - let mut state = STATE.load(deps.storage)?; + let state = STATE.load(deps.storage)?; let new_status = match msg.result { SubMsgResult::Ok(_) => JobStatus::Executed, SubMsgResult::Err(_) => JobStatus::Failed, }; - let job = PENDING_JOBS().load(deps.storage, msg.id)?; - PENDING_JOBS().remove(deps.storage, msg.id)?; - - let finished_job = FINISHED_JOBS().update(deps.storage, msg.id, |j| match j { - None => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: job.last_update_time, - name: job.name, - description: job.description, - labels: job.labels, - status: new_status, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - assets_to_withdraw: job.assets_to_withdraw, - }), - Some(_) => Err(ContractError::JobAlreadyFinished {}), - })?; + let finished_job = JobQueueInstance::finalize(&mut deps, msg.id, new_status)?; let res_attrs = match msg.result { SubMsgResult::Err(e) => vec![Attribute::new( @@ -429,34 +407,27 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Ok(Job { - id: state.current_job_id, - owner: finished_job.owner.clone(), - last_update_time: Uint64::from(env.block.time.seconds()), - name: finished_job.name.clone(), - description: finished_job.description, - labels: finished_job.labels, - status: JobStatus::Pending, - condition: finished_job.condition.clone(), - terminate_condition: finished_job.terminate_condition.clone(), - vars: new_vars, - requeue_on_evict: finished_job.requeue_on_evict, - recurring: finished_job.recurring, - msgs: finished_job.msgs.clone(), - reward: finished_job.reward, - assets_to_withdraw: finished_job.assets_to_withdraw, - }), - Some(_) => Err(ContractError::JobAlreadyExists {}), + let new_job = JobQueueInstance::add( + &mut deps, + Job { + id: state.current_job_id, + owner: finished_job.owner.clone(), + last_update_time: Uint64::from(env.block.time.seconds()), + name: finished_job.name.clone(), + description: finished_job.description, + labels: finished_job.labels, + status: JobStatus::Pending, + condition: finished_job.condition.clone(), + terminate_condition: finished_job.terminate_condition.clone(), + vars: new_vars, + requeue_on_evict: finished_job.requeue_on_evict, + recurring: finished_job.recurring, + msgs: finished_job.msgs.clone(), + reward: finished_job.reward, + assets_to_withdraw: finished_job.assets_to_withdraw, }, )?; - state.current_job_id = state.current_job_id.checked_add(Uint64::new(1))?; - state.q = state.q.checked_add(Uint64::new(1))?; - msgs.push( //send reward to controller WasmMsg::Execute { @@ -531,11 +502,9 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Date: Mon, 11 Sep 2023 18:57:54 +0200 Subject: [PATCH 006/133] refactor evict_job --- contracts/warp-controller/src/execute/job.rs | 63 +++----------------- 1 file changed, 8 insertions(+), 55 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index fe104dda..519c6246 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,4 +1,6 @@ -use crate::state::{ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS, STATE}; +use crate::state::{ + JobQueue, JobQueueInstance, ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS, STATE, +}; use crate::ContractError; use crate::ContractError::EvictionPeriodNotElapsed; use account::GenericMsg; @@ -419,7 +421,7 @@ pub fn execute_job( } pub fn evict_job( - deps: DepsMut, + mut deps: DepsMut, env: Env, info: MessageInfo, data: EvictJobMsg, @@ -476,52 +478,11 @@ pub fn evict_job( funds: vec![], }), ); - job_status = PENDING_JOBS() - .update(deps.storage, data.id.u64(), |j| match j { - None => Err(ContractError::JobDoesNotExist {}), - Some(job) => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: Uint64::new(env.block.time.seconds()), - name: job.name, - description: job.description, - labels: job.labels, - status: JobStatus::Pending, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - assets_to_withdraw: job.assets_to_withdraw, - }), - })? - .status; + job_status = JobQueueInstance::sync(&mut deps, env.clone(), job.clone())?.status; } else { - PENDING_JOBS().remove(deps.storage, data.id.u64())?; - job_status = FINISHED_JOBS() - .update(deps.storage, data.id.u64(), |j| match j { - None => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: Uint64::new(env.block.time.seconds()), - name: job.name, - description: job.description, - labels: job.labels, - status: JobStatus::Evicted, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - assets_to_withdraw: job.assets_to_withdraw, - }), - Some(_) => Err(ContractError::JobAlreadyExists {}), - })? - .status; + job_status = + JobQueueInstance::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Evicted)? + .status; cosmos_msgs.append(&mut vec![ //send reward minus fee back to account @@ -534,14 +495,6 @@ pub fn evict_job( amount: vec![Coin::new((job.reward - a).u128(), config.fee_denom)], }), ]); - - STATE.save( - deps.storage, - &State { - current_job_id: state.current_job_id, - q: state.q.checked_sub(Uint64::new(1))?, - }, - )?; } Ok(Response::new() From ac2065ccf5b0b01e699845c0565be4031737dd5f Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Sep 2023 18:59:55 +0200 Subject: [PATCH 007/133] refactor execute_job --- contracts/warp-controller/src/execute/job.rs | 35 ++------------------ 1 file changed, 3 insertions(+), 32 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 519c6246..e275940b 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -306,8 +306,8 @@ pub fn update_job( } pub fn execute_job( - deps: DepsMut, - _env: Env, + mut deps: DepsMut, + env: Env, info: MessageInfo, data: ExecuteJobMsg, ) -> Result { @@ -349,36 +349,7 @@ pub fn execute_job( if let Err(e) = resolution { attrs.push(Attribute::new("job_condition_status", "invalid")); attrs.push(Attribute::new("error", e.to_string())); - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; - FINISHED_JOBS().save( - deps.storage, - data.id.u64(), - &Job { - id: job.id, - owner: job.owner, - last_update_time: job.last_update_time, - name: job.name, - description: job.description, - labels: job.labels, - status: JobStatus::Failed, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - assets_to_withdraw: job.assets_to_withdraw, - }, - )?; - PENDING_JOBS().remove(deps.storage, data.id.u64())?; - STATE.save( - deps.storage, - &State { - current_job_id: state.current_job_id, - q: state.q.checked_sub(Uint64::new(1))?, - }, - )?; + JobQueueInstance::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Failed); } else { attrs.push(Attribute::new("job_condition_status", "valid")); if !resolution? { From 603c8f35b3f6121d4bad6062c3d2cd4d22f1704e Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Sep 2023 19:01:45 +0200 Subject: [PATCH 008/133] refactor update_job --- contracts/warp-controller/src/execute/job.rs | 27 ++------------------ 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index e275940b..35cbadb5 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -205,7 +205,7 @@ pub fn delete_job( } pub fn update_job( - deps: DepsMut, + mut deps: DepsMut, env: Env, info: MessageInfo, data: UpdateJobMsg, @@ -229,30 +229,7 @@ pub fn update_job( return Err(ContractError::NameTooShort {}); } - let job = PENDING_JOBS().update(deps.storage, data.id.u64(), |h| match h { - None => Err(ContractError::JobDoesNotExist {}), - Some(job) => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: if added_reward > config.minimum_reward { - Uint64::new(env.block.time.seconds()) - } else { - job.last_update_time - }, - name: data.name.unwrap_or(job.name), - description: data.description.unwrap_or(job.description), - labels: data.labels.unwrap_or(job.labels), - status: job.status, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward + added_reward, - assets_to_withdraw: job.assets_to_withdraw, - }), - })?; + let job = JobQueueInstance::update(&mut deps, env.clone(), data)?; let fee = added_reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); From b80c957f4a5f51e6f663feed75b72d2eb6a5ac38 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Sep 2023 19:02:08 +0200 Subject: [PATCH 009/133] add sync --- contracts/warp-controller/src/state.rs | 44 +++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 722d9c72..8af62977 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -77,8 +77,14 @@ pub const STATE: Item = Item::new("state"); pub trait JobQueue { fn add(deps: &mut DepsMut, job: Job) -> Result; fn get(deps: &DepsMut, job_id: u64) -> Result; + fn sync(deps: &mut DepsMut, env: Env, job: Job) -> Result; fn update(deps: &mut DepsMut, env: Env, data: UpdateJobMsg) -> Result; - fn finalize(deps: &mut DepsMut, job_id: u64, status: JobStatus) -> Result; + fn finalize( + deps: &mut DepsMut, + env: Env, + job_id: u64, + status: JobStatus, + ) -> Result; fn remove(deps: &mut DepsMut, job_id: u64) -> Result<(), ContractError>; } @@ -110,6 +116,29 @@ impl JobQueue for JobQueueInstance { Ok(job) } + fn sync(deps: &mut DepsMut, env: Env, job: Job) -> Result { + PENDING_JOBS().update(deps.storage, job.id.u64(), |j| match j { + None => Err(ContractError::JobDoesNotExist {}), + Some(job) => Ok(Job { + id: job.id, + owner: job.owner, + last_update_time: Uint64::new(env.block.time.seconds()), + name: job.name, + description: job.description, + labels: job.labels, + status: JobStatus::Pending, + condition: job.condition, + terminate_condition: job.terminate_condition, + msgs: job.msgs, + vars: job.vars, + recurring: job.recurring, + requeue_on_evict: job.requeue_on_evict, + reward: job.reward, + assets_to_withdraw: job.assets_to_withdraw, + }), + }) + } + fn update(deps: &mut DepsMut, env: Env, data: UpdateJobMsg) -> Result { let config = CONFIG.load(deps.storage)?; let added_reward: Uint128 = data.added_reward.unwrap_or(Uint128::new(0)); @@ -140,7 +169,12 @@ impl JobQueue for JobQueueInstance { }) } - fn finalize(deps: &mut DepsMut, job_id: u64, status: JobStatus) -> Result { + fn finalize( + deps: &mut DepsMut, + env: Env, + job_id: u64, + status: JobStatus, + ) -> Result { if status == JobStatus::Pending { return Err(ContractError::Unauthorized {}); } @@ -150,7 +184,7 @@ impl JobQueue for JobQueueInstance { let new_job = Job { id: job.id, owner: job.owner, - last_update_time: job.last_update_time, + last_update_time: Uint64::new(env.block.time.seconds()), name: job.name, description: job.description, labels: job.labels, @@ -166,10 +200,10 @@ impl JobQueue for JobQueueInstance { }; FINISHED_JOBS().update(deps.storage, job_id, |j| match j { - None => Ok(new_job), + None => Ok(new_job.clone()), Some(_) => Err(ContractError::JobAlreadyFinished {}), })?; - + PENDING_JOBS().remove(deps.storage, job_id)?; let state = STATE.load(deps.storage)?; From 7e7c7946e6b2e7f03e9c163de932094eebacb9df Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Sep 2023 19:04:22 +0200 Subject: [PATCH 010/133] refactor delete_job --- contracts/warp-controller/src/execute/job.rs | 35 +++----------------- contracts/warp-controller/src/state.rs | 17 ---------- 2 files changed, 4 insertions(+), 48 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 35cbadb5..9b6e33bd 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -130,8 +130,8 @@ pub fn create_job( } pub fn delete_job( - deps: DepsMut, - _env: Env, + mut deps: DepsMut, + env: Env, info: MessageInfo, data: DeleteJobMsg, ) -> Result { @@ -149,35 +149,8 @@ pub fn delete_job( let account = ACCOUNTS().load(deps.storage, info.sender)?; - PENDING_JOBS().remove(deps.storage, data.id.u64())?; - let _new_job = FINISHED_JOBS().update(deps.storage, data.id.u64(), |h| match h { - None => Ok(Job { - id: job.id, - owner: job.owner, - last_update_time: job.last_update_time, - name: job.name, - status: JobStatus::Cancelled, - condition: job.condition, - terminate_condition: job.terminate_condition, - msgs: job.msgs, - vars: job.vars, - recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, - reward: job.reward, - description: job.description, - labels: job.labels, - assets_to_withdraw: job.assets_to_withdraw, - }), - Some(_job) => Err(ContractError::JobAlreadyFinished {}), - })?; - - STATE.save( - deps.storage, - &State { - current_job_id: state.current_job_id, - q: state.q.checked_sub(Uint64::new(1))?, - }, - )?; + let _new_job = + JobQueueInstance::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Cancelled)?; let fee = job.reward * Uint128::from(config.cancellation_fee_percentage) / Uint128::new(100); diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 8af62977..62c159d4 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -85,7 +85,6 @@ pub trait JobQueue { job_id: u64, status: JobStatus, ) -> Result; - fn remove(deps: &mut DepsMut, job_id: u64) -> Result<(), ContractError>; } pub struct JobQueueInstance; @@ -217,20 +216,4 @@ impl JobQueue for JobQueueInstance { Ok(new_job) } - - fn remove(deps: &mut DepsMut, job_id: u64) -> Result<(), ContractError> { - PENDING_JOBS().remove(deps.storage, job_id)?; - - let state = STATE.load(deps.storage)?; - - STATE.save( - deps.storage, - &State { - current_job_id: state.current_job_id, - q: state.q.checked_sub(Uint64::new(1))?, - }, - )?; - - Ok(()) - } } From adea96bb8d782dc7e0034c9f171b6ef1b090523c Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Sep 2023 19:07:33 +0200 Subject: [PATCH 011/133] refactor create_job --- contracts/warp-controller/src/execute/job.rs | 29 ++++++-------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 9b6e33bd..9933a10b 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,13 +1,10 @@ -use crate::state::{ - JobQueue, JobQueueInstance, ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS, STATE, -}; +use crate::state::{JobQueue, JobQueueInstance, ACCOUNTS, CONFIG, STATE}; use crate::ContractError; use crate::ContractError::EvictionPeriodNotElapsed; use account::GenericMsg; use controller::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Job, JobStatus, UpdateJobMsg, }; -use controller::State; use cosmwasm_std::{ to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Coin, CosmosMsg, DepsMut, Env, MessageInfo, QueryRequest, ReplyOn, Response, StdResult, SubMsg, Uint128, Uint64, WasmMsg, @@ -17,7 +14,7 @@ use resolver::QueryHydrateMsgsMsg; const MAX_TEXT_LENGTH: usize = 280; pub fn create_job( - deps: DepsMut, + mut deps: DepsMut, env: Env, info: MessageInfo, data: CreateJobMsg, @@ -59,8 +56,9 @@ pub fn create_job( Some(record) => record.1, }; - let job = PENDING_JOBS().update(deps.storage, state.current_job_id.u64(), |s| match s { - None => Ok(Job { + let job = JobQueueInstance::add( + &mut deps, + Job { id: state.current_job_id, owner: account.owner, last_update_time: Uint64::from(env.block.time.seconds()), @@ -76,15 +74,6 @@ pub fn create_job( description: data.description, labels: data.labels, assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), - }), - Some(_) => Err(ContractError::JobAlreadyExists {}), - })?; - - STATE.save( - deps.storage, - &State { - current_job_id: state.current_job_id.checked_add(Uint64::new(1))?, - q: state.q.checked_add(Uint64::new(1))?, }, )?; @@ -137,7 +126,7 @@ pub fn delete_job( ) -> Result { let config = CONFIG.load(deps.storage)?; let state = STATE.load(deps.storage)?; - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; + let job = JobQueueInstance::get(&deps, data.id.into())?; if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); @@ -183,7 +172,7 @@ pub fn update_job( info: MessageInfo, data: UpdateJobMsg, ) -> Result { - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; + let job = JobQueueInstance::get(&deps, data.id.into())?; let config = CONFIG.load(deps.storage)?; if info.sender != job.owner { @@ -264,7 +253,7 @@ pub fn execute_job( let _config = CONFIG.load(deps.storage)?; let state = STATE.load(deps.storage)?; let config = CONFIG.load(deps.storage)?; - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; + let job = JobQueueInstance::get(&deps, data.id.into())?; let account = ACCOUNTS().load(deps.storage, job.owner.clone())?; if !ACCOUNTS().has(deps.storage, info.sender.clone()) { @@ -349,7 +338,7 @@ pub fn evict_job( ) -> Result { let config = CONFIG.load(deps.storage)?; let state = STATE.load(deps.storage)?; - let job = PENDING_JOBS().load(deps.storage, data.id.u64())?; + let job = JobQueueInstance::get(&deps, data.id.into())?; let account = ACCOUNTS().load(deps.storage, job.owner.clone())?; let account_amount = deps From 30a1e75cb5bc363f0044eb15ef3b9a74083410d5 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Sep 2023 19:14:27 +0200 Subject: [PATCH 012/133] lint + fix --- contracts/warp-controller/src/contract.rs | 2 +- contracts/warp-controller/src/execute/job.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 78cb97a8..865874b8 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -308,7 +308,7 @@ pub fn reply(mut deps: DepsMut, env: Env, msg: Reply) -> Result JobStatus::Failed, }; - let finished_job = JobQueueInstance::finalize(&mut deps, msg.id, new_status)?; + let finished_job = JobQueueInstance::finalize(&mut deps, env.clone(), msg.id, new_status)?; let res_attrs = match msg.result { SubMsgResult::Err(e) => vec![Attribute::new( diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 9933a10b..17c18c8e 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -288,7 +288,7 @@ pub fn execute_job( if let Err(e) = resolution { attrs.push(Attribute::new("job_condition_status", "invalid")); attrs.push(Attribute::new("error", e.to_string())); - JobQueueInstance::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Failed); + JobQueueInstance::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Failed)?; } else { attrs.push(Attribute::new("job_condition_status", "valid")); if !resolution? { From 71b9d0a7677946fd6477de87818642adf6784350 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Sep 2023 23:09:16 +0200 Subject: [PATCH 013/133] cleanup code --- contracts/warp-controller/src/contract.rs | 6 ++--- contracts/warp-controller/src/execute/job.rs | 24 ++++++++--------- contracts/warp-controller/src/state.rs | 27 +++++--------------- 3 files changed, 21 insertions(+), 36 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 865874b8..b0ebdb69 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -1,5 +1,5 @@ use crate::error::map_contract_error; -use crate::state::{JobQueue, JobQueueInstance, ACCOUNTS, CONFIG}; +use crate::state::{JobQueue, ACCOUNTS, CONFIG}; use crate::{execute, query, state::STATE, ContractError}; use account::{GenericMsg, WithdrawAssetsMsg}; use controller::account::{Account, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; @@ -308,7 +308,7 @@ pub fn reply(mut deps: DepsMut, env: Env, msg: Reply) -> Result JobStatus::Failed, }; - let finished_job = JobQueueInstance::finalize(&mut deps, env.clone(), msg.id, new_status)?; + let finished_job = JobQueue::finalize(&mut deps, env.clone(), msg.id, new_status)?; let res_attrs = match msg.result { SubMsgResult::Err(e) => vec![Attribute::new( @@ -407,7 +407,7 @@ pub fn reply(mut deps: DepsMut, env: Env, msg: Reply) -> Result record.1, }; - let job = JobQueueInstance::add( + let job = JobQueue::add( &mut deps, Job { id: state.current_job_id, @@ -125,8 +125,7 @@ pub fn delete_job( data: DeleteJobMsg, ) -> Result { let config = CONFIG.load(deps.storage)?; - let state = STATE.load(deps.storage)?; - let job = JobQueueInstance::get(&deps, data.id.into())?; + let job = JobQueue::get(&deps, data.id.into())?; if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); @@ -139,7 +138,7 @@ pub fn delete_job( let account = ACCOUNTS().load(deps.storage, info.sender)?; let _new_job = - JobQueueInstance::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Cancelled)?; + JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Cancelled)?; let fee = job.reward * Uint128::from(config.cancellation_fee_percentage) / Uint128::new(100); @@ -172,7 +171,7 @@ pub fn update_job( info: MessageInfo, data: UpdateJobMsg, ) -> Result { - let job = JobQueueInstance::get(&deps, data.id.into())?; + let job = JobQueue::get(&deps, data.id.into())?; let config = CONFIG.load(deps.storage)?; if info.sender != job.owner { @@ -191,7 +190,7 @@ pub fn update_job( return Err(ContractError::NameTooShort {}); } - let job = JobQueueInstance::update(&mut deps, env.clone(), data)?; + let job = JobQueue::update(&mut deps, env.clone(), data)?; let fee = added_reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); @@ -251,9 +250,8 @@ pub fn execute_job( data: ExecuteJobMsg, ) -> Result { let _config = CONFIG.load(deps.storage)?; - let state = STATE.load(deps.storage)?; let config = CONFIG.load(deps.storage)?; - let job = JobQueueInstance::get(&deps, data.id.into())?; + let job = JobQueue::get(&deps, data.id.into())?; let account = ACCOUNTS().load(deps.storage, job.owner.clone())?; if !ACCOUNTS().has(deps.storage, info.sender.clone()) { @@ -288,7 +286,7 @@ pub fn execute_job( if let Err(e) = resolution { attrs.push(Attribute::new("job_condition_status", "invalid")); attrs.push(Attribute::new("error", e.to_string())); - JobQueueInstance::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Failed)?; + JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Failed)?; } else { attrs.push(Attribute::new("job_condition_status", "valid")); if !resolution? { @@ -338,7 +336,7 @@ pub fn evict_job( ) -> Result { let config = CONFIG.load(deps.storage)?; let state = STATE.load(deps.storage)?; - let job = JobQueueInstance::get(&deps, data.id.into())?; + let job = JobQueue::get(&deps, data.id.into())?; let account = ACCOUNTS().load(deps.storage, job.owner.clone())?; let account_amount = deps @@ -388,10 +386,10 @@ pub fn evict_job( funds: vec![], }), ); - job_status = JobQueueInstance::sync(&mut deps, env.clone(), job.clone())?.status; + job_status = JobQueue::sync(&mut deps, env.clone(), job.clone())?.status; } else { job_status = - JobQueueInstance::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Evicted)? + JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Evicted)? .status; cosmos_msgs.append(&mut vec![ diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 62c159d4..39cb8ae0 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -74,23 +74,10 @@ pub const QUERY_PAGE_SIZE: u32 = 50; pub const CONFIG: Item = Item::new("config"); pub const STATE: Item = Item::new("state"); -pub trait JobQueue { - fn add(deps: &mut DepsMut, job: Job) -> Result; - fn get(deps: &DepsMut, job_id: u64) -> Result; - fn sync(deps: &mut DepsMut, env: Env, job: Job) -> Result; - fn update(deps: &mut DepsMut, env: Env, data: UpdateJobMsg) -> Result; - fn finalize( - deps: &mut DepsMut, - env: Env, - job_id: u64, - status: JobStatus, - ) -> Result; -} - -pub struct JobQueueInstance; +pub struct JobQueue; -impl JobQueue for JobQueueInstance { - fn add(deps: &mut DepsMut, job: Job) -> Result { +impl JobQueue { + pub fn add(deps: &mut DepsMut, job: Job) -> Result { let state = STATE.load(deps.storage)?; let job = PENDING_JOBS().update(deps.storage, state.current_job_id.u64(), |s| match s { @@ -109,13 +96,13 @@ impl JobQueue for JobQueueInstance { Ok(job) } - fn get(deps: &DepsMut, job_id: u64) -> Result { + pub fn get(deps: &DepsMut, job_id: u64) -> Result { let job = PENDING_JOBS().load(deps.storage, job_id)?; Ok(job) } - fn sync(deps: &mut DepsMut, env: Env, job: Job) -> Result { + pub fn sync(deps: &mut DepsMut, env: Env, job: Job) -> Result { PENDING_JOBS().update(deps.storage, job.id.u64(), |j| match j { None => Err(ContractError::JobDoesNotExist {}), Some(job) => Ok(Job { @@ -138,7 +125,7 @@ impl JobQueue for JobQueueInstance { }) } - fn update(deps: &mut DepsMut, env: Env, data: UpdateJobMsg) -> Result { + pub fn update(deps: &mut DepsMut, env: Env, data: UpdateJobMsg) -> Result { let config = CONFIG.load(deps.storage)?; let added_reward: Uint128 = data.added_reward.unwrap_or(Uint128::new(0)); @@ -168,7 +155,7 @@ impl JobQueue for JobQueueInstance { }) } - fn finalize( + pub fn finalize( deps: &mut DepsMut, env: Env, job_id: u64, From cc00a21b41bf0ed30c63376e2eaa593969e2025b Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 13 Sep 2023 17:34:14 +0200 Subject: [PATCH 014/133] add query_state to controller --- contracts/warp-controller/src/contract.rs | 3 +++ contracts/warp-controller/src/query/controller.rs | 9 +++++++-- packages/controller/src/lib.rs | 12 +++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index b0ebdb69..a0502615 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -116,6 +116,9 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryConfig(data) => { to_binary(&query::controller::query_config(deps, env, data)?) } + QueryMsg::QueryState(data) => { + to_binary(&query::controller::query_state(deps, env, data)?) + } } } diff --git a/contracts/warp-controller/src/query/controller.rs b/contracts/warp-controller/src/query/controller.rs index 6a006842..630c5118 100644 --- a/contracts/warp-controller/src/query/controller.rs +++ b/contracts/warp-controller/src/query/controller.rs @@ -1,8 +1,13 @@ -use crate::state::CONFIG; -use controller::{ConfigResponse, QueryConfigMsg}; +use crate::state::{CONFIG, STATE}; +use controller::{ConfigResponse, QueryConfigMsg, QueryStateMsg, StateResponse}; use cosmwasm_std::{Deps, Env, StdResult}; pub fn query_config(deps: Deps, _env: Env, _data: QueryConfigMsg) -> StdResult { let config = CONFIG.load(deps.storage)?; Ok(ConfigResponse { config }) } + +pub fn query_state(deps: Deps, _env: Env, _data: QueryStateMsg) -> StdResult { + let state = STATE.load(deps.storage)?; + Ok(StateResponse { state }) +} diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index e0c0ad4c..9c47f32e 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -120,17 +120,27 @@ pub enum QueryMsg { #[returns(ConfigResponse)] QueryConfig(QueryConfigMsg), + + #[returns(StateResponse)] + QueryState(QueryStateMsg), } #[cw_serde] pub struct QueryConfigMsg {} -//responses #[cw_serde] pub struct ConfigResponse { pub config: Config, } +#[cw_serde] +pub struct QueryStateMsg {} + +#[cw_serde] +pub struct StateResponse { + pub state: State, +} + //migrate//{"resolver_address":"terra1a8dxkrapwj4mkpfnrv7vahd0say0lxvd0ft6qv","warp_account_code_id":"10081"} #[cw_serde] pub struct MigrateMsg { From 616a6a5d7cf274d98bc683da37ed945120c662ae Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 13 Sep 2023 17:34:25 +0200 Subject: [PATCH 015/133] schema --- contracts/warp-controller/examples/warp-controller-schema.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/warp-controller/examples/warp-controller-schema.rs b/contracts/warp-controller/examples/warp-controller-schema.rs index 4876d400..9023652b 100644 --- a/contracts/warp-controller/examples/warp-controller-schema.rs +++ b/contracts/warp-controller/examples/warp-controller-schema.rs @@ -4,7 +4,7 @@ use std::fs::create_dir_all; use controller::{ account::{AccountResponse, AccountsResponse}, job::{JobResponse, JobsResponse}, - QueryMsg, {Config, ConfigResponse, ExecuteMsg, InstantiateMsg}, + QueryMsg, {Config, ConfigResponse, ExecuteMsg, InstantiateMsg}, State, StateResponse, }; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; @@ -19,6 +19,8 @@ fn main() { export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(Config), &out_dir); export_schema(&schema_for!(ConfigResponse), &out_dir); + export_schema(&schema_for!(State), &out_dir); + export_schema(&schema_for!(StateResponse), &out_dir); export_schema(&schema_for!(JobResponse), &out_dir); export_schema(&schema_for!(JobsResponse), &out_dir); export_schema(&schema_for!(AccountResponse), &out_dir); From 4a0c8318e9758ef62779fc0cc0852ecf82593e3d Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 13 Sep 2023 18:32:15 +0200 Subject: [PATCH 016/133] fmt --- .../warp-controller/examples/warp-controller-schema.rs | 2 +- contracts/warp-controller/src/contract.rs | 4 +--- contracts/warp-controller/src/execute/job.rs | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/contracts/warp-controller/examples/warp-controller-schema.rs b/contracts/warp-controller/examples/warp-controller-schema.rs index 9023652b..6ab9debf 100644 --- a/contracts/warp-controller/examples/warp-controller-schema.rs +++ b/contracts/warp-controller/examples/warp-controller-schema.rs @@ -4,7 +4,7 @@ use std::fs::create_dir_all; use controller::{ account::{AccountResponse, AccountsResponse}, job::{JobResponse, JobsResponse}, - QueryMsg, {Config, ConfigResponse, ExecuteMsg, InstantiateMsg}, State, StateResponse, + QueryMsg, State, StateResponse, {Config, ConfigResponse, ExecuteMsg, InstantiateMsg}, }; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index a0502615..70858811 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -116,9 +116,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryConfig(data) => { to_binary(&query::controller::query_config(deps, env, data)?) } - QueryMsg::QueryState(data) => { - to_binary(&query::controller::query_state(deps, env, data)?) - } + QueryMsg::QueryState(data) => to_binary(&query::controller::query_state(deps, env, data)?), } } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 774604c5..18a41776 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -137,8 +137,7 @@ pub fn delete_job( let account = ACCOUNTS().load(deps.storage, info.sender)?; - let _new_job = - JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Cancelled)?; + let _new_job = JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Cancelled)?; let fee = job.reward * Uint128::from(config.cancellation_fee_percentage) / Uint128::new(100); @@ -389,8 +388,7 @@ pub fn evict_job( job_status = JobQueue::sync(&mut deps, env.clone(), job.clone())?.status; } else { job_status = - JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Evicted)? - .status; + JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Evicted)?.status; cosmos_msgs.append(&mut vec![ //send reward minus fee back to account From 8c7a32ca3dac2177a49b0b8b945b7e66474ba49e Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 13 Sep 2023 18:34:45 +0200 Subject: [PATCH 017/133] clippy --- contracts/warp-controller/src/execute/job.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 18a41776..4a4e2239 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -137,7 +137,7 @@ pub fn delete_job( let account = ACCOUNTS().load(deps.storage, info.sender)?; - let _new_job = JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Cancelled)?; + let _new_job = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Cancelled)?; let fee = job.reward * Uint128::from(config.cancellation_fee_percentage) / Uint128::new(100); @@ -285,7 +285,7 @@ pub fn execute_job( if let Err(e) = resolution { attrs.push(Attribute::new("job_condition_status", "invalid")); attrs.push(Attribute::new("error", e.to_string())); - JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Failed)?; + JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Failed)?; } else { attrs.push(Attribute::new("job_condition_status", "valid")); if !resolution? { @@ -385,10 +385,10 @@ pub fn evict_job( funds: vec![], }), ); - job_status = JobQueue::sync(&mut deps, env.clone(), job.clone())?.status; + job_status = JobQueue::sync(&mut deps, env, job.clone())?.status; } else { job_status = - JobQueue::finalize(&mut deps, env.clone(), job.id.into(), JobStatus::Evicted)?.status; + JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; cosmos_msgs.append(&mut vec![ //send reward minus fee back to account From 9632381d127a8888ead47f61ecae813f12e42162 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 21 Sep 2023 19:33:13 +0200 Subject: [PATCH 018/133] add passthrough account msgs --- contracts/warp-controller/src/execute/account.rs | 1 + contracts/warp-controller/src/execute/job.rs | 13 ++++++++++++- packages/account/src/lib.rs | 1 + packages/controller/src/job.rs | 3 ++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/contracts/warp-controller/src/execute/account.rs b/contracts/warp-controller/src/execute/account.rs index 04cdeda8..2225898a 100644 --- a/contracts/warp-controller/src/execute/account.rs +++ b/contracts/warp-controller/src/execute/account.rs @@ -87,6 +87,7 @@ pub fn create_account( msg: to_binary(&account::InstantiateMsg { owner: info.sender.to_string(), funds: data.funds, + msgs: data.msgs, })?, funds: info.funds, label: info.sender.to_string(), diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index fe104dda..dc04b896 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -113,6 +113,16 @@ pub fn create_job( }, ]; + let mut account_msgs: Vec = vec![]; + + if let Some(msgs) = data.account_msgs { + account_msgs = vec![WasmMsg::Execute { + contract_addr: account.account.to_string(), + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs }))?, + funds: vec![], + }]; + } + Ok(Response::new() .add_messages(reward_send_msgs) .add_attribute("action", "create_job") @@ -124,7 +134,8 @@ pub fn create_job( .add_attribute("job_msgs", serde_json_wasm::to_string(&job.msgs)?) .add_attribute("job_reward", job.reward) .add_attribute("job_creation_fee", fee) - .add_attribute("job_last_updated_time", job.last_update_time)) + .add_attribute("job_last_updated_time", job.last_update_time) + .add_messages(account_msgs)) } pub fn delete_job( diff --git a/packages/account/src/lib.rs b/packages/account/src/lib.rs index 2674516b..b8f7d3f9 100644 --- a/packages/account/src/lib.rs +++ b/packages/account/src/lib.rs @@ -13,6 +13,7 @@ pub struct Config { #[cw_serde] pub struct InstantiateMsg { pub owner: String, + pub msgs: Option>, pub funds: Option>, } diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 37939f30..29746f07 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -1,6 +1,6 @@ use crate::account::AssetInfo; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Uint128, Uint64}; +use cosmwasm_std::{Addr, CosmosMsg, Uint128, Uint64}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use strum_macros::Display; @@ -66,6 +66,7 @@ pub struct CreateJobMsg { pub requeue_on_evict: bool, pub reward: Uint128, pub assets_to_withdraw: Option>, + pub account_msgs: Option>, } #[cw_serde] From 86f78d1672df5e42009b4cbd024acf12f60cf54b Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 21 Sep 2023 19:33:26 +0200 Subject: [PATCH 019/133] add funds on account instantiate --- contracts/warp-account/src/contract.rs | 53 +++++++++++++++++++++++++- packages/controller/src/account.rs | 3 +- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index daa191d5..ce5e2642 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -4,7 +4,9 @@ use account::{ Config, ExecuteMsg, IbcTransferMsg, InstantiateMsg, MigrateMsg, QueryMsg, TimeoutBlock, WithdrawAssetsMsg, }; -use controller::account::{AssetInfo, Cw721ExecuteMsg}; +use controller::account::{ + AssetInfo, Cw721ExecuteMsg, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, +}; use cosmwasm_std::CosmosMsg::Stargate; use cosmwasm_std::{ entry_point, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, @@ -28,12 +30,59 @@ pub fn instantiate( warp_addr: info.sender, }, )?; + + let cw_funds_vec = match msg.funds.clone() { + None => { + vec![] + } + Some(funds) => funds, + }; + + let mut fund_msgs_vec: Vec = vec![]; + + if !info.funds.is_empty() { + fund_msgs_vec.push(CosmosMsg::Bank(BankMsg::Send { + to_address: env.contract.address.to_string(), + amount: info.funds.clone(), + })) + } + + for cw_fund in &cw_funds_vec { + fund_msgs_vec.push(CosmosMsg::Wasm(match cw_fund { + Fund::Cw20(cw20_fund) => WasmMsg::Execute { + contract_addr: deps + .api + .addr_validate(&cw20_fund.contract_addr)? + .to_string(), + msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { + owner: msg.owner.clone().to_string(), + recipient: env.contract.address.clone().to_string(), + amount: cw20_fund.amount, + }))?, + funds: vec![], + }, + Fund::Cw721(cw721_fund) => WasmMsg::Execute { + contract_addr: deps + .api + .addr_validate(&cw721_fund.contract_addr)? + .to_string(), + msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { + recipient: env.contract.address.clone().to_string(), + token_id: cw721_fund.token_id.clone(), + }))?, + funds: vec![], + }, + })); + } + Ok(Response::new() .add_attribute("action", "instantiate") .add_attribute("contract_addr", env.contract.address) .add_attribute("owner", msg.owner) .add_attribute("funds", serde_json_wasm::to_string(&info.funds)?) - .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?)) + .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?) + .add_messages(fund_msgs_vec) + .add_messages(msg.msgs.unwrap_or(vec![]))) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index aa5df459..2714a1d4 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -1,9 +1,10 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::{Addr, CosmosMsg, Uint128}; #[cw_serde] pub struct CreateAccountMsg { pub funds: Option>, + pub msgs: Option>, } #[cw_serde] From 3da96cc9d0d78e78ba420258fd068f7f0a6a83bb Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 21 Sep 2023 19:47:40 +0200 Subject: [PATCH 020/133] remove cw funds from instantiate as they're already present in rpely --- contracts/warp-account/src/contract.rs | 39 +------------------------- 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index ce5e2642..fb718bc3 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -4,9 +4,7 @@ use account::{ Config, ExecuteMsg, IbcTransferMsg, InstantiateMsg, MigrateMsg, QueryMsg, TimeoutBlock, WithdrawAssetsMsg, }; -use controller::account::{ - AssetInfo, Cw721ExecuteMsg, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, -}; +use controller::account::{AssetInfo, Cw721ExecuteMsg}; use cosmwasm_std::CosmosMsg::Stargate; use cosmwasm_std::{ entry_point, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, @@ -31,13 +29,6 @@ pub fn instantiate( }, )?; - let cw_funds_vec = match msg.funds.clone() { - None => { - vec![] - } - Some(funds) => funds, - }; - let mut fund_msgs_vec: Vec = vec![]; if !info.funds.is_empty() { @@ -47,34 +38,6 @@ pub fn instantiate( })) } - for cw_fund in &cw_funds_vec { - fund_msgs_vec.push(CosmosMsg::Wasm(match cw_fund { - Fund::Cw20(cw20_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw20_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { - owner: msg.owner.clone().to_string(), - recipient: env.contract.address.clone().to_string(), - amount: cw20_fund.amount, - }))?, - funds: vec![], - }, - Fund::Cw721(cw721_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw721_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { - recipient: env.contract.address.clone().to_string(), - token_id: cw721_fund.token_id.clone(), - }))?, - funds: vec![], - }, - })); - } - Ok(Response::new() .add_attribute("action", "instantiate") .add_attribute("contract_addr", env.contract.address) From eea9f99c8b174d2a5215187ebb0f8fd92dbcb716 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 21 Sep 2023 19:58:28 +0200 Subject: [PATCH 021/133] fix tsts --- contracts/warp-account/src/tests.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contracts/warp-account/src/tests.rs b/contracts/warp-account/src/tests.rs index 24ee92bc..f5c0a193 100644 --- a/contracts/warp-account/src/tests.rs +++ b/contracts/warp-account/src/tests.rs @@ -20,6 +20,7 @@ fn test_execute_controller() { InstantiateMsg { owner: "vlad".to_string(), funds: None, + msgs: None, }, ); @@ -142,6 +143,7 @@ fn test_execute_owner() { InstantiateMsg { owner: "vlad".to_string(), funds: None, + msgs: None, }, ); @@ -266,6 +268,7 @@ fn test_execute_unauth() { InstantiateMsg { owner: "vlad".to_string(), funds: None, + msgs: None, }, ); From 86662768f8a614d9030ae812093a9472bbbb8fd5 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 22 Sep 2023 17:11:58 +0200 Subject: [PATCH 022/133] remove useless code --- contracts/warp-account/src/contract.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index fb718bc3..e3c8beb0 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -29,22 +29,12 @@ pub fn instantiate( }, )?; - let mut fund_msgs_vec: Vec = vec![]; - - if !info.funds.is_empty() { - fund_msgs_vec.push(CosmosMsg::Bank(BankMsg::Send { - to_address: env.contract.address.to_string(), - amount: info.funds.clone(), - })) - } - Ok(Response::new() .add_attribute("action", "instantiate") .add_attribute("contract_addr", env.contract.address) .add_attribute("owner", msg.owner) .add_attribute("funds", serde_json_wasm::to_string(&info.funds)?) .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?) - .add_messages(fund_msgs_vec) .add_messages(msg.msgs.unwrap_or(vec![]))) } From 158fafd93760428689582d5b885a17c830205253 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 22 Sep 2023 17:21:49 +0200 Subject: [PATCH 023/133] add account_msgs in reply --- contracts/warp-account/src/contract.rs | 2 +- contracts/warp-controller/src/contract.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index e3c8beb0..c4770d86 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -35,7 +35,7 @@ pub fn instantiate( .add_attribute("owner", msg.owner) .add_attribute("funds", serde_json_wasm::to_string(&info.funds)?) .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?) - .add_messages(msg.msgs.unwrap_or(vec![]))) + .add_attribute("account_msgs", serde_json_wasm::to_string(&msg.msgs)?)) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 84f1d691..24571b08 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -242,6 +242,16 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result> = serde_json_wasm::from_str( + &event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "account_msgs") + .ok_or_else(|| StdError::generic_err("cannot find `account_msgs` attribute"))? + .value, + )?; + let cw_funds_vec = match cw_funds { None => { vec![] @@ -279,6 +289,12 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result Date: Sat, 23 Sep 2023 23:44:03 -0700 Subject: [PATCH 024/133] wip --- contracts/warp-controller/src/contract.rs | 136 +-- .../warp-controller/src/execute/controller.rs | 14 +- contracts/warp-controller/src/execute/job.rs | 1 + contracts/warp-resolver/src/contract.rs | 11 +- contracts/warp-resolver/src/tests.rs | 873 +++++++++--------- contracts/warp-resolver/src/util/condition.rs | 82 +- contracts/warp-resolver/src/util/variable.rs | 407 +++++--- packages/resolver/src/condition.rs | 13 + packages/resolver/src/lib.rs | 2 + packages/resolver/src/variable.rs | 13 +- 10 files changed, 922 insertions(+), 630 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 84f1d691..b241bbb2 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -119,74 +119,74 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } } -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { - //STATE - #[cw_serde] - pub struct V1State { - pub current_job_id: Uint64, - pub current_template_id: Uint64, - pub q: Uint64, - } - - const V1STATE: Item = Item::new("state"); - let v1_state = V1STATE.load(deps.storage)?; - - STATE.save( - deps.storage, - &State { - current_job_id: v1_state.current_job_id, - q: v1_state.q, - }, - )?; - - //CONFIG - #[cw_serde] - pub struct V1Config { - pub owner: Addr, - pub fee_denom: String, - pub fee_collector: Addr, - pub warp_account_code_id: Uint64, - pub minimum_reward: Uint128, - pub creation_fee_percentage: Uint64, - pub cancellation_fee_percentage: Uint64, - // maximum time for evictions - pub t_max: Uint64, - // minimum time for evictions - pub t_min: Uint64, - // maximum fee for evictions - pub a_max: Uint128, - // minimum fee for evictions - pub a_min: Uint128, - // maximum length of queue modifier for evictions - pub q_max: Uint64, - } - - const V1CONFIG: Item = Item::new("config"); - - let v1_config = V1CONFIG.load(deps.storage)?; - - CONFIG.save( - deps.storage, - &Config { - owner: v1_config.owner, - fee_denom: v1_config.fee_denom, - fee_collector: v1_config.fee_collector, - warp_account_code_id: msg.warp_account_code_id, - minimum_reward: v1_config.minimum_reward, - creation_fee_percentage: v1_config.creation_fee_percentage, - cancellation_fee_percentage: v1_config.cancellation_fee_percentage, - resolver_address: deps.api.addr_validate(&msg.resolver_address)?, - t_max: v1_config.t_max, - t_min: v1_config.t_min, - a_max: v1_config.a_max, - a_min: v1_config.a_min, - q_max: v1_config.q_max, - }, - )?; - - Ok(Response::new()) -} +// #[cfg_attr(not(feature = "library"), entry_point)] +// pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { +// //STATE +// #[cw_serde] +// pub struct V1State { +// pub current_job_id: Uint64, +// pub current_template_id: Uint64, +// pub q: Uint64, +// } + +// const V1STATE: Item = Item::new("state"); +// let v1_state = V1STATE.load(deps.storage)?; + +// STATE.save( +// deps.storage, +// &State { +// current_job_id: v1_state.current_job_id, +// q: v1_state.q, +// }, +// )?; + +// //CONFIG +// #[cw_serde] +// pub struct V1Config { +// pub owner: Addr, +// pub fee_denom: String, +// pub fee_collector: Addr, +// pub warp_account_code_id: Uint64, +// pub minimum_reward: Uint128, +// pub creation_fee_percentage: Uint64, +// pub cancellation_fee_percentage: Uint64, +// // maximum time for evictions +// pub t_max: Uint64, +// // minimum time for evictions +// pub t_min: Uint64, +// // maximum fee for evictions +// pub a_max: Uint128, +// // minimum fee for evictions +// pub a_min: Uint128, +// // maximum length of queue modifier for evictions +// pub q_max: Uint64, +// } + +// const V1CONFIG: Item = Item::new("config"); + +// let v1_config = V1CONFIG.load(deps.storage)?; + +// CONFIG.save( +// deps.storage, +// &Config { +// owner: v1_config.owner, +// fee_denom: v1_config.fee_denom, +// fee_collector: v1_config.fee_collector, +// warp_account_code_id: msg.warp_account_code_id, +// minimum_reward: v1_config.minimum_reward, +// creation_fee_percentage: v1_config.creation_fee_percentage, +// cancellation_fee_percentage: v1_config.cancellation_fee_percentage, +// resolver_address: deps.api.addr_validate(&msg.resolver_address)?, +// t_max: v1_config.t_max, +// t_min: v1_config.t_min, +// a_max: v1_config.a_max, +// a_min: v1_config.a_min, +// q_max: v1_config.q_max, +// }, +// )?; + +// Ok(Response::new()) +// } #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { diff --git a/contracts/warp-controller/src/execute/controller.rs b/contracts/warp-controller/src/execute/controller.rs index 221ec294..b939e3e1 100644 --- a/contracts/warp-controller/src/execute/controller.rs +++ b/contracts/warp-controller/src/execute/controller.rs @@ -9,10 +9,10 @@ use cosmwasm_std::{ to_binary, Addr, DepsMut, Env, MessageInfo, Order, Response, Uint128, Uint64, WasmMsg, }; use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, MultiIndex, UniqueIndex}; -use resolver::condition::Condition; +use resolver::condition::{Condition, StringValue}; use resolver::variable::{ - ExternalExpr, ExternalVariable, QueryExpr, QueryVariable, StaticVariable, UpdateFn, Variable, - VariableKind, + ExternalExpr, ExternalVariable, FnValue, QueryExpr, QueryVariable, StaticVariable, UpdateFn, + Variable, VariableKind, }; //JOBS @@ -237,7 +237,9 @@ pub fn migrate_pending_jobs( kind: v.kind, name: v.name, encode: false, - value: v.value, + init_fn: FnValue::String(StringValue::Simple(v.value.clone())), + reinitialize: false, + value: Some(v.value.clone()), update_fn: v.update_fn, }), V1Variable::External(v) => Variable::External(ExternalVariable { @@ -339,7 +341,9 @@ pub fn migrate_finished_jobs( kind: v.kind, name: v.name, encode: false, - value: v.value, + init_fn: FnValue::String(StringValue::Simple(v.value.clone())), + reinitialize: false, + value: Some(v.value.clone()), update_fn: v.update_fn, }), V1Variable::External(v) => Variable::External(ExternalVariable { diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index fe104dda..bf6be7a2 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -330,6 +330,7 @@ pub fn execute_job( &resolver::QueryMsg::QueryHydrateVars(resolver::QueryHydrateVarsMsg { vars: job.vars, external_inputs: data.external_inputs, + warp_account_addr: Some(account.account.to_string()), }), )?; diff --git a/contracts/warp-resolver/src/contract.rs b/contracts/warp-resolver/src/contract.rs index 769f65a9..f7d36a70 100644 --- a/contracts/warp-resolver/src/contract.rs +++ b/contracts/warp-resolver/src/contract.rs @@ -104,6 +104,7 @@ pub fn execute_hydrate_vars( QueryHydrateVarsMsg { vars: data.vars, external_inputs: data.external_inputs, + warp_account_addr: data.warp_account_addr, }, )?; @@ -241,8 +242,14 @@ fn query_hydrate_vars(deps: Deps, env: Env, data: QueryHydrateVarsMsg) -> StdRes let vars: Vec = serde_json_wasm::from_str(&data.vars).map_err(|e| StdError::generic_err(e.to_string()))?; serde_json_wasm::to_string( - &hydrate_vars(deps, env, vars, data.external_inputs) - .map_err(|e| StdError::generic_err(e.to_string()))?, + &hydrate_vars( + deps, + env, + vars, + data.external_inputs, + data.warp_account_addr, + ) + .map_err(|e| StdError::generic_err(e.to_string()))?, ) .map_err(|e| StdError::generic_err(e.to_string())) } diff --git a/contracts/warp-resolver/src/tests.rs b/contracts/warp-resolver/src/tests.rs index cff5e899..fa415f97 100644 --- a/contracts/warp-resolver/src/tests.rs +++ b/contracts/warp-resolver/src/tests.rs @@ -1,429 +1,444 @@ -use schemars::_serde_json::json; - -use crate::util::variable::{hydrate_msgs, hydrate_vars}; - -use cosmwasm_std::{testing::mock_env, WasmQuery}; -use cosmwasm_std::{to_binary, BankQuery, Binary, ContractResult, CosmosMsg, OwnedDeps, WasmMsg}; - -use crate::contract::query; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::testing::{mock_info, MockApi, MockQuerier, MockStorage}; -use cosmwasm_std::{from_slice, Empty, Querier, QueryRequest, SystemError, SystemResult}; - -use resolver::variable::{QueryExpr, QueryVariable, StaticVariable, Variable, VariableKind}; -use resolver::{QueryMsg, QueryValidateJobCreationMsg}; -use std::marker::PhantomData; - -#[test] -fn test() { - let deps = mock_dependencies(); - let _info = mock_info("vlad", &[]); - let env = mock_env(); - let msg = QueryValidateJobCreationMsg { - condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), - terminate_condition: None, - vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), - msgs: "[{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}]".to_string(), - }; - let obj = serde_json_wasm::to_string(&vec!["{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}"]).unwrap(); - - let _msg1 = QueryValidateJobCreationMsg { - condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), - terminate_condition: None, - vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), - msgs: obj.clone(), - }; - - println!("{}", serde_json_wasm::to_string(&obj).unwrap()); - - let test = query(deps.as_ref(), env, QueryMsg::QueryValidateJobCreation(msg)).unwrap(); - println!("{}", test) -} - -#[test] -fn test_vars() { - let test_msg = "{\"execute\":{\"test\":\"$WARPVAR.test\"}}".to_string(); - - let _idx = test_msg.find("\"$WARPVAR\""); - - let _new_str = test_msg.replace("\"$WARPVAR.test\"", "\"input\""); -} - -pub fn mock_dependencies() -> OwnedDeps { - let custom_querier: WasmMockQuerier = WasmMockQuerier::new(MockQuerier::new(&[])); - - OwnedDeps { - api: MockApi::default(), - storage: MockStorage::default(), - querier: custom_querier, - custom_query_type: PhantomData, - } -} - -pub struct WasmMockQuerier { - base: MockQuerier, -} - -impl Querier for WasmMockQuerier { - fn raw_query(&self, bin_request: &[u8]) -> SystemResult> { - let request: QueryRequest = match from_slice(bin_request) { - Ok(v) => v, - Err(e) => { - return SystemResult::Err(SystemError::InvalidRequest { - error: format!("Parsing query request: {}", e), - request: bin_request.into(), - }); - } - }; - self.handle_query(&request) - } -} - -impl WasmMockQuerier { - pub fn handle_query( - &self, - request: &QueryRequest, - ) -> SystemResult> { - match &request { - QueryRequest::Wasm(WasmQuery::Smart { - contract_addr, - msg: _, - }) => { - // Mock logic for the Wasm::Smart case - // Here for simplicity, we return the contract_addr and msg as is. - - // Mock logic for the Wasm::Smart case - // Here we return a JSON object with "address" and "msg" fields. - let response: String = json!({ - "address": contract_addr, - "msg": "Mock message" - }) - .to_string(); - - SystemResult::Ok(ContractResult::Ok(to_binary(&response).unwrap())) - } - QueryRequest::Bank(BankQuery::Balance { - address: contract_addr, - denom: _, - }) => SystemResult::Ok(ContractResult::Ok( - to_binary(&contract_addr.to_string()).unwrap(), - )), - _ => self.base.handle_query(request), - } - } -} - -impl WasmMockQuerier { - pub fn new(base: MockQuerier) -> Self { - WasmMockQuerier { base } - } -} - -#[test] -fn test_hydrate_vars_nested_variables_binary_json() { - let deps = mock_dependencies(); - let env = mock_env(); - - let var5 = Variable::Static(StaticVariable { - kind: VariableKind::String, - name: "var5".to_string(), - encode: false, - value: "contract_addr".to_string(), - update_fn: None, - }); - - let var4 = Variable::Static(StaticVariable { - kind: VariableKind::String, - name: "var4".to_string(), - encode: false, - value: "$warp.variable.var5".to_string(), - update_fn: None, - }); - - let var3 = Variable::Query(QueryVariable { - name: "var3".to_string(), - kind: VariableKind::Json, - init_fn: QueryExpr { - selector: "$".to_string(), - query: QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: "contract_addr".to_string(), - msg: Binary::from(r#"{"test":"test"}"#.as_bytes()), - }), - }, - value: None, - reinitialize: false, - update_fn: None, - encode: true, - }); - - let var1 = Variable::Query(QueryVariable { - name: "var1".to_string(), - kind: VariableKind::Json, - init_fn: QueryExpr { - selector: "$".to_string(), - query: QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: "contract_addr".to_string(), - msg: Binary::from(r#"{"test":"$warp.variable.var3"}"#.as_bytes()), - }), - }, - value: None, - reinitialize: false, - update_fn: None, - encode: true, - }); - - let var2 = Variable::Query(QueryVariable { - name: "var2".to_string(), - kind: VariableKind::Json, - init_fn: QueryExpr { - selector: "$".to_string(), - query: QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: "$warp.variable.var4".to_string(), - msg: Binary::from(r#"{"test":"$warp.variable.var1"}"#.as_bytes()), - }), - }, - value: None, - reinitialize: false, - update_fn: None, - encode: false, - }); - - let vars = vec![var5, var4, var3, var1, var2]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); - - assert_eq!( - hydrated_vars[4], - Variable::Query(QueryVariable { - name: "var2".to_string(), - kind: VariableKind::Json, - init_fn: QueryExpr { - selector: "$".to_string(), - query: QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: "contract_addr".to_string(), - msg: Binary::from( - r#"{"test":"eyJhZGRyZXNzIjoiY29udHJhY3RfYWRkciIsIm1zZyI6Ik1vY2sgbWVzc2FnZSJ9"}"#.as_bytes() - ), - }), - }, - value: Some(r#"{"address":"contract_addr","msg":"Mock message"}"#.to_string()), - reinitialize: false, - update_fn: None, - encode: false, - }) - ); -} - -#[test] -fn test_hydrate_vars_nested_variables_binary() { - let deps = mock_dependencies(); - let env = mock_env(); - - let var1 = Variable::Static(StaticVariable { - name: "var1".to_string(), - kind: VariableKind::String, - value: "static_value".to_string(), - update_fn: None, - encode: false, - }); - - let init_fn = QueryExpr { - selector: "$".to_string(), - query: QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: "$warp.variable.var1".to_string(), - msg: Binary::from(r#"{"test": "$warp.variable.var1"}"#.as_bytes()), - }), - }; - - let var2 = Variable::Query(QueryVariable { - name: "var2".to_string(), - kind: VariableKind::String, - init_fn, - value: None, - reinitialize: false, - update_fn: None, - encode: false, - }); - - let vars = vec![var1, var2]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); - - assert_eq!( - hydrated_vars[1], - Variable::Query(QueryVariable { - name: "var2".to_string(), - kind: VariableKind::String, - init_fn: QueryExpr { - selector: "$".to_string(), - query: QueryRequest::Wasm(WasmQuery::Smart { - contract_addr: "static_value".to_string(), - msg: Binary::from(r#"{"test": "static_value"}"#.as_bytes()), - }), - }, - value: Some(r#"{"address":"static_value","msg":"Mock message"}"#.to_string()), - reinitialize: false, - update_fn: None, - encode: false, - }) - ); -} -#[test] -fn test_hydrate_vars_nested_variables_non_binary() { - let deps = mock_dependencies(); - let env = mock_env(); - - let var1 = Variable::Static(StaticVariable { - name: "var1".to_string(), - kind: VariableKind::String, - value: "static_value".to_string(), - update_fn: None, - encode: false, - }); - - let init_fn = QueryExpr { - selector: "$".to_string(), - query: QueryRequest::Bank(BankQuery::Balance { - address: "$warp.variable.var1".to_string(), - denom: "denom".to_string(), - }), - }; - - let var2 = Variable::Query(QueryVariable { - name: "var2".to_string(), - kind: VariableKind::String, - init_fn, - value: None, - reinitialize: false, - update_fn: None, - encode: false, - }); - - let vars = vec![var1, var2]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); - - assert_eq!( - hydrated_vars[1], - Variable::Query(QueryVariable { - name: "var2".to_string(), - kind: VariableKind::String, - init_fn: QueryExpr { - selector: "$".to_string(), - query: QueryRequest::Bank(BankQuery::Balance { - address: "static_value".to_string(), - denom: "denom".to_string(), - }), - }, - value: Some("static_value".to_string()), - reinitialize: false, - update_fn: None, - encode: false, - }) - ); -} - -#[test] -fn test_hydrate_static_nested_vars_and_hydrate_msgs() { - let deps = mock_dependencies(); - let env = mock_env(); - - let var1 = Variable::Static(StaticVariable { - name: "var1".to_string(), - kind: VariableKind::String, - value: "static_value_1".to_string(), - update_fn: None, - encode: false, - }); - - #[cw_serde] - struct TestStruct { - test: String, - } - - // ============ TEST HYDRATED VALUE ============ - - let test_msg = TestStruct { - test: format!("$warp.variable.{}", "var1"), - }; - - let json_str = serde_json_wasm::to_string(&test_msg).unwrap(); - - let raw_str = r#"{"test":"static_value_1"}"#.to_string(); - - let var2 = Variable::Static(StaticVariable { - name: "var2".to_string(), - kind: VariableKind::String, - value: json_str.clone(), - update_fn: None, - // when encode is false, value will not be base64 encoded after msgs hydration - encode: false, - }); - - let vars = vec![var1.clone(), var2]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env.clone(), vars, None).unwrap(); - let hydrated_var1 = hydrated_vars[0].clone(); - let hydrated_var2 = hydrated_vars[1].clone(); - match hydrated_var2.clone() { - Variable::Static(static_var) => { - // var3.encode = false doesn't matter here, it only matters when injecting to msgs during msg hydration - assert_eq!(String::from_utf8(static_var.value.into()).unwrap(), raw_str) - } - _ => panic!("Expected static variable"), - }; - - let var3 = Variable::Static(StaticVariable { - name: "var3".to_string(), - kind: VariableKind::String, - value: json_str, - update_fn: None, - // when encode is true, value will be base64 encoded after msgs hydration - encode: true, - }); - - let vars = vec![var1, var3]; - let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); - let hydrated_var3 = hydrated_vars[1].clone(); - match hydrated_var3.clone() { - Variable::Static(static_var) => { - // var3.encode = true doesn't matter here, it only matters when injecting to msgs during msg hydration - assert_eq!(String::from_utf8(static_var.value.into()).unwrap(), raw_str); - } - _ => panic!("Expected static variable"), - }; - - // ============ TEST HYDRATED MSG AND VAR VALUE SHOULD BE ENCODED ACCORDINGLY ============ - - let encoded_val = base64::encode(raw_str.clone()); - assert_eq!(encoded_val, "eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ=="); - let msgs = - r#"[{"wasm":{"execute":{"contract_addr":"$warp.variable.var1","msg":"eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ==","funds":[]}}}, - {"wasm":{"execute":{"contract_addr":"$warp.variable.var3","msg":"$warp.variable.var3","funds":[]}}}]"# - .to_string(); - - let hydrated_msgs = - hydrate_msgs(msgs, vec![hydrated_var1, hydrated_var2, hydrated_var3]).unwrap(); - - assert_eq!( - hydrated_msgs[0], - CosmosMsg::Wasm(WasmMsg::Execute { - // Because var1.encode = false, contract_addr should use the plain text value - contract_addr: "static_value_1".to_string(), - msg: Binary::from(raw_str.as_bytes()), - funds: vec![] - }) - ); - - assert_eq!( - hydrated_msgs[1], - CosmosMsg::Wasm(WasmMsg::Execute { - // Because var3.encode = true, contract_addr should use the encoded value - contract_addr: encoded_val, - // msg is not Binary::from(encoded_val.as_bytes()) appears to be a cosmos msg thing, not a warp thing - msg: Binary::from(raw_str.as_bytes()), - funds: vec![] - }) - ) -} - -#[test] -fn test_test() { - println! {"{}", "[\"{\\\"wasm\\\":{\\\"execute\\\":{\\\"contract_addr\\\":\\\"terra1na348k6rvwxje9jj6ftpsapfeyaejxjeq6tuzdmzysps20l6z23smnlv64\\\",\\\"msg\\\":\\\"eyJleGVjdXRlX3N3YXBfb3BlcmF0aW9ucyI6eyJtYXhfc3ByZWFkIjoiMC4xNSIsIm9wZXJhdGlvbnMiOlt7ImFzdHJvX3N3YXAiOnsib2ZmZXJfYXNzZXRfaW5mbyI6eyJuYXRpdmVfdG9rZW4iOnsiZGVub20iOiJ1bHVuYSJ9fSwiYXNrX2Fzc2V0X2luZm8iOnsidG9rZW4iOnsiY29udHJhY3RfYWRkciI6InRlcnJhMXhndnA2cDBxbWw1M3JlcWR5eGdjbDh0dGwwcGtoMG4ybXR4Mm43dHpmYWhuNmUwdmNhN3MwZzdzZzYifX19fSx7ImFzdHJvX3N3YXAiOnsib2ZmZXJfYXNzZXRfaW5mbyI6eyJ0b2tlbiI6eyJjb250cmFjdF9hZGRyIjoidGVycmExeGd2cDZwMHFtbDUzcmVxZHl4Z2NsOHR0bDBwa2gwbjJtdHgybjd0emZhaG42ZTB2Y2E3czBnN3NnNiJ9fSwiYXNrX2Fzc2V0X2luZm8iOnsidG9rZW4iOnsiY29udHJhY3RfYWRkciI6InRlcnJhMTY3ZHNxa2gyYWx1cng5OTd3bXljdzl5ZGt5dTU0Z3lzd2UzeWdtcnM0bHd1bWUzdm13a3M4cnVxbnYifX19fV0sIm1pbmltdW1fcmVjZWl2ZSI6IjIzNTM2NjEifX0=\\\",\\\"funds\\\":[{\\\"denom\\\":\\\"uluna\\\",\\\"amount\\\":\\\"10000\\\"}]}}}\"]".replace("\\\\", "")} -} +// use schemars::_serde_json::json; + +// use crate::util::variable::{hydrate_msgs, hydrate_vars}; + +// use cosmwasm_std::{testing::mock_env, WasmQuery}; +// use cosmwasm_std::{to_binary, BankQuery, Binary, ContractResult, CosmosMsg, OwnedDeps, WasmMsg}; + +// use crate::contract::query; +// use cosmwasm_schema::cw_serde; +// use cosmwasm_std::testing::{mock_info, MockApi, MockQuerier, MockStorage}; +// use cosmwasm_std::{from_slice, Empty, Querier, QueryRequest, SystemError, SystemResult}; + +// use resolver::variable::{ +// InitFnValue, QueryExpr, QueryVariable, StaticVariable, StringValue, Variable, VariableKind, +// }; +// use resolver::{QueryMsg, QueryValidateJobCreationMsg}; +// use std::marker::PhantomData; + +// #[test] +// fn test() { +// let deps = mock_dependencies(); +// let _info = mock_info("vlad", &[]); +// let env = mock_env(); +// let msg = QueryValidateJobCreationMsg { +// condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), +// terminate_condition: None, +// vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), +// msgs: "[{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}]".to_string(), +// }; +// let obj = serde_json_wasm::to_string(&vec!["{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}"]).unwrap(); + +// let _msg1 = QueryValidateJobCreationMsg { +// condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), +// terminate_condition: None, +// vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), +// msgs: obj.clone(), +// }; + +// println!("{}", serde_json_wasm::to_string(&obj).unwrap()); + +// let test = query(deps.as_ref(), env, QueryMsg::QueryValidateJobCreation(msg)).unwrap(); +// println!("{}", test) +// } + +// #[test] +// fn test_vars() { +// let test_msg = "{\"execute\":{\"test\":\"$WARPVAR.test\"}}".to_string(); + +// let _idx = test_msg.find("\"$WARPVAR\""); + +// let _new_str = test_msg.replace("\"$WARPVAR.test\"", "\"input\""); +// } + +// pub fn mock_dependencies() -> OwnedDeps { +// let custom_querier: WasmMockQuerier = WasmMockQuerier::new(MockQuerier::new(&[])); + +// OwnedDeps { +// api: MockApi::default(), +// storage: MockStorage::default(), +// querier: custom_querier, +// custom_query_type: PhantomData, +// } +// } + +// pub struct WasmMockQuerier { +// base: MockQuerier, +// } + +// impl Querier for WasmMockQuerier { +// fn raw_query(&self, bin_request: &[u8]) -> SystemResult> { +// let request: QueryRequest = match from_slice(bin_request) { +// Ok(v) => v, +// Err(e) => { +// return SystemResult::Err(SystemError::InvalidRequest { +// error: format!("Parsing query request: {}", e), +// request: bin_request.into(), +// }); +// } +// }; +// self.handle_query(&request) +// } +// } + +// impl WasmMockQuerier { +// pub fn handle_query( +// &self, +// request: &QueryRequest, +// ) -> SystemResult> { +// match &request { +// QueryRequest::Wasm(WasmQuery::Smart { +// contract_addr, +// msg: _, +// }) => { +// // Mock logic for the Wasm::Smart case +// // Here for simplicity, we return the contract_addr and msg as is. + +// // Mock logic for the Wasm::Smart case +// // Here we return a JSON object with "address" and "msg" fields. +// let response: String = json!({ +// "address": contract_addr, +// "msg": "Mock message" +// }) +// .to_string(); + +// SystemResult::Ok(ContractResult::Ok(to_binary(&response).unwrap())) +// } +// QueryRequest::Bank(BankQuery::Balance { +// address: contract_addr, +// denom: _, +// }) => SystemResult::Ok(ContractResult::Ok( +// to_binary(&contract_addr.to_string()).unwrap(), +// )), +// _ => self.base.handle_query(request), +// } +// } +// } + +// impl WasmMockQuerier { +// pub fn new(base: MockQuerier) -> Self { +// WasmMockQuerier { base } +// } +// } + +// #[test] +// fn test_hydrate_vars_nested_variables_binary_json() { +// let deps = mock_dependencies(); +// let env = mock_env(); + +// let var5 = Variable::Static(StaticVariable { +// kind: VariableKind::String, +// name: "var5".to_string(), +// encode: false, +// value: None, +// init_fn: InitFnValue::String(StringValue::Simple("contract_addr".to_string())), +// update_fn: None, +// }); + +// let var4 = Variable::Static(StaticVariable { +// kind: VariableKind::String, +// name: "var4".to_string(), +// encode: false, +// value: None, +// init_fn: InitFnValue::String(StringValue::Ref("$warp.variable.var5".to_string())), +// update_fn: None, +// }); + +// let var3 = Variable::Query(QueryVariable { +// name: "var3".to_string(), +// kind: VariableKind::Json, +// init_fn: QueryExpr { +// selector: "$".to_string(), +// query: QueryRequest::Wasm(WasmQuery::Smart { +// contract_addr: "contract_addr".to_string(), +// msg: Binary::from(r#"{"test":"test"}"#.as_bytes()), +// }), +// }, +// value: None, +// reinitialize: false, +// update_fn: None, +// encode: true, +// }); + +// let var1 = Variable::Query(QueryVariable { +// name: "var1".to_string(), +// kind: VariableKind::Json, +// init_fn: QueryExpr { +// selector: "$".to_string(), +// query: QueryRequest::Wasm(WasmQuery::Smart { +// contract_addr: "contract_addr".to_string(), +// msg: Binary::from(r#"{"test":"$warp.variable.var3"}"#.as_bytes()), +// }), +// }, +// value: None, +// reinitialize: false, +// update_fn: None, +// encode: true, +// }); + +// let var2 = Variable::Query(QueryVariable { +// name: "var2".to_string(), +// kind: VariableKind::Json, +// init_fn: QueryExpr { +// selector: "$".to_string(), +// query: QueryRequest::Wasm(WasmQuery::Smart { +// contract_addr: "$warp.variable.var4".to_string(), +// msg: Binary::from(r#"{"test":"$warp.variable.var1"}"#.as_bytes()), +// }), +// }, +// value: None, +// reinitialize: false, +// update_fn: None, +// encode: false, +// }); + +// let vars = vec![var5, var4, var3, var1, var2]; +// let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); + +// assert_eq!( +// hydrated_vars[4], +// Variable::Query(QueryVariable { +// name: "var2".to_string(), +// kind: VariableKind::Json, +// init_fn: QueryExpr { +// selector: "$".to_string(), +// query: QueryRequest::Wasm(WasmQuery::Smart { +// contract_addr: "contract_addr".to_string(), +// msg: Binary::from( +// r#"{"test":"eyJhZGRyZXNzIjoiY29udHJhY3RfYWRkciIsIm1zZyI6Ik1vY2sgbWVzc2FnZSJ9"}"#.as_bytes() +// ), +// }), +// }, +// value: Some(r#"{"address":"contract_addr","msg":"Mock message"}"#.to_string()), +// reinitialize: false, +// update_fn: None, +// encode: false, +// }) +// ); +// } + +// #[test] +// fn test_hydrate_vars_nested_variables_binary() { +// let deps = mock_dependencies(); +// let env = mock_env(); + +// let var1 = Variable::Static(StaticVariable { +// name: "var1".to_string(), +// kind: VariableKind::String, +// value: None, +// init_fn: InitFnValue::String(StringValue::Simple("static_value".to_string())), +// update_fn: None, +// encode: false, +// }); + +// let init_fn = QueryExpr { +// selector: "$".to_string(), +// query: QueryRequest::Wasm(WasmQuery::Smart { +// contract_addr: "$warp.variable.var1".to_string(), +// msg: Binary::from(r#"{"test": "$warp.variable.var1"}"#.as_bytes()), +// }), +// }; + +// let var2 = Variable::Query(QueryVariable { +// name: "var2".to_string(), +// kind: VariableKind::String, +// init_fn, +// value: None, +// reinitialize: false, +// update_fn: None, +// encode: false, +// }); + +// let vars = vec![var1, var2]; +// let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); + +// assert_eq!( +// hydrated_vars[1], +// Variable::Query(QueryVariable { +// name: "var2".to_string(), +// kind: VariableKind::String, +// init_fn: QueryExpr { +// selector: "$".to_string(), +// query: QueryRequest::Wasm(WasmQuery::Smart { +// contract_addr: "static_value".to_string(), +// msg: Binary::from(r#"{"test": "static_value"}"#.as_bytes()), +// }), +// }, +// value: Some(r#"{"address":"static_value","msg":"Mock message"}"#.to_string()), +// reinitialize: false, +// update_fn: None, +// encode: false, +// }) +// ); +// } +// #[test] +// fn test_hydrate_vars_nested_variables_non_binary() { +// let deps = mock_dependencies(); +// let env = mock_env(); + +// let var1 = Variable::Static(StaticVariable { +// name: "var1".to_string(), +// kind: VariableKind::String, +// value: None, +// init_fn: InitFnValue::String(StringValue::Simple("static_value".to_string())), +// update_fn: None, +// encode: false, +// }); + +// let init_fn = QueryExpr { +// selector: "$".to_string(), +// query: QueryRequest::Bank(BankQuery::Balance { +// address: "$warp.variable.var1".to_string(), +// denom: "denom".to_string(), +// }), +// }; + +// let var2 = Variable::Query(QueryVariable { +// name: "var2".to_string(), +// kind: VariableKind::String, +// init_fn, +// value: None, +// reinitialize: false, +// update_fn: None, +// encode: false, +// }); + +// let vars = vec![var1, var2]; +// let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); + +// assert_eq!( +// hydrated_vars[1], +// Variable::Query(QueryVariable { +// name: "var2".to_string(), +// kind: VariableKind::String, +// init_fn: QueryExpr { +// selector: "$".to_string(), +// query: QueryRequest::Bank(BankQuery::Balance { +// address: "static_value".to_string(), +// denom: "denom".to_string(), +// }), +// }, +// value: Some("static_value".to_string()), +// reinitialize: false, +// update_fn: None, +// encode: false, +// }) +// ); +// } + +// #[test] +// fn test_hydrate_static_nested_vars_and_hydrate_msgs() { +// let deps = mock_dependencies(); +// let env = mock_env(); + +// let var1 = Variable::Static(StaticVariable { +// name: "var1".to_string(), +// kind: VariableKind::String, +// value: None, +// init_fn: InitFnValue::String(StringValue::Simple("static_value_1".to_string())), +// update_fn: None, +// encode: false, +// }); + +// #[cw_serde] +// struct TestStruct { +// test: String, +// } + +// // ============ TEST HYDRATED VALUE ============ + +// let test_msg = TestStruct { +// test: format!("$warp.variable.{}", "var1"), +// }; + +// let json_str = serde_json_wasm::to_string(&test_msg).unwrap(); + +// let raw_str = r#"{"test":"static_value_1"}"#.to_string(); + +// let var2 = Variable::Static(StaticVariable { +// name: "var2".to_string(), +// kind: VariableKind::String, +// value: None, +// init_fn: InitFnValue::String(StringValue::Simple(json_str.clone())), +// update_fn: None, +// // when encode is false, value will not be base64 encoded after msgs hydration +// encode: false, +// }); + +// let vars = vec![var1.clone(), var2]; +// let hydrated_vars = hydrate_vars(deps.as_ref(), env.clone(), vars, None).unwrap(); +// let hydrated_var1 = hydrated_vars[0].clone(); +// let hydrated_var2 = hydrated_vars[1].clone(); +// match hydrated_var2.clone() { +// Variable::Static(static_var) => { +// // var3.encode = false doesn't matter here, it only matters when injecting to msgs during msg hydration +// assert_eq!( +// String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), +// raw_str +// ) +// } +// _ => panic!("Expected static variable"), +// }; + +// let var3 = Variable::Static(StaticVariable { +// name: "var3".to_string(), +// kind: VariableKind::String, +// value: None, +// init_fn: InitFnValue::String(StringValue::Simple(json_str.clone())), +// update_fn: None, +// // when encode is true, value will be base64 encoded after msgs hydration +// encode: true, +// }); + +// let vars = vec![var1, var3]; +// let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); +// let hydrated_var3 = hydrated_vars[1].clone(); +// match hydrated_var3.clone() { +// Variable::Static(static_var) => { +// // var3.encode = true doesn't matter here, it only matters when injecting to msgs during msg hydration +// assert_eq!( +// String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), +// raw_str +// ); +// } +// _ => panic!("Expected static variable"), +// }; + +// // ============ TEST HYDRATED MSG AND VAR VALUE SHOULD BE ENCODED ACCORDINGLY ============ + +// let encoded_val = base64::encode(raw_str.clone()); +// assert_eq!(encoded_val, "eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ=="); +// let msgs = +// r#"[{"wasm":{"execute":{"contract_addr":"$warp.variable.var1","msg":"eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ==","funds":[]}}}, +// {"wasm":{"execute":{"contract_addr":"$warp.variable.var3","msg":"$warp.variable.var3","funds":[]}}}]"# +// .to_string(); + +// let hydrated_msgs = +// hydrate_msgs(msgs, vec![hydrated_var1, hydrated_var2, hydrated_var3]).unwrap(); + +// assert_eq!( +// hydrated_msgs[0], +// CosmosMsg::Wasm(WasmMsg::Execute { +// // Because var1.encode = false, contract_addr should use the plain text value +// contract_addr: "static_value_1".to_string(), +// msg: Binary::from(raw_str.as_bytes()), +// funds: vec![] +// }) +// ); + +// assert_eq!( +// hydrated_msgs[1], +// CosmosMsg::Wasm(WasmMsg::Execute { +// // Because var3.encode = true, contract_addr should use the encoded value +// contract_addr: encoded_val, +// // msg is not Binary::from(encoded_val.as_bytes()) appears to be a cosmos msg thing, not a warp thing +// msg: Binary::from(raw_str.as_bytes()), +// funds: vec![] +// }) +// ) +// } + +// #[test] +// fn test_test() { +// println! {"{}", "[\"{\\\"wasm\\\":{\\\"execute\\\":{\\\"contract_addr\\\":\\\"terra1na348k6rvwxje9jj6ftpsapfeyaejxjeq6tuzdmzysps20l6z23smnlv64\\\",\\\"msg\\\":\\\"eyJleGVjdXRlX3N3YXBfb3BlcmF0aW9ucyI6eyJtYXhfc3ByZWFkIjoiMC4xNSIsIm9wZXJhdGlvbnMiOlt7ImFzdHJvX3N3YXAiOnsib2ZmZXJfYXNzZXRfaW5mbyI6eyJuYXRpdmVfdG9rZW4iOnsiZGVub20iOiJ1bHVuYSJ9fSwiYXNrX2Fzc2V0X2luZm8iOnsidG9rZW4iOnsiY29udHJhY3RfYWRkciI6InRlcnJhMXhndnA2cDBxbWw1M3JlcWR5eGdjbDh0dGwwcGtoMG4ybXR4Mm43dHpmYWhuNmUwdmNhN3MwZzdzZzYifX19fSx7ImFzdHJvX3N3YXAiOnsib2ZmZXJfYXNzZXRfaW5mbyI6eyJ0b2tlbiI6eyJjb250cmFjdF9hZGRyIjoidGVycmExeGd2cDZwMHFtbDUzcmVxZHl4Z2NsOHR0bDBwa2gwbjJtdHgybjd0emZhaG42ZTB2Y2E3czBnN3NnNiJ9fSwiYXNrX2Fzc2V0X2luZm8iOnsidG9rZW4iOnsiY29udHJhY3RfYWRkciI6InRlcnJhMTY3ZHNxa2gyYWx1cng5OTd3bXljdzl5ZGt5dTU0Z3lzd2UzeWdtcnM0bHd1bWUzdm13a3M4cnVxbnYifX19fV0sIm1pbmltdW1fcmVjZWl2ZSI6IjIzNTM2NjEifX0=\\\",\\\"funds\\\":[{\\\"denom\\\":\\\"uluna\\\",\\\"amount\\\":\\\"10000\\\"}]}}}\"]".replace("\\\\", "")} +// } diff --git a/contracts/warp-resolver/src/util/condition.rs b/contracts/warp-resolver/src/util/condition.rs index 53641741..3eb230fd 100644 --- a/contracts/warp-resolver/src/util/condition.rs +++ b/contracts/warp-resolver/src/util/condition.rs @@ -9,7 +9,8 @@ use json_codec_wasm::ast::Ref; use json_codec_wasm::Decoder; use resolver::condition::{ BlockExpr, Condition, DecimalFnOp, Expr, GenExpr, IntFnOp, NumEnvValue, NumExprOp, - NumExprValue, NumFnValue, NumOp, NumValue, StringOp, TimeExpr, TimeOp, Value, + NumExprValue, NumFnValue, NumOp, NumValue, StringEnvValue, StringOp, StringValue, TimeExpr, + TimeOp, Value, }; use resolver::variable::{QueryExpr, Variable}; use std::str::FromStr; @@ -97,7 +98,9 @@ fn resolve_ref_int( let var = get_var(r, vars)?; let res = match var { Variable::Static(s) => { - let val = s.clone().value; + let val = s.clone().value.ok_or(ContractError::ConditionError { + msg: format!("Int Static value not found: {}", s.name), + })?; str::parse::(&val)? } Variable::Query(q) => { @@ -213,7 +216,9 @@ fn resolve_ref_uint( let var = get_var(r, vars)?; let res = match var { Variable::Static(s) => { - let val = s.clone().value; + let val = s.clone().value.ok_or(ContractError::ConditionError { + msg: format!("Uint Static value not found: {}", s.name), + })?; Uint256::from_str(&val)? } Variable::Query(q) => { @@ -293,6 +298,65 @@ pub fn resolve_num_env_uint( } } +pub fn resolve_string_value( + deps: Deps, + env: Env, + value: StringValue, + vars: &Vec, + warp_account_addr: Option, +) -> Result { + match value { + StringValue::Simple(value) => Ok(value), + StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), + StringValue::Env(value) => resolve_string_value_env(value, warp_account_addr), + } +} + +pub fn resolve_string_value_env( + value: StringEnvValue, + warp_account_addr: Option, +) -> Result { + if warp_account_addr.is_none() { + return Err(ContractError::HydrationError { + msg: format!("Warp account addr not found."), + }); + } + // TODO: add warp_account_addr validation + match value { + StringEnvValue::WarpAccountAddr => Ok(warp_account_addr.unwrap()), + } +} + +pub fn resolve_string_value_asset( + deps: Deps, + env: Env, + value: StringValue, + vars: &Vec, +) -> Result { + match value { + StringValue::Simple(value) => Ok(value), + StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), + StringValue::Env(value) => Err(ContractError::HydrationError { + msg: format!("String Env value not apply to string asset"), + }), + } +} + +pub fn resolve_string_value_json( + deps: Deps, + env: Env, + value: StringValue, + vars: &Vec, +) -> Result { + match value { + StringValue::Simple(value) => Ok(value), + StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), + StringValue::Env(value) => Err(ContractError::HydrationError { + msg: format!("String Env value not apply to string json"), + }), + } +} + pub fn resolve_decimal_expr( deps: Deps, env: Env, @@ -331,7 +395,9 @@ fn resolve_ref_decimal( let var = get_var(r, vars)?; let res = match var { Variable::Static(s) => { - let val = s.clone().value; + let val = s.clone().value.ok_or(ContractError::ConditionError { + msg: format!("Decimal Static value not found: {}", s.name), + })?; Decimal256::from_str(&val)? } Variable::Query(q) => { @@ -525,7 +591,9 @@ fn resolve_ref_string( ) -> Result { let var = get_var(r, vars)?; let res = match var { - Variable::Static(s) => s.value.clone(), + Variable::Static(s) => s.value.clone().ok_or(ContractError::ConditionError { + msg: format!("String Static value not found: {}", s.name), + })?, Variable::Query(q) => q.value.clone().ok_or(ContractError::ConditionError { msg: format!("String Query value not found: {}", q.name), })?, @@ -591,7 +659,9 @@ pub fn resolve_ref_bool( let var = get_var(r, vars)?; let res = match var { Variable::Static(s) => { - let val = s.clone().value; + let val = s.clone().value.ok_or(ContractError::ConditionError { + msg: format!("Bool Static value not found: {}", s.name), + })?; str::parse::(&val)? } Variable::Query(q) => { diff --git a/contracts/warp-resolver/src/util/variable.rs b/contracts/warp-resolver/src/util/variable.rs index fcf29fb5..ea9368c9 100644 --- a/contracts/warp-resolver/src/util/variable.rs +++ b/contracts/warp-resolver/src/util/variable.rs @@ -12,20 +12,186 @@ use cosmwasm_std::{ use std::str::FromStr; use controller::job::{ExternalInput, JobStatus}; -use resolver::variable::{QueryExpr, UpdateFnValue, Variable, VariableKind}; +use resolver::variable::{FnValue, QueryExpr, Variable, VariableKind}; + +use super::condition::{ + resolve_string_value, resolve_string_value_asset, resolve_string_value_json, +}; pub fn hydrate_vars( deps: Deps, env: Env, vars: Vec, external_inputs: Option>, + warp_account_addr: Option, ) -> Result, ContractError> { let mut hydrated_vars = vec![]; for var in vars { let hydrated_var = match var { Variable::Static(mut v) => { - v.value = replace_in_string(v.value, &hydrated_vars)?; + if v.reinitialize || v.value.is_none() { + match v.kind { + VariableKind::Uint => match v.init_fn { + FnValue::Uint(val) => { + v.value = Some( + resolve_num_value_uint(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + ) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Uint." + .to_string(), + }) + } + }, + VariableKind::Int => match v.init_fn { + FnValue::Int(val) => { + v.value = Some( + resolve_num_value_int(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + ) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Int." + .to_string(), + }) + } + }, + VariableKind::Decimal => match v.init_fn { + FnValue::Decimal(val) => { + v.value = Some( + resolve_num_value_decimal( + deps, + env.clone(), + val, + &hydrated_vars, + )? + .to_string(), + ) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Decimal." + .to_string(), + }) + } + }, + VariableKind::Timestamp => { + // v.value = Some( + // resolve_query_expr_int(deps, env.clone(), v.init_fn.clone())? + // .to_string(), + // ) + match v.init_fn { + FnValue::Timestamp(val) => { + v.value = Some( + resolve_num_value_int( + deps, + env.clone(), + val, + &hydrated_vars, + )? + .to_string(), + ) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Timestamp." + .to_string(), + }) + } + } + } + VariableKind::Bool => { + // v.value = Some( + // resolve_query_expr_bool(deps, env.clone(), v.init_fn.clone())? + // .to_string(), + // ) + match v.init_fn { + FnValue::Bool(val) => { + v.value = Some( + resolve_ref_bool(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + ) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Bool." + .to_string(), + }) + } + } + } + VariableKind::Amount => match v.init_fn { + FnValue::Uint(val) => { + v.value = Some( + resolve_num_value_uint(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + ) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Uint." + .to_string(), + }) + } + }, + VariableKind::String => match v.init_fn { + FnValue::String(val) => { + v.value = Some(resolve_string_value( + deps, + env.clone(), + val, + &hydrated_vars, + warp_account_addr, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::String." + .to_string(), + }) + } + }, + VariableKind::Asset => match v.init_fn { + FnValue::String(val) => { + v.value = Some(resolve_string_value_asset( + deps, + env.clone(), + val, + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::String." + .to_string(), + }) + } + }, + VariableKind::Json => match v.init_fn { + FnValue::String(val) => { + v.value = Some(resolve_string_value_json( + deps, + env.clone(), + val, + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::String." + .to_string(), + }) + } + }, + } + } + if v.value.is_none() { + return Err(ContractError::Unauthorized {}); + } Variable::Static(v) } Variable::External(mut v) => { @@ -153,76 +319,85 @@ pub fn hydrate_msgs(msgs: String, vars: Vec) -> Result, fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), ContractError> { let (name, replacement) = match var { Variable::Static(v) => (v.name.clone(), { - match v.kind { - VariableKind::String => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(v.value.clone()) - } - false => v.value.clone(), - } - ), - VariableKind::Uint => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(v.value.clone()) - } - false => v.value.clone(), - } - ), - VariableKind::Int => match v.encode { - true => { - format!("\"{}\"", base64::encode(v.value.clone())) - } - false => v.value.clone(), - }, - VariableKind::Decimal => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(v.value.clone()) - } - false => v.value.clone(), - } - ), - VariableKind::Timestamp => match v.encode { - true => { - format!("\"{}\"", base64::encode(v.value.clone())) - } - false => v.value.clone(), - }, - VariableKind::Bool => match v.encode { - true => { - format!("\"{}\"", base64::encode(v.value.clone())) - } - false => v.value.clone(), - }, - VariableKind::Amount => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(v.value.clone()) - } - false => v.value.clone(), - } - ), - VariableKind::Asset => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(v.value.clone()) - } - false => v.value.clone(), - } - ), - VariableKind::Json => match v.encode { - true => { - format!("\"{}\"", base64::encode(v.value.clone())) + match v.value.clone() { + None => { + return Err(ContractError::HydrationError { + msg: "Static msg value is none.".to_string(), + }); + } + Some(val) => (v.name.clone(), { + match v.kind { + VariableKind::Uint => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), + VariableKind::Int => match v.encode { + true => { + format!("\"{}\"", base64::encode(val)) + } + false => val, + }, + VariableKind::Decimal => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), + VariableKind::Timestamp => match v.encode { + true => { + format!("\"{}\"", base64::encode(val)) + } + false => val, + }, + VariableKind::Bool => match v.encode { + true => { + format!("\"{}\"", base64::encode(val)) + } + false => val, + }, + VariableKind::Amount => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), + VariableKind::String => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), + VariableKind::Asset => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), + VariableKind::Json => match v.encode { + true => { + format!("\"{}\"", base64::encode(val)) + } + false => val, + }, } - false => v.value.clone(), - }, + }), } }), Variable::External(v) => match v.value.clone() { @@ -313,15 +488,6 @@ fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), Contrac } Some(val) => (v.name.clone(), { match v.kind { - VariableKind::String => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(val) - } - false => val, - } - ), VariableKind::Uint => format!( "\"{}\"", match v.encode { @@ -367,6 +533,15 @@ fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), Contrac false => val, } ), + VariableKind::String => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) + } + false => val, + } + ), VariableKind::Asset => format!( "\"{}\"", match v.encode { @@ -597,7 +772,7 @@ pub fn apply_var_fn( JobStatus::Executed => match update_fn.on_success { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "Static Uint function mismatch.".to_string(), @@ -606,7 +781,7 @@ pub fn apply_var_fn( v.value = resolve_num_value_uint(deps, env.clone(), nv, &vars)? .to_string(); } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static Int function mismatch.".to_string(), @@ -615,7 +790,7 @@ pub fn apply_var_fn( v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? .to_string(); } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "Static Decimal function mismatch.".to_string(), @@ -625,7 +800,7 @@ pub fn apply_var_fn( resolve_num_value_decimal(deps, env.clone(), nv, &vars)? .to_string(); } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static Timestamp function mismatch.".to_string(), @@ -634,7 +809,7 @@ pub fn apply_var_fn( v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? .to_string(); } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static BlockHeight function mismatch." @@ -644,7 +819,7 @@ pub fn apply_var_fn( v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? .to_string(); } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "Static Bool function mismatch.".to_string(), @@ -658,7 +833,7 @@ pub fn apply_var_fn( JobStatus::Failed => match update_fn.on_error { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "Static Uint function mismatch.".to_string(), @@ -667,7 +842,7 @@ pub fn apply_var_fn( v.value = resolve_num_value_uint(deps, env.clone(), nv, &vars)? .to_string(); } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static Int function mismatch.".to_string(), @@ -676,7 +851,7 @@ pub fn apply_var_fn( v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? .to_string(); } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "Static Uint function mismatch.".to_string(), @@ -686,7 +861,7 @@ pub fn apply_var_fn( resolve_num_value_decimal(deps, env.clone(), nv, &vars)? .to_string() } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static Timestamp function mismatch.".to_string(), @@ -695,7 +870,7 @@ pub fn apply_var_fn( v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? .to_string(); } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Static BlockHeight function mismatch." @@ -705,7 +880,7 @@ pub fn apply_var_fn( v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? .to_string(); } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "Static Bool function mismatch.".to_string(), @@ -737,7 +912,7 @@ pub fn apply_var_fn( JobStatus::Executed => match update_fn.on_success { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "External Uint function mismatch.".to_string(), @@ -748,7 +923,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External Int function mismatch.".to_string(), @@ -759,7 +934,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "External Decimal function mismatch.".to_string(), @@ -770,7 +945,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External Timestamp function mismatch." @@ -782,7 +957,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External BlockHeight function mismatch." @@ -794,7 +969,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "External Bool function mismatch.".to_string(), @@ -810,7 +985,7 @@ pub fn apply_var_fn( JobStatus::Failed => match update_fn.on_error { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "External Uint function mismatch.".to_string(), @@ -821,7 +996,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External Int function mismatch.".to_string(), @@ -832,7 +1007,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "External Decimal function mismatch.".to_string(), @@ -843,7 +1018,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External Timestamp function mismatch." @@ -855,7 +1030,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "External BlockHeight function mismatch." @@ -867,7 +1042,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "External Bool function mismatch.".to_string(), @@ -901,7 +1076,7 @@ pub fn apply_var_fn( JobStatus::Executed => match update_fn.on_success { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "Query Uint function mismatch.".to_string(), @@ -912,7 +1087,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Int function mismatch.".to_string(), @@ -923,7 +1098,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "Query Decimal function mismatch.".to_string(), @@ -934,7 +1109,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Timestamp function mismatch.".to_string(), @@ -945,7 +1120,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Blockheighht function mismatch." @@ -957,7 +1132,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "Query Bool function mismatch.".to_string(), @@ -973,7 +1148,7 @@ pub fn apply_var_fn( JobStatus::Failed => match update_fn.on_error { None => (), Some(on_success) => match on_success { - UpdateFnValue::Uint(nv) => { + FnValue::Uint(nv) => { if v.kind != VariableKind::Uint { return Err(ContractError::FunctionError { msg: "Query Uint function mismatch.".to_string(), @@ -984,7 +1159,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Int(nv) => { + FnValue::Int(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Int function mismatch.".to_string(), @@ -995,7 +1170,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Decimal(nv) => { + FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { return Err(ContractError::FunctionError { msg: "Query Decimal function mismatch.".to_string(), @@ -1006,7 +1181,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Timestamp(nv) => { + FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query Timestamp function mismatch.".to_string(), @@ -1017,7 +1192,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::BlockHeight(nv) => { + FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { return Err(ContractError::FunctionError { msg: "Query BlockHeight function mismatch.".to_string(), @@ -1028,7 +1203,7 @@ pub fn apply_var_fn( .to_string(), ) } - UpdateFnValue::Bool(val) => { + FnValue::Bool(val) => { if v.kind != VariableKind::Bool { return Err(ContractError::FunctionError { msg: "Query Bool function mismatch.".to_string(), diff --git a/packages/resolver/src/condition.rs b/packages/resolver/src/condition.rs index c3932529..c4c34cf2 100644 --- a/packages/resolver/src/condition.rs +++ b/packages/resolver/src/condition.rs @@ -36,6 +36,19 @@ pub enum Value { Ref(String), } + +#[cw_serde] +pub enum StringValue { + Simple(String), + Ref(String), + Env(StringEnvValue), +} + +#[cw_serde] +pub enum StringEnvValue { + WarpAccountAddr, +} + #[cw_serde] pub enum NumValue { Simple(T), diff --git a/packages/resolver/src/lib.rs b/packages/resolver/src/lib.rs index 8030e6ea..cd38fc6f 100644 --- a/packages/resolver/src/lib.rs +++ b/packages/resolver/src/lib.rs @@ -52,6 +52,7 @@ pub struct ExecuteHydrateMsgsMsg { pub struct ExecuteHydrateVarsMsg { pub vars: String, pub external_inputs: Option>, + pub warp_account_addr: Option, } #[cw_serde] @@ -92,6 +93,7 @@ pub struct QueryHydrateMsgsMsg { pub struct QueryHydrateVarsMsg { pub vars: String, pub external_inputs: Option>, + pub warp_account_addr: Option, } #[cw_serde] diff --git a/packages/resolver/src/variable.rs b/packages/resolver/src/variable.rs index 97e1bb9d..b8cf1228 100644 --- a/packages/resolver/src/variable.rs +++ b/packages/resolver/src/variable.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Decimal256, QueryRequest, Uint256}; +use crate::condition::StringValue; + use super::condition::{DecimalFnOp, IntFnOp, NumExprOp, NumValue}; #[cw_serde] @@ -68,19 +70,20 @@ pub enum FnOp { } #[cw_serde] -pub enum UpdateFnValue { +pub enum FnValue { Uint(NumValue), Int(NumValue), Decimal(NumValue), Timestamp(NumValue), BlockHeight(NumValue), Bool(String), //ref + String(StringValue), } #[cw_serde] pub struct UpdateFn { - pub on_success: Option, - pub on_error: Option, + pub on_success: Option, + pub on_error: Option, } // Variable is specified as a reference value (string) in form of $warp.variable.{name} @@ -97,7 +100,9 @@ pub struct StaticVariable { pub kind: VariableKind, pub name: String, pub encode: bool, - pub value: String, + pub init_fn: FnValue, + pub reinitialize: bool, + pub value: Option, //none if uninitialized pub update_fn: Option, } From 64e44b40f152ccb680e914b55dd05fd706698557 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Sun, 24 Sep 2023 01:21:57 -0700 Subject: [PATCH 025/133] refactor --- contracts/warp-controller/src/contract.rs | 137 +-- contracts/warp-resolver/src/contract.rs | 4 +- contracts/warp-resolver/src/tests.rs | 1003 +++++++++-------- contracts/warp-resolver/src/util/condition.rs | 8 +- contracts/warp-resolver/src/util/variable.rs | 547 +++++---- packages/resolver/src/condition.rs | 1 - packages/resolver/src/lib.rs | 2 + 7 files changed, 978 insertions(+), 724 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index b241bbb2..3db69814 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -119,74 +119,74 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } } -// #[cfg_attr(not(feature = "library"), entry_point)] -// pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { -// //STATE -// #[cw_serde] -// pub struct V1State { -// pub current_job_id: Uint64, -// pub current_template_id: Uint64, -// pub q: Uint64, -// } - -// const V1STATE: Item = Item::new("state"); -// let v1_state = V1STATE.load(deps.storage)?; - -// STATE.save( -// deps.storage, -// &State { -// current_job_id: v1_state.current_job_id, -// q: v1_state.q, -// }, -// )?; - -// //CONFIG -// #[cw_serde] -// pub struct V1Config { -// pub owner: Addr, -// pub fee_denom: String, -// pub fee_collector: Addr, -// pub warp_account_code_id: Uint64, -// pub minimum_reward: Uint128, -// pub creation_fee_percentage: Uint64, -// pub cancellation_fee_percentage: Uint64, -// // maximum time for evictions -// pub t_max: Uint64, -// // minimum time for evictions -// pub t_min: Uint64, -// // maximum fee for evictions -// pub a_max: Uint128, -// // minimum fee for evictions -// pub a_min: Uint128, -// // maximum length of queue modifier for evictions -// pub q_max: Uint64, -// } - -// const V1CONFIG: Item = Item::new("config"); - -// let v1_config = V1CONFIG.load(deps.storage)?; - -// CONFIG.save( -// deps.storage, -// &Config { -// owner: v1_config.owner, -// fee_denom: v1_config.fee_denom, -// fee_collector: v1_config.fee_collector, -// warp_account_code_id: msg.warp_account_code_id, -// minimum_reward: v1_config.minimum_reward, -// creation_fee_percentage: v1_config.creation_fee_percentage, -// cancellation_fee_percentage: v1_config.cancellation_fee_percentage, -// resolver_address: deps.api.addr_validate(&msg.resolver_address)?, -// t_max: v1_config.t_max, -// t_min: v1_config.t_min, -// a_max: v1_config.a_max, -// a_min: v1_config.a_min, -// q_max: v1_config.q_max, -// }, -// )?; - -// Ok(Response::new()) -// } +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + //STATE + #[cw_serde] + pub struct V1State { + pub current_job_id: Uint64, + pub current_template_id: Uint64, + pub q: Uint64, + } + + const V1STATE: Item = Item::new("state"); + let v1_state = V1STATE.load(deps.storage)?; + + STATE.save( + deps.storage, + &State { + current_job_id: v1_state.current_job_id, + q: v1_state.q, + }, + )?; + + //CONFIG + #[cw_serde] + pub struct V1Config { + pub owner: Addr, + pub fee_denom: String, + pub fee_collector: Addr, + pub warp_account_code_id: Uint64, + pub minimum_reward: Uint128, + pub creation_fee_percentage: Uint64, + pub cancellation_fee_percentage: Uint64, + // maximum time for evictions + pub t_max: Uint64, + // minimum time for evictions + pub t_min: Uint64, + // maximum fee for evictions + pub a_max: Uint128, + // minimum fee for evictions + pub a_min: Uint128, + // maximum length of queue modifier for evictions + pub q_max: Uint64, + } + + const V1CONFIG: Item = Item::new("config"); + + let v1_config = V1CONFIG.load(deps.storage)?; + + CONFIG.save( + deps.storage, + &Config { + owner: v1_config.owner, + fee_denom: v1_config.fee_denom, + fee_collector: v1_config.fee_collector, + warp_account_code_id: msg.warp_account_code_id, + minimum_reward: v1_config.minimum_reward, + creation_fee_percentage: v1_config.creation_fee_percentage, + cancellation_fee_percentage: v1_config.cancellation_fee_percentage, + resolver_address: deps.api.addr_validate(&msg.resolver_address)?, + t_max: v1_config.t_max, + t_min: v1_config.t_min, + a_max: v1_config.a_max, + a_min: v1_config.a_min, + q_max: v1_config.q_max, + }, + )?; + + Ok(Response::new()) +} #[cfg_attr(not(feature = "library"), entry_point)] pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { @@ -377,6 +377,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result StdResu let vars: Vec = serde_json_wasm::from_str(&data.vars).map_err(|e| StdError::generic_err(e.to_string()))?; - apply_var_fn(deps, env, vars, data.status).map_err(|e| StdError::generic_err(e.to_string())) + apply_var_fn(deps, env, vars, data.status, data.warp_account_addr) + .map_err(|e| StdError::generic_err(e.to_string())) } fn query_hydrate_msgs( diff --git a/contracts/warp-resolver/src/tests.rs b/contracts/warp-resolver/src/tests.rs index fa415f97..01ea73da 100644 --- a/contracts/warp-resolver/src/tests.rs +++ b/contracts/warp-resolver/src/tests.rs @@ -1,444 +1,559 @@ -// use schemars::_serde_json::json; - -// use crate::util::variable::{hydrate_msgs, hydrate_vars}; - -// use cosmwasm_std::{testing::mock_env, WasmQuery}; -// use cosmwasm_std::{to_binary, BankQuery, Binary, ContractResult, CosmosMsg, OwnedDeps, WasmMsg}; - -// use crate::contract::query; -// use cosmwasm_schema::cw_serde; -// use cosmwasm_std::testing::{mock_info, MockApi, MockQuerier, MockStorage}; -// use cosmwasm_std::{from_slice, Empty, Querier, QueryRequest, SystemError, SystemResult}; - -// use resolver::variable::{ -// InitFnValue, QueryExpr, QueryVariable, StaticVariable, StringValue, Variable, VariableKind, -// }; -// use resolver::{QueryMsg, QueryValidateJobCreationMsg}; -// use std::marker::PhantomData; - -// #[test] -// fn test() { -// let deps = mock_dependencies(); -// let _info = mock_info("vlad", &[]); -// let env = mock_env(); -// let msg = QueryValidateJobCreationMsg { -// condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), -// terminate_condition: None, -// vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), -// msgs: "[{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}]".to_string(), -// }; -// let obj = serde_json_wasm::to_string(&vec!["{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}"]).unwrap(); - -// let _msg1 = QueryValidateJobCreationMsg { -// condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), -// terminate_condition: None, -// vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), -// msgs: obj.clone(), -// }; - -// println!("{}", serde_json_wasm::to_string(&obj).unwrap()); - -// let test = query(deps.as_ref(), env, QueryMsg::QueryValidateJobCreation(msg)).unwrap(); -// println!("{}", test) -// } - -// #[test] -// fn test_vars() { -// let test_msg = "{\"execute\":{\"test\":\"$WARPVAR.test\"}}".to_string(); - -// let _idx = test_msg.find("\"$WARPVAR\""); - -// let _new_str = test_msg.replace("\"$WARPVAR.test\"", "\"input\""); -// } - -// pub fn mock_dependencies() -> OwnedDeps { -// let custom_querier: WasmMockQuerier = WasmMockQuerier::new(MockQuerier::new(&[])); - -// OwnedDeps { -// api: MockApi::default(), -// storage: MockStorage::default(), -// querier: custom_querier, -// custom_query_type: PhantomData, -// } -// } - -// pub struct WasmMockQuerier { -// base: MockQuerier, -// } - -// impl Querier for WasmMockQuerier { -// fn raw_query(&self, bin_request: &[u8]) -> SystemResult> { -// let request: QueryRequest = match from_slice(bin_request) { -// Ok(v) => v, -// Err(e) => { -// return SystemResult::Err(SystemError::InvalidRequest { -// error: format!("Parsing query request: {}", e), -// request: bin_request.into(), -// }); -// } -// }; -// self.handle_query(&request) -// } -// } - -// impl WasmMockQuerier { -// pub fn handle_query( -// &self, -// request: &QueryRequest, -// ) -> SystemResult> { -// match &request { -// QueryRequest::Wasm(WasmQuery::Smart { -// contract_addr, -// msg: _, -// }) => { -// // Mock logic for the Wasm::Smart case -// // Here for simplicity, we return the contract_addr and msg as is. - -// // Mock logic for the Wasm::Smart case -// // Here we return a JSON object with "address" and "msg" fields. -// let response: String = json!({ -// "address": contract_addr, -// "msg": "Mock message" -// }) -// .to_string(); - -// SystemResult::Ok(ContractResult::Ok(to_binary(&response).unwrap())) -// } -// QueryRequest::Bank(BankQuery::Balance { -// address: contract_addr, -// denom: _, -// }) => SystemResult::Ok(ContractResult::Ok( -// to_binary(&contract_addr.to_string()).unwrap(), -// )), -// _ => self.base.handle_query(request), -// } -// } -// } - -// impl WasmMockQuerier { -// pub fn new(base: MockQuerier) -> Self { -// WasmMockQuerier { base } -// } -// } - -// #[test] -// fn test_hydrate_vars_nested_variables_binary_json() { -// let deps = mock_dependencies(); -// let env = mock_env(); - -// let var5 = Variable::Static(StaticVariable { -// kind: VariableKind::String, -// name: "var5".to_string(), -// encode: false, -// value: None, -// init_fn: InitFnValue::String(StringValue::Simple("contract_addr".to_string())), -// update_fn: None, -// }); - -// let var4 = Variable::Static(StaticVariable { -// kind: VariableKind::String, -// name: "var4".to_string(), -// encode: false, -// value: None, -// init_fn: InitFnValue::String(StringValue::Ref("$warp.variable.var5".to_string())), -// update_fn: None, -// }); - -// let var3 = Variable::Query(QueryVariable { -// name: "var3".to_string(), -// kind: VariableKind::Json, -// init_fn: QueryExpr { -// selector: "$".to_string(), -// query: QueryRequest::Wasm(WasmQuery::Smart { -// contract_addr: "contract_addr".to_string(), -// msg: Binary::from(r#"{"test":"test"}"#.as_bytes()), -// }), -// }, -// value: None, -// reinitialize: false, -// update_fn: None, -// encode: true, -// }); - -// let var1 = Variable::Query(QueryVariable { -// name: "var1".to_string(), -// kind: VariableKind::Json, -// init_fn: QueryExpr { -// selector: "$".to_string(), -// query: QueryRequest::Wasm(WasmQuery::Smart { -// contract_addr: "contract_addr".to_string(), -// msg: Binary::from(r#"{"test":"$warp.variable.var3"}"#.as_bytes()), -// }), -// }, -// value: None, -// reinitialize: false, -// update_fn: None, -// encode: true, -// }); - -// let var2 = Variable::Query(QueryVariable { -// name: "var2".to_string(), -// kind: VariableKind::Json, -// init_fn: QueryExpr { -// selector: "$".to_string(), -// query: QueryRequest::Wasm(WasmQuery::Smart { -// contract_addr: "$warp.variable.var4".to_string(), -// msg: Binary::from(r#"{"test":"$warp.variable.var1"}"#.as_bytes()), -// }), -// }, -// value: None, -// reinitialize: false, -// update_fn: None, -// encode: false, -// }); - -// let vars = vec![var5, var4, var3, var1, var2]; -// let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); - -// assert_eq!( -// hydrated_vars[4], -// Variable::Query(QueryVariable { -// name: "var2".to_string(), -// kind: VariableKind::Json, -// init_fn: QueryExpr { -// selector: "$".to_string(), -// query: QueryRequest::Wasm(WasmQuery::Smart { -// contract_addr: "contract_addr".to_string(), -// msg: Binary::from( -// r#"{"test":"eyJhZGRyZXNzIjoiY29udHJhY3RfYWRkciIsIm1zZyI6Ik1vY2sgbWVzc2FnZSJ9"}"#.as_bytes() -// ), -// }), -// }, -// value: Some(r#"{"address":"contract_addr","msg":"Mock message"}"#.to_string()), -// reinitialize: false, -// update_fn: None, -// encode: false, -// }) -// ); -// } - -// #[test] -// fn test_hydrate_vars_nested_variables_binary() { -// let deps = mock_dependencies(); -// let env = mock_env(); - -// let var1 = Variable::Static(StaticVariable { -// name: "var1".to_string(), -// kind: VariableKind::String, -// value: None, -// init_fn: InitFnValue::String(StringValue::Simple("static_value".to_string())), -// update_fn: None, -// encode: false, -// }); - -// let init_fn = QueryExpr { -// selector: "$".to_string(), -// query: QueryRequest::Wasm(WasmQuery::Smart { -// contract_addr: "$warp.variable.var1".to_string(), -// msg: Binary::from(r#"{"test": "$warp.variable.var1"}"#.as_bytes()), -// }), -// }; - -// let var2 = Variable::Query(QueryVariable { -// name: "var2".to_string(), -// kind: VariableKind::String, -// init_fn, -// value: None, -// reinitialize: false, -// update_fn: None, -// encode: false, -// }); - -// let vars = vec![var1, var2]; -// let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); - -// assert_eq!( -// hydrated_vars[1], -// Variable::Query(QueryVariable { -// name: "var2".to_string(), -// kind: VariableKind::String, -// init_fn: QueryExpr { -// selector: "$".to_string(), -// query: QueryRequest::Wasm(WasmQuery::Smart { -// contract_addr: "static_value".to_string(), -// msg: Binary::from(r#"{"test": "static_value"}"#.as_bytes()), -// }), -// }, -// value: Some(r#"{"address":"static_value","msg":"Mock message"}"#.to_string()), -// reinitialize: false, -// update_fn: None, -// encode: false, -// }) -// ); -// } -// #[test] -// fn test_hydrate_vars_nested_variables_non_binary() { -// let deps = mock_dependencies(); -// let env = mock_env(); - -// let var1 = Variable::Static(StaticVariable { -// name: "var1".to_string(), -// kind: VariableKind::String, -// value: None, -// init_fn: InitFnValue::String(StringValue::Simple("static_value".to_string())), -// update_fn: None, -// encode: false, -// }); - -// let init_fn = QueryExpr { -// selector: "$".to_string(), -// query: QueryRequest::Bank(BankQuery::Balance { -// address: "$warp.variable.var1".to_string(), -// denom: "denom".to_string(), -// }), -// }; - -// let var2 = Variable::Query(QueryVariable { -// name: "var2".to_string(), -// kind: VariableKind::String, -// init_fn, -// value: None, -// reinitialize: false, -// update_fn: None, -// encode: false, -// }); - -// let vars = vec![var1, var2]; -// let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); - -// assert_eq!( -// hydrated_vars[1], -// Variable::Query(QueryVariable { -// name: "var2".to_string(), -// kind: VariableKind::String, -// init_fn: QueryExpr { -// selector: "$".to_string(), -// query: QueryRequest::Bank(BankQuery::Balance { -// address: "static_value".to_string(), -// denom: "denom".to_string(), -// }), -// }, -// value: Some("static_value".to_string()), -// reinitialize: false, -// update_fn: None, -// encode: false, -// }) -// ); -// } - -// #[test] -// fn test_hydrate_static_nested_vars_and_hydrate_msgs() { -// let deps = mock_dependencies(); -// let env = mock_env(); - -// let var1 = Variable::Static(StaticVariable { -// name: "var1".to_string(), -// kind: VariableKind::String, -// value: None, -// init_fn: InitFnValue::String(StringValue::Simple("static_value_1".to_string())), -// update_fn: None, -// encode: false, -// }); - -// #[cw_serde] -// struct TestStruct { -// test: String, -// } - -// // ============ TEST HYDRATED VALUE ============ - -// let test_msg = TestStruct { -// test: format!("$warp.variable.{}", "var1"), -// }; - -// let json_str = serde_json_wasm::to_string(&test_msg).unwrap(); - -// let raw_str = r#"{"test":"static_value_1"}"#.to_string(); - -// let var2 = Variable::Static(StaticVariable { -// name: "var2".to_string(), -// kind: VariableKind::String, -// value: None, -// init_fn: InitFnValue::String(StringValue::Simple(json_str.clone())), -// update_fn: None, -// // when encode is false, value will not be base64 encoded after msgs hydration -// encode: false, -// }); - -// let vars = vec![var1.clone(), var2]; -// let hydrated_vars = hydrate_vars(deps.as_ref(), env.clone(), vars, None).unwrap(); -// let hydrated_var1 = hydrated_vars[0].clone(); -// let hydrated_var2 = hydrated_vars[1].clone(); -// match hydrated_var2.clone() { -// Variable::Static(static_var) => { -// // var3.encode = false doesn't matter here, it only matters when injecting to msgs during msg hydration -// assert_eq!( -// String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), -// raw_str -// ) -// } -// _ => panic!("Expected static variable"), -// }; - -// let var3 = Variable::Static(StaticVariable { -// name: "var3".to_string(), -// kind: VariableKind::String, -// value: None, -// init_fn: InitFnValue::String(StringValue::Simple(json_str.clone())), -// update_fn: None, -// // when encode is true, value will be base64 encoded after msgs hydration -// encode: true, -// }); - -// let vars = vec![var1, var3]; -// let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None).unwrap(); -// let hydrated_var3 = hydrated_vars[1].clone(); -// match hydrated_var3.clone() { -// Variable::Static(static_var) => { -// // var3.encode = true doesn't matter here, it only matters when injecting to msgs during msg hydration -// assert_eq!( -// String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), -// raw_str -// ); -// } -// _ => panic!("Expected static variable"), -// }; - -// // ============ TEST HYDRATED MSG AND VAR VALUE SHOULD BE ENCODED ACCORDINGLY ============ - -// let encoded_val = base64::encode(raw_str.clone()); -// assert_eq!(encoded_val, "eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ=="); -// let msgs = -// r#"[{"wasm":{"execute":{"contract_addr":"$warp.variable.var1","msg":"eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ==","funds":[]}}}, -// {"wasm":{"execute":{"contract_addr":"$warp.variable.var3","msg":"$warp.variable.var3","funds":[]}}}]"# -// .to_string(); - -// let hydrated_msgs = -// hydrate_msgs(msgs, vec![hydrated_var1, hydrated_var2, hydrated_var3]).unwrap(); - -// assert_eq!( -// hydrated_msgs[0], -// CosmosMsg::Wasm(WasmMsg::Execute { -// // Because var1.encode = false, contract_addr should use the plain text value -// contract_addr: "static_value_1".to_string(), -// msg: Binary::from(raw_str.as_bytes()), -// funds: vec![] -// }) -// ); - -// assert_eq!( -// hydrated_msgs[1], -// CosmosMsg::Wasm(WasmMsg::Execute { -// // Because var3.encode = true, contract_addr should use the encoded value -// contract_addr: encoded_val, -// // msg is not Binary::from(encoded_val.as_bytes()) appears to be a cosmos msg thing, not a warp thing -// msg: Binary::from(raw_str.as_bytes()), -// funds: vec![] -// }) -// ) -// } - -// #[test] -// fn test_test() { -// println! {"{}", "[\"{\\\"wasm\\\":{\\\"execute\\\":{\\\"contract_addr\\\":\\\"terra1na348k6rvwxje9jj6ftpsapfeyaejxjeq6tuzdmzysps20l6z23smnlv64\\\",\\\"msg\\\":\\\"eyJleGVjdXRlX3N3YXBfb3BlcmF0aW9ucyI6eyJtYXhfc3ByZWFkIjoiMC4xNSIsIm9wZXJhdGlvbnMiOlt7ImFzdHJvX3N3YXAiOnsib2ZmZXJfYXNzZXRfaW5mbyI6eyJuYXRpdmVfdG9rZW4iOnsiZGVub20iOiJ1bHVuYSJ9fSwiYXNrX2Fzc2V0X2luZm8iOnsidG9rZW4iOnsiY29udHJhY3RfYWRkciI6InRlcnJhMXhndnA2cDBxbWw1M3JlcWR5eGdjbDh0dGwwcGtoMG4ybXR4Mm43dHpmYWhuNmUwdmNhN3MwZzdzZzYifX19fSx7ImFzdHJvX3N3YXAiOnsib2ZmZXJfYXNzZXRfaW5mbyI6eyJ0b2tlbiI6eyJjb250cmFjdF9hZGRyIjoidGVycmExeGd2cDZwMHFtbDUzcmVxZHl4Z2NsOHR0bDBwa2gwbjJtdHgybjd0emZhaG42ZTB2Y2E3czBnN3NnNiJ9fSwiYXNrX2Fzc2V0X2luZm8iOnsidG9rZW4iOnsiY29udHJhY3RfYWRkciI6InRlcnJhMTY3ZHNxa2gyYWx1cng5OTd3bXljdzl5ZGt5dTU0Z3lzd2UzeWdtcnM0bHd1bWUzdm13a3M4cnVxbnYifX19fV0sIm1pbmltdW1fcmVjZWl2ZSI6IjIzNTM2NjEifX0=\\\",\\\"funds\\\":[{\\\"denom\\\":\\\"uluna\\\",\\\"amount\\\":\\\"10000\\\"}]}}}\"]".replace("\\\\", "")} -// } +use resolver::condition::{StringEnvValue, StringValue}; +use schemars::_serde_json::json; + +use crate::util::variable::{hydrate_msgs, hydrate_vars}; + +use cosmwasm_std::{testing::mock_env, WasmQuery}; +use cosmwasm_std::{to_binary, BankQuery, Binary, ContractResult, CosmosMsg, OwnedDeps, WasmMsg}; + +use crate::contract::query; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::testing::{mock_info, MockApi, MockQuerier, MockStorage}; +use cosmwasm_std::{from_slice, Empty, Querier, QueryRequest, SystemError, SystemResult}; + +use resolver::variable::{ + FnValue, QueryExpr, QueryVariable, StaticVariable, Variable, VariableKind, +}; +use resolver::{QueryMsg, QueryValidateJobCreationMsg}; +use std::marker::PhantomData; + +#[cw_serde] +struct TestStruct { + test: String, +} + +#[test] +fn test() { + let deps = mock_dependencies(); + let _info = mock_info("vlad", &[]); + let env = mock_env(); + let msg = QueryValidateJobCreationMsg { + condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), + terminate_condition: None, + vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), + msgs: "[{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}]".to_string(), + }; + let obj = serde_json_wasm::to_string(&vec!["{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}"]).unwrap(); + + let _msg1 = QueryValidateJobCreationMsg { + condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), + terminate_condition: None, + vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), + msgs: obj.clone(), + }; + + println!("{}", serde_json_wasm::to_string(&obj).unwrap()); + + let test = query(deps.as_ref(), env, QueryMsg::QueryValidateJobCreation(msg)).unwrap(); + println!("{}", test) +} + +#[test] +fn test_vars() { + let test_msg = "{\"execute\":{\"test\":\"$WARPVAR.test\"}}".to_string(); + + let _idx = test_msg.find("\"$WARPVAR\""); + + let _new_str = test_msg.replace("\"$WARPVAR.test\"", "\"input\""); +} + +pub fn mock_dependencies() -> OwnedDeps { + let custom_querier: WasmMockQuerier = WasmMockQuerier::new(MockQuerier::new(&[])); + + OwnedDeps { + api: MockApi::default(), + storage: MockStorage::default(), + querier: custom_querier, + custom_query_type: PhantomData, + } +} + +pub struct WasmMockQuerier { + base: MockQuerier, +} + +impl Querier for WasmMockQuerier { + fn raw_query(&self, bin_request: &[u8]) -> SystemResult> { + let request: QueryRequest = match from_slice(bin_request) { + Ok(v) => v, + Err(e) => { + return SystemResult::Err(SystemError::InvalidRequest { + error: format!("Parsing query request: {}", e), + request: bin_request.into(), + }); + } + }; + self.handle_query(&request) + } +} + +impl WasmMockQuerier { + pub fn handle_query( + &self, + request: &QueryRequest, + ) -> SystemResult> { + match &request { + QueryRequest::Wasm(WasmQuery::Smart { + contract_addr, + msg: _, + }) => { + // Mock logic for the Wasm::Smart case + // Here for simplicity, we return the contract_addr and msg as is. + + // Mock logic for the Wasm::Smart case + // Here we return a JSON object with "address" and "msg" fields. + let response: String = json!({ + "address": contract_addr, + "msg": "Mock message" + }) + .to_string(); + + SystemResult::Ok(ContractResult::Ok(to_binary(&response).unwrap())) + } + QueryRequest::Bank(BankQuery::Balance { + address: contract_addr, + denom: _, + }) => SystemResult::Ok(ContractResult::Ok( + to_binary(&contract_addr.to_string()).unwrap(), + )), + _ => self.base.handle_query(request), + } + } +} + +impl WasmMockQuerier { + pub fn new(base: MockQuerier) -> Self { + WasmMockQuerier { base } + } +} + +#[test] +fn test_hydrate_vars_nested_variables_binary_json() { + let deps = mock_dependencies(); + let env = mock_env(); + + let var5 = Variable::Static(StaticVariable { + kind: VariableKind::String, + name: "var5".to_string(), + encode: false, + value: None, + init_fn: FnValue::String(StringValue::Simple("contract_addr".to_string())), + reinitialize: false, + update_fn: None, + }); + + let var4 = Variable::Static(StaticVariable { + kind: VariableKind::String, + name: "var4".to_string(), + encode: false, + value: None, + init_fn: FnValue::String(StringValue::Ref("$warp.variable.var5".to_string())), + reinitialize: false, + update_fn: None, + }); + + let var3 = Variable::Query(QueryVariable { + name: "var3".to_string(), + kind: VariableKind::Json, + init_fn: QueryExpr { + selector: "$".to_string(), + query: QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: "contract_addr".to_string(), + msg: Binary::from(r#"{"test":"test"}"#.as_bytes()), + }), + }, + value: None, + reinitialize: false, + update_fn: None, + encode: true, + }); + + let var1 = Variable::Query(QueryVariable { + name: "var1".to_string(), + kind: VariableKind::Json, + init_fn: QueryExpr { + selector: "$".to_string(), + query: QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: "contract_addr".to_string(), + msg: Binary::from(r#"{"test":"$warp.variable.var3"}"#.as_bytes()), + }), + }, + value: None, + reinitialize: false, + update_fn: None, + encode: true, + }); + + let var2 = Variable::Query(QueryVariable { + name: "var2".to_string(), + kind: VariableKind::Json, + init_fn: QueryExpr { + selector: "$".to_string(), + query: QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: "$warp.variable.var4".to_string(), + msg: Binary::from(r#"{"test":"$warp.variable.var1"}"#.as_bytes()), + }), + }, + value: None, + reinitialize: false, + update_fn: None, + encode: false, + }); + + let vars = vec![var5, var4, var3, var1, var2]; + let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None, None).unwrap(); + + assert_eq!( + hydrated_vars[4], + Variable::Query(QueryVariable { + name: "var2".to_string(), + kind: VariableKind::Json, + init_fn: QueryExpr { + selector: "$".to_string(), + query: QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: "contract_addr".to_string(), + msg: Binary::from( + r#"{"test":"eyJhZGRyZXNzIjoiY29udHJhY3RfYWRkciIsIm1zZyI6Ik1vY2sgbWVzc2FnZSJ9"}"#.as_bytes() + ), + }), + }, + value: Some(r#"{"address":"contract_addr","msg":"Mock message"}"#.to_string()), + reinitialize: false, + update_fn: None, + encode: false, + }) + ); +} + +#[test] +fn test_hydrate_vars_nested_variables_binary() { + let deps = mock_dependencies(); + let env = mock_env(); + + let var1 = Variable::Static(StaticVariable { + name: "var1".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Simple("static_value".to_string())), + reinitialize: false, + update_fn: None, + encode: false, + }); + + let init_fn = QueryExpr { + selector: "$".to_string(), + query: QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: "$warp.variable.var1".to_string(), + msg: Binary::from(r#"{"test": "$warp.variable.var1"}"#.as_bytes()), + }), + }; + + let var2 = Variable::Query(QueryVariable { + name: "var2".to_string(), + kind: VariableKind::String, + init_fn, + value: None, + reinitialize: false, + update_fn: None, + encode: false, + }); + + let vars = vec![var1, var2]; + let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None, None).unwrap(); + + assert_eq!( + hydrated_vars[1], + Variable::Query(QueryVariable { + name: "var2".to_string(), + kind: VariableKind::String, + init_fn: QueryExpr { + selector: "$".to_string(), + query: QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: "static_value".to_string(), + msg: Binary::from(r#"{"test": "static_value"}"#.as_bytes()), + }), + }, + value: Some(r#"{"address":"static_value","msg":"Mock message"}"#.to_string()), + reinitialize: false, + update_fn: None, + encode: false, + }) + ); +} +#[test] +fn test_hydrate_vars_nested_variables_non_binary() { + let deps = mock_dependencies(); + let env = mock_env(); + + let var1 = Variable::Static(StaticVariable { + name: "var1".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Simple("static_value".to_string())), + reinitialize: false, + update_fn: None, + encode: false, + }); + + let init_fn = QueryExpr { + selector: "$".to_string(), + query: QueryRequest::Bank(BankQuery::Balance { + address: "$warp.variable.var1".to_string(), + denom: "denom".to_string(), + }), + }; + + let var2 = Variable::Query(QueryVariable { + name: "var2".to_string(), + kind: VariableKind::String, + init_fn, + value: None, + reinitialize: false, + update_fn: None, + encode: false, + }); + + let vars = vec![var1, var2]; + let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None, None).unwrap(); + + assert_eq!( + hydrated_vars[1], + Variable::Query(QueryVariable { + name: "var2".to_string(), + kind: VariableKind::String, + init_fn: QueryExpr { + selector: "$".to_string(), + query: QueryRequest::Bank(BankQuery::Balance { + address: "static_value".to_string(), + denom: "denom".to_string(), + }), + }, + value: Some("static_value".to_string()), + reinitialize: false, + update_fn: None, + encode: false, + }) + ); +} + +#[test] +fn test_hydrate_static_nested_vars_and_hydrate_msgs() { + let deps = mock_dependencies(); + let env = mock_env(); + + let var1 = Variable::Static(StaticVariable { + name: "var1".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Simple("static_value_1".to_string())), + reinitialize: false, + update_fn: None, + encode: false, + }); + + // ============ TEST HYDRATED VALUE ============ + + let test_msg = TestStruct { + test: format!("$warp.variable.{}", "var1"), + }; + + let json_str = serde_json_wasm::to_string(&test_msg).unwrap(); + + let raw_str = r#"{"test":"static_value_1"}"#.to_string(); + + let var2 = Variable::Static(StaticVariable { + name: "var2".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Simple(json_str.clone())), + reinitialize: false, + update_fn: None, + // when encode is false, value will not be base64 encoded after msgs hydration + encode: false, + }); + + let vars = vec![var1.clone(), var2]; + let hydrated_vars = hydrate_vars(deps.as_ref(), env.clone(), vars, None, None).unwrap(); + let hydrated_var1 = hydrated_vars[0].clone(); + let hydrated_var2 = hydrated_vars[1].clone(); + match hydrated_var2.clone() { + Variable::Static(static_var) => { + // var3.encode = false doesn't matter here, it only matters when injecting to msgs during msg hydration + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + raw_str + ) + } + _ => panic!("Expected static variable"), + }; + + let var3 = Variable::Static(StaticVariable { + name: "var3".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Simple(json_str.clone())), + reinitialize: false, + update_fn: None, + // when encode is true, value will be base64 encoded after msgs hydration + encode: true, + }); + + let vars = vec![var1, var3]; + let hydrated_vars = hydrate_vars(deps.as_ref(), env, vars, None, None).unwrap(); + let hydrated_var3 = hydrated_vars[1].clone(); + match hydrated_var3.clone() { + Variable::Static(static_var) => { + // var3.encode = true doesn't matter here, it only matters when injecting to msgs during msg hydration + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + raw_str + ); + } + _ => panic!("Expected static variable"), + }; + + // ============ TEST HYDRATED MSG AND VAR VALUE SHOULD BE ENCODED ACCORDINGLY ============ + + let encoded_val = base64::encode(raw_str.clone()); + assert_eq!(encoded_val, "eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ=="); + let msgs = + r#"[{"wasm":{"execute":{"contract_addr":"$warp.variable.var1","msg":"eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ==","funds":[]}}}, + {"wasm":{"execute":{"contract_addr":"$warp.variable.var3","msg":"$warp.variable.var3","funds":[]}}}]"# + .to_string(); + + let hydrated_msgs = + hydrate_msgs(msgs, vec![hydrated_var1, hydrated_var2, hydrated_var3]).unwrap(); + + assert_eq!( + hydrated_msgs[0], + CosmosMsg::Wasm(WasmMsg::Execute { + // Because var1.encode = false, contract_addr should use the plain text value + contract_addr: "static_value_1".to_string(), + msg: Binary::from(raw_str.as_bytes()), + funds: vec![] + }) + ); + + assert_eq!( + hydrated_msgs[1], + CosmosMsg::Wasm(WasmMsg::Execute { + // Because var3.encode = true, contract_addr should use the encoded value + contract_addr: encoded_val, + // msg is not Binary::from(encoded_val.as_bytes()) appears to be a cosmos msg thing, not a warp thing + msg: Binary::from(raw_str.as_bytes()), + funds: vec![] + }) + ) +} + +#[test] +fn test_hydrate_static_env_vars_and_hydrate_msgs() { + let deps = mock_dependencies(); + let env = mock_env(); + + let dummy_warp_account_addr = "terra1".to_string(); + + let json_str = serde_json_wasm::to_string(&TestStruct { + test: format!("$warp.variable.{}", "var1"), + }) + .unwrap(); + + let raw_str = r#"{"test":"static_value_1"}"#.to_string(); + + let encoded_val = base64::encode(raw_str.clone()); + assert_eq!(encoded_val, "eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ=="); + + // ============ TEST HYDRATED VALUE ============ + + let var1 = Variable::Static(StaticVariable { + name: "var1".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Simple("static_value_1".to_string())), + reinitialize: false, + update_fn: None, + encode: false, + }); + + let var2 = Variable::Static(StaticVariable { + name: "var2".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Simple(json_str.clone())), + reinitialize: false, + update_fn: None, + encode: true, + }); + + let var3 = Variable::Static(StaticVariable { + name: "var3".to_string(), + kind: VariableKind::String, + value: None, + init_fn: FnValue::String(StringValue::Env(StringEnvValue::WarpAccountAddr)), + reinitialize: false, + update_fn: None, + encode: false, + }); + + let vars = vec![var1, var2, var3]; + let hydrated_vars = hydrate_vars( + deps.as_ref(), + env, + vars, + None, + Some(dummy_warp_account_addr.clone()), + ) + .unwrap(); + + let hydrated_var1 = hydrated_vars[0].clone(); + let hydrated_var2 = hydrated_vars[1].clone(); + match hydrated_var2.clone() { + Variable::Static(static_var) => { + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + raw_str + ) + } + _ => panic!("Expected static variable"), + }; + let hydrated_var3 = hydrated_vars[2].clone(); + match hydrated_var3.clone() { + Variable::Static(static_var) => { + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + dummy_warp_account_addr.clone() + ) + } + _ => panic!("Expected static variable"), + }; + + // ============ TEST HYDRATED MSG AND VAR VALUE SHOULD BE ENCODED ACCORDINGLY ============ + + let msgs = + r#"[ + {"wasm":{"execute":{"contract_addr":"$warp.variable.var1","msg":"eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ==","funds":[]}}}, + {"wasm":{"execute":{"contract_addr":"$warp.variable.var3","msg":"$warp.variable.var2","funds":[]}}} + ]"# + .to_string(); + + let hydrated_msgs = + hydrate_msgs(msgs, vec![hydrated_var1, hydrated_var2, hydrated_var3]).unwrap(); + + assert_eq!( + hydrated_msgs[0], + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "static_value_1".to_string(), + msg: Binary::from(raw_str.as_bytes()), + funds: vec![] + }) + ); + + assert_eq!( + hydrated_msgs[1], + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: dummy_warp_account_addr, + msg: Binary::from(raw_str.as_bytes()), + funds: vec![] + }) + ) +} diff --git a/contracts/warp-resolver/src/util/condition.rs b/contracts/warp-resolver/src/util/condition.rs index 3eb230fd..373d5c30 100644 --- a/contracts/warp-resolver/src/util/condition.rs +++ b/contracts/warp-resolver/src/util/condition.rs @@ -316,9 +316,9 @@ pub fn resolve_string_value_env( value: StringEnvValue, warp_account_addr: Option, ) -> Result { - if warp_account_addr.is_none() { + if (warp_account_addr).is_none() { return Err(ContractError::HydrationError { - msg: format!("Warp account addr not found."), + msg: "Warp account addr not found.".to_string(), }); } // TODO: add warp_account_addr validation @@ -337,7 +337,7 @@ pub fn resolve_string_value_asset( StringValue::Simple(value) => Ok(value), StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), StringValue::Env(value) => Err(ContractError::HydrationError { - msg: format!("String Env value not apply to string asset"), + msg: format!("String Env value not apply to string asset: {:?}", value), }), } } @@ -352,7 +352,7 @@ pub fn resolve_string_value_json( StringValue::Simple(value) => Ok(value), StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), StringValue::Env(value) => Err(ContractError::HydrationError { - msg: format!("String Env value not apply to string json"), + msg: format!("String Env value not apply to string json: {:?}", value), }), } } diff --git a/contracts/warp-resolver/src/util/variable.rs b/contracts/warp-resolver/src/util/variable.rs index ea9368c9..03830285 100644 --- a/contracts/warp-resolver/src/util/variable.rs +++ b/contracts/warp-resolver/src/util/variable.rs @@ -32,12 +32,13 @@ pub fn hydrate_vars( Variable::Static(mut v) => { if v.reinitialize || v.value.is_none() { match v.kind { - VariableKind::Uint => match v.init_fn { + VariableKind::Uint => match v.init_fn.clone() { FnValue::Uint(val) => { - v.value = Some( + v.value = Some(replace_in_string( resolve_num_value_uint(deps, env.clone(), val, &hydrated_vars)? .to_string(), - ) + &hydrated_vars, + )?) } _ => { return Err(ContractError::HydrationError { @@ -46,12 +47,13 @@ pub fn hydrate_vars( }) } }, - VariableKind::Int => match v.init_fn { + VariableKind::Int => match v.init_fn.clone() { FnValue::Int(val) => { - v.value = Some( + v.value = Some(replace_in_string( resolve_num_value_int(deps, env.clone(), val, &hydrated_vars)? .to_string(), - ) + &hydrated_vars, + )?) } _ => { return Err(ContractError::HydrationError { @@ -60,9 +62,9 @@ pub fn hydrate_vars( }) } }, - VariableKind::Decimal => match v.init_fn { + VariableKind::Decimal => match v.init_fn.clone() { FnValue::Decimal(val) => { - v.value = Some( + v.value = Some(replace_in_string( resolve_num_value_decimal( deps, env.clone(), @@ -70,7 +72,8 @@ pub fn hydrate_vars( &hydrated_vars, )? .to_string(), - ) + &hydrated_vars, + )?) } _ => { return Err(ContractError::HydrationError { @@ -79,57 +82,43 @@ pub fn hydrate_vars( }) } }, - VariableKind::Timestamp => { - // v.value = Some( - // resolve_query_expr_int(deps, env.clone(), v.init_fn.clone())? - // .to_string(), - // ) - match v.init_fn { - FnValue::Timestamp(val) => { - v.value = Some( - resolve_num_value_int( - deps, - env.clone(), - val, - &hydrated_vars, - )? + VariableKind::Timestamp => match v.init_fn.clone() { + FnValue::Timestamp(val) => { + v.value = Some(replace_in_string( + resolve_num_value_int(deps, env.clone(), val, &hydrated_vars)? .to_string(), - ) - } - _ => { - return Err(ContractError::HydrationError { - msg: "Variable init_fn is not of type FnValue::Timestamp." - .to_string(), - }) - } + &hydrated_vars, + )?) } - } - VariableKind::Bool => { - // v.value = Some( - // resolve_query_expr_bool(deps, env.clone(), v.init_fn.clone())? - // .to_string(), - // ) - match v.init_fn { - FnValue::Bool(val) => { - v.value = Some( - resolve_ref_bool(deps, env.clone(), val, &hydrated_vars)? - .to_string(), - ) - } - _ => { - return Err(ContractError::HydrationError { - msg: "Variable init_fn is not of type FnValue::Bool." - .to_string(), - }) - } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Timestamp." + .to_string(), + }) } - } - VariableKind::Amount => match v.init_fn { + }, + VariableKind::Bool => match v.init_fn.clone() { + FnValue::Bool(val) => { + v.value = Some(replace_in_string( + resolve_ref_bool(deps, env.clone(), val, &hydrated_vars)? + .to_string(), + &hydrated_vars, + )?) + } + _ => { + return Err(ContractError::HydrationError { + msg: "Variable init_fn is not of type FnValue::Bool." + .to_string(), + }) + } + }, + VariableKind::Amount => match v.init_fn.clone() { FnValue::Uint(val) => { - v.value = Some( + v.value = Some(replace_in_string( resolve_num_value_uint(deps, env.clone(), val, &hydrated_vars)? .to_string(), - ) + &hydrated_vars, + )?) } _ => { return Err(ContractError::HydrationError { @@ -138,14 +127,17 @@ pub fn hydrate_vars( }) } }, - VariableKind::String => match v.init_fn { + VariableKind::String => match v.init_fn.clone() { FnValue::String(val) => { - v.value = Some(resolve_string_value( - deps, - env.clone(), - val, + v.value = Some(replace_in_string( + resolve_string_value( + deps, + env.clone(), + val, + &hydrated_vars, + warp_account_addr.clone(), + )?, &hydrated_vars, - warp_account_addr, )?) } _ => { @@ -155,12 +147,15 @@ pub fn hydrate_vars( }) } }, - VariableKind::Asset => match v.init_fn { + VariableKind::Asset => match v.init_fn.clone() { FnValue::String(val) => { - v.value = Some(resolve_string_value_asset( - deps, - env.clone(), - val, + v.value = Some(replace_in_string( + resolve_string_value_asset( + deps, + env.clone(), + val, + &hydrated_vars, + )?, &hydrated_vars, )?) } @@ -171,12 +166,15 @@ pub fn hydrate_vars( }) } }, - VariableKind::Json => match v.init_fn { + VariableKind::Json => match v.init_fn.clone() { FnValue::String(val) => { - v.value = Some(resolve_string_value_json( - deps, - env.clone(), - val, + v.value = Some(replace_in_string( + resolve_string_value_json( + deps, + env.clone(), + val, + &hydrated_vars, + )?, &hydrated_vars, )?) } @@ -318,88 +316,86 @@ pub fn hydrate_msgs(msgs: String, vars: Vec) -> Result, fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), ContractError> { let (name, replacement) = match var { - Variable::Static(v) => (v.name.clone(), { - match v.value.clone() { - None => { - return Err(ContractError::HydrationError { - msg: "Static msg value is none.".to_string(), - }); - } - Some(val) => (v.name.clone(), { - match v.kind { - VariableKind::Uint => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(val) - } - false => val, - } - ), - VariableKind::Int => match v.encode { + Variable::Static(v) => match v.value.clone() { + None => { + return Err(ContractError::HydrationError { + msg: "Static msg value is none.".to_string(), + }); + } + Some(val) => (v.name.clone(), { + match v.kind { + VariableKind::Uint => format!( + "\"{}\"", + match v.encode { true => { - format!("\"{}\"", base64::encode(val)) + base64::encode(val) } false => val, - }, - VariableKind::Decimal => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(val) - } - false => val, - } - ), - VariableKind::Timestamp => match v.encode { + } + ), + VariableKind::Int => match v.encode { + true => { + format!("\"{}\"", base64::encode(val)) + } + false => val, + }, + VariableKind::Decimal => format!( + "\"{}\"", + match v.encode { true => { - format!("\"{}\"", base64::encode(val)) + base64::encode(val) } false => val, - }, - VariableKind::Bool => match v.encode { + } + ), + VariableKind::Timestamp => match v.encode { + true => { + format!("\"{}\"", base64::encode(val)) + } + false => val, + }, + VariableKind::Bool => match v.encode { + true => { + format!("\"{}\"", base64::encode(val)) + } + false => val, + }, + VariableKind::Amount => format!( + "\"{}\"", + match v.encode { true => { - format!("\"{}\"", base64::encode(val)) + base64::encode(val) } false => val, - }, - VariableKind::Amount => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(val) - } - false => val, - } - ), - VariableKind::String => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(val) - } - false => val, - } - ), - VariableKind::Asset => format!( - "\"{}\"", - match v.encode { - true => { - base64::encode(val) - } - false => val, + } + ), + VariableKind::String => format!( + "\"{}\"", + match v.encode { + true => { + base64::encode(val) } - ), - VariableKind::Json => match v.encode { + false => val, + } + ), + VariableKind::Asset => format!( + "\"{}\"", + match v.encode { true => { - format!("\"{}\"", base64::encode(val)) + base64::encode(val) } false => val, - }, - } - }), - } - }), + } + ), + VariableKind::Json => match v.encode { + true => { + format!("\"{}\"", base64::encode(val)) + } + false => val, + }, + } + }), + }, Variable::External(v) => match v.value.clone() { None => { return Err(ContractError::HydrationError { @@ -567,13 +563,20 @@ fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), Contrac fn get_replacement_in_string(var: &Variable) -> Result<(String, String), ContractError> { let (name, replacement) = match var { - Variable::Static(v) => ( - v.name.clone(), - match v.encode { - true => base64::encode(v.value.clone()), - false => v.value.clone(), - }, - ), + Variable::Static(v) => match v.value.clone() { + None => { + return Err(ContractError::HydrationError { + msg: "Static msg value is none.".to_string(), + }); + } + Some(val) => ( + v.name.clone(), + match v.encode { + true => base64::encode(val), + false => val, + }, + ), + }, Variable::External(v) => match v.value.clone() { None => { return Err(ContractError::HydrationError { @@ -756,6 +759,7 @@ pub fn apply_var_fn( env: Env, vars: Vec, status: JobStatus, + warp_account_addr: Option, ) -> Result { let mut res = vec![]; for var in vars.clone() { @@ -778,8 +782,10 @@ pub fn apply_var_fn( msg: "Static Uint function mismatch.".to_string(), }); } - v.value = resolve_num_value_uint(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_uint(deps, env.clone(), nv, &vars)? + .to_string(), + ); } FnValue::Int(nv) => { if v.kind != VariableKind::Int { @@ -787,8 +793,10 @@ pub fn apply_var_fn( msg: "Static Int function mismatch.".to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { @@ -796,9 +804,10 @@ pub fn apply_var_fn( msg: "Static Decimal function mismatch.".to_string(), }); } - v.value = + v.value = Some( resolve_num_value_decimal(deps, env.clone(), nv, &vars)? - .to_string(); + .to_string(), + ); } FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { @@ -806,8 +815,10 @@ pub fn apply_var_fn( msg: "Static Timestamp function mismatch.".to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { @@ -816,8 +827,10 @@ pub fn apply_var_fn( .to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } FnValue::Bool(val) => { if v.kind != VariableKind::Bool { @@ -825,8 +838,27 @@ pub fn apply_var_fn( msg: "Static Bool function mismatch.".to_string(), }); } - v.value = resolve_ref_bool(deps, env.clone(), val, &vars)? - .to_string(); + v.value = Some( + resolve_ref_bool(deps, env.clone(), val, &vars)? + .to_string(), + ); + } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "Static String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ); } }, }, @@ -839,8 +871,10 @@ pub fn apply_var_fn( msg: "Static Uint function mismatch.".to_string(), }); } - v.value = resolve_num_value_uint(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_uint(deps, env.clone(), nv, &vars)? + .to_string(), + ); } FnValue::Int(nv) => { if v.kind != VariableKind::Int { @@ -848,8 +882,10 @@ pub fn apply_var_fn( msg: "Static Int function mismatch.".to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } FnValue::Decimal(nv) => { if v.kind != VariableKind::Decimal { @@ -857,9 +893,10 @@ pub fn apply_var_fn( msg: "Static Uint function mismatch.".to_string(), }); } - v.value = + v.value = Some( resolve_num_value_decimal(deps, env.clone(), nv, &vars)? - .to_string() + .to_string(), + ); } FnValue::Timestamp(nv) => { if v.kind != VariableKind::Int { @@ -867,8 +904,10 @@ pub fn apply_var_fn( msg: "Static Timestamp function mismatch.".to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } FnValue::BlockHeight(nv) => { if v.kind != VariableKind::Int { @@ -877,8 +916,10 @@ pub fn apply_var_fn( .to_string(), }); } - v.value = resolve_num_value_int(deps, env.clone(), nv, &vars)? - .to_string(); + v.value = Some( + resolve_num_value_int(deps, env.clone(), nv, &vars)? + .to_string(), + ); } FnValue::Bool(val) => { if v.kind != VariableKind::Bool { @@ -886,8 +927,27 @@ pub fn apply_var_fn( msg: "Static Bool function mismatch.".to_string(), }); } - v.value = resolve_ref_bool(deps, env.clone(), val, &vars)? - .to_string(); + v.value = Some( + resolve_ref_bool(deps, env.clone(), val, &vars)? + .to_string(), + ); + } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "Static String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ); } }, }, @@ -980,6 +1040,23 @@ pub fn apply_var_fn( .to_string(), ) } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "External String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ) + } }, }, JobStatus::Failed => match update_fn.on_error { @@ -1053,6 +1130,23 @@ pub fn apply_var_fn( .to_string(), ) } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "External String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ) + } }, }, _ => { @@ -1143,6 +1237,23 @@ pub fn apply_var_fn( .to_string(), ) } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "Query String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ) + } }, }, JobStatus::Failed => match update_fn.on_error { @@ -1214,6 +1325,23 @@ pub fn apply_var_fn( .to_string(), ) } + FnValue::String(val) => { + if v.kind != VariableKind::String { + return Err(ContractError::FunctionError { + msg: "Query String function mismatch.".to_string(), + }); + } + v.value = Some( + resolve_string_value( + deps, + env.clone(), + val, + &vars, + warp_account_addr.clone(), + )? + .to_string(), + ) + } }, }, _ => { @@ -1330,45 +1458,52 @@ fn get_var_name(var: &Variable) -> String { pub fn vars_valid(vars: &Vec) -> bool { for var in vars { match var { - Variable::Static(v) => match v.kind { - VariableKind::String => {} - VariableKind::Uint => { - if Uint256::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Int => { - if i128::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Decimal => { - if Decimal256::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Timestamp => { - if i128::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Bool => { - if bool::from_str(&v.value).is_err() { - return false; - } - } - VariableKind::Amount => { - if Uint128::from_str(&v.value).is_err() { - return false; - } + Variable::Static(v) => { + if v.reinitialize && v.update_fn.is_some() { + return false; } - VariableKind::Asset => { - if v.value.is_empty() { - return false; + if let Some(val) = v.value.clone() { + match v.kind { + VariableKind::String => {} + VariableKind::Uint => { + if Uint256::from_str(&val).is_err() { + return false; + } + } + VariableKind::Int => { + if i128::from_str(&val).is_err() { + return false; + } + } + VariableKind::Decimal => { + if Decimal256::from_str(&val).is_err() { + return false; + } + } + VariableKind::Timestamp => { + if i128::from_str(&val).is_err() { + return false; + } + } + VariableKind::Bool => { + if bool::from_str(&val).is_err() { + return false; + } + } + VariableKind::Amount => { + if Uint128::from_str(&val).is_err() { + return false; + } + } + VariableKind::Asset => { + if val.is_empty() { + return false; + } + } + VariableKind::Json => {} } } - VariableKind::Json => {} - }, + } Variable::External(v) => { if v.reinitialize && v.update_fn.is_some() { return false; diff --git a/packages/resolver/src/condition.rs b/packages/resolver/src/condition.rs index c4c34cf2..250c3810 100644 --- a/packages/resolver/src/condition.rs +++ b/packages/resolver/src/condition.rs @@ -36,7 +36,6 @@ pub enum Value { Ref(String), } - #[cw_serde] pub enum StringValue { Simple(String), diff --git a/packages/resolver/src/lib.rs b/packages/resolver/src/lib.rs index cd38fc6f..6c52d3bd 100644 --- a/packages/resolver/src/lib.rs +++ b/packages/resolver/src/lib.rs @@ -65,6 +65,7 @@ pub struct ExecuteResolveConditionMsg { pub struct ExecuteApplyVarFnMsg { pub vars: String, pub status: JobStatus, + pub warp_account_addr: Option, } #[cw_serde] @@ -106,6 +107,7 @@ pub struct QueryResolveConditionMsg { pub struct QueryApplyVarFnMsg { pub vars: String, pub status: JobStatus, + pub warp_account_addr: Option, } #[cw_serde] From da110b816dbb6d7e66d5ea1a63e5f63ebade8e5c Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Sun, 24 Sep 2023 01:29:05 -0700 Subject: [PATCH 026/133] nit --- contracts/warp-resolver/src/util/condition.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/contracts/warp-resolver/src/util/condition.rs b/contracts/warp-resolver/src/util/condition.rs index 373d5c30..adb37ce1 100644 --- a/contracts/warp-resolver/src/util/condition.rs +++ b/contracts/warp-resolver/src/util/condition.rs @@ -308,22 +308,25 @@ pub fn resolve_string_value( match value { StringValue::Simple(value) => Ok(value), StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), - StringValue::Env(value) => resolve_string_value_env(value, warp_account_addr), + StringValue::Env(value) => resolve_string_value_env(deps, value, warp_account_addr), } } pub fn resolve_string_value_env( + deps: Deps, value: StringEnvValue, warp_account_addr: Option, ) -> Result { - if (warp_account_addr).is_none() { - return Err(ContractError::HydrationError { - msg: "Warp account addr not found.".to_string(), - }); - } - // TODO: add warp_account_addr validation match value { - StringEnvValue::WarpAccountAddr => Ok(warp_account_addr.unwrap()), + StringEnvValue::WarpAccountAddr => match warp_account_addr { + Some(addr) => { + deps.api.addr_validate(&addr)?; + Ok(addr) + } + None => Err(ContractError::HydrationError { + msg: "Warp account addr not found.".to_string(), + }), + }, } } From 9f52fdb21f12b936614bef25610fbae20def4726 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Sun, 24 Sep 2023 10:23:40 -0700 Subject: [PATCH 027/133] update test --- contracts/warp-resolver/src/tests.rs | 51 +++++++++++++++----- contracts/warp-resolver/src/util/variable.rs | 4 +- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/contracts/warp-resolver/src/tests.rs b/contracts/warp-resolver/src/tests.rs index 01ea73da..5a854c4e 100644 --- a/contracts/warp-resolver/src/tests.rs +++ b/contracts/warp-resolver/src/tests.rs @@ -1,10 +1,12 @@ -use resolver::condition::{StringEnvValue, StringValue}; +use resolver::condition::{NumValue, StringEnvValue, StringValue}; use schemars::_serde_json::json; use crate::util::variable::{hydrate_msgs, hydrate_vars}; use cosmwasm_std::{testing::mock_env, WasmQuery}; -use cosmwasm_std::{to_binary, BankQuery, Binary, ContractResult, CosmosMsg, OwnedDeps, WasmMsg}; +use cosmwasm_std::{ + to_binary, BankQuery, Binary, ContractResult, CosmosMsg, OwnedDeps, Uint256, WasmMsg, +}; use crate::contract::query; use cosmwasm_schema::cw_serde; @@ -454,14 +456,14 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { let dummy_warp_account_addr = "terra1".to_string(); let json_str = serde_json_wasm::to_string(&TestStruct { - test: format!("$warp.variable.{}", "var1"), + test: format!("$warp.variable.{}", "var2"), }) .unwrap(); - let raw_str = r#"{"test":"static_value_1"}"#.to_string(); + let raw_str = r#"{"test":"100"}"#.to_string(); let encoded_val = base64::encode(raw_str.clone()); - assert_eq!(encoded_val, "eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ=="); + assert_eq!(encoded_val, "eyJ0ZXN0IjoiMTAwIn0="); // ============ TEST HYDRATED VALUE ============ @@ -477,6 +479,16 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { let var2 = Variable::Static(StaticVariable { name: "var2".to_string(), + kind: VariableKind::Uint, + value: None, + init_fn: FnValue::Uint(NumValue::Simple(Uint256::from(100 as u64))), + reinitialize: false, + update_fn: None, + encode: false, + }); + + let var3 = Variable::Static(StaticVariable { + name: "var3".to_string(), kind: VariableKind::String, value: None, init_fn: FnValue::String(StringValue::Simple(json_str.clone())), @@ -485,8 +497,8 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { encode: true, }); - let var3 = Variable::Static(StaticVariable { - name: "var3".to_string(), + let var4 = Variable::Static(StaticVariable { + name: "var4".to_string(), kind: VariableKind::String, value: None, init_fn: FnValue::String(StringValue::Env(StringEnvValue::WarpAccountAddr)), @@ -495,7 +507,7 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { encode: false, }); - let vars = vec![var1, var2, var3]; + let vars = vec![var1, var2, var3, var4]; let hydrated_vars = hydrate_vars( deps.as_ref(), env, @@ -511,13 +523,23 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { Variable::Static(static_var) => { assert_eq!( String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), - raw_str + "100".to_string() ) } _ => panic!("Expected static variable"), }; let hydrated_var3 = hydrated_vars[2].clone(); match hydrated_var3.clone() { + Variable::Static(static_var) => { + assert_eq!( + String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), + raw_str + ) + } + _ => panic!("Expected static variable"), + }; + let hydrated_var4 = hydrated_vars[3].clone(); + match hydrated_var4.clone() { Variable::Static(static_var) => { assert_eq!( String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), @@ -531,13 +553,16 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { let msgs = r#"[ - {"wasm":{"execute":{"contract_addr":"$warp.variable.var1","msg":"eyJ0ZXN0Ijoic3RhdGljX3ZhbHVlXzEifQ==","funds":[]}}}, - {"wasm":{"execute":{"contract_addr":"$warp.variable.var3","msg":"$warp.variable.var2","funds":[]}}} + {"wasm":{"execute":{"contract_addr":"$warp.variable.var1","msg":"eyJ0ZXN0IjoiMTAwIn0=","funds":[]}}}, + {"wasm":{"execute":{"contract_addr":"$warp.variable.var4","msg":"$warp.variable.var3","funds":[]}}} ]"# .to_string(); - let hydrated_msgs = - hydrate_msgs(msgs, vec![hydrated_var1, hydrated_var2, hydrated_var3]).unwrap(); + let hydrated_msgs = hydrate_msgs( + msgs, + vec![hydrated_var1, hydrated_var2, hydrated_var3, hydrated_var4], + ) + .unwrap(); assert_eq!( hydrated_msgs[0], diff --git a/contracts/warp-resolver/src/util/variable.rs b/contracts/warp-resolver/src/util/variable.rs index 03830285..093a2074 100644 --- a/contracts/warp-resolver/src/util/variable.rs +++ b/contracts/warp-resolver/src/util/variable.rs @@ -142,7 +142,7 @@ pub fn hydrate_vars( } _ => { return Err(ContractError::HydrationError { - msg: "Variable init_fn is not of type FnValue::String." + msg: "1Variable init_fn is not of type FnValue::String." .to_string(), }) } @@ -182,7 +182,7 @@ pub fn hydrate_vars( return Err(ContractError::HydrationError { msg: "Variable init_fn is not of type FnValue::String." .to_string(), - }) + }); } }, } From 890bc47e3ee815eb1943fe9a9a23ca40618edcaf Mon Sep 17 00:00:00 2001 From: Vlad Date: Mon, 11 Sep 2023 11:55:48 -0400 Subject: [PATCH 028/133] Revert "ok instead of error on false condition resolution" This reverts commit 249127f07669d4f86ec1ccd9471f347a28ef003b. --- contracts/warp-controller/src/execute/job.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 12bc6629..e007ff8b 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -415,7 +415,6 @@ pub fn execute_job( .add_submessages(submsgs) .add_message(reward_msg) .add_attribute("action", "execute_job") - .add_attribute("condition", "true") .add_attribute("executor", info.sender) .add_attribute("job_id", job.id) .add_attribute("job_reward", job.reward) From fcc204c09f1cdad666b69973bb5b43ca353de98a Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 25 Sep 2023 09:07:24 -0700 Subject: [PATCH 029/133] address comment --- contracts/warp-controller/src/contract.rs | 1 + contracts/warp-controller/src/execute/job.rs | 1 + contracts/warp-resolver/src/contract.rs | 4 +- contracts/warp-resolver/src/util/condition.rs | 146 +++++++----------- contracts/warp-resolver/src/util/variable.rs | 10 +- packages/resolver/src/condition.rs | 10 +- packages/resolver/src/lib.rs | 2 + 7 files changed, 66 insertions(+), 108 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 3db69814..cb62f8ba 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -390,6 +390,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result = serde_json_wasm::from_str(&data.vars).map_err(|e| StdError::generic_err(e.to_string()))?; - resolve_cond(deps, env, condition, &vars).map_err(|e| StdError::generic_err(e.to_string())) + resolve_cond(deps, env, condition, &vars, data.warp_account_addr) + .map_err(|e| StdError::generic_err(e.to_string())) } fn query_apply_var_fn(deps: Deps, env: Env, data: QueryApplyVarFnMsg) -> StdResult { diff --git a/contracts/warp-resolver/src/util/condition.rs b/contracts/warp-resolver/src/util/condition.rs index adb37ce1..b2db2a75 100644 --- a/contracts/warp-resolver/src/util/condition.rs +++ b/contracts/warp-resolver/src/util/condition.rs @@ -10,7 +10,7 @@ use json_codec_wasm::Decoder; use resolver::condition::{ BlockExpr, Condition, DecimalFnOp, Expr, GenExpr, IntFnOp, NumEnvValue, NumExprOp, NumExprValue, NumFnValue, NumOp, NumValue, StringEnvValue, StringOp, StringValue, TimeExpr, - TimeOp, Value, + TimeOp, }; use resolver::variable::{QueryExpr, Variable}; use std::str::FromStr; @@ -20,11 +20,12 @@ pub fn resolve_cond( env: Env, cond: Condition, vars: &Vec, + warp_account_addr: Option, ) -> Result { match cond { Condition::And(conds) => { for cond in conds { - if !resolve_cond(deps, env.clone(), *cond, vars)? { + if !resolve_cond(deps, env.clone(), *cond, vars, warp_account_addr.clone())? { return Ok(false); } } @@ -32,14 +33,14 @@ pub fn resolve_cond( } Condition::Or(conds) => { for cond in conds { - if resolve_cond(deps, env.clone(), *cond, vars)? { + if resolve_cond(deps, env.clone(), *cond, vars, warp_account_addr.clone())? { return Ok(true); } } Ok(false) } - Condition::Not(cond) => Ok(!resolve_cond(deps, env, *cond, vars)?), - Condition::Expr(expr) => Ok(resolve_expr(deps, env, *expr, vars)?), + Condition::Not(cond) => Ok(!resolve_cond(deps, env, *cond, vars, warp_account_addr)?), + Condition::Expr(expr) => Ok(resolve_expr(deps, env, *expr, vars, warp_account_addr)?), } } @@ -48,9 +49,10 @@ pub fn resolve_expr( env: Env, expr: Expr, vars: &Vec, + warp_account_addr: Option, ) -> Result { match expr { - Expr::String(expr) => resolve_string_expr(deps, env, expr, vars), + Expr::String(expr) => resolve_string_expr(deps, env, expr, vars, warp_account_addr), Expr::Uint(expr) => resolve_uint_expr(deps, env, expr, vars), Expr::Int(expr) => resolve_int_expr(deps, env, expr, vars), Expr::Decimal(expr) => resolve_decimal_expr(deps, env, expr, vars), @@ -298,68 +300,6 @@ pub fn resolve_num_env_uint( } } -pub fn resolve_string_value( - deps: Deps, - env: Env, - value: StringValue, - vars: &Vec, - warp_account_addr: Option, -) -> Result { - match value { - StringValue::Simple(value) => Ok(value), - StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), - StringValue::Env(value) => resolve_string_value_env(deps, value, warp_account_addr), - } -} - -pub fn resolve_string_value_env( - deps: Deps, - value: StringEnvValue, - warp_account_addr: Option, -) -> Result { - match value { - StringEnvValue::WarpAccountAddr => match warp_account_addr { - Some(addr) => { - deps.api.addr_validate(&addr)?; - Ok(addr) - } - None => Err(ContractError::HydrationError { - msg: "Warp account addr not found.".to_string(), - }), - }, - } -} - -pub fn resolve_string_value_asset( - deps: Deps, - env: Env, - value: StringValue, - vars: &Vec, -) -> Result { - match value { - StringValue::Simple(value) => Ok(value), - StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), - StringValue::Env(value) => Err(ContractError::HydrationError { - msg: format!("String Env value not apply to string asset: {:?}", value), - }), - } -} - -pub fn resolve_string_value_json( - deps: Deps, - env: Env, - value: StringValue, - vars: &Vec, -) -> Result { - match value { - StringValue::Simple(value) => Ok(value), - StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), - StringValue::Env(value) => Err(ContractError::HydrationError { - msg: format!("String Env value not apply to string json: {:?}", value), - }), - } -} - pub fn resolve_decimal_expr( deps: Deps, env: Env, @@ -555,34 +495,52 @@ pub fn resolve_decimal_op( pub fn resolve_string_expr( deps: Deps, env: Env, - expr: GenExpr, StringOp>, + expr: GenExpr, StringOp>, vars: &Vec, + warp_account_addr: Option, ) -> Result { - match (expr.left, expr.right) { - (Value::Simple(left), Value::Simple(right)) => { - Ok(resolve_str_op(deps, env, left, right, expr.op)) - } - (Value::Simple(left), Value::Ref(right)) => Ok(resolve_str_op( - deps, - env.clone(), - left, - resolve_ref_string(deps, env, right, vars)?, - expr.op, - )), - (Value::Ref(left), Value::Simple(right)) => Ok(resolve_str_op( - deps, - env.clone(), - resolve_ref_string(deps, env, left, vars)?, - right, - expr.op, - )), - (Value::Ref(left), Value::Ref(right)) => Ok(resolve_str_op( - deps, - env.clone(), - resolve_ref_string(deps, env.clone(), left, vars)?, - resolve_ref_string(deps, env, right, vars)?, - expr.op, - )), + let left = match expr.left { + StringValue::Simple(left) => left, + StringValue::Ref(left) => resolve_ref_string(deps, env.clone(), left, vars)?, + StringValue::Env(left) => resolve_string_value_env(deps, left, warp_account_addr.clone())?, + }; + let right = match expr.right { + StringValue::Simple(right) => right, + StringValue::Ref(right) => resolve_ref_string(deps, env.clone(), right, vars)?, + StringValue::Env(right) => resolve_string_value_env(deps, right, warp_account_addr)?, + }; + Ok(resolve_str_op(deps, env, left, right, expr.op)) +} + +pub fn resolve_string_value( + deps: Deps, + env: Env, + value: StringValue, + vars: &Vec, + warp_account_addr: Option, +) -> Result { + match value { + StringValue::Simple(value) => Ok(value), + StringValue::Ref(r) => resolve_ref_string(deps, env, r, vars), + StringValue::Env(value) => resolve_string_value_env(deps, value, warp_account_addr), + } +} + +pub fn resolve_string_value_env( + deps: Deps, + value: StringEnvValue, + warp_account_addr: Option, +) -> Result { + match value { + StringEnvValue::WarpAccountAddr => match warp_account_addr { + Some(addr) => { + deps.api.addr_validate(&addr)?; + Ok(addr) + } + None => Err(ContractError::HydrationError { + msg: "Warp account addr not found.".to_string(), + }), + }, } } diff --git a/contracts/warp-resolver/src/util/variable.rs b/contracts/warp-resolver/src/util/variable.rs index 093a2074..95960bd5 100644 --- a/contracts/warp-resolver/src/util/variable.rs +++ b/contracts/warp-resolver/src/util/variable.rs @@ -14,9 +14,7 @@ use std::str::FromStr; use controller::job::{ExternalInput, JobStatus}; use resolver::variable::{FnValue, QueryExpr, Variable, VariableKind}; -use super::condition::{ - resolve_string_value, resolve_string_value_asset, resolve_string_value_json, -}; +use super::condition::resolve_string_value; pub fn hydrate_vars( deps: Deps, @@ -150,11 +148,12 @@ pub fn hydrate_vars( VariableKind::Asset => match v.init_fn.clone() { FnValue::String(val) => { v.value = Some(replace_in_string( - resolve_string_value_asset( + resolve_string_value( deps, env.clone(), val, &hydrated_vars, + warp_account_addr.clone(), )?, &hydrated_vars, )?) @@ -169,11 +168,12 @@ pub fn hydrate_vars( VariableKind::Json => match v.init_fn.clone() { FnValue::String(val) => { v.value = Some(replace_in_string( - resolve_string_value_json( + resolve_string_value( deps, env.clone(), val, &hydrated_vars, + warp_account_addr.clone(), )?, &hydrated_vars, )?) diff --git a/packages/resolver/src/condition.rs b/packages/resolver/src/condition.rs index 250c3810..1056b756 100644 --- a/packages/resolver/src/condition.rs +++ b/packages/resolver/src/condition.rs @@ -31,15 +31,9 @@ pub struct BlockExpr { } #[cw_serde] -pub enum Value { +pub enum StringValue { Simple(T), Ref(String), -} - -#[cw_serde] -pub enum StringValue { - Simple(String), - Ref(String), Env(StringEnvValue), } @@ -102,7 +96,7 @@ pub enum IntFnOp { #[cw_serde] pub enum Expr { - String(GenExpr, StringOp>), + String(GenExpr, StringOp>), Uint(GenExpr, NumOp>), Int(GenExpr, NumOp>), Decimal(GenExpr, NumOp>), diff --git a/packages/resolver/src/lib.rs b/packages/resolver/src/lib.rs index 6c52d3bd..ef827c9b 100644 --- a/packages/resolver/src/lib.rs +++ b/packages/resolver/src/lib.rs @@ -59,6 +59,7 @@ pub struct ExecuteHydrateVarsMsg { pub struct ExecuteResolveConditionMsg { pub condition: String, pub vars: String, + pub warp_account_addr: Option, } #[cw_serde] @@ -101,6 +102,7 @@ pub struct QueryHydrateVarsMsg { pub struct QueryResolveConditionMsg { pub condition: String, pub vars: String, + pub warp_account_addr: Option, } #[cw_serde] From 2c3b3d35ea13a23dc71079698db9cc2de109df06 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 25 Sep 2023 23:25:53 -0700 Subject: [PATCH 030/133] fmt --- contracts/warp-controller/src/execute/job.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 916efa92..3ed4a0f8 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -403,8 +403,7 @@ pub fn evict_job( ); job_status = JobQueue::sync(&mut deps, env, job.clone())?.status; } else { - job_status = - JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; + job_status = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; cosmos_msgs.append(&mut vec![ //send reward minus fee back to account From 66151a76690b1e9f2cf875f6d897b35ce1a16a98 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 25 Sep 2023 23:31:59 -0700 Subject: [PATCH 031/133] merge latest --- contracts/warp-controller/src/execute/controller.rs | 2 ++ contracts/warp-controller/src/execute/job.rs | 3 +-- contracts/warp-controller/src/state.rs | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/warp-controller/src/execute/controller.rs b/contracts/warp-controller/src/execute/controller.rs index 221ec294..95f06ff2 100644 --- a/contracts/warp-controller/src/execute/controller.rs +++ b/contracts/warp-controller/src/execute/controller.rs @@ -274,6 +274,7 @@ pub fn migrate_pending_jobs( job_key, &Job { id: v1_job.id, + prev_id: None, owner: v1_job.owner, last_update_time: v1_job.last_update_time, name: v1_job.name, @@ -376,6 +377,7 @@ pub fn migrate_finished_jobs( job_key, &Job { id: v1_job.id, + prev_id: None, owner: v1_job.owner, last_update_time: v1_job.last_update_time, name: v1_job.name, diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 4d22a631..4a34058e 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -397,8 +397,7 @@ pub fn evict_job( ); job_status = JobQueue::sync(&mut deps, env, job.clone())?.status; } else { - job_status = - JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; + job_status = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; cosmos_msgs.append(&mut vec![ //send reward minus fee back to account diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 39cb8ae0..8fd8703b 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -107,6 +107,7 @@ impl JobQueue { None => Err(ContractError::JobDoesNotExist {}), Some(job) => Ok(Job { id: job.id, + prev_id: job.prev_id, owner: job.owner, last_update_time: Uint64::new(env.block.time.seconds()), name: job.name, @@ -133,6 +134,7 @@ impl JobQueue { None => Err(ContractError::JobDoesNotExist {}), Some(job) => Ok(Job { id: job.id, + prev_id: job.prev_id, owner: job.owner, last_update_time: if added_reward > config.minimum_reward { Uint64::new(env.block.time.seconds()) @@ -169,6 +171,7 @@ impl JobQueue { let new_job = Job { id: job.id, + prev_id: job.prev_id, owner: job.owner, last_update_time: Uint64::new(env.block.time.seconds()), name: job.name, From cf3b491d711cf433b78c6e979ad2ec89372c8f3c Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:21:16 -0700 Subject: [PATCH 032/133] add account to job struct --- contracts/warp-controller/src/contract.rs | 26 +++++------ .../warp-controller/src/execute/controller.rs | 6 +++ contracts/warp-controller/src/execute/job.rs | 44 +++++++++---------- contracts/warp-controller/src/state.rs | 3 ++ packages/controller/src/job.rs | 3 ++ 5 files changed, 46 insertions(+), 36 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 93342124..0e7b30cc 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -192,7 +192,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { match msg.id { - //account creation + // Account creation 0 => { let reply = msg.result.into_result().map_err(StdError::generic_err)?; @@ -316,7 +316,7 @@ pub fn reply(mut deps: DepsMut, env: Env, msg: Reply) -> Result { let state = STATE.load(deps.storage)?; @@ -338,17 +338,16 @@ pub fn reply(mut deps: DepsMut, env: Env, msg: Reply) -> Result(&QueryRequest::Bank(BankQuery::Balance { - address: account.account.to_string(), + address: finished_job.account.to_string(), denom: config.fee_denom.clone(), }))? .amount @@ -372,7 +371,7 @@ pub fn reply(mut deps: DepsMut, env: Env, msg: Reply) -> Result Result Result Result Result Result MAX_TEXT_LENGTH { @@ -213,9 +212,9 @@ pub fn update_job( if added_reward.u128() > 0 { cw20_send_msgs.push( - //send reward to controller + // Job owner sends additional reward to controller WasmMsg::Execute { - contract_addr: account.account.to_string(), + contract_addr: job.account.to_string(), msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs: vec![CosmosMsg::Bank(BankMsg::Send { to_address: env.contract.address.to_string(), @@ -226,9 +225,9 @@ pub fn update_job( }, ); cw20_send_msgs.push( - //send reward to controller + // Job owner sends fee to fee collector WasmMsg::Execute { - contract_addr: account.account.to_string(), + contract_addr: job.account.to_string(), msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs: vec![CosmosMsg::Bank(BankMsg::Send { to_address: config.fee_collector.to_string(), @@ -263,7 +262,6 @@ pub fn execute_job( let _config = CONFIG.load(deps.storage)?; let config = CONFIG.load(deps.storage)?; let job = JobQueue::get(&deps, data.id.into())?; - let account = ACCOUNTS().load(deps.storage, job.owner.clone())?; if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); @@ -274,7 +272,7 @@ pub fn execute_job( &resolver::QueryMsg::QueryHydrateVars(resolver::QueryHydrateVarsMsg { vars: job.vars, external_inputs: data.external_inputs, - warp_account_addr: Some(account.account.to_string()), + warp_account_addr: Some(job.account.to_string()), }), )?; @@ -283,7 +281,7 @@ pub fn execute_job( &resolver::QueryMsg::QueryResolveCondition(resolver::QueryResolveConditionMsg { condition: job.condition, vars: vars.clone(), - warp_account_addr: Some(account.account.to_string()), + warp_account_addr: Some(job.account.to_string()), }), ); @@ -306,7 +304,7 @@ pub fn execute_job( submsgs.push(SubMsg { id: job.id.u64(), msg: CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: account.account.to_string(), + contract_addr: job.account.to_string(), msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs: deps.querier.query_wasm_smart( config.resolver_address, @@ -323,7 +321,7 @@ pub fn execute_job( }); } - //send reward to executor + // Controller sends reward to executor let reward_msg = BankMsg::Send { to_address: info.sender.to_string(), amount: vec![Coin::new(job.reward.u128(), config.fee_denom)], @@ -348,12 +346,11 @@ pub fn evict_job( let config = CONFIG.load(deps.storage)?; let state = STATE.load(deps.storage)?; let job = JobQueue::get(&deps, data.id.into())?; - let account = ACCOUNTS().load(deps.storage, job.owner.clone())?; let account_amount = deps .querier .query::(&QueryRequest::Bank(BankQuery::Balance { - address: account.account.to_string(), + address: job.account.to_string(), denom: config.fee_denom.clone(), }))? .amount @@ -385,9 +382,9 @@ pub fn evict_job( if job.requeue_on_evict && account_amount >= a { cosmos_msgs.push( - //send reward to evictor + // Controller sends reward to evictor CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: account.account.to_string(), + contract_addr: job.account.to_string(), msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs: vec![CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), @@ -402,13 +399,14 @@ pub fn evict_job( job_status = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; cosmos_msgs.append(&mut vec![ - //send reward minus fee back to account + // Controller sends reward to evictor CosmosMsg::Bank(BankMsg::Send { to_address: info.sender.to_string(), amount: vec![Coin::new(a.u128(), config.fee_denom.clone())], }), + // Controller sends reward minus fee back to account CosmosMsg::Bank(BankMsg::Send { - to_address: account.account.to_string(), + to_address: job.account.to_string(), amount: vec![Coin::new((job.reward - a).u128(), config.fee_denom)], }), ]); diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 8fd8703b..2fd89b20 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -109,6 +109,7 @@ impl JobQueue { id: job.id, prev_id: job.prev_id, owner: job.owner, + account: job.account, last_update_time: Uint64::new(env.block.time.seconds()), name: job.name, description: job.description, @@ -136,6 +137,7 @@ impl JobQueue { id: job.id, prev_id: job.prev_id, owner: job.owner, + account: job.account, last_update_time: if added_reward > config.minimum_reward { Uint64::new(env.block.time.seconds()) } else { @@ -173,6 +175,7 @@ impl JobQueue { id: job.id, prev_id: job.prev_id, owner: job.owner, + account: job.account, last_update_time: Uint64::new(env.block.time.seconds()), name: job.name, description: job.description, diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 0fc3beb2..e40c8ab6 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -25,6 +25,9 @@ pub struct Job { // Exist if job is the follow up job of a recurring job pub prev_id: Option, pub owner: Addr, + // Warp account this job is associated with, job will be executed in the context of it and pay protocol fee from it + // As job creator can have multiple warp accounts (1 main account and infinite sub accounts) + pub account: Addr, pub last_update_time: Uint64, pub name: String, pub description: String, From 180099d98cf7d50aa8c4578b2b20fcb0c0398684 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:23:46 -0700 Subject: [PATCH 033/133] add sub account management to account contract --- contracts/warp-account/src/contract.rs | 221 ++++-------------- contracts/warp-account/src/error.rs | 9 + contracts/warp-account/src/execute/account.rs | 44 ++++ contracts/warp-account/src/execute/ibc.rs | 33 +++ contracts/warp-account/src/execute/mod.rs | 3 + .../warp-account/src/execute/withdraw.rs | 139 +++++++++++ contracts/warp-account/src/lib.rs | 2 + contracts/warp-account/src/query/account.rs | 99 ++++++++ contracts/warp-account/src/query/mod.rs | 1 + contracts/warp-account/src/state.rs | 8 +- contracts/warp-account/src/tests.rs | 6 + .../warp-controller/src/execute/account.rs | 2 + packages/account/src/lib.rs | 106 ++++++++- 13 files changed, 488 insertions(+), 185 deletions(-) create mode 100644 contracts/warp-account/src/execute/account.rs create mode 100644 contracts/warp-account/src/execute/ibc.rs create mode 100644 contracts/warp-account/src/execute/mod.rs create mode 100644 contracts/warp-account/src/execute/withdraw.rs create mode 100644 contracts/warp-account/src/query/account.rs create mode 100644 contracts/warp-account/src/query/mod.rs diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index c4770d86..fca13e7c 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -1,18 +1,9 @@ use crate::state::CONFIG; -use crate::ContractError; -use account::{ - Config, ExecuteMsg, IbcTransferMsg, InstantiateMsg, MigrateMsg, QueryMsg, TimeoutBlock, - WithdrawAssetsMsg, -}; -use controller::account::{AssetInfo, Cw721ExecuteMsg}; -use cosmwasm_std::CosmosMsg::Stargate; +use crate::{execute, query, ContractError}; +use account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use cosmwasm_std::{ - entry_point, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, Uint128, WasmMsg, + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; -use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use cw721::{Cw721QueryMsg, OwnerOfResponse}; -use prost::Message; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -21,17 +12,31 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { + let instantiated_account_addr = env.contract.address; + let main_account_addr = if msg.is_sub_account.unwrap_or(false) { + instantiated_account_addr.clone() + } else { + deps.api.addr_validate(&msg.main_account_addr.unwrap())? + }; + CONFIG.save( deps.storage, &Config { owner: deps.api.addr_validate(&msg.owner)?, warp_addr: info.sender, + is_sub_account: msg.is_sub_account.unwrap_or(false), + main_account_addr: main_account_addr.clone(), }, )?; Ok(Response::new() .add_attribute("action", "instantiate") - .add_attribute("contract_addr", env.contract.address) + .add_attribute("contract_addr", instantiated_account_addr) + .add_attribute( + "is_sub_account", + format!("{}", msg.is_sub_account.unwrap_or(false)), + ) + .add_attribute("main_account_addr", main_account_addr) .add_attribute("owner", msg.owner) .add_attribute("funds", serde_json_wasm::to_string(&info.funds)?) .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?) @@ -53,18 +58,34 @@ pub fn execute( ExecuteMsg::Generic(data) => Ok(Response::new() .add_messages(data.msgs) .add_attribute("action", "generic")), - ExecuteMsg::WithdrawAssets(data) => withdraw_assets(deps, env, info, data), - ExecuteMsg::IbcTransfer(data) => ibc_transfer(deps, env, info, data), + ExecuteMsg::WithdrawAssets(data) => { + execute::withdraw::withdraw_assets(deps, env, info, data) + } + ExecuteMsg::IbcTransfer(data) => execute::ibc::ibc_transfer(env, data), + ExecuteMsg::OccupySubAccount(data) => execute::account::occupy_sub_account(deps, env, data), + ExecuteMsg::FreeSubAccount(data) => execute::account::free_sub_account(deps, env, data), } } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::Config => { - let config = CONFIG.load(deps.storage)?; - to_binary(&config) + QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), + QueryMsg::QueryOccupiedSubAccounts(data) => { + to_binary(&query::account::query_occupied_sub_accounts(deps, data)?) + } + QueryMsg::QueryFreeSubAccounts(data) => { + to_binary(&query::account::query_free_sub_accounts(deps, data)?) } + QueryMsg::QueryFirstFreeSubAccount(_) => { + to_binary(&query::account::query_first_free_sub_account(deps)?) + } + QueryMsg::QueryIsSubAccountOwnedAndOccupied(data) => to_binary( + &query::account::query_is_sub_account_owned_and_occupied(deps, data)?, + ), + QueryMsg::QueryIsSubAccountOwnedAndFree(data) => to_binary( + &query::account::query_is_sub_account_owned_and_free(deps, data)?, + ), } } @@ -72,165 +93,3 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { Ok(Response::new()) } - -pub fn ibc_transfer( - _deps: DepsMut, - env: Env, - _info: MessageInfo, - msg: IbcTransferMsg, -) -> Result { - let mut transfer_msg = msg.transfer_msg.clone(); - - if msg.timeout_block_delta.is_some() && msg.transfer_msg.timeout_block.is_some() { - let block = transfer_msg.timeout_block.unwrap(); - transfer_msg.timeout_block = Some(TimeoutBlock { - revision_number: Some(block.revision_number()), - revision_height: Some(env.block.height + msg.timeout_block_delta.unwrap()), - }) - } - - if msg.timeout_timestamp_seconds_delta.is_some() { - transfer_msg.timeout_timestamp = Some( - env.block - .time - .plus_seconds( - env.block.time.seconds() + msg.timeout_timestamp_seconds_delta.unwrap(), - ) - .nanos(), - ); - } - - Ok(Response::new().add_message(Stargate { - type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - value: transfer_msg.encode_to_vec().into(), - })) -} - -pub fn withdraw_assets( - deps: DepsMut, - env: Env, - info: MessageInfo, - data: WithdrawAssetsMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.warp_addr { - return Err(ContractError::Unauthorized {}); - } - - let mut withdraw_msgs: Vec = vec![]; - - for asset_info in &data.asset_infos { - match asset_info { - AssetInfo::Native(denom) => { - let withdraw_native_msg = - withdraw_asset_native(deps.as_ref(), env.clone(), &config.owner, denom)?; - - match withdraw_native_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw20(addr) => { - let withdraw_cw20_msg = - withdraw_asset_cw20(deps.as_ref(), env.clone(), &config.owner, addr)?; - - match withdraw_cw20_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw721(addr, token_id) => { - let withdraw_cw721_msg = - withdraw_asset_cw721(deps.as_ref(), &config.owner, addr, token_id)?; - match withdraw_cw721_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - } - } - - Ok(Response::new() - .add_messages(withdraw_msgs) - .add_attribute("action", "withdraw_assets") - .add_attribute("assets", serde_json_wasm::to_string(&data.asset_infos)?)) -} - -fn withdraw_asset_native( - deps: Deps, - env: Env, - owner: &Addr, - denom: &String, -) -> StdResult> { - let amount = deps.querier.query_balance(env.contract.address, denom)?; - - let res = if amount.amount > Uint128::zero() { - Some(CosmosMsg::Bank(BankMsg::Send { - to_address: owner.to_string(), - amount: vec![amount], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw20( - deps: Deps, - env: Env, - owner: &Addr, - token: &Addr, -) -> StdResult> { - let amount: BalanceResponse = deps.querier.query_wasm_smart( - token.to_string(), - &cw20::Cw20QueryMsg::Balance { - address: env.contract.address.to_string(), - }, - )?; - - let res = if amount.balance > Uint128::zero() { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: owner.to_string(), - amount: amount.balance, - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw721( - deps: Deps, - owner: &Addr, - token: &Addr, - token_id: &String, -) -> StdResult> { - let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( - token.to_string(), - &Cw721QueryMsg::OwnerOf { - token_id: token_id.to_string(), - include_expired: None, - }, - )?; - - let res = if owner_query.owner == *owner { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw721ExecuteMsg::TransferNft { - recipient: owner.to_string(), - token_id: token_id.to_string(), - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} diff --git a/contracts/warp-account/src/error.rs b/contracts/warp-account/src/error.rs index f8855692..81db9d26 100644 --- a/contracts/warp-account/src/error.rs +++ b/contracts/warp-account/src/error.rs @@ -37,6 +37,15 @@ pub enum ContractError { #[error("Error resolving JSON path")] ResolveError {}, + + #[error("Sub account already occupied")] + SubAccountAlreadyOccupiedError {}, + + #[error("Sub account already free")] + SubAccountAlreadyFreeError {}, + + #[error("Sub account should be occupied but it is free")] + SubAccountNotOccupiedError {}, } impl From for ContractError { diff --git a/contracts/warp-account/src/execute/account.rs b/contracts/warp-account/src/execute/account.rs new file mode 100644 index 00000000..8748883c --- /dev/null +++ b/contracts/warp-account/src/execute/account.rs @@ -0,0 +1,44 @@ +use crate::state::{FREE_SUB_ACCOUNTS, OCCUPIED_SUB_ACCOUNTS}; +use crate::ContractError; +use account::{FreeSubAccountMsg, OccupySubAccountMsg}; +use cosmwasm_std::{DepsMut, Env, Response}; + +pub fn occupy_sub_account( + deps: DepsMut, + env: Env, + data: OccupySubAccountMsg, +) -> Result { + // We do not add default account to occupied sub accounts + if data.sub_account_addr == env.contract.address { + return Ok(Response::new()); + } + FREE_SUB_ACCOUNTS.remove(deps.storage, data.sub_account_addr.clone()); + OCCUPIED_SUB_ACCOUNTS.update(deps.storage, data.sub_account_addr.clone(), |s| match s { + None => Ok(data.job_id.u64()), + Some(_) => Err(ContractError::SubAccountAlreadyOccupiedError {}), + })?; + Ok(Response::new() + .add_attribute("action", "occupy_sub_account") + .add_attribute("sub_account_addr", data.sub_account_addr) + .add_attribute("job_id", data.job_id)) +} + +pub fn free_sub_account( + deps: DepsMut, + env: Env, + data: FreeSubAccountMsg, +) -> Result { + // We do not add default account to free sub accounts + if data.sub_account_addr == env.contract.address { + return Ok(Response::new()); + } + OCCUPIED_SUB_ACCOUNTS.remove(deps.storage, data.sub_account_addr.clone()); + FREE_SUB_ACCOUNTS.update(deps.storage, data.sub_account_addr.clone(), |s| match s { + // value is a dummy data because there is no built in support for set in cosmwasm + None => Ok(0), + Some(_) => Err(ContractError::SubAccountAlreadyFreeError {}), + })?; + Ok(Response::new() + .add_attribute("action", "free_sub_account") + .add_attribute("sub_account_addr", data.sub_account_addr)) +} diff --git a/contracts/warp-account/src/execute/ibc.rs b/contracts/warp-account/src/execute/ibc.rs new file mode 100644 index 00000000..93aff4b6 --- /dev/null +++ b/contracts/warp-account/src/execute/ibc.rs @@ -0,0 +1,33 @@ +use crate::ContractError; +use account::{IbcTransferMsg, TimeoutBlock}; +use cosmwasm_std::CosmosMsg::Stargate; +use cosmwasm_std::{Env, Response}; +use prost::Message; + +pub fn ibc_transfer(env: Env, data: IbcTransferMsg) -> Result { + let mut transfer_msg = data.transfer_msg.clone(); + + if data.timeout_block_delta.is_some() && data.transfer_msg.timeout_block.is_some() { + let block = transfer_msg.timeout_block.unwrap(); + transfer_msg.timeout_block = Some(TimeoutBlock { + revision_number: Some(block.revision_number()), + revision_height: Some(env.block.height + data.timeout_block_delta.unwrap()), + }) + } + + if data.timeout_timestamp_seconds_delta.is_some() { + transfer_msg.timeout_timestamp = Some( + env.block + .time + .plus_seconds( + env.block.time.seconds() + data.timeout_timestamp_seconds_delta.unwrap(), + ) + .nanos(), + ); + } + + Ok(Response::new().add_message(Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: transfer_msg.encode_to_vec().into(), + })) +} diff --git a/contracts/warp-account/src/execute/mod.rs b/contracts/warp-account/src/execute/mod.rs new file mode 100644 index 00000000..9a01495c --- /dev/null +++ b/contracts/warp-account/src/execute/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod ibc; +pub(crate) mod account; +pub(crate) mod withdraw; diff --git a/contracts/warp-account/src/execute/withdraw.rs b/contracts/warp-account/src/execute/withdraw.rs new file mode 100644 index 00000000..7ebe27d6 --- /dev/null +++ b/contracts/warp-account/src/execute/withdraw.rs @@ -0,0 +1,139 @@ +use crate::state::CONFIG; +use crate::ContractError; +use account::WithdrawAssetsMsg; +use controller::account::{AssetInfo, Cw721ExecuteMsg}; +use cosmwasm_std::{ + to_binary, Addr, BankMsg, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + Uint128, WasmMsg, +}; +use cw20::{BalanceResponse, Cw20ExecuteMsg}; +use cw721::{Cw721QueryMsg, OwnerOfResponse}; + +pub fn withdraw_assets( + deps: DepsMut, + env: Env, + info: MessageInfo, + data: WithdrawAssetsMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner && info.sender != config.warp_addr { + return Err(ContractError::Unauthorized {}); + } + + let mut withdraw_msgs: Vec = vec![]; + + for asset_info in &data.asset_infos { + match asset_info { + AssetInfo::Native(denom) => { + let withdraw_native_msg = + withdraw_asset_native(deps.as_ref(), env.clone(), &config.owner, denom)?; + + match withdraw_native_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw20(addr) => { + let withdraw_cw20_msg = + withdraw_asset_cw20(deps.as_ref(), env.clone(), &config.owner, addr)?; + + match withdraw_cw20_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw721(addr, token_id) => { + let withdraw_cw721_msg = + withdraw_asset_cw721(deps.as_ref(), &config.owner, addr, token_id)?; + match withdraw_cw721_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + } + } + + Ok(Response::new() + .add_messages(withdraw_msgs) + .add_attribute("action", "withdraw_assets") + .add_attribute("assets", serde_json_wasm::to_string(&data.asset_infos)?)) +} + +fn withdraw_asset_native( + deps: Deps, + env: Env, + owner: &Addr, + denom: &String, +) -> StdResult> { + let amount = deps.querier.query_balance(env.contract.address, denom)?; + + let res = if amount.amount > Uint128::zero() { + Some(CosmosMsg::Bank(BankMsg::Send { + to_address: owner.to_string(), + amount: vec![amount], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw20( + deps: Deps, + env: Env, + owner: &Addr, + token: &Addr, +) -> StdResult> { + let amount: BalanceResponse = deps.querier.query_wasm_smart( + token.to_string(), + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + let res = if amount.balance > Uint128::zero() { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: owner.to_string(), + amount: amount.balance, + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw721( + deps: Deps, + owner: &Addr, + token: &Addr, + token_id: &String, +) -> StdResult> { + let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( + token.to_string(), + &Cw721QueryMsg::OwnerOf { + token_id: token_id.to_string(), + include_expired: None, + }, + )?; + + let res = if owner_query.owner == *owner { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw721ExecuteMsg::TransferNft { + recipient: owner.to_string(), + token_id: token_id.to_string(), + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} diff --git a/contracts/warp-account/src/lib.rs b/contracts/warp-account/src/lib.rs index 90d6bfa8..aff04ae7 100644 --- a/contracts/warp-account/src/lib.rs +++ b/contracts/warp-account/src/lib.rs @@ -1,5 +1,7 @@ pub mod contract; mod error; +mod execute; +mod query; pub mod state; #[cfg(test)] diff --git a/contracts/warp-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs new file mode 100644 index 00000000..d99107a4 --- /dev/null +++ b/contracts/warp-account/src/query/account.rs @@ -0,0 +1,99 @@ +use crate::state::{CONFIG, FREE_SUB_ACCOUNTS, OCCUPIED_SUB_ACCOUNTS}; +use account::{ + ConfigResponse, FirstFreeSubAccountsResponse, FreeSubAccountsResponse, + IsSubAccountOwnedAndFreeResponse, IsSubAccountOwnedAndOccupiedResponse, + OccupiedSubAccountsResponse, QueryFreeSubAccountsMsg, QueryIsSubAccountOwnedAndFreeMsg, + QueryIsSubAccountOwnedAndOccupiedMsg, QueryOccupiedSubAccountsMsg, SubAccount, +}; +use cosmwasm_std::{Deps, Order, StdResult, Uint64}; +use cw_storage_plus::Bound; + +const QUERY_LIMIT: u32 = 50; + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { config }) +} + +pub fn query_occupied_sub_accounts( + deps: Deps, + data: QueryOccupiedSubAccountsMsg, +) -> StdResult { + let sub_accounts = OCCUPIED_SUB_ACCOUNTS + .range( + deps.storage, + data.start_after.map(Bound::exclusive), + None, + Order::Descending, + ) + .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) + .map(|item| { + item.map(|(k, v)| SubAccount { + addr: k, + // owner: config.owner.clone(), + // default_account_addr: env.contract.address.clone(), + in_use_by_job_id: Some(Uint64::from(v)), + }) + }) + .collect::>>()?; + Ok(OccupiedSubAccountsResponse { sub_accounts }) +} + +pub fn query_free_sub_accounts( + deps: Deps, + data: QueryFreeSubAccountsMsg, +) -> StdResult { + let sub_accounts = FREE_SUB_ACCOUNTS + .range( + deps.storage, + data.start_after.map(Bound::exclusive), + None, + Order::Descending, + ) + .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) + .map(|item| { + item.map(|(k, _)| SubAccount { + addr: k, + // owner: config.owner.clone(), + // default_account_addr: env.contract.address.clone(), + in_use_by_job_id: Option::None, + }) + }) + .collect::>>()?; + Ok(FreeSubAccountsResponse { sub_accounts }) +} + +pub fn query_first_free_sub_account(deps: Deps) -> StdResult { + let sub_account = FREE_SUB_ACCOUNTS + .range(deps.storage, None, None, Order::Ascending) + .next(); + if sub_account.is_none() { + return Ok(FirstFreeSubAccountsResponse { + sub_account: None, + }); + } else { + let (addr, _) = sub_account.unwrap()?; + return Ok(FirstFreeSubAccountsResponse { + sub_account: Some(SubAccount { + addr: addr.clone(), + in_use_by_job_id: Option::None, + }), + }); + } +} + +pub fn query_is_sub_account_owned_and_occupied( + deps: Deps, + data: QueryIsSubAccountOwnedAndOccupiedMsg, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { config }) +} + +pub fn query_is_sub_account_owned_and_free( + deps: Deps, + data: QueryIsSubAccountOwnedAndFreeMsg, +) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { config }) +} diff --git a/contracts/warp-account/src/query/mod.rs b/contracts/warp-account/src/query/mod.rs new file mode 100644 index 00000000..d937534a --- /dev/null +++ b/contracts/warp-account/src/query/mod.rs @@ -0,0 +1 @@ +pub(crate) mod account; diff --git a/contracts/warp-account/src/state.rs b/contracts/warp-account/src/state.rs index 3e4be73e..48ed63d9 100644 --- a/contracts/warp-account/src/state.rs +++ b/contracts/warp-account/src/state.rs @@ -1,4 +1,10 @@ use account::Config; -use cw_storage_plus::Item; +use cw_storage_plus::{Item, Map}; pub const CONFIG: Item = Item::new("config"); + +// Key is the sub account address, value is the ID of the pending job currently using it +pub const OCCUPIED_SUB_ACCOUNTS: Map = Map::new("in_use_sub_accounts"); + +// Key is the sub account address, value is a dummy data to make it behave like a set +pub const FREE_SUB_ACCOUNTS: Map = Map::new("free_sub_accounts"); diff --git a/contracts/warp-account/src/tests.rs b/contracts/warp-account/src/tests.rs index f5c0a193..86b4f582 100644 --- a/contracts/warp-account/src/tests.rs +++ b/contracts/warp-account/src/tests.rs @@ -21,6 +21,8 @@ fn test_execute_controller() { owner: "vlad".to_string(), funds: None, msgs: None, + is_sub_account: Some(false), + main_account_addr: None, }, ); @@ -144,6 +146,8 @@ fn test_execute_owner() { owner: "vlad".to_string(), funds: None, msgs: None, + is_sub_account: Some(false), + main_account_addr: None, }, ); @@ -269,6 +273,8 @@ fn test_execute_unauth() { owner: "vlad".to_string(), funds: None, msgs: None, + is_sub_account: Some(false), + main_account_addr: None, }, ); diff --git a/contracts/warp-controller/src/execute/account.rs b/contracts/warp-controller/src/execute/account.rs index 2225898a..65e89ec6 100644 --- a/contracts/warp-controller/src/execute/account.rs +++ b/contracts/warp-controller/src/execute/account.rs @@ -88,6 +88,8 @@ pub fn create_account( owner: info.sender.to_string(), funds: data.funds, msgs: data.msgs, + is_sub_account: Some(false), + main_account_addr: None, })?, funds: info.funds, label: info.sender.to_string(), diff --git a/packages/account/src/lib.rs b/packages/account/src/lib.rs index b8f7d3f9..63e3741e 100644 --- a/packages/account/src/lib.rs +++ b/packages/account/src/lib.rs @@ -1,6 +1,6 @@ use controller::account::{AssetInfo, Fund}; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, CosmosMsg}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, CosmosMsg, Uint64}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -8,6 +8,17 @@ use serde::{Deserialize, Serialize}; pub struct Config { pub owner: Addr, pub warp_addr: Addr, + pub is_sub_account: bool, + // If current account is a main account, main_account_addr is itself, + // If current account is a sub account, main_account_addr is its main account address + pub main_account_addr: Addr, +} + +#[cw_serde] +pub struct SubAccount { + pub addr: String, + // If in use, in_use_by_job_id is the job id of the job that is using this sub account + pub in_use_by_job_id: Option, } #[cw_serde] @@ -15,6 +26,12 @@ pub struct InstantiateMsg { pub owner: String, pub msgs: Option>, pub funds: Option>, + // By default it's false meaning it's a main account + // If it's true, it's a sub account + pub is_sub_account: Option, + // Only supplied when is_sub_account is true + // Skipped if it's instantiating a main account + pub main_account_addr: Option, } #[cw_serde] @@ -23,6 +40,8 @@ pub enum ExecuteMsg { Generic(GenericMsg), WithdrawAssets(WithdrawAssetsMsg), IbcTransfer(IbcTransferMsg), + OccupySubAccount(OccupySubAccountMsg), + FreeSubAccount(FreeSubAccountMsg), } #[cw_serde] @@ -87,9 +106,90 @@ pub struct WithdrawAssetsMsg { #[cw_serde] pub struct ExecuteWasmMsg {} +#[cw_serde] +pub struct OccupySubAccountMsg { + pub sub_account_addr: String, + pub job_id: Uint64, +} + +#[cw_serde] +pub struct FreeSubAccountMsg { + pub sub_account_addr: String, +} + +#[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg { - Config, + #[returns(ConfigResponse)] + QueryConfig(QueryConfigMsg), + #[returns(OccupiedSubAccountsResponse)] + QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg), + #[returns(FreeSubAccountsResponse)] + QueryFreeSubAccounts(QueryFreeSubAccountsMsg), + #[returns(FirstFreeSubAccountsResponse)] + QueryFirstFreeSubAccount(QueryFirstFreeSubAccountMsg), + #[returns(IsSubAccountOwnedAndOccupiedResponse)] + QueryIsSubAccountOwnedAndOccupied(QueryIsSubAccountOwnedAndOccupiedMsg), + #[returns(IsSubAccountOwnedAndFreeResponse)] + QueryIsSubAccountOwnedAndFree(QueryIsSubAccountOwnedAndFreeMsg), +} + +#[cw_serde] +pub struct QueryConfigMsg {} + +#[cw_serde] +pub struct ConfigResponse { + pub config: Config, +} + +#[cw_serde] +pub struct QueryOccupiedSubAccountsMsg { + pub start_after: Option, + pub limit: Option, +} + +#[cw_serde] +pub struct OccupiedSubAccountsResponse { + pub sub_accounts: Vec, +} + +#[cw_serde] +pub struct QueryFreeSubAccountsMsg { + pub start_after: Option, + pub limit: Option, +} + +#[cw_serde] +pub struct FreeSubAccountsResponse { + pub sub_accounts: Vec, +} + +#[cw_serde] +pub struct QueryFirstFreeSubAccountMsg {} + +#[cw_serde] +pub struct FirstFreeSubAccountsResponse { + pub sub_account: Option, +} + +#[cw_serde] +pub struct QueryIsSubAccountOwnedAndOccupiedMsg { + pub sub_account_addr: String, +} + +#[cw_serde] +pub struct IsSubAccountOwnedAndOccupiedResponse { + pub is_in_use: bool, +} + +#[cw_serde] +pub struct QueryIsSubAccountOwnedAndFreeMsg { + pub sub_account_addr: String, +} + +#[cw_serde] +pub struct IsSubAccountOwnedAndFreeResponse { + pub is_free: bool, } #[cw_serde] From 6498dd4dd944b60cbe25055be0164dd657214f61 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:13:56 -0700 Subject: [PATCH 034/133] wip --- contracts/warp-account/src/contract.rs | 12 +- contracts/warp-account/src/execute/account.rs | 4 +- contracts/warp-account/src/query/account.rs | 82 +++--- contracts/warp-account/src/tests.rs | 233 +++++++++++++++++- packages/account/src/lib.rs | 30 +-- 5 files changed, 271 insertions(+), 90 deletions(-) diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index fca13e7c..18cc85f2 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -14,9 +14,9 @@ pub fn instantiate( ) -> Result { let instantiated_account_addr = env.contract.address; let main_account_addr = if msg.is_sub_account.unwrap_or(false) { - instantiated_account_addr.clone() - } else { deps.api.addr_validate(&msg.main_account_addr.unwrap())? + } else { + instantiated_account_addr.clone() }; CONFIG.save( @@ -68,7 +68,7 @@ pub fn execute( } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), QueryMsg::QueryOccupiedSubAccounts(data) => { @@ -80,12 +80,6 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryFirstFreeSubAccount(_) => { to_binary(&query::account::query_first_free_sub_account(deps)?) } - QueryMsg::QueryIsSubAccountOwnedAndOccupied(data) => to_binary( - &query::account::query_is_sub_account_owned_and_occupied(deps, data)?, - ), - QueryMsg::QueryIsSubAccountOwnedAndFree(data) => to_binary( - &query::account::query_is_sub_account_owned_and_free(deps, data)?, - ), } } diff --git a/contracts/warp-account/src/execute/account.rs b/contracts/warp-account/src/execute/account.rs index 8748883c..87d94169 100644 --- a/contracts/warp-account/src/execute/account.rs +++ b/contracts/warp-account/src/execute/account.rs @@ -8,7 +8,7 @@ pub fn occupy_sub_account( env: Env, data: OccupySubAccountMsg, ) -> Result { - // We do not add default account to occupied sub accounts + // We do not add main account to occupied sub accounts if data.sub_account_addr == env.contract.address { return Ok(Response::new()); } @@ -28,7 +28,7 @@ pub fn free_sub_account( env: Env, data: FreeSubAccountMsg, ) -> Result { - // We do not add default account to free sub accounts + // We do not add main account to free sub accounts if data.sub_account_addr == env.contract.address { return Ok(Response::new()); } diff --git a/contracts/warp-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs index d99107a4..4e3f66de 100644 --- a/contracts/warp-account/src/query/account.rs +++ b/contracts/warp-account/src/query/account.rs @@ -1,9 +1,7 @@ use crate::state::{CONFIG, FREE_SUB_ACCOUNTS, OCCUPIED_SUB_ACCOUNTS}; use account::{ - ConfigResponse, FirstFreeSubAccountsResponse, FreeSubAccountsResponse, - IsSubAccountOwnedAndFreeResponse, IsSubAccountOwnedAndOccupiedResponse, - OccupiedSubAccountsResponse, QueryFreeSubAccountsMsg, QueryIsSubAccountOwnedAndFreeMsg, - QueryIsSubAccountOwnedAndOccupiedMsg, QueryOccupiedSubAccountsMsg, SubAccount, + ConfigResponse, FirstFreeSubAccountResponse, FreeSubAccountsResponse, + OccupiedSubAccountsResponse, QueryFreeSubAccountsMsg, QueryOccupiedSubAccountsMsg, SubAccount, }; use cosmwasm_std::{Deps, Order, StdResult, Uint64}; use cw_storage_plus::Bound; @@ -15,6 +13,23 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(ConfigResponse { config }) } +pub fn query_first_free_sub_account(deps: Deps) -> StdResult { + let sub_account = FREE_SUB_ACCOUNTS + .range(deps.storage, None, None, Order::Ascending) + .next(); + if sub_account.is_none() { + return Ok(FirstFreeSubAccountResponse { sub_account: None }); + } else { + let (addr, _) = sub_account.unwrap()?; + return Ok(FirstFreeSubAccountResponse { + sub_account: Some(SubAccount { + addr: addr.clone(), + in_use_by_job_id: Option::None, + }), + }); + } +} + pub fn query_occupied_sub_accounts( deps: Deps, data: QueryOccupiedSubAccountsMsg, @@ -28,15 +43,16 @@ pub fn query_occupied_sub_accounts( ) .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { - item.map(|(k, v)| SubAccount { - addr: k, - // owner: config.owner.clone(), - // default_account_addr: env.contract.address.clone(), - in_use_by_job_id: Some(Uint64::from(v)), + item.map(|(sub_account_addr, job_id)| SubAccount { + addr: sub_account_addr, + in_use_by_job_id: Some(Uint64::from(job_id)), }) }) .collect::>>()?; - Ok(OccupiedSubAccountsResponse { sub_accounts }) + Ok(OccupiedSubAccountsResponse { + total_count: sub_accounts.len(), + sub_accounts, + }) } pub fn query_free_sub_accounts( @@ -52,48 +68,14 @@ pub fn query_free_sub_accounts( ) .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { - item.map(|(k, _)| SubAccount { - addr: k, - // owner: config.owner.clone(), - // default_account_addr: env.contract.address.clone(), + item.map(|(sub_account_addr, _)| SubAccount { + addr: sub_account_addr, in_use_by_job_id: Option::None, }) }) .collect::>>()?; - Ok(FreeSubAccountsResponse { sub_accounts }) -} - -pub fn query_first_free_sub_account(deps: Deps) -> StdResult { - let sub_account = FREE_SUB_ACCOUNTS - .range(deps.storage, None, None, Order::Ascending) - .next(); - if sub_account.is_none() { - return Ok(FirstFreeSubAccountsResponse { - sub_account: None, - }); - } else { - let (addr, _) = sub_account.unwrap()?; - return Ok(FirstFreeSubAccountsResponse { - sub_account: Some(SubAccount { - addr: addr.clone(), - in_use_by_job_id: Option::None, - }), - }); - } -} - -pub fn query_is_sub_account_owned_and_occupied( - deps: Deps, - data: QueryIsSubAccountOwnedAndOccupiedMsg, -) -> StdResult { - let config = CONFIG.load(deps.storage)?; - Ok(ConfigResponse { config }) -} - -pub fn query_is_sub_account_owned_and_free( - deps: Deps, - data: QueryIsSubAccountOwnedAndFreeMsg, -) -> StdResult { - let config = CONFIG.load(deps.storage)?; - Ok(ConfigResponse { config }) + Ok(FreeSubAccountsResponse { + total_count: sub_accounts.len(), + sub_accounts, + }) } diff --git a/contracts/warp-account/src/tests.rs b/contracts/warp-account/src/tests.rs index 86b4f582..7dee03ad 100644 --- a/contracts/warp-account/src/tests.rs +++ b/contracts/warp-account/src/tests.rs @@ -1,9 +1,13 @@ -use crate::contract::{execute, instantiate}; +use crate::contract::{execute, instantiate, query}; use crate::ContractError; -use account::{ExecuteMsg, GenericMsg, InstantiateMsg}; +use account::{ + Config, ConfigResponse, ExecuteMsg, FirstFreeSubAccountResponse, FreeSubAccountsResponse, + GenericMsg, InstantiateMsg, OccupiedSubAccountsResponse, QueryConfigMsg, + QueryFirstFreeSubAccountMsg, QueryFreeSubAccountsMsg, QueryMsg, QueryOccupiedSubAccountsMsg, +}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ - to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, + to_binary, Api, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, IbcTimeoutBlock, Response, StakingMsg, Uint128, VoteOption, WasmMsg, }; @@ -334,3 +338,226 @@ fn test_execute_unauth() { assert_eq!(execute_res, ContractError::Unauthorized {}) } + +#[test] +fn test_manage_sub_account() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let warp_controller = "vlad_controller"; + let owner = "vlad"; + let info = mock_info(warp_controller, &[]); + + let instantiate_msg = InstantiateMsg { + owner: owner.to_string(), + funds: None, + msgs: None, + is_sub_account: Some(false), + main_account_addr: None, + }; + let instantiate_main_account_res = instantiate( + deps.as_mut(), + env.clone(), + info.clone(), + instantiate_msg.clone(), + ) + .unwrap(); + + let main_account_addr = instantiate_main_account_res + .attributes + .iter() + .find(|attr| attr.key == "contract_addr") + .unwrap() + .value + .clone(); + + assert_eq!( + instantiate_main_account_res, + Response::new() + .add_attribute("action", "instantiate") + .add_attribute("contract_addr", main_account_addr.clone()) + .add_attribute("is_sub_account", "false") + .add_attribute("main_account_addr", main_account_addr.clone()) + .add_attribute("owner", owner) + .add_attribute("funds", serde_json_wasm::to_string(&info.funds).unwrap()) + .add_attribute( + "cw_funds", + serde_json_wasm::to_string(&instantiate_msg.clone().funds).unwrap() + ) + .add_attribute( + "account_msgs", + serde_json_wasm::to_string(&instantiate_msg.clone().msgs).unwrap() + ) + ); + + let query_config_msg = QueryMsg::QueryConfig(QueryConfigMsg {}); + let query_res = query(deps.as_ref(), env.clone(), query_config_msg).unwrap(); + assert_eq!( + query_res, + to_binary(&ConfigResponse { + config: Config { + owner: deps.api.addr_validate(owner).unwrap(), + warp_addr: deps.api.addr_validate(warp_controller).unwrap(), + is_sub_account: false, + main_account_addr: deps.api.addr_validate(main_account_addr.as_str()).unwrap(), + } + }) + .unwrap() + ); + + let query_first_free_sub_account_msg = + QueryMsg::QueryFirstFreeSubAccount(QueryFirstFreeSubAccountMsg {}); + let query_res = query(deps.as_ref(), env.clone(), query_first_free_sub_account_msg).unwrap(); + assert_eq!( + query_res, + to_binary(&FirstFreeSubAccountResponse { sub_account: None }).unwrap() + ); + + let query_free_sub_accounts_msg = QueryMsg::QueryFreeSubAccounts(QueryFreeSubAccountsMsg { + start_after: None, + limit: None, + }); + let query_res = query(deps.as_ref(), env.clone(), query_free_sub_accounts_msg).unwrap(); + assert_eq!( + query_res, + to_binary(&FreeSubAccountsResponse { + sub_accounts: vec![], + total_count: 0 + }) + .unwrap() + ); + + let query_occupied_sub_accounts_msg = + QueryMsg::QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg { + start_after: None, + limit: None, + }); + let query_res = query(deps.as_ref(), env.clone(), query_occupied_sub_accounts_msg).unwrap(); + assert_eq!( + query_res, + to_binary(&OccupiedSubAccountsResponse { + sub_accounts: vec![], + total_count: 0 + }) + .unwrap() + ); + + let _instantiate_sub_account_res = instantiate( + deps.as_mut(), + env.clone(), + info.clone(), + InstantiateMsg { + owner: "vlad".to_string(), + funds: None, + msgs: None, + is_sub_account: Some(true), + main_account_addr: None, + }, + ); + + let execute_msg = ExecuteMsg::Generic(GenericMsg { + msgs: vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + }), + CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes, + }), + CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + }), + CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0, + }), + }), + CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default(), + }, + ], + }); + + let info2 = mock_info("vlad", &[]); + + let execute_res = execute(deps.as_mut(), env, info2, execute_msg).unwrap(); + + assert_eq!( + execute_res, + Response::new() + .add_attribute("action", "generic") + .add_messages(vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }] + }), + CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes + }), + CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + }), + CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0 + }), + }), + CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default() + } + ]) + ) +} diff --git a/packages/account/src/lib.rs b/packages/account/src/lib.rs index 63e3741e..84bf9858 100644 --- a/packages/account/src/lib.rs +++ b/packages/account/src/lib.rs @@ -126,12 +126,8 @@ pub enum QueryMsg { QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg), #[returns(FreeSubAccountsResponse)] QueryFreeSubAccounts(QueryFreeSubAccountsMsg), - #[returns(FirstFreeSubAccountsResponse)] + #[returns(FirstFreeSubAccountResponse)] QueryFirstFreeSubAccount(QueryFirstFreeSubAccountMsg), - #[returns(IsSubAccountOwnedAndOccupiedResponse)] - QueryIsSubAccountOwnedAndOccupied(QueryIsSubAccountOwnedAndOccupiedMsg), - #[returns(IsSubAccountOwnedAndFreeResponse)] - QueryIsSubAccountOwnedAndFree(QueryIsSubAccountOwnedAndFreeMsg), } #[cw_serde] @@ -151,6 +147,7 @@ pub struct QueryOccupiedSubAccountsMsg { #[cw_serde] pub struct OccupiedSubAccountsResponse { pub sub_accounts: Vec, + pub total_count: usize, } #[cw_serde] @@ -162,35 +159,16 @@ pub struct QueryFreeSubAccountsMsg { #[cw_serde] pub struct FreeSubAccountsResponse { pub sub_accounts: Vec, + pub total_count: usize, } #[cw_serde] pub struct QueryFirstFreeSubAccountMsg {} #[cw_serde] -pub struct FirstFreeSubAccountsResponse { +pub struct FirstFreeSubAccountResponse { pub sub_account: Option, } -#[cw_serde] -pub struct QueryIsSubAccountOwnedAndOccupiedMsg { - pub sub_account_addr: String, -} - -#[cw_serde] -pub struct IsSubAccountOwnedAndOccupiedResponse { - pub is_in_use: bool, -} - -#[cw_serde] -pub struct QueryIsSubAccountOwnedAndFreeMsg { - pub sub_account_addr: String, -} - -#[cw_serde] -pub struct IsSubAccountOwnedAndFreeResponse { - pub is_free: bool, -} - #[cw_serde] pub struct MigrateMsg {} From 0824ddd70342adf561ece9befb06ee0252837d80 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Fri, 29 Sep 2023 17:43:25 -0700 Subject: [PATCH 035/133] add integration test for sub account --- Cargo.lock | 4 +- contracts/warp-account/src/execute/mod.rs | 2 +- .../warp-account/src/integration_tests.rs | 345 ++++++++++++++++++ contracts/warp-account/src/lib.rs | 2 + contracts/warp-account/src/query/account.rs | 6 +- contracts/warp-account/src/tests.rs | 233 +----------- 6 files changed, 356 insertions(+), 236 deletions(-) create mode 100644 contracts/warp-account/src/integration_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 8e23f20c..f7e02d80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,9 +37,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "base16ct" diff --git a/contracts/warp-account/src/execute/mod.rs b/contracts/warp-account/src/execute/mod.rs index 9a01495c..cbc6902a 100644 --- a/contracts/warp-account/src/execute/mod.rs +++ b/contracts/warp-account/src/execute/mod.rs @@ -1,3 +1,3 @@ -pub(crate) mod ibc; pub(crate) mod account; +pub(crate) mod ibc; pub(crate) mod withdraw; diff --git a/contracts/warp-account/src/integration_tests.rs b/contracts/warp-account/src/integration_tests.rs new file mode 100644 index 00000000..6e3cc1fe --- /dev/null +++ b/contracts/warp-account/src/integration_tests.rs @@ -0,0 +1,345 @@ +#[cfg(test)] +mod tests { + use account::{ + Config, ConfigResponse, ExecuteMsg, FirstFreeSubAccountResponse, FreeSubAccountMsg, + FreeSubAccountsResponse, InstantiateMsg, OccupiedSubAccountsResponse, OccupySubAccountMsg, + QueryConfigMsg, QueryFirstFreeSubAccountMsg, QueryFreeSubAccountsMsg, QueryMsg, + QueryOccupiedSubAccountsMsg, SubAccount, + }; + use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; + use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper, Executor}; + + use crate::contract::{execute, instantiate, query}; + + const USER_1: &str = "terra1"; + + fn mock_app() -> App { + AppBuilder::new().build(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(USER_1), + vec![Coin { + denom: "uluna".to_string(), + // 1_000_000_000 uLuna i.e. 1k LUNA since 1 LUNA = 1_000_000 uLuna + amount: Uint128::new(1_000_000_000), + }], + ) + .unwrap(); + }) + } + + fn contract_warp_account() -> Box> { + let contract = ContractWrapper::new(execute, instantiate, query); + Box::new(contract) + } + + fn init_warp_account( + app: &mut App, + warp_account_contract_code_id: u64, + is_sub_account: Option, + main_account_addr: Option, + ) -> Addr { + app.instantiate_contract( + warp_account_contract_code_id, + Addr::unchecked(USER_1), + &InstantiateMsg { + owner: USER_1.to_string(), + msgs: None, + funds: None, + is_sub_account, + main_account_addr, + }, + &[], + "warp_main_account", + None, + ) + .unwrap() + } + + #[test] + fn warp_account_contract_multi_test_sub_account_management() { + let mut app = mock_app(); + let warp_account_contract_code_id = app.store_code(contract_warp_account()); + + // Instantiate main account + let warp_main_account_contract_addr = + init_warp_account(&mut app, warp_account_contract_code_id, Some(false), None); + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryConfig(QueryConfigMsg {}) + ), + Ok(ConfigResponse { + config: Config { + owner: Addr::unchecked(USER_1), + warp_addr: Addr::unchecked(USER_1), + is_sub_account: false, + main_account_addr: Addr::unchecked(warp_main_account_contract_addr.clone()) + } + }) + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryFirstFreeSubAccount(QueryFirstFreeSubAccountMsg {}) + ), + Ok(FirstFreeSubAccountResponse { sub_account: None }) + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryFreeSubAccounts(QueryFreeSubAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(FreeSubAccountsResponse { + sub_accounts: vec![], + total_count: 0 + }) + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(OccupiedSubAccountsResponse { + sub_accounts: vec![], + total_count: 0 + }) + ); + + // Instantiate first sub account + let warp_sub_account_1_contract_addr = init_warp_account( + &mut app, + warp_account_contract_code_id, + Some(true), + Some(warp_main_account_contract_addr.to_string()), + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_sub_account_1_contract_addr.clone(), + &QueryMsg::QueryConfig(QueryConfigMsg {}) + ), + Ok(ConfigResponse { + config: Config { + owner: Addr::unchecked(USER_1), + warp_addr: Addr::unchecked(USER_1), + is_sub_account: true, + main_account_addr: Addr::unchecked(warp_main_account_contract_addr.clone()) + } + }) + ); + // Mark first sub account as free + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_main_account_contract_addr.clone(), + &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { + sub_account_addr: warp_sub_account_1_contract_addr.to_string(), + }), + &[], + ); + + // Instantiate second sub account + let warp_sub_account_2_contract_addr = init_warp_account( + &mut app, + warp_account_contract_code_id, + Some(true), + Some(warp_main_account_contract_addr.to_string()), + ); + // Mark second sub account as free + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_main_account_contract_addr.clone(), + &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { + sub_account_addr: warp_sub_account_2_contract_addr.to_string(), + }), + &[], + ); + + // Instantiate third sub account + let warp_sub_account_3_contract_addr = init_warp_account( + &mut app, + warp_account_contract_code_id, + Some(true), + Some(warp_main_account_contract_addr.to_string()), + ); + // Mark third sub account as free + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_main_account_contract_addr.clone(), + &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { + sub_account_addr: warp_sub_account_3_contract_addr.to_string(), + }), + &[], + ); + + // Query first free sub account + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryFirstFreeSubAccount(QueryFirstFreeSubAccountMsg {}) + ), + Ok(FirstFreeSubAccountResponse { + sub_account: Some(SubAccount { + addr: warp_sub_account_1_contract_addr.to_string(), + in_use_by_job_id: None + }) + }) + ); + + // Query free sub accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryFreeSubAccounts(QueryFreeSubAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(FreeSubAccountsResponse { + sub_accounts: vec![ + SubAccount { + addr: warp_sub_account_3_contract_addr.to_string(), + in_use_by_job_id: None + }, + SubAccount { + addr: warp_sub_account_2_contract_addr.to_string(), + in_use_by_job_id: None + }, + SubAccount { + addr: warp_sub_account_1_contract_addr.to_string(), + in_use_by_job_id: None + } + ], + total_count: 3 + }) + ); + + // Query occupied sub accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(OccupiedSubAccountsResponse { + sub_accounts: vec![], + total_count: 0 + }) + ); + + // Occupy second sub account + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_main_account_contract_addr.clone(), + &ExecuteMsg::OccupySubAccount(OccupySubAccountMsg { + sub_account_addr: warp_sub_account_2_contract_addr.to_string(), + job_id: Uint64::from(1 as u8), + }), + &[], + ); + + // Query free sub accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryFreeSubAccounts(QueryFreeSubAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(FreeSubAccountsResponse { + sub_accounts: vec![ + SubAccount { + addr: warp_sub_account_3_contract_addr.to_string(), + in_use_by_job_id: None + }, + SubAccount { + addr: warp_sub_account_1_contract_addr.to_string(), + in_use_by_job_id: None + } + ], + total_count: 2 + }) + ); + + // Query occupied sub accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(OccupiedSubAccountsResponse { + sub_accounts: vec![SubAccount { + addr: warp_sub_account_2_contract_addr.to_string(), + in_use_by_job_id: Some(Uint64::from(1 as u8)) + }], + total_count: 1 + }) + ); + + // Free second sub account + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_main_account_contract_addr.clone(), + &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { + sub_account_addr: warp_sub_account_2_contract_addr.to_string(), + }), + &[], + ); + + // Query free sub accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryFreeSubAccounts(QueryFreeSubAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(FreeSubAccountsResponse { + sub_accounts: vec![ + SubAccount { + addr: warp_sub_account_3_contract_addr.to_string(), + in_use_by_job_id: None + }, + SubAccount { + addr: warp_sub_account_2_contract_addr.to_string(), + in_use_by_job_id: None + }, + SubAccount { + addr: warp_sub_account_1_contract_addr.to_string(), + in_use_by_job_id: None + } + ], + total_count: 3 + }) + ); + + // Query occupied sub accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_main_account_contract_addr.clone(), + &QueryMsg::QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(OccupiedSubAccountsResponse { + sub_accounts: vec![], + total_count: 0 + }) + ); + } +} diff --git a/contracts/warp-account/src/lib.rs b/contracts/warp-account/src/lib.rs index aff04ae7..7511e0ce 100644 --- a/contracts/warp-account/src/lib.rs +++ b/contracts/warp-account/src/lib.rs @@ -4,6 +4,8 @@ mod execute; mod query; pub mod state; +#[cfg(test)] +mod integration_tests; #[cfg(test)] mod tests; diff --git a/contracts/warp-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs index 4e3f66de..fac049bc 100644 --- a/contracts/warp-account/src/query/account.rs +++ b/contracts/warp-account/src/query/account.rs @@ -18,15 +18,15 @@ pub fn query_first_free_sub_account(deps: Deps) -> StdResult Date: Fri, 29 Sep 2023 18:05:26 -0700 Subject: [PATCH 036/133] update test --- Cargo.lock | 1 + contracts/warp-account/Cargo.toml | 1 + .../warp-account/src/integration_tests.rs | 43 ++++++++++++++++++- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7e02d80..6c3ce2c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1035,6 +1035,7 @@ name = "warp-account" version = "0.1.0" dependencies = [ "account", + "anyhow", "base64", "controller", "cosmwasm-schema", diff --git a/contracts/warp-account/Cargo.toml b/contracts/warp-account/Cargo.toml index 8b70222b..cec48e20 100644 --- a/contracts/warp-account/Cargo.toml +++ b/contracts/warp-account/Cargo.toml @@ -49,3 +49,4 @@ prost = "0.11.9" [dev-dependencies] cw-multi-test = "0.16.0" +anyhow = "1.0.71" diff --git a/contracts/warp-account/src/integration_tests.rs b/contracts/warp-account/src/integration_tests.rs index 6e3cc1fe..8094b642 100644 --- a/contracts/warp-account/src/integration_tests.rs +++ b/contracts/warp-account/src/integration_tests.rs @@ -6,10 +6,14 @@ mod tests { QueryConfigMsg, QueryFirstFreeSubAccountMsg, QueryFreeSubAccountsMsg, QueryMsg, QueryOccupiedSubAccountsMsg, SubAccount, }; + use anyhow::Result as AnyResult; use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; - use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper, Executor}; + use cw_multi_test::{App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor}; - use crate::contract::{execute, instantiate, query}; + use crate::{ + contract::{execute, instantiate, query}, + ContractError, + }; const USER_1: &str = "terra1"; @@ -58,6 +62,16 @@ mod tests { .unwrap() } + fn assert_err(res: AnyResult, err: ContractError) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: ContractError = generic_err.downcast().unwrap(); + assert_eq!(contract_err, err); + } + } + } + #[test] fn warp_account_contract_multi_test_sub_account_management() { let mut app = mock_app(); @@ -144,6 +158,18 @@ mod tests { }), &[], ); + // Cannot free sub account twice + assert_err( + app.execute_contract( + Addr::unchecked(USER_1), + warp_main_account_contract_addr.clone(), + &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { + sub_account_addr: warp_sub_account_1_contract_addr.to_string(), + }), + &[], + ), + ContractError::SubAccountAlreadyFreeError {}, + ); // Instantiate second sub account let warp_sub_account_2_contract_addr = init_warp_account( @@ -246,6 +272,19 @@ mod tests { }), &[], ); + // Cannot occupy sub account twice + assert_err( + app.execute_contract( + Addr::unchecked(USER_1), + warp_main_account_contract_addr.clone(), + &ExecuteMsg::OccupySubAccount(OccupySubAccountMsg { + sub_account_addr: warp_sub_account_2_contract_addr.to_string(), + job_id: Uint64::from(1 as u8), + }), + &[], + ), + ContractError::SubAccountAlreadyOccupiedError {}, + ); // Query free sub accounts assert_eq!( From a2574f2a48d8653c470edbd3960004a59f944c64 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 2 Oct 2023 12:22:33 -0700 Subject: [PATCH 037/133] store addr instead of string in map for efficiency --- contracts/warp-account/src/execute/account.rs | 14 ++++---- contracts/warp-account/src/query/account.rs | 36 ++++++++++++------- contracts/warp-account/src/state.rs | 7 ++-- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/contracts/warp-account/src/execute/account.rs b/contracts/warp-account/src/execute/account.rs index 87d94169..6fa15fd3 100644 --- a/contracts/warp-account/src/execute/account.rs +++ b/contracts/warp-account/src/execute/account.rs @@ -8,13 +8,14 @@ pub fn occupy_sub_account( env: Env, data: OccupySubAccountMsg, ) -> Result { + let sub_account_addr_ref = &deps.api.addr_validate(data.sub_account_addr.as_str())?; // We do not add main account to occupied sub accounts if data.sub_account_addr == env.contract.address { return Ok(Response::new()); } - FREE_SUB_ACCOUNTS.remove(deps.storage, data.sub_account_addr.clone()); - OCCUPIED_SUB_ACCOUNTS.update(deps.storage, data.sub_account_addr.clone(), |s| match s { - None => Ok(data.job_id.u64()), + FREE_SUB_ACCOUNTS.remove(deps.storage, sub_account_addr_ref); + OCCUPIED_SUB_ACCOUNTS.update(deps.storage, sub_account_addr_ref, |s| match s { + None => Ok(data.job_id), Some(_) => Err(ContractError::SubAccountAlreadyOccupiedError {}), })?; Ok(Response::new() @@ -28,14 +29,15 @@ pub fn free_sub_account( env: Env, data: FreeSubAccountMsg, ) -> Result { + let sub_account_addr_ref = &deps.api.addr_validate(data.sub_account_addr.as_str())?; // We do not add main account to free sub accounts if data.sub_account_addr == env.contract.address { return Ok(Response::new()); } - OCCUPIED_SUB_ACCOUNTS.remove(deps.storage, data.sub_account_addr.clone()); - FREE_SUB_ACCOUNTS.update(deps.storage, data.sub_account_addr.clone(), |s| match s { + OCCUPIED_SUB_ACCOUNTS.remove(deps.storage, sub_account_addr_ref); + FREE_SUB_ACCOUNTS.update(deps.storage, sub_account_addr_ref, |s| match s { // value is a dummy data because there is no built in support for set in cosmwasm - None => Ok(0), + None => Ok(true), Some(_) => Err(ContractError::SubAccountAlreadyFreeError {}), })?; Ok(Response::new() diff --git a/contracts/warp-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs index fac049bc..9bc2c6ba 100644 --- a/contracts/warp-account/src/query/account.rs +++ b/contracts/warp-account/src/query/account.rs @@ -3,7 +3,7 @@ use account::{ ConfigResponse, FirstFreeSubAccountResponse, FreeSubAccountsResponse, OccupiedSubAccountsResponse, QueryFreeSubAccountsMsg, QueryOccupiedSubAccountsMsg, SubAccount, }; -use cosmwasm_std::{Deps, Order, StdResult, Uint64}; +use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; const QUERY_LIMIT: u32 = 50; @@ -23,7 +23,7 @@ pub fn query_first_free_sub_account(deps: Deps) -> StdResult StdResult { - let sub_accounts = OCCUPIED_SUB_ACCOUNTS - .range( + let iter = match data.start_after { + Some(start_after) => OCCUPIED_SUB_ACCOUNTS.range( deps.storage, - data.start_after.map(Bound::exclusive), + Some(Bound::exclusive( + &deps.api.addr_validate(start_after.as_str()).unwrap(), + )), None, Order::Descending, - ) + ), + None => OCCUPIED_SUB_ACCOUNTS.range(deps.storage, None, None, Order::Descending), + }; + let sub_accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { item.map(|(sub_account_addr, job_id)| SubAccount { - addr: sub_account_addr, - in_use_by_job_id: Some(Uint64::from(job_id)), + addr: sub_account_addr.to_string(), + in_use_by_job_id: Some(job_id), }) }) .collect::>>()?; @@ -59,17 +64,22 @@ pub fn query_free_sub_accounts( deps: Deps, data: QueryFreeSubAccountsMsg, ) -> StdResult { - let sub_accounts = FREE_SUB_ACCOUNTS - .range( + let iter = match data.start_after { + Some(start_after) => FREE_SUB_ACCOUNTS.range( deps.storage, - data.start_after.map(Bound::exclusive), + Some(Bound::exclusive( + &deps.api.addr_validate(start_after.as_str()).unwrap(), + )), None, Order::Descending, - ) + ), + None => FREE_SUB_ACCOUNTS.range(deps.storage, None, None, Order::Descending), + }; + let sub_accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { item.map(|(sub_account_addr, _)| SubAccount { - addr: sub_account_addr, + addr: sub_account_addr.to_string(), in_use_by_job_id: Option::None, }) }) diff --git a/contracts/warp-account/src/state.rs b/contracts/warp-account/src/state.rs index 48ed63d9..cacebfe1 100644 --- a/contracts/warp-account/src/state.rs +++ b/contracts/warp-account/src/state.rs @@ -1,10 +1,11 @@ use account::Config; +use cosmwasm_std::{Addr, Uint64}; use cw_storage_plus::{Item, Map}; pub const CONFIG: Item = Item::new("config"); // Key is the sub account address, value is the ID of the pending job currently using it -pub const OCCUPIED_SUB_ACCOUNTS: Map = Map::new("in_use_sub_accounts"); +pub const OCCUPIED_SUB_ACCOUNTS: Map<&Addr, Uint64> = Map::new("in_use_sub_accounts"); -// Key is the sub account address, value is a dummy data to make it behave like a set -pub const FREE_SUB_ACCOUNTS: Map = Map::new("free_sub_accounts"); +// Key is the sub account address, value is a dummy data that is always true to make it behave like a set +pub const FREE_SUB_ACCOUNTS: Map<&Addr, bool> = Map::new("free_sub_accounts"); From 12847a0a25aedd308779545c7d3741e860a4a0be Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:55:09 -0700 Subject: [PATCH 038/133] nit rename --- .../warp-account/src/integration_tests.rs | 20 +++++++++---------- contracts/warp-account/src/query/account.rs | 6 +++--- contracts/warp-account/src/state.rs | 2 +- packages/account/src/lib.rs | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/contracts/warp-account/src/integration_tests.rs b/contracts/warp-account/src/integration_tests.rs index 8094b642..4a56050a 100644 --- a/contracts/warp-account/src/integration_tests.rs +++ b/contracts/warp-account/src/integration_tests.rs @@ -214,7 +214,7 @@ mod tests { Ok(FirstFreeSubAccountResponse { sub_account: Some(SubAccount { addr: warp_sub_account_1_contract_addr.to_string(), - in_use_by_job_id: None + occupied_by_job_id: None }) }) ); @@ -232,15 +232,15 @@ mod tests { sub_accounts: vec![ SubAccount { addr: warp_sub_account_3_contract_addr.to_string(), - in_use_by_job_id: None + occupied_by_job_id: None }, SubAccount { addr: warp_sub_account_2_contract_addr.to_string(), - in_use_by_job_id: None + occupied_by_job_id: None }, SubAccount { addr: warp_sub_account_1_contract_addr.to_string(), - in_use_by_job_id: None + occupied_by_job_id: None } ], total_count: 3 @@ -299,11 +299,11 @@ mod tests { sub_accounts: vec![ SubAccount { addr: warp_sub_account_3_contract_addr.to_string(), - in_use_by_job_id: None + occupied_by_job_id: None }, SubAccount { addr: warp_sub_account_1_contract_addr.to_string(), - in_use_by_job_id: None + occupied_by_job_id: None } ], total_count: 2 @@ -322,7 +322,7 @@ mod tests { Ok(OccupiedSubAccountsResponse { sub_accounts: vec![SubAccount { addr: warp_sub_account_2_contract_addr.to_string(), - in_use_by_job_id: Some(Uint64::from(1 as u8)) + occupied_by_job_id: Some(Uint64::from(1 as u8)) }], total_count: 1 }) @@ -351,15 +351,15 @@ mod tests { sub_accounts: vec![ SubAccount { addr: warp_sub_account_3_contract_addr.to_string(), - in_use_by_job_id: None + occupied_by_job_id: None }, SubAccount { addr: warp_sub_account_2_contract_addr.to_string(), - in_use_by_job_id: None + occupied_by_job_id: None }, SubAccount { addr: warp_sub_account_1_contract_addr.to_string(), - in_use_by_job_id: None + occupied_by_job_id: None } ], total_count: 3 diff --git a/contracts/warp-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs index 9bc2c6ba..80f6bc15 100644 --- a/contracts/warp-account/src/query/account.rs +++ b/contracts/warp-account/src/query/account.rs @@ -24,7 +24,7 @@ pub fn query_first_free_sub_account(deps: Deps) -> StdResult>>()?; @@ -80,7 +80,7 @@ pub fn query_free_sub_accounts( .map(|item| { item.map(|(sub_account_addr, _)| SubAccount { addr: sub_account_addr.to_string(), - in_use_by_job_id: Option::None, + occupied_by_job_id: Option::None, }) }) .collect::>>()?; diff --git a/contracts/warp-account/src/state.rs b/contracts/warp-account/src/state.rs index cacebfe1..fc91c0a0 100644 --- a/contracts/warp-account/src/state.rs +++ b/contracts/warp-account/src/state.rs @@ -5,7 +5,7 @@ use cw_storage_plus::{Item, Map}; pub const CONFIG: Item = Item::new("config"); // Key is the sub account address, value is the ID of the pending job currently using it -pub const OCCUPIED_SUB_ACCOUNTS: Map<&Addr, Uint64> = Map::new("in_use_sub_accounts"); +pub const OCCUPIED_SUB_ACCOUNTS: Map<&Addr, Uint64> = Map::new("occupied_sub_accounts"); // Key is the sub account address, value is a dummy data that is always true to make it behave like a set pub const FREE_SUB_ACCOUNTS: Map<&Addr, bool> = Map::new("free_sub_accounts"); diff --git a/packages/account/src/lib.rs b/packages/account/src/lib.rs index 84bf9858..b2f42709 100644 --- a/packages/account/src/lib.rs +++ b/packages/account/src/lib.rs @@ -17,8 +17,8 @@ pub struct Config { #[cw_serde] pub struct SubAccount { pub addr: String, - // If in use, in_use_by_job_id is the job id of the job that is using this sub account - pub in_use_by_job_id: Option, + // If occupied, occupied_by_job_id is the job id of the pending job that is using this sub account + pub occupied_by_job_id: Option, } #[cw_serde] From fe39c8be70bf4b7926e03030d3b8b94a51d5e80b Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Sun, 22 Oct 2023 01:54:34 -0700 Subject: [PATCH 039/133] update sub account data struct and start updating controller --- contracts/warp-account/src/contract.rs | 52 +-- .../warp-account/src/execute/withdraw.rs | 13 +- .../warp-account/src/integration_tests.rs | 142 +++++-- contracts/warp-account/src/query/account.rs | 56 ++- contracts/warp-account/src/state.rs | 4 + contracts/warp-account/src/tests.rs | 6 +- contracts/warp-controller/src/contract.rs | 375 ++---------------- contracts/warp-controller/src/error.rs | 3 + .../warp-controller/src/execute/account.rs | 172 ++++---- contracts/warp-controller/src/execute/job.rs | 298 ++++++++++---- contracts/warp-controller/src/lib.rs | 2 + .../warp-controller/src/reply/account.rs | 273 +++++++++++++ contracts/warp-controller/src/reply/job.rs | 213 ++++++++++ contracts/warp-controller/src/reply/mod.rs | 2 + packages/account/src/lib.rs | 31 +- packages/controller/src/account.rs | 8 +- packages/controller/src/job.rs | 5 +- packages/controller/src/lib.rs | 6 +- 18 files changed, 1016 insertions(+), 645 deletions(-) create mode 100644 contracts/warp-controller/src/reply/account.rs create mode 100644 contracts/warp-controller/src/reply/job.rs create mode 100644 contracts/warp-controller/src/reply/mod.rs diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index 18cc85f2..455ec8bb 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -1,6 +1,6 @@ use crate::state::CONFIG; use crate::{execute, query, ContractError}; -use account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SubAccountConfig}; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; @@ -13,30 +13,35 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { let instantiated_account_addr = env.contract.address; - let main_account_addr = if msg.is_sub_account.unwrap_or(false) { - deps.api.addr_validate(&msg.main_account_addr.unwrap())? - } else { - instantiated_account_addr.clone() - }; CONFIG.save( deps.storage, &Config { owner: deps.api.addr_validate(&msg.owner)?, - warp_addr: info.sender, - is_sub_account: msg.is_sub_account.unwrap_or(false), - main_account_addr: main_account_addr.clone(), + creator_addr: info.sender, + account_addr: instantiated_account_addr.clone(), + sub_account_config: if msg.is_sub_account { + Some(SubAccountConfig { + main_account_addr: deps + .api + .addr_validate(&msg.main_account_addr.clone().unwrap())?, + occupied_by_job_id: None, + }) + } else { + None + }, }, )?; Ok(Response::new() .add_attribute("action", "instantiate") - .add_attribute("contract_addr", instantiated_account_addr) + .add_attribute("contract_addr", instantiated_account_addr.clone()) + .add_attribute("is_sub_account", format!("{}", msg.is_sub_account)) .add_attribute( - "is_sub_account", - format!("{}", msg.is_sub_account.unwrap_or(false)), + "main_account_addr", + msg.main_account_addr + .unwrap_or(instantiated_account_addr.to_string()), ) - .add_attribute("main_account_addr", main_account_addr) .add_attribute("owner", msg.owner) .add_attribute("funds", serde_json_wasm::to_string(&info.funds)?) .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?) @@ -51,7 +56,7 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.warp_addr { + if info.sender != config.owner && info.sender != config.creator_addr { return Err(ContractError::Unauthorized {}); } match msg { @@ -59,7 +64,7 @@ pub fn execute( .add_messages(data.msgs) .add_attribute("action", "generic")), ExecuteMsg::WithdrawAssets(data) => { - execute::withdraw::withdraw_assets(deps, env, info, data) + execute::withdraw::withdraw_assets(deps, env, data, config) } ExecuteMsg::IbcTransfer(data) => execute::ibc::ibc_transfer(env, data), ExecuteMsg::OccupySubAccount(data) => execute::account::occupy_sub_account(deps, env, data), @@ -69,16 +74,17 @@ pub fn execute( #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + let config = CONFIG.load(deps.storage)?; match msg { - QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), - QueryMsg::QueryOccupiedSubAccounts(data) => { - to_binary(&query::account::query_occupied_sub_accounts(deps, data)?) - } - QueryMsg::QueryFreeSubAccounts(data) => { - to_binary(&query::account::query_free_sub_accounts(deps, data)?) - } + QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(config)?), + QueryMsg::QueryOccupiedSubAccounts(data) => to_binary( + &query::account::query_occupied_sub_accounts(deps, data, config)?, + ), + QueryMsg::QueryFreeSubAccounts(data) => to_binary( + &query::account::query_free_sub_accounts(deps, data, config)?, + ), QueryMsg::QueryFirstFreeSubAccount(_) => { - to_binary(&query::account::query_first_free_sub_account(deps)?) + to_binary(&query::account::query_first_free_sub_account(deps, config)?) } } } diff --git a/contracts/warp-account/src/execute/withdraw.rs b/contracts/warp-account/src/execute/withdraw.rs index 7ebe27d6..1f3961b8 100644 --- a/contracts/warp-account/src/execute/withdraw.rs +++ b/contracts/warp-account/src/execute/withdraw.rs @@ -1,10 +1,8 @@ -use crate::state::CONFIG; use crate::ContractError; -use account::WithdrawAssetsMsg; +use account::{Config, WithdrawAssetsMsg}; use controller::account::{AssetInfo, Cw721ExecuteMsg}; use cosmwasm_std::{ - to_binary, Addr, BankMsg, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdResult, - Uint128, WasmMsg, + to_binary, Addr, BankMsg, CosmosMsg, Deps, DepsMut, Env, Response, StdResult, Uint128, WasmMsg, }; use cw20::{BalanceResponse, Cw20ExecuteMsg}; use cw721::{Cw721QueryMsg, OwnerOfResponse}; @@ -12,14 +10,9 @@ use cw721::{Cw721QueryMsg, OwnerOfResponse}; pub fn withdraw_assets( deps: DepsMut, env: Env, - info: MessageInfo, data: WithdrawAssetsMsg, + config: Config, ) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.warp_addr { - return Err(ContractError::Unauthorized {}); - } - let mut withdraw_msgs: Vec = vec![]; for asset_info in &data.asset_infos { diff --git a/contracts/warp-account/src/integration_tests.rs b/contracts/warp-account/src/integration_tests.rs index 4a56050a..8e69a8a3 100644 --- a/contracts/warp-account/src/integration_tests.rs +++ b/contracts/warp-account/src/integration_tests.rs @@ -4,7 +4,7 @@ mod tests { Config, ConfigResponse, ExecuteMsg, FirstFreeSubAccountResponse, FreeSubAccountMsg, FreeSubAccountsResponse, InstantiateMsg, OccupiedSubAccountsResponse, OccupySubAccountMsg, QueryConfigMsg, QueryFirstFreeSubAccountMsg, QueryFreeSubAccountsMsg, QueryMsg, - QueryOccupiedSubAccountsMsg, SubAccount, + QueryOccupiedSubAccountsMsg, SubAccountConfig, }; use anyhow::Result as AnyResult; use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; @@ -15,7 +15,8 @@ mod tests { ContractError, }; - const USER_1: &str = "terra1"; + const DUMMY_WARP_CONTROLLER_ADDR: &str = "terra1"; + const USER_1: &str = "terra2"; fn mock_app() -> App { AppBuilder::new().build(|router, _, storage| { @@ -42,12 +43,12 @@ mod tests { fn init_warp_account( app: &mut App, warp_account_contract_code_id: u64, - is_sub_account: Option, + is_sub_account: bool, main_account_addr: Option, ) -> Addr { app.instantiate_contract( warp_account_contract_code_id, - Addr::unchecked(USER_1), + Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), &InstantiateMsg { owner: USER_1.to_string(), msgs: None, @@ -79,7 +80,7 @@ mod tests { // Instantiate main account let warp_main_account_contract_addr = - init_warp_account(&mut app, warp_account_contract_code_id, Some(false), None); + init_warp_account(&mut app, warp_account_contract_code_id, false, None); assert_eq!( app.wrap().query_wasm_smart( warp_main_account_contract_addr.clone(), @@ -88,9 +89,9 @@ mod tests { Ok(ConfigResponse { config: Config { owner: Addr::unchecked(USER_1), - warp_addr: Addr::unchecked(USER_1), - is_sub_account: false, - main_account_addr: Addr::unchecked(warp_main_account_contract_addr.clone()) + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_main_account_contract_addr.clone(), + sub_account_config: None } }) ); @@ -132,7 +133,7 @@ mod tests { let warp_sub_account_1_contract_addr = init_warp_account( &mut app, warp_account_contract_code_id, - Some(true), + true, Some(warp_main_account_contract_addr.to_string()), ); assert_eq!( @@ -143,9 +144,12 @@ mod tests { Ok(ConfigResponse { config: Config { owner: Addr::unchecked(USER_1), - warp_addr: Addr::unchecked(USER_1), - is_sub_account: true, - main_account_addr: Addr::unchecked(warp_main_account_contract_addr.clone()) + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_1_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) } }) ); @@ -175,7 +179,7 @@ mod tests { let warp_sub_account_2_contract_addr = init_warp_account( &mut app, warp_account_contract_code_id, - Some(true), + true, Some(warp_main_account_contract_addr.to_string()), ); // Mark second sub account as free @@ -192,7 +196,7 @@ mod tests { let warp_sub_account_3_contract_addr = init_warp_account( &mut app, warp_account_contract_code_id, - Some(true), + true, Some(warp_main_account_contract_addr.to_string()), ); // Mark third sub account as free @@ -212,9 +216,14 @@ mod tests { &QueryMsg::QueryFirstFreeSubAccount(QueryFirstFreeSubAccountMsg {}) ), Ok(FirstFreeSubAccountResponse { - sub_account: Some(SubAccount { - addr: warp_sub_account_1_contract_addr.to_string(), - occupied_by_job_id: None + sub_account: Some(Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_1_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) }) }) ); @@ -230,17 +239,32 @@ mod tests { ), Ok(FreeSubAccountsResponse { sub_accounts: vec![ - SubAccount { - addr: warp_sub_account_3_contract_addr.to_string(), - occupied_by_job_id: None + Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_3_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) }, - SubAccount { - addr: warp_sub_account_2_contract_addr.to_string(), - occupied_by_job_id: None + Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_2_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) }, - SubAccount { - addr: warp_sub_account_1_contract_addr.to_string(), - occupied_by_job_id: None + Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_1_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) } ], total_count: 3 @@ -297,13 +321,23 @@ mod tests { ), Ok(FreeSubAccountsResponse { sub_accounts: vec![ - SubAccount { - addr: warp_sub_account_3_contract_addr.to_string(), - occupied_by_job_id: None + Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_3_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) }, - SubAccount { - addr: warp_sub_account_1_contract_addr.to_string(), - occupied_by_job_id: None + Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_1_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) } ], total_count: 2 @@ -320,9 +354,14 @@ mod tests { }) ), Ok(OccupiedSubAccountsResponse { - sub_accounts: vec![SubAccount { - addr: warp_sub_account_2_contract_addr.to_string(), - occupied_by_job_id: Some(Uint64::from(1 as u8)) + sub_accounts: vec![Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_2_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: Some(Uint64::from(1 as u8)) + }) }], total_count: 1 }) @@ -349,17 +388,32 @@ mod tests { ), Ok(FreeSubAccountsResponse { sub_accounts: vec![ - SubAccount { - addr: warp_sub_account_3_contract_addr.to_string(), - occupied_by_job_id: None + Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_3_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) }, - SubAccount { - addr: warp_sub_account_2_contract_addr.to_string(), - occupied_by_job_id: None + Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_2_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) }, - SubAccount { - addr: warp_sub_account_1_contract_addr.to_string(), - occupied_by_job_id: None + Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + account_addr: warp_sub_account_1_contract_addr.clone(), + sub_account_config: Some(SubAccountConfig { + main_account_addr: warp_main_account_contract_addr.clone(), + occupied_by_job_id: None + }) } ], total_count: 3 diff --git a/contracts/warp-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs index 80f6bc15..b86f0529 100644 --- a/contracts/warp-account/src/query/account.rs +++ b/contracts/warp-account/src/query/account.rs @@ -1,30 +1,38 @@ -use crate::state::{CONFIG, FREE_SUB_ACCOUNTS, OCCUPIED_SUB_ACCOUNTS}; +use crate::state::{FREE_SUB_ACCOUNTS, OCCUPIED_SUB_ACCOUNTS}; use account::{ - ConfigResponse, FirstFreeSubAccountResponse, FreeSubAccountsResponse, - OccupiedSubAccountsResponse, QueryFreeSubAccountsMsg, QueryOccupiedSubAccountsMsg, SubAccount, + Config, ConfigResponse, FirstFreeSubAccountResponse, FreeSubAccountsResponse, + OccupiedSubAccountsResponse, QueryFreeSubAccountsMsg, QueryOccupiedSubAccountsMsg, + SubAccountConfig, }; use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; const QUERY_LIMIT: u32 = 50; -pub fn query_config(deps: Deps) -> StdResult { - let config = CONFIG.load(deps.storage)?; +pub fn query_config(config: Config) -> StdResult { Ok(ConfigResponse { config }) } -pub fn query_first_free_sub_account(deps: Deps) -> StdResult { +pub fn query_first_free_sub_account( + deps: Deps, + config: Config, +) -> StdResult { let sub_account = FREE_SUB_ACCOUNTS .range(deps.storage, None, None, Order::Ascending) .next(); if sub_account.is_none() { Ok(FirstFreeSubAccountResponse { sub_account: None }) } else { - let (addr, _) = sub_account.unwrap()?; + let (sub_account_addr, _) = sub_account.unwrap()?; Ok(FirstFreeSubAccountResponse { - sub_account: Some(SubAccount { - addr: addr.to_string(), - occupied_by_job_id: Option::None, + sub_account: Some(Config { + owner: config.owner, + creator_addr: config.creator_addr, + account_addr: sub_account_addr, + sub_account_config: Some(SubAccountConfig { + main_account_addr: config.account_addr, + occupied_by_job_id: None, + }), }), }) } @@ -33,6 +41,7 @@ pub fn query_first_free_sub_account(deps: Deps) -> StdResult StdResult { let iter = match data.start_after { Some(start_after) => OCCUPIED_SUB_ACCOUNTS.range( @@ -48,12 +57,17 @@ pub fn query_occupied_sub_accounts( let sub_accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { - item.map(|(sub_account_addr, job_id)| SubAccount { - addr: sub_account_addr.to_string(), - occupied_by_job_id: Some(job_id), + item.map(|(sub_account_addr, job_id)| Config { + owner: config.owner.clone(), + creator_addr: config.creator_addr.clone(), + account_addr: sub_account_addr, + sub_account_config: Some(SubAccountConfig { + main_account_addr: config.account_addr.clone(), + occupied_by_job_id: Some(job_id), + }), }) }) - .collect::>>()?; + .collect::>>()?; Ok(OccupiedSubAccountsResponse { total_count: sub_accounts.len(), sub_accounts, @@ -63,6 +77,7 @@ pub fn query_occupied_sub_accounts( pub fn query_free_sub_accounts( deps: Deps, data: QueryFreeSubAccountsMsg, + config: Config, ) -> StdResult { let iter = match data.start_after { Some(start_after) => FREE_SUB_ACCOUNTS.range( @@ -78,12 +93,17 @@ pub fn query_free_sub_accounts( let sub_accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { - item.map(|(sub_account_addr, _)| SubAccount { - addr: sub_account_addr.to_string(), - occupied_by_job_id: Option::None, + item.map(|(sub_account_addr, _)| Config { + owner: config.owner.clone(), + creator_addr: config.creator_addr.clone(), + account_addr: sub_account_addr, + sub_account_config: Some(SubAccountConfig { + main_account_addr: config.account_addr.clone(), + occupied_by_job_id: None, + }), }) }) - .collect::>>()?; + .collect::>>()?; Ok(FreeSubAccountsResponse { total_count: sub_accounts.len(), sub_accounts, diff --git a/contracts/warp-account/src/state.rs b/contracts/warp-account/src/state.rs index fc91c0a0..09d379f3 100644 --- a/contracts/warp-account/src/state.rs +++ b/contracts/warp-account/src/state.rs @@ -4,8 +4,12 @@ use cw_storage_plus::{Item, Map}; pub const CONFIG: Item = Item::new("config"); +// OCCUPIED_SUB_ACCOUNTS only has value when current account is a main account +// It will be empty if current account is a sub account, because we do not supported nested sub accounts // Key is the sub account address, value is the ID of the pending job currently using it pub const OCCUPIED_SUB_ACCOUNTS: Map<&Addr, Uint64> = Map::new("occupied_sub_accounts"); +// FREE_SUB_ACCOUNTS only has value when current account is a main account +// It will be empty if current account is a sub account, because we do not supported nested sub accounts // Key is the sub account address, value is a dummy data that is always true to make it behave like a set pub const FREE_SUB_ACCOUNTS: Map<&Addr, bool> = Map::new("free_sub_accounts"); diff --git a/contracts/warp-account/src/tests.rs b/contracts/warp-account/src/tests.rs index 86b4f582..715bcc08 100644 --- a/contracts/warp-account/src/tests.rs +++ b/contracts/warp-account/src/tests.rs @@ -21,7 +21,7 @@ fn test_execute_controller() { owner: "vlad".to_string(), funds: None, msgs: None, - is_sub_account: Some(false), + is_sub_account: false, main_account_addr: None, }, ); @@ -146,7 +146,7 @@ fn test_execute_owner() { owner: "vlad".to_string(), funds: None, msgs: None, - is_sub_account: Some(false), + is_sub_account: false, main_account_addr: None, }, ); @@ -273,7 +273,7 @@ fn test_execute_unauth() { owner: "vlad".to_string(), funds: None, msgs: None, - is_sub_account: Some(false), + is_sub_account: false, main_account_addr: None, }, ); diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 0e7b30cc..f4dc7a33 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -1,19 +1,26 @@ -use crate::error::map_contract_error; -use crate::state::{JobQueue, ACCOUNTS, CONFIG}; -use crate::{execute, query, state::STATE, ContractError}; -use account::{GenericMsg, WithdrawAssetsMsg}; -use controller::account::{Account, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; -use controller::job::{Job, JobStatus}; use cosmwasm_schema::cw_serde; - -use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State}; use cosmwasm_std::{ - entry_point, to_binary, Addr, Attribute, BalanceResponse, BankMsg, BankQuery, Binary, Coin, - CosmosMsg, Deps, DepsMut, Env, MessageInfo, QueryRequest, Reply, Response, StdError, StdResult, - SubMsgResult, Uint128, Uint64, WasmMsg, + entry_point, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, + StdResult, Uint128, Uint64, }; use cw_storage_plus::Item; +use crate::state::CONFIG; +use crate::{execute, query, reply, state::STATE, ContractError}; + +use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State}; + +// Reply id for job creation +// From a totally new user using warp for the first time, does not have account yet, let alone sub account +// So we create both account and sub account and job +pub const REPLY_ID_CREATE_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB: u64 = 1; +// Reply id for job creation +// From an existing user, who has main account, but does not have available sub account +// So we create sub account and job +pub const REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB: u64 = 2; +// Reply id for job execution +pub const REPLY_ID_EXECUTE_JOB: u64 = 3; + #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -86,8 +93,6 @@ pub fn execute( ExecuteMsg::ExecuteJob(data) => execute::job::execute_job(deps, env, info, data), ExecuteMsg::EvictJob(data) => execute::job::evict_job(deps, env, info, data), - ExecuteMsg::CreateAccount(data) => execute::account::create_account(deps, env, info, data), - ExecuteMsg::UpdateConfig(data) => execute::controller::update_config(deps, env, info, data), ExecuteMsg::MigrateAccounts(data) => { @@ -192,342 +197,16 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { match msg.id { - // Account creation - 0 => { - let reply = msg.result.into_result().map_err(StdError::generic_err)?; - - let event = reply - .events - .iter() - .find(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "action" && attr.value == "instantiate") - }) - .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; - - let owner = event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "owner") - .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? - .value; - - let address = event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "contract_addr") - .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? - .value; - - let funds: Vec = serde_json_wasm::from_str( - &event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "funds") - .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? - .value, - )?; - - let cw_funds: Option> = serde_json_wasm::from_str( - &event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "cw_funds") - .ok_or_else(|| StdError::generic_err("cannot find `cw_funds` attribute"))? - .value, - )?; - - let account_msgs: Option> = serde_json_wasm::from_str( - &event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "account_msgs") - .ok_or_else(|| StdError::generic_err("cannot find `account_msgs` attribute"))? - .value, - )?; - - let cw_funds_vec = match cw_funds { - None => { - vec![] - } - Some(funds) => funds, - }; - - let mut msgs_vec: Vec = vec![]; - - for cw_fund in &cw_funds_vec { - msgs_vec.push(CosmosMsg::Wasm(match cw_fund { - Fund::Cw20(cw20_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw20_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { - owner: owner.clone(), - recipient: address.clone(), - amount: cw20_fund.amount, - }))?, - funds: vec![], - }, - Fund::Cw721(cw721_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw721_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { - recipient: address.clone(), - token_id: cw721_fund.token_id.clone(), - }))?, - funds: vec![], - }, - })) - } - - if let Some(msgs) = account_msgs { - for msg in msgs { - msgs_vec.push(msg); - } - } - - if ACCOUNTS().has(deps.storage, deps.api.addr_validate(&owner)?) { - return Err(ContractError::AccountAlreadyExists {}); - } - - ACCOUNTS().save( - deps.storage, - deps.api.addr_validate(&owner)?, - &Account { - owner: deps.api.addr_validate(&owner.clone())?, - account: deps.api.addr_validate(&address)?, - }, - )?; - Ok(Response::new() - .add_attribute("action", "save_account") - .add_attribute("owner", owner) - .add_attribute("account_address", address) - .add_attribute("funds", serde_json_wasm::to_string(&funds)?) - .add_attribute("cw_funds", serde_json_wasm::to_string(&cw_funds_vec)?) - .add_messages(msgs_vec)) + // Sub account has been created, now create job + REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB => { + reply::account::create_sub_account_and_job(deps, env, msg) } - // Job execution - _ => { - let state = STATE.load(deps.storage)?; - - let new_status = match msg.result { - SubMsgResult::Ok(_) => JobStatus::Executed, - SubMsgResult::Err(_) => JobStatus::Failed, - }; - - let finished_job = JobQueue::finalize(&mut deps, env.clone(), msg.id, new_status)?; - - let res_attrs = match msg.result { - SubMsgResult::Err(e) => vec![Attribute::new( - "transaction_error", - format!("{}. {}", &e, map_contract_error(&e)), - )], - _ => vec![], - }; - - let mut msgs = vec![]; - let mut new_job_attrs = vec![]; - - let config = CONFIG.load(deps.storage)?; - - // Assume reward.amount == warp token allowance - let fee = finished_job.reward * Uint128::from(config.creation_fee_percentage) - / Uint128::new(100); - - let account_amount = deps - .querier - .query::(&QueryRequest::Bank(BankQuery::Balance { - address: finished_job.account.to_string(), - denom: config.fee_denom.clone(), - }))? - .amount - .amount; - - if finished_job.recurring { - if account_amount < fee + finished_job.reward { - new_job_attrs.push(Attribute::new("action", "recur_job")); - new_job_attrs.push(Attribute::new("creation_status", "failed_insufficient_fee")) - } else if !(finished_job.status == JobStatus::Executed - || finished_job.status == JobStatus::Failed) - { - new_job_attrs.push(Attribute::new("action", "recur_job")); - new_job_attrs.push(Attribute::new( - "creation_status", - "failed_invalid_job_status", - )); - } else { - let new_vars: String = deps.querier.query_wasm_smart( - config.resolver_address.clone(), - &resolver::QueryMsg::QueryApplyVarFn(resolver::QueryApplyVarFnMsg { - vars: finished_job.vars, - status: finished_job.status.clone(), - warp_account_addr: Some(finished_job.account.to_string()), - }), - )?; - - let should_terminate_job: bool; - match finished_job.terminate_condition.clone() { - Some(terminate_condition) => { - let resolution: StdResult = deps.querier.query_wasm_smart( - config.resolver_address, - &resolver::QueryMsg::QueryResolveCondition( - resolver::QueryResolveConditionMsg { - condition: terminate_condition, - vars: new_vars.clone(), - warp_account_addr: Some(finished_job.account.to_string()), - }, - ), - ); - if let Err(e) = resolution { - should_terminate_job = true; - new_job_attrs.push(Attribute::new("action", "recur_job")); - new_job_attrs.push(Attribute::new( - "job_terminate_condition_status", - "invalid", - )); - new_job_attrs.push(Attribute::new( - "creation_status", - format!( - "terminated_due_to_terminate_condition_resolves_to_error. {}", - e - ), - )); - } else { - new_job_attrs.push(Attribute::new( - "job_terminate_condition_status", - "valid", - )); - if resolution? { - should_terminate_job = true; - new_job_attrs.push(Attribute::new("action", "recur_job")); - new_job_attrs.push(Attribute::new( - "creation_status", - "terminated_due_to_terminate_condition_resolves_to_true", - )); - } else { - should_terminate_job = false; - } - } - } - None => { - should_terminate_job = false; - } - } - - if !should_terminate_job { - let new_job = JobQueue::add( - &mut deps, - Job { - id: state.current_job_id, - prev_id: Some(finished_job.id), - owner: finished_job.owner.clone(), - account: finished_job.account.clone(), - last_update_time: Uint64::from(env.block.time.seconds()), - name: finished_job.name.clone(), - description: finished_job.description, - labels: finished_job.labels, - status: JobStatus::Pending, - condition: finished_job.condition.clone(), - terminate_condition: finished_job.terminate_condition.clone(), - vars: new_vars, - requeue_on_evict: finished_job.requeue_on_evict, - recurring: finished_job.recurring, - msgs: finished_job.msgs.clone(), - reward: finished_job.reward, - assets_to_withdraw: finished_job.assets_to_withdraw, - }, - )?; - - msgs.push( - // Job owner's warp account sends fee to fee collector - WasmMsg::Execute { - contract_addr: finished_job.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.fee_collector.to_string(), - amount: vec![Coin::new( - (fee).u128(), - config.fee_denom.clone(), - )], - })], - }))?, - funds: vec![], - }, - ); - - msgs.push( - // Job owner's warp account sends reward to controller - WasmMsg::Execute { - contract_addr: finished_job.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: env.contract.address.to_string(), - amount: vec![Coin::new( - (new_job.reward).u128(), - config.fee_denom, - )], - })], - }))?, - funds: vec![], - }, - ); - - msgs.push( - // Job owner withdraw all assets that are listed from warp account to itself - WasmMsg::Execute { - contract_addr: finished_job.account.to_string(), - msg: to_binary(&account::ExecuteMsg::WithdrawAssets( - WithdrawAssetsMsg { - asset_infos: new_job.assets_to_withdraw, - }, - ))?, - funds: vec![], - }, - ); - - new_job_attrs.push(Attribute::new("action", "create_job")); - new_job_attrs.push(Attribute::new("job_id", new_job.id)); - new_job_attrs.push(Attribute::new("job_owner", new_job.owner)); - new_job_attrs.push(Attribute::new("job_name", new_job.name)); - new_job_attrs.push(Attribute::new( - "job_status", - serde_json_wasm::to_string(&new_job.status)?, - )); - new_job_attrs.push(Attribute::new( - "job_condition", - serde_json_wasm::to_string(&new_job.condition)?, - )); - new_job_attrs.push(Attribute::new( - "job_msgs", - serde_json_wasm::to_string(&new_job.msgs)?, - )); - new_job_attrs.push(Attribute::new("job_reward", new_job.reward)); - new_job_attrs.push(Attribute::new("job_creation_fee", fee)); - new_job_attrs.push(Attribute::new( - "job_last_updated_time", - new_job.last_update_time, - )); - new_job_attrs.push(Attribute::new("sub_action", "recur_job")); - } - } - } - - Ok(Response::new() - .add_attribute("action", "execute_reply") - .add_attribute("job_id", finished_job.id) - .add_attributes(res_attrs) - .add_attributes(new_job_attrs) - .add_messages(msgs)) + // Main account has been created, now create sub account and job + REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB => { + reply::account::create_main_account_and_sub_account_and_job(deps, env, msg) } + // Job has been executed + REPLY_ID_EXECUTE_JOB => reply::job::execute_job(deps, env, msg), + _ => Err(ContractError::UnknownReplyId {}), } } diff --git a/contracts/warp-controller/src/error.rs b/contracts/warp-controller/src/error.rs index 174fc4f2..5af761f1 100644 --- a/contracts/warp-controller/src/error.rs +++ b/contracts/warp-controller/src/error.rs @@ -81,6 +81,9 @@ pub enum ContractError { #[error("Eviction period not elapsed.")] EvictionPeriodNotElapsed {}, + + #[error("Unknown reply ID.")] + UnknownReplyId {}, } impl From for ContractError { diff --git a/contracts/warp-controller/src/execute/account.rs b/contracts/warp-controller/src/execute/account.rs index 65e89ec6..d099170e 100644 --- a/contracts/warp-controller/src/execute/account.rs +++ b/contracts/warp-controller/src/execute/account.rs @@ -1,104 +1,102 @@ use crate::state::{ACCOUNTS, CONFIG}; use crate::ContractError; -use controller::account::{ - CreateAccountMsg, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, -}; +use controller::account::{Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; use cosmwasm_std::{ to_binary, BankMsg, CosmosMsg, DepsMut, Env, MessageInfo, ReplyOn, Response, SubMsg, WasmMsg, }; -pub fn create_account( - deps: DepsMut, - env: Env, - info: MessageInfo, - data: CreateAccountMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; +// pub fn create_account( +// deps: DepsMut, +// env: Env, +// info: MessageInfo, +// data: CreateAccountMsg, +// ) -> Result { +// let config = CONFIG.load(deps.storage)?; - let item = ACCOUNTS() - .idx - .account - .item(deps.storage, info.sender.clone()); +// let item = ACCOUNTS() +// .idx +// .account +// .item(deps.storage, info.sender.clone()); - if item?.is_some() { - return Err(ContractError::AccountCannotCreateAccount {}); - } +// if item?.is_some() { +// return Err(ContractError::AccountCannotCreateAccount {}); +// } - if ACCOUNTS().has(deps.storage, info.sender.clone()) { - let account = ACCOUNTS().load(deps.storage, info.sender.clone())?; +// if ACCOUNTS().has(deps.storage, info.sender.clone()) { +// let account = ACCOUNTS().load(deps.storage, info.sender.clone())?; - let cw_funds_vec = match data.funds { - None => { - vec![] - } - Some(funds) => funds, - }; +// let cw_funds_vec = match data.funds { +// None => { +// vec![] +// } +// Some(funds) => funds, +// }; - let mut msgs_vec: Vec = vec![]; +// let mut msgs_vec: Vec = vec![]; - if !info.funds.is_empty() { - msgs_vec.push(CosmosMsg::Bank(BankMsg::Send { - to_address: account.account.to_string(), - amount: info.funds.clone(), - })) - } +// if !info.funds.is_empty() { +// msgs_vec.push(CosmosMsg::Bank(BankMsg::Send { +// to_address: account.account.to_string(), +// amount: info.funds.clone(), +// })) +// } - for cw_fund in &cw_funds_vec { - msgs_vec.push(CosmosMsg::Wasm(match cw_fund { - Fund::Cw20(cw20_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw20_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { - owner: info.sender.clone().to_string(), - recipient: account.account.clone().to_string(), - amount: cw20_fund.amount, - }))?, - funds: vec![], - }, - Fund::Cw721(cw721_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw721_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { - recipient: account.account.clone().to_string(), - token_id: cw721_fund.token_id.clone(), - }))?, - funds: vec![], - }, - })) - } +// for cw_fund in &cw_funds_vec { +// msgs_vec.push(CosmosMsg::Wasm(match cw_fund { +// Fund::Cw20(cw20_fund) => WasmMsg::Execute { +// contract_addr: deps +// .api +// .addr_validate(&cw20_fund.contract_addr)? +// .to_string(), +// msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { +// owner: info.sender.clone().to_string(), +// recipient: account.account.clone().to_string(), +// amount: cw20_fund.amount, +// }))?, +// funds: vec![], +// }, +// Fund::Cw721(cw721_fund) => WasmMsg::Execute { +// contract_addr: deps +// .api +// .addr_validate(&cw721_fund.contract_addr)? +// .to_string(), +// msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { +// recipient: account.account.clone().to_string(), +// token_id: cw721_fund.token_id.clone(), +// }))?, +// funds: vec![], +// }, +// })) +// } - return Ok(Response::new() - .add_attribute("action", "create_account") - .add_attribute("owner", account.owner) - .add_attribute("account_address", account.account) - .add_messages(msgs_vec)); - } +// return Ok(Response::new() +// .add_attribute("action", "create_account") +// .add_attribute("owner", account.owner) +// .add_attribute("account_address", account.account) +// .add_messages(msgs_vec)); +// } - let submsg = SubMsg { - id: 0, - msg: CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some(env.contract.address.to_string()), - code_id: config.warp_account_code_id.u64(), - msg: to_binary(&account::InstantiateMsg { - owner: info.sender.to_string(), - funds: data.funds, - msgs: data.msgs, - is_sub_account: Some(false), - main_account_addr: None, - })?, - funds: info.funds, - label: info.sender.to_string(), - }), - gas_limit: None, - reply_on: ReplyOn::Always, - }; +// let submsg = SubMsg { +// id: 0, +// msg: CosmosMsg::Wasm(WasmMsg::Instantiate { +// admin: Some(env.contract.address.to_string()), +// code_id: config.warp_account_code_id.u64(), +// msg: to_binary(&account::InstantiateMsg { +// owner: info.sender.to_string(), +// funds: data.funds, +// msgs: data.msgs, +// is_sub_account: Some(false), +// main_account_addr: None, +// })?, +// funds: info.funds, +// label: info.sender.to_string(), +// }), +// gas_limit: None, +// reply_on: ReplyOn::Always, +// }; - Ok(Response::new() - .add_attribute("action", "create_account") - .add_submessage(submsg)) -} +// Ok(Response::new() +// .add_attribute("action", "create_account") +// .add_submessage(submsg)) +// } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 2a37ed83..6a37b3dc 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,7 +1,12 @@ +use crate::contract::{ + REPLY_ID_CREATE_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB, REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, + REPLY_ID_EXECUTE_JOB, +}; use crate::state::{JobQueue, ACCOUNTS, CONFIG, STATE}; use crate::ContractError; use crate::ContractError::EvictionPeriodNotElapsed; -use account::GenericMsg; +use account::{FirstFreeSubAccountResponse, GenericMsg}; +use controller::account::{Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; use controller::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Job, JobStatus, UpdateJobMsg, }; @@ -44,92 +49,219 @@ pub fn create_job( }), )?; - let account_record = ACCOUNTS() + // First try to query main account by account address (query index key which is account by sender) + // This can happen when account contract calls controller's create_job + // The result would be none if user (account owner) calls create_job directly + let main_account = match ACCOUNTS() .idx .account - .item(deps.storage, info.sender.clone())?; - - let account = match account_record { - None => ACCOUNTS() - .load(deps.storage, info.sender) - .map_err(|_e| ContractError::AccountDoesNotExist {})?, - Some(record) => record.1, - }; - - let job = JobQueue::add( - &mut deps, - Job { - id: state.current_job_id, - prev_id: None, - owner: account.owner, - account: account.account.clone(), - last_update_time: Uint64::from(env.block.time.seconds()), - name: data.name, - status: JobStatus::Pending, - condition: data.condition.clone(), - terminate_condition: data.terminate_condition, - recurring: data.recurring, - requeue_on_evict: data.requeue_on_evict, - vars: data.vars, - msgs: data.msgs, - reward: data.reward, - description: data.description, - labels: data.labels, - assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), + .item(deps.storage, info.sender.clone())? + { + // create_job is called by account contract + Some(record) => Some(record.1), + // create_job is called by user + None => match ACCOUNTS().may_load(deps.storage, info.sender.clone())? { + // User has main account + Some(account) => Some(account), + // User does not have main account + None => None, }, - )?; - - // Assume reward.amount == warp token allowance - let fee = data.reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); - - let reward_send_msgs = vec![ - // Job sends reward to controller - WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: env.contract.address.to_string(), - amount: vec![Coin::new((data.reward).u128(), config.fee_denom.clone())], - })], - }))?, - funds: vec![], - }, - // Job owner sends fee to fee collector - WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.fee_collector.to_string(), - amount: vec![Coin::new((fee).u128(), config.fee_denom)], - })], - }))?, - funds: vec![], - }, - ]; - - let mut account_msgs: Vec = vec![]; + }; - if let Some(msgs) = data.account_msgs { - account_msgs = vec![WasmMsg::Execute { - contract_addr: account.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs }))?, - funds: vec![], - }]; + match main_account { + None => { + let create_main_account_submsg = SubMsg { + id: REPLY_ID_CREATE_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB, + msg: CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: config.warp_account_code_id.u64(), + msg: to_binary(&account::InstantiateMsg { + owner: info.sender.to_string(), + funds: data.funds, + msgs: data.account_msgs, + is_sub_account: false, + main_account_addr: None, + })?, + funds: info.funds, + label: info.sender.to_string(), + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }; + + Ok(Response::new() + .add_submessage(create_main_account_submsg) + .add_attribute( + "action", + "create_job_and_new_main_account_and_new_sub_account", + )) + } + Some(main_account) => { + if main_account.owner != info.sender { + return Err(ContractError::Unauthorized {}); + } + let main_account_addr = main_account.account; + let available_sub_account: FirstFreeSubAccountResponse = + deps.querier.query_wasm_smart( + main_account_addr.clone(), + &account::QueryMsg::QueryFirstFreeSubAccount( + account::QueryFirstFreeSubAccountMsg {}, + ), + )?; + match available_sub_account.sub_account { + None => { + let create_sub_account_submsg = SubMsg { + id: REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, + msg: CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(env.contract.address.to_string()), + code_id: config.warp_account_code_id.u64(), + msg: to_binary(&account::InstantiateMsg { + owner: info.sender.to_string(), + funds: data.funds, + msgs: data.account_msgs, + is_sub_account: true, + main_account_addr: Some(main_account_addr.clone().to_string()), + })?, + funds: info.funds, + label: info.sender.to_string(), + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }; + + Ok(Response::new() + .add_submessage(create_sub_account_submsg) + .add_attribute("action", "create_job_and_new_sub_account")) + } + Some(sub_account) => { + let sub_account_addr = sub_account.account_addr; + let job = JobQueue::add( + &mut deps, + Job { + id: state.current_job_id, + prev_id: None, + owner: info.sender.clone(), + account: sub_account_addr.clone(), + last_update_time: Uint64::from(env.block.time.seconds()), + name: data.name, + status: JobStatus::Pending, + condition: data.condition.clone(), + terminate_condition: data.terminate_condition, + recurring: data.recurring, + requeue_on_evict: data.requeue_on_evict, + vars: data.vars, + msgs: data.msgs, + reward: data.reward, + description: data.description, + labels: data.labels, + assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), + }, + )?; + + // Assume reward.amount == warp token allowance + let fee = data.reward * Uint128::from(config.creation_fee_percentage) + / Uint128::new(100); + + let cw_funds_vec = match data.funds { + None => { + vec![] + } + Some(funds) => funds, + }; + + let mut fund_account_msgs: Vec = vec![]; + + if !info.funds.is_empty() { + fund_account_msgs.push(CosmosMsg::Bank(BankMsg::Send { + to_address: sub_account_addr.clone().to_string(), + amount: info.funds.clone(), + })) + } + + for cw_fund in &cw_funds_vec { + fund_account_msgs.push(CosmosMsg::Wasm(match cw_fund { + Fund::Cw20(cw20_fund) => WasmMsg::Execute { + contract_addr: deps + .api + .addr_validate(&cw20_fund.contract_addr)? + .to_string(), + msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { + owner: info.sender.clone().to_string(), + recipient: sub_account_addr.clone().to_string(), + amount: cw20_fund.amount, + }))?, + funds: vec![], + }, + Fund::Cw721(cw721_fund) => WasmMsg::Execute { + contract_addr: deps + .api + .addr_validate(&cw721_fund.contract_addr)? + .to_string(), + msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { + recipient: sub_account_addr.clone().to_string(), + token_id: cw721_fund.token_id.clone(), + }))?, + funds: vec![], + }, + })) + } + + let reward_send_msgs = vec![ + // Job sends reward to controller + WasmMsg::Execute { + contract_addr: sub_account_addr.to_string(), + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msgs: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: env.contract.address.to_string(), + amount: vec![Coin::new( + (data.reward).u128(), + config.fee_denom.clone(), + )], + })], + }))?, + funds: vec![], + }, + // Job owner sends fee to fee collector + WasmMsg::Execute { + contract_addr: sub_account_addr.to_string(), + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msgs: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: config.fee_collector.to_string(), + amount: vec![Coin::new((fee).u128(), config.fee_denom)], + })], + }))?, + funds: vec![], + }, + ]; + + let mut account_msgs: Vec = vec![]; + + if let Some(msgs) = data.account_msgs { + account_msgs = vec![WasmMsg::Execute { + contract_addr: sub_account_addr.to_string(), + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs }))?, + funds: vec![], + }]; + } + + Ok(Response::new() + .add_messages(fund_account_msgs) + .add_messages(reward_send_msgs) + .add_messages(account_msgs) + .add_attribute("action", "create_job") + .add_attribute("job_id", job.id) + .add_attribute("job_owner", job.owner) + .add_attribute("job_name", job.name) + .add_attribute("job_status", serde_json_wasm::to_string(&job.status)?) + .add_attribute("job_condition", serde_json_wasm::to_string(&job.condition)?) + .add_attribute("job_msgs", serde_json_wasm::to_string(&job.msgs)?) + .add_attribute("job_reward", job.reward) + .add_attribute("job_creation_fee", fee) + .add_attribute("job_last_updated_time", job.last_update_time)) + } + } + } } - - Ok(Response::new() - .add_messages(reward_send_msgs) - .add_attribute("action", "create_job") - .add_attribute("job_id", job.id) - .add_attribute("job_owner", job.owner) - .add_attribute("job_name", job.name) - .add_attribute("job_status", serde_json_wasm::to_string(&job.status)?) - .add_attribute("job_condition", serde_json_wasm::to_string(&job.condition)?) - .add_attribute("job_msgs", serde_json_wasm::to_string(&job.msgs)?) - .add_attribute("job_reward", job.reward) - .add_attribute("job_creation_fee", fee) - .add_attribute("job_last_updated_time", job.last_update_time) - .add_messages(account_msgs)) } pub fn delete_job( @@ -302,7 +434,7 @@ pub fn execute_job( } submsgs.push(SubMsg { - id: job.id.u64(), + id: REPLY_ID_EXECUTE_JOB, msg: CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job.account.to_string(), msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { diff --git a/contracts/warp-controller/src/lib.rs b/contracts/warp-controller/src/lib.rs index b9b3cea3..49f9d59a 100644 --- a/contracts/warp-controller/src/lib.rs +++ b/contracts/warp-controller/src/lib.rs @@ -6,6 +6,8 @@ pub use crate::error::ContractError; mod execute; mod query; +mod reply; + #[cfg(test)] mod tests; mod util; diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs new file mode 100644 index 00000000..cd3c7813 --- /dev/null +++ b/contracts/warp-controller/src/reply/account.rs @@ -0,0 +1,273 @@ +use account::{FreeSubAccountMsg, GenericMsg, OccupySubAccountMsg, WithdrawAssetsMsg}; +use controller::{ + account::{Account, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}, + job::{Job, JobStatus}, +}; +use cosmwasm_std::{ + to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Coin, CosmosMsg, DepsMut, Env, + QueryRequest, Reply, Response, StdError, StdResult, SubMsgResult, Uint128, Uint64, WasmMsg, +}; + +use crate::{ + error::map_contract_error, + state::{JobQueue, ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS, STATE}, + ContractError, +}; + +pub fn create_main_account_and_sub_account_and_job( + mut deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { + let reply = msg.result.into_result().map_err(StdError::generic_err)?; + + let event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "instantiate") + }) + .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + + let owner = event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "owner") + .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? + .value; + + let address = event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "contract_addr") + .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? + .value; + + let funds: Vec = serde_json_wasm::from_str( + &event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "funds") + .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? + .value, + )?; + + let cw_funds: Option> = serde_json_wasm::from_str( + &event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "cw_funds") + .ok_or_else(|| StdError::generic_err("cannot find `cw_funds` attribute"))? + .value, + )?; + + let account_msgs: Option> = serde_json_wasm::from_str( + &event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "account_msgs") + .ok_or_else(|| StdError::generic_err("cannot find `account_msgs` attribute"))? + .value, + )?; + + let cw_funds_vec = match cw_funds { + None => { + vec![] + } + Some(funds) => funds, + }; + + let mut msgs_vec: Vec = vec![]; + + for cw_fund in &cw_funds_vec { + msgs_vec.push(CosmosMsg::Wasm(match cw_fund { + Fund::Cw20(cw20_fund) => WasmMsg::Execute { + contract_addr: deps + .api + .addr_validate(&cw20_fund.contract_addr)? + .to_string(), + msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { + owner: owner.clone(), + recipient: address.clone(), + amount: cw20_fund.amount, + }))?, + funds: vec![], + }, + Fund::Cw721(cw721_fund) => WasmMsg::Execute { + contract_addr: deps + .api + .addr_validate(&cw721_fund.contract_addr)? + .to_string(), + msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { + recipient: address.clone(), + token_id: cw721_fund.token_id.clone(), + }))?, + funds: vec![], + }, + })) + } + + if let Some(msgs) = account_msgs { + for msg in msgs { + msgs_vec.push(msg); + } + } + + if ACCOUNTS().has(deps.storage, deps.api.addr_validate(&owner)?) { + return Err(ContractError::AccountAlreadyExists {}); + } + + ACCOUNTS().save( + deps.storage, + deps.api.addr_validate(&owner)?, + &Account { + owner: deps.api.addr_validate(&owner.clone())?, + account: deps.api.addr_validate(&address)?, + }, + )?; + Ok(Response::new() + .add_attribute("action", "create_sub_account_and_job_reply") + .add_attribute("job_id", value) + .add_attribute("owner", owner) + .add_attribute("account_address", address) + .add_attribute("funds", serde_json_wasm::to_string(&funds)?) + .add_attribute("cw_funds", serde_json_wasm::to_string(&cw_funds_vec)?) + .add_messages(msgs_vec)) +} + +pub fn create_sub_account_and_job( + mut deps: DepsMut, + env: Env, + msg: Reply, +) -> Result { + let reply = msg.result.into_result().map_err(StdError::generic_err)?; + + let event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "instantiate") + }) + .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + + let owner = event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "owner") + .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? + .value; + + let address = event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "contract_addr") + .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? + .value; + + let funds: Vec = serde_json_wasm::from_str( + &event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "funds") + .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? + .value, + )?; + + let cw_funds: Option> = serde_json_wasm::from_str( + &event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "cw_funds") + .ok_or_else(|| StdError::generic_err("cannot find `cw_funds` attribute"))? + .value, + )?; + + let account_msgs: Option> = serde_json_wasm::from_str( + &event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "account_msgs") + .ok_or_else(|| StdError::generic_err("cannot find `account_msgs` attribute"))? + .value, + )?; + + let cw_funds_vec = match cw_funds { + None => { + vec![] + } + Some(funds) => funds, + }; + + let mut msgs_vec: Vec = vec![]; + + for cw_fund in &cw_funds_vec { + msgs_vec.push(CosmosMsg::Wasm(match cw_fund { + Fund::Cw20(cw20_fund) => WasmMsg::Execute { + contract_addr: deps + .api + .addr_validate(&cw20_fund.contract_addr)? + .to_string(), + msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { + owner: owner.clone(), + recipient: address.clone(), + amount: cw20_fund.amount, + }))?, + funds: vec![], + }, + Fund::Cw721(cw721_fund) => WasmMsg::Execute { + contract_addr: deps + .api + .addr_validate(&cw721_fund.contract_addr)? + .to_string(), + msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { + recipient: address.clone(), + token_id: cw721_fund.token_id.clone(), + }))?, + funds: vec![], + }, + })) + } + + if let Some(msgs) = account_msgs { + for msg in msgs { + msgs_vec.push(msg); + } + } + + if ACCOUNTS().has(deps.storage, deps.api.addr_validate(&owner)?) { + return Err(ContractError::AccountAlreadyExists {}); + } + + ACCOUNTS().save( + deps.storage, + deps.api.addr_validate(&owner)?, + &Account { + owner: deps.api.addr_validate(&owner.clone())?, + account: deps.api.addr_validate(&address)?, + }, + )?; + Ok(Response::new() + .add_attribute("action", "create_sub_account_and_job_reply") + .add_attribute("job_id", value) + .add_attribute("owner", owner) + .add_attribute("account_address", address) + .add_attribute("funds", serde_json_wasm::to_string(&funds)?) + .add_attribute("cw_funds", serde_json_wasm::to_string(&cw_funds_vec)?) + .add_messages(msgs_vec)) +} diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs new file mode 100644 index 00000000..ffe0c5a7 --- /dev/null +++ b/contracts/warp-controller/src/reply/job.rs @@ -0,0 +1,213 @@ +use account::{FreeSubAccountMsg, GenericMsg, OccupySubAccountMsg, WithdrawAssetsMsg}; +use controller::job::{Job, JobStatus}; +use cosmwasm_std::{ + to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Coin, CosmosMsg, DepsMut, Env, + QueryRequest, Reply, Response, StdError, StdResult, SubMsgResult, Uint128, Uint64, WasmMsg, +}; + +use crate::{ + error::map_contract_error, + state::{JobQueue, ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS, STATE}, + ContractError, +}; + +pub fn execute_job(mut deps: DepsMut, env: Env, msg: Reply) -> Result { + let state = STATE.load(deps.storage)?; + + let new_status = match msg.result { + SubMsgResult::Ok(_) => JobStatus::Executed, + SubMsgResult::Err(_) => JobStatus::Failed, + }; + + let finished_job = JobQueue::finalize(&mut deps, env.clone(), msg.id, new_status)?; + + let res_attrs = match msg.result { + SubMsgResult::Err(e) => vec![Attribute::new( + "transaction_error", + format!("{}. {}", &e, map_contract_error(&e)), + )], + _ => vec![], + }; + + let mut msgs = vec![]; + let mut new_job_attrs = vec![]; + + let config = CONFIG.load(deps.storage)?; + + // Assume reward.amount == warp token allowance + let fee = + finished_job.reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); + + let account_amount = deps + .querier + .query::(&QueryRequest::Bank(BankQuery::Balance { + address: finished_job.account.to_string(), + denom: config.fee_denom.clone(), + }))? + .amount + .amount; + + if finished_job.recurring { + if account_amount < fee + finished_job.reward { + new_job_attrs.push(Attribute::new("action", "recur_job")); + new_job_attrs.push(Attribute::new("creation_status", "failed_insufficient_fee")) + } else if !(finished_job.status == JobStatus::Executed + || finished_job.status == JobStatus::Failed) + { + new_job_attrs.push(Attribute::new("action", "recur_job")); + new_job_attrs.push(Attribute::new( + "creation_status", + "failed_invalid_job_status", + )); + } else { + let new_vars: String = deps.querier.query_wasm_smart( + config.resolver_address.clone(), + &resolver::QueryMsg::QueryApplyVarFn(resolver::QueryApplyVarFnMsg { + vars: finished_job.vars, + status: finished_job.status.clone(), + warp_account_addr: Some(finished_job.account.to_string()), + }), + )?; + + let should_terminate_job: bool; + match finished_job.terminate_condition.clone() { + Some(terminate_condition) => { + let resolution: StdResult = deps.querier.query_wasm_smart( + config.resolver_address, + &resolver::QueryMsg::QueryResolveCondition( + resolver::QueryResolveConditionMsg { + condition: terminate_condition, + vars: new_vars.clone(), + warp_account_addr: Some(finished_job.account.to_string()), + }, + ), + ); + if let Err(e) = resolution { + should_terminate_job = true; + new_job_attrs.push(Attribute::new("action", "recur_job")); + new_job_attrs + .push(Attribute::new("job_terminate_condition_status", "invalid")); + new_job_attrs.push(Attribute::new( + "creation_status", + format!( + "terminated_due_to_terminate_condition_resolves_to_error. {}", + e + ), + )); + } else { + new_job_attrs + .push(Attribute::new("job_terminate_condition_status", "valid")); + if resolution? { + should_terminate_job = true; + new_job_attrs.push(Attribute::new("action", "recur_job")); + new_job_attrs.push(Attribute::new( + "creation_status", + "terminated_due_to_terminate_condition_resolves_to_true", + )); + } else { + should_terminate_job = false; + } + } + } + None => { + should_terminate_job = false; + } + } + + if !should_terminate_job { + let new_job = JobQueue::add( + &mut deps, + Job { + id: state.current_job_id, + prev_id: Some(finished_job.id), + owner: finished_job.owner.clone(), + account: finished_job.account.clone(), + last_update_time: Uint64::from(env.block.time.seconds()), + name: finished_job.name.clone(), + description: finished_job.description, + labels: finished_job.labels, + status: JobStatus::Pending, + condition: finished_job.condition.clone(), + terminate_condition: finished_job.terminate_condition.clone(), + vars: new_vars, + requeue_on_evict: finished_job.requeue_on_evict, + recurring: finished_job.recurring, + msgs: finished_job.msgs.clone(), + reward: finished_job.reward, + assets_to_withdraw: finished_job.assets_to_withdraw, + }, + )?; + + msgs.push( + // Job owner's warp account sends fee to fee collector + WasmMsg::Execute { + contract_addr: finished_job.account.to_string(), + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msgs: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: config.fee_collector.to_string(), + amount: vec![Coin::new((fee).u128(), config.fee_denom.clone())], + })], + }))?, + funds: vec![], + }, + ); + + msgs.push( + // Job owner's warp account sends reward to controller + WasmMsg::Execute { + contract_addr: finished_job.account.to_string(), + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msgs: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: env.contract.address.to_string(), + amount: vec![Coin::new((new_job.reward).u128(), config.fee_denom)], + })], + }))?, + funds: vec![], + }, + ); + + msgs.push( + // Job owner withdraw all assets that are listed from warp account to itself + WasmMsg::Execute { + contract_addr: finished_job.account.to_string(), + msg: to_binary(&account::ExecuteMsg::WithdrawAssets(WithdrawAssetsMsg { + asset_infos: new_job.assets_to_withdraw, + }))?, + funds: vec![], + }, + ); + + new_job_attrs.push(Attribute::new("action", "create_job")); + new_job_attrs.push(Attribute::new("job_id", new_job.id)); + new_job_attrs.push(Attribute::new("job_owner", new_job.owner)); + new_job_attrs.push(Attribute::new("job_name", new_job.name)); + new_job_attrs.push(Attribute::new( + "job_status", + serde_json_wasm::to_string(&new_job.status)?, + )); + new_job_attrs.push(Attribute::new( + "job_condition", + serde_json_wasm::to_string(&new_job.condition)?, + )); + new_job_attrs.push(Attribute::new( + "job_msgs", + serde_json_wasm::to_string(&new_job.msgs)?, + )); + new_job_attrs.push(Attribute::new("job_reward", new_job.reward)); + new_job_attrs.push(Attribute::new("job_creation_fee", fee)); + new_job_attrs.push(Attribute::new( + "job_last_updated_time", + new_job.last_update_time, + )); + new_job_attrs.push(Attribute::new("sub_action", "recur_job")); + } + } + } + + Ok(Response::new() + .add_attribute("action", "execute_job_reply") + .add_attribute("job_id", finished_job.id) + .add_attributes(res_attrs) + .add_attributes(new_job_attrs) + .add_messages(msgs)) +} diff --git a/contracts/warp-controller/src/reply/mod.rs b/contracts/warp-controller/src/reply/mod.rs new file mode 100644 index 00000000..d882e236 --- /dev/null +++ b/contracts/warp-controller/src/reply/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod account; +pub(crate) mod job; diff --git a/packages/account/src/lib.rs b/packages/account/src/lib.rs index b2f42709..5c4e6030 100644 --- a/packages/account/src/lib.rs +++ b/packages/account/src/lib.rs @@ -5,30 +5,29 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[cw_serde] -pub struct Config { - pub owner: Addr, - pub warp_addr: Addr, - pub is_sub_account: bool, - // If current account is a main account, main_account_addr is itself, - // If current account is a sub account, main_account_addr is its main account address +pub struct SubAccountConfig { pub main_account_addr: Addr, + // If occupied, occupied_by_job_id is the job id of the pending job that is using this sub account + pub occupied_by_job_id: Option, } #[cw_serde] -pub struct SubAccount { - pub addr: String, - // If occupied, occupied_by_job_id is the job id of the pending job that is using this sub account - pub occupied_by_job_id: Option, +pub struct Config { + pub owner: Addr, + // Address of warp controller contract + pub creator_addr: Addr, + // Address of current warp account contract + pub account_addr: Addr, + // Exist if current account is a sub account + pub sub_account_config: Option, } #[cw_serde] pub struct InstantiateMsg { pub owner: String, + pub is_sub_account: bool, pub msgs: Option>, pub funds: Option>, - // By default it's false meaning it's a main account - // If it's true, it's a sub account - pub is_sub_account: Option, // Only supplied when is_sub_account is true // Skipped if it's instantiating a main account pub main_account_addr: Option, @@ -146,7 +145,7 @@ pub struct QueryOccupiedSubAccountsMsg { #[cw_serde] pub struct OccupiedSubAccountsResponse { - pub sub_accounts: Vec, + pub sub_accounts: Vec, pub total_count: usize, } @@ -158,7 +157,7 @@ pub struct QueryFreeSubAccountsMsg { #[cw_serde] pub struct FreeSubAccountsResponse { - pub sub_accounts: Vec, + pub sub_accounts: Vec, pub total_count: usize, } @@ -167,7 +166,7 @@ pub struct QueryFirstFreeSubAccountMsg {} #[cw_serde] pub struct FirstFreeSubAccountResponse { - pub sub_account: Option, + pub sub_account: Option, } #[cw_serde] diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index 2714a1d4..88bcf8c8 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -1,11 +1,5 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, CosmosMsg, Uint128}; - -#[cw_serde] -pub struct CreateAccountMsg { - pub funds: Option>, - pub msgs: Option>, -} +use cosmwasm_std::{Addr, Uint128}; #[cw_serde] pub enum Fund { diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index e40c8ab6..a7766314 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -1,4 +1,4 @@ -use crate::account::AssetInfo; +use crate::account::{AssetInfo, Fund}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, CosmosMsg, Uint128, Uint64}; use schemars::JsonSchema; @@ -58,6 +58,8 @@ pub enum JobStatus { Evicted, } +// Create a job using sub account, if sub account does not exist, create it +// Each sub account will only be used for 1 job, so we achieve funds isolation #[cw_serde] pub struct CreateJobMsg { pub name: String, @@ -72,6 +74,7 @@ pub struct CreateJobMsg { pub reward: Uint128, pub assets_to_withdraw: Option>, pub account_msgs: Option>, + pub funds: Option>, } #[cw_serde] diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 9c47f32e..3cb87caf 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -1,6 +1,4 @@ -use crate::account::{ - AccountResponse, AccountsResponse, CreateAccountMsg, QueryAccountMsg, QueryAccountsMsg, -}; +use crate::account::{AccountResponse, AccountsResponse, QueryAccountMsg, QueryAccountsMsg}; use crate::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, JobResponse, JobsResponse, QueryJobMsg, QueryJobsMsg, UpdateJobMsg, @@ -68,8 +66,6 @@ pub enum ExecuteMsg { ExecuteJob(ExecuteJobMsg), EvictJob(EvictJobMsg), - CreateAccount(CreateAccountMsg), - UpdateConfig(UpdateConfigMsg), MigrateAccounts(MigrateAccountsMsg), From 9519371b7193a0b6fdcfa561cc1334e64c447e4c Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:34:59 -0700 Subject: [PATCH 040/133] create job will always use sub account now --- Cargo.lock | 2 + contracts/warp-account/Cargo.toml | 3 +- contracts/warp-account/src/contract.rs | 25 +- .../warp-account/src/integration_tests.rs | 7 +- contracts/warp-account/src/tests.rs | 20 +- contracts/warp-controller/src/contract.rs | 50 +- contracts/warp-controller/src/error.rs | 6 + .../warp-controller/src/execute/account.rs | 102 ---- contracts/warp-controller/src/execute/job.rs | 542 ++++++++++-------- contracts/warp-controller/src/execute/mod.rs | 1 - .../warp-controller/src/reply/account.rs | 266 +++++---- contracts/warp-controller/src/reply/job.rs | 122 ++-- .../src/tests/execute/account/mod.rs | 1 - .../execute/account/test_create_account.rs | 1 - .../warp-controller/src/tests/execute/mod.rs | 1 - .../warp-controller/src/util/attribute.rs | 0 contracts/warp-controller/src/util/fee.rs | 17 + contracts/warp-controller/src/util/mod.rs | 4 + contracts/warp-controller/src/util/msg.rs | 170 ++++++ .../warp-controller/src/util/sub_account.rs | 5 + contracts/warp-resolver/Cargo.toml | 3 +- contracts/warp-resolver/src/contract.rs | 2 + contracts/warp-templates/Cargo.toml | 2 +- contracts/warp-templates/src/contract.rs | 1 + packages/account/src/lib.rs | 17 +- packages/controller/src/account.rs | 2 +- packages/controller/src/job.rs | 4 +- 27 files changed, 811 insertions(+), 565 deletions(-) delete mode 100644 contracts/warp-controller/src/execute/account.rs delete mode 100644 contracts/warp-controller/src/tests/execute/account/mod.rs delete mode 100644 contracts/warp-controller/src/tests/execute/account/test_create_account.rs create mode 100644 contracts/warp-controller/src/util/attribute.rs create mode 100644 contracts/warp-controller/src/util/fee.rs create mode 100644 contracts/warp-controller/src/util/msg.rs create mode 100644 contracts/warp-controller/src/util/sub_account.rs diff --git a/Cargo.lock b/Cargo.lock index 6c3ce2c5..1acfb858 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1044,6 +1044,7 @@ dependencies = [ "cw-asset", "cw-multi-test", "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", "cw2 0.16.0", "cw20", "cw721", @@ -1089,6 +1090,7 @@ dependencies = [ "cw-asset", "cw-multi-test", "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", "cw2 0.16.0", "cw20", "cw721", diff --git a/contracts/warp-account/Cargo.toml b/contracts/warp-account/Cargo.toml index cec48e20..94cd2c23 100644 --- a/contracts/warp-account/Cargo.toml +++ b/contracts/warp-account/Cargo.toml @@ -39,6 +39,7 @@ cw-storage-plus = "0.16" cw2 = "0.16" cw20 = "0.16" cw721 = "0.16.0" +cw-utils = "0.16" controller = { path = "../../packages/controller", default-features = false, version = "*" } account = { path = "../../packages/account", default-features = false, version = "*" } schemars = "0.8" @@ -49,4 +50,4 @@ prost = "0.11.9" [dev-dependencies] cw-multi-test = "0.16.0" -anyhow = "1.0.71" +anyhow = "1.0.71" diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index 455ec8bb..be8df853 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -4,6 +4,7 @@ use account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SubAccou use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; +use cw_utils::nonpayable; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -34,7 +35,13 @@ pub fn instantiate( )?; Ok(Response::new() + .add_messages(if msg.is_sub_account { + msg.msgs.clone() + } else { + vec![] + }) .add_attribute("action", "instantiate") + .add_attribute("job_id", msg.job_id) .add_attribute("contract_addr", instantiated_account_addr.clone()) .add_attribute("is_sub_account", format!("{}", msg.is_sub_account)) .add_attribute( @@ -43,8 +50,11 @@ pub fn instantiate( .unwrap_or(instantiated_account_addr.to_string()), ) .add_attribute("owner", msg.owner) - .add_attribute("funds", serde_json_wasm::to_string(&info.funds)?) - .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?) + .add_attribute( + "native_funds", + serde_json_wasm::to_string(&msg.native_funds)?, + ) + .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.cw_funds)?) .add_attribute("account_msgs", serde_json_wasm::to_string(&msg.msgs)?)) } @@ -64,11 +74,18 @@ pub fn execute( .add_messages(data.msgs) .add_attribute("action", "generic")), ExecuteMsg::WithdrawAssets(data) => { + nonpayable(&info).unwrap(); execute::withdraw::withdraw_assets(deps, env, data, config) } ExecuteMsg::IbcTransfer(data) => execute::ibc::ibc_transfer(env, data), - ExecuteMsg::OccupySubAccount(data) => execute::account::occupy_sub_account(deps, env, data), - ExecuteMsg::FreeSubAccount(data) => execute::account::free_sub_account(deps, env, data), + ExecuteMsg::OccupySubAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::occupy_sub_account(deps, env, data) + } + ExecuteMsg::FreeSubAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::free_sub_account(deps, env, data) + } } } diff --git a/contracts/warp-account/src/integration_tests.rs b/contracts/warp-account/src/integration_tests.rs index 8e69a8a3..c37800a0 100644 --- a/contracts/warp-account/src/integration_tests.rs +++ b/contracts/warp-account/src/integration_tests.rs @@ -17,6 +17,7 @@ mod tests { const DUMMY_WARP_CONTROLLER_ADDR: &str = "terra1"; const USER_1: &str = "terra2"; + const DUMMY_JOB_ID: Uint64 = Uint64::zero(); fn mock_app() -> App { AppBuilder::new().build(|router, _, storage| { @@ -51,10 +52,12 @@ mod tests { Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), &InstantiateMsg { owner: USER_1.to_string(), - msgs: None, - funds: None, + job_id: DUMMY_JOB_ID, is_sub_account, main_account_addr, + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], }, &[], "warp_main_account", diff --git a/contracts/warp-account/src/tests.rs b/contracts/warp-account/src/tests.rs index 715bcc08..19f82980 100644 --- a/contracts/warp-account/src/tests.rs +++ b/contracts/warp-account/src/tests.rs @@ -4,7 +4,7 @@ use account::{ExecuteMsg, GenericMsg, InstantiateMsg}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, - IbcTimeoutBlock, Response, StakingMsg, Uint128, VoteOption, WasmMsg, + IbcTimeoutBlock, Response, StakingMsg, Uint128, Uint64, VoteOption, WasmMsg, }; #[test] @@ -19,10 +19,12 @@ fn test_execute_controller() { info.clone(), InstantiateMsg { owner: "vlad".to_string(), - funds: None, - msgs: None, + job_id: Uint64::zero(), is_sub_account: false, main_account_addr: None, + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], }, ); @@ -144,10 +146,12 @@ fn test_execute_owner() { info, InstantiateMsg { owner: "vlad".to_string(), - funds: None, - msgs: None, + job_id: Uint64::zero(), is_sub_account: false, main_account_addr: None, + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], }, ); @@ -271,10 +275,12 @@ fn test_execute_unauth() { info, InstantiateMsg { owner: "vlad".to_string(), - funds: None, - msgs: None, + job_id: Uint64::zero(), is_sub_account: false, main_account_addr: None, + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], }, ); diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index f4dc7a33..0a9e21b5 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -4,6 +4,7 @@ use cosmwasm_std::{ StdResult, Uint128, Uint64, }; use cw_storage_plus::Item; +use cw_utils::{must_pay, nonpayable}; use crate::state::CONFIG; use crate::{execute, query, reply, state::STATE, ContractError}; @@ -13,7 +14,7 @@ use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State // Reply id for job creation // From a totally new user using warp for the first time, does not have account yet, let alone sub account // So we create both account and sub account and job -pub const REPLY_ID_CREATE_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB: u64 = 1; +pub const REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB: u64 = 1; // Reply id for job creation // From an existing user, who has main account, but does not have available sub account // So we create sub account and job @@ -86,22 +87,44 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { + let config = CONFIG.load(deps.storage)?; match msg { - ExecuteMsg::CreateJob(data) => execute::job::create_job(deps, env, info, data), - ExecuteMsg::DeleteJob(data) => execute::job::delete_job(deps, env, info, data), - ExecuteMsg::UpdateJob(data) => execute::job::update_job(deps, env, info, data), - ExecuteMsg::ExecuteJob(data) => execute::job::execute_job(deps, env, info, data), - ExecuteMsg::EvictJob(data) => execute::job::evict_job(deps, env, info, data), + ExecuteMsg::CreateJob(data) => { + let fee_denom_paid_amount = must_pay(&info, &config.fee_denom).unwrap(); + execute::job::create_job(deps, env, info, data, config, fee_denom_paid_amount) + } + ExecuteMsg::DeleteJob(data) => { + let fee_denom_paid_amount = must_pay(&info, &config.fee_denom).unwrap(); + execute::job::delete_job(deps, env, info, data, config, fee_denom_paid_amount) + } + ExecuteMsg::UpdateJob(data) => { + let fee_denom_paid_amount = must_pay(&info, &config.fee_denom).unwrap(); + execute::job::update_job(deps, env, info, data, config, fee_denom_paid_amount) + } + ExecuteMsg::ExecuteJob(data) => { + nonpayable(&info).unwrap(); + execute::job::execute_job(deps, env, info, data, config) + } + ExecuteMsg::EvictJob(data) => { + nonpayable(&info).unwrap(); + execute::job::evict_job(deps, env, info, data, config) + } - ExecuteMsg::UpdateConfig(data) => execute::controller::update_config(deps, env, info, data), + ExecuteMsg::UpdateConfig(data) => { + nonpayable(&info).unwrap(); + execute::controller::update_config(deps, env, info, data) + } ExecuteMsg::MigrateAccounts(data) => { + nonpayable(&info).unwrap(); execute::controller::migrate_accounts(deps, env, info, data) } ExecuteMsg::MigratePendingJobs(data) => { + nonpayable(&info).unwrap(); execute::controller::migrate_pending_jobs(deps, env, info, data) } ExecuteMsg::MigrateFinishedJobs(data) => { + nonpayable(&info).unwrap(); execute::controller::migrate_finished_jobs(deps, env, info, data) } } @@ -195,18 +218,19 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { +pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { + let config = CONFIG.load(deps.storage)?; match msg.id { + // Main account has been created, now create sub account and job + REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB => { + reply::account::create_main_account_and_sub_account_and_job(deps, env, msg, config) + } // Sub account has been created, now create job REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB => { reply::account::create_sub_account_and_job(deps, env, msg) } - // Main account has been created, now create sub account and job - REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB => { - reply::account::create_main_account_and_sub_account_and_job(deps, env, msg) - } // Job has been executed - REPLY_ID_EXECUTE_JOB => reply::job::execute_job(deps, env, msg), + REPLY_ID_EXECUTE_JOB => reply::job::execute_job(deps, env, msg, config), _ => Err(ContractError::UnknownReplyId {}), } } diff --git a/contracts/warp-controller/src/error.rs b/contracts/warp-controller/src/error.rs index 5af761f1..ae27a6dc 100644 --- a/contracts/warp-controller/src/error.rs +++ b/contracts/warp-controller/src/error.rs @@ -12,6 +12,12 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, + #[error("Insufficient funds to pay for reward and fee.")] + InsufficientFundsToPayForRewardAndFee {}, + + #[error("Insufficient funds to pay for fee.")] + InsufficientFundsToPayForFee {}, + #[error("Funds array in message does not match funds array in job.")] FundsMismatch {}, diff --git a/contracts/warp-controller/src/execute/account.rs b/contracts/warp-controller/src/execute/account.rs deleted file mode 100644 index d099170e..00000000 --- a/contracts/warp-controller/src/execute/account.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::state::{ACCOUNTS, CONFIG}; -use crate::ContractError; -use controller::account::{Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; - -use cosmwasm_std::{ - to_binary, BankMsg, CosmosMsg, DepsMut, Env, MessageInfo, ReplyOn, Response, SubMsg, WasmMsg, -}; - -// pub fn create_account( -// deps: DepsMut, -// env: Env, -// info: MessageInfo, -// data: CreateAccountMsg, -// ) -> Result { -// let config = CONFIG.load(deps.storage)?; - -// let item = ACCOUNTS() -// .idx -// .account -// .item(deps.storage, info.sender.clone()); - -// if item?.is_some() { -// return Err(ContractError::AccountCannotCreateAccount {}); -// } - -// if ACCOUNTS().has(deps.storage, info.sender.clone()) { -// let account = ACCOUNTS().load(deps.storage, info.sender.clone())?; - -// let cw_funds_vec = match data.funds { -// None => { -// vec![] -// } -// Some(funds) => funds, -// }; - -// let mut msgs_vec: Vec = vec![]; - -// if !info.funds.is_empty() { -// msgs_vec.push(CosmosMsg::Bank(BankMsg::Send { -// to_address: account.account.to_string(), -// amount: info.funds.clone(), -// })) -// } - -// for cw_fund in &cw_funds_vec { -// msgs_vec.push(CosmosMsg::Wasm(match cw_fund { -// Fund::Cw20(cw20_fund) => WasmMsg::Execute { -// contract_addr: deps -// .api -// .addr_validate(&cw20_fund.contract_addr)? -// .to_string(), -// msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { -// owner: info.sender.clone().to_string(), -// recipient: account.account.clone().to_string(), -// amount: cw20_fund.amount, -// }))?, -// funds: vec![], -// }, -// Fund::Cw721(cw721_fund) => WasmMsg::Execute { -// contract_addr: deps -// .api -// .addr_validate(&cw721_fund.contract_addr)? -// .to_string(), -// msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { -// recipient: account.account.clone().to_string(), -// token_id: cw721_fund.token_id.clone(), -// }))?, -// funds: vec![], -// }, -// })) -// } - -// return Ok(Response::new() -// .add_attribute("action", "create_account") -// .add_attribute("owner", account.owner) -// .add_attribute("account_address", account.account) -// .add_messages(msgs_vec)); -// } - -// let submsg = SubMsg { -// id: 0, -// msg: CosmosMsg::Wasm(WasmMsg::Instantiate { -// admin: Some(env.contract.address.to_string()), -// code_id: config.warp_account_code_id.u64(), -// msg: to_binary(&account::InstantiateMsg { -// owner: info.sender.to_string(), -// funds: data.funds, -// msgs: data.msgs, -// is_sub_account: Some(false), -// main_account_addr: None, -// })?, -// funds: info.funds, -// label: info.sender.to_string(), -// }), -// gas_limit: None, -// reply_on: ReplyOn::Always, -// }; - -// Ok(Response::new() -// .add_attribute("action", "create_account") -// .add_submessage(submsg)) -// } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 6a37b3dc..3ecdd813 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,18 +1,30 @@ +use cosmwasm_std::{ + to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Coin, CosmosMsg, DepsMut, Env, + MessageInfo, QueryRequest, ReplyOn, Response, StdResult, SubMsg, Uint128, Uint64, WasmMsg, +}; + use crate::contract::{ - REPLY_ID_CREATE_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB, REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, + REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB, REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, REPLY_ID_EXECUTE_JOB, }; -use crate::state::{JobQueue, ACCOUNTS, CONFIG, STATE}; +use crate::state::{JobQueue, ACCOUNTS, STATE}; +use crate::util::{ + fee::deduct_reward_and_fee_from_native_funds, + msg::{ + build_account_execute_generic_msgs, build_account_withdraw_assets_msg, + build_free_sub_account_msg, build_instantiate_warp_account_msg, + build_occupy_sub_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, + build_transfer_native_funds_msg, + }, + sub_account::is_sub_account, +}; use crate::ContractError; -use crate::ContractError::EvictionPeriodNotElapsed; + use account::{FirstFreeSubAccountResponse, GenericMsg}; -use controller::account::{Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; -use controller::job::{ - CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Job, JobStatus, UpdateJobMsg, -}; -use cosmwasm_std::{ - to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Coin, CosmosMsg, DepsMut, Env, - MessageInfo, QueryRequest, ReplyOn, Response, StdResult, SubMsg, Uint128, Uint64, WasmMsg, +use controller::{ + account::CwFund, + job::{CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Job, JobStatus, UpdateJobMsg}, + Config, }; use resolver::QueryHydrateMsgsMsg; @@ -23,10 +35,9 @@ pub fn create_job( env: Env, info: MessageInfo, data: CreateJobMsg, + config: Config, + fee_denom_paid_amount: Uint128, ) -> Result { - let state = STATE.load(deps.storage)?; - let config = CONFIG.load(deps.storage)?; - if data.name.len() > MAX_TEXT_LENGTH { return Err(ContractError::NameTooLong {}); } @@ -49,6 +60,35 @@ pub fn create_job( }), )?; + let fee = data.reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); + let reward_plus_fee = data.reward + fee; + if reward_plus_fee > fee_denom_paid_amount { + return Err(ContractError::InsufficientFundsToPayForRewardAndFee {}); + } + + // Reward and fee will always be in native denom + let native_funds_minus_reward_and_fee = deduct_reward_and_fee_from_native_funds( + info.funds.clone(), + config.fee_denom.clone(), + reward_plus_fee, + ); + + let mut submsgs = vec![]; + let mut msgs = vec![]; + let mut attrs = vec![]; + + // Job owner sends reward to controller when it calls create_job + // Reward stays at controller, no need to send it elsewhere + + msgs.push( + // Job owner sends fee to controller when it calls create_job + // Controller sends fee to fee collector + build_transfer_native_funds_msg( + config.fee_collector.to_string(), + vec![Coin::new(fee.u128(), config.fee_denom.clone())], + ), + ); + // First try to query main account by account address (query index key which is account by sender) // This can happen when account contract calls controller's create_job // The result would be none if user (account owner) calls create_job directly @@ -68,33 +108,57 @@ pub fn create_job( }, }; + let state = STATE.load(deps.storage)?; + let mut job = JobQueue::add( + &mut deps, + Job { + id: state.current_job_id, + prev_id: None, + owner: info.sender.clone(), + // Account uses a placeholder value for now, will update it to sub account address if sub account exists or after created + // Update will happen either in create_job (sub account exists) or reply (after creation), so it's atomic + // And we guarantee we do not read this value before it's updated + account: info.sender.clone(), + last_update_time: Uint64::from(env.block.time.seconds()), + name: data.name, + status: JobStatus::Pending, + condition: data.condition.clone(), + terminate_condition: data.terminate_condition, + recurring: data.recurring, + requeue_on_evict: data.requeue_on_evict, + vars: data.vars, + msgs: data.msgs, + reward: data.reward, + description: data.description, + labels: data.labels, + assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), + }, + )?; + match main_account { None => { - let create_main_account_submsg = SubMsg { - id: REPLY_ID_CREATE_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB, - msg: CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some(env.contract.address.to_string()), - code_id: config.warp_account_code_id.u64(), - msg: to_binary(&account::InstantiateMsg { - owner: info.sender.to_string(), - funds: data.funds, - msgs: data.account_msgs, - is_sub_account: false, - main_account_addr: None, - })?, - funds: info.funds, - label: info.sender.to_string(), - }), + // Create main account then create sub account then create job in reply + submsgs.push(SubMsg { + id: REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB, + msg: build_instantiate_warp_account_msg( + false, + job.id, + env.contract.address.to_string(), + config.warp_account_code_id.u64(), + info.sender.to_string(), + None, + native_funds_minus_reward_and_fee, + data.cw_funds, + data.account_msgs, + ), gas_limit: None, reply_on: ReplyOn::Always, - }; - - Ok(Response::new() - .add_submessage(create_main_account_submsg) - .add_attribute( - "action", - "create_job_and_new_main_account_and_new_sub_account", - )) + }); + + attrs.push(Attribute::new( + "action", + "create_main_account_and_sub_account_and_job", + )); } Some(main_account) => { if main_account.owner != info.sender { @@ -110,158 +174,109 @@ pub fn create_job( )?; match available_sub_account.sub_account { None => { - let create_sub_account_submsg = SubMsg { + // Create sub account then create job in reply + submsgs.push(SubMsg { id: REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, - msg: CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some(env.contract.address.to_string()), - code_id: config.warp_account_code_id.u64(), - msg: to_binary(&account::InstantiateMsg { - owner: info.sender.to_string(), - funds: data.funds, - msgs: data.account_msgs, - is_sub_account: true, - main_account_addr: Some(main_account_addr.clone().to_string()), - })?, - funds: info.funds, - label: info.sender.to_string(), - }), + msg: build_instantiate_warp_account_msg( + true, + job.id, + env.contract.address.to_string(), + config.warp_account_code_id.u64(), + info.sender.to_string(), + Some(main_account_addr.clone().to_string()), + native_funds_minus_reward_and_fee, + data.cw_funds, + data.account_msgs, + ), gas_limit: None, reply_on: ReplyOn::Always, - }; + }); - Ok(Response::new() - .add_submessage(create_sub_account_submsg) - .add_attribute("action", "create_job_and_new_sub_account")) + attrs.push(Attribute::new("action", "create_sub_account_and_job")); } Some(sub_account) => { let sub_account_addr = sub_account.account_addr; - let job = JobQueue::add( - &mut deps, - Job { - id: state.current_job_id, - prev_id: None, - owner: info.sender.clone(), - account: sub_account_addr.clone(), - last_update_time: Uint64::from(env.block.time.seconds()), - name: data.name, - status: JobStatus::Pending, - condition: data.condition.clone(), - terminate_condition: data.terminate_condition, - recurring: data.recurring, - requeue_on_evict: data.requeue_on_evict, - vars: data.vars, - msgs: data.msgs, - reward: data.reward, - description: data.description, - labels: data.labels, - assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), - }, - )?; - - // Assume reward.amount == warp token allowance - let fee = data.reward * Uint128::from(config.creation_fee_percentage) - / Uint128::new(100); - - let cw_funds_vec = match data.funds { - None => { - vec![] - } - Some(funds) => funds, - }; - - let mut fund_account_msgs: Vec = vec![]; - - if !info.funds.is_empty() { - fund_account_msgs.push(CosmosMsg::Bank(BankMsg::Send { - to_address: sub_account_addr.clone().to_string(), - amount: info.funds.clone(), - })) + // Update job.account from placeholder value to sub account + job.account = sub_account_addr.clone(); + JobQueue::sync(&mut deps, env, job.clone())?; + + if !native_funds_minus_reward_and_fee.is_empty() { + // Fund account in native coins + msgs.push(build_transfer_native_funds_msg( + sub_account_addr.clone().to_string(), + native_funds_minus_reward_and_fee, + )) } - for cw_fund in &cw_funds_vec { - fund_account_msgs.push(CosmosMsg::Wasm(match cw_fund { - Fund::Cw20(cw20_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw20_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { - owner: info.sender.clone().to_string(), - recipient: sub_account_addr.clone().to_string(), - amount: cw20_fund.amount, - }))?, - funds: vec![], - }, - Fund::Cw721(cw721_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw721_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { - recipient: sub_account_addr.clone().to_string(), - token_id: cw721_fund.token_id.clone(), - }))?, - funds: vec![], - }, - })) + if let Some(cw_funds) = data.cw_funds { + // Fund account in CW20 / CW721 tokens + for cw_fund in cw_funds { + msgs.push(match cw_fund { + CwFund::Cw20(cw20_fund) => build_transfer_cw20_msg( + deps.api + .addr_validate(&cw20_fund.contract_addr)? + .to_string(), + info.sender.clone().to_string(), + sub_account_addr.clone().to_string(), + cw20_fund.amount, + ), + CwFund::Cw721(cw721_fund) => build_transfer_cw721_msg( + deps.api + .addr_validate(&cw721_fund.contract_addr)? + .to_string(), + sub_account_addr.clone().to_string(), + cw721_fund.token_id.clone(), + ), + }) + } } - let reward_send_msgs = vec![ - // Job sends reward to controller - WasmMsg::Execute { - contract_addr: sub_account_addr.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: env.contract.address.to_string(), - amount: vec![Coin::new( - (data.reward).u128(), - config.fee_denom.clone(), - )], - })], - }))?, - funds: vec![], - }, - // Job owner sends fee to fee collector - WasmMsg::Execute { - contract_addr: sub_account_addr.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.fee_collector.to_string(), - amount: vec![Coin::new((fee).u128(), config.fee_denom)], - })], - }))?, - funds: vec![], - }, - ]; - - let mut account_msgs: Vec = vec![]; - - if let Some(msgs) = data.account_msgs { - account_msgs = vec![WasmMsg::Execute { - contract_addr: sub_account_addr.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs }))?, - funds: vec![], - }]; + if let Some(account_msgs) = data.account_msgs { + // Account execute msgs + msgs.push(build_account_execute_generic_msgs( + sub_account_addr.to_string(), + account_msgs, + )); } - Ok(Response::new() - .add_messages(fund_account_msgs) - .add_messages(reward_send_msgs) - .add_messages(account_msgs) - .add_attribute("action", "create_job") - .add_attribute("job_id", job.id) - .add_attribute("job_owner", job.owner) - .add_attribute("job_name", job.name) - .add_attribute("job_status", serde_json_wasm::to_string(&job.status)?) - .add_attribute("job_condition", serde_json_wasm::to_string(&job.condition)?) - .add_attribute("job_msgs", serde_json_wasm::to_string(&job.msgs)?) - .add_attribute("job_reward", job.reward) - .add_attribute("job_creation_fee", fee) - .add_attribute("job_last_updated_time", job.last_update_time)) + // Occupy sub account + msgs.push(build_occupy_sub_account_msg( + main_account_addr.to_string(), + sub_account_addr.to_string(), + job.id, + )); + + attrs.push(Attribute::new("action", "create_job")); + attrs.push(Attribute::new("job_id", job.id)); + attrs.push(Attribute::new("job_owner", job.owner)); + attrs.push(Attribute::new("job_name", job.name)); + attrs.push(Attribute::new( + "job_status", + serde_json_wasm::to_string(&job.status)?, + )); + attrs.push(Attribute::new( + "job_condition", + serde_json_wasm::to_string(&job.condition)?, + )); + attrs.push(Attribute::new( + "job_msgs", + serde_json_wasm::to_string(&job.msgs)?, + )); + attrs.push(Attribute::new("job_reward", job.reward)); + attrs.push(Attribute::new("job_creation_fee", fee)); + attrs.push(Attribute::new( + "job_last_updated_time", + job.last_update_time, + )); } } } } + + Ok(Response::new() + .add_submessages(submsgs) + .add_messages(msgs) + .add_attributes(attrs)) } pub fn delete_job( @@ -269,9 +284,12 @@ pub fn delete_job( env: Env, info: MessageInfo, data: DeleteJobMsg, + config: Config, + fee_denom_paid_amount: Uint128, ) -> Result { - let config = CONFIG.load(deps.storage)?; let job = JobQueue::get(&deps, data.id.into())?; + let main_account_addr = ACCOUNTS().load(deps.storage, job.owner.clone())?.account; + let job_account_addr = job.account.clone(); if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); @@ -284,25 +302,44 @@ pub fn delete_job( let _new_job = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Cancelled)?; let fee = job.reward * Uint128::from(config.cancellation_fee_percentage) / Uint128::new(100); + if fee > fee_denom_paid_amount { + return Err(ContractError::InsufficientFundsToPayForFee {}); + } - let cw20_send_msgs = vec![ - // Job owner sends reward minus fee back to account - BankMsg::Send { - to_address: job.account.to_string(), - amount: vec![Coin::new( - (job.reward - fee).u128(), - config.fee_denom.clone(), - )], - }, - // Job owner sends fee to fee collector - BankMsg::Send { - to_address: config.fee_collector.to_string(), - amount: vec![Coin::new(fee.u128(), config.fee_denom)], - }, - ]; + let mut msgs = vec![]; + + // Controller sends reward minus cancellation fee back to job owner + msgs.push(build_transfer_native_funds_msg( + job.owner.to_string(), + vec![Coin::new( + (job.reward - fee).u128(), + config.fee_denom.clone(), + )], + )); + + // Job owner sends fee to controller when it calls delete_job + // Controller sends cancellation fee to fee collector + msgs.push(build_transfer_native_funds_msg( + config.fee_collector.to_string(), + vec![Coin::new(fee.u128(), config.fee_denom)], + )); + + if is_sub_account(&main_account_addr, &job_account_addr) { + // Free sub account + msgs.push(build_free_sub_account_msg( + main_account_addr.to_string(), + job_account_addr.to_string(), + )); + } + + // Job owner withdraw all assets that are listed from warp account to itself + msgs.push(build_account_withdraw_assets_msg( + job_account_addr.clone().to_string(), + job.assets_to_withdraw, + )); Ok(Response::new() - .add_messages(cw20_send_msgs) + .add_messages(msgs) .add_attribute("action", "delete_job") .add_attribute("job_id", job.id) .add_attribute("job_status", serde_json_wasm::to_string(&job.status)?) @@ -314,9 +351,10 @@ pub fn update_job( env: Env, info: MessageInfo, data: UpdateJobMsg, + config: Config, + fee_denom_paid_amount: Uint128, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; - let config = CONFIG.load(deps.storage)?; if info.sender != job.owner { return Err(ContractError::Unauthorized {}); @@ -339,25 +377,19 @@ pub fn update_job( if !added_reward.is_zero() && fee.is_zero() { return Err(ContractError::RewardTooSmall {}); } + if fee + added_reward > fee_denom_paid_amount { + return Err(ContractError::InsufficientFundsToPayForRewardAndFee {}); + } - let mut cw20_send_msgs = vec![]; + let mut msgs = vec![]; - if added_reward.u128() > 0 { - cw20_send_msgs.push( - // Job owner sends additional reward to controller - WasmMsg::Execute { - contract_addr: job.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: env.contract.address.to_string(), - amount: vec![Coin::new((added_reward).u128(), config.fee_denom.clone())], - })], - }))?, - funds: vec![], - }, - ); - cw20_send_msgs.push( - // Job owner sends fee to fee collector + if added_reward > Uint128::zero() { + // Job owner sends reward to controller when it calls create_job + // Reward stays at controller, no need to send it elsewhere + + msgs.push( + // Job owner sends fee to controller when it calls update_job + // Controller sends update fee to fee collector WasmMsg::Execute { contract_addr: job.account.to_string(), msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { @@ -372,7 +404,7 @@ pub fn update_job( } Ok(Response::new() - .add_messages(cw20_send_msgs) + .add_messages(msgs) .add_attribute("action", "update_job") .add_attribute("job_id", job.id) .add_attribute("job_owner", job.owner) @@ -390,10 +422,11 @@ pub fn execute_job( env: Env, info: MessageInfo, data: ExecuteJobMsg, + config: Config, ) -> Result { - let _config = CONFIG.load(deps.storage)?; - let config = CONFIG.load(deps.storage)?; let job = JobQueue::get(&deps, data.id.into())?; + let main_account_addr = ACCOUNTS().load(deps.storage, job.owner.clone())?.account; + let job_account_addr = job.account.clone(); if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); @@ -418,15 +451,28 @@ pub fn execute_job( ); let mut attrs = vec![]; + let mut msgs = vec![]; let mut submsgs = vec![]; if let Err(e) = resolution { attrs.push(Attribute::new("job_condition_status", "invalid")); attrs.push(Attribute::new("error", e.to_string())); JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Failed)?; + + if is_sub_account(&main_account_addr, &job_account_addr) { + // Free sub account + msgs.push(build_free_sub_account_msg( + main_account_addr.to_string(), + job_account_addr.to_string(), + )); + } } else { attrs.push(Attribute::new("job_condition_status", "valid")); if !resolution? { + // TODO: do we want to return OK? + // this means if a keeper accidentally executes a job whose condition is unmet + // It still cost keeper TX fee + // Shouldn't we return error so keeper will fail during simulation? return Ok(Response::new() .add_attribute("action", "execute_job") .add_attribute("condition", "false") @@ -454,14 +500,14 @@ pub fn execute_job( } // Controller sends reward to executor - let reward_msg = BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin::new(job.reward.u128(), config.fee_denom)], - }; + msgs.push(build_transfer_native_funds_msg( + info.sender.to_string(), + vec![Coin::new(job.reward.u128(), config.fee_denom)], + )); Ok(Response::new() .add_submessages(submsgs) - .add_message(reward_msg) + .add_messages(msgs) .add_attribute("action", "execute_job") .add_attribute("executor", info.sender) .add_attribute("job_id", job.id) @@ -474,15 +520,17 @@ pub fn evict_job( env: Env, info: MessageInfo, data: EvictJobMsg, + config: Config, ) -> Result { - let config = CONFIG.load(deps.storage)?; let state = STATE.load(deps.storage)?; let job = JobQueue::get(&deps, data.id.into())?; + let main_account_addr = ACCOUNTS().load(deps.storage, job.owner.clone())?.account; + let job_account_addr = job.account.clone(); let account_amount = deps .querier .query::(&QueryRequest::Bank(BankQuery::Balance { - address: job.account.to_string(), + address: job_account_addr.clone().to_string(), denom: config.fee_denom.clone(), }))? .amount @@ -505,48 +553,54 @@ pub fn evict_job( }; if env.block.time.seconds() - job.last_update_time.u64() < t.u64() { - return Err(EvictionPeriodNotElapsed {}); + return Err(ContractError::EvictionPeriodNotElapsed {}); } - let mut cosmos_msgs = vec![]; + let mut msgs = vec![]; let job_status; if job.requeue_on_evict && account_amount >= a { - cosmos_msgs.push( - // Controller sends reward to evictor - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: job.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin::new(a.u128(), config.fee_denom)], - })], - }))?, - funds: vec![], - }), + // Job will stay active cause it has enough funds to pay for eviction fee and it's set to requeue on eviction + msgs.push( + // Job owner's warp account sends reward to evictor + build_account_execute_generic_msgs( + job_account_addr.to_string(), + vec![build_transfer_native_funds_msg( + info.sender.to_string(), + vec![Coin::new(a.u128(), config.fee_denom)], + )], + ), ); job_status = JobQueue::sync(&mut deps, env, job.clone())?.status; } else { + // Job will be evicted job_status = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; - cosmos_msgs.append(&mut vec![ - // Controller sends reward to evictor - CosmosMsg::Bank(BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![Coin::new(a.u128(), config.fee_denom.clone())], - }), - // Controller sends reward minus fee back to account - CosmosMsg::Bank(BankMsg::Send { - to_address: job.account.to_string(), - amount: vec![Coin::new((job.reward - a).u128(), config.fee_denom)], - }), - ]); + // Controller sends eviction reward to evictor + msgs.push(build_transfer_native_funds_msg( + info.sender.to_string(), + vec![Coin::new(a.u128(), config.fee_denom.clone())], + )); + + // Controller sends execution reward minus eviction reward back to account + msgs.push(build_transfer_native_funds_msg( + info.sender.to_string(), + vec![Coin::new((job.reward - a).u128(), config.fee_denom.clone())], + )); + + if is_sub_account(&main_account_addr, &job_account_addr) { + // Free sub account + msgs.push(build_free_sub_account_msg( + main_account_addr.to_string(), + job_account_addr.to_string(), + )); + } } Ok(Response::new() + .add_messages(msgs) .add_attribute("action", "evict_job") .add_attribute("job_id", job.id) - .add_attribute("job_status", serde_json_wasm::to_string(&job_status)?) - .add_messages(cosmos_msgs)) + .add_attribute("job_status", serde_json_wasm::to_string(&job_status)?)) } diff --git a/contracts/warp-controller/src/execute/mod.rs b/contracts/warp-controller/src/execute/mod.rs index ee75d71c..b10bee4a 100644 --- a/contracts/warp-controller/src/execute/mod.rs +++ b/contracts/warp-controller/src/execute/mod.rs @@ -1,3 +1,2 @@ -pub(crate) mod account; pub(crate) mod controller; pub(crate) mod job; diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index cd3c7813..56cb8f5f 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -1,23 +1,28 @@ -use account::{FreeSubAccountMsg, GenericMsg, OccupySubAccountMsg, WithdrawAssetsMsg}; -use controller::{ - account::{Account, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}, - job::{Job, JobStatus}, -}; use cosmwasm_std::{ - to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Coin, CosmosMsg, DepsMut, Env, - QueryRequest, Reply, Response, StdError, StdResult, SubMsgResult, Uint128, Uint64, WasmMsg, + Coin, CosmosMsg, DepsMut, Env, Reply, ReplyOn, Response, StdError, SubMsg, Uint64, +}; + +use controller::{ + account::{Account, CwFund}, + Config, }; use crate::{ - error::map_contract_error, - state::{JobQueue, ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS, STATE}, + contract::REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, + state::{JobQueue, ACCOUNTS}, + util::msg::{ + build_account_execute_generic_msgs, build_instantiate_warp_account_msg, + build_occupy_sub_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, + build_transfer_native_funds_msg, + }, ContractError, }; pub fn create_main_account_and_sub_account_and_job( - mut deps: DepsMut, + deps: DepsMut, env: Env, msg: Reply, + config: Config, ) -> Result { let reply = msg.result.into_result().map_err(StdError::generic_err)?; @@ -32,6 +37,15 @@ pub fn create_main_account_and_sub_account_and_job( }) .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + let job_id_str = event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "job_id") + .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? + .value; + let job_id = u64::from_str_radix(job_id_str.as_str(), 10)?; + let owner = event .attributes .iter() @@ -40,7 +54,7 @@ pub fn create_main_account_and_sub_account_and_job( .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? .value; - let address = event + let main_account_addr = event .attributes .iter() .cloned() @@ -48,17 +62,17 @@ pub fn create_main_account_and_sub_account_and_job( .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? .value; - let funds: Vec = serde_json_wasm::from_str( + let native_funds: Vec = serde_json_wasm::from_str( &event .attributes .iter() .cloned() - .find(|attr| attr.key == "funds") + .find(|attr| attr.key == "native_funds") .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? .value, )?; - let cw_funds: Option> = serde_json_wasm::from_str( + let cw_funds: Option> = serde_json_wasm::from_str( &event .attributes .iter() @@ -78,49 +92,6 @@ pub fn create_main_account_and_sub_account_and_job( .value, )?; - let cw_funds_vec = match cw_funds { - None => { - vec![] - } - Some(funds) => funds, - }; - - let mut msgs_vec: Vec = vec![]; - - for cw_fund in &cw_funds_vec { - msgs_vec.push(CosmosMsg::Wasm(match cw_fund { - Fund::Cw20(cw20_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw20_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { - owner: owner.clone(), - recipient: address.clone(), - amount: cw20_fund.amount, - }))?, - funds: vec![], - }, - Fund::Cw721(cw721_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw721_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { - recipient: address.clone(), - token_id: cw721_fund.token_id.clone(), - }))?, - funds: vec![], - }, - })) - } - - if let Some(msgs) = account_msgs { - for msg in msgs { - msgs_vec.push(msg); - } - } - if ACCOUNTS().has(deps.storage, deps.api.addr_validate(&owner)?) { return Err(ContractError::AccountAlreadyExists {}); } @@ -130,17 +101,43 @@ pub fn create_main_account_and_sub_account_and_job( deps.api.addr_validate(&owner)?, &Account { owner: deps.api.addr_validate(&owner.clone())?, - account: deps.api.addr_validate(&address)?, + account: deps.api.addr_validate(&main_account_addr)?, }, )?; + + // Create new sub account then create job in reply + let create_sub_account_and_job_submsg = SubMsg { + id: REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, + msg: build_instantiate_warp_account_msg( + true, + Uint64::from(job_id), + env.contract.address.to_string(), + config.warp_account_code_id.u64(), + owner.clone(), + Some(main_account_addr.clone().to_string()), + native_funds.clone(), + cw_funds.clone(), + account_msgs, + ), + gas_limit: None, + reply_on: ReplyOn::Always, + }; + Ok(Response::new() - .add_attribute("action", "create_sub_account_and_job_reply") - .add_attribute("job_id", value) + .add_submessage(create_sub_account_and_job_submsg) + // .add_messages(msgs) + .add_attribute( + "action", + "create_main_account_and_sub_account_and_job_reply", + ) + // .add_attribute("job_id", value) .add_attribute("owner", owner) - .add_attribute("account_address", address) - .add_attribute("funds", serde_json_wasm::to_string(&funds)?) - .add_attribute("cw_funds", serde_json_wasm::to_string(&cw_funds_vec)?) - .add_messages(msgs_vec)) + .add_attribute("account_address", main_account_addr) + .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?) + .add_attribute( + "cw_funds", + serde_json_wasm::to_string(&cw_funds.unwrap_or(vec![]))?, + )) } pub fn create_sub_account_and_job( @@ -161,33 +158,54 @@ pub fn create_sub_account_and_job( }) .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; - let owner = event + let job_id_str = event .attributes .iter() .cloned() - .find(|attr| attr.key == "owner") - .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? + .find(|attr| attr.key == "job_id") + .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? .value; + let job_id = u64::from_str_radix(job_id_str.as_str(), 10)?; - let address = event + let owner = event .attributes .iter() .cloned() - .find(|attr| attr.key == "contract_addr") - .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? + .find(|attr| attr.key == "owner") + .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? .value; - let funds: Vec = serde_json_wasm::from_str( + let main_account_addr = deps.api.addr_validate( &event .attributes .iter() .cloned() - .find(|attr| attr.key == "funds") + .find(|attr| attr.key == "main_account_addr") + .ok_or_else(|| StdError::generic_err("cannot find `main_account_addr` attribute"))? + .value, + )?; + + let sub_account_addr = deps.api.addr_validate( + &event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "contract_addr") + .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? + .value, + )?; + + let native_funds: Vec = serde_json_wasm::from_str( + &event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "native_funds") .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? .value, )?; - let cw_funds: Option> = serde_json_wasm::from_str( + let cw_funds: Option> = serde_json_wasm::from_str( &event .attributes .iter() @@ -207,67 +225,67 @@ pub fn create_sub_account_and_job( .value, )?; - let cw_funds_vec = match cw_funds { - None => { - vec![] - } - Some(funds) => funds, - }; + let mut job = JobQueue::get(&deps, job_id.into())?; + job.account = sub_account_addr.clone(); + JobQueue::sync(&mut deps, env, job.clone())?; - let mut msgs_vec: Vec = vec![]; + let mut msgs: Vec = vec![]; - for cw_fund in &cw_funds_vec { - msgs_vec.push(CosmosMsg::Wasm(match cw_fund { - Fund::Cw20(cw20_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw20_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { - owner: owner.clone(), - recipient: address.clone(), - amount: cw20_fund.amount, - }))?, - funds: vec![], - }, - Fund::Cw721(cw721_fund) => WasmMsg::Execute { - contract_addr: deps - .api - .addr_validate(&cw721_fund.contract_addr)? - .to_string(), - msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { - recipient: address.clone(), - token_id: cw721_fund.token_id.clone(), - }))?, - funds: vec![], - }, - })) + if !native_funds.is_empty() { + // Fund account in native coins + msgs.push(build_transfer_native_funds_msg( + sub_account_addr.clone().to_string(), + native_funds.clone(), + )) } - if let Some(msgs) = account_msgs { - for msg in msgs { - msgs_vec.push(msg); + if let Some(cw_funds) = cw_funds.clone() { + // Fund account in CW20 / CW721 tokens + for cw_fund in cw_funds { + msgs.push(match cw_fund { + CwFund::Cw20(cw20_fund) => build_transfer_cw20_msg( + deps.api + .addr_validate(&cw20_fund.contract_addr)? + .to_string(), + owner.clone(), + sub_account_addr.clone().to_string(), + cw20_fund.amount, + ), + CwFund::Cw721(cw721_fund) => build_transfer_cw721_msg( + deps.api + .addr_validate(&cw721_fund.contract_addr)? + .to_string(), + sub_account_addr.clone().to_string(), + cw721_fund.token_id.clone(), + ), + }) } } - if ACCOUNTS().has(deps.storage, deps.api.addr_validate(&owner)?) { - return Err(ContractError::AccountAlreadyExists {}); + if let Some(account_msgs) = account_msgs { + // Account execute msgs + msgs.push(build_account_execute_generic_msgs( + sub_account_addr.to_string(), + account_msgs, + )); } - ACCOUNTS().save( - deps.storage, - deps.api.addr_validate(&owner)?, - &Account { - owner: deps.api.addr_validate(&owner.clone())?, - account: deps.api.addr_validate(&address)?, - }, - )?; + // Occupy sub account + msgs.push(build_occupy_sub_account_msg( + main_account_addr.to_string(), + sub_account_addr.to_string(), + job.id, + )); + Ok(Response::new() + .add_messages(msgs) .add_attribute("action", "create_sub_account_and_job_reply") - .add_attribute("job_id", value) + // .add_attribute("job_id", value) .add_attribute("owner", owner) - .add_attribute("account_address", address) - .add_attribute("funds", serde_json_wasm::to_string(&funds)?) - .add_attribute("cw_funds", serde_json_wasm::to_string(&cw_funds_vec)?) - .add_messages(msgs_vec)) + .add_attribute("account_address", sub_account_addr) + .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?) + .add_attribute( + "cw_funds", + serde_json_wasm::to_string(&cw_funds.unwrap_or(vec![]))?, + )) } diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index ffe0c5a7..acda9db4 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -1,17 +1,31 @@ -use account::{FreeSubAccountMsg, GenericMsg, OccupySubAccountMsg, WithdrawAssetsMsg}; -use controller::job::{Job, JobStatus}; use cosmwasm_std::{ - to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Coin, CosmosMsg, DepsMut, Env, - QueryRequest, Reply, Response, StdError, StdResult, SubMsgResult, Uint128, Uint64, WasmMsg, + Attribute, BalanceResponse, BankQuery, Coin, DepsMut, Env, QueryRequest, Reply, Response, + StdResult, SubMsgResult, Uint128, Uint64, }; use crate::{ error::map_contract_error, - state::{JobQueue, ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS, STATE}, + state::{JobQueue, ACCOUNTS, STATE}, + util::{ + msg::{ + build_account_execute_generic_msgs, build_account_withdraw_assets_msg, + build_free_sub_account_msg, build_transfer_native_funds_msg, + }, + sub_account::is_sub_account, + }, ContractError, }; +use controller::{ + job::{Job, JobStatus}, + Config, +}; -pub fn execute_job(mut deps: DepsMut, env: Env, msg: Reply) -> Result { +pub fn execute_job( + mut deps: DepsMut, + env: Env, + msg: Reply, + config: Config, +) -> Result { let state = STATE.load(deps.storage)?; let new_status = match msg.result { @@ -32,25 +46,30 @@ pub fn execute_job(mut deps: DepsMut, env: Env, msg: Reply) -> Result(&QueryRequest::Bank(BankQuery::Balance { - address: finished_job.account.to_string(), + address: job_account_addr.to_string(), denom: config.fee_denom.clone(), }))? .amount .amount; + let mut recurring_job_created = false; + if finished_job.recurring { - if account_amount < fee + finished_job.reward { + if job_account_amount < fee_plus_reward { new_job_attrs.push(Attribute::new("action", "recur_job")); - new_job_attrs.push(Attribute::new("creation_status", "failed_insufficient_fee")) + new_job_attrs.push(Attribute::new("creation_status", "failed_insufficient_fee")); } else if !(finished_job.status == JobStatus::Executed || finished_job.status == JobStatus::Failed) { @@ -65,7 +84,7 @@ pub fn execute_job(mut deps: DepsMut, env: Env, msg: Reply) -> Result Result Result Result, + fee_denom: String, + reward_plus_fee: Uint128, +) -> Vec { + let mut funds = funds; + let mut deducted_amount = reward_plus_fee; + for fund in funds.iter_mut() { + if fund.denom == fee_denom { + fund.amount = fund.amount.checked_sub(deducted_amount).unwrap(); + deducted_amount = Uint128::zero(); + } + } + funds +} diff --git a/contracts/warp-controller/src/util/mod.rs b/contracts/warp-controller/src/util/mod.rs index ff67a763..fd14ca67 100644 --- a/contracts/warp-controller/src/util/mod.rs +++ b/contracts/warp-controller/src/util/mod.rs @@ -1 +1,5 @@ +pub(crate) mod attribute; +pub(crate) mod fee; pub(crate) mod filter; +pub(crate) mod msg; +pub(crate) mod sub_account; diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs new file mode 100644 index 00000000..0a62f818 --- /dev/null +++ b/contracts/warp-controller/src/util/msg.rs @@ -0,0 +1,170 @@ +use cosmwasm_std::{to_binary, BankMsg, Coin, CosmosMsg, Uint128, Uint64, WasmMsg}; + +use account::{FreeSubAccountMsg, GenericMsg, OccupySubAccountMsg, WithdrawAssetsMsg}; +use controller::account::{AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; + +pub fn build_instantiate_warp_account_msg( + is_sub_account: bool, + job_id: Uint64, + admin_addr: String, + code_id: u64, + account_owner: String, + main_account_addr: Option, + native_funds: Vec, + cw_funds: Option>, + msgs: Option>, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(admin_addr), + code_id, + msg: to_binary(&account::InstantiateMsg { + owner: account_owner.clone(), + job_id, + is_sub_account, + main_account_addr: main_account_addr.clone(), + native_funds: native_funds.clone(), + cw_funds: cw_funds.unwrap_or(vec![]), + msgs: msgs.unwrap_or(vec![]), + }) + .unwrap(), + // Only send native funds to sub account + funds: if is_sub_account { native_funds } else { vec![] }, + label: format!( + "warp {} account, {}owner: {}", + if is_sub_account { "sub" } else { "main" }, + if is_sub_account { + format!( + "main account: {}, ", + main_account_addr.clone().clone().unwrap() + ) + } else { + "".to_string() + }, + account_owner, + ), + }) +} + +pub fn build_free_sub_account_msg( + main_account_addr: String, + sub_account_addr: String, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: main_account_addr, + msg: to_binary(&account::ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { + sub_account_addr, + })) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_occupy_sub_account_msg( + main_account_addr: String, + sub_account_addr: String, + job_id: Uint64, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: main_account_addr, + msg: to_binary(&account::ExecuteMsg::OccupySubAccount( + OccupySubAccountMsg { + sub_account_addr, + job_id, + }, + )) + .unwrap(), + funds: vec![], + }) +} + +// TODO: add cw20 increase allowance, is increase alliance transitive? +// If not we have a problem, because we need to increase allowance for warp account, however warp account may not be created yet, so user can only increase allowance for warp controller +// TODO: test do we need this? maybe user allow controller then controller can send it to sub account without increasing allowance +pub fn build_increase_cw20_allowance_msg( + cw20_token_contract_addr: String, + spender_addr: String, + amount: Uint128, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw20_token_contract_addr, + msg: to_binary(&cw20::Cw20ExecuteMsg::IncreaseAllowance { + spender: spender_addr, + amount, + expires: None, + }) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_transfer_cw20_msg( + cw20_token_contract_addr: String, + owner_addr: String, + recipient_addr: String, + amount: Uint128, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw20_token_contract_addr, + msg: to_binary(&FundTransferMsgs::TransferFrom(TransferFromMsg { + owner: owner_addr, + recipient: recipient_addr, + amount, + })) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_transfer_cw721_msg( + cw721_token_contract_addr: String, + recipient_addr: String, + token_id: String, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: cw721_token_contract_addr, + msg: to_binary(&FundTransferMsgs::TransferNft(TransferNftMsg { + recipient: recipient_addr, + token_id, + })) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_transfer_native_funds_msg( + recipient_addr: String, + native_funds: Vec, +) -> CosmosMsg { + CosmosMsg::Bank(BankMsg::Send { + to_address: recipient_addr, + amount: native_funds, + }) +} + +pub fn build_account_execute_generic_msgs( + account_addr: String, + cosmos_msgs_for_account_to_execute: Vec, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: account_addr, + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msgs: cosmos_msgs_for_account_to_execute, + })) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_account_withdraw_assets_msg( + account_addr: String, + assets_to_withdraw: Vec, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: account_addr, + msg: to_binary(&account::ExecuteMsg::WithdrawAssets(WithdrawAssetsMsg { + asset_infos: assets_to_withdraw, + })) + .unwrap(), + funds: vec![], + }) +} diff --git a/contracts/warp-controller/src/util/sub_account.rs b/contracts/warp-controller/src/util/sub_account.rs new file mode 100644 index 00000000..3e9a7070 --- /dev/null +++ b/contracts/warp-controller/src/util/sub_account.rs @@ -0,0 +1,5 @@ +use cosmwasm_std::Addr; + +pub fn is_sub_account(main_account_addr: &Addr, job_account_addr: &Addr) -> bool { + main_account_addr != job_account_addr +} diff --git a/contracts/warp-resolver/Cargo.toml b/contracts/warp-resolver/Cargo.toml index 2e2456c6..1009beb8 100644 --- a/contracts/warp-resolver/Cargo.toml +++ b/contracts/warp-resolver/Cargo.toml @@ -39,7 +39,8 @@ cw-storage-plus = "0.16" cw2 = "0.16" cw20 = "0.16" cw721 = "0.16.0" -resolver = { path = "../../packages/resolver", default-features = false, version = "*" } +cw-utils = "0.16" +resolver = { path = "../../packages/resolver", default-features = false, version = "*" } controller = { path = "../../packages/controller", default-features = false, version = "*" } schemars = "0.8" thiserror = "1" diff --git a/contracts/warp-resolver/src/contract.rs b/contracts/warp-resolver/src/contract.rs index cae93682..a61b723a 100644 --- a/contracts/warp-resolver/src/contract.rs +++ b/contracts/warp-resolver/src/contract.rs @@ -9,6 +9,7 @@ use cosmwasm_std::{ StdResult, }; +use cw_utils::nonpayable; use resolver::condition::Condition; use resolver::variable::{QueryExpr, Variable}; use resolver::{ @@ -36,6 +37,7 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { + nonpayable(&info).unwrap(); match msg { ExecuteMsg::ExecuteSimulateQuery(msg) => execute_simulate_query(deps, env, info, msg), ExecuteMsg::ExecuteValidateJobCreation(data) => { diff --git a/contracts/warp-templates/Cargo.toml b/contracts/warp-templates/Cargo.toml index 68cf75e5..1999eb81 100644 --- a/contracts/warp-templates/Cargo.toml +++ b/contracts/warp-templates/Cargo.toml @@ -39,7 +39,7 @@ cw-storage-plus = "0.16" cw2 = "0.16" cw20 = "0.16" cw721 = "0.16.0" -templates = { path = "../../packages/templates", default-features = false, version = "*" } +templates = { path = "../../packages/templates", default-features = false, version = "*" } resolver = { path = "../../packages/resolver", default-features = false, version = "*" } schemars = "0.8" thiserror = "1" diff --git a/contracts/warp-templates/src/contract.rs b/contracts/warp-templates/src/contract.rs index 298d4776..24f98420 100644 --- a/contracts/warp-templates/src/contract.rs +++ b/contracts/warp-templates/src/contract.rs @@ -59,6 +59,7 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { + match msg { ExecuteMsg::SubmitTemplate(data) => submit_template(deps, env, info, data), ExecuteMsg::EditTemplate(data) => edit_template(deps, env, info, data), diff --git a/packages/account/src/lib.rs b/packages/account/src/lib.rs index 5c4e6030..808dde1b 100644 --- a/packages/account/src/lib.rs +++ b/packages/account/src/lib.rs @@ -1,6 +1,6 @@ -use controller::account::{AssetInfo, Fund}; +use controller::account::{AssetInfo, CwFund}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, CosmosMsg, Uint64}; +use cosmwasm_std::{Addr, Coin as NativeCoin, CosmosMsg, Uint64}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -24,13 +24,22 @@ pub struct Config { #[cw_serde] pub struct InstantiateMsg { + // User who owns this account pub owner: String, + // ID of the job that is created along with the account + pub job_id: Uint64, + // Whether this account is a sub account, account can be a main account or a sub account pub is_sub_account: bool, - pub msgs: Option>, - pub funds: Option>, // Only supplied when is_sub_account is true // Skipped if it's instantiating a main account pub main_account_addr: Option, + // Only required when we are instantiate a main account + // Since we always want to fund sub account, so we will pass this value around and send it to sub account during instantiation in create main account's reply + pub native_funds: Vec, + // CW20 or CW721 funds, will be transferred to account in reply of account instantiation + pub cw_funds: Vec, + // List of cosmos msgs to execute after instantiating the account + pub msgs: Vec, } #[cw_serde] diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index 88bcf8c8..2776612d 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -2,7 +2,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint128}; #[cw_serde] -pub enum Fund { +pub enum CwFund { Cw20(Cw20Fund), Cw721(Cw721Fund), } diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index a7766314..8c182876 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -1,4 +1,4 @@ -use crate::account::{AssetInfo, Fund}; +use crate::account::{AssetInfo, CwFund}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, CosmosMsg, Uint128, Uint64}; use schemars::JsonSchema; @@ -74,7 +74,7 @@ pub struct CreateJobMsg { pub reward: Uint128, pub assets_to_withdraw: Option>, pub account_msgs: Option>, - pub funds: Option>, + pub cw_funds: Option>, } #[cw_serde] From 317e3fbd0d30c8ff49af0fcae221df087e6beb1a Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 23 Oct 2023 00:12:17 -0700 Subject: [PATCH 041/133] rename account to main account, fix free sub account --- .../examples/warp-account-schema.rs | 6 +- .../examples/warp-controller-schema.rs | 6 +- contracts/warp-controller/src/contract.rs | 8 ++- contracts/warp-controller/src/execute/job.rs | 32 +++++---- .../warp-controller/src/query/account.rs | 31 +++++--- .../warp-controller/src/reply/account.rs | 17 +++-- contracts/warp-controller/src/reply/job.rs | 24 ++++--- contracts/warp-controller/src/state.rs | 22 +++--- .../warp-controller/src/util/attribute.rs | 0 contracts/warp-controller/src/util/mod.rs | 1 - contracts/warp-controller/src/util/msg.rs | 72 +++++++++---------- contracts/warp-templates/src/contract.rs | 1 - packages/controller/src/account.rs | 14 ++-- packages/controller/src/lib.rs | 14 ++-- 14 files changed, 133 insertions(+), 115 deletions(-) delete mode 100644 contracts/warp-controller/src/util/attribute.rs diff --git a/contracts/warp-account/examples/warp-account-schema.rs b/contracts/warp-account/examples/warp-account-schema.rs index b35a3eae..8bd29135 100644 --- a/contracts/warp-account/examples/warp-account-schema.rs +++ b/contracts/warp-account/examples/warp-account-schema.rs @@ -3,7 +3,7 @@ use std::fs::create_dir_all; use account::{Config, ExecuteMsg, InstantiateMsg}; use controller::{ - account::{AccountResponse, AccountsResponse}, + account::{MainAccountResponse, MainAccountsResponse}, job::{JobResponse, JobsResponse}, }; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; @@ -19,6 +19,6 @@ fn main() { export_schema(&schema_for!(Config), &out_dir); export_schema(&schema_for!(JobResponse), &out_dir); export_schema(&schema_for!(JobsResponse), &out_dir); - export_schema(&schema_for!(AccountResponse), &out_dir); - export_schema(&schema_for!(AccountsResponse), &out_dir); + export_schema(&schema_for!(MainAccountResponse), &out_dir); + export_schema(&schema_for!(MainAccountsResponse), &out_dir); } diff --git a/contracts/warp-controller/examples/warp-controller-schema.rs b/contracts/warp-controller/examples/warp-controller-schema.rs index 6ab9debf..e17cacf8 100644 --- a/contracts/warp-controller/examples/warp-controller-schema.rs +++ b/contracts/warp-controller/examples/warp-controller-schema.rs @@ -2,7 +2,7 @@ use std::env::current_dir; use std::fs::create_dir_all; use controller::{ - account::{AccountResponse, AccountsResponse}, + account::{MainAccountResponse, MainAccountsResponse}, job::{JobResponse, JobsResponse}, QueryMsg, State, StateResponse, {Config, ConfigResponse, ExecuteMsg, InstantiateMsg}, }; @@ -23,6 +23,6 @@ fn main() { export_schema(&schema_for!(StateResponse), &out_dir); export_schema(&schema_for!(JobResponse), &out_dir); export_schema(&schema_for!(JobsResponse), &out_dir); - export_schema(&schema_for!(AccountResponse), &out_dir); - export_schema(&schema_for!(AccountsResponse), &out_dir); + export_schema(&schema_for!(MainAccountResponse), &out_dir); + export_schema(&schema_for!(MainAccountsResponse), &out_dir); } diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 0a9e21b5..18b45c11 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -136,9 +136,11 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryJob(data) => to_binary(&query::job::query_job(deps, env, data)?), QueryMsg::QueryJobs(data) => to_binary(&query::job::query_jobs(deps, env, data)?), - QueryMsg::QueryAccount(data) => to_binary(&query::account::query_account(deps, env, data)?), - QueryMsg::QueryAccounts(data) => { - to_binary(&query::account::query_accounts(deps, env, data)?) + QueryMsg::QueryMainAccount(data) => { + to_binary(&query::account::query_main_account(deps, env, data)?) + } + QueryMsg::QueryMainAccounts(data) => { + to_binary(&query::account::query_main_accounts(deps, env, data)?) } QueryMsg::QueryConfig(data) => { diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 3ecdd813..cecf7174 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -8,13 +8,15 @@ use crate::contract::{ REPLY_ID_EXECUTE_JOB, }; use crate::state::{JobQueue, ACCOUNTS, STATE}; +use crate::util::msg::{ + build_instantiate_warp_main_account_msg, build_instantiate_warp_sub_account_msg, +}; use crate::util::{ fee::deduct_reward_and_fee_from_native_funds, msg::{ build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_free_sub_account_msg, build_instantiate_warp_account_msg, - build_occupy_sub_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, - build_transfer_native_funds_msg, + build_free_sub_account_msg, build_occupy_sub_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, build_transfer_native_funds_msg, }, sub_account::is_sub_account, }; @@ -100,12 +102,7 @@ pub fn create_job( // create_job is called by account contract Some(record) => Some(record.1), // create_job is called by user - None => match ACCOUNTS().may_load(deps.storage, info.sender.clone())? { - // User has main account - Some(account) => Some(account), - // User does not have main account - None => None, - }, + None => ACCOUNTS().may_load(deps.storage, info.sender.clone())?, }; let state = STATE.load(deps.storage)?; @@ -140,13 +137,11 @@ pub fn create_job( // Create main account then create sub account then create job in reply submsgs.push(SubMsg { id: REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_account_msg( - false, + msg: build_instantiate_warp_main_account_msg( job.id, env.contract.address.to_string(), config.warp_account_code_id.u64(), info.sender.to_string(), - None, native_funds_minus_reward_and_fee, data.cw_funds, data.account_msgs, @@ -177,13 +172,12 @@ pub fn create_job( // Create sub account then create job in reply submsgs.push(SubMsg { id: REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_account_msg( - true, + msg: build_instantiate_warp_sub_account_msg( job.id, env.contract.address.to_string(), config.warp_account_code_id.u64(), info.sender.to_string(), - Some(main_account_addr.clone().to_string()), + main_account_addr.clone().to_string(), native_funds_minus_reward_and_fee, data.cw_funds, data.account_msgs, @@ -505,6 +499,14 @@ pub fn execute_job( vec![Coin::new(job.reward.u128(), config.fee_denom)], )); + if is_sub_account(&main_account_addr, &job_account_addr) { + // Free sub account + msgs.push(build_free_sub_account_msg( + main_account_addr.to_string(), + job_account_addr.to_string(), + )); + } + Ok(Response::new() .add_submessages(submsgs) .add_messages(msgs) diff --git a/contracts/warp-controller/src/query/account.rs b/contracts/warp-controller/src/query/account.rs index 2e953dcf..0730a5ce 100644 --- a/contracts/warp-controller/src/query/account.rs +++ b/contracts/warp-controller/src/query/account.rs @@ -1,19 +1,28 @@ -use crate::state::{ACCOUNTS, QUERY_PAGE_SIZE}; -use controller::account::{AccountResponse, AccountsResponse, QueryAccountMsg, QueryAccountsMsg}; use cosmwasm_std::{Deps, Env, Order, StdResult}; use cw_storage_plus::Bound; -pub fn query_account(deps: Deps, _env: Env, data: QueryAccountMsg) -> StdResult { - Ok(AccountResponse { - account: ACCOUNTS().load(deps.storage, deps.api.addr_validate(data.owner.as_str())?)?, +use crate::state::{ACCOUNTS, QUERY_PAGE_SIZE}; + +use controller::account::{ + MainAccountResponse, MainAccountsResponse, QueryMainAccountMsg, QueryMainAccountsMsg, +}; + +pub fn query_main_account( + deps: Deps, + _env: Env, + data: QueryMainAccountMsg, +) -> StdResult { + Ok(MainAccountResponse { + main_account: ACCOUNTS() + .load(deps.storage, deps.api.addr_validate(data.owner.as_str())?)?, }) } -pub fn query_accounts( +pub fn query_main_accounts( deps: Deps, _env: Env, - data: QueryAccountsMsg, -) -> StdResult { + data: QueryMainAccountsMsg, +) -> StdResult { let start_after = match data.start_after { None => None, Some(s) => Some(deps.api.addr_validate(s.as_str())?), @@ -23,9 +32,9 @@ pub fn query_accounts( .range(deps.storage, start_after, None, Order::Ascending) .take(data.limit.unwrap_or(QUERY_PAGE_SIZE) as usize) .collect::>>()?; - let mut accounts = vec![]; + let mut main_accounts = vec![]; for tuple in infos { - accounts.push(tuple.1) + main_accounts.push(tuple.1) } - Ok(AccountsResponse { accounts }) + Ok(MainAccountsResponse { main_accounts }) } diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 56cb8f5f..4bbc6de0 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{ }; use controller::{ - account::{Account, CwFund}, + account::{CwFund, MainAccount}, Config, }; @@ -11,7 +11,7 @@ use crate::{ contract::REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, state::{JobQueue, ACCOUNTS}, util::msg::{ - build_account_execute_generic_msgs, build_instantiate_warp_account_msg, + build_account_execute_generic_msgs, build_instantiate_warp_sub_account_msg, build_occupy_sub_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, }, @@ -44,7 +44,7 @@ pub fn create_main_account_and_sub_account_and_job( .find(|attr| attr.key == "job_id") .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? .value; - let job_id = u64::from_str_radix(job_id_str.as_str(), 10)?; + let job_id = job_id_str.as_str().parse::()?; let owner = event .attributes @@ -99,7 +99,7 @@ pub fn create_main_account_and_sub_account_and_job( ACCOUNTS().save( deps.storage, deps.api.addr_validate(&owner)?, - &Account { + &MainAccount { owner: deps.api.addr_validate(&owner.clone())?, account: deps.api.addr_validate(&main_account_addr)?, }, @@ -108,13 +108,12 @@ pub fn create_main_account_and_sub_account_and_job( // Create new sub account then create job in reply let create_sub_account_and_job_submsg = SubMsg { id: REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_account_msg( - true, + msg: build_instantiate_warp_sub_account_msg( Uint64::from(job_id), env.contract.address.to_string(), config.warp_account_code_id.u64(), owner.clone(), - Some(main_account_addr.clone().to_string()), + main_account_addr.clone().to_string(), native_funds.clone(), cw_funds.clone(), account_msgs, @@ -165,7 +164,7 @@ pub fn create_sub_account_and_job( .find(|attr| attr.key == "job_id") .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? .value; - let job_id = u64::from_str_radix(job_id_str.as_str(), 10)?; + let job_id = job_id_str.as_str().parse::()?; let owner = event .attributes @@ -225,7 +224,7 @@ pub fn create_sub_account_and_job( .value, )?; - let mut job = JobQueue::get(&deps, job_id.into())?; + let mut job = JobQueue::get(&deps, job_id)?; job.account = sub_account_addr.clone(); JobQueue::sync(&mut deps, env, job.clone())?; diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index acda9db4..a60490e5 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -9,7 +9,7 @@ use crate::{ util::{ msg::{ build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_free_sub_account_msg, build_transfer_native_funds_msg, + build_occupy_sub_account_msg, build_transfer_native_funds_msg, }, sub_account::is_sub_account, }, @@ -45,6 +45,7 @@ pub fn execute_job( let mut msgs = vec![]; let mut new_job_attrs = vec![]; + let new_job_id = state.current_job_id; let fee = finished_job.reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); @@ -138,7 +139,7 @@ pub fn execute_job( let new_job = JobQueue::add( &mut deps, Job { - id: state.current_job_id, + id: new_job_id, prev_id: Some(finished_job.id), owner: finished_job.owner.clone(), account: finished_job.account.clone(), @@ -201,19 +202,22 @@ pub fn execute_job( } } - if !recurring_job_created { + if recurring_job_created { + if is_sub_account(&main_account_addr, &job_account_addr) { + // Occupy sub account with the new job + msgs.push(build_occupy_sub_account_msg( + main_account_addr.to_string(), + job_account_addr.to_string(), + new_job_id, + )); + } + } else { + // No new job created, sub account has been free in execute_job, no need to free here again // Job owner withdraw all assets that are listed from warp account to itself msgs.push(build_account_withdraw_assets_msg( job_account_addr.clone().to_string(), finished_job.assets_to_withdraw, )); - if is_sub_account(&main_account_addr, &job_account_addr) { - // Free sub account - msgs.push(build_free_sub_account_msg( - main_account_addr.to_string(), - job_account_addr.clone().to_string(), - )); - } } Ok(Response::new() diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 2fd89b20..474a3a5f 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -1,9 +1,11 @@ -use controller::account::Account; use cosmwasm_std::{Addr, DepsMut, Env, Uint128, Uint64}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; -use controller::job::{Job, JobStatus, UpdateJobMsg}; -use controller::{Config, State}; +use controller::{ + account::MainAccount, + job::{Job, JobStatus, UpdateJobMsg}, + Config, State, +}; use crate::ContractError; @@ -51,20 +53,20 @@ pub fn FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, Job, JobIndexes<'a>> { IndexedMap::new("finished_jobs_v3", indexes) } -pub struct AccountIndexes<'a> { - pub account: UniqueIndex<'a, Addr, Account>, +pub struct MainAccountIndexes<'a> { + pub account: UniqueIndex<'a, Addr, MainAccount>, } -impl IndexList for AccountIndexes<'_> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.account]; +impl IndexList for MainAccountIndexes<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.account]; Box::new(v.into_iter()) } } #[allow(non_snake_case)] -pub fn ACCOUNTS<'a>() -> IndexedMap<'a, Addr, Account, AccountIndexes<'a>> { - let indexes = AccountIndexes { +pub fn ACCOUNTS<'a>() -> IndexedMap<'a, Addr, MainAccount, MainAccountIndexes<'a>> { + let indexes = MainAccountIndexes { account: UniqueIndex::new(|account| account.account.clone(), "accounts__account"), }; IndexedMap::new("accounts", indexes) diff --git a/contracts/warp-controller/src/util/attribute.rs b/contracts/warp-controller/src/util/attribute.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/contracts/warp-controller/src/util/mod.rs b/contracts/warp-controller/src/util/mod.rs index fd14ca67..f7208bba 100644 --- a/contracts/warp-controller/src/util/mod.rs +++ b/contracts/warp-controller/src/util/mod.rs @@ -1,4 +1,3 @@ -pub(crate) mod attribute; pub(crate) mod fee; pub(crate) mod filter; pub(crate) mod msg; diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 0a62f818..38a4b019 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -3,13 +3,11 @@ use cosmwasm_std::{to_binary, BankMsg, Coin, CosmosMsg, Uint128, Uint64, WasmMsg use account::{FreeSubAccountMsg, GenericMsg, OccupySubAccountMsg, WithdrawAssetsMsg}; use controller::account::{AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; -pub fn build_instantiate_warp_account_msg( - is_sub_account: bool, +pub fn build_instantiate_warp_main_account_msg( job_id: Uint64, admin_addr: String, code_id: u64, account_owner: String, - main_account_addr: Option, native_funds: Vec, cw_funds: Option>, msgs: Option>, @@ -20,27 +18,47 @@ pub fn build_instantiate_warp_account_msg( msg: to_binary(&account::InstantiateMsg { owner: account_owner.clone(), job_id, - is_sub_account, - main_account_addr: main_account_addr.clone(), + is_sub_account: false, + main_account_addr: None, native_funds: native_funds.clone(), cw_funds: cw_funds.unwrap_or(vec![]), msgs: msgs.unwrap_or(vec![]), }) .unwrap(), // Only send native funds to sub account - funds: if is_sub_account { native_funds } else { vec![] }, + funds: vec![], + label: format!("warp main account, owner: {}", account_owner), + }) +} + +#[allow(clippy::too_many_arguments)] +pub fn build_instantiate_warp_sub_account_msg( + job_id: Uint64, + admin_addr: String, + code_id: u64, + account_owner: String, + main_account_addr: String, + native_funds: Vec, + cw_funds: Option>, + msgs: Option>, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(admin_addr), + code_id, + msg: to_binary(&account::InstantiateMsg { + owner: account_owner.clone(), + job_id, + is_sub_account: true, + main_account_addr: Some(main_account_addr.clone()), + native_funds: native_funds.clone(), + cw_funds: cw_funds.unwrap_or(vec![]), + msgs: msgs.unwrap_or(vec![]), + }) + .unwrap(), + funds: native_funds, label: format!( - "warp {} account, {}owner: {}", - if is_sub_account { "sub" } else { "main" }, - if is_sub_account { - format!( - "main account: {}, ", - main_account_addr.clone().clone().unwrap() - ) - } else { - "".to_string() - }, - account_owner, + "warp sub account, main account: {}, owner: {}", + main_account_addr, account_owner, ), }) } @@ -77,26 +95,6 @@ pub fn build_occupy_sub_account_msg( }) } -// TODO: add cw20 increase allowance, is increase alliance transitive? -// If not we have a problem, because we need to increase allowance for warp account, however warp account may not be created yet, so user can only increase allowance for warp controller -// TODO: test do we need this? maybe user allow controller then controller can send it to sub account without increasing allowance -pub fn build_increase_cw20_allowance_msg( - cw20_token_contract_addr: String, - spender_addr: String, - amount: Uint128, -) -> CosmosMsg { - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: cw20_token_contract_addr, - msg: to_binary(&cw20::Cw20ExecuteMsg::IncreaseAllowance { - spender: spender_addr, - amount, - expires: None, - }) - .unwrap(), - funds: vec![], - }) -} - pub fn build_transfer_cw20_msg( cw20_token_contract_addr: String, owner_addr: String, diff --git a/contracts/warp-templates/src/contract.rs b/contracts/warp-templates/src/contract.rs index 24f98420..298d4776 100644 --- a/contracts/warp-templates/src/contract.rs +++ b/contracts/warp-templates/src/contract.rs @@ -59,7 +59,6 @@ pub fn execute( info: MessageInfo, msg: ExecuteMsg, ) -> Result { - match msg { ExecuteMsg::SubmitTemplate(data) => submit_template(deps, env, info, data), ExecuteMsg::EditTemplate(data) => edit_template(deps, env, info, data), diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index 2776612d..f9931350 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -44,30 +44,30 @@ pub enum Cw721ExecuteMsg { } #[cw_serde] -pub struct QueryAccountMsg { +pub struct QueryMainAccountMsg { pub owner: String, } #[cw_serde] -pub struct QueryAccountsMsg { +pub struct QueryMainAccountsMsg { pub start_after: Option, pub limit: Option, } #[cw_serde] -pub struct Account { +pub struct MainAccount { pub owner: Addr, pub account: Addr, } #[cw_serde] -pub struct AccountResponse { - pub account: Account, +pub struct MainAccountResponse { + pub main_account: MainAccount, } #[cw_serde] -pub struct AccountsResponse { - pub accounts: Vec, +pub struct MainAccountsResponse { + pub main_accounts: Vec, } #[cw_serde] diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 3cb87caf..68b0d20c 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -1,4 +1,6 @@ -use crate::account::{AccountResponse, AccountsResponse, QueryAccountMsg, QueryAccountsMsg}; +use crate::account::{ + MainAccountResponse, MainAccountsResponse, QueryMainAccountMsg, QueryMainAccountsMsg, +}; use crate::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, JobResponse, JobsResponse, QueryJobMsg, QueryJobsMsg, UpdateJobMsg, @@ -109,10 +111,12 @@ pub enum QueryMsg { #[returns(JobsResponse)] QueryJobs(QueryJobsMsg), - #[returns(AccountResponse)] - QueryAccount(QueryAccountMsg), - #[returns(AccountsResponse)] - QueryAccounts(QueryAccountsMsg), + // For sub account, please query it via the main account contract + // You can look at account contract for more details + #[returns(MainAccountResponse)] + QueryMainAccount(QueryMainAccountMsg), + #[returns(MainAccountsResponse)] + QueryMainAccounts(QueryMainAccountsMsg), #[returns(ConfigResponse)] QueryConfig(QueryConfigMsg), From b5ceecb5d22ef584b030d0989eb29b10e62a5784 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 23 Oct 2023 14:52:54 -0700 Subject: [PATCH 042/133] use job account tracker instead of main account --- Cargo.lock | 146 +++--- contracts/warp-account/src/execute/account.rs | 46 -- .../warp-account/src/integration_tests.rs | 441 ------------------ contracts/warp-account/src/query/account.rs | 111 ----- contracts/warp-account/src/state.rs | 15 - contracts/warp-controller/Cargo.toml | 4 +- .../examples/warp-controller-schema.rs | 6 +- contracts/warp-controller/src/contract.rs | 76 ++- contracts/warp-controller/src/error.rs | 3 + .../warp-controller/src/execute/controller.rs | 337 +------------ contracts/warp-controller/src/execute/job.rs | 175 ++++--- contracts/warp-controller/src/lib.rs | 1 + contracts/warp-controller/src/migrate/job.rs | 303 ++++++++++++ .../src/migrate/job_account.rs | 69 +++ .../src/migrate/job_account_tracker.rs | 45 ++ .../src/migrate/legacy_account.rs | 40 ++ contracts/warp-controller/src/migrate/mod.rs | 4 + .../warp-controller/src/query/account.rs | 28 +- .../warp-controller/src/reply/account.rs | 54 +-- contracts/warp-controller/src/reply/job.rs | 21 +- contracts/warp-controller/src/state.rs | 28 +- .../src/util/legacy_account.rs | 8 + contracts/warp-controller/src/util/mod.rs | 2 +- contracts/warp-controller/src/util/msg.rs | 71 ++- .../warp-controller/src/util/sub_account.rs | 5 - .../.cargo/config | 0 .../.gitignore | 0 contracts/warp-job-account-tracker/Cargo.toml | 52 +++ .../README.md | 0 .../warp-job-account-tracker-schema.rs | 17 + .../meta/README.md | 0 .../meta/appveyor.yml | 0 .../meta/test_generate.sh | 0 .../warp-job-account-tracker/src/contract.rs | 74 +++ .../warp-job-account-tracker/src/error.rs | 73 +++ .../src/execute/account.rs | 30 ++ .../src/execute}/mod.rs | 0 .../src/integration_tests.rs | 349 ++++++++++++++ .../src/lib.rs | 2 - .../src/query/account.rs | 88 ++++ .../warp-job-account-tracker/src/query/mod.rs | 1 + .../warp-job-account-tracker/src/state.rs | 13 + contracts/warp-job-account/.cargo/config | 4 + contracts/warp-job-account/.gitignore | 16 + .../Cargo.toml | 4 +- .../warp-job-account}/README.md | 0 .../examples/warp-job-account-schema.rs} | 11 +- contracts/warp-job-account/meta/README.md | 16 + contracts/warp-job-account/meta/appveyor.yml | 61 +++ .../warp-job-account/meta/test_generate.sh | 37 ++ .../src/contract.rs | 49 +- .../src/error.rs | 0 .../src/execute/ibc.rs | 2 +- .../src/execute/mod.rs | 1 - .../src/execute/withdraw.rs | 7 +- contracts/warp-job-account/src/lib.rs | 10 + .../warp-job-account/src/query/account.rs | 8 + contracts/warp-job-account/src/query/mod.rs | 1 + contracts/warp-job-account/src/state.rs | 5 + .../src/tests.rs | 11 +- contracts/warp-legacy-account/.cargo/config | 4 + contracts/warp-legacy-account/.gitignore | 16 + contracts/warp-legacy-account/Cargo.toml | 53 +++ contracts/warp-legacy-account/README.md | 106 +++++ .../examples/warp-legacy-account-schema.rs | 17 + contracts/warp-legacy-account/meta/README.md | 16 + .../warp-legacy-account/meta/appveyor.yml | 61 +++ .../warp-legacy-account/meta/test_generate.sh | 37 ++ contracts/warp-legacy-account/src/contract.rs | 76 +++ contracts/warp-legacy-account/src/error.rs | 73 +++ .../warp-legacy-account/src/execute/ibc.rs | 33 ++ .../warp-legacy-account/src/execute/mod.rs | 2 + .../src/execute/withdraw.rs | 134 ++++++ contracts/warp-legacy-account/src/lib.rs | 10 + .../warp-legacy-account/src/query/account.rs | 8 + .../warp-legacy-account/src/query/mod.rs | 1 + contracts/warp-legacy-account/src/state.rs | 4 + contracts/warp-legacy-account/src/tests.rs | 340 ++++++++++++++ packages/controller/Cargo.toml | 6 - packages/controller/src/account.rs | 14 +- packages/controller/src/lib.rs | 42 +- packages/job-account-tracker/Cargo.toml | 16 + packages/job-account-tracker/README.md | 106 +++++ packages/job-account-tracker/src/lib.rs | 89 ++++ .../{account => job-account}/.cargo/config | 0 packages/{account => job-account}/Cargo.toml | 14 +- packages/job-account/README.md | 106 +++++ .../examples/account-schema.rs | 0 packages/{account => job-account}/src/lib.rs | 81 +--- packages/legacy-account/.cargo/config | 4 + packages/legacy-account/Cargo.toml | 21 + packages/legacy-account/README.md | 106 +++++ .../legacy-account/examples/account-schema.rs | 17 + packages/legacy-account/src/lib.rs | 129 +++++ packages/resolver/Cargo.toml | 13 +- packages/templates/Cargo.toml | 15 +- 96 files changed, 3320 insertions(+), 1431 deletions(-) delete mode 100644 contracts/warp-account/src/execute/account.rs delete mode 100644 contracts/warp-account/src/integration_tests.rs delete mode 100644 contracts/warp-account/src/query/account.rs delete mode 100644 contracts/warp-account/src/state.rs create mode 100644 contracts/warp-controller/src/migrate/job.rs create mode 100644 contracts/warp-controller/src/migrate/job_account.rs create mode 100644 contracts/warp-controller/src/migrate/job_account_tracker.rs create mode 100644 contracts/warp-controller/src/migrate/legacy_account.rs create mode 100644 contracts/warp-controller/src/migrate/mod.rs create mode 100644 contracts/warp-controller/src/util/legacy_account.rs delete mode 100644 contracts/warp-controller/src/util/sub_account.rs rename contracts/{warp-account => warp-job-account-tracker}/.cargo/config (100%) rename contracts/{warp-account => warp-job-account-tracker}/.gitignore (100%) create mode 100644 contracts/warp-job-account-tracker/Cargo.toml rename contracts/{warp-account => warp-job-account-tracker}/README.md (100%) create mode 100644 contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs rename contracts/{warp-account => warp-job-account-tracker}/meta/README.md (100%) rename contracts/{warp-account => warp-job-account-tracker}/meta/appveyor.yml (100%) rename contracts/{warp-account => warp-job-account-tracker}/meta/test_generate.sh (100%) create mode 100644 contracts/warp-job-account-tracker/src/contract.rs create mode 100644 contracts/warp-job-account-tracker/src/error.rs create mode 100644 contracts/warp-job-account-tracker/src/execute/account.rs rename contracts/{warp-account/src/query => warp-job-account-tracker/src/execute}/mod.rs (100%) create mode 100644 contracts/warp-job-account-tracker/src/integration_tests.rs rename contracts/{warp-account => warp-job-account-tracker}/src/lib.rs (85%) create mode 100644 contracts/warp-job-account-tracker/src/query/account.rs create mode 100644 contracts/warp-job-account-tracker/src/query/mod.rs create mode 100644 contracts/warp-job-account-tracker/src/state.rs create mode 100644 contracts/warp-job-account/.cargo/config create mode 100644 contracts/warp-job-account/.gitignore rename contracts/{warp-account => warp-job-account}/Cargo.toml (92%) rename {packages/account => contracts/warp-job-account}/README.md (100%) rename contracts/{warp-account/examples/warp-account-schema.rs => warp-job-account/examples/warp-job-account-schema.rs} (52%) create mode 100644 contracts/warp-job-account/meta/README.md create mode 100644 contracts/warp-job-account/meta/appveyor.yml create mode 100644 contracts/warp-job-account/meta/test_generate.sh rename contracts/{warp-account => warp-job-account}/src/contract.rs (57%) rename contracts/{warp-account => warp-job-account}/src/error.rs (100%) rename contracts/{warp-account => warp-job-account}/src/execute/ibc.rs (95%) rename contracts/{warp-account => warp-job-account}/src/execute/mod.rs (65%) rename contracts/{warp-account => warp-job-account}/src/execute/withdraw.rs (98%) create mode 100644 contracts/warp-job-account/src/lib.rs create mode 100644 contracts/warp-job-account/src/query/account.rs create mode 100644 contracts/warp-job-account/src/query/mod.rs create mode 100644 contracts/warp-job-account/src/state.rs rename contracts/{warp-account => warp-job-account}/src/tests.rs (97%) create mode 100644 contracts/warp-legacy-account/.cargo/config create mode 100644 contracts/warp-legacy-account/.gitignore create mode 100644 contracts/warp-legacy-account/Cargo.toml create mode 100644 contracts/warp-legacy-account/README.md create mode 100644 contracts/warp-legacy-account/examples/warp-legacy-account-schema.rs create mode 100644 contracts/warp-legacy-account/meta/README.md create mode 100644 contracts/warp-legacy-account/meta/appveyor.yml create mode 100644 contracts/warp-legacy-account/meta/test_generate.sh create mode 100644 contracts/warp-legacy-account/src/contract.rs create mode 100644 contracts/warp-legacy-account/src/error.rs create mode 100644 contracts/warp-legacy-account/src/execute/ibc.rs create mode 100644 contracts/warp-legacy-account/src/execute/mod.rs create mode 100644 contracts/warp-legacy-account/src/execute/withdraw.rs create mode 100644 contracts/warp-legacy-account/src/lib.rs create mode 100644 contracts/warp-legacy-account/src/query/account.rs create mode 100644 contracts/warp-legacy-account/src/query/mod.rs create mode 100644 contracts/warp-legacy-account/src/state.rs create mode 100644 contracts/warp-legacy-account/src/tests.rs create mode 100644 packages/job-account-tracker/Cargo.toml create mode 100644 packages/job-account-tracker/README.md create mode 100644 packages/job-account-tracker/src/lib.rs rename packages/{account => job-account}/.cargo/config (100%) rename packages/{account => job-account}/Cargo.toml (63%) create mode 100644 packages/job-account/README.md rename packages/{account => job-account}/examples/account-schema.rs (100%) rename packages/{account => job-account}/src/lib.rs (61%) create mode 100644 packages/legacy-account/.cargo/config create mode 100644 packages/legacy-account/Cargo.toml create mode 100644 packages/legacy-account/README.md create mode 100644 packages/legacy-account/examples/account-schema.rs create mode 100644 packages/legacy-account/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1acfb858..c74b3c3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,28 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "account" -version = "0.1.0" -dependencies = [ - "controller", - "cosmwasm-schema", - "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", - "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", - "cw20", - "json-codec-wasm", - "prost 0.11.9", - "schemars", - "serde", - "strum", - "strum_macros", - "thiserror", -] - [[package]] name = "ahash" version = "0.7.6" @@ -107,17 +85,11 @@ version = "0.1.0" dependencies = [ "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", - "cw20", "schemars", "serde", "strum", "strum_macros", - "thiserror", ] [[package]] @@ -588,6 +560,28 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +[[package]] +name = "job-account" +version = "0.1.0" +dependencies = [ + "controller", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "prost 0.11.9", + "schemars", + "serde", +] + +[[package]] +name = "job-account-tracker" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", +] + [[package]] name = "json-codec-wasm" version = "0.1.0" @@ -606,6 +600,19 @@ dependencies = [ "sha2 0.10.6", ] +[[package]] +name = "legacy-account" +version = "0.1.0" +dependencies = [ + "controller", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "prost 0.11.9", + "schemars", + "serde", +] + [[package]] name = "libc" version = "0.2.138" @@ -720,18 +727,7 @@ dependencies = [ "controller", "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", - "cw20", - "json-codec-wasm", - "schemars", - "serde", - "strum", - "strum_macros", - "thiserror", ] [[package]] @@ -965,19 +961,8 @@ dependencies = [ "controller", "cosmwasm-schema", "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", - "cw20", - "json-codec-wasm", "resolver", - "schemars", - "serde", - "strum", - "strum_macros", - "thiserror", ] [[package]] @@ -1031,10 +1016,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] -name = "warp-account" +name = "warp-controller" +version = "0.1.0" +dependencies = [ + "base64", + "controller", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-asset", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", + "cw2 0.16.0", + "cw20", + "job-account", + "job-account-tracker", + "json-codec-wasm", + "legacy-account", + "resolver", + "schemars", + "serde-json-wasm 0.4.1", + "thiserror", +] + +[[package]] +name = "warp-job-account" version = "0.1.0" dependencies = [ - "account", "anyhow", "base64", "controller", @@ -1048,6 +1057,7 @@ dependencies = [ "cw2 0.16.0", "cw20", "cw721", + "job-account", "json-codec-wasm", "prost 0.11.9", "schemars", @@ -1056,10 +1066,34 @@ dependencies = [ ] [[package]] -name = "warp-controller" +name = "warp-job-account-tracker" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-asset", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw-utils 0.16.0", + "cw2 0.16.0", + "cw20", + "cw721", + "job-account-tracker", + "json-codec-wasm", + "prost 0.11.9", + "schemars", + "serde-json-wasm 0.4.1", + "thiserror", +] + +[[package]] +name = "warp-legacy-account" version = "0.1.0" dependencies = [ - "account", + "anyhow", "base64", "controller", "cosmwasm-schema", @@ -1071,8 +1105,10 @@ dependencies = [ "cw-utils 0.16.0", "cw2 0.16.0", "cw20", + "cw721", "json-codec-wasm", - "resolver", + "legacy-account", + "prost 0.11.9", "schemars", "serde-json-wasm 0.4.1", "thiserror", diff --git a/contracts/warp-account/src/execute/account.rs b/contracts/warp-account/src/execute/account.rs deleted file mode 100644 index 6fa15fd3..00000000 --- a/contracts/warp-account/src/execute/account.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::state::{FREE_SUB_ACCOUNTS, OCCUPIED_SUB_ACCOUNTS}; -use crate::ContractError; -use account::{FreeSubAccountMsg, OccupySubAccountMsg}; -use cosmwasm_std::{DepsMut, Env, Response}; - -pub fn occupy_sub_account( - deps: DepsMut, - env: Env, - data: OccupySubAccountMsg, -) -> Result { - let sub_account_addr_ref = &deps.api.addr_validate(data.sub_account_addr.as_str())?; - // We do not add main account to occupied sub accounts - if data.sub_account_addr == env.contract.address { - return Ok(Response::new()); - } - FREE_SUB_ACCOUNTS.remove(deps.storage, sub_account_addr_ref); - OCCUPIED_SUB_ACCOUNTS.update(deps.storage, sub_account_addr_ref, |s| match s { - None => Ok(data.job_id), - Some(_) => Err(ContractError::SubAccountAlreadyOccupiedError {}), - })?; - Ok(Response::new() - .add_attribute("action", "occupy_sub_account") - .add_attribute("sub_account_addr", data.sub_account_addr) - .add_attribute("job_id", data.job_id)) -} - -pub fn free_sub_account( - deps: DepsMut, - env: Env, - data: FreeSubAccountMsg, -) -> Result { - let sub_account_addr_ref = &deps.api.addr_validate(data.sub_account_addr.as_str())?; - // We do not add main account to free sub accounts - if data.sub_account_addr == env.contract.address { - return Ok(Response::new()); - } - OCCUPIED_SUB_ACCOUNTS.remove(deps.storage, sub_account_addr_ref); - FREE_SUB_ACCOUNTS.update(deps.storage, sub_account_addr_ref, |s| match s { - // value is a dummy data because there is no built in support for set in cosmwasm - None => Ok(true), - Some(_) => Err(ContractError::SubAccountAlreadyFreeError {}), - })?; - Ok(Response::new() - .add_attribute("action", "free_sub_account") - .add_attribute("sub_account_addr", data.sub_account_addr)) -} diff --git a/contracts/warp-account/src/integration_tests.rs b/contracts/warp-account/src/integration_tests.rs deleted file mode 100644 index c37800a0..00000000 --- a/contracts/warp-account/src/integration_tests.rs +++ /dev/null @@ -1,441 +0,0 @@ -#[cfg(test)] -mod tests { - use account::{ - Config, ConfigResponse, ExecuteMsg, FirstFreeSubAccountResponse, FreeSubAccountMsg, - FreeSubAccountsResponse, InstantiateMsg, OccupiedSubAccountsResponse, OccupySubAccountMsg, - QueryConfigMsg, QueryFirstFreeSubAccountMsg, QueryFreeSubAccountsMsg, QueryMsg, - QueryOccupiedSubAccountsMsg, SubAccountConfig, - }; - use anyhow::Result as AnyResult; - use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; - use cw_multi_test::{App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor}; - - use crate::{ - contract::{execute, instantiate, query}, - ContractError, - }; - - const DUMMY_WARP_CONTROLLER_ADDR: &str = "terra1"; - const USER_1: &str = "terra2"; - const DUMMY_JOB_ID: Uint64 = Uint64::zero(); - - fn mock_app() -> App { - AppBuilder::new().build(|router, _, storage| { - router - .bank - .init_balance( - storage, - &Addr::unchecked(USER_1), - vec![Coin { - denom: "uluna".to_string(), - // 1_000_000_000 uLuna i.e. 1k LUNA since 1 LUNA = 1_000_000 uLuna - amount: Uint128::new(1_000_000_000), - }], - ) - .unwrap(); - }) - } - - fn contract_warp_account() -> Box> { - let contract = ContractWrapper::new(execute, instantiate, query); - Box::new(contract) - } - - fn init_warp_account( - app: &mut App, - warp_account_contract_code_id: u64, - is_sub_account: bool, - main_account_addr: Option, - ) -> Addr { - app.instantiate_contract( - warp_account_contract_code_id, - Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - &InstantiateMsg { - owner: USER_1.to_string(), - job_id: DUMMY_JOB_ID, - is_sub_account, - main_account_addr, - native_funds: vec![], - cw_funds: vec![], - msgs: vec![], - }, - &[], - "warp_main_account", - None, - ) - .unwrap() - } - - fn assert_err(res: AnyResult, err: ContractError) { - match res { - Ok(_) => panic!("Result was not an error"), - Err(generic_err) => { - let contract_err: ContractError = generic_err.downcast().unwrap(); - assert_eq!(contract_err, err); - } - } - } - - #[test] - fn warp_account_contract_multi_test_sub_account_management() { - let mut app = mock_app(); - let warp_account_contract_code_id = app.store_code(contract_warp_account()); - - // Instantiate main account - let warp_main_account_contract_addr = - init_warp_account(&mut app, warp_account_contract_code_id, false, None); - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryConfig(QueryConfigMsg {}) - ), - Ok(ConfigResponse { - config: Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_main_account_contract_addr.clone(), - sub_account_config: None - } - }) - ); - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryFirstFreeSubAccount(QueryFirstFreeSubAccountMsg {}) - ), - Ok(FirstFreeSubAccountResponse { sub_account: None }) - ); - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryFreeSubAccounts(QueryFreeSubAccountsMsg { - start_after: None, - limit: None - }) - ), - Ok(FreeSubAccountsResponse { - sub_accounts: vec![], - total_count: 0 - }) - ); - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg { - start_after: None, - limit: None - }) - ), - Ok(OccupiedSubAccountsResponse { - sub_accounts: vec![], - total_count: 0 - }) - ); - - // Instantiate first sub account - let warp_sub_account_1_contract_addr = init_warp_account( - &mut app, - warp_account_contract_code_id, - true, - Some(warp_main_account_contract_addr.to_string()), - ); - assert_eq!( - app.wrap().query_wasm_smart( - warp_sub_account_1_contract_addr.clone(), - &QueryMsg::QueryConfig(QueryConfigMsg {}) - ), - Ok(ConfigResponse { - config: Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_1_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - } - }) - ); - // Mark first sub account as free - let _ = app.execute_contract( - Addr::unchecked(USER_1), - warp_main_account_contract_addr.clone(), - &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { - sub_account_addr: warp_sub_account_1_contract_addr.to_string(), - }), - &[], - ); - // Cannot free sub account twice - assert_err( - app.execute_contract( - Addr::unchecked(USER_1), - warp_main_account_contract_addr.clone(), - &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { - sub_account_addr: warp_sub_account_1_contract_addr.to_string(), - }), - &[], - ), - ContractError::SubAccountAlreadyFreeError {}, - ); - - // Instantiate second sub account - let warp_sub_account_2_contract_addr = init_warp_account( - &mut app, - warp_account_contract_code_id, - true, - Some(warp_main_account_contract_addr.to_string()), - ); - // Mark second sub account as free - let _ = app.execute_contract( - Addr::unchecked(USER_1), - warp_main_account_contract_addr.clone(), - &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { - sub_account_addr: warp_sub_account_2_contract_addr.to_string(), - }), - &[], - ); - - // Instantiate third sub account - let warp_sub_account_3_contract_addr = init_warp_account( - &mut app, - warp_account_contract_code_id, - true, - Some(warp_main_account_contract_addr.to_string()), - ); - // Mark third sub account as free - let _ = app.execute_contract( - Addr::unchecked(USER_1), - warp_main_account_contract_addr.clone(), - &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { - sub_account_addr: warp_sub_account_3_contract_addr.to_string(), - }), - &[], - ); - - // Query first free sub account - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryFirstFreeSubAccount(QueryFirstFreeSubAccountMsg {}) - ), - Ok(FirstFreeSubAccountResponse { - sub_account: Some(Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_1_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - }) - }) - ); - - // Query free sub accounts - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryFreeSubAccounts(QueryFreeSubAccountsMsg { - start_after: None, - limit: None - }) - ), - Ok(FreeSubAccountsResponse { - sub_accounts: vec![ - Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_3_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - }, - Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_2_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - }, - Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_1_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - } - ], - total_count: 3 - }) - ); - - // Query occupied sub accounts - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg { - start_after: None, - limit: None - }) - ), - Ok(OccupiedSubAccountsResponse { - sub_accounts: vec![], - total_count: 0 - }) - ); - - // Occupy second sub account - let _ = app.execute_contract( - Addr::unchecked(USER_1), - warp_main_account_contract_addr.clone(), - &ExecuteMsg::OccupySubAccount(OccupySubAccountMsg { - sub_account_addr: warp_sub_account_2_contract_addr.to_string(), - job_id: Uint64::from(1 as u8), - }), - &[], - ); - // Cannot occupy sub account twice - assert_err( - app.execute_contract( - Addr::unchecked(USER_1), - warp_main_account_contract_addr.clone(), - &ExecuteMsg::OccupySubAccount(OccupySubAccountMsg { - sub_account_addr: warp_sub_account_2_contract_addr.to_string(), - job_id: Uint64::from(1 as u8), - }), - &[], - ), - ContractError::SubAccountAlreadyOccupiedError {}, - ); - - // Query free sub accounts - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryFreeSubAccounts(QueryFreeSubAccountsMsg { - start_after: None, - limit: None - }) - ), - Ok(FreeSubAccountsResponse { - sub_accounts: vec![ - Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_3_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - }, - Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_1_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - } - ], - total_count: 2 - }) - ); - - // Query occupied sub accounts - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg { - start_after: None, - limit: None - }) - ), - Ok(OccupiedSubAccountsResponse { - sub_accounts: vec![Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_2_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: Some(Uint64::from(1 as u8)) - }) - }], - total_count: 1 - }) - ); - - // Free second sub account - let _ = app.execute_contract( - Addr::unchecked(USER_1), - warp_main_account_contract_addr.clone(), - &ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { - sub_account_addr: warp_sub_account_2_contract_addr.to_string(), - }), - &[], - ); - - // Query free sub accounts - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryFreeSubAccounts(QueryFreeSubAccountsMsg { - start_after: None, - limit: None - }) - ), - Ok(FreeSubAccountsResponse { - sub_accounts: vec![ - Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_3_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - }, - Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_2_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - }, - Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), - account_addr: warp_sub_account_1_contract_addr.clone(), - sub_account_config: Some(SubAccountConfig { - main_account_addr: warp_main_account_contract_addr.clone(), - occupied_by_job_id: None - }) - } - ], - total_count: 3 - }) - ); - - // Query occupied sub accounts - assert_eq!( - app.wrap().query_wasm_smart( - warp_main_account_contract_addr.clone(), - &QueryMsg::QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg { - start_after: None, - limit: None - }) - ), - Ok(OccupiedSubAccountsResponse { - sub_accounts: vec![], - total_count: 0 - }) - ); - } -} diff --git a/contracts/warp-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs deleted file mode 100644 index b86f0529..00000000 --- a/contracts/warp-account/src/query/account.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::state::{FREE_SUB_ACCOUNTS, OCCUPIED_SUB_ACCOUNTS}; -use account::{ - Config, ConfigResponse, FirstFreeSubAccountResponse, FreeSubAccountsResponse, - OccupiedSubAccountsResponse, QueryFreeSubAccountsMsg, QueryOccupiedSubAccountsMsg, - SubAccountConfig, -}; -use cosmwasm_std::{Deps, Order, StdResult}; -use cw_storage_plus::Bound; - -const QUERY_LIMIT: u32 = 50; - -pub fn query_config(config: Config) -> StdResult { - Ok(ConfigResponse { config }) -} - -pub fn query_first_free_sub_account( - deps: Deps, - config: Config, -) -> StdResult { - let sub_account = FREE_SUB_ACCOUNTS - .range(deps.storage, None, None, Order::Ascending) - .next(); - if sub_account.is_none() { - Ok(FirstFreeSubAccountResponse { sub_account: None }) - } else { - let (sub_account_addr, _) = sub_account.unwrap()?; - Ok(FirstFreeSubAccountResponse { - sub_account: Some(Config { - owner: config.owner, - creator_addr: config.creator_addr, - account_addr: sub_account_addr, - sub_account_config: Some(SubAccountConfig { - main_account_addr: config.account_addr, - occupied_by_job_id: None, - }), - }), - }) - } -} - -pub fn query_occupied_sub_accounts( - deps: Deps, - data: QueryOccupiedSubAccountsMsg, - config: Config, -) -> StdResult { - let iter = match data.start_after { - Some(start_after) => OCCUPIED_SUB_ACCOUNTS.range( - deps.storage, - Some(Bound::exclusive( - &deps.api.addr_validate(start_after.as_str()).unwrap(), - )), - None, - Order::Descending, - ), - None => OCCUPIED_SUB_ACCOUNTS.range(deps.storage, None, None, Order::Descending), - }; - let sub_accounts = iter - .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) - .map(|item| { - item.map(|(sub_account_addr, job_id)| Config { - owner: config.owner.clone(), - creator_addr: config.creator_addr.clone(), - account_addr: sub_account_addr, - sub_account_config: Some(SubAccountConfig { - main_account_addr: config.account_addr.clone(), - occupied_by_job_id: Some(job_id), - }), - }) - }) - .collect::>>()?; - Ok(OccupiedSubAccountsResponse { - total_count: sub_accounts.len(), - sub_accounts, - }) -} - -pub fn query_free_sub_accounts( - deps: Deps, - data: QueryFreeSubAccountsMsg, - config: Config, -) -> StdResult { - let iter = match data.start_after { - Some(start_after) => FREE_SUB_ACCOUNTS.range( - deps.storage, - Some(Bound::exclusive( - &deps.api.addr_validate(start_after.as_str()).unwrap(), - )), - None, - Order::Descending, - ), - None => FREE_SUB_ACCOUNTS.range(deps.storage, None, None, Order::Descending), - }; - let sub_accounts = iter - .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) - .map(|item| { - item.map(|(sub_account_addr, _)| Config { - owner: config.owner.clone(), - creator_addr: config.creator_addr.clone(), - account_addr: sub_account_addr, - sub_account_config: Some(SubAccountConfig { - main_account_addr: config.account_addr.clone(), - occupied_by_job_id: None, - }), - }) - }) - .collect::>>()?; - Ok(FreeSubAccountsResponse { - total_count: sub_accounts.len(), - sub_accounts, - }) -} diff --git a/contracts/warp-account/src/state.rs b/contracts/warp-account/src/state.rs deleted file mode 100644 index 09d379f3..00000000 --- a/contracts/warp-account/src/state.rs +++ /dev/null @@ -1,15 +0,0 @@ -use account::Config; -use cosmwasm_std::{Addr, Uint64}; -use cw_storage_plus::{Item, Map}; - -pub const CONFIG: Item = Item::new("config"); - -// OCCUPIED_SUB_ACCOUNTS only has value when current account is a main account -// It will be empty if current account is a sub account, because we do not supported nested sub accounts -// Key is the sub account address, value is the ID of the pending job currently using it -pub const OCCUPIED_SUB_ACCOUNTS: Map<&Addr, Uint64> = Map::new("occupied_sub_accounts"); - -// FREE_SUB_ACCOUNTS only has value when current account is a main account -// It will be empty if current account is a sub account, because we do not supported nested sub accounts -// Key is the sub account address, value is a dummy data that is always true to make it behave like a set -pub const FREE_SUB_ACCOUNTS: Map<&Addr, bool> = Map::new("free_sub_accounts"); diff --git a/contracts/warp-controller/Cargo.toml b/contracts/warp-controller/Cargo.toml index f9de292f..8e2000f8 100644 --- a/contracts/warp-controller/Cargo.toml +++ b/contracts/warp-controller/Cargo.toml @@ -39,7 +39,9 @@ cw-storage-plus = "0.16" cw-utils = "0.16" cw2 = "0.16" cw20 = "0.16" -account = { path = "../../packages/account", default-features = false, version = "*" } +legacy-account = { path = "../../packages/legacy-account", default-features = false, version = "*" } +job-account = { path = "../../packages/job-account", default-features = false, version = "*" } +job-account-tracker = { path = "../../packages/job-account-tracker", default-features = false, version = "*" } controller = { path = "../../packages/controller", default-features = false, version = "*" } resolver = { path = "../../packages/resolver", default-features = false, version = "*" } schemars = "0.8" diff --git a/contracts/warp-controller/examples/warp-controller-schema.rs b/contracts/warp-controller/examples/warp-controller-schema.rs index e17cacf8..63f58aa2 100644 --- a/contracts/warp-controller/examples/warp-controller-schema.rs +++ b/contracts/warp-controller/examples/warp-controller-schema.rs @@ -2,7 +2,7 @@ use std::env::current_dir; use std::fs::create_dir_all; use controller::{ - account::{MainAccountResponse, MainAccountsResponse}, + account::{LegacyAccountResponse, LegacyAccountsResponse}, job::{JobResponse, JobsResponse}, QueryMsg, State, StateResponse, {Config, ConfigResponse, ExecuteMsg, InstantiateMsg}, }; @@ -23,6 +23,6 @@ fn main() { export_schema(&schema_for!(StateResponse), &out_dir); export_schema(&schema_for!(JobResponse), &out_dir); export_schema(&schema_for!(JobsResponse), &out_dir); - export_schema(&schema_for!(MainAccountResponse), &out_dir); - export_schema(&schema_for!(MainAccountsResponse), &out_dir); + export_schema(&schema_for!(LegacyAccountResponse), &out_dir); + export_schema(&schema_for!(LegacyAccountsResponse), &out_dir); } diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 18b45c11..1a939b44 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -6,19 +6,22 @@ use cosmwasm_std::{ use cw_storage_plus::Item; use cw_utils::{must_pay, nonpayable}; -use crate::state::CONFIG; -use crate::{execute, query, reply, state::STATE, ContractError}; +use crate::{ + execute, migrate, query, reply, + state::{CONFIG, STATE}, + ContractError, +}; use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State}; // Reply id for job creation -// From a totally new user using warp for the first time, does not have account yet, let alone sub account -// So we create both account and sub account and job -pub const REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB: u64 = 1; +// From a totally new user using warp for the first time, does not have account tracker yet, let alone free account +// So we create account account and account and job +pub const REPLY_ID_CREATE_ACCOUNT_TRACKER_AND_ACCOUNT_AND_JOB: u64 = 1; // Reply id for job creation -// From an existing user, who has main account, but does not have available sub account -// So we create sub account and job -pub const REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB: u64 = 2; +// From an existing user, who has account tracker, but does not have available account +// So we create account and job +pub const REPLY_ID_CREATE_ACCOUNT_AND_JOB: u64 = 2; // Reply id for job execution pub const REPLY_ID_EXECUTE_JOB: u64 = 3; @@ -42,6 +45,7 @@ pub fn instantiate( fee_collector: deps .api .addr_validate(&msg.fee_collector.unwrap_or_else(|| info.sender.to_string()))?, + warp_job_account_tracker_code_id: msg.warp_job_account_tracker_code_id, warp_account_code_id: msg.warp_account_code_id, minimum_reward: msg.minimum_reward, creation_fee_percentage: msg.creation_fee, @@ -112,20 +116,44 @@ pub fn execute( ExecuteMsg::UpdateConfig(data) => { nonpayable(&info).unwrap(); - execute::controller::update_config(deps, env, info, data) + execute::controller::update_config(deps, env, info, data, config) } - ExecuteMsg::MigrateAccounts(data) => { + ExecuteMsg::MigrateLegacyAccounts(data) => { + nonpayable(&info).unwrap(); + migrate::legacy_account::migrate_legacy_accounts(deps, info, data, config) + } + ExecuteMsg::MigrateJobAccountTrackers(data) => { + nonpayable(&info).unwrap(); + migrate::job_account_tracker::migrate_job_account_trackers( + deps.as_ref(), + info, + data, + config, + ) + } + ExecuteMsg::MigrateFreeJobAccounts(data) => { nonpayable(&info).unwrap(); - execute::controller::migrate_accounts(deps, env, info, data) + migrate::job_account::migrate_free_job_accounts(deps.as_ref(), env, info, data, config) } + ExecuteMsg::MigrateOccupiedJobAccounts(data) => { + nonpayable(&info).unwrap(); + migrate::job_account::migrate_occupied_job_accounts( + deps.as_ref(), + env, + info, + data, + config, + ) + } + ExecuteMsg::MigratePendingJobs(data) => { nonpayable(&info).unwrap(); - execute::controller::migrate_pending_jobs(deps, env, info, data) + migrate::job::migrate_pending_jobs(deps, env, info, data, config) } ExecuteMsg::MigrateFinishedJobs(data) => { nonpayable(&info).unwrap(); - execute::controller::migrate_finished_jobs(deps, env, info, data) + migrate::job::migrate_finished_jobs(deps, env, info, data, config) } } } @@ -136,11 +164,12 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryJob(data) => to_binary(&query::job::query_job(deps, env, data)?), QueryMsg::QueryJobs(data) => to_binary(&query::job::query_jobs(deps, env, data)?), - QueryMsg::QueryMainAccount(data) => { - to_binary(&query::account::query_main_account(deps, env, data)?) + // For job account, please query it via the account tracker contract + QueryMsg::QueryLegacyAccount(data) => { + to_binary(&query::account::query_legacy_account(deps, env, data)?) } - QueryMsg::QueryMainAccounts(data) => { - to_binary(&query::account::query_main_accounts(deps, env, data)?) + QueryMsg::QueryLegacyAccounts(data) => { + to_binary(&query::account::query_legacy_accounts(deps, env, data)?) } QueryMsg::QueryConfig(data) => { @@ -203,6 +232,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result Result { let config = CONFIG.load(deps.storage)?; match msg.id { - // Main account has been created, now create sub account and job - REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB => { - reply::account::create_main_account_and_sub_account_and_job(deps, env, msg, config) - } - // Sub account has been created, now create job - REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB => { - reply::account::create_sub_account_and_job(deps, env, msg) + // Account tracker has been created, now create account and job + REPLY_ID_CREATE_ACCOUNT_TRACKER_AND_ACCOUNT_AND_JOB => { + reply::account::create_job_account_tracker_and_account_and_job(deps, env, msg, config) } + // Account has been created, now create job + REPLY_ID_CREATE_ACCOUNT_AND_JOB => reply::account::create_account_and_job(deps, env, msg), // Job has been executed REPLY_ID_EXECUTE_JOB => reply::job::execute_job(deps, env, msg, config), _ => Err(ContractError::UnknownReplyId {}), diff --git a/contracts/warp-controller/src/error.rs b/contracts/warp-controller/src/error.rs index ae27a6dc..b4b9c15c 100644 --- a/contracts/warp-controller/src/error.rs +++ b/contracts/warp-controller/src/error.rs @@ -42,6 +42,9 @@ pub enum ContractError { #[error("Account already exists")] AccountAlreadyExists {}, + #[error("Job account tracker already exists")] + JobAccountTrackerAlreadyExists {}, + #[error("Account cannot create an account")] AccountCannotCreateAccount {}, diff --git a/contracts/warp-controller/src/execute/controller.rs b/contracts/warp-controller/src/execute/controller.rs index 352986c7..d4d09b03 100644 --- a/contracts/warp-controller/src/execute/controller.rs +++ b/contracts/warp-controller/src/execute/controller.rs @@ -1,93 +1,16 @@ -use crate::state::{ACCOUNTS, CONFIG, FINISHED_JOBS, PENDING_JOBS}; -use crate::ContractError; -use controller::{MigrateAccountsMsg, MigrateJobsMsg, UpdateConfigMsg}; -use cosmwasm_schema::cw_serde; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; -use controller::account::AssetInfo; -use controller::job::{Job, JobStatus}; -use cosmwasm_std::{ - to_binary, Addr, DepsMut, Env, MessageInfo, Order, Response, Uint128, Uint64, WasmMsg, -}; -use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, MultiIndex, UniqueIndex}; -use resolver::condition::{Condition, StringValue}; -use resolver::variable::{ - ExternalExpr, ExternalVariable, FnValue, QueryExpr, QueryVariable, StaticVariable, UpdateFn, - Variable, VariableKind, -}; +use crate::{state::CONFIG, ContractError}; -//JOBS -#[cw_serde] -pub struct V1Job { - pub id: Uint64, - pub owner: Addr, - pub last_update_time: Uint64, - pub name: String, - pub description: String, - pub labels: Vec, - pub status: JobStatus, - pub condition: Condition, - pub msgs: Vec, - pub vars: Vec, - pub recurring: bool, - pub requeue_on_evict: bool, - pub reward: Uint128, - pub assets_to_withdraw: Vec, -} - -#[cw_serde] -pub enum V1Variable { - Static(V1StaticVariable), - External(V1ExternalVariable), - Query(V1QueryVariable), -} - -#[cw_serde] -pub struct V1StaticVariable { - pub kind: VariableKind, - pub name: String, - pub value: String, - pub update_fn: Option, -} - -#[cw_serde] -pub struct V1ExternalVariable { - pub kind: VariableKind, - pub name: String, - pub init_fn: ExternalExpr, - pub reinitialize: bool, - pub value: Option, //none if uninitialized - pub update_fn: Option, -} - -#[cw_serde] -pub struct V1QueryVariable { - pub kind: VariableKind, - pub name: String, - pub init_fn: QueryExpr, - pub reinitialize: bool, - pub value: Option, //none if uninitialized - pub update_fn: Option, -} - -pub struct V1JobIndexes<'a> { - pub reward: UniqueIndex<'a, (u128, u64), V1Job>, - pub publish_time: MultiIndex<'a, u64, V1Job, u64>, -} - -impl IndexList for V1JobIndexes<'_> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.reward, &self.publish_time]; - Box::new(v.into_iter()) - } -} +use controller::{Config, UpdateConfigMsg}; pub fn update_config( deps: DepsMut, _env: Env, info: MessageInfo, data: UpdateConfigMsg, + mut config: Config, ) -> Result { - let mut config = CONFIG.load(deps.storage)?; if info.sender != config.owner { return Err(ContractError::Unauthorized {}); } @@ -156,255 +79,3 @@ pub fn update_config( .add_attribute("config_t_min", config.t_min) .add_attribute("config_q_max", config.q_max)) } - -pub fn migrate_accounts( - deps: DepsMut, - _env: Env, - info: MessageInfo, - msg: MigrateAccountsMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(ContractError::Unauthorized {}); - } - - let start_after = match msg.start_after { - None => None, - Some(s) => Some(deps.api.addr_validate(s.as_str())?), - }; - let start_after = start_after.map(Bound::exclusive); - - let account_keys: Result, _> = ACCOUNTS() - .keys(deps.storage, start_after, None, Order::Ascending) - .take(msg.limit as usize) - .collect(); - let account_keys = account_keys?; - let mut migration_msgs = vec![]; - - for account_key in account_keys { - let account_address = ACCOUNTS().load(deps.storage, account_key)?.account; - migration_msgs.push(WasmMsg::Migrate { - contract_addr: account_address.to_string(), - new_code_id: msg.warp_account_code_id.u64(), - msg: to_binary(&account::MigrateMsg {})?, - }) - } - - Ok(Response::new().add_messages(migration_msgs)) -} - -pub fn migrate_pending_jobs( - deps: DepsMut, - _env: Env, - info: MessageInfo, - msg: MigrateJobsMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(ContractError::Unauthorized {}); - } - - let start_after = msg.start_after; - let start_after = start_after.map(Bound::exclusive); - - #[allow(non_snake_case)] - pub fn V1_PENDING_JOBS<'a>() -> IndexedMap<'a, u64, V1Job, V1JobIndexes<'a>> { - let indexes = V1JobIndexes { - reward: UniqueIndex::new( - |job| (job.reward.u128(), job.id.u64()), - "pending_jobs__reward_v2", - ), - publish_time: MultiIndex::new( - |_pk, job| job.last_update_time.u64(), - "pending_jobs_v2", - "pending_jobs__publish_timestamp_v2", - ), - }; - IndexedMap::new("pending_jobs_v2", indexes) - } - - let job_keys: Result, _> = V1_PENDING_JOBS() - .keys(deps.storage, start_after, None, Order::Ascending) - .take(msg.limit as usize) - .collect(); - let job_keys = job_keys?; - for job_key in job_keys { - let v1_job = V1_PENDING_JOBS().load(deps.storage, job_key)?; - let mut new_vars = vec![]; - for var in v1_job.vars { - new_vars.push(match var { - V1Variable::Static(v) => Variable::Static(StaticVariable { - kind: v.kind, - name: v.name, - encode: false, - init_fn: FnValue::String(StringValue::Simple(v.value.clone())), - reinitialize: false, - value: Some(v.value.clone()), - update_fn: v.update_fn, - }), - V1Variable::External(v) => Variable::External(ExternalVariable { - kind: v.kind, - name: v.name, - encode: false, - init_fn: v.init_fn, - reinitialize: v.reinitialize, - value: v.value, - update_fn: v.update_fn, - }), - V1Variable::Query(v) => Variable::Query(QueryVariable { - kind: v.kind, - name: v.name, - encode: false, - init_fn: v.init_fn, - reinitialize: v.reinitialize, - value: v.value, - update_fn: v.update_fn, - }), - }) - } - - let mut new_msgs = "[".to_string(); - - for msg in v1_job.msgs { - new_msgs.push_str(msg.as_str()); - } - - new_msgs.push(']'); - - let warp_account = ACCOUNTS().load(deps.storage, v1_job.owner.clone())?; - - PENDING_JOBS().save( - deps.storage, - job_key, - &Job { - id: v1_job.id, - prev_id: None, - owner: v1_job.owner, - account: warp_account.account, - last_update_time: v1_job.last_update_time, - name: v1_job.name, - description: v1_job.description, - labels: v1_job.labels, - status: v1_job.status, - condition: serde_json_wasm::to_string(&v1_job.condition)?, - terminate_condition: None, - msgs: new_msgs.to_string(), - vars: serde_json_wasm::to_string(&new_vars)?, - recurring: v1_job.recurring, - requeue_on_evict: v1_job.requeue_on_evict, - reward: v1_job.reward, - assets_to_withdraw: v1_job.assets_to_withdraw, - }, - )?; - } - - Ok(Response::new()) -} - -pub fn migrate_finished_jobs( - deps: DepsMut, - _env: Env, - info: MessageInfo, - msg: MigrateJobsMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner { - return Err(ContractError::Unauthorized {}); - } - - let start_after = msg.start_after; - let start_after = start_after.map(Bound::exclusive); - - #[allow(non_snake_case)] - pub fn V1_FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, V1Job, V1JobIndexes<'a>> { - let indexes = V1JobIndexes { - reward: UniqueIndex::new( - |job| (job.reward.u128(), job.id.u64()), - "finished_jobs__reward_v2", - ), - publish_time: MultiIndex::new( - |_pk, job| job.last_update_time.u64(), - "finished_jobs_v2", - "finished_jobs__publish_timestamp_v2", - ), - }; - IndexedMap::new("finished_jobs_v2", indexes) - } - - let job_keys: Result, _> = V1_FINISHED_JOBS() - .keys(deps.storage, start_after, None, Order::Ascending) - .take(msg.limit as usize) - .collect(); - let job_keys = job_keys?; - for job_key in job_keys { - let v1_job = V1_FINISHED_JOBS().load(deps.storage, job_key)?; - let mut new_vars = vec![]; - for var in v1_job.vars { - new_vars.push(match var { - V1Variable::Static(v) => Variable::Static(StaticVariable { - kind: v.kind, - name: v.name, - encode: false, - init_fn: FnValue::String(StringValue::Simple(v.value.clone())), - reinitialize: false, - value: Some(v.value.clone()), - update_fn: v.update_fn, - }), - V1Variable::External(v) => Variable::External(ExternalVariable { - kind: v.kind, - name: v.name, - encode: false, - init_fn: v.init_fn, - reinitialize: v.reinitialize, - value: v.value, - update_fn: v.update_fn, - }), - V1Variable::Query(v) => Variable::Query(QueryVariable { - kind: v.kind, - name: v.name, - encode: false, - init_fn: v.init_fn, - reinitialize: v.reinitialize, - value: v.value, - update_fn: v.update_fn, - }), - }) - } - - let mut new_msgs = "[".to_string(); - - for msg in v1_job.msgs { - new_msgs.push_str(msg.as_str()); - } - - new_msgs.push(']'); - - let warp_account = ACCOUNTS().load(deps.storage, v1_job.owner.clone())?; - - FINISHED_JOBS().save( - deps.storage, - job_key, - &Job { - id: v1_job.id, - prev_id: None, - owner: v1_job.owner, - account: warp_account.account, - last_update_time: v1_job.last_update_time, - name: v1_job.name, - description: v1_job.description, - labels: v1_job.labels, - status: v1_job.status, - condition: serde_json_wasm::to_string(&v1_job.condition)?, - terminate_condition: None, - msgs: new_msgs, - vars: serde_json_wasm::to_string(&new_vars)?, - recurring: v1_job.recurring, - requeue_on_evict: v1_job.requeue_on_evict, - reward: v1_job.reward, - assets_to_withdraw: v1_job.assets_to_withdraw, - }, - )?; - } - - Ok(Response::new()) -} diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index cecf7174..5f5a539e 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -3,31 +3,32 @@ use cosmwasm_std::{ MessageInfo, QueryRequest, ReplyOn, Response, StdResult, SubMsg, Uint128, Uint64, WasmMsg, }; -use crate::contract::{ - REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB, REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, - REPLY_ID_EXECUTE_JOB, -}; -use crate::state::{JobQueue, ACCOUNTS, STATE}; -use crate::util::msg::{ - build_instantiate_warp_main_account_msg, build_instantiate_warp_sub_account_msg, -}; -use crate::util::{ - fee::deduct_reward_and_fee_from_native_funds, - msg::{ - build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_free_sub_account_msg, build_occupy_sub_account_msg, build_transfer_cw20_msg, - build_transfer_cw721_msg, build_transfer_native_funds_msg, +use crate::{ + contract::{ + REPLY_ID_CREATE_ACCOUNT_AND_JOB, REPLY_ID_CREATE_ACCOUNT_TRACKER_AND_ACCOUNT_AND_JOB, + REPLY_ID_EXECUTE_JOB, + }, + state::{JobQueue, JOB_ACCOUNT_TRACKERS, LEGACY_ACCOUNTS, STATE}, + util::{ + fee::deduct_reward_and_fee_from_native_funds, + legacy_account::is_legacy_account, + msg::{ + build_account_execute_generic_msgs, build_account_withdraw_assets_msg, + build_free_account_msg, build_instantiate_warp_account_msg, + build_instantiate_warp_job_account_tracker_msg, build_occupy_account_msg, + build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, + }, }, - sub_account::is_sub_account, + ContractError, }; -use crate::ContractError; -use account::{FirstFreeSubAccountResponse, GenericMsg}; use controller::{ account::CwFund, job::{CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Job, JobStatus, UpdateJobMsg}, Config, }; +use job_account::GenericMsg; +use job_account_tracker::FirstFreeAccountResponse; use resolver::QueryHydrateMsgsMsg; const MAX_TEXT_LENGTH: usize = 280; @@ -91,20 +92,7 @@ pub fn create_job( ), ); - // First try to query main account by account address (query index key which is account by sender) - // This can happen when account contract calls controller's create_job - // The result would be none if user (account owner) calls create_job directly - let main_account = match ACCOUNTS() - .idx - .account - .item(deps.storage, info.sender.clone())? - { - // create_job is called by account contract - Some(record) => Some(record.1), - // create_job is called by user - None => ACCOUNTS().may_load(deps.storage, info.sender.clone())?, - }; - + let job_account_tracker = JOB_ACCOUNT_TRACKERS.may_load(deps.storage, &info.sender)?; let state = STATE.load(deps.storage)?; let mut job = JobQueue::add( &mut deps, @@ -112,7 +100,7 @@ pub fn create_job( id: state.current_job_id, prev_id: None, owner: info.sender.clone(), - // Account uses a placeholder value for now, will update it to sub account address if sub account exists or after created + // Account uses a placeholder value for now, will update it to job account address if job account exists or after created // Update will happen either in create_job (sub account exists) or reply (after creation), so it's atomic // And we guarantee we do not read this value before it's updated account: info.sender.clone(), @@ -132,19 +120,15 @@ pub fn create_job( }, )?; - match main_account { + match job_account_tracker { None => { - // Create main account then create sub account then create job in reply + // Create account tracker then create account then create job in reply submsgs.push(SubMsg { - id: REPLY_ID_CREATE_MAIN_ACCOUNT_AND_SUB_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_main_account_msg( - job.id, + id: REPLY_ID_CREATE_ACCOUNT_TRACKER_AND_ACCOUNT_AND_JOB, + msg: build_instantiate_warp_job_account_tracker_msg( env.contract.address.to_string(), - config.warp_account_code_id.u64(), + config.warp_job_account_tracker_code_id.u64(), info.sender.to_string(), - native_funds_minus_reward_and_fee, - data.cw_funds, - data.account_msgs, ), gas_limit: None, reply_on: ReplyOn::Always, @@ -152,32 +136,27 @@ pub fn create_job( attrs.push(Attribute::new( "action", - "create_main_account_and_sub_account_and_job", + "create_job_account_tracker_and_account_and_job", )); } - Some(main_account) => { - if main_account.owner != info.sender { - return Err(ContractError::Unauthorized {}); - } - let main_account_addr = main_account.account; - let available_sub_account: FirstFreeSubAccountResponse = - deps.querier.query_wasm_smart( - main_account_addr.clone(), - &account::QueryMsg::QueryFirstFreeSubAccount( - account::QueryFirstFreeSubAccountMsg {}, - ), - )?; - match available_sub_account.sub_account { + Some(job_account_tracker) => { + let available_account: FirstFreeAccountResponse = deps.querier.query_wasm_smart( + job_account_tracker.clone(), + &job_account_tracker::QueryMsg::QueryFirstFreeAccount( + job_account_tracker::QueryFirstFreeAccountMsg {}, + ), + )?; + match available_account.account { None => { - // Create sub account then create job in reply + // Create account then create job in reply submsgs.push(SubMsg { - id: REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_sub_account_msg( + id: REPLY_ID_CREATE_ACCOUNT_AND_JOB, + msg: build_instantiate_warp_account_msg( job.id, env.contract.address.to_string(), config.warp_account_code_id.u64(), info.sender.to_string(), - main_account_addr.clone().to_string(), + job_account_tracker.clone().to_string(), native_funds_minus_reward_and_fee, data.cw_funds, data.account_msgs, @@ -186,18 +165,18 @@ pub fn create_job( reply_on: ReplyOn::Always, }); - attrs.push(Attribute::new("action", "create_sub_account_and_job")); + attrs.push(Attribute::new("action", "create_account_and_job")); } - Some(sub_account) => { - let sub_account_addr = sub_account.account_addr; - // Update job.account from placeholder value to sub account - job.account = sub_account_addr.clone(); + Some(available_account) => { + let available_account_addr = available_account.addr; + // Update job.account from placeholder value to job account + job.account = available_account_addr.clone(); JobQueue::sync(&mut deps, env, job.clone())?; if !native_funds_minus_reward_and_fee.is_empty() { // Fund account in native coins msgs.push(build_transfer_native_funds_msg( - sub_account_addr.clone().to_string(), + available_account_addr.clone().to_string(), native_funds_minus_reward_and_fee, )) } @@ -211,14 +190,14 @@ pub fn create_job( .addr_validate(&cw20_fund.contract_addr)? .to_string(), info.sender.clone().to_string(), - sub_account_addr.clone().to_string(), + available_account_addr.clone().to_string(), cw20_fund.amount, ), CwFund::Cw721(cw721_fund) => build_transfer_cw721_msg( deps.api .addr_validate(&cw721_fund.contract_addr)? .to_string(), - sub_account_addr.clone().to_string(), + available_account_addr.clone().to_string(), cw721_fund.token_id.clone(), ), }) @@ -228,15 +207,15 @@ pub fn create_job( if let Some(account_msgs) = data.account_msgs { // Account execute msgs msgs.push(build_account_execute_generic_msgs( - sub_account_addr.to_string(), + available_account_addr.to_string(), account_msgs, )); } - // Occupy sub account - msgs.push(build_occupy_sub_account_msg( - main_account_addr.to_string(), - sub_account_addr.to_string(), + // Occupy account + msgs.push(build_occupy_account_msg( + job_account_tracker.to_string(), + available_account_addr.to_string(), job.id, )); @@ -282,7 +261,7 @@ pub fn delete_job( fee_denom_paid_amount: Uint128, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; - let main_account_addr = ACCOUNTS().load(deps.storage, job.owner.clone())?.account; + let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, job.owner.clone())?; let job_account_addr = job.account.clone(); if job.status != JobStatus::Pending { @@ -318,10 +297,12 @@ pub fn delete_job( vec![Coin::new(fee.u128(), config.fee_denom)], )); - if is_sub_account(&main_account_addr, &job_account_addr) { - // Free sub account - msgs.push(build_free_sub_account_msg( - main_account_addr.to_string(), + if !is_legacy_account(legacy_account, job_account_addr.clone()) { + // For job not using legacy account, job owner must already have account tracker instantiated + let job_account_tracker = JOB_ACCOUNT_TRACKERS.load(deps.storage, &job.owner)?; + // Free account + msgs.push(build_free_account_msg( + job_account_tracker.to_string(), job_account_addr.to_string(), )); } @@ -386,7 +367,7 @@ pub fn update_job( // Controller sends update fee to fee collector WasmMsg::Execute { contract_addr: job.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msg: to_binary(&job_account::ExecuteMsg::Generic(GenericMsg { msgs: vec![CosmosMsg::Bank(BankMsg::Send { to_address: config.fee_collector.to_string(), amount: vec![Coin::new((fee).u128(), config.fee_denom)], @@ -419,7 +400,7 @@ pub fn execute_job( config: Config, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; - let main_account_addr = ACCOUNTS().load(deps.storage, job.owner.clone())?.account; + let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, job.owner.clone())?; let job_account_addr = job.account.clone(); if job.status != JobStatus::Pending { @@ -453,13 +434,11 @@ pub fn execute_job( attrs.push(Attribute::new("error", e.to_string())); JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Failed)?; - if is_sub_account(&main_account_addr, &job_account_addr) { - // Free sub account - msgs.push(build_free_sub_account_msg( - main_account_addr.to_string(), - job_account_addr.to_string(), - )); - } + // Withdraw reward to job owner + msgs.push(build_account_withdraw_assets_msg( + job.account.to_string(), + job.assets_to_withdraw, + )); } else { attrs.push(Attribute::new("job_condition_status", "valid")); if !resolution? { @@ -477,7 +456,7 @@ pub fn execute_job( id: REPLY_ID_EXECUTE_JOB, msg: CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msg: to_binary(&job_account::ExecuteMsg::Generic(GenericMsg { msgs: deps.querier.query_wasm_smart( config.resolver_address, &resolver::QueryMsg::QueryHydrateMsgs(QueryHydrateMsgsMsg { @@ -499,10 +478,12 @@ pub fn execute_job( vec![Coin::new(job.reward.u128(), config.fee_denom)], )); - if is_sub_account(&main_account_addr, &job_account_addr) { - // Free sub account - msgs.push(build_free_sub_account_msg( - main_account_addr.to_string(), + if !is_legacy_account(legacy_account, job_account_addr.clone()) { + // For job not using legacy account, job owner must already have account tracker instantiated + let job_account_tracker = JOB_ACCOUNT_TRACKERS.load(deps.storage, &job.owner)?; + // Free account + msgs.push(build_free_account_msg( + job_account_tracker.to_string(), job_account_addr.to_string(), )); } @@ -526,7 +507,7 @@ pub fn evict_job( ) -> Result { let state = STATE.load(deps.storage)?; let job = JobQueue::get(&deps, data.id.into())?; - let main_account_addr = ACCOUNTS().load(deps.storage, job.owner.clone())?.account; + let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, job.owner.clone())?; let job_account_addr = job.account.clone(); let account_amount = deps @@ -591,10 +572,12 @@ pub fn evict_job( vec![Coin::new((job.reward - a).u128(), config.fee_denom.clone())], )); - if is_sub_account(&main_account_addr, &job_account_addr) { - // Free sub account - msgs.push(build_free_sub_account_msg( - main_account_addr.to_string(), + if !is_legacy_account(legacy_account, job_account_addr.clone()) { + // For job not using legacy account, job owner must already have account tracker instantiated + let job_account_tracker = JOB_ACCOUNT_TRACKERS.load(deps.storage, &job.owner)?; + // Free account + msgs.push(build_free_account_msg( + job_account_tracker.to_string(), job_account_addr.to_string(), )); } diff --git a/contracts/warp-controller/src/lib.rs b/contracts/warp-controller/src/lib.rs index 49f9d59a..80857688 100644 --- a/contracts/warp-controller/src/lib.rs +++ b/contracts/warp-controller/src/lib.rs @@ -5,6 +5,7 @@ pub mod state; pub use crate::error::ContractError; mod execute; +mod migrate; mod query; mod reply; diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs new file mode 100644 index 00000000..f8ab9d7a --- /dev/null +++ b/contracts/warp-controller/src/migrate/job.rs @@ -0,0 +1,303 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Order, Response, Uint128, Uint64}; +use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, MultiIndex, UniqueIndex}; + +use crate::{ + state::{FINISHED_JOBS, LEGACY_ACCOUNTS, PENDING_JOBS}, + ContractError, +}; +use controller::{ + account::AssetInfo, + job::{Job, JobStatus}, + Config, MigrateJobsMsg, +}; + +use resolver::{ + condition::{Condition, StringValue}, + variable::{ + ExternalExpr, ExternalVariable, FnValue, QueryExpr, QueryVariable, StaticVariable, + UpdateFn, Variable, VariableKind, + }, +}; + +//JOBS +#[cw_serde] +pub struct V1Job { + pub id: Uint64, + pub owner: Addr, + pub last_update_time: Uint64, + pub name: String, + pub description: String, + pub labels: Vec, + pub status: JobStatus, + pub condition: Condition, + pub msgs: Vec, + pub vars: Vec, + pub recurring: bool, + pub requeue_on_evict: bool, + pub reward: Uint128, + pub assets_to_withdraw: Vec, +} + +#[cw_serde] +pub enum V1Variable { + Static(V1StaticVariable), + External(V1ExternalVariable), + Query(V1QueryVariable), +} + +#[cw_serde] +pub struct V1StaticVariable { + pub kind: VariableKind, + pub name: String, + pub value: String, + pub update_fn: Option, +} + +#[cw_serde] +pub struct V1ExternalVariable { + pub kind: VariableKind, + pub name: String, + pub init_fn: ExternalExpr, + pub reinitialize: bool, + pub value: Option, //none if uninitialized + pub update_fn: Option, +} + +#[cw_serde] +pub struct V1QueryVariable { + pub kind: VariableKind, + pub name: String, + pub init_fn: QueryExpr, + pub reinitialize: bool, + pub value: Option, //none if uninitialized + pub update_fn: Option, +} + +pub struct V1JobIndexes<'a> { + pub reward: UniqueIndex<'a, (u128, u64), V1Job>, + pub publish_time: MultiIndex<'a, u64, V1Job, u64>, +} + +impl IndexList for V1JobIndexes<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.reward, &self.publish_time]; + Box::new(v.into_iter()) + } +} + +pub fn migrate_pending_jobs( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: MigrateJobsMsg, + config: Config, +) -> Result { + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let start_after = msg.start_after; + let start_after = start_after.map(Bound::exclusive); + + #[allow(non_snake_case)] + pub fn V1_PENDING_JOBS<'a>() -> IndexedMap<'a, u64, V1Job, V1JobIndexes<'a>> { + let indexes = V1JobIndexes { + reward: UniqueIndex::new( + |job| (job.reward.u128(), job.id.u64()), + "pending_jobs__reward_v2", + ), + publish_time: MultiIndex::new( + |_pk, job| job.last_update_time.u64(), + "pending_jobs_v2", + "pending_jobs__publish_timestamp_v2", + ), + }; + IndexedMap::new("pending_jobs_v2", indexes) + } + + let job_keys: Result, _> = V1_PENDING_JOBS() + .keys(deps.storage, start_after, None, Order::Ascending) + .take(msg.limit as usize) + .collect(); + let job_keys = job_keys?; + for job_key in job_keys { + let v1_job = V1_PENDING_JOBS().load(deps.storage, job_key)?; + let mut new_vars = vec![]; + for var in v1_job.vars { + new_vars.push(match var { + V1Variable::Static(v) => Variable::Static(StaticVariable { + kind: v.kind, + name: v.name, + encode: false, + init_fn: FnValue::String(StringValue::Simple(v.value.clone())), + reinitialize: false, + value: Some(v.value.clone()), + update_fn: v.update_fn, + }), + V1Variable::External(v) => Variable::External(ExternalVariable { + kind: v.kind, + name: v.name, + encode: false, + init_fn: v.init_fn, + reinitialize: v.reinitialize, + value: v.value, + update_fn: v.update_fn, + }), + V1Variable::Query(v) => Variable::Query(QueryVariable { + kind: v.kind, + name: v.name, + encode: false, + init_fn: v.init_fn, + reinitialize: v.reinitialize, + value: v.value, + update_fn: v.update_fn, + }), + }) + } + + let mut new_msgs = "[".to_string(); + + for msg in v1_job.msgs { + new_msgs.push_str(msg.as_str()); + } + + new_msgs.push(']'); + + let warp_account = LEGACY_ACCOUNTS().load(deps.storage, v1_job.owner.clone())?; + + PENDING_JOBS().save( + deps.storage, + job_key, + &Job { + id: v1_job.id, + prev_id: None, + owner: v1_job.owner, + account: warp_account.account, + last_update_time: v1_job.last_update_time, + name: v1_job.name, + description: v1_job.description, + labels: v1_job.labels, + status: v1_job.status, + condition: serde_json_wasm::to_string(&v1_job.condition)?, + terminate_condition: None, + msgs: new_msgs.to_string(), + vars: serde_json_wasm::to_string(&new_vars)?, + recurring: v1_job.recurring, + requeue_on_evict: v1_job.requeue_on_evict, + reward: v1_job.reward, + assets_to_withdraw: v1_job.assets_to_withdraw, + }, + )?; + } + + Ok(Response::new()) +} + +pub fn migrate_finished_jobs( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: MigrateJobsMsg, + config: Config, +) -> Result { + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let start_after = msg.start_after; + let start_after = start_after.map(Bound::exclusive); + + #[allow(non_snake_case)] + pub fn V1_FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, V1Job, V1JobIndexes<'a>> { + let indexes = V1JobIndexes { + reward: UniqueIndex::new( + |job| (job.reward.u128(), job.id.u64()), + "finished_jobs__reward_v2", + ), + publish_time: MultiIndex::new( + |_pk, job| job.last_update_time.u64(), + "finished_jobs_v2", + "finished_jobs__publish_timestamp_v2", + ), + }; + IndexedMap::new("finished_jobs_v2", indexes) + } + + let job_keys: Result, _> = V1_FINISHED_JOBS() + .keys(deps.storage, start_after, None, Order::Ascending) + .take(msg.limit as usize) + .collect(); + let job_keys = job_keys?; + for job_key in job_keys { + let v1_job = V1_FINISHED_JOBS().load(deps.storage, job_key)?; + let mut new_vars = vec![]; + for var in v1_job.vars { + new_vars.push(match var { + V1Variable::Static(v) => Variable::Static(StaticVariable { + kind: v.kind, + name: v.name, + encode: false, + init_fn: FnValue::String(StringValue::Simple(v.value.clone())), + reinitialize: false, + value: Some(v.value.clone()), + update_fn: v.update_fn, + }), + V1Variable::External(v) => Variable::External(ExternalVariable { + kind: v.kind, + name: v.name, + encode: false, + init_fn: v.init_fn, + reinitialize: v.reinitialize, + value: v.value, + update_fn: v.update_fn, + }), + V1Variable::Query(v) => Variable::Query(QueryVariable { + kind: v.kind, + name: v.name, + encode: false, + init_fn: v.init_fn, + reinitialize: v.reinitialize, + value: v.value, + update_fn: v.update_fn, + }), + }) + } + + let mut new_msgs = "[".to_string(); + + for msg in v1_job.msgs { + new_msgs.push_str(msg.as_str()); + } + + new_msgs.push(']'); + + let warp_account = LEGACY_ACCOUNTS().load(deps.storage, v1_job.owner.clone())?; + + FINISHED_JOBS().save( + deps.storage, + job_key, + &Job { + id: v1_job.id, + prev_id: None, + owner: v1_job.owner, + account: warp_account.account, + last_update_time: v1_job.last_update_time, + name: v1_job.name, + description: v1_job.description, + labels: v1_job.labels, + status: v1_job.status, + condition: serde_json_wasm::to_string(&v1_job.condition)?, + terminate_condition: None, + msgs: new_msgs, + vars: serde_json_wasm::to_string(&new_vars)?, + recurring: v1_job.recurring, + requeue_on_evict: v1_job.requeue_on_evict, + reward: v1_job.reward, + assets_to_withdraw: v1_job.assets_to_withdraw, + }, + )?; + } + + Ok(Response::new()) +} diff --git a/contracts/warp-controller/src/migrate/job_account.rs b/contracts/warp-controller/src/migrate/job_account.rs new file mode 100644 index 00000000..b7894e40 --- /dev/null +++ b/contracts/warp-controller/src/migrate/job_account.rs @@ -0,0 +1,69 @@ +use cosmwasm_std::{to_binary, Deps, Env, MessageInfo, Response, WasmMsg}; + +use crate::ContractError; +use controller::{Config, MigrateJobAccountsMsg}; +use job_account_tracker::{ + AccountsResponse, MigrateMsg, QueryFreeAccountsMsg, QueryOccupiedAccountsMsg, +}; + +pub fn migrate_free_job_accounts( + deps: Deps, + _env: Env, + info: MessageInfo, + msg: MigrateJobAccountsMsg, + config: Config, +) -> Result { + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let free_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( + msg.job_account_tracker_addr, + &job_account_tracker::QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + start_after: msg.start_after, + limit: Some(msg.limit as u32), + }), + )?; + + let mut migration_msgs = vec![]; + for job_account in free_job_accounts.accounts { + migration_msgs.push(WasmMsg::Migrate { + contract_addr: job_account.addr.to_string(), + new_code_id: msg.warp_job_account_code_id.u64(), + msg: to_binary(&MigrateMsg {})?, + }); + } + + Ok(Response::new().add_messages(migration_msgs)) +} + +pub fn migrate_occupied_job_accounts( + deps: Deps, + _env: Env, + info: MessageInfo, + msg: MigrateJobAccountsMsg, + config: Config, +) -> Result { + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let occupied_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( + msg.job_account_tracker_addr, + &job_account_tracker::QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + start_after: msg.start_after, + limit: Some(msg.limit as u32), + }), + )?; + + let mut migration_msgs = vec![]; + for job_account in occupied_job_accounts.accounts { + migration_msgs.push(WasmMsg::Migrate { + contract_addr: job_account.addr.to_string(), + new_code_id: msg.warp_job_account_code_id.u64(), + msg: to_binary(&MigrateMsg {})?, + }); + } + + Ok(Response::new().add_messages(migration_msgs)) +} diff --git a/contracts/warp-controller/src/migrate/job_account_tracker.rs b/contracts/warp-controller/src/migrate/job_account_tracker.rs new file mode 100644 index 00000000..0cec0d96 --- /dev/null +++ b/contracts/warp-controller/src/migrate/job_account_tracker.rs @@ -0,0 +1,45 @@ +use cosmwasm_std::{to_binary, Addr, Deps, MessageInfo, Order, Response, StdResult, WasmMsg}; +use cw_storage_plus::Bound; + +use crate::{state::JOB_ACCOUNT_TRACKERS, ContractError}; +use controller::{Config, MigrateJobAccountTrackersMsg}; + +pub fn migrate_job_account_trackers( + deps: Deps, + info: MessageInfo, + msg: MigrateJobAccountTrackersMsg, + config: Config, +) -> Result { + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let job_account_tracker_keys = match msg.start_after { + None => JOB_ACCOUNT_TRACKERS.keys(deps.storage, None, None, Order::Ascending), + Some(start_after) => JOB_ACCOUNT_TRACKERS.keys( + deps.storage, + Some(Bound::exclusive( + &deps.api.addr_validate(start_after.as_str())?, + )), + None, + Order::Ascending, + ), + } + .take(msg.limit as usize) + .collect::>>()?; + + // let job_account_tracker_keys = job_account_tracker_keys?; + let mut migration_msgs = vec![]; + + for job_account_tracker_key in job_account_tracker_keys { + let job_account_tracker = + JOB_ACCOUNT_TRACKERS.load(deps.storage, &job_account_tracker_key)?; + migration_msgs.push(WasmMsg::Migrate { + contract_addr: job_account_tracker.to_string(), + new_code_id: msg.warp_job_account_tracker_code_id.u64(), + msg: to_binary(&job_account_tracker::MigrateMsg {})?, + }) + } + + Ok(Response::new().add_messages(migration_msgs)) +} diff --git a/contracts/warp-controller/src/migrate/legacy_account.rs b/contracts/warp-controller/src/migrate/legacy_account.rs new file mode 100644 index 00000000..c7c7baf0 --- /dev/null +++ b/contracts/warp-controller/src/migrate/legacy_account.rs @@ -0,0 +1,40 @@ +use cosmwasm_std::{to_binary, DepsMut, MessageInfo, Order, Response, WasmMsg}; +use cw_storage_plus::Bound; + +use crate::{state::LEGACY_ACCOUNTS, ContractError}; +use controller::{Config, MigrateLegacyAccountsMsg}; + +pub fn migrate_legacy_accounts( + deps: DepsMut, + info: MessageInfo, + msg: MigrateLegacyAccountsMsg, + config: Config, +) -> Result { + if info.sender != config.owner { + return Err(ContractError::Unauthorized {}); + } + + let start_after = match msg.start_after { + None => None, + Some(s) => Some(deps.api.addr_validate(s.as_str())?), + }; + let start_after = start_after.map(Bound::exclusive); + + let account_keys: Result, _> = LEGACY_ACCOUNTS() + .keys(deps.storage, start_after, None, Order::Ascending) + .take(msg.limit as usize) + .collect(); + let account_keys = account_keys?; + let mut migration_msgs = vec![]; + + for account_key in account_keys { + let account_address = LEGACY_ACCOUNTS().load(deps.storage, account_key)?.account; + migration_msgs.push(WasmMsg::Migrate { + contract_addr: account_address.to_string(), + new_code_id: msg.warp_legacy_account_code_id.u64(), + msg: to_binary(&legacy_account::MigrateMsg {})?, + }) + } + + Ok(Response::new().add_messages(migration_msgs)) +} diff --git a/contracts/warp-controller/src/migrate/mod.rs b/contracts/warp-controller/src/migrate/mod.rs new file mode 100644 index 00000000..d85652dc --- /dev/null +++ b/contracts/warp-controller/src/migrate/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod job; +pub(crate) mod job_account; +pub(crate) mod job_account_tracker; +pub(crate) mod legacy_account; diff --git a/contracts/warp-controller/src/query/account.rs b/contracts/warp-controller/src/query/account.rs index 0730a5ce..f5817723 100644 --- a/contracts/warp-controller/src/query/account.rs +++ b/contracts/warp-controller/src/query/account.rs @@ -1,40 +1,40 @@ use cosmwasm_std::{Deps, Env, Order, StdResult}; use cw_storage_plus::Bound; -use crate::state::{ACCOUNTS, QUERY_PAGE_SIZE}; +use crate::state::{LEGACY_ACCOUNTS, QUERY_PAGE_SIZE}; use controller::account::{ - MainAccountResponse, MainAccountsResponse, QueryMainAccountMsg, QueryMainAccountsMsg, + LegacyAccountResponse, LegacyAccountsResponse, QueryLegacyAccountMsg, QueryLegacyAccountsMsg, }; -pub fn query_main_account( +pub fn query_legacy_account( deps: Deps, _env: Env, - data: QueryMainAccountMsg, -) -> StdResult { - Ok(MainAccountResponse { - main_account: ACCOUNTS() + data: QueryLegacyAccountMsg, +) -> StdResult { + Ok(LegacyAccountResponse { + account: LEGACY_ACCOUNTS() .load(deps.storage, deps.api.addr_validate(data.owner.as_str())?)?, }) } -pub fn query_main_accounts( +pub fn query_legacy_accounts( deps: Deps, _env: Env, - data: QueryMainAccountsMsg, -) -> StdResult { + data: QueryLegacyAccountsMsg, +) -> StdResult { let start_after = match data.start_after { None => None, Some(s) => Some(deps.api.addr_validate(s.as_str())?), }; let start_after = start_after.map(Bound::exclusive); - let infos = ACCOUNTS() + let infos = LEGACY_ACCOUNTS() .range(deps.storage, start_after, None, Order::Ascending) .take(data.limit.unwrap_or(QUERY_PAGE_SIZE) as usize) .collect::>>()?; - let mut main_accounts = vec![]; + let mut accounts = vec![]; for tuple in infos { - main_accounts.push(tuple.1) + accounts.push(tuple.1) } - Ok(MainAccountsResponse { main_accounts }) + Ok(LegacyAccountsResponse { accounts }) } diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 4bbc6de0..db0ceda8 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -2,23 +2,20 @@ use cosmwasm_std::{ Coin, CosmosMsg, DepsMut, Env, Reply, ReplyOn, Response, StdError, SubMsg, Uint64, }; -use controller::{ - account::{CwFund, MainAccount}, - Config, -}; +use controller::{account::CwFund, Config}; use crate::{ - contract::REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, - state::{JobQueue, ACCOUNTS}, + contract::REPLY_ID_CREATE_ACCOUNT_AND_JOB, + state::{JobQueue, JOB_ACCOUNT_TRACKERS}, util::msg::{ - build_account_execute_generic_msgs, build_instantiate_warp_sub_account_msg, - build_occupy_sub_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, + build_account_execute_generic_msgs, build_instantiate_warp_account_msg, + build_occupy_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, }, ContractError, }; -pub fn create_main_account_and_sub_account_and_job( +pub fn create_job_account_tracker_and_account_and_job( deps: DepsMut, env: Env, msg: Reply, @@ -53,8 +50,9 @@ pub fn create_main_account_and_sub_account_and_job( .find(|attr| attr.key == "owner") .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? .value; + let owner_addr_ref = &deps.api.addr_validate(&owner)?; - let main_account_addr = event + let job_account_tracker_addr = event .attributes .iter() .cloned() @@ -92,28 +90,25 @@ pub fn create_main_account_and_sub_account_and_job( .value, )?; - if ACCOUNTS().has(deps.storage, deps.api.addr_validate(&owner)?) { - return Err(ContractError::AccountAlreadyExists {}); + if JOB_ACCOUNT_TRACKERS.has(deps.storage, owner_addr_ref) { + return Err(ContractError::JobAccountTrackerAlreadyExists {}); } - ACCOUNTS().save( + JOB_ACCOUNT_TRACKERS.save( deps.storage, - deps.api.addr_validate(&owner)?, - &MainAccount { - owner: deps.api.addr_validate(&owner.clone())?, - account: deps.api.addr_validate(&main_account_addr)?, - }, + owner_addr_ref, + &deps.api.addr_validate(&job_account_tracker_addr)?, )?; // Create new sub account then create job in reply - let create_sub_account_and_job_submsg = SubMsg { - id: REPLY_ID_CREATE_SUB_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_sub_account_msg( + let create_account_and_job_submsg = SubMsg { + id: REPLY_ID_CREATE_ACCOUNT_AND_JOB, + msg: build_instantiate_warp_account_msg( Uint64::from(job_id), env.contract.address.to_string(), config.warp_account_code_id.u64(), owner.clone(), - main_account_addr.clone().to_string(), + job_account_tracker_addr.clone(), native_funds.clone(), cw_funds.clone(), account_msgs, @@ -123,15 +118,14 @@ pub fn create_main_account_and_sub_account_and_job( }; Ok(Response::new() - .add_submessage(create_sub_account_and_job_submsg) - // .add_messages(msgs) + .add_submessage(create_account_and_job_submsg) .add_attribute( "action", - "create_main_account_and_sub_account_and_job_reply", + "create_job_account_tracker_and_account_and_job_reply", ) - // .add_attribute("job_id", value) + .add_attribute("job_id", job_id_str) .add_attribute("owner", owner) - .add_attribute("account_address", main_account_addr) + .add_attribute("job_account_tracker_addr", job_account_tracker_addr) .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?) .add_attribute( "cw_funds", @@ -139,7 +133,7 @@ pub fn create_main_account_and_sub_account_and_job( )) } -pub fn create_sub_account_and_job( +pub fn create_account_and_job( mut deps: DepsMut, env: Env, msg: Reply, @@ -270,7 +264,7 @@ pub fn create_sub_account_and_job( } // Occupy sub account - msgs.push(build_occupy_sub_account_msg( + msgs.push(build_occupy_account_msg( main_account_addr.to_string(), sub_account_addr.to_string(), job.id, @@ -278,7 +272,7 @@ pub fn create_sub_account_and_job( Ok(Response::new() .add_messages(msgs) - .add_attribute("action", "create_sub_account_and_job_reply") + .add_attribute("action", "create_account_and_job_reply") // .add_attribute("job_id", value) .add_attribute("owner", owner) .add_attribute("account_address", sub_account_addr) diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index a60490e5..800b932c 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -5,13 +5,13 @@ use cosmwasm_std::{ use crate::{ error::map_contract_error, - state::{JobQueue, ACCOUNTS, STATE}, + state::{JobQueue, JOB_ACCOUNT_TRACKERS, LEGACY_ACCOUNTS, STATE}, util::{ + legacy_account::is_legacy_account, msg::{ build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_occupy_sub_account_msg, build_transfer_native_funds_msg, + build_occupy_account_msg, build_transfer_native_funds_msg, }, - sub_account::is_sub_account, }, ContractError, }; @@ -51,9 +51,7 @@ pub fn execute_job( finished_job.reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); let fee_plus_reward = fee + finished_job.reward; - let main_account_addr = ACCOUNTS() - .load(deps.storage, finished_job.owner.clone())? - .account; + let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, finished_job.owner.clone())?; let job_account_addr = finished_job.account.clone(); let job_account_amount = deps @@ -203,16 +201,19 @@ pub fn execute_job( } if recurring_job_created { - if is_sub_account(&main_account_addr, &job_account_addr) { + if !is_legacy_account(legacy_account, job_account_addr.clone()) { + // For job not using legacy account, job owner must already have account tracker instantiated + let job_account_tracker = + JOB_ACCOUNT_TRACKERS.load(deps.storage, &finished_job.owner)?; // Occupy sub account with the new job - msgs.push(build_occupy_sub_account_msg( - main_account_addr.to_string(), + msgs.push(build_occupy_account_msg( + job_account_tracker.to_string(), job_account_addr.to_string(), new_job_id, )); } } else { - // No new job created, sub account has been free in execute_job, no need to free here again + // No new job created, account has been free in execute_job, no need to free here again // Job owner withdraw all assets that are listed from warp account to itself msgs.push(build_account_withdraw_assets_msg( job_account_addr.clone().to_string(), diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 474a3a5f..17cf2d4a 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -1,8 +1,8 @@ use cosmwasm_std::{Addr, DepsMut, Env, Uint128, Uint64}; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; use controller::{ - account::MainAccount, + account::LegacyAccount, job::{Job, JobStatus, UpdateJobMsg}, Config, State, }; @@ -53,25 +53,35 @@ pub fn FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, Job, JobIndexes<'a>> { IndexedMap::new("finished_jobs_v3", indexes) } -pub struct MainAccountIndexes<'a> { - pub account: UniqueIndex<'a, Addr, MainAccount>, +pub struct LegacyAccountIndexes<'a> { + pub account: UniqueIndex<'a, Addr, LegacyAccount>, } -impl IndexList for MainAccountIndexes<'_> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.account]; +impl IndexList for LegacyAccountIndexes<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.account]; Box::new(v.into_iter()) } } +// !!! DEPRECATED !!! +// LEGACY_ACCOUNTS stores legacy account (all user's jobs share the same account, this is the old way of doing things before introducing job account) +// As of late 2023, we introduced job account meaning each job will have its own account, no more job sharing the same account +// We keep this for backward compatibility so user can withdraw from their old accounts #[allow(non_snake_case)] -pub fn ACCOUNTS<'a>() -> IndexedMap<'a, Addr, MainAccount, MainAccountIndexes<'a>> { - let indexes = MainAccountIndexes { +pub fn LEGACY_ACCOUNTS<'a>() -> IndexedMap<'a, Addr, LegacyAccount, LegacyAccountIndexes<'a>> { + let indexes = LegacyAccountIndexes { account: UniqueIndex::new(|account| account.account.clone(), "accounts__account"), }; IndexedMap::new("accounts", indexes) } +// ACCOUNTS_TRACKER stores account tracker, this is the successor of ACCOUNTS +// Key is user address, value is address of job account tracker contract +// By querying each user's account tracker contract, we know all accounts owned by that user and each account's availability +// For more detail, please refer to account tracker contract +pub const JOB_ACCOUNT_TRACKERS: Map<&Addr, Addr> = Map::new("job_job_account_trackers"); + pub const QUERY_PAGE_SIZE: u32 = 50; pub const CONFIG: Item = Item::new("config"); pub const STATE: Item = Item::new("state"); diff --git a/contracts/warp-controller/src/util/legacy_account.rs b/contracts/warp-controller/src/util/legacy_account.rs new file mode 100644 index 00000000..86da41ec --- /dev/null +++ b/contracts/warp-controller/src/util/legacy_account.rs @@ -0,0 +1,8 @@ +use controller::account::LegacyAccount; +use cosmwasm_std::Addr; + +pub fn is_legacy_account(legacy_account: Option, job_account_addr: Addr) -> bool { + legacy_account.map_or(false, |legacy_account| { + legacy_account.account == job_account_addr + }) +} diff --git a/contracts/warp-controller/src/util/mod.rs b/contracts/warp-controller/src/util/mod.rs index f7208bba..5a7c7196 100644 --- a/contracts/warp-controller/src/util/mod.rs +++ b/contracts/warp-controller/src/util/mod.rs @@ -1,4 +1,4 @@ pub(crate) mod fee; pub(crate) mod filter; +pub(crate) mod legacy_account; pub(crate) mod msg; -pub(crate) mod sub_account; diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 38a4b019..01ba22cf 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -1,43 +1,33 @@ use cosmwasm_std::{to_binary, BankMsg, Coin, CosmosMsg, Uint128, Uint64, WasmMsg}; -use account::{FreeSubAccountMsg, GenericMsg, OccupySubAccountMsg, WithdrawAssetsMsg}; use controller::account::{AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; +use job_account::{GenericMsg, WithdrawAssetsMsg}; +use job_account_tracker::{FreeAccountMsg, OccupyAccountMsg}; -pub fn build_instantiate_warp_main_account_msg( - job_id: Uint64, +pub fn build_instantiate_warp_job_account_tracker_msg( admin_addr: String, code_id: u64, account_owner: String, - native_funds: Vec, - cw_funds: Option>, - msgs: Option>, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(admin_addr), code_id, - msg: to_binary(&account::InstantiateMsg { + msg: to_binary(&job_account_tracker::InstantiateMsg { owner: account_owner.clone(), - job_id, - is_sub_account: false, - main_account_addr: None, - native_funds: native_funds.clone(), - cw_funds: cw_funds.unwrap_or(vec![]), - msgs: msgs.unwrap_or(vec![]), }) .unwrap(), - // Only send native funds to sub account funds: vec![], - label: format!("warp main account, owner: {}", account_owner), + label: format!("warp account tracker, owner: {}", account_owner), }) } #[allow(clippy::too_many_arguments)] -pub fn build_instantiate_warp_sub_account_msg( +pub fn build_instantiate_warp_account_msg( job_id: Uint64, admin_addr: String, code_id: u64, account_owner: String, - main_account_addr: String, + job_account_tracker_addr: String, native_funds: Vec, cw_funds: Option>, msgs: Option>, @@ -45,48 +35,41 @@ pub fn build_instantiate_warp_sub_account_msg( CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(admin_addr), code_id, - msg: to_binary(&account::InstantiateMsg { + msg: to_binary(&job_account::InstantiateMsg { owner: account_owner.clone(), job_id, - is_sub_account: true, - main_account_addr: Some(main_account_addr.clone()), + job_account_tracker_addr, native_funds: native_funds.clone(), cw_funds: cw_funds.unwrap_or(vec![]), msgs: msgs.unwrap_or(vec![]), }) .unwrap(), funds: native_funds, - label: format!( - "warp sub account, main account: {}, owner: {}", - main_account_addr, account_owner, - ), + label: format!("warp account, owner: {}", account_owner,), }) } -pub fn build_free_sub_account_msg( - main_account_addr: String, - sub_account_addr: String, -) -> CosmosMsg { +pub fn build_free_account_msg(job_account_tracker_addr: String, account_addr: String) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: main_account_addr, - msg: to_binary(&account::ExecuteMsg::FreeSubAccount(FreeSubAccountMsg { - sub_account_addr, - })) + contract_addr: job_account_tracker_addr, + msg: to_binary(&job_account_tracker::ExecuteMsg::FreeAccount( + FreeAccountMsg { account_addr }, + )) .unwrap(), funds: vec![], }) } -pub fn build_occupy_sub_account_msg( - main_account_addr: String, - sub_account_addr: String, +pub fn build_occupy_account_msg( + job_account_tracker_addr: String, + account_addr: String, job_id: Uint64, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: main_account_addr, - msg: to_binary(&account::ExecuteMsg::OccupySubAccount( - OccupySubAccountMsg { - sub_account_addr, + contract_addr: job_account_tracker_addr, + msg: to_binary(&job_account_tracker::ExecuteMsg::OccupyAccount( + OccupyAccountMsg { + account_addr, job_id, }, )) @@ -145,7 +128,7 @@ pub fn build_account_execute_generic_msgs( ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: account_addr, - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msg: to_binary(&job_account::ExecuteMsg::Generic(GenericMsg { msgs: cosmos_msgs_for_account_to_execute, })) .unwrap(), @@ -159,9 +142,11 @@ pub fn build_account_withdraw_assets_msg( ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: account_addr, - msg: to_binary(&account::ExecuteMsg::WithdrawAssets(WithdrawAssetsMsg { - asset_infos: assets_to_withdraw, - })) + msg: to_binary(&job_account::ExecuteMsg::WithdrawAssets( + WithdrawAssetsMsg { + asset_infos: assets_to_withdraw, + }, + )) .unwrap(), funds: vec![], }) diff --git a/contracts/warp-controller/src/util/sub_account.rs b/contracts/warp-controller/src/util/sub_account.rs deleted file mode 100644 index 3e9a7070..00000000 --- a/contracts/warp-controller/src/util/sub_account.rs +++ /dev/null @@ -1,5 +0,0 @@ -use cosmwasm_std::Addr; - -pub fn is_sub_account(main_account_addr: &Addr, job_account_addr: &Addr) -> bool { - main_account_addr != job_account_addr -} diff --git a/contracts/warp-account/.cargo/config b/contracts/warp-job-account-tracker/.cargo/config similarity index 100% rename from contracts/warp-account/.cargo/config rename to contracts/warp-job-account-tracker/.cargo/config diff --git a/contracts/warp-account/.gitignore b/contracts/warp-job-account-tracker/.gitignore similarity index 100% rename from contracts/warp-account/.gitignore rename to contracts/warp-job-account-tracker/.gitignore diff --git a/contracts/warp-job-account-tracker/Cargo.toml b/contracts/warp-job-account-tracker/Cargo.toml new file mode 100644 index 00000000..ac23a59f --- /dev/null +++ b/contracts/warp-job-account-tracker/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "warp-job-account-tracker" +version = "0.1.0" +authors = ["Terra Money "] +edition = "2021" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[package.metadata.scripts] +optimize = """docker run --rm -v "${process.cwd()}":/code \ + -v "${path.join(process.cwd(), "../../", "packages")}":/packages \ + --mount type=volume,source="${contract}_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/rust-optimizer${process.env.TERRARIUM_ARCH_ARM64 ? "-arm64" : ""}:0.12.6 +""" + +[dependencies] +cosmwasm-std = "1.1" +cosmwasm-storage = "1.1" +cosmwasm-schema = "1.1" +base64 = "0.13.0" +cw-asset = "2.2" +cw-storage-plus = "0.16" +cw2 = "0.16" +cw20 = "0.16" +cw721 = "0.16.0" +cw-utils = "0.16" +job-account-tracker = { path = "../../packages/job-account-tracker", default-features = false, version = "*" } +schemars = "0.8" +thiserror = "1" +serde-json-wasm = "0.4.1" +json-codec-wasm = "0.1.0" +prost = "0.11.9" + +[dev-dependencies] +cw-multi-test = "0.16.0" +anyhow = "1.0.71" diff --git a/contracts/warp-account/README.md b/contracts/warp-job-account-tracker/README.md similarity index 100% rename from contracts/warp-account/README.md rename to contracts/warp-job-account-tracker/README.md diff --git a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs new file mode 100644 index 00000000..f699fa0e --- /dev/null +++ b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs @@ -0,0 +1,17 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; +use job_account_tracker::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(Config), &out_dir); +} diff --git a/contracts/warp-account/meta/README.md b/contracts/warp-job-account-tracker/meta/README.md similarity index 100% rename from contracts/warp-account/meta/README.md rename to contracts/warp-job-account-tracker/meta/README.md diff --git a/contracts/warp-account/meta/appveyor.yml b/contracts/warp-job-account-tracker/meta/appveyor.yml similarity index 100% rename from contracts/warp-account/meta/appveyor.yml rename to contracts/warp-job-account-tracker/meta/appveyor.yml diff --git a/contracts/warp-account/meta/test_generate.sh b/contracts/warp-job-account-tracker/meta/test_generate.sh similarity index 100% rename from contracts/warp-account/meta/test_generate.sh rename to contracts/warp-job-account-tracker/meta/test_generate.sh diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs new file mode 100644 index 00000000..84189098 --- /dev/null +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -0,0 +1,74 @@ +use crate::state::CONFIG; +use crate::{execute, query, ContractError}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, +}; +use cw_utils::nonpayable; +use job_account_tracker::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let instantiated_account_addr = env.contract.address; + + CONFIG.save( + deps.storage, + &Config { + owner: deps.api.addr_validate(&msg.owner)?, + creator_addr: info.sender, + }, + )?; + + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute("contract_addr", instantiated_account_addr.clone()) + .add_attribute("owner", msg.owner)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + _env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner && info.sender != config.creator_addr { + return Err(ContractError::Unauthorized {}); + } + match msg { + ExecuteMsg::OccupyAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::occupy_account(deps, data) + } + ExecuteMsg::FreeAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::free_account(deps, data) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), + QueryMsg::QueryOccupiedAccounts(data) => { + to_binary(&query::account::query_occupied_accounts(deps, data)?) + } + QueryMsg::QueryFreeAccounts(data) => { + to_binary(&query::account::query_free_accounts(deps, data)?) + } + QueryMsg::QueryFirstFreeAccount(_) => { + to_binary(&query::account::query_first_free_account(deps)?) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Response::new()) +} diff --git a/contracts/warp-job-account-tracker/src/error.rs b/contracts/warp-job-account-tracker/src/error.rs new file mode 100644 index 00000000..0eefe76a --- /dev/null +++ b/contracts/warp-job-account-tracker/src/error.rs @@ -0,0 +1,73 @@ +use crate::ContractError::{DecodeError, DeserializationError, SerializationError}; +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Invalid fee")] + InvalidFee {}, + + #[error("Funds array in message does not match funds array in job.")] + FundsMismatch {}, + + #[error("Reward provided is smaller than minimum")] + RewardTooSmall {}, + + #[error("Invalid arguments")] + InvalidArguments {}, + + #[error("Custom Error val: {val:?}")] + CustomError { val: String }, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. + #[error("Error deserializing data")] + DeserializationError {}, + + #[error("Error serializing data")] + SerializationError {}, + + #[error("Error decoding JSON result")] + DecodeError {}, + + #[error("Error resolving JSON path")] + ResolveError {}, + + #[error("Sub account already occupied")] + AccountAlreadyOccupiedError {}, + + #[error("Sub account already free")] + AccountAlreadyFreeError {}, + + #[error("Sub account should be occupied but it is free")] + AccountNotOccupiedError {}, +} + +impl From for ContractError { + fn from(_: serde_json_wasm::de::Error) -> Self { + DeserializationError {} + } +} + +impl From for ContractError { + fn from(_: serde_json_wasm::ser::Error) -> Self { + SerializationError {} + } +} + +impl From for ContractError { + fn from(_: json_codec_wasm::DecodeError) -> Self { + DecodeError {} + } +} + +impl From for ContractError { + fn from(_: base64::DecodeError) -> Self { + DecodeError {} + } +} diff --git a/contracts/warp-job-account-tracker/src/execute/account.rs b/contracts/warp-job-account-tracker/src/execute/account.rs new file mode 100644 index 00000000..0c3d9933 --- /dev/null +++ b/contracts/warp-job-account-tracker/src/execute/account.rs @@ -0,0 +1,30 @@ +use crate::state::{FREE_ACCOUNTS, OCCUPIED_ACCOUNTS}; +use crate::ContractError; +use cosmwasm_std::{DepsMut, Response}; +use job_account_tracker::{FreeAccountMsg, OccupyAccountMsg}; + +pub fn occupy_account(deps: DepsMut, data: OccupyAccountMsg) -> Result { + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + FREE_ACCOUNTS.remove(deps.storage, account_addr_ref); + OCCUPIED_ACCOUNTS.update(deps.storage, account_addr_ref, |s| match s { + None => Ok(data.job_id), + Some(_) => Err(ContractError::AccountAlreadyOccupiedError {}), + })?; + Ok(Response::new() + .add_attribute("action", "occupy_account") + .add_attribute("account_addr", data.account_addr) + .add_attribute("job_id", data.job_id)) +} + +pub fn free_account(deps: DepsMut, data: FreeAccountMsg) -> Result { + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + OCCUPIED_ACCOUNTS.remove(deps.storage, account_addr_ref); + FREE_ACCOUNTS.update(deps.storage, account_addr_ref, |s| match s { + // value is a dummy data because there is no built in support for set in cosmwasm + None => Ok(true), + Some(_) => Err(ContractError::AccountAlreadyFreeError {}), + })?; + Ok(Response::new() + .add_attribute("action", "free_account") + .add_attribute("account_addr", data.account_addr)) +} diff --git a/contracts/warp-account/src/query/mod.rs b/contracts/warp-job-account-tracker/src/execute/mod.rs similarity index 100% rename from contracts/warp-account/src/query/mod.rs rename to contracts/warp-job-account-tracker/src/execute/mod.rs diff --git a/contracts/warp-job-account-tracker/src/integration_tests.rs b/contracts/warp-job-account-tracker/src/integration_tests.rs new file mode 100644 index 00000000..1605f0b9 --- /dev/null +++ b/contracts/warp-job-account-tracker/src/integration_tests.rs @@ -0,0 +1,349 @@ +#[cfg(test)] +mod tests { + use anyhow::Result as AnyResult; + use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; + use cw_multi_test::{App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor}; + use job_account_tracker::{ + Account, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FirstFreeAccountResponse, + FreeAccountMsg, InstantiateMsg, OccupyAccountMsg, QueryConfigMsg, QueryFirstFreeAccountMsg, + QueryFreeAccountsMsg, QueryMsg, QueryOccupiedAccountsMsg, + }; + + use crate::{ + contract::{execute, instantiate, query}, + ContractError, + }; + + const DUMMY_WARP_CONTROLLER_ADDR: &str = "terra1"; + const USER_1: &str = "terra2"; + const DUMMY_WARP_ACCOUNT_1_ADDR: &str = "terra3"; + const DUMMY_WARP_ACCOUNT_2_ADDR: &str = "terra4"; + const DUMMY_WARP_ACCOUNT_3_ADDR: &str = "terra5"; + const DUMMY_JOB_1_ID: Uint64 = Uint64::zero(); + const DUMMY_JOB_2_ID: Uint64 = Uint64::one(); + + fn mock_app() -> App { + AppBuilder::new().build(|router, _, storage| { + router + .bank + .init_balance( + storage, + &Addr::unchecked(USER_1), + vec![Coin { + denom: "uluna".to_string(), + // 1_000_000_000 uLuna i.e. 1k LUNA since 1 LUNA = 1_000_000 uLuna + amount: Uint128::new(1_000_000_000), + }], + ) + .unwrap(); + }) + } + + fn contract_warp_job_account_tracker() -> Box> { + let contract = ContractWrapper::new(execute, instantiate, query); + Box::new(contract) + } + + fn init_warp_job_account_tracker( + app: &mut App, + warp_job_account_tracker_contract_code_id: u64, + ) -> Addr { + app.instantiate_contract( + warp_job_account_tracker_contract_code_id, + Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + &InstantiateMsg { + owner: USER_1.to_string(), + }, + &[], + "warp_job_account_tracker", + None, + ) + .unwrap() + } + + fn assert_err(res: AnyResult, err: ContractError) { + match res { + Ok(_) => panic!("Result was not an error"), + Err(generic_err) => { + let contract_err: ContractError = generic_err.downcast().unwrap(); + assert_eq!(contract_err, err); + } + } + } + + #[test] + fn warp_job_account_tracker_contract_multi_test_account_management() { + let mut app = mock_app(); + let warp_job_account_tracker_contract_code_id = + app.store_code(contract_warp_job_account_tracker()); + + // Instantiate account + let warp_job_account_tracker_contract_addr = + init_warp_job_account_tracker(&mut app, warp_job_account_tracker_contract_code_id); + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryConfig(QueryConfigMsg {}) + ), + Ok(ConfigResponse { + config: Config { + owner: Addr::unchecked(USER_1), + creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + } + }) + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryFirstFreeAccount(QueryFirstFreeAccountMsg {}) + ), + Ok(FirstFreeAccountResponse { account: None }) + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(AccountsResponse { + accounts: vec![], + total_count: 0 + }) + ); + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(AccountsResponse { + accounts: vec![], + total_count: 0 + }) + ); + + // Mark first account as free + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_job_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), + }), + &[], + ); + + // Cannot free account twice + assert_err( + app.execute_contract( + Addr::unchecked(USER_1), + warp_job_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), + }), + &[], + ), + ContractError::AccountAlreadyFreeError {}, + ); + + // Mark second account as free + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_job_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + }), + &[], + ); + + // Mark third account as free + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_job_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_addr: DUMMY_WARP_ACCOUNT_3_ADDR.to_string(), + }), + &[], + ); + + // Query first free account + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryFirstFreeAccount(QueryFirstFreeAccountMsg {}) + ), + Ok(FirstFreeAccountResponse { + account: Some(Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + occupied_by_job_id: None + }) + }) + ); + + // Query free accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(AccountsResponse { + accounts: vec![ + Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), + occupied_by_job_id: None + }, + Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), + occupied_by_job_id: None + }, + Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + occupied_by_job_id: None + } + ], + total_count: 3 + }) + ); + + // Query occupied accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(AccountsResponse { + accounts: vec![], + total_count: 0 + }) + ); + + // Occupy second account with job 1 + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_job_account_tracker_contract_addr.clone(), + &ExecuteMsg::OccupyAccount(OccupyAccountMsg { + account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + job_id: DUMMY_JOB_1_ID, + }), + &[], + ); + + // Cannot occupy account twice + assert_err( + app.execute_contract( + Addr::unchecked(USER_1), + warp_job_account_tracker_contract_addr.clone(), + &ExecuteMsg::OccupyAccount(OccupyAccountMsg { + account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + job_id: DUMMY_JOB_2_ID, + }), + &[], + ), + ContractError::AccountAlreadyOccupiedError {}, + ); + + // Query free accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(AccountsResponse { + accounts: vec![ + Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), + occupied_by_job_id: None + }, + Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + occupied_by_job_id: None + } + ], + total_count: 2 + }) + ); + + // Query occupied accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(AccountsResponse { + accounts: vec![Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), + occupied_by_job_id: Some(DUMMY_JOB_1_ID) + },], + total_count: 1 + }) + ); + + // Free second account + let _ = app.execute_contract( + Addr::unchecked(USER_1), + warp_job_account_tracker_contract_addr.clone(), + &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + }), + &[], + ); + + // Query free accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(AccountsResponse { + accounts: vec![ + Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), + occupied_by_job_id: None + }, + Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), + occupied_by_job_id: None + }, + Account { + addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + occupied_by_job_id: None + } + ], + total_count: 3 + }) + ); + + // Query occupied accounts + assert_eq!( + app.wrap().query_wasm_smart( + warp_job_account_tracker_contract_addr.clone(), + &QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + start_after: None, + limit: None + }) + ), + Ok(AccountsResponse { + accounts: vec![], + total_count: 0 + }) + ); + } +} diff --git a/contracts/warp-account/src/lib.rs b/contracts/warp-job-account-tracker/src/lib.rs similarity index 85% rename from contracts/warp-account/src/lib.rs rename to contracts/warp-job-account-tracker/src/lib.rs index 7511e0ce..71a7ed70 100644 --- a/contracts/warp-account/src/lib.rs +++ b/contracts/warp-job-account-tracker/src/lib.rs @@ -6,7 +6,5 @@ pub mod state; #[cfg(test)] mod integration_tests; -#[cfg(test)] -mod tests; pub use crate::error::ContractError; diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-job-account-tracker/src/query/account.rs new file mode 100644 index 00000000..85ed6937 --- /dev/null +++ b/contracts/warp-job-account-tracker/src/query/account.rs @@ -0,0 +1,88 @@ +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::Bound; + +use crate::state::{CONFIG, FREE_ACCOUNTS, OCCUPIED_ACCOUNTS}; + +use job_account_tracker::{ + Account, AccountsResponse, ConfigResponse, FirstFreeAccountResponse, QueryFreeAccountsMsg, + QueryOccupiedAccountsMsg, +}; + +const QUERY_LIMIT: u32 = 50; + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { config }) +} + +pub fn query_first_free_account(deps: Deps) -> StdResult { + match FREE_ACCOUNTS + .range(deps.storage, None, None, Order::Ascending) + .next() + { + Some(free_account) => Ok(FirstFreeAccountResponse { + account: Some(Account { + addr: free_account.unwrap().0, + occupied_by_job_id: None, + }), + }), + None => Ok(FirstFreeAccountResponse { account: None }), + } +} + +pub fn query_occupied_accounts( + deps: Deps, + data: QueryOccupiedAccountsMsg, +) -> StdResult { + let iter = match data.start_after { + Some(start_after) => OCCUPIED_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive( + &deps.api.addr_validate(start_after.as_str()).unwrap(), + )), + None, + Order::Descending, + ), + None => OCCUPIED_ACCOUNTS.range(deps.storage, None, None, Order::Descending), + }; + let accounts = iter + .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) + .map(|item| { + item.map(|(account_addr, job_id)| Account { + addr: account_addr, + occupied_by_job_id: Some(job_id), + }) + }) + .collect::>>()?; + Ok(AccountsResponse { + total_count: accounts.len(), + accounts, + }) +} + +pub fn query_free_accounts(deps: Deps, data: QueryFreeAccountsMsg) -> StdResult { + let iter = match data.start_after { + Some(start_after) => FREE_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive( + &deps.api.addr_validate(start_after.as_str()).unwrap(), + )), + None, + Order::Descending, + ), + None => FREE_ACCOUNTS.range(deps.storage, None, None, Order::Descending), + }; + let accounts = iter + .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) + .map(|item| { + item.map(|(account_addr, _)| Account { + addr: account_addr, + occupied_by_job_id: None, + }) + }) + .collect::>>()?; + Ok(AccountsResponse { + total_count: accounts.len(), + accounts, + }) +} diff --git a/contracts/warp-job-account-tracker/src/query/mod.rs b/contracts/warp-job-account-tracker/src/query/mod.rs new file mode 100644 index 00000000..d937534a --- /dev/null +++ b/contracts/warp-job-account-tracker/src/query/mod.rs @@ -0,0 +1 @@ +pub(crate) mod account; diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-job-account-tracker/src/state.rs new file mode 100644 index 00000000..83f700a8 --- /dev/null +++ b/contracts/warp-job-account-tracker/src/state.rs @@ -0,0 +1,13 @@ +use cosmwasm_std::{Addr, Uint64}; +use cw_storage_plus::{Item, Map}; +use job_account_tracker::Config; + +pub const CONFIG: Item = Item::new("config"); + +// OCCUPIED_ACCOUNTS only has value when current account is a main account +// Key is the account address, value is the ID of the pending job currently using it +pub const OCCUPIED_ACCOUNTS: Map<&Addr, Uint64> = Map::new("occupied_accounts"); + +// FREE_ACCOUNTS only has value when current account is a main account +// Key is the account address, value is a dummy data that is always true to make it behave like a set +pub const FREE_ACCOUNTS: Map<&Addr, bool> = Map::new("free_accounts"); diff --git a/contracts/warp-job-account/.cargo/config b/contracts/warp-job-account/.cargo/config new file mode 100644 index 00000000..f4940a9d --- /dev/null +++ b/contracts/warp-job-account/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example warp-account-schema" diff --git a/contracts/warp-job-account/.gitignore b/contracts/warp-job-account/.gitignore new file mode 100644 index 00000000..9095deaa --- /dev/null +++ b/contracts/warp-job-account/.gitignore @@ -0,0 +1,16 @@ +# Build results +/target +/schema + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/warp-account/Cargo.toml b/contracts/warp-job-account/Cargo.toml similarity index 92% rename from contracts/warp-account/Cargo.toml rename to contracts/warp-job-account/Cargo.toml index 94cd2c23..9cefbc9d 100644 --- a/contracts/warp-account/Cargo.toml +++ b/contracts/warp-job-account/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "warp-account" +name = "warp-job-account" version = "0.1.0" authors = ["Terra Money "] edition = "2021" @@ -41,7 +41,7 @@ cw20 = "0.16" cw721 = "0.16.0" cw-utils = "0.16" controller = { path = "../../packages/controller", default-features = false, version = "*" } -account = { path = "../../packages/account", default-features = false, version = "*" } +job-account = { path = "../../packages/job-account", default-features = false, version = "*" } schemars = "0.8" thiserror = "1" serde-json-wasm = "0.4.1" diff --git a/packages/account/README.md b/contracts/warp-job-account/README.md similarity index 100% rename from packages/account/README.md rename to contracts/warp-job-account/README.md diff --git a/contracts/warp-account/examples/warp-account-schema.rs b/contracts/warp-job-account/examples/warp-job-account-schema.rs similarity index 52% rename from contracts/warp-account/examples/warp-account-schema.rs rename to contracts/warp-job-account/examples/warp-job-account-schema.rs index 8bd29135..85c750f5 100644 --- a/contracts/warp-account/examples/warp-account-schema.rs +++ b/contracts/warp-job-account/examples/warp-job-account-schema.rs @@ -1,12 +1,8 @@ use std::env::current_dir; use std::fs::create_dir_all; -use account::{Config, ExecuteMsg, InstantiateMsg}; -use controller::{ - account::{MainAccountResponse, MainAccountsResponse}, - job::{JobResponse, JobsResponse}, -}; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; +use job_account::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; fn main() { let mut out_dir = current_dir().unwrap(); @@ -16,9 +12,6 @@ fn main() { export_schema(&schema_for!(InstantiateMsg), &out_dir); export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(Config), &out_dir); - export_schema(&schema_for!(JobResponse), &out_dir); - export_schema(&schema_for!(JobsResponse), &out_dir); - export_schema(&schema_for!(MainAccountResponse), &out_dir); - export_schema(&schema_for!(MainAccountsResponse), &out_dir); } diff --git a/contracts/warp-job-account/meta/README.md b/contracts/warp-job-account/meta/README.md new file mode 100644 index 00000000..279d1db4 --- /dev/null +++ b/contracts/warp-job-account/meta/README.md @@ -0,0 +1,16 @@ +# The meta folder + +This folder is ignored via the `.genignore` file. It contains meta files +that should not make it into the generated project. + +In particular, it is used for an AppVeyor CI script that runs on `cw-template` +itself (running the cargo-generate script, then testing the generated project). +The `.circleci` and `.github` directories contain scripts destined for any projects created from +this template. + +## Files + +- `appveyor.yml`: The AppVeyor CI configuration +- `test_generate.sh`: A script for generating a project from the template and + runnings builds and tests in it. This works almost like the CI script but + targets local UNIX-like dev environments. diff --git a/contracts/warp-job-account/meta/appveyor.yml b/contracts/warp-job-account/meta/appveyor.yml new file mode 100644 index 00000000..5da37f70 --- /dev/null +++ b/contracts/warp-job-account/meta/appveyor.yml @@ -0,0 +1,61 @@ +# This CI configuration tests the cw-template repository itself, +# not the resulting project. We want to ensure that +# 1. the template to project generation works +# 2. the template files are up to date +# +# We chose Appveyor for this task as it allows us to use an arbitrary config +# location. Furthermore it allows us to ship Circle CI and GitHub Actions configs +# generated for the resulting project. + +image: Ubuntu + +environment: + TOOLCHAIN: 1.58.1 + +services: + - docker + +cache: + - $HOME/.rustup/ -> meta/appveyor.yml + # For details about cargo caching see https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci + - $HOME/.cargo/bin/ -> meta/appveyor.yml + - $HOME/.cargo/registry/index/ -> meta/appveyor.yml + - $HOME/.cargo/registry/cache/ -> meta/appveyor.yml + - $HOME/.cargo/git/db/ -> meta/appveyor.yml + +install: + - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain "$TOOLCHAIN" -y + - source $HOME/.cargo/env + - rustc --version + - cargo --version + - rustup target add wasm32-unknown-unknown + - cargo install --features vendored-openssl cargo-generate || true + +build_script: + # No matter what is currently checked out by the CI (main, other branch, PR merge commit), + # we create a temporary local branch from that point with a constant name, which we need for + # cargo generate. + - git branch current-ci-checkout + - cd .. + - cargo generate --git cw-template --name testgen-ci --branch current-ci-checkout + - cd testgen-ci + - ls -lA + - cargo fmt -- --check + - cargo unit-test + - cargo wasm + - cargo schema + - docker build --pull -t "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" . + - \[ "${APPVEYOR_REPO_BRANCH}" = "main" \] && image_tag=latest || image_tag=${APPVEYOR_REPO_TAG_NAME} + - docker tag "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" "cosmwasm/cw-gitpod-base:${image_tag}" + +on_success: + # publish docker image + - docker login --password-stdin -u "$DOCKER_USER" <<<"$DOCKER_PASS" + - docker push + - docker logout + +branches: +# whitelist long living branches and tags + only: + # - main + - /v\d+\.\d+\.\d+/ diff --git a/contracts/warp-job-account/meta/test_generate.sh b/contracts/warp-job-account/meta/test_generate.sh new file mode 100644 index 00000000..b9aaa237 --- /dev/null +++ b/contracts/warp-job-account/meta/test_generate.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -o errexit -o nounset -o pipefail +command -v shellcheck > /dev/null && shellcheck "$0" + +REPO_ROOT="$(realpath "$(dirname "$0")/..")" + +TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/cw-template.XXXXXXXXX") +PROJECT_NAME="testgen-local" + +( + echo "Navigating to $TMP_DIR" + cd "$TMP_DIR" + + GIT_BRANCH=$(git -C "$REPO_ROOT" branch --show-current) + + echo "Generating project from local repository (branch $GIT_BRANCH) ..." + cargo generate --git "$REPO_ROOT" --name "$PROJECT_NAME" --branch "$GIT_BRANCH" + + ( + cd "$PROJECT_NAME" + echo "This is what was generated" + ls -lA + + # Check formatting + echo "Checking formatting ..." + cargo fmt -- --check + + # Debug builds first to fail fast + echo "Running unit tests ..." + cargo unit-test + echo "Creating schema ..." + cargo schema + + echo "Building wasm ..." + cargo wasm + ) +) diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-job-account/src/contract.rs similarity index 57% rename from contracts/warp-account/src/contract.rs rename to contracts/warp-job-account/src/contract.rs index be8df853..046c0d48 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-job-account/src/contract.rs @@ -1,10 +1,10 @@ use crate::state::CONFIG; use crate::{execute, query, ContractError}; -use account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, SubAccountConfig}; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; use cw_utils::nonpayable; +use job_account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -20,35 +20,16 @@ pub fn instantiate( &Config { owner: deps.api.addr_validate(&msg.owner)?, creator_addr: info.sender, - account_addr: instantiated_account_addr.clone(), - sub_account_config: if msg.is_sub_account { - Some(SubAccountConfig { - main_account_addr: deps - .api - .addr_validate(&msg.main_account_addr.clone().unwrap())?, - occupied_by_job_id: None, - }) - } else { - None - }, + job_account_tracker_addr: deps.api.addr_validate(&msg.job_account_tracker_addr)?, }, )?; Ok(Response::new() - .add_messages(if msg.is_sub_account { - msg.msgs.clone() - } else { - vec![] - }) + .add_messages(msg.msgs.clone()) .add_attribute("action", "instantiate") .add_attribute("job_id", msg.job_id) - .add_attribute("contract_addr", instantiated_account_addr.clone()) - .add_attribute("is_sub_account", format!("{}", msg.is_sub_account)) - .add_attribute( - "main_account_addr", - msg.main_account_addr - .unwrap_or(instantiated_account_addr.to_string()), - ) + .add_attribute("contract_addr", instantiated_account_addr) + .add_attribute("job_account_tracker_addr", msg.job_account_tracker_addr) .add_attribute("owner", msg.owner) .add_attribute( "native_funds", @@ -78,31 +59,13 @@ pub fn execute( execute::withdraw::withdraw_assets(deps, env, data, config) } ExecuteMsg::IbcTransfer(data) => execute::ibc::ibc_transfer(env, data), - ExecuteMsg::OccupySubAccount(data) => { - nonpayable(&info).unwrap(); - execute::account::occupy_sub_account(deps, env, data) - } - ExecuteMsg::FreeSubAccount(data) => { - nonpayable(&info).unwrap(); - execute::account::free_sub_account(deps, env, data) - } } } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - let config = CONFIG.load(deps.storage)?; match msg { - QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(config)?), - QueryMsg::QueryOccupiedSubAccounts(data) => to_binary( - &query::account::query_occupied_sub_accounts(deps, data, config)?, - ), - QueryMsg::QueryFreeSubAccounts(data) => to_binary( - &query::account::query_free_sub_accounts(deps, data, config)?, - ), - QueryMsg::QueryFirstFreeSubAccount(_) => { - to_binary(&query::account::query_first_free_sub_account(deps, config)?) - } + QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), } } diff --git a/contracts/warp-account/src/error.rs b/contracts/warp-job-account/src/error.rs similarity index 100% rename from contracts/warp-account/src/error.rs rename to contracts/warp-job-account/src/error.rs diff --git a/contracts/warp-account/src/execute/ibc.rs b/contracts/warp-job-account/src/execute/ibc.rs similarity index 95% rename from contracts/warp-account/src/execute/ibc.rs rename to contracts/warp-job-account/src/execute/ibc.rs index 93aff4b6..7c7023af 100644 --- a/contracts/warp-account/src/execute/ibc.rs +++ b/contracts/warp-job-account/src/execute/ibc.rs @@ -1,7 +1,7 @@ use crate::ContractError; -use account::{IbcTransferMsg, TimeoutBlock}; use cosmwasm_std::CosmosMsg::Stargate; use cosmwasm_std::{Env, Response}; +use job_account::{IbcTransferMsg, TimeoutBlock}; use prost::Message; pub fn ibc_transfer(env: Env, data: IbcTransferMsg) -> Result { diff --git a/contracts/warp-account/src/execute/mod.rs b/contracts/warp-job-account/src/execute/mod.rs similarity index 65% rename from contracts/warp-account/src/execute/mod.rs rename to contracts/warp-job-account/src/execute/mod.rs index cbc6902a..2237e0b9 100644 --- a/contracts/warp-account/src/execute/mod.rs +++ b/contracts/warp-job-account/src/execute/mod.rs @@ -1,3 +1,2 @@ -pub(crate) mod account; pub(crate) mod ibc; pub(crate) mod withdraw; diff --git a/contracts/warp-account/src/execute/withdraw.rs b/contracts/warp-job-account/src/execute/withdraw.rs similarity index 98% rename from contracts/warp-account/src/execute/withdraw.rs rename to contracts/warp-job-account/src/execute/withdraw.rs index 1f3961b8..2cc9afc7 100644 --- a/contracts/warp-account/src/execute/withdraw.rs +++ b/contracts/warp-job-account/src/execute/withdraw.rs @@ -1,12 +1,13 @@ -use crate::ContractError; -use account::{Config, WithdrawAssetsMsg}; -use controller::account::{AssetInfo, Cw721ExecuteMsg}; use cosmwasm_std::{ to_binary, Addr, BankMsg, CosmosMsg, Deps, DepsMut, Env, Response, StdResult, Uint128, WasmMsg, }; use cw20::{BalanceResponse, Cw20ExecuteMsg}; use cw721::{Cw721QueryMsg, OwnerOfResponse}; +use crate::ContractError; +use controller::account::{AssetInfo, Cw721ExecuteMsg}; +use job_account::{Config, WithdrawAssetsMsg}; + pub fn withdraw_assets( deps: DepsMut, env: Env, diff --git a/contracts/warp-job-account/src/lib.rs b/contracts/warp-job-account/src/lib.rs new file mode 100644 index 00000000..aff04ae7 --- /dev/null +++ b/contracts/warp-job-account/src/lib.rs @@ -0,0 +1,10 @@ +pub mod contract; +mod error; +mod execute; +mod query; +pub mod state; + +#[cfg(test)] +mod tests; + +pub use crate::error::ContractError; diff --git a/contracts/warp-job-account/src/query/account.rs b/contracts/warp-job-account/src/query/account.rs new file mode 100644 index 00000000..98265862 --- /dev/null +++ b/contracts/warp-job-account/src/query/account.rs @@ -0,0 +1,8 @@ +use crate::state::CONFIG; +use cosmwasm_std::{Deps, StdResult}; +use job_account::ConfigResponse; + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { config }) +} diff --git a/contracts/warp-job-account/src/query/mod.rs b/contracts/warp-job-account/src/query/mod.rs new file mode 100644 index 00000000..d937534a --- /dev/null +++ b/contracts/warp-job-account/src/query/mod.rs @@ -0,0 +1 @@ +pub(crate) mod account; diff --git a/contracts/warp-job-account/src/state.rs b/contracts/warp-job-account/src/state.rs new file mode 100644 index 00000000..9fbff4cf --- /dev/null +++ b/contracts/warp-job-account/src/state.rs @@ -0,0 +1,5 @@ +use cw_storage_plus::Item; + +use job_account::Config; + +pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/warp-account/src/tests.rs b/contracts/warp-job-account/src/tests.rs similarity index 97% rename from contracts/warp-account/src/tests.rs rename to contracts/warp-job-account/src/tests.rs index 19f82980..6b928768 100644 --- a/contracts/warp-account/src/tests.rs +++ b/contracts/warp-job-account/src/tests.rs @@ -1,11 +1,11 @@ use crate::contract::{execute, instantiate}; use crate::ContractError; -use account::{ExecuteMsg, GenericMsg, InstantiateMsg}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, IbcTimeoutBlock, Response, StakingMsg, Uint128, Uint64, VoteOption, WasmMsg, }; +use job_account::{ExecuteMsg, GenericMsg, InstantiateMsg}; #[test] fn test_execute_controller() { @@ -20,8 +20,7 @@ fn test_execute_controller() { InstantiateMsg { owner: "vlad".to_string(), job_id: Uint64::zero(), - is_sub_account: false, - main_account_addr: None, + job_account_tracker_addr: "vlad".to_string(), native_funds: vec![], cw_funds: vec![], msgs: vec![], @@ -147,8 +146,7 @@ fn test_execute_owner() { InstantiateMsg { owner: "vlad".to_string(), job_id: Uint64::zero(), - is_sub_account: false, - main_account_addr: None, + job_account_tracker_addr: "vlad".to_string(), native_funds: vec![], cw_funds: vec![], msgs: vec![], @@ -276,8 +274,7 @@ fn test_execute_unauth() { InstantiateMsg { owner: "vlad".to_string(), job_id: Uint64::zero(), - is_sub_account: false, - main_account_addr: None, + job_account_tracker_addr: "vlad".to_string(), native_funds: vec![], cw_funds: vec![], msgs: vec![], diff --git a/contracts/warp-legacy-account/.cargo/config b/contracts/warp-legacy-account/.cargo/config new file mode 100644 index 00000000..f4940a9d --- /dev/null +++ b/contracts/warp-legacy-account/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example warp-account-schema" diff --git a/contracts/warp-legacy-account/.gitignore b/contracts/warp-legacy-account/.gitignore new file mode 100644 index 00000000..9095deaa --- /dev/null +++ b/contracts/warp-legacy-account/.gitignore @@ -0,0 +1,16 @@ +# Build results +/target +/schema + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/warp-legacy-account/Cargo.toml b/contracts/warp-legacy-account/Cargo.toml new file mode 100644 index 00000000..b4350b67 --- /dev/null +++ b/contracts/warp-legacy-account/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "warp-legacy-account" +version = "0.1.0" +authors = ["Terra Money "] +edition = "2021" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[package.metadata.scripts] +optimize = """docker run --rm -v "${process.cwd()}":/code \ + -v "${path.join(process.cwd(), "../../", "packages")}":/packages \ + --mount type=volume,source="${contract}_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/rust-optimizer${process.env.TERRARIUM_ARCH_ARM64 ? "-arm64" : ""}:0.12.6 +""" + +[dependencies] +cosmwasm-std = "1.1" +cosmwasm-storage = "1.1" +cosmwasm-schema = "1.1" +base64 = "0.13.0" +cw-asset = "2.2" +cw-storage-plus = "0.16" +cw2 = "0.16" +cw20 = "0.16" +cw721 = "0.16.0" +cw-utils = "0.16" +controller = { path = "../../packages/controller", default-features = false, version = "*" } +legacy-account = { path = "../../packages/legacy-account", default-features = false, version = "*" } +schemars = "0.8" +thiserror = "1" +serde-json-wasm = "0.4.1" +json-codec-wasm = "0.1.0" +prost = "0.11.9" + +[dev-dependencies] +cw-multi-test = "0.16.0" +anyhow = "1.0.71" diff --git a/contracts/warp-legacy-account/README.md b/contracts/warp-legacy-account/README.md new file mode 100644 index 00000000..954383af --- /dev/null +++ b/contracts/warp-legacy-account/README.md @@ -0,0 +1,106 @@ +# CosmWasm Starter Pack + +This is a template to build smart contracts in Rust to run inside a +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. +To understand the framework better, please read the overview in the +[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), +and dig into the [cosmwasm docs](https://www.cosmwasm.com). +This assumes you understand the theory and just want to get coding. + +## Creating a new repo from template + +Assuming you have a recent version of rust and cargo (v1.58.1+) installed +(via [rustup](https://rustup.rs/)), +then the following should get you a new repo to start a contract: + +Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. +Unless you did that before, run this line now: + +```sh +cargo install cargo-generate --features vendored-openssl +cargo install cargo-run-script +``` + +Now, use it to create your new contract. +Go to the folder in which you want to place it and run: + + +**Latest: 1.0.0-beta6** + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME +```` + +**Older Version** + +Pass version as branch flag: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME +```` + +Example: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME +``` + +You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) +containing a simple working contract and build system that you can customize. + +## Create a Repo + +After generating, you have a initialized local git repo, but no commits, and no remote. +Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). +Then run the following: + +```sh +# this is needed to create a valid Cargo.lock file (see below) +cargo check +git branch -M main +git add . +git commit -m 'Initial Commit' +git remote add origin YOUR-GIT-URL +git push -u origin main +``` + +## CI Support + +We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) +and [Circle CI](.circleci/config.yml) in the generated project, so you can +get up and running with CI right away. + +One note is that the CI runs all `cargo` commands +with `--locked` to ensure it uses the exact same versions as you have locally. This also means +you must have an up-to-date `Cargo.lock` file, which is not auto-generated. +The first time you set up the project (or after adding any dep), you should ensure the +`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by +running `cargo check` or `cargo unit-test`. + +## Using your project + +Once you have your custom repo, you should check out [Developing](./Developing.md) to explain +more on how to run tests and develop code. Or go through the +[online tutorial](https://docs.cosmwasm.com/) to get a better feel +of how to develop. + +[Publishing](./Publishing.md) contains useful information on how to publish your contract +to the world, once you are ready to deploy it on a running blockchain. And +[Importing](./Importing.md) contains information about pulling in other contracts or crates +that have been published. + +Please replace this README file with information about your specific project. You can keep +the `Developing.md` and `Publishing.md` files as useful referenced, but please set some +proper description in the README. + +## Gitpod integration + +[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. + +Workspace contains: + - **rust**: for builds + - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client + - **jq**: shell JSON manipulation tool + +Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. + diff --git a/contracts/warp-legacy-account/examples/warp-legacy-account-schema.rs b/contracts/warp-legacy-account/examples/warp-legacy-account-schema.rs new file mode 100644 index 00000000..cec9da8b --- /dev/null +++ b/contracts/warp-legacy-account/examples/warp-legacy-account-schema.rs @@ -0,0 +1,17 @@ +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; +use std::env::current_dir; +use std::fs::create_dir_all; + +use legacy_account::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); + export_schema(&schema_for!(Config), &out_dir); +} diff --git a/contracts/warp-legacy-account/meta/README.md b/contracts/warp-legacy-account/meta/README.md new file mode 100644 index 00000000..279d1db4 --- /dev/null +++ b/contracts/warp-legacy-account/meta/README.md @@ -0,0 +1,16 @@ +# The meta folder + +This folder is ignored via the `.genignore` file. It contains meta files +that should not make it into the generated project. + +In particular, it is used for an AppVeyor CI script that runs on `cw-template` +itself (running the cargo-generate script, then testing the generated project). +The `.circleci` and `.github` directories contain scripts destined for any projects created from +this template. + +## Files + +- `appveyor.yml`: The AppVeyor CI configuration +- `test_generate.sh`: A script for generating a project from the template and + runnings builds and tests in it. This works almost like the CI script but + targets local UNIX-like dev environments. diff --git a/contracts/warp-legacy-account/meta/appveyor.yml b/contracts/warp-legacy-account/meta/appveyor.yml new file mode 100644 index 00000000..5da37f70 --- /dev/null +++ b/contracts/warp-legacy-account/meta/appveyor.yml @@ -0,0 +1,61 @@ +# This CI configuration tests the cw-template repository itself, +# not the resulting project. We want to ensure that +# 1. the template to project generation works +# 2. the template files are up to date +# +# We chose Appveyor for this task as it allows us to use an arbitrary config +# location. Furthermore it allows us to ship Circle CI and GitHub Actions configs +# generated for the resulting project. + +image: Ubuntu + +environment: + TOOLCHAIN: 1.58.1 + +services: + - docker + +cache: + - $HOME/.rustup/ -> meta/appveyor.yml + # For details about cargo caching see https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci + - $HOME/.cargo/bin/ -> meta/appveyor.yml + - $HOME/.cargo/registry/index/ -> meta/appveyor.yml + - $HOME/.cargo/registry/cache/ -> meta/appveyor.yml + - $HOME/.cargo/git/db/ -> meta/appveyor.yml + +install: + - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain "$TOOLCHAIN" -y + - source $HOME/.cargo/env + - rustc --version + - cargo --version + - rustup target add wasm32-unknown-unknown + - cargo install --features vendored-openssl cargo-generate || true + +build_script: + # No matter what is currently checked out by the CI (main, other branch, PR merge commit), + # we create a temporary local branch from that point with a constant name, which we need for + # cargo generate. + - git branch current-ci-checkout + - cd .. + - cargo generate --git cw-template --name testgen-ci --branch current-ci-checkout + - cd testgen-ci + - ls -lA + - cargo fmt -- --check + - cargo unit-test + - cargo wasm + - cargo schema + - docker build --pull -t "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" . + - \[ "${APPVEYOR_REPO_BRANCH}" = "main" \] && image_tag=latest || image_tag=${APPVEYOR_REPO_TAG_NAME} + - docker tag "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" "cosmwasm/cw-gitpod-base:${image_tag}" + +on_success: + # publish docker image + - docker login --password-stdin -u "$DOCKER_USER" <<<"$DOCKER_PASS" + - docker push + - docker logout + +branches: +# whitelist long living branches and tags + only: + # - main + - /v\d+\.\d+\.\d+/ diff --git a/contracts/warp-legacy-account/meta/test_generate.sh b/contracts/warp-legacy-account/meta/test_generate.sh new file mode 100644 index 00000000..b9aaa237 --- /dev/null +++ b/contracts/warp-legacy-account/meta/test_generate.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -o errexit -o nounset -o pipefail +command -v shellcheck > /dev/null && shellcheck "$0" + +REPO_ROOT="$(realpath "$(dirname "$0")/..")" + +TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/cw-template.XXXXXXXXX") +PROJECT_NAME="testgen-local" + +( + echo "Navigating to $TMP_DIR" + cd "$TMP_DIR" + + GIT_BRANCH=$(git -C "$REPO_ROOT" branch --show-current) + + echo "Generating project from local repository (branch $GIT_BRANCH) ..." + cargo generate --git "$REPO_ROOT" --name "$PROJECT_NAME" --branch "$GIT_BRANCH" + + ( + cd "$PROJECT_NAME" + echo "This is what was generated" + ls -lA + + # Check formatting + echo "Checking formatting ..." + cargo fmt -- --check + + # Debug builds first to fail fast + echo "Running unit tests ..." + cargo unit-test + echo "Creating schema ..." + cargo schema + + echo "Building wasm ..." + cargo wasm + ) +) diff --git a/contracts/warp-legacy-account/src/contract.rs b/contracts/warp-legacy-account/src/contract.rs new file mode 100644 index 00000000..f1c7f076 --- /dev/null +++ b/contracts/warp-legacy-account/src/contract.rs @@ -0,0 +1,76 @@ +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, +}; +use cw_utils::nonpayable; + +use crate::state::CONFIG; +use crate::{execute, query, ContractError}; +use legacy_account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let instantiated_account_addr = env.contract.address; + + CONFIG.save( + deps.storage, + &Config { + owner: deps.api.addr_validate(&msg.owner)?, + creator_addr: info.sender, + job_account_tracker_addr: deps.api.addr_validate(&msg.job_account_tracker_addr)?, + }, + )?; + + Ok(Response::new() + .add_messages(msg.msgs.clone()) + .add_attribute("action", "instantiate") + .add_attribute("job_id", msg.job_id) + .add_attribute("contract_addr", instantiated_account_addr) + .add_attribute("job_account_tracker_addr", msg.job_account_tracker_addr) + .add_attribute("owner", msg.owner) + .add_attribute( + "native_funds", + serde_json_wasm::to_string(&msg.native_funds)?, + ) + .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.cw_funds)?) + .add_attribute("account_msgs", serde_json_wasm::to_string(&msg.msgs)?)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner && info.sender != config.creator_addr { + return Err(ContractError::Unauthorized {}); + } + match msg { + ExecuteMsg::Generic(data) => Ok(Response::new() + .add_messages(data.msgs) + .add_attribute("action", "generic")), + ExecuteMsg::WithdrawAssets(data) => { + nonpayable(&info).unwrap(); + execute::withdraw::withdraw_assets(deps, env, data, config) + } + ExecuteMsg::IbcTransfer(data) => execute::ibc::ibc_transfer(env, data), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Response::new()) +} diff --git a/contracts/warp-legacy-account/src/error.rs b/contracts/warp-legacy-account/src/error.rs new file mode 100644 index 00000000..81db9d26 --- /dev/null +++ b/contracts/warp-legacy-account/src/error.rs @@ -0,0 +1,73 @@ +use crate::ContractError::{DecodeError, DeserializationError, SerializationError}; +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Invalid fee")] + InvalidFee {}, + + #[error("Funds array in message does not match funds array in job.")] + FundsMismatch {}, + + #[error("Reward provided is smaller than minimum")] + RewardTooSmall {}, + + #[error("Invalid arguments")] + InvalidArguments {}, + + #[error("Custom Error val: {val:?}")] + CustomError { val: String }, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. + #[error("Error deserializing data")] + DeserializationError {}, + + #[error("Error serializing data")] + SerializationError {}, + + #[error("Error decoding JSON result")] + DecodeError {}, + + #[error("Error resolving JSON path")] + ResolveError {}, + + #[error("Sub account already occupied")] + SubAccountAlreadyOccupiedError {}, + + #[error("Sub account already free")] + SubAccountAlreadyFreeError {}, + + #[error("Sub account should be occupied but it is free")] + SubAccountNotOccupiedError {}, +} + +impl From for ContractError { + fn from(_: serde_json_wasm::de::Error) -> Self { + DeserializationError {} + } +} + +impl From for ContractError { + fn from(_: serde_json_wasm::ser::Error) -> Self { + SerializationError {} + } +} + +impl From for ContractError { + fn from(_: json_codec_wasm::DecodeError) -> Self { + DecodeError {} + } +} + +impl From for ContractError { + fn from(_: base64::DecodeError) -> Self { + DecodeError {} + } +} diff --git a/contracts/warp-legacy-account/src/execute/ibc.rs b/contracts/warp-legacy-account/src/execute/ibc.rs new file mode 100644 index 00000000..0ded7be5 --- /dev/null +++ b/contracts/warp-legacy-account/src/execute/ibc.rs @@ -0,0 +1,33 @@ +use crate::ContractError; +use cosmwasm_std::CosmosMsg::Stargate; +use cosmwasm_std::{Env, Response}; +use legacy_account::{IbcTransferMsg, TimeoutBlock}; +use prost::Message; + +pub fn ibc_transfer(env: Env, data: IbcTransferMsg) -> Result { + let mut transfer_msg = data.transfer_msg.clone(); + + if data.timeout_block_delta.is_some() && data.transfer_msg.timeout_block.is_some() { + let block = transfer_msg.timeout_block.unwrap(); + transfer_msg.timeout_block = Some(TimeoutBlock { + revision_number: Some(block.revision_number()), + revision_height: Some(env.block.height + data.timeout_block_delta.unwrap()), + }) + } + + if data.timeout_timestamp_seconds_delta.is_some() { + transfer_msg.timeout_timestamp = Some( + env.block + .time + .plus_seconds( + env.block.time.seconds() + data.timeout_timestamp_seconds_delta.unwrap(), + ) + .nanos(), + ); + } + + Ok(Response::new().add_message(Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: transfer_msg.encode_to_vec().into(), + })) +} diff --git a/contracts/warp-legacy-account/src/execute/mod.rs b/contracts/warp-legacy-account/src/execute/mod.rs new file mode 100644 index 00000000..2237e0b9 --- /dev/null +++ b/contracts/warp-legacy-account/src/execute/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod ibc; +pub(crate) mod withdraw; diff --git a/contracts/warp-legacy-account/src/execute/withdraw.rs b/contracts/warp-legacy-account/src/execute/withdraw.rs new file mode 100644 index 00000000..f29685cb --- /dev/null +++ b/contracts/warp-legacy-account/src/execute/withdraw.rs @@ -0,0 +1,134 @@ +use cosmwasm_std::{ + to_binary, Addr, BankMsg, CosmosMsg, Deps, DepsMut, Env, Response, StdResult, Uint128, WasmMsg, +}; +use cw20::{BalanceResponse, Cw20ExecuteMsg}; +use cw721::{Cw721QueryMsg, OwnerOfResponse}; + +use crate::ContractError; + +use controller::account::{AssetInfo, Cw721ExecuteMsg}; +use legacy_account::{Config, WithdrawAssetsMsg}; + +pub fn withdraw_assets( + deps: DepsMut, + env: Env, + data: WithdrawAssetsMsg, + config: Config, +) -> Result { + let mut withdraw_msgs: Vec = vec![]; + + for asset_info in &data.asset_infos { + match asset_info { + AssetInfo::Native(denom) => { + let withdraw_native_msg = + withdraw_asset_native(deps.as_ref(), env.clone(), &config.owner, denom)?; + + match withdraw_native_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw20(addr) => { + let withdraw_cw20_msg = + withdraw_asset_cw20(deps.as_ref(), env.clone(), &config.owner, addr)?; + + match withdraw_cw20_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw721(addr, token_id) => { + let withdraw_cw721_msg = + withdraw_asset_cw721(deps.as_ref(), &config.owner, addr, token_id)?; + match withdraw_cw721_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + } + } + + Ok(Response::new() + .add_messages(withdraw_msgs) + .add_attribute("action", "withdraw_assets") + .add_attribute("assets", serde_json_wasm::to_string(&data.asset_infos)?)) +} + +fn withdraw_asset_native( + deps: Deps, + env: Env, + owner: &Addr, + denom: &String, +) -> StdResult> { + let amount = deps.querier.query_balance(env.contract.address, denom)?; + + let res = if amount.amount > Uint128::zero() { + Some(CosmosMsg::Bank(BankMsg::Send { + to_address: owner.to_string(), + amount: vec![amount], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw20( + deps: Deps, + env: Env, + owner: &Addr, + token: &Addr, +) -> StdResult> { + let amount: BalanceResponse = deps.querier.query_wasm_smart( + token.to_string(), + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + let res = if amount.balance > Uint128::zero() { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: owner.to_string(), + amount: amount.balance, + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw721( + deps: Deps, + owner: &Addr, + token: &Addr, + token_id: &String, +) -> StdResult> { + let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( + token.to_string(), + &Cw721QueryMsg::OwnerOf { + token_id: token_id.to_string(), + include_expired: None, + }, + )?; + + let res = if owner_query.owner == *owner { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw721ExecuteMsg::TransferNft { + recipient: owner.to_string(), + token_id: token_id.to_string(), + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} diff --git a/contracts/warp-legacy-account/src/lib.rs b/contracts/warp-legacy-account/src/lib.rs new file mode 100644 index 00000000..aff04ae7 --- /dev/null +++ b/contracts/warp-legacy-account/src/lib.rs @@ -0,0 +1,10 @@ +pub mod contract; +mod error; +mod execute; +mod query; +pub mod state; + +#[cfg(test)] +mod tests; + +pub use crate::error::ContractError; diff --git a/contracts/warp-legacy-account/src/query/account.rs b/contracts/warp-legacy-account/src/query/account.rs new file mode 100644 index 00000000..28d420b7 --- /dev/null +++ b/contracts/warp-legacy-account/src/query/account.rs @@ -0,0 +1,8 @@ +use crate::state::CONFIG; +use cosmwasm_std::{Deps, StdResult}; +use legacy_account::ConfigResponse; + +pub fn query_config(deps: Deps) -> StdResult { + let config = CONFIG.load(deps.storage)?; + Ok(ConfigResponse { config }) +} diff --git a/contracts/warp-legacy-account/src/query/mod.rs b/contracts/warp-legacy-account/src/query/mod.rs new file mode 100644 index 00000000..d937534a --- /dev/null +++ b/contracts/warp-legacy-account/src/query/mod.rs @@ -0,0 +1 @@ +pub(crate) mod account; diff --git a/contracts/warp-legacy-account/src/state.rs b/contracts/warp-legacy-account/src/state.rs new file mode 100644 index 00000000..9483f694 --- /dev/null +++ b/contracts/warp-legacy-account/src/state.rs @@ -0,0 +1,4 @@ +use cw_storage_plus::Item; +use legacy_account::Config; + +pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/warp-legacy-account/src/tests.rs b/contracts/warp-legacy-account/src/tests.rs new file mode 100644 index 00000000..a45ca5c3 --- /dev/null +++ b/contracts/warp-legacy-account/src/tests.rs @@ -0,0 +1,340 @@ +use cosmwasm_std::{ + testing::{mock_dependencies, mock_env, mock_info}, + to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, + IbcTimeoutBlock, Response, StakingMsg, Uint128, Uint64, VoteOption, WasmMsg, +}; + +use crate::contract::{execute, instantiate}; +use crate::ContractError; +use legacy_account::{ExecuteMsg, GenericMsg, InstantiateMsg}; + +#[test] +fn test_execute_controller() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("vlad_controller", &[]); + + let _instantiate_res = instantiate( + deps.as_mut(), + env.clone(), + info.clone(), + InstantiateMsg { + owner: "vlad".to_string(), + job_id: Uint64::zero(), + job_account_tracker_addr: "vlad".to_string(), + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], + }, + ); + + let execute_msg = ExecuteMsg::Generic(GenericMsg { + msgs: vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + }), + CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes, + }), + CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + }), + CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0, + }), + }), + CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default(), + }, + ], + }); + + let execute_res = execute(deps.as_mut(), env, info, execute_msg).unwrap(); + + assert_eq!( + execute_res, + Response::new() + .add_attribute("action", "generic") + .add_messages(vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }] + }), + CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes + }), + CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + }), + CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0 + }), + }), + CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default() + } + ]) + ) +} + +#[test] +fn test_execute_owner() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("vlad_controller", &[]); + + let _instantiate_res = instantiate( + deps.as_mut(), + env.clone(), + info, + InstantiateMsg { + owner: "vlad".to_string(), + job_id: Uint64::zero(), + job_account_tracker_addr: "vlad".to_string(), + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], + }, + ); + + let execute_msg = ExecuteMsg::Generic(GenericMsg { + msgs: vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + }), + CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes, + }), + CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + }), + CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0, + }), + }), + CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default(), + }, + ], + }); + + let info2 = mock_info("vlad", &[]); + + let execute_res = execute(deps.as_mut(), env, info2, execute_msg).unwrap(); + + assert_eq!( + execute_res, + Response::new() + .add_attribute("action", "generic") + .add_messages(vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }] + }), + CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes + }), + CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + }), + CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0 + }), + }), + CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default() + } + ]) + ) +} + +#[test] +fn test_execute_unauth() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("vlad_controller", &[]); + + let _instantiate_res = instantiate( + deps.as_mut(), + env.clone(), + info, + InstantiateMsg { + owner: "vlad".to_string(), + job_id: Uint64::zero(), + job_account_tracker_addr: "vlad".to_string(), + native_funds: vec![], + cw_funds: vec![], + msgs: vec![], + }, + ); + + let execute_msg = ExecuteMsg::Generic(GenericMsg { + msgs: vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + }), + CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes, + }), + CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + }), + CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0, + }), + }), + CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default(), + }, + ], + }); + + let info2 = mock_info("vlad2", &[]); + + let execute_res = execute(deps.as_mut(), env, info2, execute_msg).unwrap_err(); + + assert_eq!(execute_res, ContractError::Unauthorized {}) +} diff --git a/packages/controller/Cargo.toml b/packages/controller/Cargo.toml index 89222f6e..14556755 100644 --- a/packages/controller/Cargo.toml +++ b/packages/controller/Cargo.toml @@ -10,17 +10,11 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" cosmwasm-schema = "1.1" -cw-asset = "2.2" -cw20 = "0.16" -cw-storage-plus = "0.16" -cw2 = "0.16" schemars = "0.8" serde = { version = "1", default-features = false, features = ["derive"] } strum = "0.24" strum_macros = "0.24" -thiserror = { version = "1" } [dev-dependencies] cw-multi-test = "0.16" diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index f9931350..87488adc 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -44,30 +44,30 @@ pub enum Cw721ExecuteMsg { } #[cw_serde] -pub struct QueryMainAccountMsg { +pub struct QueryLegacyAccountMsg { pub owner: String, } #[cw_serde] -pub struct QueryMainAccountsMsg { +pub struct QueryLegacyAccountsMsg { pub start_after: Option, pub limit: Option, } #[cw_serde] -pub struct MainAccount { +pub struct LegacyAccount { pub owner: Addr, pub account: Addr, } #[cw_serde] -pub struct MainAccountResponse { - pub main_account: MainAccount, +pub struct LegacyAccountResponse { + pub account: LegacyAccount, } #[cw_serde] -pub struct MainAccountsResponse { - pub main_accounts: Vec, +pub struct LegacyAccountsResponse { + pub accounts: Vec, } #[cw_serde] diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 68b0d20c..aac4a37d 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -1,5 +1,5 @@ use crate::account::{ - MainAccountResponse, MainAccountsResponse, QueryMainAccountMsg, QueryMainAccountsMsg, + LegacyAccountResponse, LegacyAccountsResponse, QueryLegacyAccountMsg, QueryLegacyAccountsMsg, }; use crate::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, JobResponse, JobsResponse, QueryJobMsg, @@ -17,6 +17,7 @@ pub struct Config { pub owner: Addr, pub fee_denom: String, pub fee_collector: Addr, + pub warp_job_account_tracker_code_id: Uint64, pub warp_account_code_id: Uint64, pub minimum_reward: Uint128, pub creation_fee_percentage: Uint64, @@ -47,6 +48,7 @@ pub struct InstantiateMsg { pub owner: Option, pub fee_denom: String, pub fee_collector: Option, + pub warp_job_account_tracker_code_id: Uint64, pub warp_account_code_id: Uint64, pub minimum_reward: Uint128, pub creation_fee: Uint64, @@ -70,7 +72,11 @@ pub enum ExecuteMsg { UpdateConfig(UpdateConfigMsg), - MigrateAccounts(MigrateAccountsMsg), + MigrateLegacyAccounts(MigrateLegacyAccountsMsg), + MigrateJobAccountTrackers(MigrateJobAccountTrackersMsg), + MigrateFreeJobAccounts(MigrateJobAccountsMsg), + MigrateOccupiedJobAccounts(MigrateJobAccountsMsg), + MigratePendingJobs(MigrateJobsMsg), MigrateFinishedJobs(MigrateJobsMsg), } @@ -90,8 +96,23 @@ pub struct UpdateConfigMsg { } #[cw_serde] -pub struct MigrateAccountsMsg { - pub warp_account_code_id: Uint64, +pub struct MigrateLegacyAccountsMsg { + pub warp_legacy_account_code_id: Uint64, + pub start_after: Option, + pub limit: u8, +} + +#[cw_serde] +pub struct MigrateJobAccountTrackersMsg { + pub warp_job_account_tracker_code_id: Uint64, + pub start_after: Option, + pub limit: u8, +} + +#[cw_serde] +pub struct MigrateJobAccountsMsg { + pub job_account_tracker_addr: String, + pub warp_job_account_code_id: Uint64, pub start_after: Option, pub limit: u8, } @@ -111,12 +132,12 @@ pub enum QueryMsg { #[returns(JobsResponse)] QueryJobs(QueryJobsMsg), - // For sub account, please query it via the main account contract - // You can look at account contract for more details - #[returns(MainAccountResponse)] - QueryMainAccount(QueryMainAccountMsg), - #[returns(MainAccountsResponse)] - QueryMainAccounts(QueryMainAccountsMsg), + // For job account, please query it via the account tracker contract + // You can look at account tracker contract for more details + #[returns(LegacyAccountResponse)] + QueryLegacyAccount(QueryLegacyAccountMsg), + #[returns(LegacyAccountsResponse)] + QueryLegacyAccounts(QueryLegacyAccountsMsg), #[returns(ConfigResponse)] QueryConfig(QueryConfigMsg), @@ -144,6 +165,7 @@ pub struct StateResponse { //migrate//{"resolver_address":"terra1a8dxkrapwj4mkpfnrv7vahd0say0lxvd0ft6qv","warp_account_code_id":"10081"} #[cw_serde] pub struct MigrateMsg { + pub warp_job_account_tracker_code_id: Uint64, pub warp_account_code_id: Uint64, pub resolver_address: String, } diff --git a/packages/job-account-tracker/Cargo.toml b/packages/job-account-tracker/Cargo.toml new file mode 100644 index 00000000..c7e71053 --- /dev/null +++ b/packages/job-account-tracker/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "job-account-tracker" +version = "0.1.0" +authors = ["Terra Money "] +edition = "2021" + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = "1.1" +cosmwasm-schema = "1.1" + +[dev-dependencies] +cw-multi-test = "0.16" diff --git a/packages/job-account-tracker/README.md b/packages/job-account-tracker/README.md new file mode 100644 index 00000000..954383af --- /dev/null +++ b/packages/job-account-tracker/README.md @@ -0,0 +1,106 @@ +# CosmWasm Starter Pack + +This is a template to build smart contracts in Rust to run inside a +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. +To understand the framework better, please read the overview in the +[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), +and dig into the [cosmwasm docs](https://www.cosmwasm.com). +This assumes you understand the theory and just want to get coding. + +## Creating a new repo from template + +Assuming you have a recent version of rust and cargo (v1.58.1+) installed +(via [rustup](https://rustup.rs/)), +then the following should get you a new repo to start a contract: + +Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. +Unless you did that before, run this line now: + +```sh +cargo install cargo-generate --features vendored-openssl +cargo install cargo-run-script +``` + +Now, use it to create your new contract. +Go to the folder in which you want to place it and run: + + +**Latest: 1.0.0-beta6** + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME +```` + +**Older Version** + +Pass version as branch flag: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME +```` + +Example: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME +``` + +You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) +containing a simple working contract and build system that you can customize. + +## Create a Repo + +After generating, you have a initialized local git repo, but no commits, and no remote. +Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). +Then run the following: + +```sh +# this is needed to create a valid Cargo.lock file (see below) +cargo check +git branch -M main +git add . +git commit -m 'Initial Commit' +git remote add origin YOUR-GIT-URL +git push -u origin main +``` + +## CI Support + +We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) +and [Circle CI](.circleci/config.yml) in the generated project, so you can +get up and running with CI right away. + +One note is that the CI runs all `cargo` commands +with `--locked` to ensure it uses the exact same versions as you have locally. This also means +you must have an up-to-date `Cargo.lock` file, which is not auto-generated. +The first time you set up the project (or after adding any dep), you should ensure the +`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by +running `cargo check` or `cargo unit-test`. + +## Using your project + +Once you have your custom repo, you should check out [Developing](./Developing.md) to explain +more on how to run tests and develop code. Or go through the +[online tutorial](https://docs.cosmwasm.com/) to get a better feel +of how to develop. + +[Publishing](./Publishing.md) contains useful information on how to publish your contract +to the world, once you are ready to deploy it on a running blockchain. And +[Importing](./Importing.md) contains information about pulling in other contracts or crates +that have been published. + +Please replace this README file with information about your specific project. You can keep +the `Developing.md` and `Publishing.md` files as useful referenced, but please set some +proper description in the README. + +## Gitpod integration + +[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. + +Workspace contains: + - **rust**: for builds + - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client + - **jq**: shell JSON manipulation tool + +Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. + diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs new file mode 100644 index 00000000..6163ad25 --- /dev/null +++ b/packages/job-account-tracker/src/lib.rs @@ -0,0 +1,89 @@ +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Uint64}; + +#[cw_serde] +pub struct Config { + pub owner: Addr, + // Address of warp controller contract + pub creator_addr: Addr, +} + +#[cw_serde] +pub struct InstantiateMsg { + // User who owns this account + pub owner: String, +} + +#[cw_serde] +#[allow(clippy::large_enum_variant)] +pub enum ExecuteMsg { + OccupyAccount(OccupyAccountMsg), + FreeAccount(FreeAccountMsg), +} + +#[cw_serde] +pub struct OccupyAccountMsg { + pub account_addr: String, + pub job_id: Uint64, +} + +#[cw_serde] +pub struct FreeAccountMsg { + pub account_addr: String, +} + +#[derive(QueryResponses)] +#[cw_serde] +pub enum QueryMsg { + #[returns(ConfigResponse)] + QueryConfig(QueryConfigMsg), + #[returns(AccountsResponse)] + QueryOccupiedAccounts(QueryOccupiedAccountsMsg), + #[returns(AccountsResponse)] + QueryFreeAccounts(QueryFreeAccountsMsg), + #[returns(FirstFreeAccountResponse)] + QueryFirstFreeAccount(QueryFirstFreeAccountMsg), +} + +#[cw_serde] +pub struct QueryConfigMsg {} + +#[cw_serde] +pub struct ConfigResponse { + pub config: Config, +} + +#[cw_serde] +pub struct QueryOccupiedAccountsMsg { + pub start_after: Option, + pub limit: Option, +} + +#[cw_serde] +pub struct QueryFreeAccountsMsg { + pub start_after: Option, + pub limit: Option, +} + +#[cw_serde] +pub struct Account { + pub addr: Addr, + pub occupied_by_job_id: Option, +} + +#[cw_serde] +pub struct AccountsResponse { + pub accounts: Vec, + pub total_count: usize, +} + +#[cw_serde] +pub struct QueryFirstFreeAccountMsg {} + +#[cw_serde] +pub struct FirstFreeAccountResponse { + pub account: Option, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/packages/account/.cargo/config b/packages/job-account/.cargo/config similarity index 100% rename from packages/account/.cargo/config rename to packages/job-account/.cargo/config diff --git a/packages/account/Cargo.toml b/packages/job-account/Cargo.toml similarity index 63% rename from packages/account/Cargo.toml rename to packages/job-account/Cargo.toml index de3fb46b..753f2e0a 100644 --- a/packages/account/Cargo.toml +++ b/packages/job-account/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "account" +name = "job-account" version = "0.1.0" authors = ["Terra Money "] edition = "2021" @@ -10,20 +10,12 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" cosmwasm-schema = "1.1" -cw-asset = "2.2" -cw20 = "0.16" -cw-storage-plus = "0.16" -cw2 = "0.16" schemars = "0.8" serde = { version = "1", default-features = false, features = ["derive"] } -json-codec-wasm = "0.1.0" -strum = "0.24" -strum_macros = "0.24" -thiserror = { version = "1" } -controller = {path = "../controller"} prost = "0.11.9" +controller = { path = "../controller" } + [dev-dependencies] cw-multi-test = "0.16" diff --git a/packages/job-account/README.md b/packages/job-account/README.md new file mode 100644 index 00000000..954383af --- /dev/null +++ b/packages/job-account/README.md @@ -0,0 +1,106 @@ +# CosmWasm Starter Pack + +This is a template to build smart contracts in Rust to run inside a +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. +To understand the framework better, please read the overview in the +[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), +and dig into the [cosmwasm docs](https://www.cosmwasm.com). +This assumes you understand the theory and just want to get coding. + +## Creating a new repo from template + +Assuming you have a recent version of rust and cargo (v1.58.1+) installed +(via [rustup](https://rustup.rs/)), +then the following should get you a new repo to start a contract: + +Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. +Unless you did that before, run this line now: + +```sh +cargo install cargo-generate --features vendored-openssl +cargo install cargo-run-script +``` + +Now, use it to create your new contract. +Go to the folder in which you want to place it and run: + + +**Latest: 1.0.0-beta6** + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME +```` + +**Older Version** + +Pass version as branch flag: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME +```` + +Example: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME +``` + +You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) +containing a simple working contract and build system that you can customize. + +## Create a Repo + +After generating, you have a initialized local git repo, but no commits, and no remote. +Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). +Then run the following: + +```sh +# this is needed to create a valid Cargo.lock file (see below) +cargo check +git branch -M main +git add . +git commit -m 'Initial Commit' +git remote add origin YOUR-GIT-URL +git push -u origin main +``` + +## CI Support + +We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) +and [Circle CI](.circleci/config.yml) in the generated project, so you can +get up and running with CI right away. + +One note is that the CI runs all `cargo` commands +with `--locked` to ensure it uses the exact same versions as you have locally. This also means +you must have an up-to-date `Cargo.lock` file, which is not auto-generated. +The first time you set up the project (or after adding any dep), you should ensure the +`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by +running `cargo check` or `cargo unit-test`. + +## Using your project + +Once you have your custom repo, you should check out [Developing](./Developing.md) to explain +more on how to run tests and develop code. Or go through the +[online tutorial](https://docs.cosmwasm.com/) to get a better feel +of how to develop. + +[Publishing](./Publishing.md) contains useful information on how to publish your contract +to the world, once you are ready to deploy it on a running blockchain. And +[Importing](./Importing.md) contains information about pulling in other contracts or crates +that have been published. + +Please replace this README file with information about your specific project. You can keep +the `Developing.md` and `Publishing.md` files as useful referenced, but please set some +proper description in the README. + +## Gitpod integration + +[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. + +Workspace contains: + - **rust**: for builds + - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client + - **jq**: shell JSON manipulation tool + +Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. + diff --git a/packages/account/examples/account-schema.rs b/packages/job-account/examples/account-schema.rs similarity index 100% rename from packages/account/examples/account-schema.rs rename to packages/job-account/examples/account-schema.rs diff --git a/packages/account/src/lib.rs b/packages/job-account/src/lib.rs similarity index 61% rename from packages/account/src/lib.rs rename to packages/job-account/src/lib.rs index 808dde1b..bb31abac 100644 --- a/packages/account/src/lib.rs +++ b/packages/job-account/src/lib.rs @@ -4,22 +4,19 @@ use cosmwasm_std::{Addr, Coin as NativeCoin, CosmosMsg, Uint64}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[cw_serde] -pub struct SubAccountConfig { - pub main_account_addr: Addr, - // If occupied, occupied_by_job_id is the job id of the pending job that is using this sub account - pub occupied_by_job_id: Option, -} - #[cw_serde] pub struct Config { pub owner: Addr, // Address of warp controller contract pub creator_addr: Addr, - // Address of current warp account contract - pub account_addr: Addr, - // Exist if current account is a sub account - pub sub_account_config: Option, + + // Address of account tracker contract + pub job_account_tracker_addr: Addr, + // // Address of current warp account contract + // pub account_addr: Addr, + + // // If occupied, occupied_by_job_id is the job id of the pending job that is using this sub account + // pub occupied_by_job_id: Option, } #[cw_serde] @@ -28,11 +25,12 @@ pub struct InstantiateMsg { pub owner: String, // ID of the job that is created along with the account pub job_id: Uint64, - // Whether this account is a sub account, account can be a main account or a sub account - pub is_sub_account: bool, - // Only supplied when is_sub_account is true - // Skipped if it's instantiating a main account - pub main_account_addr: Option, + + // Account tracker tracks all accounts owned by user + // Store it inside account for easier lookup, though most of time we only lookup account from account tracker + // But store it enables us the other way around + pub job_account_tracker_addr: String, + // Only required when we are instantiate a main account // Since we always want to fund sub account, so we will pass this value around and send it to sub account during instantiation in create main account's reply pub native_funds: Vec, @@ -48,8 +46,6 @@ pub enum ExecuteMsg { Generic(GenericMsg), WithdrawAssets(WithdrawAssetsMsg), IbcTransfer(IbcTransferMsg), - OccupySubAccount(OccupySubAccountMsg), - FreeSubAccount(FreeSubAccountMsg), } #[cw_serde] @@ -114,28 +110,11 @@ pub struct WithdrawAssetsMsg { #[cw_serde] pub struct ExecuteWasmMsg {} -#[cw_serde] -pub struct OccupySubAccountMsg { - pub sub_account_addr: String, - pub job_id: Uint64, -} - -#[cw_serde] -pub struct FreeSubAccountMsg { - pub sub_account_addr: String, -} - #[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg { #[returns(ConfigResponse)] QueryConfig(QueryConfigMsg), - #[returns(OccupiedSubAccountsResponse)] - QueryOccupiedSubAccounts(QueryOccupiedSubAccountsMsg), - #[returns(FreeSubAccountsResponse)] - QueryFreeSubAccounts(QueryFreeSubAccountsMsg), - #[returns(FirstFreeSubAccountResponse)] - QueryFirstFreeSubAccount(QueryFirstFreeSubAccountMsg), } #[cw_serde] @@ -146,37 +125,5 @@ pub struct ConfigResponse { pub config: Config, } -#[cw_serde] -pub struct QueryOccupiedSubAccountsMsg { - pub start_after: Option, - pub limit: Option, -} - -#[cw_serde] -pub struct OccupiedSubAccountsResponse { - pub sub_accounts: Vec, - pub total_count: usize, -} - -#[cw_serde] -pub struct QueryFreeSubAccountsMsg { - pub start_after: Option, - pub limit: Option, -} - -#[cw_serde] -pub struct FreeSubAccountsResponse { - pub sub_accounts: Vec, - pub total_count: usize, -} - -#[cw_serde] -pub struct QueryFirstFreeSubAccountMsg {} - -#[cw_serde] -pub struct FirstFreeSubAccountResponse { - pub sub_account: Option, -} - #[cw_serde] pub struct MigrateMsg {} diff --git a/packages/legacy-account/.cargo/config b/packages/legacy-account/.cargo/config new file mode 100644 index 00000000..c7f1d9fa --- /dev/null +++ b/packages/legacy-account/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example warp-protocol-schema" diff --git a/packages/legacy-account/Cargo.toml b/packages/legacy-account/Cargo.toml new file mode 100644 index 00000000..8d2455da --- /dev/null +++ b/packages/legacy-account/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "legacy-account" +version = "0.1.0" +authors = ["Terra Money "] +edition = "2021" + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = "1.1" +cosmwasm-schema = "1.1" +schemars = "0.8" +serde = { version = "1", default-features = false, features = ["derive"] } +prost = "0.11.9" + +controller = { path = "../controller" } + +[dev-dependencies] +cw-multi-test = "0.16" diff --git a/packages/legacy-account/README.md b/packages/legacy-account/README.md new file mode 100644 index 00000000..954383af --- /dev/null +++ b/packages/legacy-account/README.md @@ -0,0 +1,106 @@ +# CosmWasm Starter Pack + +This is a template to build smart contracts in Rust to run inside a +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. +To understand the framework better, please read the overview in the +[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), +and dig into the [cosmwasm docs](https://www.cosmwasm.com). +This assumes you understand the theory and just want to get coding. + +## Creating a new repo from template + +Assuming you have a recent version of rust and cargo (v1.58.1+) installed +(via [rustup](https://rustup.rs/)), +then the following should get you a new repo to start a contract: + +Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. +Unless you did that before, run this line now: + +```sh +cargo install cargo-generate --features vendored-openssl +cargo install cargo-run-script +``` + +Now, use it to create your new contract. +Go to the folder in which you want to place it and run: + + +**Latest: 1.0.0-beta6** + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME +```` + +**Older Version** + +Pass version as branch flag: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME +```` + +Example: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME +``` + +You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) +containing a simple working contract and build system that you can customize. + +## Create a Repo + +After generating, you have a initialized local git repo, but no commits, and no remote. +Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). +Then run the following: + +```sh +# this is needed to create a valid Cargo.lock file (see below) +cargo check +git branch -M main +git add . +git commit -m 'Initial Commit' +git remote add origin YOUR-GIT-URL +git push -u origin main +``` + +## CI Support + +We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) +and [Circle CI](.circleci/config.yml) in the generated project, so you can +get up and running with CI right away. + +One note is that the CI runs all `cargo` commands +with `--locked` to ensure it uses the exact same versions as you have locally. This also means +you must have an up-to-date `Cargo.lock` file, which is not auto-generated. +The first time you set up the project (or after adding any dep), you should ensure the +`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by +running `cargo check` or `cargo unit-test`. + +## Using your project + +Once you have your custom repo, you should check out [Developing](./Developing.md) to explain +more on how to run tests and develop code. Or go through the +[online tutorial](https://docs.cosmwasm.com/) to get a better feel +of how to develop. + +[Publishing](./Publishing.md) contains useful information on how to publish your contract +to the world, once you are ready to deploy it on a running blockchain. And +[Importing](./Importing.md) contains information about pulling in other contracts or crates +that have been published. + +Please replace this README file with information about your specific project. You can keep +the `Developing.md` and `Publishing.md` files as useful referenced, but please set some +proper description in the README. + +## Gitpod integration + +[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. + +Workspace contains: + - **rust**: for builds + - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client + - **jq**: shell JSON manipulation tool + +Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. + diff --git a/packages/legacy-account/examples/account-schema.rs b/packages/legacy-account/examples/account-schema.rs new file mode 100644 index 00000000..b6ba6260 --- /dev/null +++ b/packages/legacy-account/examples/account-schema.rs @@ -0,0 +1,17 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use controller::QueryMsg; +use controller::{ExecuteMsg, InstantiateMsg}; +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); +} diff --git a/packages/legacy-account/src/lib.rs b/packages/legacy-account/src/lib.rs new file mode 100644 index 00000000..bb31abac --- /dev/null +++ b/packages/legacy-account/src/lib.rs @@ -0,0 +1,129 @@ +use controller::account::{AssetInfo, CwFund}; +use cosmwasm_schema::{cw_serde, QueryResponses}; +use cosmwasm_std::{Addr, Coin as NativeCoin, CosmosMsg, Uint64}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cw_serde] +pub struct Config { + pub owner: Addr, + // Address of warp controller contract + pub creator_addr: Addr, + + // Address of account tracker contract + pub job_account_tracker_addr: Addr, + // // Address of current warp account contract + // pub account_addr: Addr, + + // // If occupied, occupied_by_job_id is the job id of the pending job that is using this sub account + // pub occupied_by_job_id: Option, +} + +#[cw_serde] +pub struct InstantiateMsg { + // User who owns this account + pub owner: String, + // ID of the job that is created along with the account + pub job_id: Uint64, + + // Account tracker tracks all accounts owned by user + // Store it inside account for easier lookup, though most of time we only lookup account from account tracker + // But store it enables us the other way around + pub job_account_tracker_addr: String, + + // Only required when we are instantiate a main account + // Since we always want to fund sub account, so we will pass this value around and send it to sub account during instantiation in create main account's reply + pub native_funds: Vec, + // CW20 or CW721 funds, will be transferred to account in reply of account instantiation + pub cw_funds: Vec, + // List of cosmos msgs to execute after instantiating the account + pub msgs: Vec, +} + +#[cw_serde] +#[allow(clippy::large_enum_variant)] +pub enum ExecuteMsg { + Generic(GenericMsg), + WithdrawAssets(WithdrawAssetsMsg), + IbcTransfer(IbcTransferMsg), +} + +#[cw_serde] +pub struct GenericMsg { + pub msgs: Vec, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] +pub struct Coin { + #[prost(string, tag = "1")] + pub denom: String, + #[prost(string, tag = "2")] + pub amount: String, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] +pub struct TimeoutBlock { + #[prost(uint64, optional, tag = "1")] + pub revision_number: Option, + #[prost(uint64, optional, tag = "2")] + pub revision_height: Option, +} +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] +pub struct TransferMsg { + #[prost(string, tag = "1")] + pub source_port: String, + + #[prost(string, tag = "2")] + pub source_channel: String, + + #[prost(message, optional, tag = "3")] + pub token: Option, + + #[prost(string, tag = "4")] + pub sender: String, + + #[prost(string, tag = "5")] + pub receiver: String, + + #[prost(message, optional, tag = "6")] + pub timeout_block: Option, + + #[prost(uint64, optional, tag = "7")] + pub timeout_timestamp: Option, + + #[prost(string, tag = "8")] + pub memo: String, +} + +#[cw_serde] +pub struct IbcTransferMsg { + pub transfer_msg: TransferMsg, + pub timeout_block_delta: Option, + pub timeout_timestamp_seconds_delta: Option, +} + +#[cw_serde] +pub struct WithdrawAssetsMsg { + pub asset_infos: Vec, +} + +#[cw_serde] +pub struct ExecuteWasmMsg {} + +#[derive(QueryResponses)] +#[cw_serde] +pub enum QueryMsg { + #[returns(ConfigResponse)] + QueryConfig(QueryConfigMsg), +} + +#[cw_serde] +pub struct QueryConfigMsg {} + +#[cw_serde] +pub struct ConfigResponse { + pub config: Config, +} + +#[cw_serde] +pub struct MigrateMsg {} diff --git a/packages/resolver/Cargo.toml b/packages/resolver/Cargo.toml index 704870a3..aefa9005 100644 --- a/packages/resolver/Cargo.toml +++ b/packages/resolver/Cargo.toml @@ -10,19 +10,8 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" cosmwasm-schema = "1.1" -cw-asset = "2.2" -cw20 = "0.16" -cw-storage-plus = "0.16" -cw2 = "0.16" -schemars = "0.8" -serde = { version = "1", default-features = false, features = ["derive"] } -json-codec-wasm = "0.1.0" -strum = "0.24" -strum_macros = "0.24" -thiserror = { version = "1" } -controller = {path = "../controller"} +controller = { path = "../controller" } [dev-dependencies] cw-multi-test = "0.16" diff --git a/packages/templates/Cargo.toml b/packages/templates/Cargo.toml index 919276d5..02e1f5f8 100644 --- a/packages/templates/Cargo.toml +++ b/packages/templates/Cargo.toml @@ -10,20 +10,9 @@ backtraces = ["cosmwasm-std/backtraces"] [dependencies] cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" cosmwasm-schema = "1.1" -cw-asset = "2.2" -cw20 = "0.16" -cw-storage-plus = "0.16" -cw2 = "0.16" -schemars = "0.8" -serde = { version = "1", default-features = false, features = ["derive"] } -resolver = { path = "../resolver", default-features = false, version = "*" } -json-codec-wasm = "0.1.0" -strum = "0.24" -strum_macros = "0.24" -thiserror = { version = "1" } -controller = {path = "../controller"} +controller = { path = "../controller" } +resolver = { path = "../resolver" } [dev-dependencies] cw-multi-test = "0.16" From 26bf33ab6d1ddae7362a90c58e9eaf29c6f42081 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:04:59 -0700 Subject: [PATCH 043/133] restore legacy account to master branch version --- contracts/warp-legacy-account/src/contract.rs | 214 +++++++++++++++--- .../warp-legacy-account/src/execute/ibc.rs | 33 --- .../warp-legacy-account/src/execute/mod.rs | 2 - .../src/execute/withdraw.rs | 134 ----------- contracts/warp-legacy-account/src/lib.rs | 2 - .../warp-legacy-account/src/query/account.rs | 8 - .../warp-legacy-account/src/query/mod.rs | 1 - contracts/warp-legacy-account/src/tests.rs | 27 +-- packages/job-account/src/lib.rs | 8 +- packages/legacy-account/src/lib.rs | 44 +--- 10 files changed, 199 insertions(+), 274 deletions(-) delete mode 100644 contracts/warp-legacy-account/src/execute/ibc.rs delete mode 100644 contracts/warp-legacy-account/src/execute/mod.rs delete mode 100644 contracts/warp-legacy-account/src/execute/withdraw.rs delete mode 100644 contracts/warp-legacy-account/src/query/account.rs delete mode 100644 contracts/warp-legacy-account/src/query/mod.rs diff --git a/contracts/warp-legacy-account/src/contract.rs b/contracts/warp-legacy-account/src/contract.rs index f1c7f076..ba00f27f 100644 --- a/contracts/warp-legacy-account/src/contract.rs +++ b/contracts/warp-legacy-account/src/contract.rs @@ -1,11 +1,18 @@ +use crate::state::CONFIG; +use crate::ContractError; +use controller::account::{AssetInfo, Cw721ExecuteMsg}; +use cosmwasm_std::CosmosMsg::Stargate; use cosmwasm_std::{ - entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + entry_point, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, + Response, StdResult, Uint128, WasmMsg, }; -use cw_utils::nonpayable; - -use crate::state::CONFIG; -use crate::{execute, query, ContractError}; -use legacy_account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use cw20::{BalanceResponse, Cw20ExecuteMsg}; +use cw721::{Cw721QueryMsg, OwnerOfResponse}; +use legacy_account::{ + Config, ExecuteMsg, IbcTransferMsg, InstantiateMsg, MigrateMsg, QueryMsg, TimeoutBlock, + WithdrawAssetsMsg, +}; +use prost::Message; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -14,30 +21,19 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - let instantiated_account_addr = env.contract.address; - CONFIG.save( deps.storage, &Config { owner: deps.api.addr_validate(&msg.owner)?, - creator_addr: info.sender, - job_account_tracker_addr: deps.api.addr_validate(&msg.job_account_tracker_addr)?, + warp_addr: info.sender, }, )?; - Ok(Response::new() - .add_messages(msg.msgs.clone()) .add_attribute("action", "instantiate") - .add_attribute("job_id", msg.job_id) - .add_attribute("contract_addr", instantiated_account_addr) - .add_attribute("job_account_tracker_addr", msg.job_account_tracker_addr) + .add_attribute("contract_addr", env.contract.address) .add_attribute("owner", msg.owner) - .add_attribute( - "native_funds", - serde_json_wasm::to_string(&msg.native_funds)?, - ) - .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.cw_funds)?) - .add_attribute("account_msgs", serde_json_wasm::to_string(&msg.msgs)?)) + .add_attribute("funds", serde_json_wasm::to_string(&info.funds)?) + .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -48,25 +44,25 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.creator_addr { + if info.sender != config.owner && info.sender != config.warp_addr { return Err(ContractError::Unauthorized {}); } match msg { ExecuteMsg::Generic(data) => Ok(Response::new() .add_messages(data.msgs) .add_attribute("action", "generic")), - ExecuteMsg::WithdrawAssets(data) => { - nonpayable(&info).unwrap(); - execute::withdraw::withdraw_assets(deps, env, data, config) - } - ExecuteMsg::IbcTransfer(data) => execute::ibc::ibc_transfer(env, data), + ExecuteMsg::WithdrawAssets(data) => withdraw_assets(deps, env, info, data), + ExecuteMsg::IbcTransfer(data) => ibc_transfer(deps, env, info, data), } } #[cfg_attr(not(feature = "library"), entry_point)] pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { - QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), + QueryMsg::Config => { + let config = CONFIG.load(deps.storage)?; + to_binary(&config) + } } } @@ -74,3 +70,165 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { Ok(Response::new()) } + +pub fn ibc_transfer( + _deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: IbcTransferMsg, +) -> Result { + let mut transfer_msg = msg.transfer_msg.clone(); + + if msg.timeout_block_delta.is_some() && msg.transfer_msg.timeout_block.is_some() { + let block = transfer_msg.timeout_block.unwrap(); + transfer_msg.timeout_block = Some(TimeoutBlock { + revision_number: Some(block.revision_number()), + revision_height: Some(env.block.height + msg.timeout_block_delta.unwrap()), + }) + } + + if msg.timeout_timestamp_seconds_delta.is_some() { + transfer_msg.timeout_timestamp = Some( + env.block + .time + .plus_seconds( + env.block.time.seconds() + msg.timeout_timestamp_seconds_delta.unwrap(), + ) + .nanos(), + ); + } + + Ok(Response::new().add_message(Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: transfer_msg.encode_to_vec().into(), + })) +} + +pub fn withdraw_assets( + deps: DepsMut, + env: Env, + info: MessageInfo, + data: WithdrawAssetsMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner && info.sender != config.warp_addr { + return Err(ContractError::Unauthorized {}); + } + + let mut withdraw_msgs: Vec = vec![]; + + for asset_info in &data.asset_infos { + match asset_info { + AssetInfo::Native(denom) => { + let withdraw_native_msg = + withdraw_asset_native(deps.as_ref(), env.clone(), &config.owner, denom)?; + + match withdraw_native_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw20(addr) => { + let withdraw_cw20_msg = + withdraw_asset_cw20(deps.as_ref(), env.clone(), &config.owner, addr)?; + + match withdraw_cw20_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw721(addr, token_id) => { + let withdraw_cw721_msg = + withdraw_asset_cw721(deps.as_ref(), &config.owner, addr, token_id)?; + match withdraw_cw721_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + } + } + + Ok(Response::new() + .add_messages(withdraw_msgs) + .add_attribute("action", "withdraw_assets") + .add_attribute("assets", serde_json_wasm::to_string(&data.asset_infos)?)) +} + +fn withdraw_asset_native( + deps: Deps, + env: Env, + owner: &Addr, + denom: &String, +) -> StdResult> { + let amount = deps.querier.query_balance(env.contract.address, denom)?; + + let res = if amount.amount > Uint128::zero() { + Some(CosmosMsg::Bank(BankMsg::Send { + to_address: owner.to_string(), + amount: vec![amount], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw20( + deps: Deps, + env: Env, + owner: &Addr, + token: &Addr, +) -> StdResult> { + let amount: BalanceResponse = deps.querier.query_wasm_smart( + token.to_string(), + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + let res = if amount.balance > Uint128::zero() { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: owner.to_string(), + amount: amount.balance, + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw721( + deps: Deps, + owner: &Addr, + token: &Addr, + token_id: &String, +) -> StdResult> { + let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( + token.to_string(), + &Cw721QueryMsg::OwnerOf { + token_id: token_id.to_string(), + include_expired: None, + }, + )?; + + let res = if owner_query.owner == *owner { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw721ExecuteMsg::TransferNft { + recipient: owner.to_string(), + token_id: token_id.to_string(), + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} diff --git a/contracts/warp-legacy-account/src/execute/ibc.rs b/contracts/warp-legacy-account/src/execute/ibc.rs deleted file mode 100644 index 0ded7be5..00000000 --- a/contracts/warp-legacy-account/src/execute/ibc.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::ContractError; -use cosmwasm_std::CosmosMsg::Stargate; -use cosmwasm_std::{Env, Response}; -use legacy_account::{IbcTransferMsg, TimeoutBlock}; -use prost::Message; - -pub fn ibc_transfer(env: Env, data: IbcTransferMsg) -> Result { - let mut transfer_msg = data.transfer_msg.clone(); - - if data.timeout_block_delta.is_some() && data.transfer_msg.timeout_block.is_some() { - let block = transfer_msg.timeout_block.unwrap(); - transfer_msg.timeout_block = Some(TimeoutBlock { - revision_number: Some(block.revision_number()), - revision_height: Some(env.block.height + data.timeout_block_delta.unwrap()), - }) - } - - if data.timeout_timestamp_seconds_delta.is_some() { - transfer_msg.timeout_timestamp = Some( - env.block - .time - .plus_seconds( - env.block.time.seconds() + data.timeout_timestamp_seconds_delta.unwrap(), - ) - .nanos(), - ); - } - - Ok(Response::new().add_message(Stargate { - type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - value: transfer_msg.encode_to_vec().into(), - })) -} diff --git a/contracts/warp-legacy-account/src/execute/mod.rs b/contracts/warp-legacy-account/src/execute/mod.rs deleted file mode 100644 index 2237e0b9..00000000 --- a/contracts/warp-legacy-account/src/execute/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub(crate) mod ibc; -pub(crate) mod withdraw; diff --git a/contracts/warp-legacy-account/src/execute/withdraw.rs b/contracts/warp-legacy-account/src/execute/withdraw.rs deleted file mode 100644 index f29685cb..00000000 --- a/contracts/warp-legacy-account/src/execute/withdraw.rs +++ /dev/null @@ -1,134 +0,0 @@ -use cosmwasm_std::{ - to_binary, Addr, BankMsg, CosmosMsg, Deps, DepsMut, Env, Response, StdResult, Uint128, WasmMsg, -}; -use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use cw721::{Cw721QueryMsg, OwnerOfResponse}; - -use crate::ContractError; - -use controller::account::{AssetInfo, Cw721ExecuteMsg}; -use legacy_account::{Config, WithdrawAssetsMsg}; - -pub fn withdraw_assets( - deps: DepsMut, - env: Env, - data: WithdrawAssetsMsg, - config: Config, -) -> Result { - let mut withdraw_msgs: Vec = vec![]; - - for asset_info in &data.asset_infos { - match asset_info { - AssetInfo::Native(denom) => { - let withdraw_native_msg = - withdraw_asset_native(deps.as_ref(), env.clone(), &config.owner, denom)?; - - match withdraw_native_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw20(addr) => { - let withdraw_cw20_msg = - withdraw_asset_cw20(deps.as_ref(), env.clone(), &config.owner, addr)?; - - match withdraw_cw20_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw721(addr, token_id) => { - let withdraw_cw721_msg = - withdraw_asset_cw721(deps.as_ref(), &config.owner, addr, token_id)?; - match withdraw_cw721_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - } - } - - Ok(Response::new() - .add_messages(withdraw_msgs) - .add_attribute("action", "withdraw_assets") - .add_attribute("assets", serde_json_wasm::to_string(&data.asset_infos)?)) -} - -fn withdraw_asset_native( - deps: Deps, - env: Env, - owner: &Addr, - denom: &String, -) -> StdResult> { - let amount = deps.querier.query_balance(env.contract.address, denom)?; - - let res = if amount.amount > Uint128::zero() { - Some(CosmosMsg::Bank(BankMsg::Send { - to_address: owner.to_string(), - amount: vec![amount], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw20( - deps: Deps, - env: Env, - owner: &Addr, - token: &Addr, -) -> StdResult> { - let amount: BalanceResponse = deps.querier.query_wasm_smart( - token.to_string(), - &cw20::Cw20QueryMsg::Balance { - address: env.contract.address.to_string(), - }, - )?; - - let res = if amount.balance > Uint128::zero() { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: owner.to_string(), - amount: amount.balance, - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw721( - deps: Deps, - owner: &Addr, - token: &Addr, - token_id: &String, -) -> StdResult> { - let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( - token.to_string(), - &Cw721QueryMsg::OwnerOf { - token_id: token_id.to_string(), - include_expired: None, - }, - )?; - - let res = if owner_query.owner == *owner { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw721ExecuteMsg::TransferNft { - recipient: owner.to_string(), - token_id: token_id.to_string(), - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} diff --git a/contracts/warp-legacy-account/src/lib.rs b/contracts/warp-legacy-account/src/lib.rs index aff04ae7..90d6bfa8 100644 --- a/contracts/warp-legacy-account/src/lib.rs +++ b/contracts/warp-legacy-account/src/lib.rs @@ -1,7 +1,5 @@ pub mod contract; mod error; -mod execute; -mod query; pub mod state; #[cfg(test)] diff --git a/contracts/warp-legacy-account/src/query/account.rs b/contracts/warp-legacy-account/src/query/account.rs deleted file mode 100644 index 28d420b7..00000000 --- a/contracts/warp-legacy-account/src/query/account.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::state::CONFIG; -use cosmwasm_std::{Deps, StdResult}; -use legacy_account::ConfigResponse; - -pub fn query_config(deps: Deps) -> StdResult { - let config = CONFIG.load(deps.storage)?; - Ok(ConfigResponse { config }) -} diff --git a/contracts/warp-legacy-account/src/query/mod.rs b/contracts/warp-legacy-account/src/query/mod.rs deleted file mode 100644 index d937534a..00000000 --- a/contracts/warp-legacy-account/src/query/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub(crate) mod account; diff --git a/contracts/warp-legacy-account/src/tests.rs b/contracts/warp-legacy-account/src/tests.rs index a45ca5c3..68871a06 100644 --- a/contracts/warp-legacy-account/src/tests.rs +++ b/contracts/warp-legacy-account/src/tests.rs @@ -1,11 +1,10 @@ +use crate::contract::{execute, instantiate}; +use crate::ContractError; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ - testing::{mock_dependencies, mock_env, mock_info}, to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, - IbcTimeoutBlock, Response, StakingMsg, Uint128, Uint64, VoteOption, WasmMsg, + IbcTimeoutBlock, Response, StakingMsg, Uint128, VoteOption, WasmMsg, }; - -use crate::contract::{execute, instantiate}; -use crate::ContractError; use legacy_account::{ExecuteMsg, GenericMsg, InstantiateMsg}; #[test] @@ -20,11 +19,7 @@ fn test_execute_controller() { info.clone(), InstantiateMsg { owner: "vlad".to_string(), - job_id: Uint64::zero(), - job_account_tracker_addr: "vlad".to_string(), - native_funds: vec![], - cw_funds: vec![], - msgs: vec![], + funds: None, }, ); @@ -146,11 +141,7 @@ fn test_execute_owner() { info, InstantiateMsg { owner: "vlad".to_string(), - job_id: Uint64::zero(), - job_account_tracker_addr: "vlad".to_string(), - native_funds: vec![], - cw_funds: vec![], - msgs: vec![], + funds: None, }, ); @@ -274,11 +265,7 @@ fn test_execute_unauth() { info, InstantiateMsg { owner: "vlad".to_string(), - job_id: Uint64::zero(), - job_account_tracker_addr: "vlad".to_string(), - native_funds: vec![], - cw_funds: vec![], - msgs: vec![], + funds: None, }, ); diff --git a/packages/job-account/src/lib.rs b/packages/job-account/src/lib.rs index bb31abac..bcd0a2e3 100644 --- a/packages/job-account/src/lib.rs +++ b/packages/job-account/src/lib.rs @@ -12,11 +12,6 @@ pub struct Config { // Address of account tracker contract pub job_account_tracker_addr: Addr, - // // Address of current warp account contract - // pub account_addr: Addr, - - // // If occupied, occupied_by_job_id is the job id of the pending job that is using this sub account - // pub occupied_by_job_id: Option, } #[cw_serde] @@ -31,8 +26,7 @@ pub struct InstantiateMsg { // But store it enables us the other way around pub job_account_tracker_addr: String, - // Only required when we are instantiate a main account - // Since we always want to fund sub account, so we will pass this value around and send it to sub account during instantiation in create main account's reply + // Native funds pub native_funds: Vec, // CW20 or CW721 funds, will be transferred to account in reply of account instantiation pub cw_funds: Vec, diff --git a/packages/legacy-account/src/lib.rs b/packages/legacy-account/src/lib.rs index bb31abac..a261a62c 100644 --- a/packages/legacy-account/src/lib.rs +++ b/packages/legacy-account/src/lib.rs @@ -1,43 +1,19 @@ use controller::account::{AssetInfo, CwFund}; -use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Coin as NativeCoin, CosmosMsg, Uint64}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, CosmosMsg}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[cw_serde] pub struct Config { pub owner: Addr, - // Address of warp controller contract - pub creator_addr: Addr, - - // Address of account tracker contract - pub job_account_tracker_addr: Addr, - // // Address of current warp account contract - // pub account_addr: Addr, - - // // If occupied, occupied_by_job_id is the job id of the pending job that is using this sub account - // pub occupied_by_job_id: Option, + pub warp_addr: Addr, } #[cw_serde] pub struct InstantiateMsg { - // User who owns this account pub owner: String, - // ID of the job that is created along with the account - pub job_id: Uint64, - - // Account tracker tracks all accounts owned by user - // Store it inside account for easier lookup, though most of time we only lookup account from account tracker - // But store it enables us the other way around - pub job_account_tracker_addr: String, - - // Only required when we are instantiate a main account - // Since we always want to fund sub account, so we will pass this value around and send it to sub account during instantiation in create main account's reply - pub native_funds: Vec, - // CW20 or CW721 funds, will be transferred to account in reply of account instantiation - pub cw_funds: Vec, - // List of cosmos msgs to execute after instantiating the account - pub msgs: Vec, + pub funds: Option>, } #[cw_serde] @@ -110,19 +86,9 @@ pub struct WithdrawAssetsMsg { #[cw_serde] pub struct ExecuteWasmMsg {} -#[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg { - #[returns(ConfigResponse)] - QueryConfig(QueryConfigMsg), -} - -#[cw_serde] -pub struct QueryConfigMsg {} - -#[cw_serde] -pub struct ConfigResponse { - pub config: Config, + Config, } #[cw_serde] From b402f3ba7317548cc88cf58c6df76d063148110a Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:34:35 -0700 Subject: [PATCH 044/133] update comment --- contracts/warp-controller/src/execute/job.rs | 2 +- contracts/warp-controller/src/reply/account.rs | 4 ++-- contracts/warp-controller/src/reply/job.rs | 2 +- contracts/warp-job-account-tracker/src/state.rs | 2 -- packages/controller/src/job.rs | 7 ++++--- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 5f5a539e..6a8001b9 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -101,7 +101,7 @@ pub fn create_job( prev_id: None, owner: info.sender.clone(), // Account uses a placeholder value for now, will update it to job account address if job account exists or after created - // Update will happen either in create_job (sub account exists) or reply (after creation), so it's atomic + // Update will happen either in create_job (exists free job account) or reply (after creation), so it's atomic // And we guarantee we do not read this value before it's updated account: info.sender.clone(), last_update_time: Uint64::from(env.block.time.seconds()), diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index db0ceda8..32bc0ab5 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -100,7 +100,7 @@ pub fn create_job_account_tracker_and_account_and_job( &deps.api.addr_validate(&job_account_tracker_addr)?, )?; - // Create new sub account then create job in reply + // Create new job account then create job in reply let create_account_and_job_submsg = SubMsg { id: REPLY_ID_CREATE_ACCOUNT_AND_JOB, msg: build_instantiate_warp_account_msg( @@ -263,7 +263,7 @@ pub fn create_account_and_job( )); } - // Occupy sub account + // Occupy job account msgs.push(build_occupy_account_msg( main_account_addr.to_string(), sub_account_addr.to_string(), diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 800b932c..d1c5ffa5 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -205,7 +205,7 @@ pub fn execute_job( // For job not using legacy account, job owner must already have account tracker instantiated let job_account_tracker = JOB_ACCOUNT_TRACKERS.load(deps.storage, &finished_job.owner)?; - // Occupy sub account with the new job + // Occupy job account with the new job msgs.push(build_occupy_account_msg( job_account_tracker.to_string(), job_account_addr.to_string(), diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-job-account-tracker/src/state.rs index 83f700a8..03b30f77 100644 --- a/contracts/warp-job-account-tracker/src/state.rs +++ b/contracts/warp-job-account-tracker/src/state.rs @@ -4,10 +4,8 @@ use job_account_tracker::Config; pub const CONFIG: Item = Item::new("config"); -// OCCUPIED_ACCOUNTS only has value when current account is a main account // Key is the account address, value is the ID of the pending job currently using it pub const OCCUPIED_ACCOUNTS: Map<&Addr, Uint64> = Map::new("occupied_accounts"); -// FREE_ACCOUNTS only has value when current account is a main account // Key is the account address, value is a dummy data that is always true to make it behave like a set pub const FREE_ACCOUNTS: Map<&Addr, bool> = Map::new("free_accounts"); diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 8c182876..bc6ae7f0 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -26,7 +26,8 @@ pub struct Job { pub prev_id: Option, pub owner: Addr, // Warp account this job is associated with, job will be executed in the context of it and pay protocol fee from it - // As job creator can have multiple warp accounts (1 main account and infinite sub accounts) + // As job creator can have infinite job accounts, each job account can only be used by up to 1 active job + // So each job's fund is isolated pub account: Addr, pub last_update_time: Uint64, pub name: String, @@ -58,8 +59,8 @@ pub enum JobStatus { Evicted, } -// Create a job using sub account, if sub account does not exist, create it -// Each sub account will only be used for 1 job, so we achieve funds isolation +// Create a job using job account, if job account does not exist, create it +// Each job account will only be used for 1 job, therefore we achieve funds isolation #[cw_serde] pub struct CreateJobMsg { pub name: String, From bc4f401fa3c26829ba7feedb156057ec75d98572 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Mon, 23 Oct 2023 19:03:31 -0700 Subject: [PATCH 045/133] fix naming --- contracts/warp-controller/src/contract.rs | 14 ++++---- contracts/warp-controller/src/execute/job.rs | 8 ++--- .../warp-controller/src/reply/account.rs | 34 ++++++++++--------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 1a939b44..d0c02909 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -17,11 +17,11 @@ use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State // Reply id for job creation // From a totally new user using warp for the first time, does not have account tracker yet, let alone free account // So we create account account and account and job -pub const REPLY_ID_CREATE_ACCOUNT_TRACKER_AND_ACCOUNT_AND_JOB: u64 = 1; +pub const REPLY_ID_CREATE_JOB_ACCOUNT_TRACKER_AND_JOB_ACCOUNT_AND_JOB: u64 = 1; // Reply id for job creation // From an existing user, who has account tracker, but does not have available account // So we create account and job -pub const REPLY_ID_CREATE_ACCOUNT_AND_JOB: u64 = 2; +pub const REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB: u64 = 2; // Reply id for job execution pub const REPLY_ID_EXECUTE_JOB: u64 = 3; @@ -253,12 +253,14 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { let config = CONFIG.load(deps.storage)?; match msg.id { - // Account tracker has been created, now create account and job - REPLY_ID_CREATE_ACCOUNT_TRACKER_AND_ACCOUNT_AND_JOB => { + // Job account tracker has been created, now create job account and job + REPLY_ID_CREATE_JOB_ACCOUNT_TRACKER_AND_JOB_ACCOUNT_AND_JOB => { reply::account::create_job_account_tracker_and_account_and_job(deps, env, msg, config) } - // Account has been created, now create job - REPLY_ID_CREATE_ACCOUNT_AND_JOB => reply::account::create_account_and_job(deps, env, msg), + // Job account has been created, now create job + REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB => { + reply::account::create_job_account_and_job(deps, env, msg) + } // Job has been executed REPLY_ID_EXECUTE_JOB => reply::job::execute_job(deps, env, msg, config), _ => Err(ContractError::UnknownReplyId {}), diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 6a8001b9..7e39dbdc 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -5,8 +5,8 @@ use cosmwasm_std::{ use crate::{ contract::{ - REPLY_ID_CREATE_ACCOUNT_AND_JOB, REPLY_ID_CREATE_ACCOUNT_TRACKER_AND_ACCOUNT_AND_JOB, - REPLY_ID_EXECUTE_JOB, + REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, + REPLY_ID_CREATE_JOB_ACCOUNT_TRACKER_AND_JOB_ACCOUNT_AND_JOB, REPLY_ID_EXECUTE_JOB, }, state::{JobQueue, JOB_ACCOUNT_TRACKERS, LEGACY_ACCOUNTS, STATE}, util::{ @@ -124,7 +124,7 @@ pub fn create_job( None => { // Create account tracker then create account then create job in reply submsgs.push(SubMsg { - id: REPLY_ID_CREATE_ACCOUNT_TRACKER_AND_ACCOUNT_AND_JOB, + id: REPLY_ID_CREATE_JOB_ACCOUNT_TRACKER_AND_JOB_ACCOUNT_AND_JOB, msg: build_instantiate_warp_job_account_tracker_msg( env.contract.address.to_string(), config.warp_job_account_tracker_code_id.u64(), @@ -150,7 +150,7 @@ pub fn create_job( None => { // Create account then create job in reply submsgs.push(SubMsg { - id: REPLY_ID_CREATE_ACCOUNT_AND_JOB, + id: REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, msg: build_instantiate_warp_account_msg( job.id, env.contract.address.to_string(), diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 32bc0ab5..b928148b 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ use controller::{account::CwFund, Config}; use crate::{ - contract::REPLY_ID_CREATE_ACCOUNT_AND_JOB, + contract::REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, state::{JobQueue, JOB_ACCOUNT_TRACKERS}, util::msg::{ build_account_execute_generic_msgs, build_instantiate_warp_account_msg, @@ -102,7 +102,7 @@ pub fn create_job_account_tracker_and_account_and_job( // Create new job account then create job in reply let create_account_and_job_submsg = SubMsg { - id: REPLY_ID_CREATE_ACCOUNT_AND_JOB, + id: REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, msg: build_instantiate_warp_account_msg( Uint64::from(job_id), env.contract.address.to_string(), @@ -133,7 +133,7 @@ pub fn create_job_account_tracker_and_account_and_job( )) } -pub fn create_account_and_job( +pub fn create_job_account_and_job( mut deps: DepsMut, env: Env, msg: Reply, @@ -168,17 +168,19 @@ pub fn create_account_and_job( .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? .value; - let main_account_addr = deps.api.addr_validate( + let job_account_tracker_addr = deps.api.addr_validate( &event .attributes .iter() .cloned() - .find(|attr| attr.key == "main_account_addr") - .ok_or_else(|| StdError::generic_err("cannot find `main_account_addr` attribute"))? + .find(|attr| attr.key == "job_account_tracker_addr") + .ok_or_else(|| { + StdError::generic_err("cannot find `job_account_tracker_addr` attribute") + })? .value, )?; - let sub_account_addr = deps.api.addr_validate( + let job_account_addr = deps.api.addr_validate( &event .attributes .iter() @@ -219,7 +221,7 @@ pub fn create_account_and_job( )?; let mut job = JobQueue::get(&deps, job_id)?; - job.account = sub_account_addr.clone(); + job.account = job_account_addr.clone(); JobQueue::sync(&mut deps, env, job.clone())?; let mut msgs: Vec = vec![]; @@ -227,7 +229,7 @@ pub fn create_account_and_job( if !native_funds.is_empty() { // Fund account in native coins msgs.push(build_transfer_native_funds_msg( - sub_account_addr.clone().to_string(), + job_account_addr.clone().to_string(), native_funds.clone(), )) } @@ -241,14 +243,14 @@ pub fn create_account_and_job( .addr_validate(&cw20_fund.contract_addr)? .to_string(), owner.clone(), - sub_account_addr.clone().to_string(), + job_account_addr.clone().to_string(), cw20_fund.amount, ), CwFund::Cw721(cw721_fund) => build_transfer_cw721_msg( deps.api .addr_validate(&cw721_fund.contract_addr)? .to_string(), - sub_account_addr.clone().to_string(), + job_account_addr.clone().to_string(), cw721_fund.token_id.clone(), ), }) @@ -258,24 +260,24 @@ pub fn create_account_and_job( if let Some(account_msgs) = account_msgs { // Account execute msgs msgs.push(build_account_execute_generic_msgs( - sub_account_addr.to_string(), + job_account_addr.to_string(), account_msgs, )); } // Occupy job account msgs.push(build_occupy_account_msg( - main_account_addr.to_string(), - sub_account_addr.to_string(), + job_account_tracker_addr.to_string(), + job_account_addr.to_string(), job.id, )); Ok(Response::new() .add_messages(msgs) - .add_attribute("action", "create_account_and_job_reply") + .add_attribute("action", "create_job_account_and_job_reply") // .add_attribute("job_id", value) .add_attribute("owner", owner) - .add_attribute("account_address", sub_account_addr) + .add_attribute("job_account_address", job_account_addr) .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?) .add_attribute( "cw_funds", From bbb56945227f99942a4372ba7331803820912136 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Sun, 29 Oct 2023 23:12:40 +0100 Subject: [PATCH 046/133] new fees logic --- contracts/warp-controller/src/contract.rs | 21 +++++ contracts/warp-controller/src/execute/fee.rs | 87 ++++++++++++++++++++ contracts/warp-controller/src/execute/job.rs | 42 ++++++++-- contracts/warp-controller/src/execute/mod.rs | 1 + packages/controller/src/job.rs | 1 + packages/controller/src/lib.rs | 36 ++++++++ 6 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 contracts/warp-controller/src/execute/fee.rs diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 0e7b30cc..2ea45c5b 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -44,6 +44,16 @@ pub fn instantiate( a_max: msg.a_max, a_min: msg.a_min, q_max: msg.q_max, + creation_fee_min: msg.creation_fee_min, + creation_fee_max: msg.creation_fee_max, + burn_fee_min: msg.burn_fee_min, + maintenance_fee_min: msg.maintenance_fee_min, + maintenance_fee_max: msg.maintenance_fee_max, + duration_days_left: msg.duration_days_left, + duration_days_right: msg.duration_days_right, + queue_size_left: msg.queue_size_left, + queue_size_right: msg.queue_size_right, + burn_fee_rate: msg.burn_fee_rate, }; if config.a_max < config.a_min { @@ -183,6 +193,17 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Uint128 { + // Sigmoid function: 1 / (1 + exp(-x)) + let one = FIXED_POINT_FACTOR; + + // Using the negative exponentiation rule: exp(-x) = 1 / exp(x) + let exp_neg_x = one / exp(x); + one / (one + exp_neg_x) +} + +fn exp(x: Uint128) -> Uint128 { + // For simplicity, we are using the exponential function's Taylor series expansion: + // exp(x) = 1 + x + x^2/2! + x^3/3! + ... + let mut result = FIXED_POINT_FACTOR; + let mut term = FIXED_POINT_FACTOR; + + for n in 1..10 { + term = term * x / Uint128::new(n as u128); + result += term; + } + + result +} + +fn smooth_transition(x: Uint128, min: Uint128, max: Uint128) -> Uint128 { + const K_CONSTANT_FACTOR: Uint128 = Uint128::new(2); + const K_DIVISOR: Uint128 = Uint128::new(10); + + let a = min; + let b = max; + let c = (max + min) / Uint128::new(2); + + let k_constant = K_CONSTANT_FACTOR * (FIXED_POINT_FACTOR / K_DIVISOR); + let sigmoid_val = sigmoid((k_constant * (x - c)) / Uint128::from(FIXED_POINT_FACTOR)); + + a + ((b - a) * sigmoid_val) / Uint128::from(FIXED_POINT_FACTOR) +} + +// can be in native decimals +pub fn compute_creation_fee(queue_size: Uint128, config: &Config) -> Uint128 { + let x1 = config.queue_size_left; + let y1 = config.creation_fee_min; + let x2 = config.queue_size_right; + let y2 = config.creation_fee_max; + + let slope = (y2 - y1) / (x2 - x1); + let y_intercept = y1 - slope * x1; + + if queue_size < x1 { + config.creation_fee_min + } else if queue_size < x2 { + slope * queue_size + y_intercept + } else { + config.creation_fee_max + } +} + +// can be in native decimals +pub fn compute_maintenance_fee(duration_days: Uint128, config: &Config) -> Uint128 { + if duration_days < config.duration_days_left { + config.maintenance_fee_min + } else if duration_days <= config.duration_days_right { + smooth_transition( + duration_days, + config.maintenance_fee_min, + config.maintenance_fee_max, + ) + } else { + config.maintenance_fee_max + } +} + +// can be in native decimals +pub fn compute_burn_fee(job_reward: Uint128, config: &Config) -> Uint128 { + let min_fee = config.burn_fee_min; + let calculated_fee = job_reward * config.burn_fee_rate / Uint128::new(100); + + if calculated_fee > min_fee { + calculated_fee + } else { + min_fee + } +} diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 2a37ed83..044bcd22 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -11,6 +11,8 @@ use cosmwasm_std::{ }; use resolver::QueryHydrateMsgsMsg; +use super::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}; + const MAX_TEXT_LENGTH: usize = 280; pub fn create_job( @@ -35,7 +37,7 @@ pub fn create_job( } let _validate_conditions_and_variables: Option = deps.querier.query_wasm_smart( - config.resolver_address, + &config.resolver_address, &resolver::QueryMsg::QueryValidateJobCreation(resolver::QueryValidateJobCreationMsg { condition: data.condition.clone(), terminate_condition: data.terminate_condition.clone(), @@ -79,8 +81,11 @@ pub fn create_job( }, )?; - // Assume reward.amount == warp token allowance - let fee = data.reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); + let creation_fee = compute_creation_fee(Uint128::from(state.q), &config); + let maintenance_fee = compute_maintenance_fee(data.duration_days, &config); + let burn_fee = compute_burn_fee(data.reward, &config); + + let total_fees = creation_fee + maintenance_fee + burn_fee; let reward_send_msgs = vec![ // Job sends reward to controller @@ -94,13 +99,34 @@ pub fn create_job( }))?, funds: vec![], }, - // Job owner sends fee to fee collector + ]; + + let fee_send_msgs = vec![ WasmMsg::Execute { contract_addr: account.account.to_string(), msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs: vec![CosmosMsg::Bank(BankMsg::Send { to_address: config.fee_collector.to_string(), - amount: vec![Coin::new((fee).u128(), config.fee_denom)], + amount: vec![Coin::new(creation_fee.u128(), config.fee_denom.clone())], + })], + }))?, + funds: vec![], + }, + WasmMsg::Execute { + contract_addr: account.account.to_string(), + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msgs: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: config.fee_collector.to_string(), + amount: vec![Coin::new(maintenance_fee.u128(), config.fee_denom.clone())], + })], + }))?, + funds: vec![], + }, + WasmMsg::Execute { + contract_addr: account.account.to_string(), + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msgs: vec![CosmosMsg::Bank(BankMsg::Burn { + amount: vec![Coin::new(burn_fee.u128(), config.fee_denom.clone())], })], }))?, funds: vec![], @@ -119,6 +145,7 @@ pub fn create_job( Ok(Response::new() .add_messages(reward_send_msgs) + .add_messages(fee_send_msgs) .add_attribute("action", "create_job") .add_attribute("job_id", job.id) .add_attribute("job_owner", job.owner) @@ -127,7 +154,10 @@ pub fn create_job( .add_attribute("job_condition", serde_json_wasm::to_string(&job.condition)?) .add_attribute("job_msgs", serde_json_wasm::to_string(&job.msgs)?) .add_attribute("job_reward", job.reward) - .add_attribute("job_creation_fee", fee) + .add_attribute("job_creation_fee", creation_fee.to_string()) + .add_attribute("job_maintenance_fee", maintenance_fee.to_string()) + .add_attribute("job_burn_fee", burn_fee.to_string()) + .add_attribute("job_total_fees", total_fees.to_string()) .add_attribute("job_last_updated_time", job.last_update_time) .add_messages(account_msgs)) } diff --git a/contracts/warp-controller/src/execute/mod.rs b/contracts/warp-controller/src/execute/mod.rs index ee75d71c..e4a83744 100644 --- a/contracts/warp-controller/src/execute/mod.rs +++ b/contracts/warp-controller/src/execute/mod.rs @@ -1,3 +1,4 @@ pub(crate) mod account; pub(crate) mod controller; +pub(crate) mod fee; pub(crate) mod job; diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index e40c8ab6..60e73b4a 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -70,6 +70,7 @@ pub struct CreateJobMsg { pub recurring: bool, pub requeue_on_evict: bool, pub reward: Uint128, + pub duration_days: Uint128, pub assets_to_withdraw: Option>, pub account_msgs: Option>, } diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 9c47f32e..f1fec7c1 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -22,6 +22,18 @@ pub struct Config { pub creation_fee_percentage: Uint64, pub cancellation_fee_percentage: Uint64, pub resolver_address: Addr, + pub creation_fee_min: Uint128, + pub creation_fee_max: Uint128, + pub burn_fee_min: Uint128, + pub maintenance_fee_min: Uint128, + pub maintenance_fee_max: Uint128, + // duration_days fn interval [left, right] + pub duration_days_left: Uint128, + pub duration_days_right: Uint128, + // queue_size fn interval [left, right] + pub queue_size_left: Uint128, + pub queue_size_right: Uint128, + pub burn_fee_rate: Uint128, // maximum time for evictions pub t_max: Uint64, // minimum time for evictions @@ -57,6 +69,18 @@ pub struct InstantiateMsg { pub a_max: Uint128, pub a_min: Uint128, pub q_max: Uint64, + pub creation_fee_min: Uint128, + pub creation_fee_max: Uint128, + pub burn_fee_min: Uint128, + pub maintenance_fee_min: Uint128, + pub maintenance_fee_max: Uint128, + // duration_days fn interval [left, right] + pub duration_days_left: Uint128, + pub duration_days_right: Uint128, + // queue_size fn interval [left, right] + pub queue_size_left: Uint128, + pub queue_size_right: Uint128, + pub burn_fee_rate: Uint128, } //execute @@ -89,6 +113,18 @@ pub struct UpdateConfigMsg { pub a_max: Option, pub a_min: Option, pub q_max: Option, + pub creation_fee_min: Option, + pub creation_fee_max: Option, + pub burn_fee_min: Option, + pub maintenance_fee_min: Option, + pub maintenance_fee_max: Option, + // duration_days fn interval [left, right] + pub duration_days_left: Option, + pub duration_days_right: Option, + // queue_size fn interval [left, right] + pub queue_size_left: Option, + pub queue_size_right: Option, + pub burn_fee_rate: Option, } #[cw_serde] From 238adc53ed27fa9be1e8affeb019f4ba83b9cf70 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 30 Oct 2023 18:15:16 +0100 Subject: [PATCH 047/133] change maintenance fee to linear function + resolve uint overflows --- contracts/warp-controller/src/execute/fee.rs | 64 ++++---------------- 1 file changed, 12 insertions(+), 52 deletions(-) diff --git a/contracts/warp-controller/src/execute/fee.rs b/contracts/warp-controller/src/execute/fee.rs index 1f91dfdd..e95acfbd 100644 --- a/contracts/warp-controller/src/execute/fee.rs +++ b/contracts/warp-controller/src/execute/fee.rs @@ -1,46 +1,6 @@ use controller::Config; use cosmwasm_std::Uint128; -const FIXED_POINT_FACTOR: Uint128 = Uint128::new(1_000_000u128); - -fn sigmoid(x: Uint128) -> Uint128 { - // Sigmoid function: 1 / (1 + exp(-x)) - let one = FIXED_POINT_FACTOR; - - // Using the negative exponentiation rule: exp(-x) = 1 / exp(x) - let exp_neg_x = one / exp(x); - one / (one + exp_neg_x) -} - -fn exp(x: Uint128) -> Uint128 { - // For simplicity, we are using the exponential function's Taylor series expansion: - // exp(x) = 1 + x + x^2/2! + x^3/3! + ... - let mut result = FIXED_POINT_FACTOR; - let mut term = FIXED_POINT_FACTOR; - - for n in 1..10 { - term = term * x / Uint128::new(n as u128); - result += term; - } - - result -} - -fn smooth_transition(x: Uint128, min: Uint128, max: Uint128) -> Uint128 { - const K_CONSTANT_FACTOR: Uint128 = Uint128::new(2); - const K_DIVISOR: Uint128 = Uint128::new(10); - - let a = min; - let b = max; - let c = (max + min) / Uint128::new(2); - - let k_constant = K_CONSTANT_FACTOR * (FIXED_POINT_FACTOR / K_DIVISOR); - let sigmoid_val = sigmoid((k_constant * (x - c)) / Uint128::from(FIXED_POINT_FACTOR)); - - a + ((b - a) * sigmoid_val) / Uint128::from(FIXED_POINT_FACTOR) -} - -// can be in native decimals pub fn compute_creation_fee(queue_size: Uint128, config: &Config) -> Uint128 { let x1 = config.queue_size_left; let y1 = config.creation_fee_min; @@ -48,35 +8,35 @@ pub fn compute_creation_fee(queue_size: Uint128, config: &Config) -> Uint128 { let y2 = config.creation_fee_max; let slope = (y2 - y1) / (x2 - x1); - let y_intercept = y1 - slope * x1; if queue_size < x1 { config.creation_fee_min } else if queue_size < x2 { - slope * queue_size + y_intercept + slope * queue_size + y1 - slope * x1 } else { config.creation_fee_max } } -// can be in native decimals pub fn compute_maintenance_fee(duration_days: Uint128, config: &Config) -> Uint128 { - if duration_days < config.duration_days_left { + let x1 = config.duration_days_left; + let y1 = config.maintenance_fee_min; + let x2 = config.duration_days_right; + let y2 = config.maintenance_fee_max; + + let slope = (y2 - y1) / (x2 - x1); + + if duration_days < x1 { config.maintenance_fee_min - } else if duration_days <= config.duration_days_right { - smooth_transition( - duration_days, - config.maintenance_fee_min, - config.maintenance_fee_max, - ) + } else if duration_days < x2 { + slope * duration_days + y1 - slope * x1 } else { config.maintenance_fee_max } } -// can be in native decimals pub fn compute_burn_fee(job_reward: Uint128, config: &Config) -> Uint128 { - let min_fee = config.burn_fee_min; + let min_fee: Uint128 = config.burn_fee_min; let calculated_fee = job_reward * config.burn_fee_rate / Uint128::new(100); if calculated_fee > min_fee { From 769e60b9ad3989eb4e9a8fad675e7a71eb528275 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 30 Oct 2023 18:23:08 +0100 Subject: [PATCH 048/133] add migrate code --- contracts/warp-controller/src/contract.rs | 45 +++++++---------------- packages/controller/src/lib.rs | 35 +++++++++++------- 2 files changed, 35 insertions(+), 45 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 2ea45c5b..2fd3b5c9 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -132,25 +132,6 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { #[cfg_attr(not(feature = "library"), entry_point)] pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { - //STATE - #[cw_serde] - pub struct V1State { - pub current_job_id: Uint64, - pub current_template_id: Uint64, - pub q: Uint64, - } - - const V1STATE: Item = Item::new("state"); - let v1_state = V1STATE.load(deps.storage)?; - - STATE.save( - deps.storage, - &State { - current_job_id: v1_state.current_job_id, - q: v1_state.q, - }, - )?; - //CONFIG #[cw_serde] pub struct V1Config { @@ -161,6 +142,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result Date: Thu, 2 Nov 2023 14:30:53 +0100 Subject: [PATCH 049/133] add job executions (switch statements) --- contracts/warp-controller/src/contract.rs | 11 +-- .../warp-controller/src/execute/controller.rs | 8 +- contracts/warp-controller/src/execute/job.rs | 96 +++++++++---------- contracts/warp-controller/src/state.rs | 9 +- contracts/warp-resolver/src/contract.rs | 77 +++++++-------- packages/controller/src/job.rs | 12 ++- packages/resolver/src/lib.rs | 8 +- 7 files changed, 108 insertions(+), 113 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 2fd3b5c9..c6b0d222 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -439,12 +439,11 @@ pub fn reply(mut deps: DepsMut, env: Env, msg: Reply) -> Result Result = deps.querier.query_wasm_smart( &config.resolver_address, &resolver::QueryMsg::QueryValidateJobCreation(resolver::QueryValidateJobCreationMsg { - condition: data.condition.clone(), terminate_condition: data.terminate_condition.clone(), vars: data.vars.clone(), - msgs: data.msgs.clone(), + executions: data.executions.clone(), }), )?; @@ -68,12 +67,11 @@ pub fn create_job( last_update_time: Uint64::from(env.block.time.seconds()), name: data.name, status: JobStatus::Pending, - condition: data.condition.clone(), terminate_condition: data.terminate_condition, recurring: data.recurring, requeue_on_evict: data.requeue_on_evict, vars: data.vars, - msgs: data.msgs, + executions: data.executions, reward: data.reward, description: data.description, labels: data.labels, @@ -151,8 +149,10 @@ pub fn create_job( .add_attribute("job_owner", job.owner) .add_attribute("job_name", job.name) .add_attribute("job_status", serde_json_wasm::to_string(&job.status)?) - .add_attribute("job_condition", serde_json_wasm::to_string(&job.condition)?) - .add_attribute("job_msgs", serde_json_wasm::to_string(&job.msgs)?) + .add_attribute( + "job_executions", + serde_json_wasm::to_string(&job.executions)?, + ) .add_attribute("job_reward", job.reward) .add_attribute("job_creation_fee", creation_fee.to_string()) .add_attribute("job_maintenance_fee", maintenance_fee.to_string()) @@ -276,8 +276,10 @@ pub fn update_job( .add_attribute("job_owner", job.owner) .add_attribute("job_name", job.name) .add_attribute("job_status", serde_json_wasm::to_string(&job.status)?) - .add_attribute("job_condition", serde_json_wasm::to_string(&job.condition)?) - .add_attribute("job_msgs", serde_json_wasm::to_string(&job.msgs)?) + .add_attribute( + "job_executions", + serde_json_wasm::to_string(&job.executions)?, + ) .add_attribute("job_reward", job.reward) .add_attribute("job_update_fee", fee) .add_attribute("job_last_updated_time", job.last_update_time)) @@ -306,49 +308,47 @@ pub fn execute_job( }), )?; - let resolution: StdResult = deps.querier.query_wasm_smart( - config.resolver_address.clone(), - &resolver::QueryMsg::QueryResolveCondition(resolver::QueryResolveConditionMsg { - condition: job.condition, - vars: vars.clone(), - warp_account_addr: Some(job.account.to_string()), - }), - ); - let mut attrs = vec![]; let mut submsgs = vec![]; - if let Err(e) = resolution { - attrs.push(Attribute::new("job_condition_status", "invalid")); - attrs.push(Attribute::new("error", e.to_string())); - JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Failed)?; - } else { - attrs.push(Attribute::new("job_condition_status", "valid")); - if !resolution? { - return Ok(Response::new() - .add_attribute("action", "execute_job") - .add_attribute("condition", "false") - .add_attribute("job_id", job.id)); + for Execution { condition, msgs } in job.executions { + let resolution: StdResult = deps + .querier + .query_wasm_smart(config.resolver_address.clone(), &condition); + + match resolution { + Ok(true) => { + submsgs.push(SubMsg { + id: job.id.u64(), + msg: CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: job.account.to_string(), + msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { + msgs: deps.querier.query_wasm_smart( + config.resolver_address, + &resolver::QueryMsg::QueryHydrateMsgs(QueryHydrateMsgsMsg { + msgs, + vars, + }), + )?, + }))?, + funds: vec![], + }), + gas_limit: None, + reply_on: ReplyOn::Always, + }); + break; + } + Ok(false) => { + // Continue to the next condition + continue; + } + Err(e) => { + attrs.push(Attribute::new("job_condition_status", "invalid")); + attrs.push(Attribute::new("error", e.to_string())); + JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Failed)?; + break; + } } - - submsgs.push(SubMsg { - id: job.id.u64(), - msg: CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: job.account.to_string(), - msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: deps.querier.query_wasm_smart( - config.resolver_address, - &resolver::QueryMsg::QueryHydrateMsgs(QueryHydrateMsgsMsg { - msgs: job.msgs, - vars, - }), - )?, - }))?, - funds: vec![], - }), - gas_limit: None, - reply_on: ReplyOn::Always, - }); } // Controller sends reward to executor diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 2fd89b20..7007de58 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -115,9 +115,8 @@ impl JobQueue { description: job.description, labels: job.labels, status: JobStatus::Pending, - condition: job.condition, + executions: job.executions, terminate_condition: job.terminate_condition, - msgs: job.msgs, vars: job.vars, recurring: job.recurring, requeue_on_evict: job.requeue_on_evict, @@ -147,9 +146,8 @@ impl JobQueue { description: data.description.unwrap_or(job.description), labels: data.labels.unwrap_or(job.labels), status: job.status, - condition: job.condition, + executions: job.executions, terminate_condition: job.terminate_condition, - msgs: job.msgs, vars: job.vars, recurring: job.recurring, requeue_on_evict: job.requeue_on_evict, @@ -181,9 +179,8 @@ impl JobQueue { description: job.description, labels: job.labels, status, - condition: job.condition, terminate_condition: job.terminate_condition, - msgs: job.msgs, + executions: job.executions, vars: job.vars, recurring: job.recurring, requeue_on_evict: job.requeue_on_evict, diff --git a/contracts/warp-resolver/src/contract.rs b/contracts/warp-resolver/src/contract.rs index cae93682..31971b29 100644 --- a/contracts/warp-resolver/src/contract.rs +++ b/contracts/warp-resolver/src/contract.rs @@ -73,10 +73,9 @@ pub fn execute_validate_job_creation( deps.as_ref(), env, QueryValidateJobCreationMsg { - condition: data.condition, terminate_condition: data.terminate_condition, vars: data.vars, - msgs: data.msgs, + executions: data.executions }, )?; @@ -195,46 +194,48 @@ fn query_validate_job_creation( _env: Env, data: QueryValidateJobCreationMsg, ) -> StdResult { - let _condition: Condition = serde_json_wasm::from_str(&data.condition) - .map_err(|e| StdError::generic_err(format!("Condition input invalid: {}", e)))?; - let terminate_condition_str = data.terminate_condition.clone().unwrap_or("".to_string()); - if !terminate_condition_str.is_empty() { - let _terminate_condition: Condition = serde_json_wasm::from_str(&terminate_condition_str) - .map_err(|e| { - StdError::generic_err(format!("Terminate condition input invalid: {}", e)) - })?; - } - let vars: Vec = serde_json_wasm::from_str(&data.vars) - .map_err(|e| StdError::generic_err(format!("Vars input invalid: {}", e)))?; + for execution in data.executions { + let _condition: Condition = serde_json_wasm::from_str(&execution.condition) + .map_err(|e| StdError::generic_err(format!("Condition input invalid: {}", e)))?; + let terminate_condition_str = data.terminate_condition.clone().unwrap_or("".to_string()); + if !terminate_condition_str.is_empty() { + let _terminate_condition: Condition = + serde_json_wasm::from_str(&terminate_condition_str).map_err(|e| { + StdError::generic_err(format!("Terminate condition input invalid: {}", e)) + })?; + } + let vars: Vec = serde_json_wasm::from_str(&data.vars) + .map_err(|e| StdError::generic_err(format!("Vars input invalid: {}", e)))?; - if !vars_valid(&vars) { - return Err(StdError::generic_err( - ContractError::InvalidVariables {}.to_string(), - )); - } + if !vars_valid(&vars) { + return Err(StdError::generic_err( + ContractError::InvalidVariables {}.to_string(), + )); + } - if has_duplicates(&vars) { - return Err(StdError::generic_err( - ContractError::VariablesContainDuplicates {}.to_string(), - )); - } + if has_duplicates(&vars) { + return Err(StdError::generic_err( + ContractError::VariablesContainDuplicates {}.to_string(), + )); + } - if !(string_vars_in_vector(&vars, &data.condition) - && string_vars_in_vector(&vars, &terminate_condition_str) - && string_vars_in_vector(&vars, &data.msgs)) - { - return Err(StdError::generic_err( - ContractError::VariablesMissingFromVector {}.to_string(), - )); - } + if !(string_vars_in_vector(&vars, &execution.condition) + && string_vars_in_vector(&vars, &terminate_condition_str) + && string_vars_in_vector(&vars, &execution.msgs)) + { + return Err(StdError::generic_err( + ContractError::VariablesMissingFromVector {}.to_string(), + )); + } - if !msgs_valid(&data.msgs, &vars).map_err(|e| StdError::generic_err(e.to_string()))? { - return Err(StdError::generic_err( - ContractError::MsgError { - msg: "msgs are invalid".to_string(), - } - .to_string(), - )); + if !msgs_valid(&execution.msgs, &vars).map_err(|e| StdError::generic_err(e.to_string()))? { + return Err(StdError::generic_err( + ContractError::MsgError { + msg: "msgs are invalid".to_string(), + } + .to_string(), + )); + } } Ok("".to_string()) diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 60e73b4a..6447b83b 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -33,9 +33,8 @@ pub struct Job { pub description: String, pub labels: Vec, pub status: JobStatus, - pub condition: String, pub terminate_condition: Option, - pub msgs: String, + pub executions: Vec, pub vars: String, pub recurring: bool, pub requeue_on_evict: bool, @@ -58,14 +57,19 @@ pub enum JobStatus { Evicted, } +#[cw_serde] +pub struct Execution { + pub condition: String, + pub msgs: String, +} + #[cw_serde] pub struct CreateJobMsg { pub name: String, pub description: String, pub labels: Vec, - pub condition: String, pub terminate_condition: Option, - pub msgs: String, + pub executions: Vec, pub vars: String, pub recurring: bool, pub requeue_on_evict: bool, diff --git a/packages/resolver/src/lib.rs b/packages/resolver/src/lib.rs index ef827c9b..0f232b78 100644 --- a/packages/resolver/src/lib.rs +++ b/packages/resolver/src/lib.rs @@ -1,7 +1,7 @@ pub mod condition; pub mod variable; -use controller::job::{ExternalInput, JobStatus}; +use controller::job::{ExternalInput, JobStatus, Execution}; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{CosmosMsg, QueryRequest}; #[cw_serde] @@ -71,18 +71,16 @@ pub struct ExecuteApplyVarFnMsg { #[cw_serde] pub struct ExecuteValidateJobCreationMsg { - pub condition: String, pub terminate_condition: Option, pub vars: String, - pub msgs: String, + pub executions: Vec } #[cw_serde] pub struct QueryValidateJobCreationMsg { - pub condition: String, pub terminate_condition: Option, pub vars: String, - pub msgs: String, + pub executions: Vec, } #[cw_serde] From 257126f463d3d9fb7830346685750f959e2f72c2 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 2 Nov 2023 16:25:44 +0100 Subject: [PATCH 050/133] add migration code for executions --- .../warp-controller/src/execute/controller.rs | 71 ++++++++++--------- contracts/warp-resolver/src/contract.rs | 2 +- packages/resolver/src/lib.rs | 4 +- 3 files changed, 39 insertions(+), 38 deletions(-) diff --git a/contracts/warp-controller/src/execute/controller.rs b/contracts/warp-controller/src/execute/controller.rs index 4badc734..944dfc91 100644 --- a/contracts/warp-controller/src/execute/controller.rs +++ b/contracts/warp-controller/src/execute/controller.rs @@ -4,18 +4,17 @@ use controller::{MigrateAccountsMsg, MigrateJobsMsg, UpdateConfigMsg}; use cosmwasm_schema::cw_serde; use controller::account::AssetInfo; -use controller::job::{Job, JobStatus}; +use controller::job::{Execution, Job, JobStatus}; use cosmwasm_std::{ - to_binary, Addr, DepsMut, Env, MessageInfo, Order, Response, Uint128, Uint64, WasmMsg, + to_binary, Addr, DepsMut, Env, MessageInfo, Order, Response, StdError, Uint128, Uint64, WasmMsg, }; use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, MultiIndex, UniqueIndex}; -use resolver::condition::{Condition, StringValue}; +use resolver::condition::StringValue; use resolver::variable::{ ExternalExpr, ExternalVariable, FnValue, QueryExpr, QueryVariable, StaticVariable, UpdateFn, Variable, VariableKind, }; -//JOBS #[cw_serde] pub struct V1Job { pub id: Uint64, @@ -25,9 +24,10 @@ pub struct V1Job { pub description: String, pub labels: Vec, pub status: JobStatus, - pub condition: Condition, - pub msgs: Vec, - pub vars: Vec, + pub terminate_condition: Option, + pub condition: String, + pub msgs: String, + pub vars: String, pub recurring: bool, pub requeue_on_evict: bool, pub reward: Uint128, @@ -47,12 +47,14 @@ pub struct V1StaticVariable { pub name: String, pub value: String, pub update_fn: Option, + pub encode: bool, } #[cw_serde] pub struct V1ExternalVariable { pub kind: VariableKind, pub name: String, + pub encode: bool, pub init_fn: ExternalExpr, pub reinitialize: bool, pub value: Option, //none if uninitialized @@ -63,6 +65,7 @@ pub struct V1ExternalVariable { pub struct V1QueryVariable { pub kind: VariableKind, pub name: String, + pub encode: bool, pub init_fn: QueryExpr, pub reinitialize: bool, pub value: Option, //none if uninitialized @@ -228,15 +231,20 @@ pub fn migrate_pending_jobs( .take(msg.limit as usize) .collect(); let job_keys = job_keys?; + for job_key in job_keys { let v1_job = V1_PENDING_JOBS().load(deps.storage, job_key)?; let mut new_vars = vec![]; - for var in v1_job.vars { + + let job_vars: Vec = serde_json_wasm::from_str(&v1_job.vars) + .map_err(|e| StdError::generic_err(e.to_string()))?; + + for var in job_vars { new_vars.push(match var { V1Variable::Static(v) => Variable::Static(StaticVariable { kind: v.kind, name: v.name, - encode: false, + encode: v.encode, init_fn: FnValue::String(StringValue::Simple(v.value.clone())), reinitialize: false, value: Some(v.value.clone()), @@ -245,7 +253,7 @@ pub fn migrate_pending_jobs( V1Variable::External(v) => Variable::External(ExternalVariable { kind: v.kind, name: v.name, - encode: false, + encode: v.encode, init_fn: v.init_fn, reinitialize: v.reinitialize, value: v.value, @@ -254,7 +262,7 @@ pub fn migrate_pending_jobs( V1Variable::Query(v) => Variable::Query(QueryVariable { kind: v.kind, name: v.name, - encode: false, + encode: v.encode, init_fn: v.init_fn, reinitialize: v.reinitialize, value: v.value, @@ -263,14 +271,6 @@ pub fn migrate_pending_jobs( }) } - let mut new_msgs = "[".to_string(); - - for msg in v1_job.msgs { - new_msgs.push_str(msg.as_str()); - } - - new_msgs.push(']'); - let warp_account = ACCOUNTS().load(deps.storage, v1_job.owner.clone())?; PENDING_JOBS().save( @@ -287,8 +287,10 @@ pub fn migrate_pending_jobs( labels: v1_job.labels, status: v1_job.status, terminate_condition: None, - // TODO: migrate executions - executions: vec![], + executions: vec![Execution { + condition: v1_job.condition, + msgs: v1_job.msgs, + }], vars: serde_json_wasm::to_string(&new_vars)?, recurring: v1_job.recurring, requeue_on_evict: v1_job.requeue_on_evict, @@ -336,15 +338,20 @@ pub fn migrate_finished_jobs( .take(msg.limit as usize) .collect(); let job_keys = job_keys?; + for job_key in job_keys { let v1_job = V1_FINISHED_JOBS().load(deps.storage, job_key)?; let mut new_vars = vec![]; - for var in v1_job.vars { + + let job_vars: Vec = serde_json_wasm::from_str(&v1_job.vars) + .map_err(|e| StdError::generic_err(e.to_string()))?; + + for var in job_vars { new_vars.push(match var { V1Variable::Static(v) => Variable::Static(StaticVariable { kind: v.kind, name: v.name, - encode: false, + encode: v.encode, init_fn: FnValue::String(StringValue::Simple(v.value.clone())), reinitialize: false, value: Some(v.value.clone()), @@ -353,7 +360,7 @@ pub fn migrate_finished_jobs( V1Variable::External(v) => Variable::External(ExternalVariable { kind: v.kind, name: v.name, - encode: false, + encode: v.encode, init_fn: v.init_fn, reinitialize: v.reinitialize, value: v.value, @@ -362,7 +369,7 @@ pub fn migrate_finished_jobs( V1Variable::Query(v) => Variable::Query(QueryVariable { kind: v.kind, name: v.name, - encode: false, + encode: v.encode, init_fn: v.init_fn, reinitialize: v.reinitialize, value: v.value, @@ -371,14 +378,6 @@ pub fn migrate_finished_jobs( }) } - let mut new_msgs = "[".to_string(); - - for msg in v1_job.msgs { - new_msgs.push_str(msg.as_str()); - } - - new_msgs.push(']'); - let warp_account = ACCOUNTS().load(deps.storage, v1_job.owner.clone())?; FINISHED_JOBS().save( @@ -394,8 +393,10 @@ pub fn migrate_finished_jobs( description: v1_job.description, labels: v1_job.labels, status: v1_job.status, - // TODO: migrate executions - executions: vec![], + executions: vec![Execution { + condition: v1_job.condition, + msgs: v1_job.msgs, + }], terminate_condition: None, vars: serde_json_wasm::to_string(&new_vars)?, recurring: v1_job.recurring, diff --git a/contracts/warp-resolver/src/contract.rs b/contracts/warp-resolver/src/contract.rs index 31971b29..be74cd8a 100644 --- a/contracts/warp-resolver/src/contract.rs +++ b/contracts/warp-resolver/src/contract.rs @@ -75,7 +75,7 @@ pub fn execute_validate_job_creation( QueryValidateJobCreationMsg { terminate_condition: data.terminate_condition, vars: data.vars, - executions: data.executions + executions: data.executions, }, )?; diff --git a/packages/resolver/src/lib.rs b/packages/resolver/src/lib.rs index 0f232b78..6ddfd23c 100644 --- a/packages/resolver/src/lib.rs +++ b/packages/resolver/src/lib.rs @@ -1,7 +1,7 @@ pub mod condition; pub mod variable; -use controller::job::{ExternalInput, JobStatus, Execution}; +use controller::job::{Execution, ExternalInput, JobStatus}; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{CosmosMsg, QueryRequest}; #[cw_serde] @@ -73,7 +73,7 @@ pub struct ExecuteApplyVarFnMsg { pub struct ExecuteValidateJobCreationMsg { pub terminate_condition: Option, pub vars: String, - pub executions: Vec + pub executions: Vec, } #[cw_serde] From 6ff56af7faa15827727dfeb01934f72c8f78f42f Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 2 Nov 2023 16:37:29 +0100 Subject: [PATCH 051/133] rename v1_jobs to old_jobs + add new table --- .../warp-controller/src/execute/controller.rs | 132 +++++++++--------- contracts/warp-controller/src/state.rs | 16 +-- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/contracts/warp-controller/src/execute/controller.rs b/contracts/warp-controller/src/execute/controller.rs index 944dfc91..2493fc35 100644 --- a/contracts/warp-controller/src/execute/controller.rs +++ b/contracts/warp-controller/src/execute/controller.rs @@ -16,7 +16,7 @@ use resolver::variable::{ }; #[cw_serde] -pub struct V1Job { +pub struct OldJob { pub id: Uint64, pub owner: Addr, pub last_update_time: Uint64, @@ -35,14 +35,14 @@ pub struct V1Job { } #[cw_serde] -pub enum V1Variable { - Static(V1StaticVariable), - External(V1ExternalVariable), - Query(V1QueryVariable), +pub enum OldVariable { + Static(OldStaticVariable), + External(OldExternalVariable), + Query(OldQueryVariable), } #[cw_serde] -pub struct V1StaticVariable { +pub struct OldStaticVariable { pub kind: VariableKind, pub name: String, pub value: String, @@ -51,7 +51,7 @@ pub struct V1StaticVariable { } #[cw_serde] -pub struct V1ExternalVariable { +pub struct OldExternalVariable { pub kind: VariableKind, pub name: String, pub encode: bool, @@ -62,7 +62,7 @@ pub struct V1ExternalVariable { } #[cw_serde] -pub struct V1QueryVariable { +pub struct OldQueryVariable { pub kind: VariableKind, pub name: String, pub encode: bool, @@ -72,14 +72,14 @@ pub struct V1QueryVariable { pub update_fn: Option, } -pub struct V1JobIndexes<'a> { - pub reward: UniqueIndex<'a, (u128, u64), V1Job>, - pub publish_time: MultiIndex<'a, u64, V1Job, u64>, +pub struct OldJobIndexes<'a> { + pub reward: UniqueIndex<'a, (u128, u64), OldJob>, + pub publish_time: MultiIndex<'a, u64, OldJob, u64>, } -impl IndexList for V1JobIndexes<'_> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.reward, &self.publish_time]; +impl IndexList for OldJobIndexes<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.reward, &self.publish_time]; Box::new(v.into_iter()) } } @@ -211,37 +211,37 @@ pub fn migrate_pending_jobs( let start_after = start_after.map(Bound::exclusive); #[allow(non_snake_case)] - pub fn V1_PENDING_JOBS<'a>() -> IndexedMap<'a, u64, V1Job, V1JobIndexes<'a>> { - let indexes = V1JobIndexes { + pub fn OLD_PENDING_JOBS<'a>() -> IndexedMap<'a, u64, OldJob, OldJobIndexes<'a>> { + let indexes = OldJobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "pending_jobs__reward_v2", + "pending_jobs__reward_v3", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "pending_jobs_v2", - "pending_jobs__publish_timestamp_v2", + "pending_jobs_v3", + "pending_jobs__publish_timestamp_v3", ), }; - IndexedMap::new("pending_jobs_v2", indexes) + IndexedMap::new("pending_jobs_v3", indexes) } - let job_keys: Result, _> = V1_PENDING_JOBS() + let job_keys: Result, _> = OLD_PENDING_JOBS() .keys(deps.storage, start_after, None, Order::Ascending) .take(msg.limit as usize) .collect(); let job_keys = job_keys?; for job_key in job_keys { - let v1_job = V1_PENDING_JOBS().load(deps.storage, job_key)?; + let old_job = OLD_PENDING_JOBS().load(deps.storage, job_key)?; let mut new_vars = vec![]; - let job_vars: Vec = serde_json_wasm::from_str(&v1_job.vars) + let job_vars: Vec = serde_json_wasm::from_str(&old_job.vars) .map_err(|e| StdError::generic_err(e.to_string()))?; for var in job_vars { new_vars.push(match var { - V1Variable::Static(v) => Variable::Static(StaticVariable { + OldVariable::Static(v) => Variable::Static(StaticVariable { kind: v.kind, name: v.name, encode: v.encode, @@ -250,7 +250,7 @@ pub fn migrate_pending_jobs( value: Some(v.value.clone()), update_fn: v.update_fn, }), - V1Variable::External(v) => Variable::External(ExternalVariable { + OldVariable::External(v) => Variable::External(ExternalVariable { kind: v.kind, name: v.name, encode: v.encode, @@ -259,7 +259,7 @@ pub fn migrate_pending_jobs( value: v.value, update_fn: v.update_fn, }), - V1Variable::Query(v) => Variable::Query(QueryVariable { + OldVariable::Query(v) => Variable::Query(QueryVariable { kind: v.kind, name: v.name, encode: v.encode, @@ -271,31 +271,31 @@ pub fn migrate_pending_jobs( }) } - let warp_account = ACCOUNTS().load(deps.storage, v1_job.owner.clone())?; + let warp_account = ACCOUNTS().load(deps.storage, old_job.owner.clone())?; PENDING_JOBS().save( deps.storage, job_key, &Job { - id: v1_job.id, + id: old_job.id, prev_id: None, - owner: v1_job.owner, + owner: old_job.owner, account: warp_account.account, - last_update_time: v1_job.last_update_time, - name: v1_job.name, - description: v1_job.description, - labels: v1_job.labels, - status: v1_job.status, + last_update_time: old_job.last_update_time, + name: old_job.name, + description: old_job.description, + labels: old_job.labels, + status: old_job.status, terminate_condition: None, executions: vec![Execution { - condition: v1_job.condition, - msgs: v1_job.msgs, + condition: old_job.condition, + msgs: old_job.msgs, }], vars: serde_json_wasm::to_string(&new_vars)?, - recurring: v1_job.recurring, - requeue_on_evict: v1_job.requeue_on_evict, - reward: v1_job.reward, - assets_to_withdraw: v1_job.assets_to_withdraw, + recurring: old_job.recurring, + requeue_on_evict: old_job.requeue_on_evict, + reward: old_job.reward, + assets_to_withdraw: old_job.assets_to_withdraw, }, )?; } @@ -318,37 +318,37 @@ pub fn migrate_finished_jobs( let start_after = start_after.map(Bound::exclusive); #[allow(non_snake_case)] - pub fn V1_FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, V1Job, V1JobIndexes<'a>> { - let indexes = V1JobIndexes { + pub fn OLD_FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, OldJob, OldJobIndexes<'a>> { + let indexes = OldJobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "finished_jobs__reward_v2", + "finished_jobs__reward_v3", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "finished_jobs_v2", - "finished_jobs__publish_timestamp_v2", + "finished_jobs_v3", + "finished_jobs__publish_timestamp_v3", ), }; - IndexedMap::new("finished_jobs_v2", indexes) + IndexedMap::new("finished_jobs_v3", indexes) } - let job_keys: Result, _> = V1_FINISHED_JOBS() + let job_keys: Result, _> = OLD_FINISHED_JOBS() .keys(deps.storage, start_after, None, Order::Ascending) .take(msg.limit as usize) .collect(); let job_keys = job_keys?; for job_key in job_keys { - let v1_job = V1_FINISHED_JOBS().load(deps.storage, job_key)?; + let old_job = OLD_FINISHED_JOBS().load(deps.storage, job_key)?; let mut new_vars = vec![]; - let job_vars: Vec = serde_json_wasm::from_str(&v1_job.vars) + let job_vars: Vec = serde_json_wasm::from_str(&old_job.vars) .map_err(|e| StdError::generic_err(e.to_string()))?; for var in job_vars { new_vars.push(match var { - V1Variable::Static(v) => Variable::Static(StaticVariable { + OldVariable::Static(v) => Variable::Static(StaticVariable { kind: v.kind, name: v.name, encode: v.encode, @@ -357,7 +357,7 @@ pub fn migrate_finished_jobs( value: Some(v.value.clone()), update_fn: v.update_fn, }), - V1Variable::External(v) => Variable::External(ExternalVariable { + OldVariable::External(v) => Variable::External(ExternalVariable { kind: v.kind, name: v.name, encode: v.encode, @@ -366,7 +366,7 @@ pub fn migrate_finished_jobs( value: v.value, update_fn: v.update_fn, }), - V1Variable::Query(v) => Variable::Query(QueryVariable { + OldVariable::Query(v) => Variable::Query(QueryVariable { kind: v.kind, name: v.name, encode: v.encode, @@ -378,31 +378,31 @@ pub fn migrate_finished_jobs( }) } - let warp_account = ACCOUNTS().load(deps.storage, v1_job.owner.clone())?; + let warp_account = ACCOUNTS().load(deps.storage, old_job.owner.clone())?; FINISHED_JOBS().save( deps.storage, job_key, &Job { - id: v1_job.id, + id: old_job.id, prev_id: None, - owner: v1_job.owner, + owner: old_job.owner, account: warp_account.account, - last_update_time: v1_job.last_update_time, - name: v1_job.name, - description: v1_job.description, - labels: v1_job.labels, - status: v1_job.status, + last_update_time: old_job.last_update_time, + name: old_job.name, + description: old_job.description, + labels: old_job.labels, + status: old_job.status, executions: vec![Execution { - condition: v1_job.condition, - msgs: v1_job.msgs, + condition: old_job.condition, + msgs: old_job.msgs, }], terminate_condition: None, vars: serde_json_wasm::to_string(&new_vars)?, - recurring: v1_job.recurring, - requeue_on_evict: v1_job.requeue_on_evict, - reward: v1_job.reward, - assets_to_withdraw: v1_job.assets_to_withdraw, + recurring: old_job.recurring, + requeue_on_evict: old_job.requeue_on_evict, + reward: old_job.reward, + assets_to_withdraw: old_job.assets_to_withdraw, }, )?; } diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 7007de58..fa9c3448 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -24,15 +24,15 @@ pub fn PENDING_JOBS<'a>() -> IndexedMap<'a, u64, Job, JobIndexes<'a>> { let indexes = JobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "pending_jobs__reward_v3", + "pending_jobs__reward_v4", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "pending_jobs_v3", - "pending_jobs__publish_timestamp_v3", + "pending_jobs_v4", + "pending_jobs__publish_timestamp_v4", ), }; - IndexedMap::new("pending_jobs_v3", indexes) + IndexedMap::new("pending_jobs_v4", indexes) } #[allow(non_snake_case)] @@ -40,15 +40,15 @@ pub fn FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, Job, JobIndexes<'a>> { let indexes = JobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "finished_jobs__reward_v3", + "finished_jobs__reward_v4", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "finished_jobs_v3", - "finished_jobs__publish_timestamp_v3", + "finished_jobs_v4", + "finished_jobs__publish_timestamp_v4", ), }; - IndexedMap::new("finished_jobs_v3", indexes) + IndexedMap::new("finished_jobs_v4", indexes) } pub struct AccountIndexes<'a> { From 6b270cbca1239718a3cdc947fe3acf95c7390b67 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 2 Nov 2023 20:13:25 +0100 Subject: [PATCH 052/133] fix tests --- contracts/warp-resolver/src/tests.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/contracts/warp-resolver/src/tests.rs b/contracts/warp-resolver/src/tests.rs index 5a854c4e..f52a782f 100644 --- a/contracts/warp-resolver/src/tests.rs +++ b/contracts/warp-resolver/src/tests.rs @@ -1,3 +1,4 @@ +use controller::job::Execution; use resolver::condition::{NumValue, StringEnvValue, StringValue}; use schemars::_serde_json::json; @@ -30,18 +31,22 @@ fn test() { let _info = mock_info("vlad", &[]); let env = mock_env(); let msg = QueryValidateJobCreationMsg { - condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), + executions: vec![Execution { + condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), + msgs: "[{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}]".to_string(), + }], terminate_condition: None, vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), - msgs: "[{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}]".to_string(), }; let obj = serde_json_wasm::to_string(&vec!["{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}"]).unwrap(); let _msg1 = QueryValidateJobCreationMsg { - condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), terminate_condition: None, vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), - msgs: obj.clone(), + executions: vec![Execution { + condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), + msgs: obj.clone(), + }], }; println!("{}", serde_json_wasm::to_string(&obj).unwrap()); From 137df3ff7bd786453120e3a1707e5c4f93592312 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 2 Nov 2023 20:24:02 +0100 Subject: [PATCH 053/133] clippy fixes --- contracts/warp-controller/src/contract.rs | 22 +++++++++++--------- contracts/warp-controller/src/execute/job.rs | 2 +- packages/controller/src/lib.rs | 20 +++++++++--------- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index c6b0d222..b8469f2f 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -90,24 +90,26 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::CreateJob(data) => execute::job::create_job(deps, env, info, data), - ExecuteMsg::DeleteJob(data) => execute::job::delete_job(deps, env, info, data), - ExecuteMsg::UpdateJob(data) => execute::job::update_job(deps, env, info, data), - ExecuteMsg::ExecuteJob(data) => execute::job::execute_job(deps, env, info, data), - ExecuteMsg::EvictJob(data) => execute::job::evict_job(deps, env, info, data), + ExecuteMsg::CreateJob(data) => execute::job::create_job(deps, env, info, *data), + ExecuteMsg::DeleteJob(data) => execute::job::delete_job(deps, env, info, *data), + ExecuteMsg::UpdateJob(data) => execute::job::update_job(deps, env, info, *data), + ExecuteMsg::ExecuteJob(data) => execute::job::execute_job(deps, env, info, *data), + ExecuteMsg::EvictJob(data) => execute::job::evict_job(deps, env, info, *data), - ExecuteMsg::CreateAccount(data) => execute::account::create_account(deps, env, info, data), + ExecuteMsg::CreateAccount(data) => execute::account::create_account(deps, env, info, *data), - ExecuteMsg::UpdateConfig(data) => execute::controller::update_config(deps, env, info, data), + ExecuteMsg::UpdateConfig(data) => { + execute::controller::update_config(deps, env, info, *data) + } ExecuteMsg::MigrateAccounts(data) => { - execute::controller::migrate_accounts(deps, env, info, data) + execute::controller::migrate_accounts(deps, env, info, *data) } ExecuteMsg::MigratePendingJobs(data) => { - execute::controller::migrate_pending_jobs(deps, env, info, data) + execute::controller::migrate_pending_jobs(deps, env, info, *data) } ExecuteMsg::MigrateFinishedJobs(data) => { - execute::controller::migrate_finished_jobs(deps, env, info, data) + execute::controller::migrate_finished_jobs(deps, env, info, *data) } } } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 52bfbf5a..094762d4 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -124,7 +124,7 @@ pub fn create_job( contract_addr: account.account.to_string(), msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { msgs: vec![CosmosMsg::Bank(BankMsg::Burn { - amount: vec![Coin::new(burn_fee.u128(), config.fee_denom.clone())], + amount: vec![Coin::new(burn_fee.u128(), config.fee_denom)], })], }))?, funds: vec![], diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 9907a2d5..169e1643 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -86,19 +86,19 @@ pub struct InstantiateMsg { //execute #[cw_serde] pub enum ExecuteMsg { - CreateJob(CreateJobMsg), - DeleteJob(DeleteJobMsg), - UpdateJob(UpdateJobMsg), - ExecuteJob(ExecuteJobMsg), - EvictJob(EvictJobMsg), + CreateJob(Box), + DeleteJob(Box), + UpdateJob(Box), + ExecuteJob(Box), + EvictJob(Box), - CreateAccount(CreateAccountMsg), + CreateAccount(Box), - UpdateConfig(UpdateConfigMsg), + UpdateConfig(Box), - MigrateAccounts(MigrateAccountsMsg), - MigratePendingJobs(MigrateJobsMsg), - MigrateFinishedJobs(MigrateJobsMsg), + MigrateAccounts(Box), + MigratePendingJobs(Box), + MigrateFinishedJobs(Box), } #[cw_serde] From 3a4e12f4431249d0474f43459b2cc00a4af54421 Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Thu, 2 Nov 2023 19:11:27 -0700 Subject: [PATCH 054/133] rename occupied account to taken account --- contracts/warp-controller/src/execute/job.rs | 4 ++-- .../warp-controller/src/migrate/job_account.rs | 4 ++-- contracts/warp-controller/src/reply/account.rs | 4 ++-- contracts/warp-controller/src/reply/job.rs | 4 ++-- contracts/warp-controller/src/util/msg.rs | 8 ++++---- .../warp-job-account-tracker/src/contract.rs | 8 ++++---- .../src/execute/account.rs | 12 ++++++------ .../src/integration_tests.rs | 18 +++++++++--------- .../src/query/account.rs | 12 ++++++------ .../warp-job-account-tracker/src/state.rs | 2 +- packages/job-account-tracker/src/lib.rs | 8 ++++---- 11 files changed, 42 insertions(+), 42 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 7e39dbdc..698ba9b2 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -15,7 +15,7 @@ use crate::{ msg::{ build_account_execute_generic_msgs, build_account_withdraw_assets_msg, build_free_account_msg, build_instantiate_warp_account_msg, - build_instantiate_warp_job_account_tracker_msg, build_occupy_account_msg, + build_instantiate_warp_job_account_tracker_msg, build_taken_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, }, }, @@ -213,7 +213,7 @@ pub fn create_job( } // Occupy account - msgs.push(build_occupy_account_msg( + msgs.push(build_taken_account_msg( job_account_tracker.to_string(), available_account_addr.to_string(), job.id, diff --git a/contracts/warp-controller/src/migrate/job_account.rs b/contracts/warp-controller/src/migrate/job_account.rs index b7894e40..1f04c7d8 100644 --- a/contracts/warp-controller/src/migrate/job_account.rs +++ b/contracts/warp-controller/src/migrate/job_account.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{to_binary, Deps, Env, MessageInfo, Response, WasmMsg}; use crate::ContractError; use controller::{Config, MigrateJobAccountsMsg}; use job_account_tracker::{ - AccountsResponse, MigrateMsg, QueryFreeAccountsMsg, QueryOccupiedAccountsMsg, + AccountsResponse, MigrateMsg, QueryFreeAccountsMsg, QueryTakenAccountsMsg, }; pub fn migrate_free_job_accounts( @@ -50,7 +50,7 @@ pub fn migrate_occupied_job_accounts( let occupied_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( msg.job_account_tracker_addr, - &job_account_tracker::QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + &job_account_tracker::QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { start_after: msg.start_after, limit: Some(msg.limit as u32), }), diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index b928148b..28c01440 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -9,7 +9,7 @@ use crate::{ state::{JobQueue, JOB_ACCOUNT_TRACKERS}, util::msg::{ build_account_execute_generic_msgs, build_instantiate_warp_account_msg, - build_occupy_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, + build_taken_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, }, ContractError, @@ -266,7 +266,7 @@ pub fn create_job_account_and_job( } // Occupy job account - msgs.push(build_occupy_account_msg( + msgs.push(build_taken_account_msg( job_account_tracker_addr.to_string(), job_account_addr.to_string(), job.id, diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index d1c5ffa5..b5214f13 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -10,7 +10,7 @@ use crate::{ legacy_account::is_legacy_account, msg::{ build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_occupy_account_msg, build_transfer_native_funds_msg, + build_taken_account_msg, build_transfer_native_funds_msg, }, }, ContractError, @@ -206,7 +206,7 @@ pub fn execute_job( let job_account_tracker = JOB_ACCOUNT_TRACKERS.load(deps.storage, &finished_job.owner)?; // Occupy job account with the new job - msgs.push(build_occupy_account_msg( + msgs.push(build_taken_account_msg( job_account_tracker.to_string(), job_account_addr.to_string(), new_job_id, diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 01ba22cf..4d9eeeec 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -2,7 +2,7 @@ use cosmwasm_std::{to_binary, BankMsg, Coin, CosmosMsg, Uint128, Uint64, WasmMsg use controller::account::{AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; use job_account::{GenericMsg, WithdrawAssetsMsg}; -use job_account_tracker::{FreeAccountMsg, OccupyAccountMsg}; +use job_account_tracker::{FreeAccountMsg, TakeAccountMsg}; pub fn build_instantiate_warp_job_account_tracker_msg( admin_addr: String, @@ -60,15 +60,15 @@ pub fn build_free_account_msg(job_account_tracker_addr: String, account_addr: St }) } -pub fn build_occupy_account_msg( +pub fn build_taken_account_msg( job_account_tracker_addr: String, account_addr: String, job_id: Uint64, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job_account_tracker_addr, - msg: to_binary(&job_account_tracker::ExecuteMsg::OccupyAccount( - OccupyAccountMsg { + msg: to_binary(&job_account_tracker::ExecuteMsg::TakeAccount( + TakeAccountMsg { account_addr, job_id, }, diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index 84189098..ad7a1155 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -41,9 +41,9 @@ pub fn execute( return Err(ContractError::Unauthorized {}); } match msg { - ExecuteMsg::OccupyAccount(data) => { + ExecuteMsg::TakeAccount(data) => { nonpayable(&info).unwrap(); - execute::account::occupy_account(deps, data) + execute::account::taken_account(deps, data) } ExecuteMsg::FreeAccount(data) => { nonpayable(&info).unwrap(); @@ -56,8 +56,8 @@ pub fn execute( pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), - QueryMsg::QueryOccupiedAccounts(data) => { - to_binary(&query::account::query_occupied_accounts(deps, data)?) + QueryMsg::QueryTakenAccounts(data) => { + to_binary(&query::account::query_taken_accounts(deps, data)?) } QueryMsg::QueryFreeAccounts(data) => { to_binary(&query::account::query_free_accounts(deps, data)?) diff --git a/contracts/warp-job-account-tracker/src/execute/account.rs b/contracts/warp-job-account-tracker/src/execute/account.rs index 0c3d9933..9a5bb2a5 100644 --- a/contracts/warp-job-account-tracker/src/execute/account.rs +++ b/contracts/warp-job-account-tracker/src/execute/account.rs @@ -1,24 +1,24 @@ -use crate::state::{FREE_ACCOUNTS, OCCUPIED_ACCOUNTS}; +use crate::state::{FREE_ACCOUNTS, TAKEN_ACCOUNTS}; use crate::ContractError; use cosmwasm_std::{DepsMut, Response}; -use job_account_tracker::{FreeAccountMsg, OccupyAccountMsg}; +use job_account_tracker::{FreeAccountMsg, TakeAccountMsg}; -pub fn occupy_account(deps: DepsMut, data: OccupyAccountMsg) -> Result { +pub fn taken_account(deps: DepsMut, data: TakeAccountMsg) -> Result { let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; FREE_ACCOUNTS.remove(deps.storage, account_addr_ref); - OCCUPIED_ACCOUNTS.update(deps.storage, account_addr_ref, |s| match s { + TAKEN_ACCOUNTS.update(deps.storage, account_addr_ref, |s| match s { None => Ok(data.job_id), Some(_) => Err(ContractError::AccountAlreadyOccupiedError {}), })?; Ok(Response::new() - .add_attribute("action", "occupy_account") + .add_attribute("action", "taken_account") .add_attribute("account_addr", data.account_addr) .add_attribute("job_id", data.job_id)) } pub fn free_account(deps: DepsMut, data: FreeAccountMsg) -> Result { let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; - OCCUPIED_ACCOUNTS.remove(deps.storage, account_addr_ref); + TAKEN_ACCOUNTS.remove(deps.storage, account_addr_ref); FREE_ACCOUNTS.update(deps.storage, account_addr_ref, |s| match s { // value is a dummy data because there is no built in support for set in cosmwasm None => Ok(true), diff --git a/contracts/warp-job-account-tracker/src/integration_tests.rs b/contracts/warp-job-account-tracker/src/integration_tests.rs index 1605f0b9..252c31f3 100644 --- a/contracts/warp-job-account-tracker/src/integration_tests.rs +++ b/contracts/warp-job-account-tracker/src/integration_tests.rs @@ -5,8 +5,8 @@ mod tests { use cw_multi_test::{App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor}; use job_account_tracker::{ Account, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FirstFreeAccountResponse, - FreeAccountMsg, InstantiateMsg, OccupyAccountMsg, QueryConfigMsg, QueryFirstFreeAccountMsg, - QueryFreeAccountsMsg, QueryMsg, QueryOccupiedAccountsMsg, + FreeAccountMsg, InstantiateMsg, QueryConfigMsg, QueryFirstFreeAccountMsg, + QueryFreeAccountsMsg, QueryMsg, QueryTakenAccountsMsg, TakeAccountMsg, }; use crate::{ @@ -115,7 +115,7 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), - &QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { start_after: None, limit: None }) @@ -215,7 +215,7 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), - &QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { start_after: None, limit: None }) @@ -230,19 +230,19 @@ mod tests { let _ = app.execute_contract( Addr::unchecked(USER_1), warp_job_account_tracker_contract_addr.clone(), - &ExecuteMsg::OccupyAccount(OccupyAccountMsg { + &ExecuteMsg::TakeAccount(TakeAccountMsg { account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), job_id: DUMMY_JOB_1_ID, }), &[], ); - // Cannot occupy account twice + // Cannot take account twice assert_err( app.execute_contract( Addr::unchecked(USER_1), warp_job_account_tracker_contract_addr.clone(), - &ExecuteMsg::OccupyAccount(OccupyAccountMsg { + &ExecuteMsg::TakeAccount(TakeAccountMsg { account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), job_id: DUMMY_JOB_2_ID, }), @@ -279,7 +279,7 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), - &QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { start_after: None, limit: None }) @@ -335,7 +335,7 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), - &QueryMsg::QueryOccupiedAccounts(QueryOccupiedAccountsMsg { + &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { start_after: None, limit: None }) diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-job-account-tracker/src/query/account.rs index 85ed6937..abbd3ad0 100644 --- a/contracts/warp-job-account-tracker/src/query/account.rs +++ b/contracts/warp-job-account-tracker/src/query/account.rs @@ -1,11 +1,11 @@ use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; -use crate::state::{CONFIG, FREE_ACCOUNTS, OCCUPIED_ACCOUNTS}; +use crate::state::{CONFIG, FREE_ACCOUNTS, TAKEN_ACCOUNTS}; use job_account_tracker::{ Account, AccountsResponse, ConfigResponse, FirstFreeAccountResponse, QueryFreeAccountsMsg, - QueryOccupiedAccountsMsg, + QueryTakenAccountsMsg, }; const QUERY_LIMIT: u32 = 50; @@ -30,12 +30,12 @@ pub fn query_first_free_account(deps: Deps) -> StdResult StdResult { let iter = match data.start_after { - Some(start_after) => OCCUPIED_ACCOUNTS.range( + Some(start_after) => TAKEN_ACCOUNTS.range( deps.storage, Some(Bound::exclusive( &deps.api.addr_validate(start_after.as_str()).unwrap(), @@ -43,7 +43,7 @@ pub fn query_occupied_accounts( None, Order::Descending, ), - None => OCCUPIED_ACCOUNTS.range(deps.storage, None, None, Order::Descending), + None => TAKEN_ACCOUNTS.range(deps.storage, None, None, Order::Descending), }; let accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-job-account-tracker/src/state.rs index 03b30f77..8314bfdd 100644 --- a/contracts/warp-job-account-tracker/src/state.rs +++ b/contracts/warp-job-account-tracker/src/state.rs @@ -5,7 +5,7 @@ use job_account_tracker::Config; pub const CONFIG: Item = Item::new("config"); // Key is the account address, value is the ID of the pending job currently using it -pub const OCCUPIED_ACCOUNTS: Map<&Addr, Uint64> = Map::new("occupied_accounts"); +pub const TAKEN_ACCOUNTS: Map<&Addr, Uint64> = Map::new("taken_accounts"); // Key is the account address, value is a dummy data that is always true to make it behave like a set pub const FREE_ACCOUNTS: Map<&Addr, bool> = Map::new("free_accounts"); diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs index 6163ad25..1f2f1d6a 100644 --- a/packages/job-account-tracker/src/lib.rs +++ b/packages/job-account-tracker/src/lib.rs @@ -17,12 +17,12 @@ pub struct InstantiateMsg { #[cw_serde] #[allow(clippy::large_enum_variant)] pub enum ExecuteMsg { - OccupyAccount(OccupyAccountMsg), + TakeAccount(TakeAccountMsg), FreeAccount(FreeAccountMsg), } #[cw_serde] -pub struct OccupyAccountMsg { +pub struct TakeAccountMsg { pub account_addr: String, pub job_id: Uint64, } @@ -38,7 +38,7 @@ pub enum QueryMsg { #[returns(ConfigResponse)] QueryConfig(QueryConfigMsg), #[returns(AccountsResponse)] - QueryOccupiedAccounts(QueryOccupiedAccountsMsg), + QueryTakenAccounts(QueryTakenAccountsMsg), #[returns(AccountsResponse)] QueryFreeAccounts(QueryFreeAccountsMsg), #[returns(FirstFreeAccountResponse)] @@ -54,7 +54,7 @@ pub struct ConfigResponse { } #[cw_serde] -pub struct QueryOccupiedAccountsMsg { +pub struct QueryTakenAccountsMsg { pub start_after: Option, pub limit: Option, } From c31d244aa0ad5b8b47a9fb7df97174258ea8ac9f Mon Sep 17 00:00:00 2001 From: llllllluc <58892938+llllllluc@users.noreply.github.com> Date: Thu, 2 Nov 2023 20:07:37 -0700 Subject: [PATCH 055/133] use single job account tracker for all users --- contracts/warp-controller/src/contract.rs | 43 +--- contracts/warp-controller/src/execute/job.rs | 229 ++++++++---------- .../src/migrate/job_account.rs | 12 +- .../src/migrate/job_account_tracker.rs | 47 +--- .../warp-controller/src/reply/account.rs | 148 +---------- contracts/warp-controller/src/reply/job.rs | 10 +- contracts/warp-controller/src/state.rs | 8 +- contracts/warp-controller/src/util/msg.rs | 32 +-- .../warp-job-account-tracker/src/contract.rs | 17 +- .../warp-job-account-tracker/src/error.rs | 8 +- .../src/execute/account.rs | 32 ++- .../src/integration_tests.rs | 60 +++-- .../src/query/account.rs | 96 +++++--- .../warp-job-account-tracker/src/state.rs | 8 +- contracts/warp-job-account/src/contract.rs | 2 - contracts/warp-job-account/src/error.rs | 8 +- contracts/warp-job-account/src/tests.rs | 3 - contracts/warp-legacy-account/src/error.rs | 8 +- packages/controller/src/lib.rs | 19 +- packages/job-account-tracker/src/lib.rs | 18 +- packages/job-account/src/lib.rs | 9 - 21 files changed, 329 insertions(+), 488 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index d0c02909..0f31ad57 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -15,15 +15,11 @@ use crate::{ use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State}; // Reply id for job creation -// From a totally new user using warp for the first time, does not have account tracker yet, let alone free account -// So we create account account and account and job -pub const REPLY_ID_CREATE_JOB_ACCOUNT_TRACKER_AND_JOB_ACCOUNT_AND_JOB: u64 = 1; -// Reply id for job creation -// From an existing user, who has account tracker, but does not have available account -// So we create account and job -pub const REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB: u64 = 2; +// For user does not have available account +// So we create new job account account and job +pub const REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB: u64 = 1; // Reply id for job execution -pub const REPLY_ID_EXECUTE_JOB: u64 = 3; +pub const REPLY_ID_EXECUTE_JOB: u64 = 2; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -45,12 +41,12 @@ pub fn instantiate( fee_collector: deps .api .addr_validate(&msg.fee_collector.unwrap_or_else(|| info.sender.to_string()))?, - warp_job_account_tracker_code_id: msg.warp_job_account_tracker_code_id, warp_account_code_id: msg.warp_account_code_id, minimum_reward: msg.minimum_reward, creation_fee_percentage: msg.creation_fee, cancellation_fee_percentage: msg.cancellation_fee, resolver_address: deps.api.addr_validate(&msg.resolver_address)?, + job_account_tracker_address: deps.api.addr_validate(&msg.job_account_tracker_address)?, t_max: msg.t_max, t_min: msg.t_min, a_max: msg.a_max, @@ -123,28 +119,17 @@ pub fn execute( nonpayable(&info).unwrap(); migrate::legacy_account::migrate_legacy_accounts(deps, info, data, config) } - ExecuteMsg::MigrateJobAccountTrackers(data) => { + ExecuteMsg::MigrateJobAccountTracker(data) => { nonpayable(&info).unwrap(); - migrate::job_account_tracker::migrate_job_account_trackers( - deps.as_ref(), - info, - data, - config, - ) + migrate::job_account_tracker::migrate_job_account_tracker(info, data, config) } ExecuteMsg::MigrateFreeJobAccounts(data) => { nonpayable(&info).unwrap(); migrate::job_account::migrate_free_job_accounts(deps.as_ref(), env, info, data, config) } - ExecuteMsg::MigrateOccupiedJobAccounts(data) => { + ExecuteMsg::MigrateTakenJobAccounts(data) => { nonpayable(&info).unwrap(); - migrate::job_account::migrate_occupied_job_accounts( - deps.as_ref(), - env, - info, - data, - config, - ) + migrate::job_account::migrate_taken_job_accounts(deps.as_ref(), env, info, data, config) } ExecuteMsg::MigratePendingJobs(data) => { @@ -232,12 +217,14 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result Result { let config = CONFIG.load(deps.storage)?; match msg.id { - // Job account tracker has been created, now create job account and job - REPLY_ID_CREATE_JOB_ACCOUNT_TRACKER_AND_JOB_ACCOUNT_AND_JOB => { - reply::account::create_job_account_tracker_and_account_and_job(deps, env, msg, config) - } // Job account has been created, now create job REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB => { - reply::account::create_job_account_and_job(deps, env, msg) + reply::account::create_job_account_and_job(deps, env, msg, config) } // Job has been executed REPLY_ID_EXECUTE_JOB => reply::job::execute_job(deps, env, msg, config), diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 698ba9b2..9d1dcbe6 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -4,18 +4,14 @@ use cosmwasm_std::{ }; use crate::{ - contract::{ - REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, - REPLY_ID_CREATE_JOB_ACCOUNT_TRACKER_AND_JOB_ACCOUNT_AND_JOB, REPLY_ID_EXECUTE_JOB, - }, - state::{JobQueue, JOB_ACCOUNT_TRACKERS, LEGACY_ACCOUNTS, STATE}, + contract::{REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, REPLY_ID_EXECUTE_JOB}, + state::{JobQueue, LEGACY_ACCOUNTS, STATE}, util::{ fee::deduct_reward_and_fee_from_native_funds, legacy_account::is_legacy_account, msg::{ build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_free_account_msg, build_instantiate_warp_account_msg, - build_instantiate_warp_job_account_tracker_msg, build_taken_account_msg, + build_free_account_msg, build_instantiate_warp_account_msg, build_taken_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, }, }, @@ -53,6 +49,9 @@ pub fn create_job( return Err(ContractError::RewardTooSmall {}); } + let job_owner = info.sender.clone(); + let job_account_tracker_address_ref = &config.job_account_tracker_address.to_string(); + let _validate_conditions_and_variables: Option = deps.querier.query_wasm_smart( config.resolver_address, &resolver::QueryMsg::QueryValidateJobCreation(resolver::QueryValidateJobCreationMsg { @@ -92,14 +91,13 @@ pub fn create_job( ), ); - let job_account_tracker = JOB_ACCOUNT_TRACKERS.may_load(deps.storage, &info.sender)?; let state = STATE.load(deps.storage)?; let mut job = JobQueue::add( &mut deps, Job { id: state.current_job_id, prev_id: None, - owner: info.sender.clone(), + owner: job_owner.clone(), // Account uses a placeholder value for now, will update it to job account address if job account exists or after created // Update will happen either in create_job (exists free job account) or reply (after creation), so it's atomic // And we guarantee we do not read this value before it's updated @@ -120,129 +118,109 @@ pub fn create_job( }, )?; - match job_account_tracker { + let available_account: FirstFreeAccountResponse = deps.querier.query_wasm_smart( + job_account_tracker_address_ref, + &job_account_tracker::QueryMsg::QueryFirstFreeAccount( + job_account_tracker::QueryFirstFreeAccountMsg { + account_owner_addr: job_owner.to_string(), + }, + ), + )?; + match available_account.account { None => { - // Create account tracker then create account then create job in reply + // Create account then create job in reply submsgs.push(SubMsg { - id: REPLY_ID_CREATE_JOB_ACCOUNT_TRACKER_AND_JOB_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_job_account_tracker_msg( + id: REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, + msg: build_instantiate_warp_account_msg( + job.id, env.contract.address.to_string(), - config.warp_job_account_tracker_code_id.u64(), + config.warp_account_code_id.u64(), info.sender.to_string(), + native_funds_minus_reward_and_fee, + data.cw_funds, + data.account_msgs, ), gas_limit: None, reply_on: ReplyOn::Always, }); - attrs.push(Attribute::new( - "action", - "create_job_account_tracker_and_account_and_job", - )); + attrs.push(Attribute::new("action", "create_account_and_job")); } - Some(job_account_tracker) => { - let available_account: FirstFreeAccountResponse = deps.querier.query_wasm_smart( - job_account_tracker.clone(), - &job_account_tracker::QueryMsg::QueryFirstFreeAccount( - job_account_tracker::QueryFirstFreeAccountMsg {}, - ), - )?; - match available_account.account { - None => { - // Create account then create job in reply - submsgs.push(SubMsg { - id: REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_account_msg( - job.id, - env.contract.address.to_string(), - config.warp_account_code_id.u64(), - info.sender.to_string(), - job_account_tracker.clone().to_string(), - native_funds_minus_reward_and_fee, - data.cw_funds, - data.account_msgs, - ), - gas_limit: None, - reply_on: ReplyOn::Always, - }); + Some(available_account) => { + let available_account_addr = available_account.addr; + // Update job.account from placeholder value to job account + job.account = available_account_addr.clone(); + JobQueue::sync(&mut deps, env, job.clone())?; + + if !native_funds_minus_reward_and_fee.is_empty() { + // Fund account in native coins + msgs.push(build_transfer_native_funds_msg( + available_account_addr.clone().to_string(), + native_funds_minus_reward_and_fee, + )) + } - attrs.push(Attribute::new("action", "create_account_and_job")); - } - Some(available_account) => { - let available_account_addr = available_account.addr; - // Update job.account from placeholder value to job account - job.account = available_account_addr.clone(); - JobQueue::sync(&mut deps, env, job.clone())?; - - if !native_funds_minus_reward_and_fee.is_empty() { - // Fund account in native coins - msgs.push(build_transfer_native_funds_msg( + if let Some(cw_funds) = data.cw_funds { + // Fund account in CW20 / CW721 tokens + for cw_fund in cw_funds { + msgs.push(match cw_fund { + CwFund::Cw20(cw20_fund) => build_transfer_cw20_msg( + deps.api + .addr_validate(&cw20_fund.contract_addr)? + .to_string(), + info.sender.clone().to_string(), available_account_addr.clone().to_string(), - native_funds_minus_reward_and_fee, - )) - } - - if let Some(cw_funds) = data.cw_funds { - // Fund account in CW20 / CW721 tokens - for cw_fund in cw_funds { - msgs.push(match cw_fund { - CwFund::Cw20(cw20_fund) => build_transfer_cw20_msg( - deps.api - .addr_validate(&cw20_fund.contract_addr)? - .to_string(), - info.sender.clone().to_string(), - available_account_addr.clone().to_string(), - cw20_fund.amount, - ), - CwFund::Cw721(cw721_fund) => build_transfer_cw721_msg( - deps.api - .addr_validate(&cw721_fund.contract_addr)? - .to_string(), - available_account_addr.clone().to_string(), - cw721_fund.token_id.clone(), - ), - }) - } - } - - if let Some(account_msgs) = data.account_msgs { - // Account execute msgs - msgs.push(build_account_execute_generic_msgs( - available_account_addr.to_string(), - account_msgs, - )); - } - - // Occupy account - msgs.push(build_taken_account_msg( - job_account_tracker.to_string(), - available_account_addr.to_string(), - job.id, - )); - - attrs.push(Attribute::new("action", "create_job")); - attrs.push(Attribute::new("job_id", job.id)); - attrs.push(Attribute::new("job_owner", job.owner)); - attrs.push(Attribute::new("job_name", job.name)); - attrs.push(Attribute::new( - "job_status", - serde_json_wasm::to_string(&job.status)?, - )); - attrs.push(Attribute::new( - "job_condition", - serde_json_wasm::to_string(&job.condition)?, - )); - attrs.push(Attribute::new( - "job_msgs", - serde_json_wasm::to_string(&job.msgs)?, - )); - attrs.push(Attribute::new("job_reward", job.reward)); - attrs.push(Attribute::new("job_creation_fee", fee)); - attrs.push(Attribute::new( - "job_last_updated_time", - job.last_update_time, - )); + cw20_fund.amount, + ), + CwFund::Cw721(cw721_fund) => build_transfer_cw721_msg( + deps.api + .addr_validate(&cw721_fund.contract_addr)? + .to_string(), + available_account_addr.clone().to_string(), + cw721_fund.token_id.clone(), + ), + }) } } + + if let Some(account_msgs) = data.account_msgs { + // Account execute msgs + msgs.push(build_account_execute_generic_msgs( + available_account_addr.to_string(), + account_msgs, + )); + } + + // Take account + msgs.push(build_taken_account_msg( + config.job_account_tracker_address.to_string(), + job_owner.to_string(), + available_account_addr.to_string(), + job.id, + )); + + attrs.push(Attribute::new("action", "create_job")); + attrs.push(Attribute::new("job_id", job.id)); + attrs.push(Attribute::new("job_owner", job.owner)); + attrs.push(Attribute::new("job_name", job.name)); + attrs.push(Attribute::new( + "job_status", + serde_json_wasm::to_string(&job.status)?, + )); + attrs.push(Attribute::new( + "job_condition", + serde_json_wasm::to_string(&job.condition)?, + )); + attrs.push(Attribute::new( + "job_msgs", + serde_json_wasm::to_string(&job.msgs)?, + )); + attrs.push(Attribute::new("job_reward", job.reward)); + attrs.push(Attribute::new("job_creation_fee", fee)); + attrs.push(Attribute::new( + "job_last_updated_time", + job.last_update_time, + )); } } @@ -298,11 +276,10 @@ pub fn delete_job( )); if !is_legacy_account(legacy_account, job_account_addr.clone()) { - // For job not using legacy account, job owner must already have account tracker instantiated - let job_account_tracker = JOB_ACCOUNT_TRACKERS.load(deps.storage, &job.owner)?; // Free account msgs.push(build_free_account_msg( - job_account_tracker.to_string(), + config.job_account_tracker_address.to_string(), + job.owner.to_string(), job_account_addr.to_string(), )); } @@ -479,11 +456,10 @@ pub fn execute_job( )); if !is_legacy_account(legacy_account, job_account_addr.clone()) { - // For job not using legacy account, job owner must already have account tracker instantiated - let job_account_tracker = JOB_ACCOUNT_TRACKERS.load(deps.storage, &job.owner)?; // Free account msgs.push(build_free_account_msg( - job_account_tracker.to_string(), + config.job_account_tracker_address.to_string(), + job.owner.to_string(), job_account_addr.to_string(), )); } @@ -573,11 +549,10 @@ pub fn evict_job( )); if !is_legacy_account(legacy_account, job_account_addr.clone()) { - // For job not using legacy account, job owner must already have account tracker instantiated - let job_account_tracker = JOB_ACCOUNT_TRACKERS.load(deps.storage, &job.owner)?; // Free account msgs.push(build_free_account_msg( - job_account_tracker.to_string(), + config.job_account_tracker_address.to_string(), + job.owner.to_string(), job_account_addr.to_string(), )); } diff --git a/contracts/warp-controller/src/migrate/job_account.rs b/contracts/warp-controller/src/migrate/job_account.rs index 1f04c7d8..c972be54 100644 --- a/contracts/warp-controller/src/migrate/job_account.rs +++ b/contracts/warp-controller/src/migrate/job_account.rs @@ -18,8 +18,9 @@ pub fn migrate_free_job_accounts( } let free_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( - msg.job_account_tracker_addr, + config.job_account_tracker_address, &job_account_tracker::QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + account_owner_addr: msg.account_owner_addr, start_after: msg.start_after, limit: Some(msg.limit as u32), }), @@ -37,7 +38,7 @@ pub fn migrate_free_job_accounts( Ok(Response::new().add_messages(migration_msgs)) } -pub fn migrate_occupied_job_accounts( +pub fn migrate_taken_job_accounts( deps: Deps, _env: Env, info: MessageInfo, @@ -48,16 +49,17 @@ pub fn migrate_occupied_job_accounts( return Err(ContractError::Unauthorized {}); } - let occupied_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( - msg.job_account_tracker_addr, + let taken_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( + config.job_account_tracker_address, &job_account_tracker::QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { + account_owner_addr: msg.account_owner_addr, start_after: msg.start_after, limit: Some(msg.limit as u32), }), )?; let mut migration_msgs = vec![]; - for job_account in occupied_job_accounts.accounts { + for job_account in taken_job_accounts.accounts { migration_msgs.push(WasmMsg::Migrate { contract_addr: job_account.addr.to_string(), new_code_id: msg.warp_job_account_code_id.u64(), diff --git a/contracts/warp-controller/src/migrate/job_account_tracker.rs b/contracts/warp-controller/src/migrate/job_account_tracker.rs index 0cec0d96..05e1805f 100644 --- a/contracts/warp-controller/src/migrate/job_account_tracker.rs +++ b/contracts/warp-controller/src/migrate/job_account_tracker.rs @@ -1,45 +1,20 @@ -use cosmwasm_std::{to_binary, Addr, Deps, MessageInfo, Order, Response, StdResult, WasmMsg}; -use cw_storage_plus::Bound; +use cosmwasm_std::{to_binary, MessageInfo, Response, WasmMsg}; -use crate::{state::JOB_ACCOUNT_TRACKERS, ContractError}; -use controller::{Config, MigrateJobAccountTrackersMsg}; +use crate::ContractError; +use controller::{Config, MigrateJobAccountTrackerMsg}; -pub fn migrate_job_account_trackers( - deps: Deps, +pub fn migrate_job_account_tracker( info: MessageInfo, - msg: MigrateJobAccountTrackersMsg, + msg: MigrateJobAccountTrackerMsg, config: Config, ) -> Result { if info.sender != config.owner { return Err(ContractError::Unauthorized {}); } - - let job_account_tracker_keys = match msg.start_after { - None => JOB_ACCOUNT_TRACKERS.keys(deps.storage, None, None, Order::Ascending), - Some(start_after) => JOB_ACCOUNT_TRACKERS.keys( - deps.storage, - Some(Bound::exclusive( - &deps.api.addr_validate(start_after.as_str())?, - )), - None, - Order::Ascending, - ), - } - .take(msg.limit as usize) - .collect::>>()?; - - // let job_account_tracker_keys = job_account_tracker_keys?; - let mut migration_msgs = vec![]; - - for job_account_tracker_key in job_account_tracker_keys { - let job_account_tracker = - JOB_ACCOUNT_TRACKERS.load(deps.storage, &job_account_tracker_key)?; - migration_msgs.push(WasmMsg::Migrate { - contract_addr: job_account_tracker.to_string(), - new_code_id: msg.warp_job_account_tracker_code_id.u64(), - msg: to_binary(&job_account_tracker::MigrateMsg {})?, - }) - } - - Ok(Response::new().add_messages(migration_msgs)) + let migration_msg = WasmMsg::Migrate { + contract_addr: config.job_account_tracker_address.to_string(), + new_code_id: msg.warp_job_account_tracker_code_id.u64(), + msg: to_binary(&job_account_tracker::MigrateMsg {})?, + }; + Ok(Response::new().add_message(migration_msg)) } diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 28c01440..e5e8634f 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -1,142 +1,21 @@ -use cosmwasm_std::{ - Coin, CosmosMsg, DepsMut, Env, Reply, ReplyOn, Response, StdError, SubMsg, Uint64, -}; +use cosmwasm_std::{Coin, CosmosMsg, DepsMut, Env, Reply, Response, StdError}; use controller::{account::CwFund, Config}; use crate::{ - contract::REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, - state::{JobQueue, JOB_ACCOUNT_TRACKERS}, + state::JobQueue, util::msg::{ - build_account_execute_generic_msgs, build_instantiate_warp_account_msg, - build_taken_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, - build_transfer_native_funds_msg, + build_account_execute_generic_msgs, build_taken_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, build_transfer_native_funds_msg, }, ContractError, }; -pub fn create_job_account_tracker_and_account_and_job( - deps: DepsMut, - env: Env, - msg: Reply, - config: Config, -) -> Result { - let reply = msg.result.into_result().map_err(StdError::generic_err)?; - - let event = reply - .events - .iter() - .find(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "action" && attr.value == "instantiate") - }) - .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; - - let job_id_str = event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "job_id") - .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? - .value; - let job_id = job_id_str.as_str().parse::()?; - - let owner = event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "owner") - .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? - .value; - let owner_addr_ref = &deps.api.addr_validate(&owner)?; - - let job_account_tracker_addr = event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "contract_addr") - .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? - .value; - - let native_funds: Vec = serde_json_wasm::from_str( - &event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "native_funds") - .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? - .value, - )?; - - let cw_funds: Option> = serde_json_wasm::from_str( - &event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "cw_funds") - .ok_or_else(|| StdError::generic_err("cannot find `cw_funds` attribute"))? - .value, - )?; - - let account_msgs: Option> = serde_json_wasm::from_str( - &event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "account_msgs") - .ok_or_else(|| StdError::generic_err("cannot find `account_msgs` attribute"))? - .value, - )?; - - if JOB_ACCOUNT_TRACKERS.has(deps.storage, owner_addr_ref) { - return Err(ContractError::JobAccountTrackerAlreadyExists {}); - } - - JOB_ACCOUNT_TRACKERS.save( - deps.storage, - owner_addr_ref, - &deps.api.addr_validate(&job_account_tracker_addr)?, - )?; - - // Create new job account then create job in reply - let create_account_and_job_submsg = SubMsg { - id: REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_account_msg( - Uint64::from(job_id), - env.contract.address.to_string(), - config.warp_account_code_id.u64(), - owner.clone(), - job_account_tracker_addr.clone(), - native_funds.clone(), - cw_funds.clone(), - account_msgs, - ), - gas_limit: None, - reply_on: ReplyOn::Always, - }; - - Ok(Response::new() - .add_submessage(create_account_and_job_submsg) - .add_attribute( - "action", - "create_job_account_tracker_and_account_and_job_reply", - ) - .add_attribute("job_id", job_id_str) - .add_attribute("owner", owner) - .add_attribute("job_account_tracker_addr", job_account_tracker_addr) - .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?) - .add_attribute( - "cw_funds", - serde_json_wasm::to_string(&cw_funds.unwrap_or(vec![]))?, - )) -} - pub fn create_job_account_and_job( mut deps: DepsMut, env: Env, msg: Reply, + config: Config, ) -> Result { let reply = msg.result.into_result().map_err(StdError::generic_err)?; @@ -168,18 +47,6 @@ pub fn create_job_account_and_job( .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? .value; - let job_account_tracker_addr = deps.api.addr_validate( - &event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "job_account_tracker_addr") - .ok_or_else(|| { - StdError::generic_err("cannot find `job_account_tracker_addr` attribute") - })? - .value, - )?; - let job_account_addr = deps.api.addr_validate( &event .attributes @@ -265,9 +132,10 @@ pub fn create_job_account_and_job( )); } - // Occupy job account + // Take job account msgs.push(build_taken_account_msg( - job_account_tracker_addr.to_string(), + config.job_account_tracker_address.to_string(), + job.owner.to_string(), job_account_addr.to_string(), job.id, )); diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index b5214f13..7549d629 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -5,7 +5,7 @@ use cosmwasm_std::{ use crate::{ error::map_contract_error, - state::{JobQueue, JOB_ACCOUNT_TRACKERS, LEGACY_ACCOUNTS, STATE}, + state::{JobQueue, LEGACY_ACCOUNTS, STATE}, util::{ legacy_account::is_legacy_account, msg::{ @@ -202,12 +202,10 @@ pub fn execute_job( if recurring_job_created { if !is_legacy_account(legacy_account, job_account_addr.clone()) { - // For job not using legacy account, job owner must already have account tracker instantiated - let job_account_tracker = - JOB_ACCOUNT_TRACKERS.load(deps.storage, &finished_job.owner)?; - // Occupy job account with the new job + // Take job account with the new job msgs.push(build_taken_account_msg( - job_account_tracker.to_string(), + config.job_account_tracker_address.to_string(), + finished_job.owner.to_string(), job_account_addr.to_string(), new_job_id, )); diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 17cf2d4a..bbd1cac7 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{Addr, DepsMut, Env, Uint128, Uint64}; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; use controller::{ account::LegacyAccount, @@ -76,12 +76,6 @@ pub fn LEGACY_ACCOUNTS<'a>() -> IndexedMap<'a, Addr, LegacyAccount, LegacyAccoun IndexedMap::new("accounts", indexes) } -// ACCOUNTS_TRACKER stores account tracker, this is the successor of ACCOUNTS -// Key is user address, value is address of job account tracker contract -// By querying each user's account tracker contract, we know all accounts owned by that user and each account's availability -// For more detail, please refer to account tracker contract -pub const JOB_ACCOUNT_TRACKERS: Map<&Addr, Addr> = Map::new("job_job_account_trackers"); - pub const QUERY_PAGE_SIZE: u32 = 50; pub const CONFIG: Item = Item::new("config"); pub const STATE: Item = Item::new("state"); diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 4d9eeeec..e21d05ce 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -4,30 +4,12 @@ use controller::account::{AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, use job_account::{GenericMsg, WithdrawAssetsMsg}; use job_account_tracker::{FreeAccountMsg, TakeAccountMsg}; -pub fn build_instantiate_warp_job_account_tracker_msg( - admin_addr: String, - code_id: u64, - account_owner: String, -) -> CosmosMsg { - CosmosMsg::Wasm(WasmMsg::Instantiate { - admin: Some(admin_addr), - code_id, - msg: to_binary(&job_account_tracker::InstantiateMsg { - owner: account_owner.clone(), - }) - .unwrap(), - funds: vec![], - label: format!("warp account tracker, owner: {}", account_owner), - }) -} - #[allow(clippy::too_many_arguments)] pub fn build_instantiate_warp_account_msg( job_id: Uint64, admin_addr: String, code_id: u64, account_owner: String, - job_account_tracker_addr: String, native_funds: Vec, cw_funds: Option>, msgs: Option>, @@ -38,7 +20,6 @@ pub fn build_instantiate_warp_account_msg( msg: to_binary(&job_account::InstantiateMsg { owner: account_owner.clone(), job_id, - job_account_tracker_addr, native_funds: native_funds.clone(), cw_funds: cw_funds.unwrap_or(vec![]), msgs: msgs.unwrap_or(vec![]), @@ -49,11 +30,18 @@ pub fn build_instantiate_warp_account_msg( }) } -pub fn build_free_account_msg(job_account_tracker_addr: String, account_addr: String) -> CosmosMsg { +pub fn build_free_account_msg( + job_account_tracker_addr: String, + account_owner_addr: String, + account_addr: String, +) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job_account_tracker_addr, msg: to_binary(&job_account_tracker::ExecuteMsg::FreeAccount( - FreeAccountMsg { account_addr }, + FreeAccountMsg { + account_owner_addr, + account_addr, + }, )) .unwrap(), funds: vec![], @@ -62,6 +50,7 @@ pub fn build_free_account_msg(job_account_tracker_addr: String, account_addr: St pub fn build_taken_account_msg( job_account_tracker_addr: String, + account_owner_addr: String, account_addr: String, job_id: Uint64, ) -> CosmosMsg { @@ -69,6 +58,7 @@ pub fn build_taken_account_msg( contract_addr: job_account_tracker_addr, msg: to_binary(&job_account_tracker::ExecuteMsg::TakeAccount( TakeAccountMsg { + account_owner_addr, account_addr, job_id, }, diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index ad7a1155..4e3e73df 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -10,7 +10,7 @@ use job_account_tracker::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryM pub fn instantiate( deps: DepsMut, env: Env, - info: MessageInfo, + _info: MessageInfo, msg: InstantiateMsg, ) -> Result { let instantiated_account_addr = env.contract.address; @@ -18,15 +18,18 @@ pub fn instantiate( CONFIG.save( deps.storage, &Config { - owner: deps.api.addr_validate(&msg.owner)?, - creator_addr: info.sender, + // owner: deps.api.addr_validate(&msg.owner)?, + // creator_addr: info.sender, + admin: deps.api.addr_validate(&msg.admin)?, + warp_addr: deps.api.addr_validate(&msg.warp_addr)?, }, )?; Ok(Response::new() .add_attribute("action", "instantiate") .add_attribute("contract_addr", instantiated_account_addr.clone()) - .add_attribute("owner", msg.owner)) + .add_attribute("admin", msg.admin) + .add_attribute("warp_addr", msg.warp_addr)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -37,7 +40,7 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.creator_addr { + if info.sender != config.admin && info.sender != config.warp_addr { return Err(ContractError::Unauthorized {}); } match msg { @@ -62,8 +65,8 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryFreeAccounts(data) => { to_binary(&query::account::query_free_accounts(deps, data)?) } - QueryMsg::QueryFirstFreeAccount(_) => { - to_binary(&query::account::query_first_free_account(deps)?) + QueryMsg::QueryFirstFreeAccount(data) => { + to_binary(&query::account::query_first_free_account(deps, data)?) } } } diff --git a/contracts/warp-job-account-tracker/src/error.rs b/contracts/warp-job-account-tracker/src/error.rs index 0eefe76a..592558a5 100644 --- a/contracts/warp-job-account-tracker/src/error.rs +++ b/contracts/warp-job-account-tracker/src/error.rs @@ -38,14 +38,14 @@ pub enum ContractError { #[error("Error resolving JSON path")] ResolveError {}, - #[error("Sub account already occupied")] - AccountAlreadyOccupiedError {}, + #[error("Sub account already taken")] + AccountAlreadyTakenError {}, #[error("Sub account already free")] AccountAlreadyFreeError {}, - #[error("Sub account should be occupied but it is free")] - AccountNotOccupiedError {}, + #[error("Sub account should be taken but it is free")] + AccountNotTakenError {}, } impl From for ContractError { diff --git a/contracts/warp-job-account-tracker/src/execute/account.rs b/contracts/warp-job-account-tracker/src/execute/account.rs index 9a5bb2a5..fc460636 100644 --- a/contracts/warp-job-account-tracker/src/execute/account.rs +++ b/contracts/warp-job-account-tracker/src/execute/account.rs @@ -4,12 +4,17 @@ use cosmwasm_std::{DepsMut, Response}; use job_account_tracker::{FreeAccountMsg, TakeAccountMsg}; pub fn taken_account(deps: DepsMut, data: TakeAccountMsg) -> Result { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; - FREE_ACCOUNTS.remove(deps.storage, account_addr_ref); - TAKEN_ACCOUNTS.update(deps.storage, account_addr_ref, |s| match s { - None => Ok(data.job_id), - Some(_) => Err(ContractError::AccountAlreadyOccupiedError {}), - })?; + FREE_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); + TAKEN_ACCOUNTS.update( + deps.storage, + (account_owner_ref, account_addr_ref), + |s| match s { + None => Ok(data.job_id), + Some(_) => Err(ContractError::AccountAlreadyTakenError {}), + }, + )?; Ok(Response::new() .add_attribute("action", "taken_account") .add_attribute("account_addr", data.account_addr) @@ -17,13 +22,18 @@ pub fn taken_account(deps: DepsMut, data: TakeAccountMsg) -> Result Result { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; - TAKEN_ACCOUNTS.remove(deps.storage, account_addr_ref); - FREE_ACCOUNTS.update(deps.storage, account_addr_ref, |s| match s { - // value is a dummy data because there is no built in support for set in cosmwasm - None => Ok(true), - Some(_) => Err(ContractError::AccountAlreadyFreeError {}), - })?; + TAKEN_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); + FREE_ACCOUNTS.update( + deps.storage, + (account_owner_ref, account_addr_ref), + |s| match s { + // value is a dummy data because there is no built in support for set in cosmwasm + None => Ok(true), + Some(_) => Err(ContractError::AccountAlreadyFreeError {}), + }, + )?; Ok(Response::new() .add_attribute("action", "free_account") .add_attribute("account_addr", data.account_addr)) diff --git a/contracts/warp-job-account-tracker/src/integration_tests.rs b/contracts/warp-job-account-tracker/src/integration_tests.rs index 252c31f3..0d6dc961 100644 --- a/contracts/warp-job-account-tracker/src/integration_tests.rs +++ b/contracts/warp-job-account-tracker/src/integration_tests.rs @@ -52,7 +52,8 @@ mod tests { warp_job_account_tracker_contract_code_id, Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), &InstantiateMsg { - owner: USER_1.to_string(), + admin: USER_1.to_string(), + warp_addr: DUMMY_WARP_CONTROLLER_ADDR.to_string(), }, &[], "warp_job_account_tracker", @@ -87,15 +88,17 @@ mod tests { ), Ok(ConfigResponse { config: Config { - owner: Addr::unchecked(USER_1), - creator_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), + admin: Addr::unchecked(USER_1), + warp_addr: Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), } }) ); assert_eq!( app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), - &QueryMsg::QueryFirstFreeAccount(QueryFirstFreeAccountMsg {}) + &QueryMsg::QueryFirstFreeAccount(QueryFirstFreeAccountMsg { + account_owner_addr: USER_1.to_string(), + }) ), Ok(FirstFreeAccountResponse { account: None }) ); @@ -103,6 +106,7 @@ mod tests { app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + account_owner_addr: USER_1.to_string(), start_after: None, limit: None }) @@ -116,6 +120,7 @@ mod tests { app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { + account_owner_addr: USER_1.to_string(), start_after: None, limit: None }) @@ -131,6 +136,7 @@ mod tests { Addr::unchecked(USER_1), warp_job_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), }), &[], @@ -142,6 +148,7 @@ mod tests { Addr::unchecked(USER_1), warp_job_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), }), &[], @@ -154,6 +161,7 @@ mod tests { Addr::unchecked(USER_1), warp_job_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), }), &[], @@ -164,6 +172,7 @@ mod tests { Addr::unchecked(USER_1), warp_job_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_3_ADDR.to_string(), }), &[], @@ -173,12 +182,14 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), - &QueryMsg::QueryFirstFreeAccount(QueryFirstFreeAccountMsg {}) + &QueryMsg::QueryFirstFreeAccount(QueryFirstFreeAccountMsg { + account_owner_addr: USER_1.to_string(), + }) ), Ok(FirstFreeAccountResponse { account: Some(Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - occupied_by_job_id: None + taken_by_job_id: None }) }) ); @@ -188,6 +199,7 @@ mod tests { app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + account_owner_addr: USER_1.to_string(), start_after: None, limit: None }) @@ -196,26 +208,27 @@ mod tests { accounts: vec![ Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), - occupied_by_job_id: None + taken_by_job_id: None }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), - occupied_by_job_id: None + taken_by_job_id: None }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - occupied_by_job_id: None + taken_by_job_id: None } ], total_count: 3 }) ); - // Query occupied accounts + // Query taken accounts assert_eq!( app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { + account_owner_addr: USER_1.to_string(), start_after: None, limit: None }) @@ -226,11 +239,12 @@ mod tests { }) ); - // Occupy second account with job 1 + // Take second account with job 1 let _ = app.execute_contract( Addr::unchecked(USER_1), warp_job_account_tracker_contract_addr.clone(), &ExecuteMsg::TakeAccount(TakeAccountMsg { + account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), job_id: DUMMY_JOB_1_ID, }), @@ -243,12 +257,13 @@ mod tests { Addr::unchecked(USER_1), warp_job_account_tracker_contract_addr.clone(), &ExecuteMsg::TakeAccount(TakeAccountMsg { + account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), job_id: DUMMY_JOB_2_ID, }), &[], ), - ContractError::AccountAlreadyOccupiedError {}, + ContractError::AccountAlreadyTakenError {}, ); // Query free accounts @@ -256,6 +271,7 @@ mod tests { app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + account_owner_addr: USER_1.to_string(), start_after: None, limit: None }) @@ -264,22 +280,23 @@ mod tests { accounts: vec![ Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), - occupied_by_job_id: None + taken_by_job_id: None }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - occupied_by_job_id: None + taken_by_job_id: None } ], total_count: 2 }) ); - // Query occupied accounts + // Query taken accounts assert_eq!( app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { + account_owner_addr: USER_1.to_string(), start_after: None, limit: None }) @@ -287,7 +304,7 @@ mod tests { Ok(AccountsResponse { accounts: vec![Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), - occupied_by_job_id: Some(DUMMY_JOB_1_ID) + taken_by_job_id: Some(DUMMY_JOB_1_ID) },], total_count: 1 }) @@ -298,6 +315,7 @@ mod tests { Addr::unchecked(USER_1), warp_job_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { + account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), }), &[], @@ -308,6 +326,7 @@ mod tests { app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + account_owner_addr: USER_1.to_string(), start_after: None, limit: None }) @@ -316,26 +335,27 @@ mod tests { accounts: vec![ Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), - occupied_by_job_id: None + taken_by_job_id: None }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), - occupied_by_job_id: None + taken_by_job_id: None }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - occupied_by_job_id: None + taken_by_job_id: None } ], total_count: 3 }) ); - // Query occupied accounts + // Query taken accounts assert_eq!( app.wrap().query_wasm_smart( warp_job_account_tracker_contract_addr.clone(), &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { + account_owner_addr: USER_1.to_string(), start_after: None, limit: None }) diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-job-account-tracker/src/query/account.rs index abbd3ad0..f89c67af 100644 --- a/contracts/warp-job-account-tracker/src/query/account.rs +++ b/contracts/warp-job-account-tracker/src/query/account.rs @@ -1,11 +1,11 @@ use cosmwasm_std::{Deps, Order, StdResult}; -use cw_storage_plus::Bound; +use cw_storage_plus::{Bound, PrefixBound}; use crate::state::{CONFIG, FREE_ACCOUNTS, TAKEN_ACCOUNTS}; use job_account_tracker::{ - Account, AccountsResponse, ConfigResponse, FirstFreeAccountResponse, QueryFreeAccountsMsg, - QueryTakenAccountsMsg, + Account, AccountsResponse, ConfigResponse, FirstFreeAccountResponse, QueryFirstFreeAccountMsg, + QueryFreeAccountsMsg, QueryTakenAccountsMsg, }; const QUERY_LIMIT: u32 = 50; @@ -15,42 +15,62 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(ConfigResponse { config }) } -pub fn query_first_free_account(deps: Deps) -> StdResult { - match FREE_ACCOUNTS - .range(deps.storage, None, None, Order::Ascending) - .next() - { - Some(free_account) => Ok(FirstFreeAccountResponse { - account: Some(Account { - addr: free_account.unwrap().0, - occupied_by_job_id: None, - }), +pub fn query_first_free_account( + deps: Deps, + data: QueryFirstFreeAccountMsg, +) -> StdResult { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let maybe_free_account = FREE_ACCOUNTS + .prefix_range( + deps.storage, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ) + .next(); + let free_account = match maybe_free_account { + Some(Ok((account, _))) => Some(Account { + addr: account.1, + taken_by_job_id: None, }), - None => Ok(FirstFreeAccountResponse { account: None }), - } + _ => None, + }; + Ok(FirstFreeAccountResponse { + account: free_account, + }) } pub fn query_taken_accounts( deps: Deps, data: QueryTakenAccountsMsg, ) -> StdResult { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let iter = match data.start_after { - Some(start_after) => TAKEN_ACCOUNTS.range( + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + TAKEN_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Descending, + ) + } + None => TAKEN_ACCOUNTS.prefix_range( deps.storage, - Some(Bound::exclusive( - &deps.api.addr_validate(start_after.as_str()).unwrap(), - )), - None, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), Order::Descending, ), - None => TAKEN_ACCOUNTS.range(deps.storage, None, None, Order::Descending), }; let accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { - item.map(|(account_addr, job_id)| Account { - addr: account_addr, - occupied_by_job_id: Some(job_id), + item.map(|(account, job_id)| Account { + addr: account.1, + taken_by_job_id: Some(job_id), }) }) .collect::>>()?; @@ -61,23 +81,33 @@ pub fn query_taken_accounts( } pub fn query_free_accounts(deps: Deps, data: QueryFreeAccountsMsg) -> StdResult { + let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let iter = match data.start_after { - Some(start_after) => FREE_ACCOUNTS.range( + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + FREE_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Descending, + ) + } + None => FREE_ACCOUNTS.prefix_range( deps.storage, - Some(Bound::exclusive( - &deps.api.addr_validate(start_after.as_str()).unwrap(), - )), - None, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), Order::Descending, ), - None => FREE_ACCOUNTS.range(deps.storage, None, None, Order::Descending), }; let accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { - item.map(|(account_addr, _)| Account { - addr: account_addr, - occupied_by_job_id: None, + item.map(|(account, _)| Account { + addr: account.1, + taken_by_job_id: None, }) }) .collect::>>()?; diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-job-account-tracker/src/state.rs index 8314bfdd..710537be 100644 --- a/contracts/warp-job-account-tracker/src/state.rs +++ b/contracts/warp-job-account-tracker/src/state.rs @@ -4,8 +4,8 @@ use job_account_tracker::Config; pub const CONFIG: Item = Item::new("config"); -// Key is the account address, value is the ID of the pending job currently using it -pub const TAKEN_ACCOUNTS: Map<&Addr, Uint64> = Map::new("taken_accounts"); +// Key is the (account owner address, account address), value is the ID of the pending job currently using it +pub const TAKEN_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("taken_accounts"); -// Key is the account address, value is a dummy data that is always true to make it behave like a set -pub const FREE_ACCOUNTS: Map<&Addr, bool> = Map::new("free_accounts"); +// Key is the (account owner address, account address), value is a dummy data that is always true to make it behave like a set +pub const FREE_ACCOUNTS: Map<(&Addr, &Addr), bool> = Map::new("free_accounts"); diff --git a/contracts/warp-job-account/src/contract.rs b/contracts/warp-job-account/src/contract.rs index 046c0d48..c83e8a16 100644 --- a/contracts/warp-job-account/src/contract.rs +++ b/contracts/warp-job-account/src/contract.rs @@ -20,7 +20,6 @@ pub fn instantiate( &Config { owner: deps.api.addr_validate(&msg.owner)?, creator_addr: info.sender, - job_account_tracker_addr: deps.api.addr_validate(&msg.job_account_tracker_addr)?, }, )?; @@ -29,7 +28,6 @@ pub fn instantiate( .add_attribute("action", "instantiate") .add_attribute("job_id", msg.job_id) .add_attribute("contract_addr", instantiated_account_addr) - .add_attribute("job_account_tracker_addr", msg.job_account_tracker_addr) .add_attribute("owner", msg.owner) .add_attribute( "native_funds", diff --git a/contracts/warp-job-account/src/error.rs b/contracts/warp-job-account/src/error.rs index 81db9d26..85ae8cd0 100644 --- a/contracts/warp-job-account/src/error.rs +++ b/contracts/warp-job-account/src/error.rs @@ -38,14 +38,14 @@ pub enum ContractError { #[error("Error resolving JSON path")] ResolveError {}, - #[error("Sub account already occupied")] - SubAccountAlreadyOccupiedError {}, + #[error("Sub account already taken")] + SubAccountAlreadyTakenError {}, #[error("Sub account already free")] SubAccountAlreadyFreeError {}, - #[error("Sub account should be occupied but it is free")] - SubAccountNotOccupiedError {}, + #[error("Sub account should be taken but it is free")] + SubAccountNotTakenError {}, } impl From for ContractError { diff --git a/contracts/warp-job-account/src/tests.rs b/contracts/warp-job-account/src/tests.rs index 6b928768..5775661e 100644 --- a/contracts/warp-job-account/src/tests.rs +++ b/contracts/warp-job-account/src/tests.rs @@ -20,7 +20,6 @@ fn test_execute_controller() { InstantiateMsg { owner: "vlad".to_string(), job_id: Uint64::zero(), - job_account_tracker_addr: "vlad".to_string(), native_funds: vec![], cw_funds: vec![], msgs: vec![], @@ -146,7 +145,6 @@ fn test_execute_owner() { InstantiateMsg { owner: "vlad".to_string(), job_id: Uint64::zero(), - job_account_tracker_addr: "vlad".to_string(), native_funds: vec![], cw_funds: vec![], msgs: vec![], @@ -274,7 +272,6 @@ fn test_execute_unauth() { InstantiateMsg { owner: "vlad".to_string(), job_id: Uint64::zero(), - job_account_tracker_addr: "vlad".to_string(), native_funds: vec![], cw_funds: vec![], msgs: vec![], diff --git a/contracts/warp-legacy-account/src/error.rs b/contracts/warp-legacy-account/src/error.rs index 81db9d26..85ae8cd0 100644 --- a/contracts/warp-legacy-account/src/error.rs +++ b/contracts/warp-legacy-account/src/error.rs @@ -38,14 +38,14 @@ pub enum ContractError { #[error("Error resolving JSON path")] ResolveError {}, - #[error("Sub account already occupied")] - SubAccountAlreadyOccupiedError {}, + #[error("Sub account already taken")] + SubAccountAlreadyTakenError {}, #[error("Sub account already free")] SubAccountAlreadyFreeError {}, - #[error("Sub account should be occupied but it is free")] - SubAccountNotOccupiedError {}, + #[error("Sub account should be taken but it is free")] + SubAccountNotTakenError {}, } impl From for ContractError { diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index aac4a37d..3203a38d 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -17,11 +17,14 @@ pub struct Config { pub owner: Addr, pub fee_denom: String, pub fee_collector: Addr, - pub warp_job_account_tracker_code_id: Uint64, pub warp_account_code_id: Uint64, pub minimum_reward: Uint128, pub creation_fee_percentage: Uint64, pub cancellation_fee_percentage: Uint64, + // By querying job account tracker contract + // We know all accounts owned by that user and each account's availability + // For more detail, please refer to job account tracker contract + pub job_account_tracker_address: Addr, pub resolver_address: Addr, // maximum time for evictions pub t_max: Uint64, @@ -48,12 +51,12 @@ pub struct InstantiateMsg { pub owner: Option, pub fee_denom: String, pub fee_collector: Option, - pub warp_job_account_tracker_code_id: Uint64, pub warp_account_code_id: Uint64, pub minimum_reward: Uint128, pub creation_fee: Uint64, pub cancellation_fee: Uint64, pub resolver_address: String, + pub job_account_tracker_address: String, pub t_max: Uint64, pub t_min: Uint64, pub a_max: Uint128, @@ -73,9 +76,9 @@ pub enum ExecuteMsg { UpdateConfig(UpdateConfigMsg), MigrateLegacyAccounts(MigrateLegacyAccountsMsg), - MigrateJobAccountTrackers(MigrateJobAccountTrackersMsg), + MigrateJobAccountTracker(MigrateJobAccountTrackerMsg), MigrateFreeJobAccounts(MigrateJobAccountsMsg), - MigrateOccupiedJobAccounts(MigrateJobAccountsMsg), + MigrateTakenJobAccounts(MigrateJobAccountsMsg), MigratePendingJobs(MigrateJobsMsg), MigrateFinishedJobs(MigrateJobsMsg), @@ -103,15 +106,13 @@ pub struct MigrateLegacyAccountsMsg { } #[cw_serde] -pub struct MigrateJobAccountTrackersMsg { +pub struct MigrateJobAccountTrackerMsg { pub warp_job_account_tracker_code_id: Uint64, - pub start_after: Option, - pub limit: u8, } #[cw_serde] pub struct MigrateJobAccountsMsg { - pub job_account_tracker_addr: String, + pub account_owner_addr: String, pub warp_job_account_code_id: Uint64, pub start_after: Option, pub limit: u8, @@ -165,7 +166,7 @@ pub struct StateResponse { //migrate//{"resolver_address":"terra1a8dxkrapwj4mkpfnrv7vahd0say0lxvd0ft6qv","warp_account_code_id":"10081"} #[cw_serde] pub struct MigrateMsg { - pub warp_job_account_tracker_code_id: Uint64, pub warp_account_code_id: Uint64, pub resolver_address: String, + pub job_account_tracker_address: String, } diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs index 1f2f1d6a..841745fc 100644 --- a/packages/job-account-tracker/src/lib.rs +++ b/packages/job-account-tracker/src/lib.rs @@ -3,15 +3,15 @@ use cosmwasm_std::{Addr, Uint64}; #[cw_serde] pub struct Config { - pub owner: Addr, + pub admin: Addr, // Address of warp controller contract - pub creator_addr: Addr, + pub warp_addr: Addr, } #[cw_serde] pub struct InstantiateMsg { - // User who owns this account - pub owner: String, + pub admin: String, + pub warp_addr: String, } #[cw_serde] @@ -23,12 +23,14 @@ pub enum ExecuteMsg { #[cw_serde] pub struct TakeAccountMsg { + pub account_owner_addr: String, pub account_addr: String, pub job_id: Uint64, } #[cw_serde] pub struct FreeAccountMsg { + pub account_owner_addr: String, pub account_addr: String, } @@ -55,12 +57,14 @@ pub struct ConfigResponse { #[cw_serde] pub struct QueryTakenAccountsMsg { + pub account_owner_addr: String, pub start_after: Option, pub limit: Option, } #[cw_serde] pub struct QueryFreeAccountsMsg { + pub account_owner_addr: String, pub start_after: Option, pub limit: Option, } @@ -68,7 +72,7 @@ pub struct QueryFreeAccountsMsg { #[cw_serde] pub struct Account { pub addr: Addr, - pub occupied_by_job_id: Option, + pub taken_by_job_id: Option, } #[cw_serde] @@ -78,7 +82,9 @@ pub struct AccountsResponse { } #[cw_serde] -pub struct QueryFirstFreeAccountMsg {} +pub struct QueryFirstFreeAccountMsg { + pub account_owner_addr: String, +} #[cw_serde] pub struct FirstFreeAccountResponse { diff --git a/packages/job-account/src/lib.rs b/packages/job-account/src/lib.rs index bcd0a2e3..07048e05 100644 --- a/packages/job-account/src/lib.rs +++ b/packages/job-account/src/lib.rs @@ -9,9 +9,6 @@ pub struct Config { pub owner: Addr, // Address of warp controller contract pub creator_addr: Addr, - - // Address of account tracker contract - pub job_account_tracker_addr: Addr, } #[cw_serde] @@ -20,12 +17,6 @@ pub struct InstantiateMsg { pub owner: String, // ID of the job that is created along with the account pub job_id: Uint64, - - // Account tracker tracks all accounts owned by user - // Store it inside account for easier lookup, though most of time we only lookup account from account tracker - // But store it enables us the other way around - pub job_account_tracker_addr: String, - // Native funds pub native_funds: Vec, // CW20 or CW721 funds, will be transferred to account in reply of account instantiation From ca23c53ec298f29da8e8a2b8bf76fa7a5cf4e4e7 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 3 Nov 2023 15:24:51 +0100 Subject: [PATCH 056/133] update refs + migration + change burn message to fee collector send --- contracts/warp-controller/src/contract.rs | 116 +++++++++---------- contracts/warp-controller/src/execute/job.rs | 5 +- refs.json | 10 +- 3 files changed, 65 insertions(+), 66 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index b8469f2f..ce9d193a 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -4,15 +4,13 @@ use crate::{execute, query, state::STATE, ContractError}; use account::{GenericMsg, WithdrawAssetsMsg}; use controller::account::{Account, Fund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; use controller::job::{Job, JobStatus}; -use cosmwasm_schema::cw_serde; use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State}; use cosmwasm_std::{ - entry_point, to_binary, Addr, Attribute, BalanceResponse, BankMsg, BankQuery, Binary, Coin, + entry_point, to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, QueryRequest, Reply, Response, StdError, StdResult, SubMsgResult, Uint128, Uint64, WasmMsg, }; -use cw_storage_plus::Item; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -133,62 +131,62 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { - //CONFIG - #[cw_serde] - pub struct V1Config { - pub owner: Addr, - pub fee_denom: String, - pub fee_collector: Addr, - pub warp_account_code_id: Uint64, - pub minimum_reward: Uint128, - pub creation_fee_percentage: Uint64, - pub cancellation_fee_percentage: Uint64, - pub resolver_address: Addr, - // maximum time for evictions - pub t_max: Uint64, - // minimum time for evictions - pub t_min: Uint64, - // maximum fee for evictions - pub a_max: Uint128, - // minimum fee for evictions - pub a_min: Uint128, - // maximum length of queue modifier for evictions - pub q_max: Uint64, - } - - const V1CONFIG: Item = Item::new("config"); - - let v1_config = V1CONFIG.load(deps.storage)?; - - CONFIG.save( - deps.storage, - &Config { - owner: v1_config.owner, - fee_denom: v1_config.fee_denom, - fee_collector: v1_config.fee_collector, - warp_account_code_id: v1_config.warp_account_code_id, - minimum_reward: v1_config.minimum_reward, - creation_fee_percentage: v1_config.creation_fee_percentage, - cancellation_fee_percentage: v1_config.cancellation_fee_percentage, - resolver_address: v1_config.resolver_address, - t_max: v1_config.t_max, - t_min: v1_config.t_min, - a_max: v1_config.a_max, - a_min: v1_config.a_min, - q_max: v1_config.q_max, - creation_fee_min: msg.creation_fee_min, - creation_fee_max: msg.creation_fee_max, - burn_fee_min: msg.burn_fee_min, - maintenance_fee_min: msg.maintenance_fee_min, - maintenance_fee_max: msg.maintenance_fee_max, - duration_days_left: msg.duration_days_left, - duration_days_right: msg.duration_days_right, - queue_size_left: msg.queue_size_left, - queue_size_right: msg.queue_size_right, - burn_fee_rate: msg.burn_fee_rate, - }, - )?; +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + // //CONFIG + // #[cw_serde] + // pub struct V1Config { + // pub owner: Addr, + // pub fee_denom: String, + // pub fee_collector: Addr, + // pub warp_account_code_id: Uint64, + // pub minimum_reward: Uint128, + // pub creation_fee_percentage: Uint64, + // pub cancellation_fee_percentage: Uint64, + // pub resolver_address: Addr, + // // maximum time for evictions + // pub t_max: Uint64, + // // minimum time for evictions + // pub t_min: Uint64, + // // maximum fee for evictions + // pub a_max: Uint128, + // // minimum fee for evictions + // pub a_min: Uint128, + // // maximum length of queue modifier for evictions + // pub q_max: Uint64, + // } + + // const V1CONFIG: Item = Item::new("config"); + + // let v1_config = V1CONFIG.load(deps.storage)?; + + // CONFIG.save( + // deps.storage, + // &Config { + // owner: v1_config.owner, + // fee_denom: v1_config.fee_denom, + // fee_collector: v1_config.fee_collector, + // warp_account_code_id: v1_config.warp_account_code_id, + // minimum_reward: v1_config.minimum_reward, + // creation_fee_percentage: v1_config.creation_fee_percentage, + // cancellation_fee_percentage: v1_config.cancellation_fee_percentage, + // resolver_address: v1_config.resolver_address, + // t_max: v1_config.t_max, + // t_min: v1_config.t_min, + // a_max: v1_config.a_max, + // a_min: v1_config.a_min, + // q_max: v1_config.q_max, + // creation_fee_min: msg.creation_fee_min, + // creation_fee_max: msg.creation_fee_max, + // burn_fee_min: msg.burn_fee_min, + // maintenance_fee_min: msg.maintenance_fee_min, + // maintenance_fee_max: msg.maintenance_fee_max, + // duration_days_left: msg.duration_days_left, + // duration_days_right: msg.duration_days_right, + // queue_size_left: msg.queue_size_left, + // queue_size_right: msg.queue_size_right, + // burn_fee_rate: msg.burn_fee_rate, + // }, + // )?; Ok(Response::new()) } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 094762d4..789eb544 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -123,8 +123,9 @@ pub fn create_job( WasmMsg::Execute { contract_addr: account.account.to_string(), msg: to_binary(&account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Burn { - amount: vec![Coin::new(burn_fee.u128(), config.fee_denom)], + msgs: vec![CosmosMsg::Bank(BankMsg::Send { + to_address: config.fee_collector.to_string(), + amount: vec![Coin::new(burn_fee.u128(), config.fee_denom.clone())], })], }))?, funds: vec![], diff --git a/refs.json b/refs.json index fd05aef3..b93f185f 100644 --- a/refs.json +++ b/refs.json @@ -12,15 +12,15 @@ "codeId": "9626" }, "warp-controller": { - "codeId": "9629", - "address": "terra1evfl60alw2kf60levpmc5py6wa5m64eymvsy5zgnkw9kcr9vlnsqntjwma" + "codeId": "11360", + "address": "terra1fqcfh8vpqsl7l5yjjtq5wwu6sv989txncq5fa756tv7lywqexraq5vnjvt" }, "warp-resolver": { - "codeId": "9627", - "address": "terra14ut3phcu9cc64prrz8uup6evfnc73evpy8fxqxfujpp0tdyhf39q7ksvj3" + "codeId": "9263", + "address": "terra1lxfx6n792aw3hg47tchmyuhv5t30f334gus67pc250qx5zljadws65elnf" }, "warp-templates": { - "codeId": "9628", + "codeId": "9263", "address": "terra17xm2ewyg60y7eypnwav33fwm23hxs3qyd8qk9tnntj4d0rp2vvhsgkpwwp" } }, From 3e6972396afb17a8dfc64525419318706d9b8c2c Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 3 Nov 2023 15:29:08 +0100 Subject: [PATCH 057/133] minor --- packages/controller/src/lib.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 169e1643..9f39735b 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -179,16 +179,16 @@ pub struct StateResponse { #[cw_serde] pub struct MigrateMsg { - pub creation_fee_min: Uint128, - pub creation_fee_max: Uint128, - pub burn_fee_min: Uint128, - pub maintenance_fee_min: Uint128, - pub maintenance_fee_max: Uint128, - // duration_days fn interval [left, right] - pub duration_days_left: Uint128, - pub duration_days_right: Uint128, - // queue_size fn interval [left, right] - pub queue_size_left: Uint128, - pub queue_size_right: Uint128, - pub burn_fee_rate: Uint128, + // pub creation_fee_min: Uint128, + // pub creation_fee_max: Uint128, + // pub burn_fee_min: Uint128, + // pub maintenance_fee_min: Uint128, + // pub maintenance_fee_max: Uint128, + // // duration_days fn interval [left, right] + // pub duration_days_left: Uint128, + // pub duration_days_right: Uint128, + // // queue_size fn interval [left, right] + // pub queue_size_left: Uint128, + // pub queue_size_right: Uint128, + // pub burn_fee_rate: Uint128, } From df945aa38b0433f36ba86a10928c012e69f5d470 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 3 Nov 2023 16:15:37 +0100 Subject: [PATCH 058/133] new codeid for controller --- refs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refs.json b/refs.json index b93f185f..3ee53feb 100644 --- a/refs.json +++ b/refs.json @@ -12,7 +12,7 @@ "codeId": "9626" }, "warp-controller": { - "codeId": "11360", + "codeId": "11362", "address": "terra1fqcfh8vpqsl7l5yjjtq5wwu6sv989txncq5fa756tv7lywqexraq5vnjvt" }, "warp-resolver": { From 8a8ff7e71a61ed31cbaace57fc44318f3fec3fc2 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 6 Nov 2023 18:17:46 +0100 Subject: [PATCH 059/133] fixes --- contracts/warp-controller/src/execute/job.rs | 6 ++-- contracts/warp-controller/src/migrate/job.rs | 2 ++ .../warp-controller/src/reply/account.rs | 2 +- contracts/warp-controller/src/reply/job.rs | 28 +++++++++++++------ contracts/warp-controller/src/state.rs | 3 ++ .../warp-job-account-tracker/src/contract.rs | 2 +- packages/controller/src/job.rs | 1 + 7 files changed, 31 insertions(+), 13 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index f3546a11..7d100d4e 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -121,6 +121,7 @@ pub fn create_job( description: data.description, labels: data.labels, assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), + duration_days: data.duration_days, }, )?; @@ -162,7 +163,7 @@ pub fn create_job( if !native_funds_minus_reward_and_fee.is_empty() { // Fund account in native coins msgs.push(build_transfer_native_funds_msg( - available_account_addr.clone().to_string(), + available_account_addr.to_string(), native_funds_minus_reward_and_fee, )) } @@ -333,8 +334,7 @@ pub fn update_job( let job = JobQueue::update(&mut deps, env.clone(), data)?; - // TODO: add creation fee - let fee = added_reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); + let fee = compute_burn_fee(added_reward, &config); if !added_reward.is_zero() && fee.is_zero() { return Err(ContractError::RewardTooSmall {}); diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs index 6eb66967..ffc9ed80 100644 --- a/contracts/warp-controller/src/migrate/job.rs +++ b/contracts/warp-controller/src/migrate/job.rs @@ -100,6 +100,7 @@ pub fn migrate_pending_jobs( requeue_on_evict: old_job.requeue_on_evict, reward: old_job.reward, assets_to_withdraw: old_job.assets_to_withdraw, + duration_days: Uint128::from(30u128), }, )?; } @@ -167,6 +168,7 @@ pub fn migrate_finished_jobs( requeue_on_evict: old_job.requeue_on_evict, reward: old_job.reward, assets_to_withdraw: old_job.assets_to_withdraw, + duration_days: Uint128::from(30u128), }, )?; } diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index e5e8634f..d5856db2 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -96,7 +96,7 @@ pub fn create_job_account_and_job( if !native_funds.is_empty() { // Fund account in native coins msgs.push(build_transfer_native_funds_msg( - job_account_addr.clone().to_string(), + job_account_addr.to_string(), native_funds.clone(), )) } diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 1c324479..03dd0ad9 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -5,6 +5,7 @@ use cosmwasm_std::{ use crate::{ error::map_contract_error, + execute::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}, state::{JobQueue, LEGACY_ACCOUNTS, STATE}, util::{ legacy_account::is_legacy_account, @@ -75,9 +76,13 @@ pub fn execute_job( let mut new_job_attrs = vec![]; let new_job_id = state.current_job_id; - let fee = - finished_job.reward * Uint128::from(config.creation_fee_percentage) / Uint128::new(100); - let fee_plus_reward = fee + finished_job.reward; + let creation_fee = compute_creation_fee(Uint128::from(state.q), &config); + let maintenance_fee = compute_maintenance_fee(finished_job.duration_days, &config); + let burn_fee = compute_burn_fee(finished_job.reward, &config); + + let total_fees = creation_fee + maintenance_fee + burn_fee; + + let reward_plus_fee = finished_job.reward + total_fees; let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, finished_job.owner.clone())?; let job_account_addr = finished_job.account.clone(); @@ -94,7 +99,7 @@ pub fn execute_job( let mut recurring_job_created = false; if finished_job.recurring { - if job_account_amount < fee_plus_reward { + if job_account_amount < reward_plus_fee { new_job_attrs.push(Attribute::new("action", "recur_job")); new_job_attrs.push(Attribute::new("creation_status", "failed_insufficient_fee")); } else if !(finished_job.status == JobStatus::Executed @@ -181,16 +186,17 @@ pub fn execute_job( recurring: finished_job.recurring, reward: finished_job.reward, assets_to_withdraw: finished_job.assets_to_withdraw.clone(), + duration_days: finished_job.duration_days, }, )?; msgs.push(build_account_execute_generic_msgs( - job_account_addr.clone().to_string(), + job_account_addr.to_string(), vec![ // Job owner's warp account sends fee to fee collector build_transfer_native_funds_msg( config.fee_collector.to_string(), - vec![Coin::new(fee.u128(), config.fee_denom.clone())], + vec![Coin::new(total_fees.u128(), config.fee_denom.clone())], ), // Job owner's warp account sends reward to controller build_transfer_native_funds_msg( @@ -213,7 +219,13 @@ pub fn execute_job( serde_json_wasm::to_string(&new_job.executions)?, )); new_job_attrs.push(Attribute::new("job_reward", new_job.reward)); - new_job_attrs.push(Attribute::new("job_creation_fee", fee)); + new_job_attrs.push(Attribute::new("job_creation_fee", creation_fee.to_string())); + new_job_attrs.push(Attribute::new( + "job_maintenance_fee", + maintenance_fee.to_string(), + )); + new_job_attrs.push(Attribute::new("job_burn_fee", burn_fee.to_string())); + new_job_attrs.push(Attribute::new("job_total_fees", total_fees.to_string())); new_job_attrs.push(Attribute::new( "job_last_updated_time", new_job.last_update_time, @@ -237,7 +249,7 @@ pub fn execute_job( // No new job created, account has been free in execute_job, no need to free here again // Job owner withdraw all assets that are listed from warp account to itself msgs.push(build_account_withdraw_assets_msg( - job_account_addr.clone().to_string(), + job_account_addr.to_string(), finished_job.assets_to_withdraw, )); } diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 279e4c22..0e94473e 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -128,6 +128,7 @@ impl JobQueue { requeue_on_evict: job.requeue_on_evict, reward: job.reward, assets_to_withdraw: job.assets_to_withdraw, + duration_days: job.duration_days, }), }) } @@ -159,6 +160,7 @@ impl JobQueue { requeue_on_evict: job.requeue_on_evict, reward: job.reward + added_reward, assets_to_withdraw: job.assets_to_withdraw, + duration_days: job.duration_days, }), }) } @@ -192,6 +194,7 @@ impl JobQueue { requeue_on_evict: job.requeue_on_evict, reward: job.reward, assets_to_withdraw: job.assets_to_withdraw, + duration_days: job.duration_days, }; FINISHED_JOBS().update(deps.storage, job_id, |j| match j { diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index 4e3e73df..2563b660 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -27,7 +27,7 @@ pub fn instantiate( Ok(Response::new() .add_attribute("action", "instantiate") - .add_attribute("contract_addr", instantiated_account_addr.clone()) + .add_attribute("contract_addr", instantiated_account_addr) .add_attribute("admin", msg.admin) .add_attribute("warp_addr", msg.warp_addr)) } diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 04b092b7..187caa5b 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -39,6 +39,7 @@ pub struct Job { pub vars: String, pub recurring: bool, pub requeue_on_evict: bool, + pub duration_days: Uint128, pub reward: Uint128, pub assets_to_withdraw: Vec, } From d7a01fc965227c0d3dd7386664126028fea9f6f7 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 6 Nov 2023 18:18:58 +0100 Subject: [PATCH 060/133] clippy --- contracts/warp-controller/src/execute/job.rs | 6 +++--- contracts/warp-controller/src/reply/job.rs | 2 +- .../warp-job-account-tracker/src/integration_tests.rs | 2 +- contracts/warp-resolver/src/tests.rs | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 7d100d4e..f289b5da 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -296,7 +296,7 @@ pub fn delete_job( // Job owner withdraw all assets that are listed from warp account to itself msgs.push(build_account_withdraw_assets_msg( - job_account_addr.clone().to_string(), + job_account_addr.to_string(), job.assets_to_withdraw, )); @@ -332,7 +332,7 @@ pub fn update_job( return Err(ContractError::NameTooShort {}); } - let job = JobQueue::update(&mut deps, env.clone(), data)?; + let job = JobQueue::update(&mut deps, env, data)?; let fee = compute_burn_fee(added_reward, &config); @@ -489,7 +489,7 @@ pub fn evict_job( let account_amount = deps .querier .query::(&QueryRequest::Bank(BankQuery::Balance { - address: job_account_addr.clone().to_string(), + address: job_account_addr.to_string(), denom: config.fee_denom.clone(), }))? .amount diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 03dd0ad9..c5648bcf 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -116,7 +116,7 @@ pub fn execute_job( &resolver::QueryMsg::QueryApplyVarFn(resolver::QueryApplyVarFnMsg { vars: finished_job.vars, status: finished_job.status.clone(), - warp_account_addr: Some(finished_job.account.clone().to_string()), + warp_account_addr: Some(finished_job.account.to_string()), }), )?; diff --git a/contracts/warp-job-account-tracker/src/integration_tests.rs b/contracts/warp-job-account-tracker/src/integration_tests.rs index 0d6dc961..78c65da4 100644 --- a/contracts/warp-job-account-tracker/src/integration_tests.rs +++ b/contracts/warp-job-account-tracker/src/integration_tests.rs @@ -353,7 +353,7 @@ mod tests { // Query taken accounts assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_job_account_tracker_contract_addr, &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, diff --git a/contracts/warp-resolver/src/tests.rs b/contracts/warp-resolver/src/tests.rs index f52a782f..3ef11a61 100644 --- a/contracts/warp-resolver/src/tests.rs +++ b/contracts/warp-resolver/src/tests.rs @@ -398,7 +398,7 @@ fn test_hydrate_static_nested_vars_and_hydrate_msgs() { name: "var3".to_string(), kind: VariableKind::String, value: None, - init_fn: FnValue::String(StringValue::Simple(json_str.clone())), + init_fn: FnValue::String(StringValue::Simple(json_str)), reinitialize: false, update_fn: None, // when encode is true, value will be base64 encoded after msgs hydration @@ -486,7 +486,7 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { name: "var2".to_string(), kind: VariableKind::Uint, value: None, - init_fn: FnValue::Uint(NumValue::Simple(Uint256::from(100 as u64))), + init_fn: FnValue::Uint(NumValue::Simple(Uint256::from(100_u64))), reinitialize: false, update_fn: None, encode: false, @@ -496,7 +496,7 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { name: "var3".to_string(), kind: VariableKind::String, value: None, - init_fn: FnValue::String(StringValue::Simple(json_str.clone())), + init_fn: FnValue::String(StringValue::Simple(json_str)), reinitialize: false, update_fn: None, encode: true, @@ -548,7 +548,7 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { Variable::Static(static_var) => { assert_eq!( String::from_utf8(static_var.value.unwrap_or_default().into()).unwrap(), - dummy_warp_account_addr.clone() + dummy_warp_account_addr ) } _ => panic!("Expected static variable"), From 6ac2f5184e68993c3c719cd97daa5829a53e4ea9 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 6 Nov 2023 18:40:24 +0100 Subject: [PATCH 061/133] fix reply --- contracts/warp-controller/src/contract.rs | 18 +++-------- contracts/warp-controller/src/execute/job.rs | 5 ++-- contracts/warp-controller/src/reply/job.rs | 30 ++----------------- .../warp-job-account-tracker/src/state.rs | 2 +- 4 files changed, 9 insertions(+), 46 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index eacbbad6..9d0bad9d 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -14,13 +14,6 @@ use crate::{ use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State}; -// Reply id for job creation -// For user does not have available account -// So we create new job account account and job -pub const REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB: u64 = 1; -// Reply id for job execution -pub const REPLY_ID_EXECUTE_JOB: u64 = 2; - #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, @@ -259,13 +252,10 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { let config = CONFIG.load(deps.storage)?; + match msg.id { - // Job account has been created, now create job - REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB => { - reply::account::create_job_account_and_job(deps, env, msg, config) - } - // Job has been executed - REPLY_ID_EXECUTE_JOB => reply::job::execute_job(deps, env, msg, config), - _ => Err(ContractError::UnknownReplyId {}), + // use 0 as hack to call create_job_account_and_job + 0 => reply::account::create_job_account_and_job(deps, env, msg, config), + _id => reply::job::execute_job(deps, env, msg, config), } } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index f289b5da..c4221352 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -9,7 +9,6 @@ use cosmwasm_std::{ }; use crate::{ - contract::{REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, REPLY_ID_EXECUTE_JOB}, state::LEGACY_ACCOUNTS, util::{ fee::deduct_reward_and_fee_from_native_funds, @@ -138,7 +137,7 @@ pub fn create_job( None => { // Create account then create job in reply submsgs.push(SubMsg { - id: REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, + id: 0, msg: build_instantiate_warp_account_msg( job.id, env.contract.address.to_string(), @@ -417,7 +416,7 @@ pub fn execute_job( match resolution { Ok(true) => { submsgs.push(SubMsg { - id: REPLY_ID_EXECUTE_JOB, + id: data.id.u64(), msg: CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job.account.to_string(), msg: to_binary(&job_account::ExecuteMsg::Generic(GenericMsg { diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index c5648bcf..3c9dd496 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ Attribute, BalanceResponse, BankQuery, Coin, DepsMut, Env, QueryRequest, Reply, Response, - StdError, StdResult, SubMsgResult, Uint128, Uint64, + StdResult, SubMsgResult, Uint128, Uint64, }; use crate::{ @@ -34,33 +34,7 @@ pub fn execute_job( SubMsgResult::Err(_) => JobStatus::Failed, }; - let reply = msg - .result - .clone() - .into_result() - .map_err(StdError::generic_err)?; - - // TODO: how to get this event - let event = reply - .events - .iter() - .find(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "action" && attr.value == "execute_job") - }) - .ok_or_else(|| StdError::generic_err("cannot find `execute_job` event"))?; - - let job_id_str = event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "job_id") - .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? - .value; - - let job_id = job_id_str.as_str().parse::()?; + let job_id = msg.id; let finished_job = JobQueue::finalize(&mut deps, env.clone(), job_id, new_status)?; diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-job-account-tracker/src/state.rs index 710537be..400c501f 100644 --- a/contracts/warp-job-account-tracker/src/state.rs +++ b/contracts/warp-job-account-tracker/src/state.rs @@ -7,5 +7,5 @@ pub const CONFIG: Item = Item::new("config"); // Key is the (account owner address, account address), value is the ID of the pending job currently using it pub const TAKEN_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("taken_accounts"); -// Key is the (account owner address, account address), value is a dummy data that is always true to make it behave like a set +// Key is the (account owner address, account address), value is id of the last job which reserved it pub const FREE_ACCOUNTS: Map<(&Addr, &Addr), bool> = Map::new("free_accounts"); From c7a165b8ae04ad1d08154c4f60c336ad32dfb49f Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 7 Nov 2023 14:51:40 +0100 Subject: [PATCH 062/133] add last_job_id to free_accounts --- contracts/warp-controller/src/execute/job.rs | 3 +++ contracts/warp-controller/src/util/msg.rs | 2 ++ .../src/execute/account.rs | 2 +- .../src/integration_tests.rs | 23 +++++++++++-------- .../src/query/account.rs | 8 +++---- .../warp-job-account-tracker/src/state.rs | 2 +- packages/job-account-tracker/src/lib.rs | 1 + 7 files changed, 26 insertions(+), 15 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index c4221352..7ec8b2ab 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -290,6 +290,7 @@ pub fn delete_job( config.job_account_tracker_address.to_string(), job.owner.to_string(), job_account_addr.to_string(), + job.id, )); } @@ -460,6 +461,7 @@ pub fn execute_job( config.job_account_tracker_address.to_string(), job.owner.to_string(), job_account_addr.to_string(), + job.id, )); } @@ -553,6 +555,7 @@ pub fn evict_job( config.job_account_tracker_address.to_string(), job.owner.to_string(), job_account_addr.to_string(), + job.id, )); } } diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index e21d05ce..2a1d05ad 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -34,6 +34,7 @@ pub fn build_free_account_msg( job_account_tracker_addr: String, account_owner_addr: String, account_addr: String, + last_job_id: Uint64, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job_account_tracker_addr, @@ -41,6 +42,7 @@ pub fn build_free_account_msg( FreeAccountMsg { account_owner_addr, account_addr, + last_job_id, }, )) .unwrap(), diff --git a/contracts/warp-job-account-tracker/src/execute/account.rs b/contracts/warp-job-account-tracker/src/execute/account.rs index fc460636..501bd114 100644 --- a/contracts/warp-job-account-tracker/src/execute/account.rs +++ b/contracts/warp-job-account-tracker/src/execute/account.rs @@ -30,7 +30,7 @@ pub fn free_account(deps: DepsMut, data: FreeAccountMsg) -> Result Ok(true), + None => Ok(data.last_job_id), Some(_) => Err(ContractError::AccountAlreadyFreeError {}), }, )?; diff --git a/contracts/warp-job-account-tracker/src/integration_tests.rs b/contracts/warp-job-account-tracker/src/integration_tests.rs index 78c65da4..ca29f8d2 100644 --- a/contracts/warp-job-account-tracker/src/integration_tests.rs +++ b/contracts/warp-job-account-tracker/src/integration_tests.rs @@ -138,6 +138,7 @@ mod tests { &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), + last_job_id: DUMMY_JOB_1_ID, }), &[], ); @@ -150,6 +151,7 @@ mod tests { &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), + last_job_id: DUMMY_JOB_1_ID, }), &[], ), @@ -163,6 +165,7 @@ mod tests { &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + last_job_id: DUMMY_JOB_2_ID, }), &[], ); @@ -174,6 +177,7 @@ mod tests { &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_3_ADDR.to_string(), + last_job_id: DUMMY_JOB_1_ID, }), &[], ); @@ -189,7 +193,7 @@ mod tests { Ok(FirstFreeAccountResponse { account: Some(Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - taken_by_job_id: None + taken_by_job_id: Some(DUMMY_JOB_1_ID) }) }) ); @@ -208,15 +212,15 @@ mod tests { accounts: vec![ Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), - taken_by_job_id: None + taken_by_job_id: Some(DUMMY_JOB_1_ID) }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), - taken_by_job_id: None + taken_by_job_id: Some(DUMMY_JOB_2_ID) }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - taken_by_job_id: None + taken_by_job_id: Some(DUMMY_JOB_1_ID) } ], total_count: 3 @@ -280,11 +284,11 @@ mod tests { accounts: vec![ Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), - taken_by_job_id: None + taken_by_job_id: Some(DUMMY_JOB_1_ID) }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - taken_by_job_id: None + taken_by_job_id: Some(DUMMY_JOB_1_ID) } ], total_count: 2 @@ -317,6 +321,7 @@ mod tests { &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), + last_job_id: DUMMY_JOB_1_ID, }), &[], ); @@ -335,15 +340,15 @@ mod tests { accounts: vec![ Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), - taken_by_job_id: None + taken_by_job_id: Some(DUMMY_JOB_1_ID) }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), - taken_by_job_id: None + taken_by_job_id: Some(DUMMY_JOB_1_ID) }, Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - taken_by_job_id: None + taken_by_job_id: Some(DUMMY_JOB_1_ID) } ], total_count: 3 diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-job-account-tracker/src/query/account.rs index f89c67af..1f6aad72 100644 --- a/contracts/warp-job-account-tracker/src/query/account.rs +++ b/contracts/warp-job-account-tracker/src/query/account.rs @@ -29,9 +29,9 @@ pub fn query_first_free_account( ) .next(); let free_account = match maybe_free_account { - Some(Ok((account, _))) => Some(Account { + Some(Ok((account, last_job_id))) => Some(Account { addr: account.1, - taken_by_job_id: None, + taken_by_job_id: Some(last_job_id), }), _ => None, }; @@ -105,9 +105,9 @@ pub fn query_free_accounts(deps: Deps, data: QueryFreeAccountsMsg) -> StdResult< let accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { - item.map(|(account, _)| Account { + item.map(|(account, last_job_id)| Account { addr: account.1, - taken_by_job_id: None, + taken_by_job_id: Some(last_job_id), }) }) .collect::>>()?; diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-job-account-tracker/src/state.rs index 400c501f..5dca505e 100644 --- a/contracts/warp-job-account-tracker/src/state.rs +++ b/contracts/warp-job-account-tracker/src/state.rs @@ -8,4 +8,4 @@ pub const CONFIG: Item = Item::new("config"); pub const TAKEN_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("taken_accounts"); // Key is the (account owner address, account address), value is id of the last job which reserved it -pub const FREE_ACCOUNTS: Map<(&Addr, &Addr), bool> = Map::new("free_accounts"); +pub const FREE_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("free_accounts"); diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs index 841745fc..fb265394 100644 --- a/packages/job-account-tracker/src/lib.rs +++ b/packages/job-account-tracker/src/lib.rs @@ -32,6 +32,7 @@ pub struct TakeAccountMsg { pub struct FreeAccountMsg { pub account_owner_addr: String, pub account_addr: String, + pub last_job_id: Uint64, } #[derive(QueryResponses)] From 4d61198de9009fcae12adc2e87793fa5fa8ba2be Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 7 Nov 2023 15:27:27 +0100 Subject: [PATCH 063/133] remove added_reward from update_job --- contracts/warp-controller/src/contract.rs | 5 +-- contracts/warp-controller/src/execute/job.rs | 41 +------------------- contracts/warp-controller/src/state.rs | 15 ++----- packages/controller/src/job.rs | 1 - 4 files changed, 7 insertions(+), 55 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 9d0bad9d..bd920c72 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -107,10 +107,7 @@ pub fn execute( let fee_denom_paid_amount = must_pay(&info, &config.fee_denom).unwrap(); execute::job::delete_job(deps, env, info, *data, config, fee_denom_paid_amount) } - ExecuteMsg::UpdateJob(data) => { - let fee_denom_paid_amount = must_pay(&info, &config.fee_denom).unwrap(); - execute::job::update_job(deps, env, info, *data, config, fee_denom_paid_amount) - } + ExecuteMsg::UpdateJob(data) => execute::job::update_job(deps, env, info, *data), ExecuteMsg::ExecuteJob(data) => { nonpayable(&info).unwrap(); execute::job::execute_job(deps, env, info, *data, config) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 7ec8b2ab..961a5316 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -4,8 +4,8 @@ use controller::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Execution, Job, JobStatus, UpdateJobMsg, }; use cosmwasm_std::{ - to_binary, Attribute, BalanceResponse, BankMsg, BankQuery, Coin, CosmosMsg, DepsMut, Env, - MessageInfo, QueryRequest, ReplyOn, Response, StdResult, SubMsg, Uint128, Uint64, WasmMsg, + to_binary, Attribute, BalanceResponse, BankQuery, Coin, CosmosMsg, DepsMut, Env, MessageInfo, + QueryRequest, ReplyOn, Response, StdResult, SubMsg, Uint128, Uint64, WasmMsg, }; use crate::{ @@ -313,8 +313,6 @@ pub fn update_job( env: Env, info: MessageInfo, data: UpdateJobMsg, - config: Config, - fee_denom_paid_amount: Uint128, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; @@ -322,8 +320,6 @@ pub fn update_job( return Err(ContractError::Unauthorized {}); } - let added_reward = data.added_reward.unwrap_or(Uint128::new(0)); - if data.name.is_some() && data.name.clone().unwrap().len() > MAX_TEXT_LENGTH { return Err(ContractError::NameTooLong {}); } @@ -334,39 +330,7 @@ pub fn update_job( let job = JobQueue::update(&mut deps, env, data)?; - let fee = compute_burn_fee(added_reward, &config); - - if !added_reward.is_zero() && fee.is_zero() { - return Err(ContractError::RewardTooSmall {}); - } - if fee + added_reward > fee_denom_paid_amount { - return Err(ContractError::InsufficientFundsToPayForRewardAndFee {}); - } - - let mut msgs = vec![]; - - if added_reward > Uint128::zero() { - // Job owner sends reward to controller when it calls create_job - // Reward stays at controller, no need to send it elsewhere - - msgs.push( - // Job owner sends fee to controller when it calls update_job - // Controller sends update fee to fee collector - WasmMsg::Execute { - contract_addr: job.account.to_string(), - msg: to_binary(&job_account::ExecuteMsg::Generic(GenericMsg { - msgs: vec![CosmosMsg::Bank(BankMsg::Send { - to_address: config.fee_collector.to_string(), - amount: vec![Coin::new((fee).u128(), config.fee_denom)], - })], - }))?, - funds: vec![], - }, - ); - } - Ok(Response::new() - .add_messages(msgs) .add_attribute("action", "update_job") .add_attribute("job_id", job.id) .add_attribute("job_owner", job.owner) @@ -377,7 +341,6 @@ pub fn update_job( serde_json_wasm::to_string(&job.executions)?, ) .add_attribute("job_reward", job.reward) - .add_attribute("job_update_fee", fee) .add_attribute("job_last_updated_time", job.last_update_time)) } diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 0e94473e..28843710 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Addr, DepsMut, Env, Uint128, Uint64}; +use cosmwasm_std::{Addr, DepsMut, Env, Uint64}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; use controller::{ @@ -133,10 +133,7 @@ impl JobQueue { }) } - pub fn update(deps: &mut DepsMut, env: Env, data: UpdateJobMsg) -> Result { - let config = CONFIG.load(deps.storage)?; - let added_reward: Uint128 = data.added_reward.unwrap_or(Uint128::new(0)); - + pub fn update(deps: &mut DepsMut, _env: Env, data: UpdateJobMsg) -> Result { PENDING_JOBS().update(deps.storage, data.id.u64(), |h| match h { None => Err(ContractError::JobDoesNotExist {}), Some(job) => Ok(Job { @@ -144,11 +141,7 @@ impl JobQueue { prev_id: job.prev_id, owner: job.owner, account: job.account, - last_update_time: if added_reward > config.minimum_reward { - Uint64::new(env.block.time.seconds()) - } else { - job.last_update_time - }, + last_update_time: job.last_update_time, name: data.name.unwrap_or(job.name), description: data.description.unwrap_or(job.description), labels: data.labels.unwrap_or(job.labels), @@ -158,7 +151,7 @@ impl JobQueue { vars: job.vars, recurring: job.recurring, requeue_on_evict: job.requeue_on_evict, - reward: job.reward + added_reward, + reward: job.reward, assets_to_withdraw: job.assets_to_withdraw, duration_days: job.duration_days, }), diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 187caa5b..5915b667 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -95,7 +95,6 @@ pub struct UpdateJobMsg { pub name: Option, pub description: Option, pub labels: Option>, - pub added_reward: Option, } #[cw_serde] From b76a52c5c7dbb92fc7b4af02853881d9d1aa9e87 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 7 Nov 2023 15:43:28 +0100 Subject: [PATCH 064/133] add created_at_time and remove requeue_on_evict to job --- contracts/warp-controller/src/execute/job.rs | 70 ++++++-------------- contracts/warp-controller/src/migrate/job.rs | 5 +- contracts/warp-controller/src/reply/job.rs | 2 +- contracts/warp-controller/src/state.rs | 6 +- packages/controller/src/job.rs | 17 +---- 5 files changed, 29 insertions(+), 71 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 961a5316..fd04878b 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -4,8 +4,8 @@ use controller::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Execution, Job, JobStatus, UpdateJobMsg, }; use cosmwasm_std::{ - to_binary, Attribute, BalanceResponse, BankQuery, Coin, CosmosMsg, DepsMut, Env, MessageInfo, - QueryRequest, ReplyOn, Response, StdResult, SubMsg, Uint128, Uint64, WasmMsg, + to_binary, Attribute, Coin, CosmosMsg, DepsMut, Env, MessageInfo, ReplyOn, Response, StdResult, + SubMsg, Uint128, Uint64, WasmMsg, }; use crate::{ @@ -113,7 +113,6 @@ pub fn create_job( status: JobStatus::Pending, terminate_condition: data.terminate_condition, recurring: data.recurring, - requeue_on_evict: data.requeue_on_evict, vars: data.vars, executions: data.executions, reward: data.reward, @@ -121,6 +120,7 @@ pub fn create_job( labels: data.labels, assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), duration_days: data.duration_days, + created_at_time: Uint64::from(env.block.time.seconds()), }, )?; @@ -450,15 +450,6 @@ pub fn evict_job( let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, job.owner.clone())?; let job_account_addr = job.account.clone(); - let account_amount = deps - .querier - .query::(&QueryRequest::Bank(BankQuery::Balance { - address: job_account_addr.to_string(), - denom: config.fee_denom.clone(), - }))? - .amount - .amount; - if job.status != JobStatus::Pending { return Err(ContractError::Unauthorized {}); } @@ -481,46 +472,29 @@ pub fn evict_job( let mut msgs = vec![]; - let job_status; + // Job will be evicted + let job_status = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; - if job.requeue_on_evict && account_amount >= a { - // Job will stay active cause it has enough funds to pay for eviction fee and it's set to requeue on eviction - msgs.push( - // Job owner's warp account sends reward to evictor - build_account_execute_generic_msgs( - job_account_addr.to_string(), - vec![build_transfer_native_funds_msg( - info.sender.to_string(), - vec![Coin::new(a.u128(), config.fee_denom)], - )], - ), - ); - job_status = JobQueue::sync(&mut deps, env, job.clone())?.status; - } else { - // Job will be evicted - job_status = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; + // Controller sends eviction reward to evictor + msgs.push(build_transfer_native_funds_msg( + info.sender.to_string(), + vec![Coin::new(a.u128(), config.fee_denom.clone())], + )); - // Controller sends eviction reward to evictor - msgs.push(build_transfer_native_funds_msg( - info.sender.to_string(), - vec![Coin::new(a.u128(), config.fee_denom.clone())], - )); + // Controller sends execution reward minus eviction reward back to account + msgs.push(build_transfer_native_funds_msg( + info.sender.to_string(), + vec![Coin::new((job.reward - a).u128(), config.fee_denom.clone())], + )); - // Controller sends execution reward minus eviction reward back to account - msgs.push(build_transfer_native_funds_msg( - info.sender.to_string(), - vec![Coin::new((job.reward - a).u128(), config.fee_denom.clone())], + if !is_legacy_account(legacy_account, job_account_addr.clone()) { + // Free account + msgs.push(build_free_account_msg( + config.job_account_tracker_address.to_string(), + job.owner.to_string(), + job_account_addr.to_string(), + job.id, )); - - if !is_legacy_account(legacy_account, job_account_addr.clone()) { - // Free account - msgs.push(build_free_account_msg( - config.job_account_tracker_address.to_string(), - job.owner.to_string(), - job_account_addr.to_string(), - job.id, - )); - } } Ok(Response::new() diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs index ffc9ed80..d00ae2fd 100644 --- a/contracts/warp-controller/src/migrate/job.rs +++ b/contracts/warp-controller/src/migrate/job.rs @@ -23,7 +23,6 @@ pub struct OldJob { pub executions: Vec, pub vars: String, pub recurring: bool, - pub requeue_on_evict: bool, pub reward: Uint128, pub assets_to_withdraw: Vec, } @@ -97,10 +96,10 @@ pub fn migrate_pending_jobs( executions: old_job.executions, vars: old_job.vars, recurring: old_job.recurring, - requeue_on_evict: old_job.requeue_on_evict, reward: old_job.reward, assets_to_withdraw: old_job.assets_to_withdraw, duration_days: Uint128::from(30u128), + created_at_time: old_job.last_update_time, }, )?; } @@ -165,10 +164,10 @@ pub fn migrate_finished_jobs( terminate_condition: None, vars: old_job.vars, recurring: old_job.recurring, - requeue_on_evict: old_job.requeue_on_evict, reward: old_job.reward, assets_to_withdraw: old_job.assets_to_withdraw, duration_days: Uint128::from(30u128), + created_at_time: old_job.last_update_time, }, )?; } diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 3c9dd496..9e2e25ac 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -156,11 +156,11 @@ pub fn execute_job( executions: finished_job.executions, terminate_condition: finished_job.terminate_condition.clone(), vars: new_vars, - requeue_on_evict: finished_job.requeue_on_evict, recurring: finished_job.recurring, reward: finished_job.reward, assets_to_withdraw: finished_job.assets_to_withdraw.clone(), duration_days: finished_job.duration_days, + created_at_time: Uint64::from(env.block.time.seconds()), }, )?; diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 28843710..8eab9f50 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -125,10 +125,10 @@ impl JobQueue { terminate_condition: job.terminate_condition, vars: job.vars, recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, reward: job.reward, assets_to_withdraw: job.assets_to_withdraw, duration_days: job.duration_days, + created_at_time: Uint64::from(env.block.time.seconds()), }), }) } @@ -150,10 +150,10 @@ impl JobQueue { terminate_condition: job.terminate_condition, vars: job.vars, recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, reward: job.reward, assets_to_withdraw: job.assets_to_withdraw, duration_days: job.duration_days, + created_at_time: job.created_at_time, }), }) } @@ -184,10 +184,10 @@ impl JobQueue { executions: job.executions, vars: job.vars, recurring: job.recurring, - requeue_on_evict: job.requeue_on_evict, reward: job.reward, assets_to_withdraw: job.assets_to_withdraw, duration_days: job.duration_days, + created_at_time: job.created_at_time, }; FINISHED_JOBS().update(deps.storage, job_id, |j| match j { diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 5915b667..131439ce 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -5,20 +5,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use strum_macros::Display; -// pub enum JobFund { -// Cw20(...), -// Native(...), -// Ibc(...) -// } - -// 1. create_account (can potential embed funds here) -// 2. cw20_sends, native (native send or within the create_job msg itself), ibc_send (to account) -// 3. create_job msg -// - job.funds -> withdraw_asset_from_account(...), withdraws from account to controller contract -// ... -// 4. execute_job msg -// - job succceeded - - #[cw_serde] pub struct Job { pub id: Uint64, @@ -38,8 +24,8 @@ pub struct Job { pub executions: Vec, pub vars: String, pub recurring: bool, - pub requeue_on_evict: bool, pub duration_days: Uint128, + pub created_at_time: Uint64, pub reward: Uint128, pub assets_to_withdraw: Vec, } @@ -76,7 +62,6 @@ pub struct CreateJobMsg { pub executions: Vec, pub vars: String, pub recurring: bool, - pub requeue_on_evict: bool, pub reward: Uint128, pub duration_days: Uint128, pub assets_to_withdraw: Option>, From d5dfd040794059336168e5387a6fca3e08b64d2c Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 7 Nov 2023 15:58:33 +0100 Subject: [PATCH 065/133] new eviction logic + refactor --- contracts/warp-controller/src/contract.rs | 8 +++--- contracts/warp-controller/src/execute/fee.rs | 28 +++++++++++--------- contracts/warp-controller/src/execute/job.rs | 24 ++++++----------- contracts/warp-controller/src/migrate/job.rs | 4 +-- contracts/warp-controller/src/reply/job.rs | 4 +-- packages/controller/src/job.rs | 4 +-- packages/controller/src/lib.rs | 16 +++++------ 7 files changed, 41 insertions(+), 47 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index bd920c72..b62f78b1 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -235,10 +235,10 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Uint128 { - let x1 = config.queue_size_left; +pub fn compute_creation_fee(queue_size: Uint64, config: &Config) -> Uint128 { + let x1 = Uint128::from(config.queue_size_left); let y1 = config.creation_fee_min; - let x2 = config.queue_size_right; + let x2 = Uint128::from(config.queue_size_right); let y2 = config.creation_fee_max; + let qs = Uint128::from(queue_size); let slope = (y2 - y1) / (x2 - x1); - if queue_size < x1 { + if qs < x1 { config.creation_fee_min - } else if queue_size < x2 { - slope * queue_size + y1 - slope * x1 + } else if qs < x2 { + slope * qs + y1 - slope * x1 } else { config.creation_fee_max } } -pub fn compute_maintenance_fee(duration_days: Uint128, config: &Config) -> Uint128 { - let x1 = config.duration_days_left; +pub fn compute_maintenance_fee(duration_days: Uint64, config: &Config) -> Uint128 { + let x1 = Uint128::from(config.duration_days_left); let y1 = config.maintenance_fee_min; - let x2 = config.duration_days_right; + let x2 = Uint128::from(config.duration_days_right); let y2 = config.maintenance_fee_max; + let dd = Uint128::from(duration_days); let slope = (y2 - y1) / (x2 - x1); - if duration_days < x1 { + if dd < x1 { config.maintenance_fee_min - } else if duration_days < x2 { - slope * duration_days + y1 - slope * x1 + } else if dd < x2 { + slope * dd + y1 - slope * x1 } else { config.maintenance_fee_max } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index fd04878b..528c37e3 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -64,7 +64,7 @@ pub fn create_job( }), )?; - let creation_fee = compute_creation_fee(Uint128::from(state.q), &config); + let creation_fee = compute_creation_fee(state.q, &config); let maintenance_fee = compute_maintenance_fee(data.duration_days, &config); let burn_fee = compute_burn_fee(data.reward, &config); @@ -445,7 +445,6 @@ pub fn evict_job( data: EvictJobMsg, config: Config, ) -> Result { - let state = STATE.load(deps.storage)?; let job = JobQueue::get(&deps, data.id.into())?; let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, job.owner.clone())?; let job_account_addr = job.account.clone(); @@ -454,19 +453,9 @@ pub fn evict_job( return Err(ContractError::Unauthorized {}); } - let t = if state.q < config.q_max { - config.t_max - state.q * (config.t_max - config.t_min) / config.q_max - } else { - config.t_min - }; - - let a = if state.q < config.q_max { - config.a_min - } else { - config.a_max - }; + let eviction_fee = config.a_max; - if env.block.time.seconds() - job.last_update_time.u64() < t.u64() { + if (env.block.time.seconds() - job.created_at_time.u64()) < (job.duration_days.u64() * 86400) { return Err(ContractError::EvictionPeriodNotElapsed {}); } @@ -478,13 +467,16 @@ pub fn evict_job( // Controller sends eviction reward to evictor msgs.push(build_transfer_native_funds_msg( info.sender.to_string(), - vec![Coin::new(a.u128(), config.fee_denom.clone())], + vec![Coin::new(eviction_fee.u128(), config.fee_denom.clone())], )); // Controller sends execution reward minus eviction reward back to account msgs.push(build_transfer_native_funds_msg( info.sender.to_string(), - vec![Coin::new((job.reward - a).u128(), config.fee_denom.clone())], + vec![Coin::new( + (job.reward - eviction_fee).u128(), + config.fee_denom.clone(), + )], )); if !is_legacy_account(legacy_account, job_account_addr.clone()) { diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs index d00ae2fd..c63bf204 100644 --- a/contracts/warp-controller/src/migrate/job.rs +++ b/contracts/warp-controller/src/migrate/job.rs @@ -98,7 +98,7 @@ pub fn migrate_pending_jobs( recurring: old_job.recurring, reward: old_job.reward, assets_to_withdraw: old_job.assets_to_withdraw, - duration_days: Uint128::from(30u128), + duration_days: Uint64::from(30u64), created_at_time: old_job.last_update_time, }, )?; @@ -166,7 +166,7 @@ pub fn migrate_finished_jobs( recurring: old_job.recurring, reward: old_job.reward, assets_to_withdraw: old_job.assets_to_withdraw, - duration_days: Uint128::from(30u128), + duration_days: Uint64::from(30u64), created_at_time: old_job.last_update_time, }, )?; diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 9e2e25ac..671e35db 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{ Attribute, BalanceResponse, BankQuery, Coin, DepsMut, Env, QueryRequest, Reply, Response, - StdResult, SubMsgResult, Uint128, Uint64, + StdResult, SubMsgResult, Uint64, }; use crate::{ @@ -50,7 +50,7 @@ pub fn execute_job( let mut new_job_attrs = vec![]; let new_job_id = state.current_job_id; - let creation_fee = compute_creation_fee(Uint128::from(state.q), &config); + let creation_fee = compute_creation_fee(state.q, &config); let maintenance_fee = compute_maintenance_fee(finished_job.duration_days, &config); let burn_fee = compute_burn_fee(finished_job.reward, &config); diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 131439ce..74a3c8b7 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -24,7 +24,7 @@ pub struct Job { pub executions: Vec, pub vars: String, pub recurring: bool, - pub duration_days: Uint128, + pub duration_days: Uint64, pub created_at_time: Uint64, pub reward: Uint128, pub assets_to_withdraw: Vec, @@ -63,7 +63,7 @@ pub struct CreateJobMsg { pub vars: String, pub recurring: bool, pub reward: Uint128, - pub duration_days: Uint128, + pub duration_days: Uint64, pub assets_to_withdraw: Option>, pub account_msgs: Option>, pub cw_funds: Option>, diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 08540e69..0dea8abb 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -42,11 +42,11 @@ pub struct Config { pub maintenance_fee_min: Uint128, pub maintenance_fee_max: Uint128, // duration_days fn interval [left, right] - pub duration_days_left: Uint128, - pub duration_days_right: Uint128, + pub duration_days_left: Uint64, + pub duration_days_right: Uint64, // queue_size fn interval [left, right] - pub queue_size_left: Uint128, - pub queue_size_right: Uint128, + pub queue_size_left: Uint64, + pub queue_size_right: Uint64, pub burn_fee_rate: Uint128, } @@ -80,11 +80,11 @@ pub struct InstantiateMsg { pub maintenance_fee_min: Uint128, pub maintenance_fee_max: Uint128, // duration_days fn interval [left, right] - pub duration_days_left: Uint128, - pub duration_days_right: Uint128, + pub duration_days_left: Uint64, + pub duration_days_right: Uint64, // queue_size fn interval [left, right] - pub queue_size_left: Uint128, - pub queue_size_right: Uint128, + pub queue_size_left: Uint64, + pub queue_size_right: Uint64, pub burn_fee_rate: Uint128, } From 9bf1e19ab83df13d7dd60b14e24de7436a238faa Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 9 Nov 2023 12:38:47 +0100 Subject: [PATCH 066/133] fix condition resolution in execute_job --- contracts/warp-controller/src/execute/job.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 528c37e3..73491524 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -373,9 +373,14 @@ pub fn execute_job( let mut submsgs = vec![]; for Execution { condition, msgs } in job.executions { - let resolution: StdResult = deps - .querier - .query_wasm_smart(config.resolver_address.clone(), &condition); + let resolution: StdResult = deps.querier.query_wasm_smart( + config.resolver_address.clone(), + &resolver::QueryMsg::QueryResolveCondition(resolver::QueryResolveConditionMsg { + condition, + vars: vars.clone(), + warp_account_addr: Some(job.account.to_string()), + }), + ); match resolution { Ok(true) => { From 71fffd59f2f139e472bff85543bf4be1ba72e7ad Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 9 Nov 2023 16:44:48 +0100 Subject: [PATCH 067/133] fix float point thing --- contracts/warp-controller/src/query/job.rs | 10 +++++----- .../warp-job-account-tracker/src/query/account.rs | 4 ++-- packages/controller/src/job.rs | 2 +- packages/job-account-tracker/src/lib.rs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/warp-controller/src/query/job.rs b/contracts/warp-controller/src/query/job.rs index 3d461b49..fdf01458 100644 --- a/contracts/warp-controller/src/query/job.rs +++ b/contracts/warp-controller/src/query/job.rs @@ -50,7 +50,7 @@ pub fn query_jobs(deps: Deps, env: Env, data: QueryJobsMsg) -> StdResult, job_status: Option, start_after: Option<(u128, u64)>, - limit: usize, + limit: u32, ) -> StdResult { let start = start_after.map(Bound::exclusive); let map = if job_status.is_some() && job_status.clone().unwrap() != JobStatus::Pending { @@ -118,7 +118,7 @@ pub fn query_jobs_by_reward( job_status.clone(), ) }) - .take(limit) + .take(limit as usize) .collect::>>()?; let mut jobs = vec![]; @@ -127,6 +127,6 @@ pub fn query_jobs_by_reward( } Ok(JobsResponse { jobs, - total_count: infos.len(), + total_count: infos.len() as u32, }) } diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-job-account-tracker/src/query/account.rs index 1f6aad72..602d28b7 100644 --- a/contracts/warp-job-account-tracker/src/query/account.rs +++ b/contracts/warp-job-account-tracker/src/query/account.rs @@ -75,7 +75,7 @@ pub fn query_taken_accounts( }) .collect::>>()?; Ok(AccountsResponse { - total_count: accounts.len(), + total_count: accounts.len() as u32, accounts, }) } @@ -112,7 +112,7 @@ pub fn query_free_accounts(deps: Deps, data: QueryFreeAccountsMsg) -> StdResult< }) .collect::>>()?; Ok(AccountsResponse { - total_count: accounts.len(), + total_count: accounts.len() as u32, accounts, }) } diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 74a3c8b7..2608a81c 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -146,5 +146,5 @@ pub struct JobResponse { #[cw_serde] pub struct JobsResponse { pub jobs: Vec, - pub total_count: usize, + pub total_count: u32, } diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs index fb265394..7b58fcac 100644 --- a/packages/job-account-tracker/src/lib.rs +++ b/packages/job-account-tracker/src/lib.rs @@ -79,7 +79,7 @@ pub struct Account { #[cw_serde] pub struct AccountsResponse { pub accounts: Vec, - pub total_count: usize, + pub total_count: u32, } #[cw_serde] From 6dc2b503aa82dfe9dc57d04adc73f5c31f950b64 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 9 Nov 2023 16:54:45 +0100 Subject: [PATCH 068/133] fix v1config --- contracts/warp-controller/src/contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index b62f78b1..fc92884d 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -183,6 +183,7 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Date: Thu, 9 Nov 2023 17:45:24 +0100 Subject: [PATCH 069/133] last set of changes --- contracts/warp-controller/src/contract.rs | 79 +------------------- contracts/warp-controller/src/migrate/job.rs | 19 ++--- packages/controller/src/lib.rs | 6 +- refs.json | 11 ++- 4 files changed, 23 insertions(+), 92 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index fc92884d..0005dd96 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -1,9 +1,7 @@ -use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - entry_point, to_binary, Addr, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, - StdResult, Uint128, Uint64, + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, + Uint64, }; -use cw_storage_plus::Item; use cw_utils::{must_pay, nonpayable}; use crate::{ @@ -172,78 +170,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { - //CONFIG - #[cw_serde] - pub struct V1Config { - pub owner: Addr, - pub fee_denom: String, - pub fee_collector: Addr, - pub warp_account_code_id: Uint64, - pub minimum_reward: Uint128, - pub creation_fee_percentage: Uint64, - pub cancellation_fee_percentage: Uint64, - pub resolver_address: Addr, - // maximum time for evictions - pub t_max: Uint64, - // minimum time for evictions - pub t_min: Uint64, - // maximum fee for evictions - pub a_max: Uint128, - // minimum fee for evictions - pub a_min: Uint128, - // maximum length of queue modifier for evictions - pub q_max: Uint64, - pub creation_fee_min: Uint128, - pub creation_fee_max: Uint128, - pub burn_fee_min: Uint128, - pub maintenance_fee_min: Uint128, - pub maintenance_fee_max: Uint128, - // duration_days fn interval [left, right] - pub duration_days_left: Uint128, - pub duration_days_right: Uint128, - // queue_size fn interval [left, right] - pub queue_size_left: Uint128, - pub queue_size_right: Uint128, - pub burn_fee_rate: Uint128, - } - - const V1CONFIG: Item = Item::new("config"); - - let v1_config = V1CONFIG.load(deps.storage)?; - - CONFIG.save( - deps.storage, - &Config { - owner: v1_config.owner, - fee_denom: v1_config.fee_denom, - fee_collector: v1_config.fee_collector, - warp_account_code_id: msg.warp_account_code_id, - minimum_reward: v1_config.minimum_reward, - creation_fee_percentage: v1_config.creation_fee_percentage, - cancellation_fee_percentage: v1_config.cancellation_fee_percentage, - resolver_address: deps.api.addr_validate(&msg.resolver_address)?, - job_account_tracker_address: deps - .api - .addr_validate(&msg.job_account_tracker_address)?, - t_max: v1_config.t_max, - t_min: v1_config.t_min, - a_max: v1_config.a_max, - a_min: v1_config.a_min, - q_max: v1_config.q_max, - creation_fee_min: v1_config.creation_fee_min, - creation_fee_max: v1_config.creation_fee_max, - burn_fee_min: v1_config.burn_fee_min, - maintenance_fee_min: v1_config.maintenance_fee_min, - maintenance_fee_max: v1_config.maintenance_fee_max, - duration_days_left: Uint64::from(v1_config.duration_days_left.u128() as u64), - duration_days_right: Uint64::from(v1_config.duration_days_right.u128() as u64), - queue_size_left: Uint64::from(v1_config.queue_size_left.u128() as u64), - queue_size_right: Uint64::from(v1_config.queue_size_right.u128() as u64), - burn_fee_rate: v1_config.burn_fee_rate, - }, - )?; - +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { Ok(Response::new()) } diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs index c63bf204..fd72cae8 100644 --- a/contracts/warp-controller/src/migrate/job.rs +++ b/contracts/warp-controller/src/migrate/job.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; -use crate::state::{FINISHED_JOBS, LEGACY_ACCOUNTS, PENDING_JOBS}; +use crate::state::{FINISHED_JOBS, PENDING_JOBS}; use crate::{state::CONFIG, ContractError}; use controller::account::AssetInfo; @@ -13,7 +13,9 @@ use cw_storage_plus::{Bound, Index, IndexList, IndexedMap, MultiIndex, UniqueInd #[cw_serde] pub struct OldJob { pub id: Uint64, + pub prev_id: Option, pub owner: Addr, + pub account: Addr, pub last_update_time: Uint64, pub name: String, pub description: String, @@ -23,6 +25,7 @@ pub struct OldJob { pub executions: Vec, pub vars: String, pub recurring: bool, + pub requeue_on_evict: bool, pub reward: Uint128, pub assets_to_withdraw: Vec, } @@ -77,22 +80,21 @@ pub fn migrate_pending_jobs( for job_key in job_keys { let old_job = OLD_PENDING_JOBS().load(deps.storage, job_key)?; - let warp_account = LEGACY_ACCOUNTS().load(deps.storage, old_job.owner.clone())?; PENDING_JOBS().save( deps.storage, job_key, &Job { id: old_job.id, - prev_id: None, + prev_id: old_job.prev_id, owner: old_job.owner, - account: warp_account.account, + account: old_job.account, last_update_time: old_job.last_update_time, name: old_job.name, description: old_job.description, labels: old_job.labels, status: old_job.status, - terminate_condition: None, + terminate_condition: old_job.terminate_condition, executions: old_job.executions, vars: old_job.vars, recurring: old_job.recurring, @@ -145,23 +147,22 @@ pub fn migrate_finished_jobs( for job_key in job_keys { let old_job = OLD_FINISHED_JOBS().load(deps.storage, job_key)?; - let warp_account = LEGACY_ACCOUNTS().load(deps.storage, old_job.owner.clone())?; FINISHED_JOBS().save( deps.storage, job_key, &Job { id: old_job.id, - prev_id: None, + prev_id: old_job.prev_id, owner: old_job.owner, - account: warp_account.account, + account: old_job.account, last_update_time: old_job.last_update_time, name: old_job.name, description: old_job.description, labels: old_job.labels, status: old_job.status, executions: old_job.executions, - terminate_condition: None, + terminate_condition: old_job.terminate_condition, vars: old_job.vars, recurring: old_job.recurring, reward: old_job.reward, diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 0dea8abb..1b5ee7cc 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -194,8 +194,4 @@ pub struct StateResponse { } #[cw_serde] -pub struct MigrateMsg { - pub warp_account_code_id: Uint64, - pub resolver_address: String, - pub job_account_tracker_address: String, -} +pub struct MigrateMsg {} diff --git a/refs.json b/refs.json index 3ee53feb..5de98d19 100644 --- a/refs.json +++ b/refs.json @@ -12,16 +12,23 @@ "codeId": "9626" }, "warp-controller": { - "codeId": "11362", + "codeId": "11634", "address": "terra1fqcfh8vpqsl7l5yjjtq5wwu6sv989txncq5fa756tv7lywqexraq5vnjvt" }, "warp-resolver": { - "codeId": "9263", + "codeId": "11521", "address": "terra1lxfx6n792aw3hg47tchmyuhv5t30f334gus67pc250qx5zljadws65elnf" }, "warp-templates": { "codeId": "9263", "address": "terra17xm2ewyg60y7eypnwav33fwm23hxs3qyd8qk9tnntj4d0rp2vvhsgkpwwp" + }, + "warp-job-account": { + "codeId": "11522" + }, + "warp-job-account-tracker": { + "codeId": "11630", + "address": "terra1zzgg30ygltd5s3xtescfquwmm2jktaq28t37f2j9h5wwswpxtyyspugek8" } }, "mainnet": { From c60f42e51ef7b0a28aee8208aff096c7e19fc87b Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 9 Nov 2023 17:52:59 +0100 Subject: [PATCH 070/133] fix schemas --- contracts/warp-job-account-tracker/.cargo/config | 2 +- contracts/warp-job-account/.cargo/config | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/warp-job-account-tracker/.cargo/config b/contracts/warp-job-account-tracker/.cargo/config index f4940a9d..b3ad083c 100644 --- a/contracts/warp-job-account-tracker/.cargo/config +++ b/contracts/warp-job-account-tracker/.cargo/config @@ -1,4 +1,4 @@ [alias] wasm = "build --release --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --example warp-account-schema" +schema = "run --example warp-job-account-tracker-schema" diff --git a/contracts/warp-job-account/.cargo/config b/contracts/warp-job-account/.cargo/config index f4940a9d..1fa27f1e 100644 --- a/contracts/warp-job-account/.cargo/config +++ b/contracts/warp-job-account/.cargo/config @@ -1,4 +1,4 @@ [alias] wasm = "build --release --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --example warp-account-schema" +schema = "run --example warp-job-account-schema" From 168d7c61a1c1ccfc360404c617c08dd8aeef3b53 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 9 Nov 2023 17:54:47 +0100 Subject: [PATCH 071/133] minor fix --- contracts/warp-legacy-account/.cargo/config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/warp-legacy-account/.cargo/config b/contracts/warp-legacy-account/.cargo/config index f4940a9d..b1f3d363 100644 --- a/contracts/warp-legacy-account/.cargo/config +++ b/contracts/warp-legacy-account/.cargo/config @@ -1,4 +1,4 @@ [alias] wasm = "build --release --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --example warp-account-schema" +schema = "run --example warp-legacy-account-schema" From 2421ebc41121405e85280aa03bab64bd5c83d0a4 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 13 Nov 2023 14:05:17 +0100 Subject: [PATCH 072/133] update schema --- .../examples/warp-job-account-tracker-schema.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs index f699fa0e..b6e74090 100644 --- a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs +++ b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs @@ -2,7 +2,10 @@ use std::env::current_dir; use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; -use job_account_tracker::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; +use job_account_tracker::{ + Account, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FirstFreeAccountResponse, + InstantiateMsg, QueryMsg, +}; fn main() { let mut out_dir = current_dir().unwrap(); @@ -14,4 +17,8 @@ fn main() { export_schema(&schema_for!(ExecuteMsg), &out_dir); export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(Config), &out_dir); + export_schema(&schema_for!(AccountsResponse), &out_dir); + export_schema(&schema_for!(FirstFreeAccountResponse), &out_dir); + export_schema(&schema_for!(ConfigResponse), &out_dir); + export_schema(&schema_for!(Account), &out_dir); } From dd5f2eb575e58499bb9d48c779673263c85c9075 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 13 Nov 2023 16:58:19 +0100 Subject: [PATCH 073/133] add WarpMsgs logic to job-account --- contracts/warp-job-account/src/contract.rs | 3 +- contracts/warp-job-account/src/execute/mod.rs | 1 + .../warp-job-account/src/execute/msgs.rs | 44 +++++++++++++++++++ .../warp-job-account/src/execute/withdraw.rs | 11 +++-- packages/job-account/src/lib.rs | 15 +++++++ 5 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 contracts/warp-job-account/src/execute/msgs.rs diff --git a/contracts/warp-job-account/src/contract.rs b/contracts/warp-job-account/src/contract.rs index c83e8a16..95d65efb 100644 --- a/contracts/warp-job-account/src/contract.rs +++ b/contracts/warp-job-account/src/contract.rs @@ -54,9 +54,10 @@ pub fn execute( .add_attribute("action", "generic")), ExecuteMsg::WithdrawAssets(data) => { nonpayable(&info).unwrap(); - execute::withdraw::withdraw_assets(deps, env, data, config) + execute::withdraw::withdraw_assets(deps.as_ref(), env, data, config) } ExecuteMsg::IbcTransfer(data) => execute::ibc::ibc_transfer(env, data), + ExecuteMsg::WarpMsgs(data) => execute::msgs::execute_warp_msgs(deps, env, data, config), } } diff --git a/contracts/warp-job-account/src/execute/mod.rs b/contracts/warp-job-account/src/execute/mod.rs index 2237e0b9..beccf81c 100644 --- a/contracts/warp-job-account/src/execute/mod.rs +++ b/contracts/warp-job-account/src/execute/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod ibc; +pub(crate) mod msgs; pub(crate) mod withdraw; diff --git a/contracts/warp-job-account/src/execute/msgs.rs b/contracts/warp-job-account/src/execute/msgs.rs new file mode 100644 index 00000000..78732ead --- /dev/null +++ b/contracts/warp-job-account/src/execute/msgs.rs @@ -0,0 +1,44 @@ +use crate::ContractError; +use cosmwasm_std::{Env, Response}; +use job_account::{Config, WarpMsg, WarpMsgs}; + +use cosmwasm_std::{CosmosMsg, DepsMut}; + +use super::ibc::ibc_transfer; +use super::withdraw::withdraw_assets; + +pub fn execute_warp_msgs( + deps: DepsMut, + env: Env, + data: WarpMsgs, + config: Config, +) -> Result { + let msgs = data + .msgs + .into_iter() + .flat_map(|msg| -> Vec { + match msg { + WarpMsg::Generic(msg) => vec![msg], + WarpMsg::IbcTransfer(msg) => ibc_transfer(env.clone(), msg) + .map(extract_messages) + .unwrap(), + WarpMsg::WithdrawAssets(msg) => { + withdraw_assets(deps.as_ref(), env.clone(), msg, config.clone()) + .map(extract_messages) + .unwrap() + } + } + }) + .collect::>(); + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "warp_msgs")) +} + +fn extract_messages(resp: Response) -> Vec { + resp.messages + .into_iter() + .map(|cosmos_msg| cosmos_msg.msg) + .collect() +} diff --git a/contracts/warp-job-account/src/execute/withdraw.rs b/contracts/warp-job-account/src/execute/withdraw.rs index 2cc9afc7..efbdde76 100644 --- a/contracts/warp-job-account/src/execute/withdraw.rs +++ b/contracts/warp-job-account/src/execute/withdraw.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{ - to_binary, Addr, BankMsg, CosmosMsg, Deps, DepsMut, Env, Response, StdResult, Uint128, WasmMsg, + to_binary, Addr, BankMsg, CosmosMsg, Deps, Env, Response, StdResult, Uint128, WasmMsg, }; use cw20::{BalanceResponse, Cw20ExecuteMsg}; use cw721::{Cw721QueryMsg, OwnerOfResponse}; @@ -9,7 +9,7 @@ use controller::account::{AssetInfo, Cw721ExecuteMsg}; use job_account::{Config, WithdrawAssetsMsg}; pub fn withdraw_assets( - deps: DepsMut, + deps: Deps, env: Env, data: WithdrawAssetsMsg, config: Config, @@ -20,7 +20,7 @@ pub fn withdraw_assets( match asset_info { AssetInfo::Native(denom) => { let withdraw_native_msg = - withdraw_asset_native(deps.as_ref(), env.clone(), &config.owner, denom)?; + withdraw_asset_native(deps, env.clone(), &config.owner, denom)?; match withdraw_native_msg { None => {} @@ -29,7 +29,7 @@ pub fn withdraw_assets( } AssetInfo::Cw20(addr) => { let withdraw_cw20_msg = - withdraw_asset_cw20(deps.as_ref(), env.clone(), &config.owner, addr)?; + withdraw_asset_cw20(deps, env.clone(), &config.owner, addr)?; match withdraw_cw20_msg { None => {} @@ -37,8 +37,7 @@ pub fn withdraw_assets( } } AssetInfo::Cw721(addr, token_id) => { - let withdraw_cw721_msg = - withdraw_asset_cw721(deps.as_ref(), &config.owner, addr, token_id)?; + let withdraw_cw721_msg = withdraw_asset_cw721(deps, &config.owner, addr, token_id)?; match withdraw_cw721_msg { None => {} Some(msg) => withdraw_msgs.push(msg), diff --git a/packages/job-account/src/lib.rs b/packages/job-account/src/lib.rs index 07048e05..50f3d6f7 100644 --- a/packages/job-account/src/lib.rs +++ b/packages/job-account/src/lib.rs @@ -28,6 +28,9 @@ pub struct InstantiateMsg { #[cw_serde] #[allow(clippy::large_enum_variant)] pub enum ExecuteMsg { + WarpMsgs(WarpMsgs), + + // legacy flow Generic(GenericMsg), WithdrawAssets(WithdrawAssetsMsg), IbcTransfer(IbcTransferMsg), @@ -38,6 +41,18 @@ pub struct GenericMsg { pub msgs: Vec, } +#[cw_serde] +pub struct WarpMsgs { + pub msgs: Vec, +} + +#[cw_serde] +pub enum WarpMsg { + Generic(CosmosMsg), + IbcTransfer(IbcTransferMsg), + WithdrawAssets(WithdrawAssetsMsg), +} + #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] pub struct Coin { #[prost(string, tag = "1")] From 1810a7f7e23c17bfeb7d44d372878a0e10c6000d Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 13 Nov 2023 17:15:47 +0100 Subject: [PATCH 074/133] add warp msg to hydration + fallback --- Cargo.lock | 1 + contracts/warp-resolver/Cargo.toml | 1 + contracts/warp-resolver/src/contract.rs | 7 ++++--- contracts/warp-resolver/src/tests.rs | 17 +++++++++-------- contracts/warp-resolver/src/util/variable.rs | 18 ++++++++++++++++-- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c74b3c3c..4200eb51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1130,6 +1130,7 @@ dependencies = [ "cw2 0.16.0", "cw20", "cw721", + "job-account", "json-codec-wasm", "resolver", "schemars", diff --git a/contracts/warp-resolver/Cargo.toml b/contracts/warp-resolver/Cargo.toml index 1009beb8..4a6479b7 100644 --- a/contracts/warp-resolver/Cargo.toml +++ b/contracts/warp-resolver/Cargo.toml @@ -42,6 +42,7 @@ cw721 = "0.16.0" cw-utils = "0.16" resolver = { path = "../../packages/resolver", default-features = false, version = "*" } controller = { path = "../../packages/controller", default-features = false, version = "*" } +job-account = { path = "../../packages/job-account", default-features = false, version = "*" } schemars = "0.8" thiserror = "1" serde-json-wasm = "0.4.1" diff --git a/contracts/warp-resolver/src/contract.rs b/contracts/warp-resolver/src/contract.rs index a3f32eee..f05e047b 100644 --- a/contracts/warp-resolver/src/contract.rs +++ b/contracts/warp-resolver/src/contract.rs @@ -5,11 +5,11 @@ use crate::util::variable::{ }; use crate::ContractError; use cosmwasm_std::{ - entry_point, to_binary, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, Response, StdError, - StdResult, + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, }; use cw_utils::nonpayable; +use job_account::WarpMsg; use resolver::condition::Condition; use resolver::variable::{QueryExpr, Variable}; use resolver::{ @@ -246,6 +246,7 @@ fn query_validate_job_creation( fn query_hydrate_vars(deps: Deps, env: Env, data: QueryHydrateVarsMsg) -> StdResult { let vars: Vec = serde_json_wasm::from_str(&data.vars).map_err(|e| StdError::generic_err(e.to_string()))?; + serde_json_wasm::to_string( &hydrate_vars( deps, @@ -285,7 +286,7 @@ fn query_hydrate_msgs( _deps: Deps, _env: Env, data: QueryHydrateMsgsMsg, -) -> StdResult> { +) -> StdResult> { let vars: Vec = serde_json_wasm::from_str(&data.vars).map_err(|e| StdError::generic_err(e.to_string()))?; diff --git a/contracts/warp-resolver/src/tests.rs b/contracts/warp-resolver/src/tests.rs index 3ef11a61..c1d0af5b 100644 --- a/contracts/warp-resolver/src/tests.rs +++ b/contracts/warp-resolver/src/tests.rs @@ -1,4 +1,5 @@ use controller::job::Execution; +use job_account::WarpMsg; use resolver::condition::{NumValue, StringEnvValue, StringValue}; use schemars::_serde_json::json; @@ -433,23 +434,23 @@ fn test_hydrate_static_nested_vars_and_hydrate_msgs() { assert_eq!( hydrated_msgs[0], - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { // Because var1.encode = false, contract_addr should use the plain text value contract_addr: "static_value_1".to_string(), msg: Binary::from(raw_str.as_bytes()), funds: vec![] - }) + })) ); assert_eq!( hydrated_msgs[1], - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { // Because var3.encode = true, contract_addr should use the encoded value contract_addr: encoded_val, // msg is not Binary::from(encoded_val.as_bytes()) appears to be a cosmos msg thing, not a warp thing msg: Binary::from(raw_str.as_bytes()), funds: vec![] - }) + })) ) } @@ -571,19 +572,19 @@ fn test_hydrate_static_env_vars_and_hydrate_msgs() { assert_eq!( hydrated_msgs[0], - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "static_value_1".to_string(), msg: Binary::from(raw_str.as_bytes()), funds: vec![] - }) + })) ); assert_eq!( hydrated_msgs[1], - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: dummy_warp_account_addr, msg: Binary::from(raw_str.as_bytes()), funds: vec![] - }) + })) ) } diff --git a/contracts/warp-resolver/src/util/variable.rs b/contracts/warp-resolver/src/util/variable.rs index 95960bd5..550a25c2 100644 --- a/contracts/warp-resolver/src/util/variable.rs +++ b/contracts/warp-resolver/src/util/variable.rs @@ -9,6 +9,7 @@ use cosmwasm_schema::serde::Serialize; use cosmwasm_std::{ Binary, CosmosMsg, Decimal256, Deps, Env, QueryRequest, Uint128, Uint256, WasmQuery, }; +use job_account::WarpMsg; use std::str::FromStr; use controller::job::{ExternalInput, JobStatus}; @@ -298,7 +299,7 @@ pub fn hydrate_vars( Ok(hydrated_vars) } -pub fn hydrate_msgs(msgs: String, vars: Vec) -> Result, ContractError> { +pub fn hydrate_msgs(msgs: String, vars: Vec) -> Result, ContractError> { let mut replaced_msgs = msgs; for var in &vars { let (name, replacement) = get_replacement_in_struct(var)?; @@ -311,7 +312,20 @@ pub fn hydrate_msgs(msgs: String, vars: Vec) -> Result, } } - Ok(serde_json_wasm::from_str::>(&replaced_msgs)?) + match serde_json_wasm::from_str::>(&replaced_msgs) { + Ok(msgs) => Ok(msgs), + + // fallback to legacy flow + Err(_) => { + let msgs = serde_json_wasm::from_str::>(&replaced_msgs) + .unwrap() + .into_iter() + .map(WarpMsg::Generic) + .collect(); + + Ok(msgs) + } + } } fn get_replacement_in_struct(var: &Variable) -> Result<(String, String), ContractError> { From 52e37484495861c7d9a319d42acfa4d3030fa6c3 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 13 Nov 2023 18:11:13 +0100 Subject: [PATCH 075/133] change account_msgs to WarpMsgs + refactor + migrations --- Cargo.lock | 2 +- contracts/warp-controller/src/contract.rs | 8 ++- contracts/warp-controller/src/execute/job.rs | 13 ++-- .../warp-controller/src/reply/account.rs | 11 +-- contracts/warp-controller/src/util/msg.rs | 23 +++++- contracts/warp-job-account/src/contract.rs | 21 +++--- contracts/warp-job-account/src/execute/ibc.rs | 2 +- .../warp-job-account/src/execute/msgs.rs | 27 ++++--- .../warp-job-account/src/execute/withdraw.rs | 4 +- contracts/warp-resolver/Cargo.toml | 1 - contracts/warp-resolver/src/contract.rs | 2 +- contracts/warp-resolver/src/tests.rs | 2 +- contracts/warp-resolver/src/util/variable.rs | 4 +- packages/controller/Cargo.toml | 1 + packages/controller/src/account.rs | 70 +++++++++++++++++- packages/controller/src/job.rs | 6 +- packages/controller/src/lib.rs | 4 +- packages/job-account/src/lib.rs | 72 +------------------ packages/resolver/src/lib.rs | 9 ++- refs.json | 6 +- 20 files changed, 166 insertions(+), 122 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4200eb51..59ab554d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "prost 0.11.9", "schemars", "serde", "strum", @@ -1130,7 +1131,6 @@ dependencies = [ "cw2 0.16.0", "cw20", "cw721", - "job-account", "json-codec-wasm", "resolver", "schemars", diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 0005dd96..e2c16c1e 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -170,7 +170,13 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { +pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + config.warp_account_code_id = msg.warp_account_code_id; + + CONFIG.save(deps.storage, &config)?; + Ok(Response::new()) } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 73491524..a529226b 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,5 +1,7 @@ use crate::state::{JobQueue, STATE}; +use crate::util::msg::build_account_execute_warp_msgs; use crate::ContractError; +use controller::account::WarpMsgs; use controller::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Execution, Job, JobStatus, UpdateJobMsg, }; @@ -14,15 +16,14 @@ use crate::{ fee::deduct_reward_and_fee_from_native_funds, legacy_account::is_legacy_account, msg::{ - build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_free_account_msg, build_instantiate_warp_account_msg, build_taken_account_msg, - build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, + build_account_withdraw_assets_msg, build_free_account_msg, + build_instantiate_warp_account_msg, build_taken_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, build_transfer_native_funds_msg, }, }, }; use controller::{account::CwFund, Config}; -use job_account::GenericMsg; use job_account_tracker::FirstFreeAccountResponse; use resolver::QueryHydrateMsgsMsg; @@ -192,7 +193,7 @@ pub fn create_job( if let Some(account_msgs) = data.account_msgs { // Account execute msgs - msgs.push(build_account_execute_generic_msgs( + msgs.push(build_account_execute_warp_msgs( available_account_addr.to_string(), account_msgs, )); @@ -388,7 +389,7 @@ pub fn execute_job( id: data.id.u64(), msg: CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job.account.to_string(), - msg: to_binary(&job_account::ExecuteMsg::Generic(GenericMsg { + msg: to_binary(&job_account::ExecuteMsg::WarpMsgs(WarpMsgs { msgs: deps.querier.query_wasm_smart( config.resolver_address, &resolver::QueryMsg::QueryHydrateMsgs(QueryHydrateMsgsMsg { diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index d5856db2..8b671ead 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -1,11 +1,14 @@ use cosmwasm_std::{Coin, CosmosMsg, DepsMut, Env, Reply, Response, StdError}; -use controller::{account::CwFund, Config}; +use controller::{ + account::{CwFund, WarpMsg}, + Config, +}; use crate::{ state::JobQueue, util::msg::{ - build_account_execute_generic_msgs, build_taken_account_msg, build_transfer_cw20_msg, + build_account_execute_warp_msgs, build_taken_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, }, ContractError, @@ -77,7 +80,7 @@ pub fn create_job_account_and_job( .value, )?; - let account_msgs: Option> = serde_json_wasm::from_str( + let account_msgs: Option> = serde_json_wasm::from_str( &event .attributes .iter() @@ -126,7 +129,7 @@ pub fn create_job_account_and_job( if let Some(account_msgs) = account_msgs { // Account execute msgs - msgs.push(build_account_execute_generic_msgs( + msgs.push(build_account_execute_warp_msgs( job_account_addr.to_string(), account_msgs, )); diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 2a1d05ad..214d7596 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -1,7 +1,10 @@ use cosmwasm_std::{to_binary, BankMsg, Coin, CosmosMsg, Uint128, Uint64, WasmMsg}; -use controller::account::{AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg}; -use job_account::{GenericMsg, WithdrawAssetsMsg}; +use controller::account::{ + AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, WarpMsg, WarpMsgs, + WithdrawAssetsMsg, +}; +use job_account::GenericMsg; use job_account_tracker::{FreeAccountMsg, TakeAccountMsg}; #[allow(clippy::too_many_arguments)] @@ -12,7 +15,7 @@ pub fn build_instantiate_warp_account_msg( account_owner: String, native_funds: Vec, cw_funds: Option>, - msgs: Option>, + msgs: Option>, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(admin_addr), @@ -128,6 +131,20 @@ pub fn build_account_execute_generic_msgs( }) } +pub fn build_account_execute_warp_msgs( + account_addr: String, + warp_msgs_for_account_to_execute: Vec, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: account_addr, + msg: to_binary(&job_account::ExecuteMsg::WarpMsgs(WarpMsgs { + msgs: warp_msgs_for_account_to_execute, + })) + .unwrap(), + funds: vec![], + }) +} + pub fn build_account_withdraw_assets_msg( account_addr: String, assets_to_withdraw: Vec, diff --git a/contracts/warp-job-account/src/contract.rs b/contracts/warp-job-account/src/contract.rs index 95d65efb..47c5f1e2 100644 --- a/contracts/warp-job-account/src/contract.rs +++ b/contracts/warp-job-account/src/contract.rs @@ -1,3 +1,4 @@ +use crate::execute::msgs::warp_msgs_to_cosmos_msgs; use crate::state::CONFIG; use crate::{execute, query, ContractError}; use cosmwasm_std::{ @@ -13,18 +14,18 @@ pub fn instantiate( info: MessageInfo, msg: InstantiateMsg, ) -> Result { - let instantiated_account_addr = env.contract.address; + let instantiated_account_addr = env.contract.address.clone(); + let config = Config { + owner: deps.api.addr_validate(&msg.owner)?, + creator_addr: info.sender, + }; - CONFIG.save( - deps.storage, - &Config { - owner: deps.api.addr_validate(&msg.owner)?, - creator_addr: info.sender, - }, - )?; + CONFIG.save(deps.storage, &config)?; + + let msgs = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, msg.msgs, config).unwrap(); Ok(Response::new() - .add_messages(msg.msgs.clone()) + .add_messages(msgs.clone()) .add_attribute("action", "instantiate") .add_attribute("job_id", msg.job_id) .add_attribute("contract_addr", instantiated_account_addr) @@ -34,7 +35,7 @@ pub fn instantiate( serde_json_wasm::to_string(&msg.native_funds)?, ) .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.cw_funds)?) - .add_attribute("account_msgs", serde_json_wasm::to_string(&msg.msgs)?)) + .add_attribute("account_msgs", serde_json_wasm::to_string(&msgs)?)) } #[cfg_attr(not(feature = "library"), entry_point)] diff --git a/contracts/warp-job-account/src/execute/ibc.rs b/contracts/warp-job-account/src/execute/ibc.rs index 7c7023af..f114b24e 100644 --- a/contracts/warp-job-account/src/execute/ibc.rs +++ b/contracts/warp-job-account/src/execute/ibc.rs @@ -1,7 +1,7 @@ use crate::ContractError; +use controller::account::{IbcTransferMsg, TimeoutBlock}; use cosmwasm_std::CosmosMsg::Stargate; use cosmwasm_std::{Env, Response}; -use job_account::{IbcTransferMsg, TimeoutBlock}; use prost::Message; pub fn ibc_transfer(env: Env, data: IbcTransferMsg) -> Result { diff --git a/contracts/warp-job-account/src/execute/msgs.rs b/contracts/warp-job-account/src/execute/msgs.rs index 78732ead..3b0cf97a 100644 --- a/contracts/warp-job-account/src/execute/msgs.rs +++ b/contracts/warp-job-account/src/execute/msgs.rs @@ -1,6 +1,7 @@ use crate::ContractError; -use cosmwasm_std::{Env, Response}; -use job_account::{Config, WarpMsg, WarpMsgs}; +use controller::account::{WarpMsg, WarpMsgs}; +use cosmwasm_std::{Deps, Env, Response}; +use job_account::Config; use cosmwasm_std::{CosmosMsg, DepsMut}; @@ -13,8 +14,20 @@ pub fn execute_warp_msgs( data: WarpMsgs, config: Config, ) -> Result { - let msgs = data - .msgs + let msgs = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, data.msgs, config).unwrap(); + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "warp_msgs")) +} + +pub fn warp_msgs_to_cosmos_msgs( + deps: Deps, + env: Env, + msgs: Vec, + config: Config, +) -> Result, ContractError> { + let result = msgs .into_iter() .flat_map(|msg| -> Vec { match msg { @@ -23,7 +36,7 @@ pub fn execute_warp_msgs( .map(extract_messages) .unwrap(), WarpMsg::WithdrawAssets(msg) => { - withdraw_assets(deps.as_ref(), env.clone(), msg, config.clone()) + withdraw_assets(deps, env.clone(), msg, config.clone()) .map(extract_messages) .unwrap() } @@ -31,9 +44,7 @@ pub fn execute_warp_msgs( }) .collect::>(); - Ok(Response::new() - .add_messages(msgs) - .add_attribute("action", "warp_msgs")) + Ok(result) } fn extract_messages(resp: Response) -> Vec { diff --git a/contracts/warp-job-account/src/execute/withdraw.rs b/contracts/warp-job-account/src/execute/withdraw.rs index efbdde76..8e13e3d7 100644 --- a/contracts/warp-job-account/src/execute/withdraw.rs +++ b/contracts/warp-job-account/src/execute/withdraw.rs @@ -5,8 +5,8 @@ use cw20::{BalanceResponse, Cw20ExecuteMsg}; use cw721::{Cw721QueryMsg, OwnerOfResponse}; use crate::ContractError; -use controller::account::{AssetInfo, Cw721ExecuteMsg}; -use job_account::{Config, WithdrawAssetsMsg}; +use controller::account::{AssetInfo, Cw721ExecuteMsg, WithdrawAssetsMsg}; +use job_account::Config; pub fn withdraw_assets( deps: Deps, diff --git a/contracts/warp-resolver/Cargo.toml b/contracts/warp-resolver/Cargo.toml index 4a6479b7..1009beb8 100644 --- a/contracts/warp-resolver/Cargo.toml +++ b/contracts/warp-resolver/Cargo.toml @@ -42,7 +42,6 @@ cw721 = "0.16.0" cw-utils = "0.16" resolver = { path = "../../packages/resolver", default-features = false, version = "*" } controller = { path = "../../packages/controller", default-features = false, version = "*" } -job-account = { path = "../../packages/job-account", default-features = false, version = "*" } schemars = "0.8" thiserror = "1" serde-json-wasm = "0.4.1" diff --git a/contracts/warp-resolver/src/contract.rs b/contracts/warp-resolver/src/contract.rs index f05e047b..5a22d42d 100644 --- a/contracts/warp-resolver/src/contract.rs +++ b/contracts/warp-resolver/src/contract.rs @@ -4,12 +4,12 @@ use crate::util::variable::{ vars_valid, }; use crate::ContractError; +use controller::account::WarpMsg; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, }; use cw_utils::nonpayable; -use job_account::WarpMsg; use resolver::condition::Condition; use resolver::variable::{QueryExpr, Variable}; use resolver::{ diff --git a/contracts/warp-resolver/src/tests.rs b/contracts/warp-resolver/src/tests.rs index c1d0af5b..b64fd339 100644 --- a/contracts/warp-resolver/src/tests.rs +++ b/contracts/warp-resolver/src/tests.rs @@ -1,5 +1,5 @@ +use controller::account::WarpMsg; use controller::job::Execution; -use job_account::WarpMsg; use resolver::condition::{NumValue, StringEnvValue, StringValue}; use schemars::_serde_json::json; diff --git a/contracts/warp-resolver/src/util/variable.rs b/contracts/warp-resolver/src/util/variable.rs index 550a25c2..bf6dfbaf 100644 --- a/contracts/warp-resolver/src/util/variable.rs +++ b/contracts/warp-resolver/src/util/variable.rs @@ -4,12 +4,12 @@ use crate::util::condition::{ resolve_query_expr_string, resolve_query_expr_uint, resolve_ref_bool, }; use crate::ContractError; +use controller::account::WarpMsg; use cosmwasm_schema::serde::de::DeserializeOwned; use cosmwasm_schema::serde::Serialize; use cosmwasm_std::{ Binary, CosmosMsg, Decimal256, Deps, Env, QueryRequest, Uint128, Uint256, WasmQuery, }; -use job_account::WarpMsg; use std::str::FromStr; use controller::job::{ExternalInput, JobStatus}; @@ -763,7 +763,7 @@ pub fn msgs_valid(msgs: &str, vars: &Vec) -> Result>(&replaced_msgs)?; + let _msgs = serde_json_wasm::from_str::>(&replaced_msgs)?; Ok(true) } diff --git a/packages/controller/Cargo.toml b/packages/controller/Cargo.toml index 14556755..3d3c835f 100644 --- a/packages/controller/Cargo.toml +++ b/packages/controller/Cargo.toml @@ -15,6 +15,7 @@ schemars = "0.8" serde = { version = "1", default-features = false, features = ["derive"] } strum = "0.24" strum_macros = "0.24" +prost = "0.11.9" [dev-dependencies] cw-multi-test = "0.16" diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index 87488adc..9d11b229 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -1,5 +1,7 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::{Addr, CosmosMsg, Uint128}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; #[cw_serde] pub enum CwFund { @@ -76,3 +78,69 @@ pub enum AssetInfo { Cw20(Addr), Cw721(Addr, String), } + +#[cw_serde] +pub struct WarpMsgs { + pub msgs: Vec, +} + +#[cw_serde] +pub enum WarpMsg { + Generic(CosmosMsg), + IbcTransfer(IbcTransferMsg), + WithdrawAssets(WithdrawAssetsMsg), +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] +pub struct Coin { + #[prost(string, tag = "1")] + pub denom: String, + #[prost(string, tag = "2")] + pub amount: String, +} + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] +pub struct TimeoutBlock { + #[prost(uint64, optional, tag = "1")] + pub revision_number: Option, + #[prost(uint64, optional, tag = "2")] + pub revision_height: Option, +} +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] +pub struct TransferMsg { + #[prost(string, tag = "1")] + pub source_port: String, + + #[prost(string, tag = "2")] + pub source_channel: String, + + #[prost(message, optional, tag = "3")] + pub token: Option, + + #[prost(string, tag = "4")] + pub sender: String, + + #[prost(string, tag = "5")] + pub receiver: String, + + #[prost(message, optional, tag = "6")] + pub timeout_block: Option, + + #[prost(uint64, optional, tag = "7")] + pub timeout_timestamp: Option, + + #[prost(string, tag = "8")] + pub memo: String, +} + +#[cw_serde] +pub struct IbcTransferMsg { + pub transfer_msg: TransferMsg, + pub timeout_block_delta: Option, + pub timeout_timestamp_seconds_delta: Option, +} + +#[cw_serde] +pub struct WithdrawAssetsMsg { + pub asset_infos: Vec, +} diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 2608a81c..ba36b2f0 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -1,6 +1,6 @@ -use crate::account::{AssetInfo, CwFund}; +use crate::account::{AssetInfo, CwFund, WarpMsg}; use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, CosmosMsg, Uint128, Uint64}; +use cosmwasm_std::{Addr, Uint128, Uint64}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use strum_macros::Display; @@ -65,7 +65,7 @@ pub struct CreateJobMsg { pub reward: Uint128, pub duration_days: Uint64, pub assets_to_withdraw: Option>, - pub account_msgs: Option>, + pub account_msgs: Option>, pub cw_funds: Option>, } diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 1b5ee7cc..29f65654 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -194,4 +194,6 @@ pub struct StateResponse { } #[cw_serde] -pub struct MigrateMsg {} +pub struct MigrateMsg { + pub warp_account_code_id: Uint64, +} diff --git a/packages/job-account/src/lib.rs b/packages/job-account/src/lib.rs index 50f3d6f7..620e80f8 100644 --- a/packages/job-account/src/lib.rs +++ b/packages/job-account/src/lib.rs @@ -1,8 +1,6 @@ -use controller::account::{AssetInfo, CwFund}; +use controller::account::{CwFund, IbcTransferMsg, WarpMsg, WarpMsgs, WithdrawAssetsMsg}; use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Coin as NativeCoin, CosmosMsg, Uint64}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; #[cw_serde] pub struct Config { @@ -22,7 +20,7 @@ pub struct InstantiateMsg { // CW20 or CW721 funds, will be transferred to account in reply of account instantiation pub cw_funds: Vec, // List of cosmos msgs to execute after instantiating the account - pub msgs: Vec, + pub msgs: Vec, } #[cw_serde] @@ -41,72 +39,6 @@ pub struct GenericMsg { pub msgs: Vec, } -#[cw_serde] -pub struct WarpMsgs { - pub msgs: Vec, -} - -#[cw_serde] -pub enum WarpMsg { - Generic(CosmosMsg), - IbcTransfer(IbcTransferMsg), - WithdrawAssets(WithdrawAssetsMsg), -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] -pub struct Coin { - #[prost(string, tag = "1")] - pub denom: String, - #[prost(string, tag = "2")] - pub amount: String, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] -pub struct TimeoutBlock { - #[prost(uint64, optional, tag = "1")] - pub revision_number: Option, - #[prost(uint64, optional, tag = "2")] - pub revision_height: Option, -} -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] -pub struct TransferMsg { - #[prost(string, tag = "1")] - pub source_port: String, - - #[prost(string, tag = "2")] - pub source_channel: String, - - #[prost(message, optional, tag = "3")] - pub token: Option, - - #[prost(string, tag = "4")] - pub sender: String, - - #[prost(string, tag = "5")] - pub receiver: String, - - #[prost(message, optional, tag = "6")] - pub timeout_block: Option, - - #[prost(uint64, optional, tag = "7")] - pub timeout_timestamp: Option, - - #[prost(string, tag = "8")] - pub memo: String, -} - -#[cw_serde] -pub struct IbcTransferMsg { - pub transfer_msg: TransferMsg, - pub timeout_block_delta: Option, - pub timeout_timestamp_seconds_delta: Option, -} - -#[cw_serde] -pub struct WithdrawAssetsMsg { - pub asset_infos: Vec, -} - #[cw_serde] pub struct ExecuteWasmMsg {} diff --git a/packages/resolver/src/lib.rs b/packages/resolver/src/lib.rs index 6ddfd23c..ad5abe6d 100644 --- a/packages/resolver/src/lib.rs +++ b/packages/resolver/src/lib.rs @@ -1,9 +1,12 @@ pub mod condition; pub mod variable; -use controller::job::{Execution, ExternalInput, JobStatus}; +use controller::{ + account::WarpMsg, + job::{Execution, ExternalInput, JobStatus}, +}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{CosmosMsg, QueryRequest}; +use cosmwasm_std::QueryRequest; #[cw_serde] pub struct InstantiateMsg {} @@ -30,7 +33,7 @@ pub enum QueryMsg { QueryResolveCondition(QueryResolveConditionMsg), #[returns(String)] QueryApplyVarFn(QueryApplyVarFnMsg), - #[returns(Vec)] + #[returns(Vec)] QueryHydrateMsgs(QueryHydrateMsgsMsg), } diff --git a/refs.json b/refs.json index 5de98d19..9c273795 100644 --- a/refs.json +++ b/refs.json @@ -12,11 +12,11 @@ "codeId": "9626" }, "warp-controller": { - "codeId": "11634", + "codeId": "11718", "address": "terra1fqcfh8vpqsl7l5yjjtq5wwu6sv989txncq5fa756tv7lywqexraq5vnjvt" }, "warp-resolver": { - "codeId": "11521", + "codeId": "11717", "address": "terra1lxfx6n792aw3hg47tchmyuhv5t30f334gus67pc250qx5zljadws65elnf" }, "warp-templates": { @@ -24,7 +24,7 @@ "address": "terra17xm2ewyg60y7eypnwav33fwm23hxs3qyd8qk9tnntj4d0rp2vvhsgkpwwp" }, "warp-job-account": { - "codeId": "11522" + "codeId": "11716" }, "warp-job-account-tracker": { "codeId": "11630", From a0d23647c9b2b0b4be9b41c9a6d57e56b03f2a78 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 13 Nov 2023 18:49:43 +0100 Subject: [PATCH 076/133] schema --- contracts/warp-resolver/examples/warp-resolver-schema.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/warp-resolver/examples/warp-resolver-schema.rs b/contracts/warp-resolver/examples/warp-resolver-schema.rs index 443622f3..3abd5e69 100644 --- a/contracts/warp-resolver/examples/warp-resolver-schema.rs +++ b/contracts/warp-resolver/examples/warp-resolver-schema.rs @@ -1,6 +1,7 @@ use std::env::current_dir; use std::fs::create_dir_all; +use controller::account::WarpMsg; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use cosmwasm_std::{CosmosMsg, QueryRequest}; use resolver::{ @@ -22,4 +23,5 @@ fn main() { export_schema(&schema_for!(SimulateResponse), &out_dir); export_schema(&schema_for!(CosmosMsg), &out_dir); export_schema(&schema_for!(QueryRequest), &out_dir); + export_schema(&schema_for!(WarpMsg), &out_dir); } From 6b320971a02f610f099203d87dc3ee10e0daa572 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 13 Nov 2023 18:57:25 +0100 Subject: [PATCH 077/133] fix failing test --- contracts/warp-resolver/src/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/warp-resolver/src/tests.rs b/contracts/warp-resolver/src/tests.rs index b64fd339..a1c9f1fa 100644 --- a/contracts/warp-resolver/src/tests.rs +++ b/contracts/warp-resolver/src/tests.rs @@ -34,12 +34,12 @@ fn test() { let msg = QueryValidateJobCreationMsg { executions: vec![Execution { condition: "{\"expr\":{\"decimal\":{\"op\":\"gte\",\"left\":{\"ref\":\"$warp.variable.return_amount\"},\"right\":{\"simple\":\"620000\"}}}}".parse().unwrap(), - msgs: "[{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}]".to_string(), + msgs: "[{\"generic\":{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}}]".to_string(), }], terminate_condition: None, vars: "[{\"query\":{\"kind\":\"decimal\",\"name\":\"return_amount\",\"init_fn\":{\"query\":{\"wasm\":{\"smart\":{\"msg\":\"eyJzaW11bGF0aW9uIjp7Im9mZmVyX2Fzc2V0Ijp7ImFtb3VudCI6IjEwMDAwMDAiLCJpbmZvIjp7Im5hdGl2ZV90b2tlbiI6eyJkZW5vbSI6ImliYy9CMzUwNEUwOTI0NTZCQTYxOENDMjhBQzY3MUE3MUZCMDhDNkNBMEZEMEJFN0M4QTVCNUEzRTJERDkzM0NDOUU0In19fX19\",\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\"}}},\"selector\":\"$.return_amount\"},\"reinitialize\":false,\"encode\":false}}]".to_string(), }; - let obj = serde_json_wasm::to_string(&vec!["{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}"]).unwrap(); + let obj = serde_json_wasm::to_string(&vec!["{\"generic\":{\"wasm\":{\"execute\":{\"contract_addr\":\"terra1fd68ah02gr2y8ze7tm9te7m70zlmc7vjyyhs6xlhsdmqqcjud4dql4wpxr\",\"msg\":\"eyJzd2FwIjp7Im9mZmVyX2Fzc2V0Ijp7ImluZm8iOnsibmF0aXZlX3Rva2VuIjp7ImRlbm9tIjoiaWJjL0IzNTA0RTA5MjQ1NkJBNjE4Q0MyOEFDNjcxQTcxRkIwOEM2Q0EwRkQwQkU3QzhBNUI1QTNFMkREOTMzQ0M5RTQifX0sImFtb3VudCI6IjEwMDAwMDAifSwibWF4X3NwcmVhZCI6IjAuNSIsImJlbGllZl9wcmljZSI6IjAuNjEwMzg3MzI3MzgyNDYzODE2In19\",\"funds\":[{\"denom\":\"ibc/B3504E092456BA618CC28AC671A71FB08C6CA0FD0BE7C8A5B5A3E2DD933CC9E4\",\"amount\":\"1000000\"}]}}}}"]).unwrap(); let _msg1 = QueryValidateJobCreationMsg { terminate_condition: None, From ebbe982864463973b461059442c632f33d1ca606 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 16 Nov 2023 19:00:24 +0100 Subject: [PATCH 078/133] extract warp_msgs into packages/controller/accounts as shared util --- contracts/warp-job-account/src/contract.rs | 16 +- contracts/warp-job-account/src/execute/ibc.rs | 33 --- contracts/warp-job-account/src/execute/mod.rs | 3 - .../warp-job-account/src/execute/msgs.rs | 55 ----- .../warp-job-account/src/execute/withdraw.rs | 132 ------------ contracts/warp-job-account/src/lib.rs | 1 - packages/controller/Cargo.toml | 3 + packages/controller/src/account.rs | 204 +++++++++++++++++- 8 files changed, 216 insertions(+), 231 deletions(-) delete mode 100644 contracts/warp-job-account/src/execute/ibc.rs delete mode 100644 contracts/warp-job-account/src/execute/mod.rs delete mode 100644 contracts/warp-job-account/src/execute/msgs.rs delete mode 100644 contracts/warp-job-account/src/execute/withdraw.rs diff --git a/contracts/warp-job-account/src/contract.rs b/contracts/warp-job-account/src/contract.rs index 47c5f1e2..ad429fcf 100644 --- a/contracts/warp-job-account/src/contract.rs +++ b/contracts/warp-job-account/src/contract.rs @@ -1,6 +1,8 @@ -use crate::execute::msgs::warp_msgs_to_cosmos_msgs; use crate::state::CONFIG; -use crate::{execute, query, ContractError}; +use crate::{query, ContractError}; +use controller::account::{ + execute_warp_msgs, ibc_transfer, warp_msgs_to_cosmos_msgs, withdraw_assets, +}; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; @@ -22,7 +24,7 @@ pub fn instantiate( CONFIG.save(deps.storage, &config)?; - let msgs = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, msg.msgs, config).unwrap(); + let msgs = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, msg.msgs, &config.owner).unwrap(); Ok(Response::new() .add_messages(msgs.clone()) @@ -55,10 +57,12 @@ pub fn execute( .add_attribute("action", "generic")), ExecuteMsg::WithdrawAssets(data) => { nonpayable(&info).unwrap(); - execute::withdraw::withdraw_assets(deps.as_ref(), env, data, config) + withdraw_assets(deps.as_ref(), env, data, &config.owner).map_err(ContractError::Std) + } + ExecuteMsg::IbcTransfer(data) => ibc_transfer(env, data).map_err(ContractError::Std), + ExecuteMsg::WarpMsgs(data) => { + execute_warp_msgs(deps, env, data, &config.owner).map_err(ContractError::Std) } - ExecuteMsg::IbcTransfer(data) => execute::ibc::ibc_transfer(env, data), - ExecuteMsg::WarpMsgs(data) => execute::msgs::execute_warp_msgs(deps, env, data, config), } } diff --git a/contracts/warp-job-account/src/execute/ibc.rs b/contracts/warp-job-account/src/execute/ibc.rs deleted file mode 100644 index f114b24e..00000000 --- a/contracts/warp-job-account/src/execute/ibc.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::ContractError; -use controller::account::{IbcTransferMsg, TimeoutBlock}; -use cosmwasm_std::CosmosMsg::Stargate; -use cosmwasm_std::{Env, Response}; -use prost::Message; - -pub fn ibc_transfer(env: Env, data: IbcTransferMsg) -> Result { - let mut transfer_msg = data.transfer_msg.clone(); - - if data.timeout_block_delta.is_some() && data.transfer_msg.timeout_block.is_some() { - let block = transfer_msg.timeout_block.unwrap(); - transfer_msg.timeout_block = Some(TimeoutBlock { - revision_number: Some(block.revision_number()), - revision_height: Some(env.block.height + data.timeout_block_delta.unwrap()), - }) - } - - if data.timeout_timestamp_seconds_delta.is_some() { - transfer_msg.timeout_timestamp = Some( - env.block - .time - .plus_seconds( - env.block.time.seconds() + data.timeout_timestamp_seconds_delta.unwrap(), - ) - .nanos(), - ); - } - - Ok(Response::new().add_message(Stargate { - type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - value: transfer_msg.encode_to_vec().into(), - })) -} diff --git a/contracts/warp-job-account/src/execute/mod.rs b/contracts/warp-job-account/src/execute/mod.rs deleted file mode 100644 index beccf81c..00000000 --- a/contracts/warp-job-account/src/execute/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod ibc; -pub(crate) mod msgs; -pub(crate) mod withdraw; diff --git a/contracts/warp-job-account/src/execute/msgs.rs b/contracts/warp-job-account/src/execute/msgs.rs deleted file mode 100644 index 3b0cf97a..00000000 --- a/contracts/warp-job-account/src/execute/msgs.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::ContractError; -use controller::account::{WarpMsg, WarpMsgs}; -use cosmwasm_std::{Deps, Env, Response}; -use job_account::Config; - -use cosmwasm_std::{CosmosMsg, DepsMut}; - -use super::ibc::ibc_transfer; -use super::withdraw::withdraw_assets; - -pub fn execute_warp_msgs( - deps: DepsMut, - env: Env, - data: WarpMsgs, - config: Config, -) -> Result { - let msgs = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, data.msgs, config).unwrap(); - - Ok(Response::new() - .add_messages(msgs) - .add_attribute("action", "warp_msgs")) -} - -pub fn warp_msgs_to_cosmos_msgs( - deps: Deps, - env: Env, - msgs: Vec, - config: Config, -) -> Result, ContractError> { - let result = msgs - .into_iter() - .flat_map(|msg| -> Vec { - match msg { - WarpMsg::Generic(msg) => vec![msg], - WarpMsg::IbcTransfer(msg) => ibc_transfer(env.clone(), msg) - .map(extract_messages) - .unwrap(), - WarpMsg::WithdrawAssets(msg) => { - withdraw_assets(deps, env.clone(), msg, config.clone()) - .map(extract_messages) - .unwrap() - } - } - }) - .collect::>(); - - Ok(result) -} - -fn extract_messages(resp: Response) -> Vec { - resp.messages - .into_iter() - .map(|cosmos_msg| cosmos_msg.msg) - .collect() -} diff --git a/contracts/warp-job-account/src/execute/withdraw.rs b/contracts/warp-job-account/src/execute/withdraw.rs deleted file mode 100644 index 8e13e3d7..00000000 --- a/contracts/warp-job-account/src/execute/withdraw.rs +++ /dev/null @@ -1,132 +0,0 @@ -use cosmwasm_std::{ - to_binary, Addr, BankMsg, CosmosMsg, Deps, Env, Response, StdResult, Uint128, WasmMsg, -}; -use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use cw721::{Cw721QueryMsg, OwnerOfResponse}; - -use crate::ContractError; -use controller::account::{AssetInfo, Cw721ExecuteMsg, WithdrawAssetsMsg}; -use job_account::Config; - -pub fn withdraw_assets( - deps: Deps, - env: Env, - data: WithdrawAssetsMsg, - config: Config, -) -> Result { - let mut withdraw_msgs: Vec = vec![]; - - for asset_info in &data.asset_infos { - match asset_info { - AssetInfo::Native(denom) => { - let withdraw_native_msg = - withdraw_asset_native(deps, env.clone(), &config.owner, denom)?; - - match withdraw_native_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw20(addr) => { - let withdraw_cw20_msg = - withdraw_asset_cw20(deps, env.clone(), &config.owner, addr)?; - - match withdraw_cw20_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw721(addr, token_id) => { - let withdraw_cw721_msg = withdraw_asset_cw721(deps, &config.owner, addr, token_id)?; - match withdraw_cw721_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - } - } - - Ok(Response::new() - .add_messages(withdraw_msgs) - .add_attribute("action", "withdraw_assets") - .add_attribute("assets", serde_json_wasm::to_string(&data.asset_infos)?)) -} - -fn withdraw_asset_native( - deps: Deps, - env: Env, - owner: &Addr, - denom: &String, -) -> StdResult> { - let amount = deps.querier.query_balance(env.contract.address, denom)?; - - let res = if amount.amount > Uint128::zero() { - Some(CosmosMsg::Bank(BankMsg::Send { - to_address: owner.to_string(), - amount: vec![amount], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw20( - deps: Deps, - env: Env, - owner: &Addr, - token: &Addr, -) -> StdResult> { - let amount: BalanceResponse = deps.querier.query_wasm_smart( - token.to_string(), - &cw20::Cw20QueryMsg::Balance { - address: env.contract.address.to_string(), - }, - )?; - - let res = if amount.balance > Uint128::zero() { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: owner.to_string(), - amount: amount.balance, - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw721( - deps: Deps, - owner: &Addr, - token: &Addr, - token_id: &String, -) -> StdResult> { - let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( - token.to_string(), - &Cw721QueryMsg::OwnerOf { - token_id: token_id.to_string(), - include_expired: None, - }, - )?; - - let res = if owner_query.owner == *owner { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw721ExecuteMsg::TransferNft { - recipient: owner.to_string(), - token_id: token_id.to_string(), - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} diff --git a/contracts/warp-job-account/src/lib.rs b/contracts/warp-job-account/src/lib.rs index aff04ae7..dfce1919 100644 --- a/contracts/warp-job-account/src/lib.rs +++ b/contracts/warp-job-account/src/lib.rs @@ -1,6 +1,5 @@ pub mod contract; mod error; -mod execute; mod query; pub mod state; diff --git a/packages/controller/Cargo.toml b/packages/controller/Cargo.toml index 3d3c835f..c41b0f7b 100644 --- a/packages/controller/Cargo.toml +++ b/packages/controller/Cargo.toml @@ -16,6 +16,9 @@ serde = { version = "1", default-features = false, features = ["derive"] } strum = "0.24" strum_macros = "0.24" prost = "0.11.9" +cw20 = "0.16" +cw721 = "0.16.0" +serde-json-wasm = "0.4.1" [dev-dependencies] cw-multi-test = "0.16" diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index 9d11b229..376b99e6 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -1,8 +1,14 @@ use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, CosmosMsg, Uint128}; +use cosmwasm_std::CosmosMsg::Stargate; +use cosmwasm_std::{to_binary, BankMsg, DepsMut, WasmMsg}; +use cosmwasm_std::{Addr, CosmosMsg, Deps, Env, Response, StdError, StdResult, Uint128}; +use cw20::{BalanceResponse, Cw20ExecuteMsg}; +use cw721::{Cw721QueryMsg, OwnerOfResponse}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use prost::Message; + #[cw_serde] pub enum CwFund { Cw20(Cw20Fund), @@ -144,3 +150,199 @@ pub struct IbcTransferMsg { pub struct WithdrawAssetsMsg { pub asset_infos: Vec, } + +pub fn execute_warp_msgs( + deps: DepsMut, + env: Env, + data: WarpMsgs, + owner: &Addr, +) -> Result { + let msgs = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, data.msgs, owner).unwrap(); + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "warp_msgs")) +} + +pub fn warp_msgs_to_cosmos_msgs( + deps: Deps, + env: Env, + msgs: Vec, + owner: &Addr, +) -> Result, StdError> { + let result = msgs + .into_iter() + .flat_map(|msg| -> Vec { + match msg { + WarpMsg::Generic(msg) => vec![msg], + WarpMsg::IbcTransfer(msg) => ibc_transfer(env.clone(), msg) + .map(extract_messages) + .unwrap(), + WarpMsg::WithdrawAssets(msg) => withdraw_assets(deps, env.clone(), msg, owner) + .map(extract_messages) + .unwrap(), + } + }) + .collect::>(); + + Ok(result) +} + +fn extract_messages(resp: Response) -> Vec { + resp.messages + .into_iter() + .map(|cosmos_msg| cosmos_msg.msg) + .collect() +} + +pub fn withdraw_assets( + deps: Deps, + env: Env, + data: WithdrawAssetsMsg, + owner: &Addr, +) -> Result { + let mut withdraw_msgs: Vec = vec![]; + + for asset_info in &data.asset_infos { + match asset_info { + AssetInfo::Native(denom) => { + let withdraw_native_msg = withdraw_asset_native(deps, env.clone(), owner, denom)?; + + match withdraw_native_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw20(addr) => { + let withdraw_cw20_msg = withdraw_asset_cw20(deps, env.clone(), owner, addr)?; + + match withdraw_cw20_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + AssetInfo::Cw721(addr, token_id) => { + let withdraw_cw721_msg = withdraw_asset_cw721(deps, owner, addr, token_id)?; + match withdraw_cw721_msg { + None => {} + Some(msg) => withdraw_msgs.push(msg), + } + } + } + } + + Ok(Response::new() + .add_messages(withdraw_msgs) + .add_attribute("action", "withdraw_assets") + .add_attribute( + "assets", + serde_json_wasm::to_string(&data.asset_infos).unwrap(), + )) +} + +fn withdraw_asset_native( + deps: Deps, + env: Env, + owner: &Addr, + denom: &String, +) -> StdResult> { + let amount = deps.querier.query_balance(env.contract.address, denom)?; + + let res = if amount.amount > Uint128::zero() { + Some(CosmosMsg::Bank(BankMsg::Send { + to_address: owner.to_string(), + amount: vec![amount], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw20( + deps: Deps, + env: Env, + owner: &Addr, + token: &Addr, +) -> StdResult> { + let amount: BalanceResponse = deps.querier.query_wasm_smart( + token.to_string(), + &cw20::Cw20QueryMsg::Balance { + address: env.contract.address.to_string(), + }, + )?; + + let res = if amount.balance > Uint128::zero() { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw20ExecuteMsg::Transfer { + recipient: owner.to_string(), + amount: amount.balance, + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} + +fn withdraw_asset_cw721( + deps: Deps, + owner: &Addr, + token: &Addr, + token_id: &String, +) -> StdResult> { + let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( + token.to_string(), + &Cw721QueryMsg::OwnerOf { + token_id: token_id.to_string(), + include_expired: None, + }, + )?; + + let res = if owner_query.owner == *owner { + Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: token.to_string(), + msg: to_binary(&Cw721ExecuteMsg::TransferNft { + recipient: owner.to_string(), + token_id: token_id.to_string(), + })?, + funds: vec![], + })) + } else { + None + }; + + Ok(res) +} + +pub fn ibc_transfer(env: Env, data: IbcTransferMsg) -> Result { + let mut transfer_msg = data.transfer_msg.clone(); + + if data.timeout_block_delta.is_some() && data.transfer_msg.timeout_block.is_some() { + let block = transfer_msg.timeout_block.unwrap(); + transfer_msg.timeout_block = Some(TimeoutBlock { + revision_number: Some(block.revision_number()), + revision_height: Some(env.block.height + data.timeout_block_delta.unwrap()), + }) + } + + if data.timeout_timestamp_seconds_delta.is_some() { + transfer_msg.timeout_timestamp = Some( + env.block + .time + .plus_seconds( + env.block.time.seconds() + data.timeout_timestamp_seconds_delta.unwrap(), + ) + .nanos(), + ); + } + + Ok(Response::new().add_message(Stargate { + type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), + value: transfer_msg.encode_to_vec().into(), + })) +} From 99f9f5d699146d1e8a65971f56ddbb6b60c09ff3 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 16 Nov 2023 19:01:09 +0100 Subject: [PATCH 079/133] add funding account contract --- Cargo.lock | 48 +++ contracts/warp-funding-account/.cargo/config | 4 + contracts/warp-funding-account/.gitignore | 16 + contracts/warp-funding-account/Cargo.toml | 51 +++ contracts/warp-funding-account/README.md | 106 ++++++ .../examples/warp-funding-account-schema.rs | 24 ++ contracts/warp-funding-account/meta/README.md | 16 + .../warp-funding-account/meta/appveyor.yml | 61 ++++ .../meta/test_generate.sh | 37 ++ .../warp-funding-account/src/contract.rs | 60 ++++ contracts/warp-funding-account/src/error.rs | 64 ++++ contracts/warp-funding-account/src/lib.rs | 8 + contracts/warp-funding-account/src/state.rs | 4 + contracts/warp-funding-account/src/tests.rs | 329 ++++++++++++++++++ packages/funding-account/.cargo/config | 4 + packages/funding-account/Cargo.toml | 29 ++ packages/funding-account/README.md | 106 ++++++ .../examples/funding-account-schema.rs | 17 + packages/funding-account/src/lib.rs | 28 ++ 19 files changed, 1012 insertions(+) create mode 100644 contracts/warp-funding-account/.cargo/config create mode 100644 contracts/warp-funding-account/.gitignore create mode 100644 contracts/warp-funding-account/Cargo.toml create mode 100644 contracts/warp-funding-account/README.md create mode 100644 contracts/warp-funding-account/examples/warp-funding-account-schema.rs create mode 100644 contracts/warp-funding-account/meta/README.md create mode 100644 contracts/warp-funding-account/meta/appveyor.yml create mode 100644 contracts/warp-funding-account/meta/test_generate.sh create mode 100644 contracts/warp-funding-account/src/contract.rs create mode 100644 contracts/warp-funding-account/src/error.rs create mode 100644 contracts/warp-funding-account/src/lib.rs create mode 100644 contracts/warp-funding-account/src/state.rs create mode 100644 contracts/warp-funding-account/src/tests.rs create mode 100644 packages/funding-account/.cargo/config create mode 100644 packages/funding-account/Cargo.toml create mode 100644 packages/funding-account/README.md create mode 100644 packages/funding-account/examples/funding-account-schema.rs create mode 100644 packages/funding-account/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 59ab554d..2d124550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,9 +86,12 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-multi-test", + "cw20", + "cw721", "prost 0.11.9", "schemars", "serde", + "serde-json-wasm 0.4.1", "strum", "strum_macros", ] @@ -484,6 +487,28 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" +[[package]] +name = "funding-account" +version = "0.1.0" +dependencies = [ + "controller", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-asset", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw2 0.16.0", + "cw20", + "json-codec-wasm", + "prost 0.11.9", + "schemars", + "serde", + "strum", + "strum_macros", + "thiserror", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -1016,6 +1041,29 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "warp-account" +version = "0.1.0" +dependencies = [ + "base64", + "controller", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", + "cw-asset", + "cw-multi-test", + "cw-storage-plus 0.16.0", + "cw2 0.16.0", + "cw20", + "cw721", + "funding-account", + "json-codec-wasm", + "prost 0.11.9", + "schemars", + "serde-json-wasm 0.4.1", + "thiserror", +] + [[package]] name = "warp-controller" version = "0.1.0" diff --git a/contracts/warp-funding-account/.cargo/config b/contracts/warp-funding-account/.cargo/config new file mode 100644 index 00000000..752de1ab --- /dev/null +++ b/contracts/warp-funding-account/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example warp-funding-account-schema" diff --git a/contracts/warp-funding-account/.gitignore b/contracts/warp-funding-account/.gitignore new file mode 100644 index 00000000..9095deaa --- /dev/null +++ b/contracts/warp-funding-account/.gitignore @@ -0,0 +1,16 @@ +# Build results +/target +/schema + +# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) +.cargo-ok + +# Text file backups +**/*.rs.bk + +# macOS +.DS_Store + +# IDEs +*.iml +.idea diff --git a/contracts/warp-funding-account/Cargo.toml b/contracts/warp-funding-account/Cargo.toml new file mode 100644 index 00000000..415cb322 --- /dev/null +++ b/contracts/warp-funding-account/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "warp-account" +version = "0.1.0" +authors = ["Terra Money "] +edition = "2021" + +exclude = [ + # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. + "contract.wasm", + "hash.txt", +] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] +# use library feature to disable all instantiate/execute/query exports +library = [] + +[package.metadata.scripts] +optimize = """docker run --rm -v "${process.cwd()}":/code \ + -v "${path.join(process.cwd(), "../../", "packages")}":/packages \ + --mount type=volume,source="${contract}_cache",target=/code/target \ + --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ + cosmwasm/rust-optimizer${process.env.TERRARIUM_ARCH_ARM64 ? "-arm64" : ""}:0.12.6 +""" + +[dependencies] +cosmwasm-std = "1.1" +cosmwasm-storage = "1.1" +cosmwasm-schema = "1.1" +base64 = "0.13.0" +cw-asset = "2.2" +cw-storage-plus = "0.16" +cw2 = "0.16" +cw20 = "0.16" +cw721 = "0.16.0" +controller = { path = "../../packages/controller", default-features = false, version = "*" } +funding-account = { path = "../../packages/funding-account", default-features = false, version = "*" } +schemars = "0.8" +thiserror = "1" +serde-json-wasm = "0.4.1" +json-codec-wasm = "0.1.0" +prost = "0.11.9" + +[dev-dependencies] +cw-multi-test = "0.16.0" diff --git a/contracts/warp-funding-account/README.md b/contracts/warp-funding-account/README.md new file mode 100644 index 00000000..954383af --- /dev/null +++ b/contracts/warp-funding-account/README.md @@ -0,0 +1,106 @@ +# CosmWasm Starter Pack + +This is a template to build smart contracts in Rust to run inside a +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. +To understand the framework better, please read the overview in the +[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), +and dig into the [cosmwasm docs](https://www.cosmwasm.com). +This assumes you understand the theory and just want to get coding. + +## Creating a new repo from template + +Assuming you have a recent version of rust and cargo (v1.58.1+) installed +(via [rustup](https://rustup.rs/)), +then the following should get you a new repo to start a contract: + +Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. +Unless you did that before, run this line now: + +```sh +cargo install cargo-generate --features vendored-openssl +cargo install cargo-run-script +``` + +Now, use it to create your new contract. +Go to the folder in which you want to place it and run: + + +**Latest: 1.0.0-beta6** + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME +```` + +**Older Version** + +Pass version as branch flag: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME +```` + +Example: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME +``` + +You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) +containing a simple working contract and build system that you can customize. + +## Create a Repo + +After generating, you have a initialized local git repo, but no commits, and no remote. +Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). +Then run the following: + +```sh +# this is needed to create a valid Cargo.lock file (see below) +cargo check +git branch -M main +git add . +git commit -m 'Initial Commit' +git remote add origin YOUR-GIT-URL +git push -u origin main +``` + +## CI Support + +We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) +and [Circle CI](.circleci/config.yml) in the generated project, so you can +get up and running with CI right away. + +One note is that the CI runs all `cargo` commands +with `--locked` to ensure it uses the exact same versions as you have locally. This also means +you must have an up-to-date `Cargo.lock` file, which is not auto-generated. +The first time you set up the project (or after adding any dep), you should ensure the +`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by +running `cargo check` or `cargo unit-test`. + +## Using your project + +Once you have your custom repo, you should check out [Developing](./Developing.md) to explain +more on how to run tests and develop code. Or go through the +[online tutorial](https://docs.cosmwasm.com/) to get a better feel +of how to develop. + +[Publishing](./Publishing.md) contains useful information on how to publish your contract +to the world, once you are ready to deploy it on a running blockchain. And +[Importing](./Importing.md) contains information about pulling in other contracts or crates +that have been published. + +Please replace this README file with information about your specific project. You can keep +the `Developing.md` and `Publishing.md` files as useful referenced, but please set some +proper description in the README. + +## Gitpod integration + +[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. + +Workspace contains: + - **rust**: for builds + - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client + - **jq**: shell JSON manipulation tool + +Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. + diff --git a/contracts/warp-funding-account/examples/warp-funding-account-schema.rs b/contracts/warp-funding-account/examples/warp-funding-account-schema.rs new file mode 100644 index 00000000..1f9d1db8 --- /dev/null +++ b/contracts/warp-funding-account/examples/warp-funding-account-schema.rs @@ -0,0 +1,24 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use controller::{ + account::{FundingAccountResponse, FundingAccountsResponse}, + job::{JobResponse, JobsResponse}, +}; +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; +use funding_account::{Config, ExecuteMsg, InstantiateMsg}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(Config), &out_dir); + export_schema(&schema_for!(JobResponse), &out_dir); + export_schema(&schema_for!(JobsResponse), &out_dir); + export_schema(&schema_for!(FundingAccountResponse), &out_dir); + export_schema(&schema_for!(FundingAccountsResponse), &out_dir); +} diff --git a/contracts/warp-funding-account/meta/README.md b/contracts/warp-funding-account/meta/README.md new file mode 100644 index 00000000..279d1db4 --- /dev/null +++ b/contracts/warp-funding-account/meta/README.md @@ -0,0 +1,16 @@ +# The meta folder + +This folder is ignored via the `.genignore` file. It contains meta files +that should not make it into the generated project. + +In particular, it is used for an AppVeyor CI script that runs on `cw-template` +itself (running the cargo-generate script, then testing the generated project). +The `.circleci` and `.github` directories contain scripts destined for any projects created from +this template. + +## Files + +- `appveyor.yml`: The AppVeyor CI configuration +- `test_generate.sh`: A script for generating a project from the template and + runnings builds and tests in it. This works almost like the CI script but + targets local UNIX-like dev environments. diff --git a/contracts/warp-funding-account/meta/appveyor.yml b/contracts/warp-funding-account/meta/appveyor.yml new file mode 100644 index 00000000..5da37f70 --- /dev/null +++ b/contracts/warp-funding-account/meta/appveyor.yml @@ -0,0 +1,61 @@ +# This CI configuration tests the cw-template repository itself, +# not the resulting project. We want to ensure that +# 1. the template to project generation works +# 2. the template files are up to date +# +# We chose Appveyor for this task as it allows us to use an arbitrary config +# location. Furthermore it allows us to ship Circle CI and GitHub Actions configs +# generated for the resulting project. + +image: Ubuntu + +environment: + TOOLCHAIN: 1.58.1 + +services: + - docker + +cache: + - $HOME/.rustup/ -> meta/appveyor.yml + # For details about cargo caching see https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci + - $HOME/.cargo/bin/ -> meta/appveyor.yml + - $HOME/.cargo/registry/index/ -> meta/appveyor.yml + - $HOME/.cargo/registry/cache/ -> meta/appveyor.yml + - $HOME/.cargo/git/db/ -> meta/appveyor.yml + +install: + - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain "$TOOLCHAIN" -y + - source $HOME/.cargo/env + - rustc --version + - cargo --version + - rustup target add wasm32-unknown-unknown + - cargo install --features vendored-openssl cargo-generate || true + +build_script: + # No matter what is currently checked out by the CI (main, other branch, PR merge commit), + # we create a temporary local branch from that point with a constant name, which we need for + # cargo generate. + - git branch current-ci-checkout + - cd .. + - cargo generate --git cw-template --name testgen-ci --branch current-ci-checkout + - cd testgen-ci + - ls -lA + - cargo fmt -- --check + - cargo unit-test + - cargo wasm + - cargo schema + - docker build --pull -t "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" . + - \[ "${APPVEYOR_REPO_BRANCH}" = "main" \] && image_tag=latest || image_tag=${APPVEYOR_REPO_TAG_NAME} + - docker tag "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" "cosmwasm/cw-gitpod-base:${image_tag}" + +on_success: + # publish docker image + - docker login --password-stdin -u "$DOCKER_USER" <<<"$DOCKER_PASS" + - docker push + - docker logout + +branches: +# whitelist long living branches and tags + only: + # - main + - /v\d+\.\d+\.\d+/ diff --git a/contracts/warp-funding-account/meta/test_generate.sh b/contracts/warp-funding-account/meta/test_generate.sh new file mode 100644 index 00000000..b9aaa237 --- /dev/null +++ b/contracts/warp-funding-account/meta/test_generate.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -o errexit -o nounset -o pipefail +command -v shellcheck > /dev/null && shellcheck "$0" + +REPO_ROOT="$(realpath "$(dirname "$0")/..")" + +TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/cw-template.XXXXXXXXX") +PROJECT_NAME="testgen-local" + +( + echo "Navigating to $TMP_DIR" + cd "$TMP_DIR" + + GIT_BRANCH=$(git -C "$REPO_ROOT" branch --show-current) + + echo "Generating project from local repository (branch $GIT_BRANCH) ..." + cargo generate --git "$REPO_ROOT" --name "$PROJECT_NAME" --branch "$GIT_BRANCH" + + ( + cd "$PROJECT_NAME" + echo "This is what was generated" + ls -lA + + # Check formatting + echo "Checking formatting ..." + cargo fmt -- --check + + # Debug builds first to fail fast + echo "Running unit tests ..." + cargo unit-test + echo "Creating schema ..." + cargo schema + + echo "Building wasm ..." + cargo wasm + ) +) diff --git a/contracts/warp-funding-account/src/contract.rs b/contracts/warp-funding-account/src/contract.rs new file mode 100644 index 00000000..344c01ce --- /dev/null +++ b/contracts/warp-funding-account/src/contract.rs @@ -0,0 +1,60 @@ +use crate::state::CONFIG; +use crate::ContractError; +use controller::account::execute_warp_msgs; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, +}; +use funding_account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn instantiate( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + CONFIG.save( + deps.storage, + &Config { + owner: deps.api.addr_validate(&msg.owner)?, + warp_addr: info.sender, + }, + )?; + Ok(Response::new() + .add_attribute("action", "instantiate") + .add_attribute("contract_addr", env.contract.address) + .add_attribute("owner", msg.owner)) +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + if info.sender != config.owner && info.sender != config.warp_addr { + return Err(ContractError::Unauthorized {}); + } + match msg { + ExecuteMsg::WarpMsgs(data) => { + execute_warp_msgs(deps, env, data, &config.owner).map_err(ContractError::Std) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::Config => { + let config = CONFIG.load(deps.storage)?; + to_binary(&config) + } + } +} + +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Response::new()) +} diff --git a/contracts/warp-funding-account/src/error.rs b/contracts/warp-funding-account/src/error.rs new file mode 100644 index 00000000..f8855692 --- /dev/null +++ b/contracts/warp-funding-account/src/error.rs @@ -0,0 +1,64 @@ +use crate::ContractError::{DecodeError, DeserializationError, SerializationError}; +use cosmwasm_std::StdError; +use thiserror::Error; + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Unauthorized")] + Unauthorized {}, + + #[error("Invalid fee")] + InvalidFee {}, + + #[error("Funds array in message does not match funds array in job.")] + FundsMismatch {}, + + #[error("Reward provided is smaller than minimum")] + RewardTooSmall {}, + + #[error("Invalid arguments")] + InvalidArguments {}, + + #[error("Custom Error val: {val:?}")] + CustomError { val: String }, + // Add any other custom errors you like here. + // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. + #[error("Error deserializing data")] + DeserializationError {}, + + #[error("Error serializing data")] + SerializationError {}, + + #[error("Error decoding JSON result")] + DecodeError {}, + + #[error("Error resolving JSON path")] + ResolveError {}, +} + +impl From for ContractError { + fn from(_: serde_json_wasm::de::Error) -> Self { + DeserializationError {} + } +} + +impl From for ContractError { + fn from(_: serde_json_wasm::ser::Error) -> Self { + SerializationError {} + } +} + +impl From for ContractError { + fn from(_: json_codec_wasm::DecodeError) -> Self { + DecodeError {} + } +} + +impl From for ContractError { + fn from(_: base64::DecodeError) -> Self { + DecodeError {} + } +} diff --git a/contracts/warp-funding-account/src/lib.rs b/contracts/warp-funding-account/src/lib.rs new file mode 100644 index 00000000..90d6bfa8 --- /dev/null +++ b/contracts/warp-funding-account/src/lib.rs @@ -0,0 +1,8 @@ +pub mod contract; +mod error; +pub mod state; + +#[cfg(test)] +mod tests; + +pub use crate::error::ContractError; diff --git a/contracts/warp-funding-account/src/state.rs b/contracts/warp-funding-account/src/state.rs new file mode 100644 index 00000000..9ba4e512 --- /dev/null +++ b/contracts/warp-funding-account/src/state.rs @@ -0,0 +1,4 @@ +use cw_storage_plus::Item; +use funding_account::Config; + +pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/warp-funding-account/src/tests.rs b/contracts/warp-funding-account/src/tests.rs new file mode 100644 index 00000000..71af730f --- /dev/null +++ b/contracts/warp-funding-account/src/tests.rs @@ -0,0 +1,329 @@ +use crate::contract::{execute, instantiate}; +use crate::ContractError; +use controller::account::{WarpMsg, WarpMsgs}; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; +use cosmwasm_std::{ + to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, + IbcTimeoutBlock, Response, StakingMsg, Uint128, VoteOption, WasmMsg, +}; +use funding_account::{ExecuteMsg, InstantiateMsg}; + +#[test] +fn test_execute_controller() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("vlad_controller", &[]); + + let _instantiate_res = instantiate( + deps.as_mut(), + env.clone(), + info.clone(), + InstantiateMsg { + owner: "vlad".to_string(), + }, + ); + + let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { + msgs: vec![ + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + })), + WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + })), + WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes, + })), + WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + })), + WarpMsg::Generic(CosmosMsg::Distribution( + DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }, + )), + WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0, + }), + })), + WarpMsg::Generic(CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default(), + }), + ], + }); + + let execute_res = execute(deps.as_mut(), env, info, execute_msg).unwrap(); + + assert_eq!( + execute_res, + Response::new() + .add_attribute("action", "warp_msgs") + .add_messages(vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }] + }), + CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes + }), + CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + }), + CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0 + }), + }), + CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default() + } + ]) + ) +} + +#[test] +fn test_execute_owner() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("vlad_controller", &[]); + + let _instantiate_res = instantiate( + deps.as_mut(), + env.clone(), + info, + InstantiateMsg { + owner: "vlad".to_string(), + }, + ); + let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { + msgs: vec![ + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + })), + WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + })), + WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes, + })), + WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + })), + WarpMsg::Generic(CosmosMsg::Distribution( + DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }, + )), + WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0, + }), + })), + WarpMsg::Generic(CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default(), + }), + ], + }); + + let info2 = mock_info("vlad", &[]); + + let execute_res = execute(deps.as_mut(), env, info2, execute_msg).unwrap(); + + assert_eq!( + execute_res, + Response::new() + .add_attribute("action", "warp_msgs") + .add_messages(vec![ + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }], + }), + CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }] + }), + CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes + }), + CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + }), + CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }), + CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100) + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0 + }), + }), + CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default() + } + ]) + ) +} + +#[test] +fn test_execute_unauth() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let info = mock_info("vlad_controller", &[]); + + let _instantiate_res = instantiate( + deps.as_mut(), + env.clone(), + info, + InstantiateMsg { + owner: "vlad".to_string(), + }, + ); + let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { + msgs: vec![ + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: "contract".to_string(), + msg: to_binary("test").unwrap(), + funds: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + })), + WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { + to_address: "vlad2".to_string(), + amount: vec![Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }], + })), + WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { + proposal_id: 0, + vote: VoteOption::Yes, + })), + WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { + validator: "vladidator".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + })), + WarpMsg::Generic(CosmosMsg::Distribution( + DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }, + )), + WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { + channel_id: "channel_vlad".to_string(), + to_address: "vlad3".to_string(), + amount: Coin { + denom: "coin".to_string(), + amount: Uint128::new(100), + }, + timeout: IbcTimeout::with_block(IbcTimeoutBlock { + revision: 0, + height: 0, + }), + })), + WarpMsg::Generic(CosmosMsg::Stargate { + type_url: "utl".to_string(), + value: Default::default(), + }), + ], + }); + + let info2 = mock_info("vlad2", &[]); + + let execute_res = execute(deps.as_mut(), env, info2, execute_msg).unwrap_err(); + + assert_eq!(execute_res, ContractError::Unauthorized {}) +} diff --git a/packages/funding-account/.cargo/config b/packages/funding-account/.cargo/config new file mode 100644 index 00000000..48a4663c --- /dev/null +++ b/packages/funding-account/.cargo/config @@ -0,0 +1,4 @@ +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +unit-test = "test --lib" +schema = "run --example funding-account-schema" diff --git a/packages/funding-account/Cargo.toml b/packages/funding-account/Cargo.toml new file mode 100644 index 00000000..73255c32 --- /dev/null +++ b/packages/funding-account/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "funding-account" +version = "0.1.0" +authors = ["Terra Money "] +edition = "2021" + +[features] +# for more explicit tests, cargo test --features=backtraces +backtraces = ["cosmwasm-std/backtraces"] + +[dependencies] +cosmwasm-std = "1.1" +cosmwasm-storage = "1.1" +cosmwasm-schema = "1.1" +cw-asset = "2.2" +cw20 = "0.16" +cw-storage-plus = "0.16" +cw2 = "0.16" +schemars = "0.8" +serde = { version = "1", default-features = false, features = ["derive"] } +json-codec-wasm = "0.1.0" +strum = "0.24" +strum_macros = "0.24" +thiserror = { version = "1" } +controller = {path = "../controller"} +prost = "0.11.9" + +[dev-dependencies] +cw-multi-test = "0.16" diff --git a/packages/funding-account/README.md b/packages/funding-account/README.md new file mode 100644 index 00000000..954383af --- /dev/null +++ b/packages/funding-account/README.md @@ -0,0 +1,106 @@ +# CosmWasm Starter Pack + +This is a template to build smart contracts in Rust to run inside a +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. +To understand the framework better, please read the overview in the +[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), +and dig into the [cosmwasm docs](https://www.cosmwasm.com). +This assumes you understand the theory and just want to get coding. + +## Creating a new repo from template + +Assuming you have a recent version of rust and cargo (v1.58.1+) installed +(via [rustup](https://rustup.rs/)), +then the following should get you a new repo to start a contract: + +Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. +Unless you did that before, run this line now: + +```sh +cargo install cargo-generate --features vendored-openssl +cargo install cargo-run-script +``` + +Now, use it to create your new contract. +Go to the folder in which you want to place it and run: + + +**Latest: 1.0.0-beta6** + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME +```` + +**Older Version** + +Pass version as branch flag: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME +```` + +Example: + +```sh +cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME +``` + +You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) +containing a simple working contract and build system that you can customize. + +## Create a Repo + +After generating, you have a initialized local git repo, but no commits, and no remote. +Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). +Then run the following: + +```sh +# this is needed to create a valid Cargo.lock file (see below) +cargo check +git branch -M main +git add . +git commit -m 'Initial Commit' +git remote add origin YOUR-GIT-URL +git push -u origin main +``` + +## CI Support + +We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) +and [Circle CI](.circleci/config.yml) in the generated project, so you can +get up and running with CI right away. + +One note is that the CI runs all `cargo` commands +with `--locked` to ensure it uses the exact same versions as you have locally. This also means +you must have an up-to-date `Cargo.lock` file, which is not auto-generated. +The first time you set up the project (or after adding any dep), you should ensure the +`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by +running `cargo check` or `cargo unit-test`. + +## Using your project + +Once you have your custom repo, you should check out [Developing](./Developing.md) to explain +more on how to run tests and develop code. Or go through the +[online tutorial](https://docs.cosmwasm.com/) to get a better feel +of how to develop. + +[Publishing](./Publishing.md) contains useful information on how to publish your contract +to the world, once you are ready to deploy it on a running blockchain. And +[Importing](./Importing.md) contains information about pulling in other contracts or crates +that have been published. + +Please replace this README file with information about your specific project. You can keep +the `Developing.md` and `Publishing.md` files as useful referenced, but please set some +proper description in the README. + +## Gitpod integration + +[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. + +Workspace contains: + - **rust**: for builds + - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client + - **jq**: shell JSON manipulation tool + +Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. + diff --git a/packages/funding-account/examples/funding-account-schema.rs b/packages/funding-account/examples/funding-account-schema.rs new file mode 100644 index 00000000..b6ba6260 --- /dev/null +++ b/packages/funding-account/examples/funding-account-schema.rs @@ -0,0 +1,17 @@ +use std::env::current_dir; +use std::fs::create_dir_all; + +use controller::QueryMsg; +use controller::{ExecuteMsg, InstantiateMsg}; +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; + +fn main() { + let mut out_dir = current_dir().unwrap(); + out_dir.push("schema"); + create_dir_all(&out_dir).unwrap(); + remove_schemas(&out_dir).unwrap(); + + export_schema(&schema_for!(InstantiateMsg), &out_dir); + export_schema(&schema_for!(ExecuteMsg), &out_dir); + export_schema(&schema_for!(QueryMsg), &out_dir); +} diff --git a/packages/funding-account/src/lib.rs b/packages/funding-account/src/lib.rs new file mode 100644 index 00000000..84eeb07f --- /dev/null +++ b/packages/funding-account/src/lib.rs @@ -0,0 +1,28 @@ +use controller::account::WarpMsgs; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::Addr; + +#[cw_serde] +pub struct Config { + pub owner: Addr, + pub warp_addr: Addr, +} + +#[cw_serde] +pub struct InstantiateMsg { + pub owner: String, +} + +#[cw_serde] +#[allow(clippy::large_enum_variant)] +pub enum ExecuteMsg { + WarpMsgs(WarpMsgs), +} + +#[cw_serde] +pub enum QueryMsg { + Config, +} + +#[cw_serde] +pub struct MigrateMsg {} From ca91a3f4c8a4d1ef97dcf5520390bdc719752091 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 20 Nov 2023 16:15:42 +0100 Subject: [PATCH 080/133] add funding_account to job --- contracts/warp-controller/src/execute/job.rs | 1 + contracts/warp-controller/src/migrate/job.rs | 4 ++++ contracts/warp-controller/src/reply/job.rs | 1 + contracts/warp-controller/src/state.rs | 3 +++ .../examples/warp-funding-account-schema.rs | 8 -------- packages/controller/src/job.rs | 2 ++ 6 files changed, 11 insertions(+), 8 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index a529226b..0a8c9787 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -122,6 +122,7 @@ pub fn create_job( assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), duration_days: data.duration_days, created_at_time: Uint64::from(env.block.time.seconds()), + funding_account: data.funding_account, }, )?; diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs index fd72cae8..7d71fb58 100644 --- a/contracts/warp-controller/src/migrate/job.rs +++ b/contracts/warp-controller/src/migrate/job.rs @@ -102,6 +102,8 @@ pub fn migrate_pending_jobs( assets_to_withdraw: old_job.assets_to_withdraw, duration_days: Uint64::from(30u64), created_at_time: old_job.last_update_time, + // TODO: update to old_job.funding_account + funding_account: None, }, )?; } @@ -169,6 +171,8 @@ pub fn migrate_finished_jobs( assets_to_withdraw: old_job.assets_to_withdraw, duration_days: Uint64::from(30u64), created_at_time: old_job.last_update_time, + // TODO: update to old_job.funding_account + funding_account: None, }, )?; } diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 671e35db..d0cd1d82 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -161,6 +161,7 @@ pub fn execute_job( assets_to_withdraw: finished_job.assets_to_withdraw.clone(), duration_days: finished_job.duration_days, created_at_time: Uint64::from(env.block.time.seconds()), + funding_account: finished_job.funding_account, }, )?; diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 8eab9f50..b8f71ec1 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -129,6 +129,7 @@ impl JobQueue { assets_to_withdraw: job.assets_to_withdraw, duration_days: job.duration_days, created_at_time: Uint64::from(env.block.time.seconds()), + funding_account: job.funding_account, }), }) } @@ -154,6 +155,7 @@ impl JobQueue { assets_to_withdraw: job.assets_to_withdraw, duration_days: job.duration_days, created_at_time: job.created_at_time, + funding_account: job.funding_account, }), }) } @@ -188,6 +190,7 @@ impl JobQueue { assets_to_withdraw: job.assets_to_withdraw, duration_days: job.duration_days, created_at_time: job.created_at_time, + funding_account: job.funding_account, }; FINISHED_JOBS().update(deps.storage, job_id, |j| match j { diff --git a/contracts/warp-funding-account/examples/warp-funding-account-schema.rs b/contracts/warp-funding-account/examples/warp-funding-account-schema.rs index 1f9d1db8..9d955c5a 100644 --- a/contracts/warp-funding-account/examples/warp-funding-account-schema.rs +++ b/contracts/warp-funding-account/examples/warp-funding-account-schema.rs @@ -1,10 +1,6 @@ use std::env::current_dir; use std::fs::create_dir_all; -use controller::{ - account::{FundingAccountResponse, FundingAccountsResponse}, - job::{JobResponse, JobsResponse}, -}; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use funding_account::{Config, ExecuteMsg, InstantiateMsg}; @@ -17,8 +13,4 @@ fn main() { export_schema(&schema_for!(InstantiateMsg), &out_dir); export_schema(&schema_for!(ExecuteMsg), &out_dir); export_schema(&schema_for!(Config), &out_dir); - export_schema(&schema_for!(JobResponse), &out_dir); - export_schema(&schema_for!(JobsResponse), &out_dir); - export_schema(&schema_for!(FundingAccountResponse), &out_dir); - export_schema(&schema_for!(FundingAccountsResponse), &out_dir); } diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index ba36b2f0..4c5aca97 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -15,6 +15,7 @@ pub struct Job { // As job creator can have infinite job accounts, each job account can only be used by up to 1 active job // So each job's fund is isolated pub account: Addr, + pub funding_account: Option, pub last_update_time: Uint64, pub name: String, pub description: String, @@ -67,6 +68,7 @@ pub struct CreateJobMsg { pub assets_to_withdraw: Option>, pub account_msgs: Option>, pub cw_funds: Option>, + pub funding_account: Option, } #[cw_serde] From 32224bcc96b23bcbfe6c320df2761dc5cbb93603 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 21 Nov 2023 18:00:41 +0100 Subject: [PATCH 081/133] add funding accounts support to jobs - operational amount is always kept in funding accs + user can optionally attach a funding account in create job msg, otherwise it is taken on the fly --- contracts/warp-controller/src/contract.rs | 2 + contracts/warp-controller/src/execute/job.rs | 156 ++++++++++++++++-- contracts/warp-controller/src/migrate/job.rs | 2 + .../warp-controller/src/reply/account.rs | 101 +++++++++++- contracts/warp-controller/src/reply/job.rs | 56 +++++-- contracts/warp-controller/src/state.rs | 3 + .../warp-job-account-tracker-schema.rs | 6 +- .../warp-job-account-tracker/src/contract.rs | 3 + .../src/integration_tests.rs | 6 +- .../src/query/account.rs | 32 +++- packages/controller/src/job.rs | 2 + packages/job-account-tracker/src/lib.rs | 11 +- 12 files changed, 334 insertions(+), 46 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index e2c16c1e..17521acc 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -184,9 +184,11 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { let config = CONFIG.load(deps.storage)?; + // 0-1 reserved match msg.id { // use 0 as hack to call create_job_account_and_job 0 => reply::account::create_job_account_and_job(deps, env, msg, config), + 1 => reply::account::create_funding_account_and_job(deps, env, msg, config), _id => reply::job::execute_job(deps, env, msg, config), } } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 0a8c9787..9c8d9157 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,7 +1,7 @@ use crate::state::{JobQueue, STATE}; use crate::util::msg::build_account_execute_warp_msgs; use crate::ContractError; -use controller::account::WarpMsgs; +use controller::account::{AssetInfo, WarpMsgs}; use controller::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Execution, Job, JobStatus, UpdateJobMsg, }; @@ -24,7 +24,7 @@ use crate::{ }; use controller::{account::CwFund, Config}; -use job_account_tracker::FirstFreeAccountResponse; +use job_account_tracker::{Account, AccountResponse, AccountsResponse}; use resolver::QueryHydrateMsgsMsg; use super::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}; @@ -99,6 +99,11 @@ pub fn create_job( ); let state = STATE.load(deps.storage)?; + + let operational_amount_minus_reward_and_fee = data + .operational_amount + .checked_sub(data.reward + total_fees)?; + let mut job = JobQueue::add( &mut deps, Job { @@ -122,20 +127,45 @@ pub fn create_job( assets_to_withdraw: data.assets_to_withdraw.unwrap_or(vec![]), duration_days: data.duration_days, created_at_time: Uint64::from(env.block.time.seconds()), - funding_account: data.funding_account, + // placeholder, will be updated later on + funding_account: None, + // needs to have reward and total_fees subtracted from it (reward is sent to controller, fees are sent to fee collector) + operational_amount: operational_amount_minus_reward_and_fee, }, )?; - let available_account: FirstFreeAccountResponse = deps.querier.query_wasm_smart( + let free_accounts: AccountsResponse = deps.querier.query_wasm_smart( job_account_tracker_address_ref, - &job_account_tracker::QueryMsg::QueryFirstFreeAccount( - job_account_tracker::QueryFirstFreeAccountMsg { + &job_account_tracker::QueryMsg::QueryFreeAccounts( + job_account_tracker::QueryFreeAccountsMsg { account_owner_addr: job_owner.to_string(), + start_after: None, + limit: Some(2), }, ), )?; - match available_account.account { + let job_account = free_accounts.accounts.get(0); + + let funding_account: Option; + + if let Some(funding_account_addr) = data.funding_account { + // fetch funding account and check if it exists, throw otherwise + let funding_account_response: AccountResponse = deps.querier.query_wasm_smart( + job_account_tracker_address_ref, + &job_account_tracker::QueryMsg::QueryFreeAccount( + job_account_tracker::QueryFreeAccountMsg { + account_addr: funding_account_addr.to_string(), + }, + ), + )?; + + funding_account = Some(funding_account_response.account.unwrap()); + } else { + funding_account = free_accounts.accounts.get(1).cloned(); + } + + match job_account { None => { // Create account then create job in reply submsgs.push(SubMsg { @@ -156,10 +186,10 @@ pub fn create_job( attrs.push(Attribute::new("action", "create_account_and_job")); } Some(available_account) => { - let available_account_addr = available_account.addr; + let available_account_addr = &available_account.addr; // Update job.account from placeholder value to job account job.account = available_account_addr.clone(); - JobQueue::sync(&mut deps, env, job.clone())?; + JobQueue::sync(&mut deps, env.clone(), job.clone())?; if !native_funds_minus_reward_and_fee.is_empty() { // Fund account in native coins @@ -210,8 +240,8 @@ pub fn create_job( attrs.push(Attribute::new("action", "create_job")); attrs.push(Attribute::new("job_id", job.id)); - attrs.push(Attribute::new("job_owner", job.owner)); - attrs.push(Attribute::new("job_name", job.name)); + attrs.push(Attribute::new("job_owner", job.owner.clone())); + attrs.push(Attribute::new("job_name", job.name.clone())); attrs.push(Attribute::new( "job_status", serde_json_wasm::to_string(&job.status)?, @@ -235,6 +265,82 @@ pub fn create_job( } } + // not sure if to check for small amounts here? + if !operational_amount_minus_reward_and_fee.is_zero() { + match funding_account { + None => { + // Create funding account then create job in reply + submsgs.push(SubMsg { + id: 1, + msg: build_instantiate_warp_account_msg( + job.id, + env.contract.address.to_string(), + config.warp_account_code_id.u64(), + info.sender.to_string(), + vec![Coin::new( + operational_amount_minus_reward_and_fee.u128(), + config.fee_denom, + )], + None, + None, + ), + gas_limit: None, + reply_on: ReplyOn::Always, + }); + + attrs.push(Attribute::new("action", "create_funding_account_and_job")); + } + Some(available_account) => { + let available_account_addr = &available_account.addr; + // Update job.account from placeholder value to job account + job.funding_account = Some(available_account_addr.clone()); + JobQueue::sync(&mut deps, env, job.clone())?; + + // Fund account in native coins + msgs.push(build_transfer_native_funds_msg( + available_account_addr.to_string(), + vec![Coin::new( + operational_amount_minus_reward_and_fee.u128(), + config.fee_denom, + )], + )); + + // Take account + msgs.push(build_taken_account_msg( + config.job_account_tracker_address.to_string(), + job_owner.to_string(), + available_account_addr.to_string(), + job.id, + )); + + attrs.push(Attribute::new("action", "create_job")); + attrs.push(Attribute::new("job_id", job.id)); + attrs.push(Attribute::new("job_owner", job.owner)); + attrs.push(Attribute::new("job_name", job.name)); + attrs.push(Attribute::new( + "job_status", + serde_json_wasm::to_string(&job.status)?, + )); + attrs.push(Attribute::new( + "job_executions", + serde_json_wasm::to_string(&job.executions)?, + )); + attrs.push(Attribute::new("job_reward", job.reward)); + attrs.push(Attribute::new("job_creation_fee", creation_fee.to_string())); + attrs.push(Attribute::new( + "job_maintenance_fee", + maintenance_fee.to_string(), + )); + attrs.push(Attribute::new("job_burn_fee", burn_fee.to_string())); + attrs.push(Attribute::new("job_total_fees", total_fees.to_string())); + attrs.push(Attribute::new( + "job_last_updated_time", + job.last_update_time, + )); + } + } + } + Ok(Response::new() .add_submessages(submsgs) .add_messages(msgs) @@ -283,7 +389,7 @@ pub fn delete_job( // Controller sends cancellation fee to fee collector msgs.push(build_transfer_native_funds_msg( config.fee_collector.to_string(), - vec![Coin::new(fee.u128(), config.fee_denom)], + vec![Coin::new(fee.u128(), config.fee_denom.clone())], )); if !is_legacy_account(legacy_account, job_account_addr.clone()) { @@ -294,6 +400,21 @@ pub fn delete_job( job_account_addr.to_string(), job.id, )); + + if let Some(funding_account) = job.funding_account { + msgs.push(build_free_account_msg( + config.job_account_tracker_address.to_string(), + job.owner.to_string(), + funding_account.to_string(), + job.id, + )); + + // withdraws all native funds from funding account + msgs.push(build_account_withdraw_assets_msg( + funding_account.to_string(), + vec![AssetInfo::Native(config.fee_denom)], + )); + } } // Job owner withdraw all assets that are listed from warp account to itself @@ -433,6 +554,15 @@ pub fn execute_job( job_account_addr.to_string(), job.id, )); + + if let Some(funding_account) = job.funding_account { + msgs.push(build_free_account_msg( + config.job_account_tracker_address.to_string(), + job.owner.to_string(), + funding_account.to_string(), + job.id, + )); + } } Ok(Response::new() @@ -479,7 +609,7 @@ pub fn evict_job( // Controller sends execution reward minus eviction reward back to account msgs.push(build_transfer_native_funds_msg( - info.sender.to_string(), + job.account.to_string(), vec![Coin::new( (job.reward - eviction_fee).u128(), config.fee_denom.clone(), diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs index 7d71fb58..4c598f8c 100644 --- a/contracts/warp-controller/src/migrate/job.rs +++ b/contracts/warp-controller/src/migrate/job.rs @@ -104,6 +104,7 @@ pub fn migrate_pending_jobs( created_at_time: old_job.last_update_time, // TODO: update to old_job.funding_account funding_account: None, + operational_amount: old_job.reward, }, )?; } @@ -173,6 +174,7 @@ pub fn migrate_finished_jobs( created_at_time: old_job.last_update_time, // TODO: update to old_job.funding_account funding_account: None, + operational_amount: old_job.reward, }, )?; } diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 8b671ead..8904dc75 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -22,7 +22,7 @@ pub fn create_job_account_and_job( ) -> Result { let reply = msg.result.into_result().map_err(StdError::generic_err)?; - let event = reply + let job_account_event = reply .events .iter() .find(|event| { @@ -33,7 +33,7 @@ pub fn create_job_account_and_job( }) .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; - let job_id_str = event + let job_id_str = job_account_event .attributes .iter() .cloned() @@ -42,7 +42,7 @@ pub fn create_job_account_and_job( .value; let job_id = job_id_str.as_str().parse::()?; - let owner = event + let owner = job_account_event .attributes .iter() .cloned() @@ -51,7 +51,7 @@ pub fn create_job_account_and_job( .value; let job_account_addr = deps.api.addr_validate( - &event + &job_account_event .attributes .iter() .cloned() @@ -61,7 +61,7 @@ pub fn create_job_account_and_job( )?; let native_funds: Vec = serde_json_wasm::from_str( - &event + &job_account_event .attributes .iter() .cloned() @@ -71,7 +71,7 @@ pub fn create_job_account_and_job( )?; let cw_funds: Option> = serde_json_wasm::from_str( - &event + &job_account_event .attributes .iter() .cloned() @@ -81,7 +81,7 @@ pub fn create_job_account_and_job( )?; let account_msgs: Option> = serde_json_wasm::from_str( - &event + &job_account_event .attributes .iter() .cloned() @@ -155,3 +155,90 @@ pub fn create_job_account_and_job( serde_json_wasm::to_string(&cw_funds.unwrap_or(vec![]))?, )) } + +pub fn create_funding_account_and_job( + mut deps: DepsMut, + env: Env, + msg: Reply, + config: Config, +) -> Result { + let reply = msg.result.into_result().map_err(StdError::generic_err)?; + + let funding_account_event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "instantiate") + }) + .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + + let job_id_str = funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "job_id") + .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? + .value; + let job_id = job_id_str.as_str().parse::()?; + + let owner = funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "owner") + .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? + .value; + + let funding_account_addr = deps.api.addr_validate( + &funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "contract_addr") + .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? + .value, + )?; + + let native_funds: Vec = serde_json_wasm::from_str( + &funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "native_funds") + .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? + .value, + )?; + + let mut job = JobQueue::get(&deps, job_id)?; + job.funding_account = Some(funding_account_addr.clone()); + JobQueue::sync(&mut deps, env, job.clone())?; + + let mut msgs: Vec = vec![]; + + if !native_funds.is_empty() { + // Fund account in native coins + msgs.push(build_transfer_native_funds_msg( + funding_account_addr.to_string(), + native_funds.clone(), + )) + } + + // Take job account + msgs.push(build_taken_account_msg( + config.job_account_tracker_address.to_string(), + job.owner.to_string(), + funding_account_addr.to_string(), + job.id, + )); + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "create_job_account_and_job_reply") + // .add_attribute("job_id", value) + .add_attribute("owner", owner) + .add_attribute("funding_account_address", funding_account_addr) + .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) +} diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index d0cd1d82..4deaa93e 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -17,6 +17,7 @@ use crate::{ ContractError, }; use controller::{ + account::AssetInfo, job::{Job, JobStatus}, Config, }; @@ -61,19 +62,25 @@ pub fn execute_job( let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, finished_job.owner.clone())?; let job_account_addr = finished_job.account.clone(); - let job_account_amount = deps - .querier - .query::(&QueryRequest::Bank(BankQuery::Balance { - address: job_account_addr.to_string(), - denom: config.fee_denom.clone(), - }))? - .amount - .amount; - let mut recurring_job_created = false; + // backwards compability with legacy accounts, funding account is job's account + let funding_account_addr = finished_job + .funding_account + .clone() + .unwrap_or(job_account_addr.clone()); + if finished_job.recurring { - if job_account_amount < reward_plus_fee { + let operational_amount = deps + .querier + .query::(&QueryRequest::Bank(BankQuery::Balance { + address: funding_account_addr.to_string(), + denom: config.fee_denom.clone(), + }))? + .amount + .amount; + + if operational_amount < reward_plus_fee { new_job_attrs.push(Attribute::new("action", "recur_job")); new_job_attrs.push(Attribute::new("creation_status", "failed_insufficient_fee")); } else if !(finished_job.status == JobStatus::Executed @@ -141,6 +148,10 @@ pub fn execute_job( if !should_terminate_job { recurring_job_created = true; + + let operational_amount_minus_reward_and_fee = + operational_amount.checked_sub(finished_job.reward + total_fees)?; + let new_job = JobQueue::add( &mut deps, Job { @@ -158,22 +169,23 @@ pub fn execute_job( vars: new_vars, recurring: finished_job.recurring, reward: finished_job.reward, + operational_amount: operational_amount_minus_reward_and_fee, assets_to_withdraw: finished_job.assets_to_withdraw.clone(), duration_days: finished_job.duration_days, created_at_time: Uint64::from(env.block.time.seconds()), - funding_account: finished_job.funding_account, + funding_account: finished_job.funding_account.clone(), }, )?; msgs.push(build_account_execute_generic_msgs( - job_account_addr.to_string(), + funding_account_addr.to_string(), vec![ - // Job owner's warp account sends fee to fee collector + // Job owner's funding account sends fee to fee collector build_transfer_native_funds_msg( config.fee_collector.to_string(), vec![Coin::new(total_fees.u128(), config.fee_denom.clone())], ), - // Job owner's warp account sends reward to controller + // Job owner's funding account sends reward to controller build_transfer_native_funds_msg( env.contract.address.to_string(), vec![Coin::new(new_job.reward.u128(), config.fee_denom.clone())], @@ -219,6 +231,14 @@ pub fn execute_job( job_account_addr.to_string(), new_job_id, )); + + // take funding account with new job + msgs.push(build_taken_account_msg( + config.job_account_tracker_address.to_string(), + finished_job.owner.to_string(), + funding_account_addr.to_string(), + new_job_id, + )); } } else { // No new job created, account has been free in execute_job, no need to free here again @@ -227,6 +247,14 @@ pub fn execute_job( job_account_addr.to_string(), finished_job.assets_to_withdraw, )); + + // withdraw all funds if funding acc exists + if let Some(acc) = finished_job.funding_account { + msgs.push(build_account_withdraw_assets_msg( + acc.to_string(), + vec![AssetInfo::Native(config.fee_denom)], + )); + } } Ok(Response::new() diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index b8f71ec1..a3cddd9e 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -126,6 +126,7 @@ impl JobQueue { vars: job.vars, recurring: job.recurring, reward: job.reward, + operational_amount: job.operational_amount, assets_to_withdraw: job.assets_to_withdraw, duration_days: job.duration_days, created_at_time: Uint64::from(env.block.time.seconds()), @@ -156,6 +157,7 @@ impl JobQueue { duration_days: job.duration_days, created_at_time: job.created_at_time, funding_account: job.funding_account, + operational_amount: job.operational_amount, }), }) } @@ -191,6 +193,7 @@ impl JobQueue { duration_days: job.duration_days, created_at_time: job.created_at_time, funding_account: job.funding_account, + operational_amount: job.operational_amount, }; FINISHED_JOBS().update(deps.storage, job_id, |j| match j { diff --git a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs index b6e74090..6ba7656c 100644 --- a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs +++ b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs @@ -3,8 +3,8 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use job_account_tracker::{ - Account, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FirstFreeAccountResponse, - InstantiateMsg, QueryMsg, + Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, InstantiateMsg, + QueryMsg, }; fn main() { @@ -18,7 +18,7 @@ fn main() { export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(Config), &out_dir); export_schema(&schema_for!(AccountsResponse), &out_dir); - export_schema(&schema_for!(FirstFreeAccountResponse), &out_dir); + export_schema(&schema_for!(AccountResponse), &out_dir); export_schema(&schema_for!(ConfigResponse), &out_dir); export_schema(&schema_for!(Account), &out_dir); } diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index 2563b660..d77bb23f 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -68,6 +68,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryFirstFreeAccount(data) => { to_binary(&query::account::query_first_free_account(deps, data)?) } + QueryMsg::QueryFreeAccount(data) => { + to_binary(&query::account::query_free_account(deps, data)?) + } } } diff --git a/contracts/warp-job-account-tracker/src/integration_tests.rs b/contracts/warp-job-account-tracker/src/integration_tests.rs index ca29f8d2..d6b0943e 100644 --- a/contracts/warp-job-account-tracker/src/integration_tests.rs +++ b/contracts/warp-job-account-tracker/src/integration_tests.rs @@ -4,7 +4,7 @@ mod tests { use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; use cw_multi_test::{App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor}; use job_account_tracker::{ - Account, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FirstFreeAccountResponse, + Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FreeAccountMsg, InstantiateMsg, QueryConfigMsg, QueryFirstFreeAccountMsg, QueryFreeAccountsMsg, QueryMsg, QueryTakenAccountsMsg, TakeAccountMsg, }; @@ -100,7 +100,7 @@ mod tests { account_owner_addr: USER_1.to_string(), }) ), - Ok(FirstFreeAccountResponse { account: None }) + Ok(AccountResponse { account: None }) ); assert_eq!( app.wrap().query_wasm_smart( @@ -190,7 +190,7 @@ mod tests { account_owner_addr: USER_1.to_string(), }) ), - Ok(FirstFreeAccountResponse { + Ok(AccountResponse { account: Some(Account { addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), taken_by_job_id: Some(DUMMY_JOB_1_ID) diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-job-account-tracker/src/query/account.rs index 602d28b7..c7d49444 100644 --- a/contracts/warp-job-account-tracker/src/query/account.rs +++ b/contracts/warp-job-account-tracker/src/query/account.rs @@ -4,8 +4,8 @@ use cw_storage_plus::{Bound, PrefixBound}; use crate::state::{CONFIG, FREE_ACCOUNTS, TAKEN_ACCOUNTS}; use job_account_tracker::{ - Account, AccountsResponse, ConfigResponse, FirstFreeAccountResponse, QueryFirstFreeAccountMsg, - QueryFreeAccountsMsg, QueryTakenAccountsMsg, + Account, AccountResponse, AccountsResponse, ConfigResponse, QueryFirstFreeAccountMsg, + QueryFreeAccountMsg, QueryFreeAccountsMsg, QueryTakenAccountsMsg, }; const QUERY_LIMIT: u32 = 50; @@ -18,7 +18,7 @@ pub fn query_config(deps: Deps) -> StdResult { pub fn query_first_free_account( deps: Deps, data: QueryFirstFreeAccountMsg, -) -> StdResult { +) -> StdResult { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let maybe_free_account = FREE_ACCOUNTS .prefix_range( @@ -35,7 +35,7 @@ pub fn query_first_free_account( }), _ => None, }; - Ok(FirstFreeAccountResponse { + Ok(AccountResponse { account: free_account, }) } @@ -116,3 +116,27 @@ pub fn query_free_accounts(deps: Deps, data: QueryFreeAccountsMsg) -> StdResult< accounts, }) } + +pub fn query_free_account(deps: Deps, data: QueryFreeAccountMsg) -> StdResult { + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + let maybe_free_account = FREE_ACCOUNTS + .prefix_range( + deps.storage, + Some(PrefixBound::inclusive(account_addr_ref)), + Some(PrefixBound::inclusive(account_addr_ref)), + Order::Ascending, + ) + .next(); + + let free_account = match maybe_free_account { + Some(Ok((account, last_job_id))) => Some(Account { + addr: account.1, + taken_by_job_id: Some(last_job_id), + }), + _ => None, + }; + + Ok(AccountResponse { + account: free_account, + }) +} diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 4c5aca97..c262b737 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -28,6 +28,7 @@ pub struct Job { pub duration_days: Uint64, pub created_at_time: Uint64, pub reward: Uint128, + pub operational_amount: Uint128, pub assets_to_withdraw: Vec, } @@ -64,6 +65,7 @@ pub struct CreateJobMsg { pub vars: String, pub recurring: bool, pub reward: Uint128, + pub operational_amount: Uint128, pub duration_days: Uint64, pub assets_to_withdraw: Option>, pub account_msgs: Option>, diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs index 7b58fcac..b0fe77d7 100644 --- a/packages/job-account-tracker/src/lib.rs +++ b/packages/job-account-tracker/src/lib.rs @@ -44,8 +44,10 @@ pub enum QueryMsg { QueryTakenAccounts(QueryTakenAccountsMsg), #[returns(AccountsResponse)] QueryFreeAccounts(QueryFreeAccountsMsg), - #[returns(FirstFreeAccountResponse)] + #[returns(AccountResponse)] QueryFirstFreeAccount(QueryFirstFreeAccountMsg), + #[returns(AccountResponse)] + QueryFreeAccount(QueryFreeAccountMsg), } #[cw_serde] @@ -88,7 +90,12 @@ pub struct QueryFirstFreeAccountMsg { } #[cw_serde] -pub struct FirstFreeAccountResponse { +pub struct QueryFreeAccountMsg { + pub account_addr: String, +} + +#[cw_serde] +pub struct AccountResponse { pub account: Option, } From 8c0a3e219b41e14c321c28fb7436854c05224e73 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 21 Nov 2023 18:07:46 +0100 Subject: [PATCH 082/133] funding account logic only when createJobMsg.recurring is true --- contracts/warp-controller/src/execute/job.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 9c8d9157..47e902bb 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -265,8 +265,7 @@ pub fn create_job( } } - // not sure if to check for small amounts here? - if !operational_amount_minus_reward_and_fee.is_zero() { + if data.recurring { match funding_account { None => { // Create funding account then create job in reply From edbb294e61537c8cc6dc1963fdfb926b25ab4440 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 21 Nov 2023 18:12:42 +0100 Subject: [PATCH 083/133] remove warp-funding-account contract --- Cargo.lock | 45 --- contracts/warp-funding-account/.cargo/config | 4 - contracts/warp-funding-account/.gitignore | 16 - contracts/warp-funding-account/Cargo.toml | 51 --- contracts/warp-funding-account/README.md | 106 ------ .../examples/warp-funding-account-schema.rs | 16 - contracts/warp-funding-account/meta/README.md | 16 - .../warp-funding-account/meta/appveyor.yml | 61 ---- .../meta/test_generate.sh | 37 -- .../warp-funding-account/src/contract.rs | 60 ---- contracts/warp-funding-account/src/error.rs | 64 ---- contracts/warp-funding-account/src/lib.rs | 8 - contracts/warp-funding-account/src/state.rs | 4 - contracts/warp-funding-account/src/tests.rs | 329 ------------------ packages/funding-account/.cargo/config | 4 - packages/funding-account/Cargo.toml | 29 -- packages/funding-account/README.md | 106 ------ .../examples/funding-account-schema.rs | 17 - packages/funding-account/src/lib.rs | 28 -- 19 files changed, 1001 deletions(-) delete mode 100644 contracts/warp-funding-account/.cargo/config delete mode 100644 contracts/warp-funding-account/.gitignore delete mode 100644 contracts/warp-funding-account/Cargo.toml delete mode 100644 contracts/warp-funding-account/README.md delete mode 100644 contracts/warp-funding-account/examples/warp-funding-account-schema.rs delete mode 100644 contracts/warp-funding-account/meta/README.md delete mode 100644 contracts/warp-funding-account/meta/appveyor.yml delete mode 100644 contracts/warp-funding-account/meta/test_generate.sh delete mode 100644 contracts/warp-funding-account/src/contract.rs delete mode 100644 contracts/warp-funding-account/src/error.rs delete mode 100644 contracts/warp-funding-account/src/lib.rs delete mode 100644 contracts/warp-funding-account/src/state.rs delete mode 100644 contracts/warp-funding-account/src/tests.rs delete mode 100644 packages/funding-account/.cargo/config delete mode 100644 packages/funding-account/Cargo.toml delete mode 100644 packages/funding-account/README.md delete mode 100644 packages/funding-account/examples/funding-account-schema.rs delete mode 100644 packages/funding-account/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2d124550..a5281143 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -487,28 +487,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" -[[package]] -name = "funding-account" -version = "0.1.0" -dependencies = [ - "controller", - "cosmwasm-schema", - "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", - "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", - "cw20", - "json-codec-wasm", - "prost 0.11.9", - "schemars", - "serde", - "strum", - "strum_macros", - "thiserror", -] - [[package]] name = "generic-array" version = "0.14.6" @@ -1041,29 +1019,6 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "warp-account" -version = "0.1.0" -dependencies = [ - "base64", - "controller", - "cosmwasm-schema", - "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", - "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw2 0.16.0", - "cw20", - "cw721", - "funding-account", - "json-codec-wasm", - "prost 0.11.9", - "schemars", - "serde-json-wasm 0.4.1", - "thiserror", -] - [[package]] name = "warp-controller" version = "0.1.0" diff --git a/contracts/warp-funding-account/.cargo/config b/contracts/warp-funding-account/.cargo/config deleted file mode 100644 index 752de1ab..00000000 --- a/contracts/warp-funding-account/.cargo/config +++ /dev/null @@ -1,4 +0,0 @@ -[alias] -wasm = "build --release --target wasm32-unknown-unknown" -unit-test = "test --lib" -schema = "run --example warp-funding-account-schema" diff --git a/contracts/warp-funding-account/.gitignore b/contracts/warp-funding-account/.gitignore deleted file mode 100644 index 9095deaa..00000000 --- a/contracts/warp-funding-account/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -# Build results -/target -/schema - -# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) -.cargo-ok - -# Text file backups -**/*.rs.bk - -# macOS -.DS_Store - -# IDEs -*.iml -.idea diff --git a/contracts/warp-funding-account/Cargo.toml b/contracts/warp-funding-account/Cargo.toml deleted file mode 100644 index 415cb322..00000000 --- a/contracts/warp-funding-account/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "warp-account" -version = "0.1.0" -authors = ["Terra Money "] -edition = "2021" - -exclude = [ - # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. - "contract.wasm", - "hash.txt", -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] -# use library feature to disable all instantiate/execute/query exports -library = [] - -[package.metadata.scripts] -optimize = """docker run --rm -v "${process.cwd()}":/code \ - -v "${path.join(process.cwd(), "../../", "packages")}":/packages \ - --mount type=volume,source="${contract}_cache",target=/code/target \ - --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/rust-optimizer${process.env.TERRARIUM_ARCH_ARM64 ? "-arm64" : ""}:0.12.6 -""" - -[dependencies] -cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" -cosmwasm-schema = "1.1" -base64 = "0.13.0" -cw-asset = "2.2" -cw-storage-plus = "0.16" -cw2 = "0.16" -cw20 = "0.16" -cw721 = "0.16.0" -controller = { path = "../../packages/controller", default-features = false, version = "*" } -funding-account = { path = "../../packages/funding-account", default-features = false, version = "*" } -schemars = "0.8" -thiserror = "1" -serde-json-wasm = "0.4.1" -json-codec-wasm = "0.1.0" -prost = "0.11.9" - -[dev-dependencies] -cw-multi-test = "0.16.0" diff --git a/contracts/warp-funding-account/README.md b/contracts/warp-funding-account/README.md deleted file mode 100644 index 954383af..00000000 --- a/contracts/warp-funding-account/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# CosmWasm Starter Pack - -This is a template to build smart contracts in Rust to run inside a -[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. -To understand the framework better, please read the overview in the -[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), -and dig into the [cosmwasm docs](https://www.cosmwasm.com). -This assumes you understand the theory and just want to get coding. - -## Creating a new repo from template - -Assuming you have a recent version of rust and cargo (v1.58.1+) installed -(via [rustup](https://rustup.rs/)), -then the following should get you a new repo to start a contract: - -Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. -Unless you did that before, run this line now: - -```sh -cargo install cargo-generate --features vendored-openssl -cargo install cargo-run-script -``` - -Now, use it to create your new contract. -Go to the folder in which you want to place it and run: - - -**Latest: 1.0.0-beta6** - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -```` - -**Older Version** - -Pass version as branch flag: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME -```` - -Example: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME -``` - -You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) -containing a simple working contract and build system that you can customize. - -## Create a Repo - -After generating, you have a initialized local git repo, but no commits, and no remote. -Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). -Then run the following: - -```sh -# this is needed to create a valid Cargo.lock file (see below) -cargo check -git branch -M main -git add . -git commit -m 'Initial Commit' -git remote add origin YOUR-GIT-URL -git push -u origin main -``` - -## CI Support - -We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) -and [Circle CI](.circleci/config.yml) in the generated project, so you can -get up and running with CI right away. - -One note is that the CI runs all `cargo` commands -with `--locked` to ensure it uses the exact same versions as you have locally. This also means -you must have an up-to-date `Cargo.lock` file, which is not auto-generated. -The first time you set up the project (or after adding any dep), you should ensure the -`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by -running `cargo check` or `cargo unit-test`. - -## Using your project - -Once you have your custom repo, you should check out [Developing](./Developing.md) to explain -more on how to run tests and develop code. Or go through the -[online tutorial](https://docs.cosmwasm.com/) to get a better feel -of how to develop. - -[Publishing](./Publishing.md) contains useful information on how to publish your contract -to the world, once you are ready to deploy it on a running blockchain. And -[Importing](./Importing.md) contains information about pulling in other contracts or crates -that have been published. - -Please replace this README file with information about your specific project. You can keep -the `Developing.md` and `Publishing.md` files as useful referenced, but please set some -proper description in the README. - -## Gitpod integration - -[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. - -Workspace contains: - - **rust**: for builds - - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client - - **jq**: shell JSON manipulation tool - -Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. - diff --git a/contracts/warp-funding-account/examples/warp-funding-account-schema.rs b/contracts/warp-funding-account/examples/warp-funding-account-schema.rs deleted file mode 100644 index 9d955c5a..00000000 --- a/contracts/warp-funding-account/examples/warp-funding-account-schema.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::env::current_dir; -use std::fs::create_dir_all; - -use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; -use funding_account::{Config, ExecuteMsg, InstantiateMsg}; - -fn main() { - let mut out_dir = current_dir().unwrap(); - out_dir.push("schema"); - create_dir_all(&out_dir).unwrap(); - remove_schemas(&out_dir).unwrap(); - - export_schema(&schema_for!(InstantiateMsg), &out_dir); - export_schema(&schema_for!(ExecuteMsg), &out_dir); - export_schema(&schema_for!(Config), &out_dir); -} diff --git a/contracts/warp-funding-account/meta/README.md b/contracts/warp-funding-account/meta/README.md deleted file mode 100644 index 279d1db4..00000000 --- a/contracts/warp-funding-account/meta/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# The meta folder - -This folder is ignored via the `.genignore` file. It contains meta files -that should not make it into the generated project. - -In particular, it is used for an AppVeyor CI script that runs on `cw-template` -itself (running the cargo-generate script, then testing the generated project). -The `.circleci` and `.github` directories contain scripts destined for any projects created from -this template. - -## Files - -- `appveyor.yml`: The AppVeyor CI configuration -- `test_generate.sh`: A script for generating a project from the template and - runnings builds and tests in it. This works almost like the CI script but - targets local UNIX-like dev environments. diff --git a/contracts/warp-funding-account/meta/appveyor.yml b/contracts/warp-funding-account/meta/appveyor.yml deleted file mode 100644 index 5da37f70..00000000 --- a/contracts/warp-funding-account/meta/appveyor.yml +++ /dev/null @@ -1,61 +0,0 @@ -# This CI configuration tests the cw-template repository itself, -# not the resulting project. We want to ensure that -# 1. the template to project generation works -# 2. the template files are up to date -# -# We chose Appveyor for this task as it allows us to use an arbitrary config -# location. Furthermore it allows us to ship Circle CI and GitHub Actions configs -# generated for the resulting project. - -image: Ubuntu - -environment: - TOOLCHAIN: 1.58.1 - -services: - - docker - -cache: - - $HOME/.rustup/ -> meta/appveyor.yml - # For details about cargo caching see https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci - - $HOME/.cargo/bin/ -> meta/appveyor.yml - - $HOME/.cargo/registry/index/ -> meta/appveyor.yml - - $HOME/.cargo/registry/cache/ -> meta/appveyor.yml - - $HOME/.cargo/git/db/ -> meta/appveyor.yml - -install: - - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain "$TOOLCHAIN" -y - - source $HOME/.cargo/env - - rustc --version - - cargo --version - - rustup target add wasm32-unknown-unknown - - cargo install --features vendored-openssl cargo-generate || true - -build_script: - # No matter what is currently checked out by the CI (main, other branch, PR merge commit), - # we create a temporary local branch from that point with a constant name, which we need for - # cargo generate. - - git branch current-ci-checkout - - cd .. - - cargo generate --git cw-template --name testgen-ci --branch current-ci-checkout - - cd testgen-ci - - ls -lA - - cargo fmt -- --check - - cargo unit-test - - cargo wasm - - cargo schema - - docker build --pull -t "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" . - - \[ "${APPVEYOR_REPO_BRANCH}" = "main" \] && image_tag=latest || image_tag=${APPVEYOR_REPO_TAG_NAME} - - docker tag "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" "cosmwasm/cw-gitpod-base:${image_tag}" - -on_success: - # publish docker image - - docker login --password-stdin -u "$DOCKER_USER" <<<"$DOCKER_PASS" - - docker push - - docker logout - -branches: -# whitelist long living branches and tags - only: - # - main - - /v\d+\.\d+\.\d+/ diff --git a/contracts/warp-funding-account/meta/test_generate.sh b/contracts/warp-funding-account/meta/test_generate.sh deleted file mode 100644 index b9aaa237..00000000 --- a/contracts/warp-funding-account/meta/test_generate.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -o errexit -o nounset -o pipefail -command -v shellcheck > /dev/null && shellcheck "$0" - -REPO_ROOT="$(realpath "$(dirname "$0")/..")" - -TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/cw-template.XXXXXXXXX") -PROJECT_NAME="testgen-local" - -( - echo "Navigating to $TMP_DIR" - cd "$TMP_DIR" - - GIT_BRANCH=$(git -C "$REPO_ROOT" branch --show-current) - - echo "Generating project from local repository (branch $GIT_BRANCH) ..." - cargo generate --git "$REPO_ROOT" --name "$PROJECT_NAME" --branch "$GIT_BRANCH" - - ( - cd "$PROJECT_NAME" - echo "This is what was generated" - ls -lA - - # Check formatting - echo "Checking formatting ..." - cargo fmt -- --check - - # Debug builds first to fail fast - echo "Running unit tests ..." - cargo unit-test - echo "Creating schema ..." - cargo schema - - echo "Building wasm ..." - cargo wasm - ) -) diff --git a/contracts/warp-funding-account/src/contract.rs b/contracts/warp-funding-account/src/contract.rs deleted file mode 100644 index 344c01ce..00000000 --- a/contracts/warp-funding-account/src/contract.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::state::CONFIG; -use crate::ContractError; -use controller::account::execute_warp_msgs; -use cosmwasm_std::{ - entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, -}; -use funding_account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn instantiate( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: InstantiateMsg, -) -> Result { - CONFIG.save( - deps.storage, - &Config { - owner: deps.api.addr_validate(&msg.owner)?, - warp_addr: info.sender, - }, - )?; - Ok(Response::new() - .add_attribute("action", "instantiate") - .add_attribute("contract_addr", env.contract.address) - .add_attribute("owner", msg.owner)) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.warp_addr { - return Err(ContractError::Unauthorized {}); - } - match msg { - ExecuteMsg::WarpMsgs(data) => { - execute_warp_msgs(deps, env, data, &config.owner).map_err(ContractError::Std) - } - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::Config => { - let config = CONFIG.load(deps.storage)?; - to_binary(&config) - } - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { - Ok(Response::new()) -} diff --git a/contracts/warp-funding-account/src/error.rs b/contracts/warp-funding-account/src/error.rs deleted file mode 100644 index f8855692..00000000 --- a/contracts/warp-funding-account/src/error.rs +++ /dev/null @@ -1,64 +0,0 @@ -use crate::ContractError::{DecodeError, DeserializationError, SerializationError}; -use cosmwasm_std::StdError; -use thiserror::Error; - -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("Unauthorized")] - Unauthorized {}, - - #[error("Invalid fee")] - InvalidFee {}, - - #[error("Funds array in message does not match funds array in job.")] - FundsMismatch {}, - - #[error("Reward provided is smaller than minimum")] - RewardTooSmall {}, - - #[error("Invalid arguments")] - InvalidArguments {}, - - #[error("Custom Error val: {val:?}")] - CustomError { val: String }, - // Add any other custom errors you like here. - // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. - #[error("Error deserializing data")] - DeserializationError {}, - - #[error("Error serializing data")] - SerializationError {}, - - #[error("Error decoding JSON result")] - DecodeError {}, - - #[error("Error resolving JSON path")] - ResolveError {}, -} - -impl From for ContractError { - fn from(_: serde_json_wasm::de::Error) -> Self { - DeserializationError {} - } -} - -impl From for ContractError { - fn from(_: serde_json_wasm::ser::Error) -> Self { - SerializationError {} - } -} - -impl From for ContractError { - fn from(_: json_codec_wasm::DecodeError) -> Self { - DecodeError {} - } -} - -impl From for ContractError { - fn from(_: base64::DecodeError) -> Self { - DecodeError {} - } -} diff --git a/contracts/warp-funding-account/src/lib.rs b/contracts/warp-funding-account/src/lib.rs deleted file mode 100644 index 90d6bfa8..00000000 --- a/contracts/warp-funding-account/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod contract; -mod error; -pub mod state; - -#[cfg(test)] -mod tests; - -pub use crate::error::ContractError; diff --git a/contracts/warp-funding-account/src/state.rs b/contracts/warp-funding-account/src/state.rs deleted file mode 100644 index 9ba4e512..00000000 --- a/contracts/warp-funding-account/src/state.rs +++ /dev/null @@ -1,4 +0,0 @@ -use cw_storage_plus::Item; -use funding_account::Config; - -pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/warp-funding-account/src/tests.rs b/contracts/warp-funding-account/src/tests.rs deleted file mode 100644 index 71af730f..00000000 --- a/contracts/warp-funding-account/src/tests.rs +++ /dev/null @@ -1,329 +0,0 @@ -use crate::contract::{execute, instantiate}; -use crate::ContractError; -use controller::account::{WarpMsg, WarpMsgs}; -use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::{ - to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, - IbcTimeoutBlock, Response, StakingMsg, Uint128, VoteOption, WasmMsg, -}; -use funding_account::{ExecuteMsg, InstantiateMsg}; - -#[test] -fn test_execute_controller() { - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info("vlad_controller", &[]); - - let _instantiate_res = instantiate( - deps.as_mut(), - env.clone(), - info.clone(), - InstantiateMsg { - owner: "vlad".to_string(), - }, - ); - - let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { - msgs: vec![ - WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - })), - WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - })), - WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes, - })), - WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - })), - WarpMsg::Generic(CosmosMsg::Distribution( - DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }, - )), - WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0, - }), - })), - WarpMsg::Generic(CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default(), - }), - ], - }); - - let execute_res = execute(deps.as_mut(), env, info, execute_msg).unwrap(); - - assert_eq!( - execute_res, - Response::new() - .add_attribute("action", "warp_msgs") - .add_messages(vec![ - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }] - }), - CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes - }), - CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0 - }), - }), - CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default() - } - ]) - ) -} - -#[test] -fn test_execute_owner() { - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info("vlad_controller", &[]); - - let _instantiate_res = instantiate( - deps.as_mut(), - env.clone(), - info, - InstantiateMsg { - owner: "vlad".to_string(), - }, - ); - let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { - msgs: vec![ - WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - })), - WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - })), - WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes, - })), - WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - })), - WarpMsg::Generic(CosmosMsg::Distribution( - DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }, - )), - WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0, - }), - })), - WarpMsg::Generic(CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default(), - }), - ], - }); - - let info2 = mock_info("vlad", &[]); - - let execute_res = execute(deps.as_mut(), env, info2, execute_msg).unwrap(); - - assert_eq!( - execute_res, - Response::new() - .add_attribute("action", "warp_msgs") - .add_messages(vec![ - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }] - }), - CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes - }), - CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0 - }), - }), - CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default() - } - ]) - ) -} - -#[test] -fn test_execute_unauth() { - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info("vlad_controller", &[]); - - let _instantiate_res = instantiate( - deps.as_mut(), - env.clone(), - info, - InstantiateMsg { - owner: "vlad".to_string(), - }, - ); - let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { - msgs: vec![ - WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - })), - WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - })), - WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes, - })), - WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - })), - WarpMsg::Generic(CosmosMsg::Distribution( - DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }, - )), - WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0, - }), - })), - WarpMsg::Generic(CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default(), - }), - ], - }); - - let info2 = mock_info("vlad2", &[]); - - let execute_res = execute(deps.as_mut(), env, info2, execute_msg).unwrap_err(); - - assert_eq!(execute_res, ContractError::Unauthorized {}) -} diff --git a/packages/funding-account/.cargo/config b/packages/funding-account/.cargo/config deleted file mode 100644 index 48a4663c..00000000 --- a/packages/funding-account/.cargo/config +++ /dev/null @@ -1,4 +0,0 @@ -[alias] -wasm = "build --release --target wasm32-unknown-unknown" -unit-test = "test --lib" -schema = "run --example funding-account-schema" diff --git a/packages/funding-account/Cargo.toml b/packages/funding-account/Cargo.toml deleted file mode 100644 index 73255c32..00000000 --- a/packages/funding-account/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "funding-account" -version = "0.1.0" -authors = ["Terra Money "] -edition = "2021" - -[features] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] - -[dependencies] -cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" -cosmwasm-schema = "1.1" -cw-asset = "2.2" -cw20 = "0.16" -cw-storage-plus = "0.16" -cw2 = "0.16" -schemars = "0.8" -serde = { version = "1", default-features = false, features = ["derive"] } -json-codec-wasm = "0.1.0" -strum = "0.24" -strum_macros = "0.24" -thiserror = { version = "1" } -controller = {path = "../controller"} -prost = "0.11.9" - -[dev-dependencies] -cw-multi-test = "0.16" diff --git a/packages/funding-account/README.md b/packages/funding-account/README.md deleted file mode 100644 index 954383af..00000000 --- a/packages/funding-account/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# CosmWasm Starter Pack - -This is a template to build smart contracts in Rust to run inside a -[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. -To understand the framework better, please read the overview in the -[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), -and dig into the [cosmwasm docs](https://www.cosmwasm.com). -This assumes you understand the theory and just want to get coding. - -## Creating a new repo from template - -Assuming you have a recent version of rust and cargo (v1.58.1+) installed -(via [rustup](https://rustup.rs/)), -then the following should get you a new repo to start a contract: - -Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. -Unless you did that before, run this line now: - -```sh -cargo install cargo-generate --features vendored-openssl -cargo install cargo-run-script -``` - -Now, use it to create your new contract. -Go to the folder in which you want to place it and run: - - -**Latest: 1.0.0-beta6** - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -```` - -**Older Version** - -Pass version as branch flag: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME -```` - -Example: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME -``` - -You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) -containing a simple working contract and build system that you can customize. - -## Create a Repo - -After generating, you have a initialized local git repo, but no commits, and no remote. -Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). -Then run the following: - -```sh -# this is needed to create a valid Cargo.lock file (see below) -cargo check -git branch -M main -git add . -git commit -m 'Initial Commit' -git remote add origin YOUR-GIT-URL -git push -u origin main -``` - -## CI Support - -We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) -and [Circle CI](.circleci/config.yml) in the generated project, so you can -get up and running with CI right away. - -One note is that the CI runs all `cargo` commands -with `--locked` to ensure it uses the exact same versions as you have locally. This also means -you must have an up-to-date `Cargo.lock` file, which is not auto-generated. -The first time you set up the project (or after adding any dep), you should ensure the -`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by -running `cargo check` or `cargo unit-test`. - -## Using your project - -Once you have your custom repo, you should check out [Developing](./Developing.md) to explain -more on how to run tests and develop code. Or go through the -[online tutorial](https://docs.cosmwasm.com/) to get a better feel -of how to develop. - -[Publishing](./Publishing.md) contains useful information on how to publish your contract -to the world, once you are ready to deploy it on a running blockchain. And -[Importing](./Importing.md) contains information about pulling in other contracts or crates -that have been published. - -Please replace this README file with information about your specific project. You can keep -the `Developing.md` and `Publishing.md` files as useful referenced, but please set some -proper description in the README. - -## Gitpod integration - -[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. - -Workspace contains: - - **rust**: for builds - - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client - - **jq**: shell JSON manipulation tool - -Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. - diff --git a/packages/funding-account/examples/funding-account-schema.rs b/packages/funding-account/examples/funding-account-schema.rs deleted file mode 100644 index b6ba6260..00000000 --- a/packages/funding-account/examples/funding-account-schema.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::env::current_dir; -use std::fs::create_dir_all; - -use controller::QueryMsg; -use controller::{ExecuteMsg, InstantiateMsg}; -use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; - -fn main() { - let mut out_dir = current_dir().unwrap(); - out_dir.push("schema"); - create_dir_all(&out_dir).unwrap(); - remove_schemas(&out_dir).unwrap(); - - export_schema(&schema_for!(InstantiateMsg), &out_dir); - export_schema(&schema_for!(ExecuteMsg), &out_dir); - export_schema(&schema_for!(QueryMsg), &out_dir); -} diff --git a/packages/funding-account/src/lib.rs b/packages/funding-account/src/lib.rs deleted file mode 100644 index 84eeb07f..00000000 --- a/packages/funding-account/src/lib.rs +++ /dev/null @@ -1,28 +0,0 @@ -use controller::account::WarpMsgs; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::Addr; - -#[cw_serde] -pub struct Config { - pub owner: Addr, - pub warp_addr: Addr, -} - -#[cw_serde] -pub struct InstantiateMsg { - pub owner: String, -} - -#[cw_serde] -#[allow(clippy::large_enum_variant)] -pub enum ExecuteMsg { - WarpMsgs(WarpMsgs), -} - -#[cw_serde] -pub enum QueryMsg { - Config, -} - -#[cw_serde] -pub struct MigrateMsg {} From f716cc8e27248933e261882a9ce3bd3abe7a9aeb Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 22 Nov 2023 17:00:57 +0100 Subject: [PATCH 084/133] funding account support in job-account-tracker - user can hold multiple funding accounts - jobs can have a single funding account attached - recurring jobs ad hoc create funding accounts - user can optionally provide one --- contracts/warp-controller/src/execute/job.rs | 50 +++++----- .../warp-controller/src/reply/account.rs | 6 +- contracts/warp-controller/src/reply/job.rs | 4 +- contracts/warp-controller/src/util/msg.rs | 44 ++++++++- .../warp-job-account-tracker/src/contract.rs | 17 ++++ .../warp-job-account-tracker/src/error.rs | 9 +- .../src/execute/account.rs | 99 ++++++++++++++++++- .../src/query/account.rs | 54 +++++++++- .../warp-job-account-tracker/src/state.rs | 9 +- packages/job-account-tracker/src/lib.rs | 55 +++++++++++ 10 files changed, 309 insertions(+), 38 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 47e902bb..80110d4c 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,5 +1,7 @@ use crate::state::{JobQueue, STATE}; -use crate::util::msg::build_account_execute_warp_msgs; +use crate::util::msg::{ + build_account_execute_warp_msgs, build_free_funding_account_msg, build_take_funding_account_msg, +}; use crate::ContractError; use controller::account::{AssetInfo, WarpMsgs}; use controller::job::{ @@ -24,7 +26,7 @@ use crate::{ }; use controller::{account::CwFund, Config}; -use job_account_tracker::{Account, AccountResponse, AccountsResponse}; +use job_account_tracker::{AccountResponse, FundingAccountResponse}; use resolver::QueryHydrateMsgsMsg; use super::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}; @@ -134,38 +136,40 @@ pub fn create_job( }, )?; - let free_accounts: AccountsResponse = deps.querier.query_wasm_smart( + let job_account_resp: AccountResponse = deps.querier.query_wasm_smart( job_account_tracker_address_ref, - &job_account_tracker::QueryMsg::QueryFreeAccounts( - job_account_tracker::QueryFreeAccountsMsg { + &job_account_tracker::QueryMsg::QueryFirstFreeAccount( + job_account_tracker::QueryFirstFreeAccountMsg { account_owner_addr: job_owner.to_string(), - start_after: None, - limit: Some(2), }, ), )?; - let job_account = free_accounts.accounts.get(0); - - let funding_account: Option; + let funding_account_resp: FundingAccountResponse; if let Some(funding_account_addr) = data.funding_account { // fetch funding account and check if it exists, throw otherwise - let funding_account_response: AccountResponse = deps.querier.query_wasm_smart( + funding_account_resp = deps.querier.query_wasm_smart( job_account_tracker_address_ref, - &job_account_tracker::QueryMsg::QueryFreeAccount( - job_account_tracker::QueryFreeAccountMsg { + &job_account_tracker::QueryMsg::QueryFundingAccount( + job_account_tracker::QueryFundingAccountMsg { account_addr: funding_account_addr.to_string(), + account_owner_addr: info.sender.to_string(), }, ), )?; - - funding_account = Some(funding_account_response.account.unwrap()); } else { - funding_account = free_accounts.accounts.get(1).cloned(); + funding_account_resp = deps.querier.query_wasm_smart( + job_account_tracker_address_ref, + &job_account_tracker::QueryMsg::QueryFirstFreeFundingAccount( + job_account_tracker::QueryFirstFreeFundingAccountMsg { + account_owner_addr: job_owner.to_string(), + }, + ), + )?; } - match job_account { + match job_account_resp.account { None => { // Create account then create job in reply submsgs.push(SubMsg { @@ -266,7 +270,7 @@ pub fn create_job( } if data.recurring { - match funding_account { + match funding_account_resp.funding_account { None => { // Create funding account then create job in reply submsgs.push(SubMsg { @@ -290,8 +294,8 @@ pub fn create_job( attrs.push(Attribute::new("action", "create_funding_account_and_job")); } Some(available_account) => { - let available_account_addr = &available_account.addr; - // Update job.account from placeholder value to job account + let available_account_addr = &available_account.account_addr; + // Update funding_account from placeholder value to funding account job.funding_account = Some(available_account_addr.clone()); JobQueue::sync(&mut deps, env, job.clone())?; @@ -305,7 +309,7 @@ pub fn create_job( )); // Take account - msgs.push(build_taken_account_msg( + msgs.push(build_take_funding_account_msg( config.job_account_tracker_address.to_string(), job_owner.to_string(), available_account_addr.to_string(), @@ -401,7 +405,7 @@ pub fn delete_job( )); if let Some(funding_account) = job.funding_account { - msgs.push(build_free_account_msg( + msgs.push(build_free_funding_account_msg( config.job_account_tracker_address.to_string(), job.owner.to_string(), funding_account.to_string(), @@ -555,7 +559,7 @@ pub fn execute_job( )); if let Some(funding_account) = job.funding_account { - msgs.push(build_free_account_msg( + msgs.push(build_free_funding_account_msg( config.job_account_tracker_address.to_string(), job.owner.to_string(), funding_account.to_string(), diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 8904dc75..3400efde 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -9,7 +9,7 @@ use crate::{ state::JobQueue, util::msg::{ build_account_execute_warp_msgs, build_taken_account_msg, build_transfer_cw20_msg, - build_transfer_cw721_msg, build_transfer_native_funds_msg, + build_transfer_cw721_msg, build_transfer_native_funds_msg, build_take_funding_account_msg, }, ContractError, }; @@ -226,8 +226,8 @@ pub fn create_funding_account_and_job( )) } - // Take job account - msgs.push(build_taken_account_msg( + // Take funding account + msgs.push(build_take_funding_account_msg( config.job_account_tracker_address.to_string(), job.owner.to_string(), funding_account_addr.to_string(), diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 4deaa93e..3da05a90 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -11,7 +11,7 @@ use crate::{ legacy_account::is_legacy_account, msg::{ build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_taken_account_msg, build_transfer_native_funds_msg, + build_taken_account_msg, build_transfer_native_funds_msg, build_take_funding_account_msg, }, }, ContractError, @@ -233,7 +233,7 @@ pub fn execute_job( )); // take funding account with new job - msgs.push(build_taken_account_msg( + msgs.push(build_take_funding_account_msg( config.job_account_tracker_address.to_string(), finished_job.owner.to_string(), funding_account_addr.to_string(), diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 214d7596..5fbe44f2 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -5,7 +5,9 @@ use controller::account::{ WithdrawAssetsMsg, }; use job_account::GenericMsg; -use job_account_tracker::{FreeAccountMsg, TakeAccountMsg}; +use job_account_tracker::{ + FreeAccountMsg, FreeFundingAccountMsg, TakeAccountMsg, TakeFundingAccountMsg, +}; #[allow(clippy::too_many_arguments)] pub fn build_instantiate_warp_account_msg( @@ -73,6 +75,46 @@ pub fn build_taken_account_msg( }) } +pub fn build_free_funding_account_msg( + job_account_tracker_addr: String, + account_owner_addr: String, + account_addr: String, + job_id: Uint64, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: job_account_tracker_addr, + msg: to_binary(&job_account_tracker::ExecuteMsg::FreeFundingAccount( + FreeFundingAccountMsg { + account_owner_addr, + account_addr, + job_id, + }, + )) + .unwrap(), + funds: vec![], + }) +} + +pub fn build_take_funding_account_msg( + job_account_tracker_addr: String, + account_owner_addr: String, + account_addr: String, + job_id: Uint64, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: job_account_tracker_addr, + msg: to_binary(&job_account_tracker::ExecuteMsg::TakeFundingAccount( + TakeFundingAccountMsg { + account_owner_addr, + account_addr, + job_id, + }, + )) + .unwrap(), + funds: vec![], + }) +} + pub fn build_transfer_cw20_msg( cw20_token_contract_addr: String, owner_addr: String, diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index d77bb23f..fb75da1b 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -52,6 +52,14 @@ pub fn execute( nonpayable(&info).unwrap(); execute::account::free_account(deps, data) } + ExecuteMsg::TakeFundingAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::take_funding_account(deps, data) + } + ExecuteMsg::FreeFundingAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::free_funding_account(deps, data) + } } } @@ -71,6 +79,15 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryFreeAccount(data) => { to_binary(&query::account::query_free_account(deps, data)?) } + QueryMsg::QueryFundingAccounts(data) => { + to_binary(&query::account::query_funding_accounts(deps, data)?) + } + QueryMsg::QueryFundingAccount(data) => { + to_binary(&query::account::query_funding_account(deps, data)?) + } + QueryMsg::QueryFirstFreeFundingAccount(data) => { + to_binary(&query::account::query_first_free_funding_account(deps, data)?) + } } } diff --git a/contracts/warp-job-account-tracker/src/error.rs b/contracts/warp-job-account-tracker/src/error.rs index 592558a5..65fa0305 100644 --- a/contracts/warp-job-account-tracker/src/error.rs +++ b/contracts/warp-job-account-tracker/src/error.rs @@ -38,14 +38,17 @@ pub enum ContractError { #[error("Error resolving JSON path")] ResolveError {}, - #[error("Sub account already taken")] + #[error("Account already taken")] AccountAlreadyTakenError {}, - #[error("Sub account already free")] + #[error("Account already free")] AccountAlreadyFreeError {}, - #[error("Sub account should be taken but it is free")] + #[error("Account should be taken but it is free")] AccountNotTakenError {}, + + #[error("Account not found")] + AccountNotFound {}, } impl From for ContractError { diff --git a/contracts/warp-job-account-tracker/src/execute/account.rs b/contracts/warp-job-account-tracker/src/execute/account.rs index 501bd114..a4051e3e 100644 --- a/contracts/warp-job-account-tracker/src/execute/account.rs +++ b/contracts/warp-job-account-tracker/src/execute/account.rs @@ -1,7 +1,9 @@ -use crate::state::{FREE_ACCOUNTS, TAKEN_ACCOUNTS}; +use crate::state::{FREE_ACCOUNTS, FUNDING_ACCOUNTS, TAKEN_ACCOUNTS, TAKEN_FUNDING_ACCOUNT_BY_JOB}; use crate::ContractError; use cosmwasm_std::{DepsMut, Response}; -use job_account_tracker::{FreeAccountMsg, TakeAccountMsg}; +use job_account_tracker::{ + FreeAccountMsg, FreeFundingAccountMsg, FundingAccount, TakeAccountMsg, TakeFundingAccountMsg, +}; pub fn taken_account(deps: DepsMut, data: TakeAccountMsg) -> Result { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; @@ -38,3 +40,96 @@ pub fn free_account(deps: DepsMut, data: FreeAccountMsg) -> Result Result { + let account_owner_addr_ref = deps.api.addr_validate(&data.account_owner_addr)?; + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + + TAKEN_FUNDING_ACCOUNT_BY_JOB.update(deps.storage, data.job_id.u64(), |s| match s { + // value is a dummy data because there is no built in support for set in cosmwasm + None => Ok(account_addr_ref.clone()), + Some(_) => Err(ContractError::AccountAlreadyTakenError {}), + })?; + + FUNDING_ACCOUNTS.update( + deps.storage, + &account_owner_addr_ref, + |accounts_opt| -> Result, ContractError> { + match accounts_opt { + None => { + // No funding accounts exist for this user, create a new vec + Ok(vec![FundingAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_ids: vec![data.job_id], + }]) + } + Some(mut accounts) => { + // Check if a funding account with the specified address already exists + if let Some(funding_account) = accounts + .iter_mut() + .find(|acc| acc.account_addr == account_addr_ref.clone()) + { + // Funding account exists, update its job_ids + funding_account.taken_by_job_ids.push(data.job_id); + } else { + // Funding account does not exist, add a new one + accounts.push(FundingAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_ids: vec![data.job_id], + }); + } + Ok(accounts) + } + } + }, + )?; + + Ok(Response::new() + .add_attribute("action", "take_funding_account") + .add_attribute("account_addr", data.account_addr) + .add_attribute("job_id", data.job_id.to_string())) +} + +pub fn free_funding_account( + deps: DepsMut, + data: FreeFundingAccountMsg, +) -> Result { + let account_owner_addr_ref = deps.api.addr_validate(&data.account_owner_addr)?; + let account_addr_ref = deps.api.addr_validate(&data.account_addr)?; + + TAKEN_FUNDING_ACCOUNT_BY_JOB.remove(deps.storage, data.job_id.u64()); + + FUNDING_ACCOUNTS.update( + deps.storage, + &account_owner_addr_ref, + |accounts_opt| -> Result, ContractError> { + match accounts_opt { + Some(mut accounts) => { + // Find the funding account with the specified address + if let Some(funding_account) = accounts + .iter_mut() + .find(|acc| acc.account_addr == account_addr_ref) + { + // Remove the specified job ID + funding_account + .taken_by_job_ids + .retain(|&id| id != data.job_id); + + Ok(accounts) + } else { + Err(ContractError::AccountNotFound {}) + } + } + None => Err(ContractError::AccountNotFound {}), + } + }, + )?; + + Ok(Response::new() + .add_attribute("action", "free_funding_account") + .add_attribute("account_addr", data.account_addr) + .add_attribute("job_id", data.job_id.to_string())) +} diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-job-account-tracker/src/query/account.rs index c7d49444..b6ab2250 100644 --- a/contracts/warp-job-account-tracker/src/query/account.rs +++ b/contracts/warp-job-account-tracker/src/query/account.rs @@ -1,11 +1,13 @@ use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::{Bound, PrefixBound}; -use crate::state::{CONFIG, FREE_ACCOUNTS, TAKEN_ACCOUNTS}; +use crate::state::{CONFIG, FREE_ACCOUNTS, FUNDING_ACCOUNTS, TAKEN_ACCOUNTS}; use job_account_tracker::{ - Account, AccountResponse, AccountsResponse, ConfigResponse, QueryFirstFreeAccountMsg, - QueryFreeAccountMsg, QueryFreeAccountsMsg, QueryTakenAccountsMsg, + Account, AccountResponse, AccountsResponse, ConfigResponse, FundingAccountResponse, + FundingAccountsResponse, QueryFirstFreeAccountMsg, QueryFirstFreeFundingAccountMsg, + QueryFreeAccountMsg, QueryFreeAccountsMsg, QueryFundingAccountMsg, QueryFundingAccountsMsg, + QueryTakenAccountsMsg, }; const QUERY_LIMIT: u32 = 50; @@ -140,3 +142,49 @@ pub fn query_free_account(deps: Deps, data: QueryFreeAccountMsg) -> StdResult StdResult { + let account_addr_ref = deps.api.addr_validate(data.account_addr.as_str())?; + let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; + + let funding_accounts = FUNDING_ACCOUNTS.load(deps.storage, &account_owner_addr_ref)?; + + Ok(FundingAccountResponse { + funding_account: funding_accounts + .iter() + .find(|fa| fa.account_addr == account_addr_ref.clone()) + .cloned(), + }) +} + +pub fn query_funding_accounts( + deps: Deps, + data: QueryFundingAccountsMsg, +) -> StdResult { + let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; + + let funding_accounts = FUNDING_ACCOUNTS.load(deps.storage, &account_owner_addr_ref)?; + + Ok(FundingAccountsResponse { funding_accounts }) +} + +pub fn query_first_free_funding_account( + deps: Deps, + data: QueryFirstFreeFundingAccountMsg, +) -> StdResult { + let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; + + let funding_accounts = FUNDING_ACCOUNTS.load(deps.storage, &account_owner_addr_ref)?; + + let funding_account = funding_accounts + .iter() + .find(|fa| fa.taken_by_job_ids.is_empty()) + .cloned(); + + Ok(FundingAccountResponse { funding_account }) +} diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-job-account-tracker/src/state.rs index 5dca505e..8a34b7ee 100644 --- a/contracts/warp-job-account-tracker/src/state.rs +++ b/contracts/warp-job-account-tracker/src/state.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Addr, Uint64}; use cw_storage_plus::{Item, Map}; -use job_account_tracker::Config; +use job_account_tracker::{Config, FundingAccount}; pub const CONFIG: Item = Item::new("config"); @@ -9,3 +9,10 @@ pub const TAKEN_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("taken_accounts // Key is the (account owner address, account address), value is id of the last job which reserved it pub const FREE_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("free_accounts"); + +// owner address -> funding_account[] +// - user can have multiple funding accounts +// - a job can be assigned to only one funding account +// - funding account can fund multiple jobs +pub const FUNDING_ACCOUNTS: Map<&Addr, Vec> = Map::new("funding_accounts"); +pub const TAKEN_FUNDING_ACCOUNT_BY_JOB: Map = Map::new("funding_account_by_job"); \ No newline at end of file diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs index b0fe77d7..b7b4bb7e 100644 --- a/packages/job-account-tracker/src/lib.rs +++ b/packages/job-account-tracker/src/lib.rs @@ -19,6 +19,8 @@ pub struct InstantiateMsg { pub enum ExecuteMsg { TakeAccount(TakeAccountMsg), FreeAccount(FreeAccountMsg), + TakeFundingAccount(TakeFundingAccountMsg), + FreeFundingAccount(FreeFundingAccountMsg), } #[cw_serde] @@ -35,6 +37,21 @@ pub struct FreeAccountMsg { pub last_job_id: Uint64, } +#[cw_serde] +pub struct TakeFundingAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, + pub job_id: Uint64, +} + +#[cw_serde] +pub struct FreeFundingAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, + pub job_id: Uint64, +} + + #[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg { @@ -48,6 +65,12 @@ pub enum QueryMsg { QueryFirstFreeAccount(QueryFirstFreeAccountMsg), #[returns(AccountResponse)] QueryFreeAccount(QueryFreeAccountMsg), + #[returns(FundingAccountResponse)] + QueryFirstFreeFundingAccount(QueryFirstFreeFundingAccountMsg), + #[returns(FundingAccountsResponse)] + QueryFundingAccounts(QueryFundingAccountsMsg), + #[returns(FundingAccountResponse)] + QueryFundingAccount(QueryFundingAccountMsg), } #[cw_serde] @@ -78,6 +101,12 @@ pub struct Account { pub taken_by_job_id: Option, } +#[cw_serde] +pub struct FundingAccount { + pub account_addr: Addr, + pub taken_by_job_ids: Vec, // List of job IDs using this account +} + #[cw_serde] pub struct AccountsResponse { pub accounts: Vec, @@ -94,10 +123,36 @@ pub struct QueryFreeAccountMsg { pub account_addr: String, } +#[cw_serde] +pub struct QueryFirstFreeFundingAccountMsg { + pub account_owner_addr: String, +} + +#[cw_serde] +pub struct QueryFundingAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, +} + +#[cw_serde] +pub struct QueryFundingAccountsMsg { + pub account_owner_addr: String, +} + +#[cw_serde] +pub struct FundingAccountsResponse { + pub funding_accounts: Vec, +} + #[cw_serde] pub struct AccountResponse { pub account: Option, } +#[cw_serde] +pub struct FundingAccountResponse { + pub funding_account: Option, +} + #[cw_serde] pub struct MigrateMsg {} From a9c746b1d97caa83075140806f04664288631cf1 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 22 Nov 2023 17:05:38 +0100 Subject: [PATCH 085/133] fmt --- contracts/warp-controller/src/reply/account.rs | 4 ++-- contracts/warp-controller/src/reply/job.rs | 3 ++- contracts/warp-job-account-tracker/src/contract.rs | 6 +++--- contracts/warp-job-account-tracker/src/state.rs | 2 +- packages/job-account-tracker/src/lib.rs | 1 - 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 3400efde..3031f548 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -8,8 +8,8 @@ use controller::{ use crate::{ state::JobQueue, util::msg::{ - build_account_execute_warp_msgs, build_taken_account_msg, build_transfer_cw20_msg, - build_transfer_cw721_msg, build_transfer_native_funds_msg, build_take_funding_account_msg, + build_account_execute_warp_msgs, build_take_funding_account_msg, build_taken_account_msg, + build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, }, ContractError, }; diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 3da05a90..af81cc13 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -11,7 +11,8 @@ use crate::{ legacy_account::is_legacy_account, msg::{ build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_taken_account_msg, build_transfer_native_funds_msg, build_take_funding_account_msg, + build_take_funding_account_msg, build_taken_account_msg, + build_transfer_native_funds_msg, }, }, ContractError, diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index fb75da1b..610c9dae 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -85,9 +85,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryFundingAccount(data) => { to_binary(&query::account::query_funding_account(deps, data)?) } - QueryMsg::QueryFirstFreeFundingAccount(data) => { - to_binary(&query::account::query_first_free_funding_account(deps, data)?) - } + QueryMsg::QueryFirstFreeFundingAccount(data) => to_binary( + &query::account::query_first_free_funding_account(deps, data)?, + ), } } diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-job-account-tracker/src/state.rs index 8a34b7ee..78693781 100644 --- a/contracts/warp-job-account-tracker/src/state.rs +++ b/contracts/warp-job-account-tracker/src/state.rs @@ -15,4 +15,4 @@ pub const FREE_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("free_accounts") // - a job can be assigned to only one funding account // - funding account can fund multiple jobs pub const FUNDING_ACCOUNTS: Map<&Addr, Vec> = Map::new("funding_accounts"); -pub const TAKEN_FUNDING_ACCOUNT_BY_JOB: Map = Map::new("funding_account_by_job"); \ No newline at end of file +pub const TAKEN_FUNDING_ACCOUNT_BY_JOB: Map = Map::new("funding_account_by_job"); diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs index b7b4bb7e..ac4cc024 100644 --- a/packages/job-account-tracker/src/lib.rs +++ b/packages/job-account-tracker/src/lib.rs @@ -51,7 +51,6 @@ pub struct FreeFundingAccountMsg { pub job_id: Uint64, } - #[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg { From f1be2f7d4b2a1aae3d8b928a201f69b1ede7f209 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 23 Nov 2023 16:54:34 +0100 Subject: [PATCH 086/133] add create funding account flow --- contracts/warp-controller/src/contract.rs | 35 +++++---- .../warp-controller/src/execute/account.rs | 32 ++++++++ contracts/warp-controller/src/execute/mod.rs | 1 + .../warp-controller/src/reply/account.rs | 75 ++++++++++++++++++- contracts/warp-controller/src/util/msg.rs | 21 +++++- .../warp-job-account-tracker/src/contract.rs | 5 ++ .../src/execute/account.rs | 52 ++++++++++++- .../src/query/account.rs | 8 +- .../warp-job-account-tracker/src/state.rs | 3 +- packages/controller/src/lib.rs | 28 ++++--- packages/job-account-tracker/src/lib.rs | 7 ++ 11 files changed, 226 insertions(+), 41 deletions(-) create mode 100644 contracts/warp-controller/src/execute/account.rs diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 17521acc..b4b0da88 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -99,51 +99,49 @@ pub fn execute( .unwrap() .amount; - execute::job::create_job(deps, env, info, *data, config, fee_denom_paid_amount) + execute::job::create_job(deps, env, info, data, config, fee_denom_paid_amount) } ExecuteMsg::DeleteJob(data) => { let fee_denom_paid_amount = must_pay(&info, &config.fee_denom).unwrap(); - execute::job::delete_job(deps, env, info, *data, config, fee_denom_paid_amount) + execute::job::delete_job(deps, env, info, data, config, fee_denom_paid_amount) } - ExecuteMsg::UpdateJob(data) => execute::job::update_job(deps, env, info, *data), + ExecuteMsg::UpdateJob(data) => execute::job::update_job(deps, env, info, data), ExecuteMsg::ExecuteJob(data) => { nonpayable(&info).unwrap(); - execute::job::execute_job(deps, env, info, *data, config) + execute::job::execute_job(deps, env, info, data, config) } ExecuteMsg::EvictJob(data) => { nonpayable(&info).unwrap(); - execute::job::evict_job(deps, env, info, *data, config) + execute::job::evict_job(deps, env, info, data, config) } ExecuteMsg::UpdateConfig(data) => { nonpayable(&info).unwrap(); - execute::controller::update_config(deps, env, info, *data, config) + execute::controller::update_config(deps, env, info, data, config) } ExecuteMsg::MigrateLegacyAccounts(data) => { nonpayable(&info).unwrap(); - migrate::legacy_account::migrate_legacy_accounts(deps, info, *data, config) + migrate::legacy_account::migrate_legacy_accounts(deps, info, data, config) } ExecuteMsg::MigrateFreeJobAccounts(data) => { nonpayable(&info).unwrap(); - migrate::job_account::migrate_free_job_accounts(deps.as_ref(), env, info, *data, config) + migrate::job_account::migrate_free_job_accounts(deps.as_ref(), env, info, data, config) } ExecuteMsg::MigrateTakenJobAccounts(data) => { nonpayable(&info).unwrap(); - migrate::job_account::migrate_taken_job_accounts( - deps.as_ref(), - env, - info, - *data, - config, - ) + migrate::job_account::migrate_taken_job_accounts(deps.as_ref(), env, info, data, config) } ExecuteMsg::MigratePendingJobs(data) => { nonpayable(&info).unwrap(); - migrate::job::migrate_pending_jobs(deps, env, info, *data) + migrate::job::migrate_pending_jobs(deps, env, info, data) } ExecuteMsg::MigrateFinishedJobs(data) => { nonpayable(&info).unwrap(); - migrate::job::migrate_finished_jobs(deps, env, info, *data) + migrate::job::migrate_finished_jobs(deps, env, info, data) + } + + ExecuteMsg::CreateFundingAccount(data) => { + execute::account::create_funding_account(deps, env, info, data) } } } @@ -184,11 +182,12 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { let config = CONFIG.load(deps.storage)?; - // 0-1 reserved + // 0-2 reserved match msg.id { // use 0 as hack to call create_job_account_and_job 0 => reply::account::create_job_account_and_job(deps, env, msg, config), 1 => reply::account::create_funding_account_and_job(deps, env, msg, config), + 2 => reply::account::create_funding_account(deps, env, msg, config), _id => reply::job::execute_job(deps, env, msg, config), } } diff --git a/contracts/warp-controller/src/execute/account.rs b/contracts/warp-controller/src/execute/account.rs new file mode 100644 index 00000000..d192641e --- /dev/null +++ b/contracts/warp-controller/src/execute/account.rs @@ -0,0 +1,32 @@ +use controller::CreateFundingAccountMsg; +use cosmwasm_std::{DepsMut, Env, MessageInfo, ReplyOn, Response, SubMsg, Uint64}; + +use crate::{state::CONFIG, util::msg::build_instantiate_warp_account_msg, ContractError}; + +pub fn create_funding_account( + deps: DepsMut, + env: Env, + info: MessageInfo, + _data: CreateFundingAccountMsg, +) -> Result { + let config = CONFIG.load(deps.storage)?; + + let submsgs = vec![SubMsg { + id: 2, + msg: build_instantiate_warp_account_msg( + Uint64::from(0u64), // placeholder + env.contract.address.to_string(), + config.warp_account_code_id.u64(), + info.sender.to_string(), + info.funds, + None, + None, + ), + gas_limit: None, + reply_on: ReplyOn::Always, + }]; + + Ok(Response::new() + .add_attribute("action", "create_funding_account") + .add_submessages(submsgs)) +} diff --git a/contracts/warp-controller/src/execute/mod.rs b/contracts/warp-controller/src/execute/mod.rs index 987e7319..e4a83744 100644 --- a/contracts/warp-controller/src/execute/mod.rs +++ b/contracts/warp-controller/src/execute/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod account; pub(crate) mod controller; pub(crate) mod fee; pub(crate) mod job; diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 3031f548..6f5b1333 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -8,8 +8,9 @@ use controller::{ use crate::{ state::JobQueue, util::msg::{ - build_account_execute_warp_msgs, build_take_funding_account_msg, build_taken_account_msg, - build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, + build_account_execute_warp_msgs, build_add_funding_account_msg, + build_take_funding_account_msg, build_taken_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, build_transfer_native_funds_msg, }, ContractError, }; @@ -242,3 +243,73 @@ pub fn create_funding_account_and_job( .add_attribute("funding_account_address", funding_account_addr) .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) } + +pub fn create_funding_account( + deps: DepsMut, + _env: Env, + msg: Reply, + config: Config, +) -> Result { + let reply = msg.result.into_result().map_err(StdError::generic_err)?; + + let funding_account_event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "instantiate") + }) + .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + + let owner = funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "owner") + .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? + .value; + + let funding_account_addr = deps.api.addr_validate( + &funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "contract_addr") + .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? + .value, + )?; + + let native_funds: Vec = serde_json_wasm::from_str( + &funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "native_funds") + .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? + .value, + )?; + + let mut msgs: Vec = vec![]; + + if !native_funds.is_empty() { + // Fund account in native coins + msgs.push(build_transfer_native_funds_msg( + funding_account_addr.to_string(), + native_funds, + )) + } + + msgs.push(build_add_funding_account_msg( + config.job_account_tracker_address.to_string(), + owner.to_string(), + funding_account_addr.to_string(), + )); + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "create_job_account_reply") + .add_attribute("owner", owner) + .add_attribute("funding_account_address", funding_account_addr)) +} diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 5fbe44f2..70f7a25d 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -6,7 +6,8 @@ use controller::account::{ }; use job_account::GenericMsg; use job_account_tracker::{ - FreeAccountMsg, FreeFundingAccountMsg, TakeAccountMsg, TakeFundingAccountMsg, + AddFundingAccountMsg, FreeAccountMsg, FreeFundingAccountMsg, TakeAccountMsg, + TakeFundingAccountMsg, }; #[allow(clippy::too_many_arguments)] @@ -115,6 +116,24 @@ pub fn build_take_funding_account_msg( }) } +pub fn build_add_funding_account_msg( + job_account_tracker_addr: String, + account_owner_addr: String, + account_addr: String, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: job_account_tracker_addr, + msg: to_binary(&job_account_tracker::ExecuteMsg::AddFundingAccount( + AddFundingAccountMsg { + account_owner_addr, + account_addr, + }, + )) + .unwrap(), + funds: vec![], + }) +} + pub fn build_transfer_cw20_msg( cw20_token_contract_addr: String, owner_addr: String, diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index 610c9dae..e71f1b8e 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -43,6 +43,7 @@ pub fn execute( if info.sender != config.admin && info.sender != config.warp_addr { return Err(ContractError::Unauthorized {}); } + match msg { ExecuteMsg::TakeAccount(data) => { nonpayable(&info).unwrap(); @@ -60,6 +61,10 @@ pub fn execute( nonpayable(&info).unwrap(); execute::account::free_funding_account(deps, data) } + ExecuteMsg::AddFundingAccount(data) => { + nonpayable(&info).unwrap(); + execute::account::add_funding_account(deps, data) + } } } diff --git a/contracts/warp-job-account-tracker/src/execute/account.rs b/contracts/warp-job-account-tracker/src/execute/account.rs index a4051e3e..c673d820 100644 --- a/contracts/warp-job-account-tracker/src/execute/account.rs +++ b/contracts/warp-job-account-tracker/src/execute/account.rs @@ -1,8 +1,11 @@ -use crate::state::{FREE_ACCOUNTS, FUNDING_ACCOUNTS, TAKEN_ACCOUNTS, TAKEN_FUNDING_ACCOUNT_BY_JOB}; +use crate::state::{ + FREE_ACCOUNTS, FUNDING_ACCOUNTS_BY_USER, TAKEN_ACCOUNTS, TAKEN_FUNDING_ACCOUNT_BY_JOB, +}; use crate::ContractError; use cosmwasm_std::{DepsMut, Response}; use job_account_tracker::{ - FreeAccountMsg, FreeFundingAccountMsg, FundingAccount, TakeAccountMsg, TakeFundingAccountMsg, + AddFundingAccountMsg, FreeAccountMsg, FreeFundingAccountMsg, FundingAccount, TakeAccountMsg, + TakeFundingAccountMsg, }; pub fn taken_account(deps: DepsMut, data: TakeAccountMsg) -> Result { @@ -54,7 +57,7 @@ pub fn take_funding_account( Some(_) => Err(ContractError::AccountAlreadyTakenError {}), })?; - FUNDING_ACCOUNTS.update( + FUNDING_ACCOUNTS_BY_USER.update( deps.storage, &account_owner_addr_ref, |accounts_opt| -> Result, ContractError> { @@ -102,7 +105,7 @@ pub fn free_funding_account( TAKEN_FUNDING_ACCOUNT_BY_JOB.remove(deps.storage, data.job_id.u64()); - FUNDING_ACCOUNTS.update( + FUNDING_ACCOUNTS_BY_USER.update( deps.storage, &account_owner_addr_ref, |accounts_opt| -> Result, ContractError> { @@ -133,3 +136,44 @@ pub fn free_funding_account( .add_attribute("account_addr", data.account_addr) .add_attribute("job_id", data.job_id.to_string())) } + +pub fn add_funding_account( + deps: DepsMut, + data: AddFundingAccountMsg, +) -> Result { + let account_owner_addr_ref = deps.api.addr_validate(&data.account_owner_addr)?; + let account_addr_ref = deps.api.addr_validate(&data.account_addr)?; + + FUNDING_ACCOUNTS_BY_USER.update( + deps.storage, + &account_owner_addr_ref, + |accounts_opt| -> Result, ContractError> { + match accounts_opt { + Some(mut accounts) => { + if accounts + .iter_mut() + .any(|acc| acc.account_addr == account_addr_ref.clone()) + { + // account already exists, do nothing + Ok(accounts) + } else { + accounts.push(FundingAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_ids: vec![], + }); + + Ok(accounts) + } + } + None => Ok(vec![FundingAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_ids: vec![], + }]), + } + }, + )?; + + Ok(Response::new() + .add_attribute("action", "add_funding_account") + .add_attribute("account_addr", data.account_addr)) +} diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-job-account-tracker/src/query/account.rs index b6ab2250..d6a483af 100644 --- a/contracts/warp-job-account-tracker/src/query/account.rs +++ b/contracts/warp-job-account-tracker/src/query/account.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::{Bound, PrefixBound}; -use crate::state::{CONFIG, FREE_ACCOUNTS, FUNDING_ACCOUNTS, TAKEN_ACCOUNTS}; +use crate::state::{CONFIG, FREE_ACCOUNTS, FUNDING_ACCOUNTS_BY_USER, TAKEN_ACCOUNTS}; use job_account_tracker::{ Account, AccountResponse, AccountsResponse, ConfigResponse, FundingAccountResponse, @@ -152,7 +152,7 @@ pub fn query_funding_account( let account_addr_ref = deps.api.addr_validate(data.account_addr.as_str())?; let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; - let funding_accounts = FUNDING_ACCOUNTS.load(deps.storage, &account_owner_addr_ref)?; + let funding_accounts = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref)?; Ok(FundingAccountResponse { funding_account: funding_accounts @@ -168,7 +168,7 @@ pub fn query_funding_accounts( ) -> StdResult { let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; - let funding_accounts = FUNDING_ACCOUNTS.load(deps.storage, &account_owner_addr_ref)?; + let funding_accounts = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref)?; Ok(FundingAccountsResponse { funding_accounts }) } @@ -179,7 +179,7 @@ pub fn query_first_free_funding_account( ) -> StdResult { let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; - let funding_accounts = FUNDING_ACCOUNTS.load(deps.storage, &account_owner_addr_ref)?; + let funding_accounts = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref)?; let funding_account = funding_accounts .iter() diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-job-account-tracker/src/state.rs index 78693781..4409599a 100644 --- a/contracts/warp-job-account-tracker/src/state.rs +++ b/contracts/warp-job-account-tracker/src/state.rs @@ -14,5 +14,6 @@ pub const FREE_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("free_accounts") // - user can have multiple funding accounts // - a job can be assigned to only one funding account // - funding account can fund multiple jobs -pub const FUNDING_ACCOUNTS: Map<&Addr, Vec> = Map::new("funding_accounts"); +pub const FUNDING_ACCOUNTS_BY_USER: Map<&Addr, Vec> = + Map::new("funding_accounts_by_user"); pub const TAKEN_FUNDING_ACCOUNT_BY_JOB: Map = Map::new("funding_account_by_job"); diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 29f65654..bfca0117 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -90,21 +90,24 @@ pub struct InstantiateMsg { //execute #[cw_serde] +#[allow(clippy::large_enum_variant)] pub enum ExecuteMsg { - CreateJob(Box), - DeleteJob(Box), - UpdateJob(Box), - ExecuteJob(Box), - EvictJob(Box), + CreateJob(CreateJobMsg), + DeleteJob(DeleteJobMsg), + UpdateJob(UpdateJobMsg), + ExecuteJob(ExecuteJobMsg), + EvictJob(EvictJobMsg), - UpdateConfig(Box), + UpdateConfig(UpdateConfigMsg), - MigrateLegacyAccounts(Box), - MigrateFreeJobAccounts(Box), - MigrateTakenJobAccounts(Box), + MigrateLegacyAccounts(MigrateLegacyAccountsMsg), + MigrateFreeJobAccounts(MigrateJobAccountsMsg), + MigrateTakenJobAccounts(MigrateJobAccountsMsg), - MigratePendingJobs(Box), - MigrateFinishedJobs(Box), + MigratePendingJobs(MigrateJobsMsg), + MigrateFinishedJobs(MigrateJobsMsg), + + CreateFundingAccount(CreateFundingAccountMsg), } #[cw_serde] @@ -154,6 +157,9 @@ pub struct MigrateJobsMsg { pub limit: u8, } +#[cw_serde] +pub struct CreateFundingAccountMsg {} + //query #[derive(QueryResponses)] #[cw_serde] diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs index ac4cc024..d181dc27 100644 --- a/packages/job-account-tracker/src/lib.rs +++ b/packages/job-account-tracker/src/lib.rs @@ -21,6 +21,7 @@ pub enum ExecuteMsg { FreeAccount(FreeAccountMsg), TakeFundingAccount(TakeFundingAccountMsg), FreeFundingAccount(FreeFundingAccountMsg), + AddFundingAccount(AddFundingAccountMsg), } #[cw_serde] @@ -51,6 +52,12 @@ pub struct FreeFundingAccountMsg { pub job_id: Uint64, } +#[cw_serde] +pub struct AddFundingAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, +} + #[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg { From cfeb9a14ca0341b95a07d16b59a5d39ce0a81a51 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 23 Nov 2023 17:10:32 +0100 Subject: [PATCH 087/133] fix operational amount deduction from native funds when funding job account --- contracts/warp-controller/src/execute/job.rs | 15 +++++++-------- contracts/warp-controller/src/util/fee.rs | 6 +++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 80110d4c..7025a848 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -15,7 +15,7 @@ use cosmwasm_std::{ use crate::{ state::LEGACY_ACCOUNTS, util::{ - fee::deduct_reward_and_fee_from_native_funds, + fee::deduct_from_native_funds, legacy_account::is_legacy_account, msg::{ build_account_withdraw_assets_msg, build_free_account_msg, @@ -73,16 +73,15 @@ pub fn create_job( let total_fees = creation_fee + maintenance_fee + burn_fee; - let reward_plus_fee = data.reward + total_fees; - if reward_plus_fee > fee_denom_paid_amount { + if data.operational_amount > fee_denom_paid_amount { return Err(ContractError::InsufficientFundsToPayForRewardAndFee {}); } // Reward and fee will always be in native denom - let native_funds_minus_reward_and_fee = deduct_reward_and_fee_from_native_funds( + let native_funds_minus_operational_amount = deduct_from_native_funds( info.funds.clone(), config.fee_denom.clone(), - reward_plus_fee, + data.operational_amount, ); let mut submsgs = vec![]; @@ -179,7 +178,7 @@ pub fn create_job( env.contract.address.to_string(), config.warp_account_code_id.u64(), info.sender.to_string(), - native_funds_minus_reward_and_fee, + native_funds_minus_operational_amount, data.cw_funds, data.account_msgs, ), @@ -195,11 +194,11 @@ pub fn create_job( job.account = available_account_addr.clone(); JobQueue::sync(&mut deps, env.clone(), job.clone())?; - if !native_funds_minus_reward_and_fee.is_empty() { + if !native_funds_minus_operational_amount.is_empty() { // Fund account in native coins msgs.push(build_transfer_native_funds_msg( available_account_addr.to_string(), - native_funds_minus_reward_and_fee, + native_funds_minus_operational_amount, )) } diff --git a/contracts/warp-controller/src/util/fee.rs b/contracts/warp-controller/src/util/fee.rs index 280b7f84..d507819f 100644 --- a/contracts/warp-controller/src/util/fee.rs +++ b/contracts/warp-controller/src/util/fee.rs @@ -1,12 +1,12 @@ use cosmwasm_std::{Coin, Uint128}; -pub fn deduct_reward_and_fee_from_native_funds( +pub fn deduct_from_native_funds( funds: Vec, fee_denom: String, - reward_plus_fee: Uint128, + deducted: Uint128, ) -> Vec { let mut funds = funds; - let mut deducted_amount = reward_plus_fee; + let mut deducted_amount = deducted; for fund in funds.iter_mut() { if fund.denom == fee_denom { fund.amount = fund.amount.checked_sub(deducted_amount).unwrap(); From 017411193ffd28268c9ea19d721c83b26e1e9cc9 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 23 Nov 2023 17:14:57 +0100 Subject: [PATCH 088/133] remove extra transfering of native funds in reply --- .../warp-controller/src/reply/account.rs | 48 ++++--------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 6f5b1333..7b83a3a6 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -10,7 +10,7 @@ use crate::{ util::msg::{ build_account_execute_warp_msgs, build_add_funding_account_msg, build_take_funding_account_msg, build_taken_account_msg, build_transfer_cw20_msg, - build_transfer_cw721_msg, build_transfer_native_funds_msg, + build_transfer_cw721_msg, }, ContractError, }; @@ -67,7 +67,7 @@ pub fn create_job_account_and_job( .iter() .cloned() .find(|attr| attr.key == "native_funds") - .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? + .ok_or_else(|| StdError::generic_err("cannot find `native_funds` attribute"))? .value, )?; @@ -97,14 +97,6 @@ pub fn create_job_account_and_job( let mut msgs: Vec = vec![]; - if !native_funds.is_empty() { - // Fund account in native coins - msgs.push(build_transfer_native_funds_msg( - job_account_addr.to_string(), - native_funds.clone(), - )) - } - if let Some(cw_funds) = cw_funds.clone() { // Fund account in CW20 / CW721 tokens for cw_fund in cw_funds { @@ -209,7 +201,7 @@ pub fn create_funding_account_and_job( .iter() .cloned() .find(|attr| attr.key == "native_funds") - .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? + .ok_or_else(|| StdError::generic_err("cannot find `native_funds` attribute"))? .value, )?; @@ -217,23 +209,12 @@ pub fn create_funding_account_and_job( job.funding_account = Some(funding_account_addr.clone()); JobQueue::sync(&mut deps, env, job.clone())?; - let mut msgs: Vec = vec![]; - - if !native_funds.is_empty() { - // Fund account in native coins - msgs.push(build_transfer_native_funds_msg( - funding_account_addr.to_string(), - native_funds.clone(), - )) - } - - // Take funding account - msgs.push(build_take_funding_account_msg( + let msgs: Vec = vec![build_take_funding_account_msg( config.job_account_tracker_address.to_string(), job.owner.to_string(), funding_account_addr.to_string(), job.id, - )); + )]; Ok(Response::new() .add_messages(msgs) @@ -287,29 +268,20 @@ pub fn create_funding_account( .iter() .cloned() .find(|attr| attr.key == "native_funds") - .ok_or_else(|| StdError::generic_err("cannot find `funds` attribute"))? + .ok_or_else(|| StdError::generic_err("cannot find `native_funds` attribute"))? .value, )?; - let mut msgs: Vec = vec![]; - - if !native_funds.is_empty() { - // Fund account in native coins - msgs.push(build_transfer_native_funds_msg( - funding_account_addr.to_string(), - native_funds, - )) - } - - msgs.push(build_add_funding_account_msg( + let msgs: Vec = vec![build_add_funding_account_msg( config.job_account_tracker_address.to_string(), owner.to_string(), funding_account_addr.to_string(), - )); + )]; Ok(Response::new() .add_messages(msgs) .add_attribute("action", "create_job_account_reply") .add_attribute("owner", owner) - .add_attribute("funding_account_address", funding_account_addr)) + .add_attribute("funding_account_address", funding_account_addr) + .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) } From cea1036d23412e1631f578ba038076910f4c1173 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 23 Nov 2023 17:22:19 +0100 Subject: [PATCH 089/133] prevent job accounts as funding accounts --- .../src/execute/account.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contracts/warp-job-account-tracker/src/execute/account.rs b/contracts/warp-job-account-tracker/src/execute/account.rs index c673d820..0c52f45a 100644 --- a/contracts/warp-job-account-tracker/src/execute/account.rs +++ b/contracts/warp-job-account-tracker/src/execute/account.rs @@ -51,6 +51,13 @@ pub fn take_funding_account( let account_owner_addr_ref = deps.api.addr_validate(&data.account_owner_addr)?; let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + // prevent taking job accounts as funding accounts + if TAKEN_ACCOUNTS.has(deps.storage, (&account_owner_addr_ref, account_addr_ref)) + || FREE_ACCOUNTS.has(deps.storage, (&account_owner_addr_ref, account_addr_ref)) + { + return Err(ContractError::AccountAlreadyTakenError {}); + } + TAKEN_FUNDING_ACCOUNT_BY_JOB.update(deps.storage, data.job_id.u64(), |s| match s { // value is a dummy data because there is no built in support for set in cosmwasm None => Ok(account_addr_ref.clone()), @@ -144,6 +151,13 @@ pub fn add_funding_account( let account_owner_addr_ref = deps.api.addr_validate(&data.account_owner_addr)?; let account_addr_ref = deps.api.addr_validate(&data.account_addr)?; + // prevent adding job accounts as funding accounts + if TAKEN_ACCOUNTS.has(deps.storage, (&account_owner_addr_ref, &account_addr_ref)) + || FREE_ACCOUNTS.has(deps.storage, (&account_owner_addr_ref, &account_addr_ref)) + { + return Err(ContractError::AccountAlreadyTakenError {}); + } + FUNDING_ACCOUNTS_BY_USER.update( deps.storage, &account_owner_addr_ref, From c82097d6bb59f13661f4019510358903e9b91411 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 24 Nov 2023 17:01:16 +0100 Subject: [PATCH 090/133] remove legacy flows from new job-account contract --- contracts/warp-controller/src/util/msg.rs | 32 +++---- contracts/warp-job-account/src/contract.rs | 13 +-- contracts/warp-job-account/src/tests.rs | 105 +++++++++++---------- packages/job-account/src/lib.rs | 17 +--- 4 files changed, 72 insertions(+), 95 deletions(-) diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 70f7a25d..682f14ad 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -4,7 +4,6 @@ use controller::account::{ AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, WarpMsg, WarpMsgs, WithdrawAssetsMsg, }; -use job_account::GenericMsg; use job_account_tracker::{ AddFundingAccountMsg, FreeAccountMsg, FreeFundingAccountMsg, TakeAccountMsg, TakeFundingAccountMsg, @@ -182,14 +181,13 @@ pub fn build_account_execute_generic_msgs( account_addr: String, cosmos_msgs_for_account_to_execute: Vec, ) -> CosmosMsg { - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: account_addr, - msg: to_binary(&job_account::ExecuteMsg::Generic(GenericMsg { - msgs: cosmos_msgs_for_account_to_execute, - })) - .unwrap(), - funds: vec![], - }) + build_account_execute_warp_msgs( + account_addr, + cosmos_msgs_for_account_to_execute + .into_iter() + .map(WarpMsg::Generic) + .collect(), + ) } pub fn build_account_execute_warp_msgs( @@ -210,14 +208,10 @@ pub fn build_account_withdraw_assets_msg( account_addr: String, assets_to_withdraw: Vec, ) -> CosmosMsg { - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: account_addr, - msg: to_binary(&job_account::ExecuteMsg::WithdrawAssets( - WithdrawAssetsMsg { - asset_infos: assets_to_withdraw, - }, - )) - .unwrap(), - funds: vec![], - }) + build_account_execute_warp_msgs( + account_addr, + vec![WarpMsg::WithdrawAssets(WithdrawAssetsMsg { + asset_infos: assets_to_withdraw, + })], + ) } diff --git a/contracts/warp-job-account/src/contract.rs b/contracts/warp-job-account/src/contract.rs index ad429fcf..2cb6658a 100644 --- a/contracts/warp-job-account/src/contract.rs +++ b/contracts/warp-job-account/src/contract.rs @@ -1,12 +1,9 @@ use crate::state::CONFIG; use crate::{query, ContractError}; -use controller::account::{ - execute_warp_msgs, ibc_transfer, warp_msgs_to_cosmos_msgs, withdraw_assets, -}; +use controller::account::{execute_warp_msgs, warp_msgs_to_cosmos_msgs}; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; -use cw_utils::nonpayable; use job_account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; #[cfg_attr(not(feature = "library"), entry_point)] @@ -52,14 +49,6 @@ pub fn execute( return Err(ContractError::Unauthorized {}); } match msg { - ExecuteMsg::Generic(data) => Ok(Response::new() - .add_messages(data.msgs) - .add_attribute("action", "generic")), - ExecuteMsg::WithdrawAssets(data) => { - nonpayable(&info).unwrap(); - withdraw_assets(deps.as_ref(), env, data, &config.owner).map_err(ContractError::Std) - } - ExecuteMsg::IbcTransfer(data) => ibc_transfer(env, data).map_err(ContractError::Std), ExecuteMsg::WarpMsgs(data) => { execute_warp_msgs(deps, env, data, &config.owner).map_err(ContractError::Std) } diff --git a/contracts/warp-job-account/src/tests.rs b/contracts/warp-job-account/src/tests.rs index 5775661e..18ad9baf 100644 --- a/contracts/warp-job-account/src/tests.rs +++ b/contracts/warp-job-account/src/tests.rs @@ -1,11 +1,12 @@ use crate::contract::{execute, instantiate}; use crate::ContractError; +use controller::account::{WarpMsg, WarpMsgs}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, IbcTimeoutBlock, Response, StakingMsg, Uint128, Uint64, VoteOption, WasmMsg, }; -use job_account::{ExecuteMsg, GenericMsg, InstantiateMsg}; +use job_account::{ExecuteMsg, InstantiateMsg}; #[test] fn test_execute_controller() { @@ -26,38 +27,40 @@ fn test_execute_controller() { }, ); - let execute_msg = ExecuteMsg::Generic(GenericMsg { + let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { msgs: vec![ - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), msg: to_binary("test").unwrap(), funds: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Bank(BankMsg::Send { + })), + WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { to_address: "vlad2".to_string(), amount: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Gov(GovMsg::Vote { + })), + WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { proposal_id: 0, vote: VoteOption::Yes, - }), - CosmosMsg::Staking(StakingMsg::Delegate { + })), + WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { validator: "vladidator".to_string(), amount: Coin { denom: "coin".to_string(), amount: Uint128::new(100), }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { + })), + WarpMsg::Generic(CosmosMsg::Distribution( + DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }, + )), + WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { channel_id: "channel_vlad".to_string(), to_address: "vlad3".to_string(), amount: Coin { @@ -68,11 +71,11 @@ fn test_execute_controller() { revision: 0, height: 0, }), - }), - CosmosMsg::Stargate { + })), + WarpMsg::Generic(CosmosMsg::Stargate { type_url: "utl".to_string(), value: Default::default(), - }, + }), ], }); @@ -151,38 +154,40 @@ fn test_execute_owner() { }, ); - let execute_msg = ExecuteMsg::Generic(GenericMsg { + let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { msgs: vec![ - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), msg: to_binary("test").unwrap(), funds: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Bank(BankMsg::Send { + })), + WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { to_address: "vlad2".to_string(), amount: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Gov(GovMsg::Vote { + })), + WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { proposal_id: 0, vote: VoteOption::Yes, - }), - CosmosMsg::Staking(StakingMsg::Delegate { + })), + WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { validator: "vladidator".to_string(), amount: Coin { denom: "coin".to_string(), amount: Uint128::new(100), }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { + })), + WarpMsg::Generic(CosmosMsg::Distribution( + DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }, + )), + WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { channel_id: "channel_vlad".to_string(), to_address: "vlad3".to_string(), amount: Coin { @@ -193,11 +198,11 @@ fn test_execute_owner() { revision: 0, height: 0, }), - }), - CosmosMsg::Stargate { + })), + WarpMsg::Generic(CosmosMsg::Stargate { type_url: "utl".to_string(), value: Default::default(), - }, + }), ], }); @@ -278,38 +283,40 @@ fn test_execute_unauth() { }, ); - let execute_msg = ExecuteMsg::Generic(GenericMsg { + let execute_msg = ExecuteMsg::WarpMsgs(WarpMsgs { msgs: vec![ - CosmosMsg::Wasm(WasmMsg::Execute { + WarpMsg::Generic(CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), msg: to_binary("test").unwrap(), funds: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Bank(BankMsg::Send { + })), + WarpMsg::Generic(CosmosMsg::Bank(BankMsg::Send { to_address: "vlad2".to_string(), amount: vec![Coin { denom: "coin".to_string(), amount: Uint128::new(100), }], - }), - CosmosMsg::Gov(GovMsg::Vote { + })), + WarpMsg::Generic(CosmosMsg::Gov(GovMsg::Vote { proposal_id: 0, vote: VoteOption::Yes, - }), - CosmosMsg::Staking(StakingMsg::Delegate { + })), + WarpMsg::Generic(CosmosMsg::Staking(StakingMsg::Delegate { validator: "vladidator".to_string(), amount: Coin { denom: "coin".to_string(), amount: Uint128::new(100), }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { + })), + WarpMsg::Generic(CosmosMsg::Distribution( + DistributionMsg::SetWithdrawAddress { + address: "vladdress".to_string(), + }, + )), + WarpMsg::Generic(CosmosMsg::Ibc(IbcMsg::Transfer { channel_id: "channel_vlad".to_string(), to_address: "vlad3".to_string(), amount: Coin { @@ -320,11 +327,11 @@ fn test_execute_unauth() { revision: 0, height: 0, }), - }), - CosmosMsg::Stargate { + })), + WarpMsg::Generic(CosmosMsg::Stargate { type_url: "utl".to_string(), value: Default::default(), - }, + }), ], }); diff --git a/packages/job-account/src/lib.rs b/packages/job-account/src/lib.rs index 620e80f8..c48c1644 100644 --- a/packages/job-account/src/lib.rs +++ b/packages/job-account/src/lib.rs @@ -1,6 +1,6 @@ -use controller::account::{CwFund, IbcTransferMsg, WarpMsg, WarpMsgs, WithdrawAssetsMsg}; +use controller::account::{CwFund, WarpMsg, WarpMsgs}; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Addr, Coin as NativeCoin, CosmosMsg, Uint64}; +use cosmwasm_std::{Addr, Coin as NativeCoin, Uint64}; #[cw_serde] pub struct Config { @@ -27,21 +27,8 @@ pub struct InstantiateMsg { #[allow(clippy::large_enum_variant)] pub enum ExecuteMsg { WarpMsgs(WarpMsgs), - - // legacy flow - Generic(GenericMsg), - WithdrawAssets(WithdrawAssetsMsg), - IbcTransfer(IbcTransferMsg), -} - -#[cw_serde] -pub struct GenericMsg { - pub msgs: Vec, } -#[cw_serde] -pub struct ExecuteWasmMsg {} - #[derive(QueryResponses)] #[cw_serde] pub enum QueryMsg { From aaf67540861c80d227e86b3de83898ff6dd71a45 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 24 Nov 2023 17:36:33 +0100 Subject: [PATCH 091/133] add instantiate job account tracker in controller --- contracts/warp-controller/src/contract.rs | 30 ++++++++++---- contracts/warp-controller/src/reply/job.rs | 41 ++++++++++++++++++- contracts/warp-controller/src/util/msg.rs | 19 +++++++++ .../warp-job-account-tracker/src/contract.rs | 3 +- packages/controller/src/lib.rs | 2 +- 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index b4b0da88..504a5d45 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -1,12 +1,13 @@ use cosmwasm_std::{ - entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult, - Uint64, + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, ReplyOn, Response, + StdResult, SubMsg, Uint64, }; use cw_utils::{must_pay, nonpayable}; use crate::{ execute, migrate, query, reply, state::{CONFIG, STATE}, + util::msg::build_instantiate_job_account_tracker_msg, ContractError, }; @@ -15,12 +16,13 @@ use controller::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, State #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, msg: InstantiateMsg, ) -> Result { let state = State { - current_job_id: Uint64::one(), + // start from 10, 0-9 reserved for reply logic + current_job_id: Uint64::from(10u64), q: Uint64::zero(), }; @@ -37,7 +39,8 @@ pub fn instantiate( creation_fee_percentage: msg.creation_fee, cancellation_fee_percentage: msg.cancellation_fee, resolver_address: deps.api.addr_validate(&msg.resolver_address)?, - job_account_tracker_address: deps.api.addr_validate(&msg.job_account_tracker_address)?, + // placeholder, will be updated in reply + job_account_tracker_address: deps.api.addr_validate(&msg.resolver_address)?, t_max: msg.t_max, t_min: msg.t_min, a_max: msg.a_max, @@ -78,7 +81,18 @@ pub fn instantiate( STATE.save(deps.storage, &state)?; CONFIG.save(deps.storage, &config)?; - Ok(Response::new()) + let submsgs = vec![SubMsg { + id: 3, + msg: build_instantiate_job_account_tracker_msg( + config.owner.to_string(), + env.contract.address.to_string(), + msg.job_account_tracker_code_id.u64(), + ), + gas_limit: None, + reply_on: ReplyOn::Always, + }]; + + Ok(Response::new().add_submessages(submsgs)) } #[cfg_attr(not(feature = "library"), entry_point)] @@ -182,12 +196,12 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { let config = CONFIG.load(deps.storage)?; - // 0-2 reserved + // 0-10 reserved match msg.id { - // use 0 as hack to call create_job_account_and_job 0 => reply::account::create_job_account_and_job(deps, env, msg, config), 1 => reply::account::create_funding_account_and_job(deps, env, msg, config), 2 => reply::account::create_funding_account(deps, env, msg, config), + 3 => reply::job::instantiate_sub_contracts(deps, env, msg, config), _id => reply::job::execute_job(deps, env, msg, config), } } diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index af81cc13..e4be78f7 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -1,12 +1,12 @@ use cosmwasm_std::{ Attribute, BalanceResponse, BankQuery, Coin, DepsMut, Env, QueryRequest, Reply, Response, - StdResult, SubMsgResult, Uint64, + StdError, StdResult, SubMsgResult, Uint64, }; use crate::{ error::map_contract_error, execute::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}, - state::{JobQueue, LEGACY_ACCOUNTS, STATE}, + state::{JobQueue, CONFIG, LEGACY_ACCOUNTS, STATE}, util::{ legacy_account::is_legacy_account, msg::{ @@ -265,3 +265,40 @@ pub fn execute_job( .add_attributes(res_attrs) .add_attributes(new_job_attrs)) } + +pub fn instantiate_sub_contracts( + deps: DepsMut, + _env: Env, + msg: Reply, + mut config: Config, +) -> Result { + let reply: cosmwasm_std::SubMsgResponse = + msg.result.into_result().map_err(StdError::generic_err)?; + + let job_account_tracker_instantiate_event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "instantiate") + }) + .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + + let job_account_tracker_addr = deps.api.addr_validate( + &job_account_tracker_instantiate_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "job_account_tracker") + .ok_or_else(|| StdError::generic_err("cannot find `job_account_tracker` attribute"))? + .value, + )?; + + config.job_account_tracker_address = job_account_tracker_addr; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new().add_attribute("action", "instantiate_sub_contracts_reply")) +} diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 682f14ad..e2d149c6 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -9,6 +9,25 @@ use job_account_tracker::{ TakeFundingAccountMsg, }; +#[allow(clippy::too_many_arguments)] +pub fn build_instantiate_job_account_tracker_msg( + admin_addr: String, + controller_addr: String, + code_id: u64, +) -> CosmosMsg { + CosmosMsg::Wasm(WasmMsg::Instantiate { + admin: Some(admin_addr.clone()), + code_id, + msg: to_binary(&job_account_tracker::InstantiateMsg { + admin: admin_addr, + warp_addr: controller_addr, + }) + .unwrap(), + funds: vec![], + label: "warp job account tracker".to_string(), + }) +} + #[allow(clippy::too_many_arguments)] pub fn build_instantiate_warp_account_msg( job_id: Uint64, diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index e71f1b8e..d17dc037 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -27,7 +27,8 @@ pub fn instantiate( Ok(Response::new() .add_attribute("action", "instantiate") - .add_attribute("contract_addr", instantiated_account_addr) + .add_attribute("contract_addr", instantiated_account_addr.clone()) + .add_attribute("job_account_tracker", instantiated_account_addr) .add_attribute("admin", msg.admin) .add_attribute("warp_addr", msg.warp_addr)) } diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index bfca0117..cb797bca 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -64,11 +64,11 @@ pub struct InstantiateMsg { pub fee_denom: String, pub fee_collector: Option, pub warp_account_code_id: Uint64, + pub job_account_tracker_code_id: Uint64, pub minimum_reward: Uint128, pub creation_fee: Uint64, pub cancellation_fee: Uint64, pub resolver_address: String, - pub job_account_tracker_address: String, pub t_max: Uint64, pub t_min: Uint64, pub a_max: Uint128, From 049b61cce03ada9df965a3ad00fc79365d5b6de9 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 24 Nov 2023 17:39:34 +0100 Subject: [PATCH 092/133] fix tests --- contracts/warp-job-account/src/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/warp-job-account/src/tests.rs b/contracts/warp-job-account/src/tests.rs index 18ad9baf..0b0f86e8 100644 --- a/contracts/warp-job-account/src/tests.rs +++ b/contracts/warp-job-account/src/tests.rs @@ -84,7 +84,7 @@ fn test_execute_controller() { assert_eq!( execute_res, Response::new() - .add_attribute("action", "generic") + .add_attribute("action", "warp_msgs") .add_messages(vec![ CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), @@ -213,7 +213,7 @@ fn test_execute_owner() { assert_eq!( execute_res, Response::new() - .add_attribute("action", "generic") + .add_attribute("action", "warp_msgs") .add_messages(vec![ CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: "contract".to_string(), From 900d694130bcf36410de468695a8f629b34822f3 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 28 Nov 2023 17:39:02 +0100 Subject: [PATCH 093/133] schema updates --- .../examples/warp-job-account-tracker-schema.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs index 6ba7656c..1c25f770 100644 --- a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs +++ b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs @@ -4,7 +4,7 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use job_account_tracker::{ Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, InstantiateMsg, - QueryMsg, + QueryMsg, FundingAccountResponse, FundingAccountsResponse, }; fn main() { @@ -19,6 +19,8 @@ fn main() { export_schema(&schema_for!(Config), &out_dir); export_schema(&schema_for!(AccountsResponse), &out_dir); export_schema(&schema_for!(AccountResponse), &out_dir); + export_schema(&schema_for!(FundingAccountResponse), &out_dir); + export_schema(&schema_for!(FundingAccountsResponse), &out_dir); export_schema(&schema_for!(ConfigResponse), &out_dir); export_schema(&schema_for!(Account), &out_dir); } From ad1e05f2bf5c30201afd43f14d361c02e494b4d9 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 29 Nov 2023 14:47:17 +0100 Subject: [PATCH 094/133] migrations --- contracts/warp-controller/src/migrate/job.rs | 21 ++++++++++---------- contracts/warp-controller/src/state.rs | 16 +++++++-------- refs.json | 8 ++++---- tasks/migrate_warp.ts | 8 +++++--- 4 files changed, 28 insertions(+), 25 deletions(-) diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs index 4c598f8c..cb230d90 100644 --- a/contracts/warp-controller/src/migrate/job.rs +++ b/contracts/warp-controller/src/migrate/job.rs @@ -22,6 +22,7 @@ pub struct OldJob { pub labels: Vec, pub status: JobStatus, pub terminate_condition: Option, + pub duration_days: Uint64, pub executions: Vec, pub vars: String, pub recurring: bool, @@ -61,15 +62,15 @@ pub fn migrate_pending_jobs( let indexes = OldJobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "pending_jobs__reward_v4", + "pending_jobs__reward_v5", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "pending_jobs_v4", - "pending_jobs__publish_timestamp_v4", + "pending_jobs_v5", + "pending_jobs__publish_timestamp_v5", ), }; - IndexedMap::new("pending_jobs_v4", indexes) + IndexedMap::new("pending_jobs_v5", indexes) } let job_keys: Result, _> = OLD_PENDING_JOBS() @@ -100,7 +101,7 @@ pub fn migrate_pending_jobs( recurring: old_job.recurring, reward: old_job.reward, assets_to_withdraw: old_job.assets_to_withdraw, - duration_days: Uint64::from(30u64), + duration_days: old_job.duration_days, created_at_time: old_job.last_update_time, // TODO: update to old_job.funding_account funding_account: None, @@ -131,15 +132,15 @@ pub fn migrate_finished_jobs( let indexes = OldJobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "finished_jobs__reward_v4", + "finished_jobs__reward_v5", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "finished_jobs_v4", - "finished_jobs__publish_timestamp_v4", + "finished_jobs_v5", + "finished_jobs__publish_timestamp_v5", ), }; - IndexedMap::new("finished_jobs_v4", indexes) + IndexedMap::new("finished_jobs_v5", indexes) } let job_keys: Result, _> = OLD_FINISHED_JOBS() @@ -170,7 +171,7 @@ pub fn migrate_finished_jobs( recurring: old_job.recurring, reward: old_job.reward, assets_to_withdraw: old_job.assets_to_withdraw, - duration_days: Uint64::from(30u64), + duration_days: old_job.duration_days, created_at_time: old_job.last_update_time, // TODO: update to old_job.funding_account funding_account: None, diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index a3cddd9e..4c58430a 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -26,15 +26,15 @@ pub fn PENDING_JOBS<'a>() -> IndexedMap<'a, u64, Job, JobIndexes<'a>> { let indexes = JobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "pending_jobs__reward_v5", + "pending_jobs__reward_v6", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "pending_jobs_v5", - "pending_jobs__publish_timestamp_v5", + "pending_jobs_v6", + "pending_jobs__publish_timestamp_v6", ), }; - IndexedMap::new("pending_jobs_v5", indexes) + IndexedMap::new("pending_jobs_v6", indexes) } #[allow(non_snake_case)] @@ -42,15 +42,15 @@ pub fn FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, Job, JobIndexes<'a>> { let indexes = JobIndexes { reward: UniqueIndex::new( |job| (job.reward.u128(), job.id.u64()), - "finished_jobs__reward_v5", + "finished_jobs__reward_v6", ), publish_time: MultiIndex::new( |_pk, job| job.last_update_time.u64(), - "finished_jobs_v5", - "finished_jobs__publish_timestamp_v5", + "finished_jobs_v6", + "finished_jobs__publish_timestamp_v6", ), }; - IndexedMap::new("finished_jobs_v5", indexes) + IndexedMap::new("finished_jobs_v6", indexes) } pub struct LegacyAccountIndexes<'a> { diff --git a/refs.json b/refs.json index 9c273795..96ce95bd 100644 --- a/refs.json +++ b/refs.json @@ -12,11 +12,11 @@ "codeId": "9626" }, "warp-controller": { - "codeId": "11718", + "codeId": "12126", "address": "terra1fqcfh8vpqsl7l5yjjtq5wwu6sv989txncq5fa756tv7lywqexraq5vnjvt" }, "warp-resolver": { - "codeId": "11717", + "codeId": "12124", "address": "terra1lxfx6n792aw3hg47tchmyuhv5t30f334gus67pc250qx5zljadws65elnf" }, "warp-templates": { @@ -24,10 +24,10 @@ "address": "terra17xm2ewyg60y7eypnwav33fwm23hxs3qyd8qk9tnntj4d0rp2vvhsgkpwwp" }, "warp-job-account": { - "codeId": "11716" + "codeId": "12123" }, "warp-job-account-tracker": { - "codeId": "11630", + "codeId": "12122", "address": "terra1zzgg30ygltd5s3xtescfquwmm2jktaq28t37f2j9h5wwswpxtyyspugek8" } }, diff --git a/tasks/migrate_warp.ts b/tasks/migrate_warp.ts index ff488a53..7207d7c6 100644 --- a/tasks/migrate_warp.ts +++ b/tasks/migrate_warp.ts @@ -2,8 +2,8 @@ import { MsgMigrateContract } from "@terra-money/terra.js"; import task, { info } from "@terra-money/terrariums"; task(async ({ deployer, signer, refs, network }) => { - // deployer.buildContract("warp-controller"); - // deployer.optimizeContract("warp-controller"); + deployer.buildContract("warp-controller"); + deployer.optimizeContract("warp-controller"); await deployer.storeCode("warp-controller"); await new Promise((resolve) => setTimeout(resolve, 3000)); @@ -14,7 +14,9 @@ task(async ({ deployer, signer, refs, network }) => { signer.key.accAddress, contract.address!, parseInt(contract.codeId!), - {} + { + warp_account_code_id: "12123" + } ); try { From 2c3d37c53d06a44be24a9fdf7f6fb15b3c83ae1f Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 1 Dec 2023 22:00:25 +0100 Subject: [PATCH 095/133] remove legacy account support --- Cargo.lock | 39 --- contracts/warp-controller/Cargo.toml | 1 - .../examples/warp-controller-schema.rs | 3 - contracts/warp-controller/src/contract.rs | 13 - contracts/warp-controller/src/execute/job.rs | 93 +++-- .../src/migrate/legacy_account.rs | 40 --- contracts/warp-controller/src/migrate/mod.rs | 1 - .../warp-controller/src/query/account.rs | 40 --- contracts/warp-controller/src/query/mod.rs | 1 - contracts/warp-controller/src/reply/job.rs | 53 ++- contracts/warp-controller/src/state.rs | 26 +- .../src/util/legacy_account.rs | 8 - contracts/warp-controller/src/util/mod.rs | 1 - .../warp-job-account-tracker-schema.rs | 4 +- contracts/warp-legacy-account/.cargo/config | 4 - contracts/warp-legacy-account/.gitignore | 16 - contracts/warp-legacy-account/Cargo.toml | 53 --- contracts/warp-legacy-account/README.md | 106 ------ .../examples/warp-legacy-account-schema.rs | 17 - contracts/warp-legacy-account/meta/README.md | 16 - .../warp-legacy-account/meta/appveyor.yml | 61 ---- .../warp-legacy-account/meta/test_generate.sh | 37 -- contracts/warp-legacy-account/src/contract.rs | 234 ------------- contracts/warp-legacy-account/src/error.rs | 73 ---- contracts/warp-legacy-account/src/lib.rs | 8 - contracts/warp-legacy-account/src/state.rs | 4 - contracts/warp-legacy-account/src/tests.rs | 327 ------------------ packages/controller/src/account.rs | 27 -- packages/controller/src/lib.rs | 18 - packages/legacy-account/.cargo/config | 4 - packages/legacy-account/Cargo.toml | 21 -- packages/legacy-account/README.md | 106 ------ .../legacy-account/examples/account-schema.rs | 17 - packages/legacy-account/src/lib.rs | 95 ----- 34 files changed, 65 insertions(+), 1502 deletions(-) delete mode 100644 contracts/warp-controller/src/migrate/legacy_account.rs delete mode 100644 contracts/warp-controller/src/query/account.rs delete mode 100644 contracts/warp-controller/src/util/legacy_account.rs delete mode 100644 contracts/warp-legacy-account/.cargo/config delete mode 100644 contracts/warp-legacy-account/.gitignore delete mode 100644 contracts/warp-legacy-account/Cargo.toml delete mode 100644 contracts/warp-legacy-account/README.md delete mode 100644 contracts/warp-legacy-account/examples/warp-legacy-account-schema.rs delete mode 100644 contracts/warp-legacy-account/meta/README.md delete mode 100644 contracts/warp-legacy-account/meta/appveyor.yml delete mode 100644 contracts/warp-legacy-account/meta/test_generate.sh delete mode 100644 contracts/warp-legacy-account/src/contract.rs delete mode 100644 contracts/warp-legacy-account/src/error.rs delete mode 100644 contracts/warp-legacy-account/src/lib.rs delete mode 100644 contracts/warp-legacy-account/src/state.rs delete mode 100644 contracts/warp-legacy-account/src/tests.rs delete mode 100644 packages/legacy-account/.cargo/config delete mode 100644 packages/legacy-account/Cargo.toml delete mode 100644 packages/legacy-account/README.md delete mode 100644 packages/legacy-account/examples/account-schema.rs delete mode 100644 packages/legacy-account/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a5281143..abf7dcb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -604,19 +604,6 @@ dependencies = [ "sha2 0.10.6", ] -[[package]] -name = "legacy-account" -version = "0.1.0" -dependencies = [ - "controller", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", - "prost 0.11.9", - "schemars", - "serde", -] - [[package]] name = "libc" version = "0.2.138" @@ -1037,7 +1024,6 @@ dependencies = [ "job-account", "job-account-tracker", "json-codec-wasm", - "legacy-account", "resolver", "schemars", "serde-json-wasm 0.4.1", @@ -1093,31 +1079,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "warp-legacy-account" -version = "0.1.0" -dependencies = [ - "anyhow", - "base64", - "controller", - "cosmwasm-schema", - "cosmwasm-std", - "cosmwasm-storage", - "cw-asset", - "cw-multi-test", - "cw-storage-plus 0.16.0", - "cw-utils 0.16.0", - "cw2 0.16.0", - "cw20", - "cw721", - "json-codec-wasm", - "legacy-account", - "prost 0.11.9", - "schemars", - "serde-json-wasm 0.4.1", - "thiserror", -] - [[package]] name = "warp-resolver" version = "0.1.0" diff --git a/contracts/warp-controller/Cargo.toml b/contracts/warp-controller/Cargo.toml index 8e2000f8..e669cfbf 100644 --- a/contracts/warp-controller/Cargo.toml +++ b/contracts/warp-controller/Cargo.toml @@ -39,7 +39,6 @@ cw-storage-plus = "0.16" cw-utils = "0.16" cw2 = "0.16" cw20 = "0.16" -legacy-account = { path = "../../packages/legacy-account", default-features = false, version = "*" } job-account = { path = "../../packages/job-account", default-features = false, version = "*" } job-account-tracker = { path = "../../packages/job-account-tracker", default-features = false, version = "*" } controller = { path = "../../packages/controller", default-features = false, version = "*" } diff --git a/contracts/warp-controller/examples/warp-controller-schema.rs b/contracts/warp-controller/examples/warp-controller-schema.rs index 63f58aa2..961b7ab8 100644 --- a/contracts/warp-controller/examples/warp-controller-schema.rs +++ b/contracts/warp-controller/examples/warp-controller-schema.rs @@ -2,7 +2,6 @@ use std::env::current_dir; use std::fs::create_dir_all; use controller::{ - account::{LegacyAccountResponse, LegacyAccountsResponse}, job::{JobResponse, JobsResponse}, QueryMsg, State, StateResponse, {Config, ConfigResponse, ExecuteMsg, InstantiateMsg}, }; @@ -23,6 +22,4 @@ fn main() { export_schema(&schema_for!(StateResponse), &out_dir); export_schema(&schema_for!(JobResponse), &out_dir); export_schema(&schema_for!(JobsResponse), &out_dir); - export_schema(&schema_for!(LegacyAccountResponse), &out_dir); - export_schema(&schema_for!(LegacyAccountsResponse), &out_dir); } diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 504a5d45..4522c91a 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -132,10 +132,6 @@ pub fn execute( nonpayable(&info).unwrap(); execute::controller::update_config(deps, env, info, data, config) } - ExecuteMsg::MigrateLegacyAccounts(data) => { - nonpayable(&info).unwrap(); - migrate::legacy_account::migrate_legacy_accounts(deps, info, data, config) - } ExecuteMsg::MigrateFreeJobAccounts(data) => { nonpayable(&info).unwrap(); migrate::job_account::migrate_free_job_accounts(deps.as_ref(), env, info, data, config) @@ -165,15 +161,6 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::QueryJob(data) => to_binary(&query::job::query_job(deps, env, data)?), QueryMsg::QueryJobs(data) => to_binary(&query::job::query_jobs(deps, env, data)?), - - // For job account, please query it via the account tracker contract - QueryMsg::QueryLegacyAccount(data) => { - to_binary(&query::account::query_legacy_account(deps, env, data)?) - } - QueryMsg::QueryLegacyAccounts(data) => { - to_binary(&query::account::query_legacy_accounts(deps, env, data)?) - } - QueryMsg::QueryConfig(data) => { to_binary(&query::controller::query_config(deps, env, data)?) } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 7025a848..ad0ae5fc 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -12,16 +12,12 @@ use cosmwasm_std::{ SubMsg, Uint128, Uint64, WasmMsg, }; -use crate::{ - state::LEGACY_ACCOUNTS, - util::{ - fee::deduct_from_native_funds, - legacy_account::is_legacy_account, - msg::{ - build_account_withdraw_assets_msg, build_free_account_msg, - build_instantiate_warp_account_msg, build_taken_account_msg, build_transfer_cw20_msg, - build_transfer_cw721_msg, build_transfer_native_funds_msg, - }, +use crate::util::{ + fee::deduct_from_native_funds, + msg::{ + build_account_withdraw_assets_msg, build_free_account_msg, + build_instantiate_warp_account_msg, build_taken_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, build_transfer_native_funds_msg, }, }; @@ -358,7 +354,6 @@ pub fn delete_job( fee_denom_paid_amount: Uint128, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; - let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, job.owner.clone())?; let job_account_addr = job.account.clone(); if job.status != JobStatus::Pending { @@ -394,29 +389,27 @@ pub fn delete_job( vec![Coin::new(fee.u128(), config.fee_denom.clone())], )); - if !is_legacy_account(legacy_account, job_account_addr.clone()) { - // Free account - msgs.push(build_free_account_msg( + // Free account + msgs.push(build_free_account_msg( + config.job_account_tracker_address.to_string(), + job.owner.to_string(), + job_account_addr.to_string(), + job.id, + )); + + if let Some(funding_account) = job.funding_account { + msgs.push(build_free_funding_account_msg( config.job_account_tracker_address.to_string(), job.owner.to_string(), - job_account_addr.to_string(), + funding_account.to_string(), job.id, )); - if let Some(funding_account) = job.funding_account { - msgs.push(build_free_funding_account_msg( - config.job_account_tracker_address.to_string(), - job.owner.to_string(), - funding_account.to_string(), - job.id, - )); - - // withdraws all native funds from funding account - msgs.push(build_account_withdraw_assets_msg( - funding_account.to_string(), - vec![AssetInfo::Native(config.fee_denom)], - )); - } + // withdraws all native funds from funding account + msgs.push(build_account_withdraw_assets_msg( + funding_account.to_string(), + vec![AssetInfo::Native(config.fee_denom)], + )); } // Job owner withdraw all assets that are listed from warp account to itself @@ -477,7 +470,6 @@ pub fn execute_job( config: Config, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; - let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, job.owner.clone())?; let job_account_addr = job.account.clone(); if job.status != JobStatus::Pending { @@ -548,23 +540,21 @@ pub fn execute_job( vec![Coin::new(job.reward.u128(), config.fee_denom)], )); - if !is_legacy_account(legacy_account, job_account_addr.clone()) { - // Free account - msgs.push(build_free_account_msg( + // Free account + msgs.push(build_free_account_msg( + config.job_account_tracker_address.to_string(), + job.owner.to_string(), + job_account_addr.to_string(), + job.id, + )); + + if let Some(funding_account) = job.funding_account { + msgs.push(build_free_funding_account_msg( config.job_account_tracker_address.to_string(), job.owner.to_string(), - job_account_addr.to_string(), + funding_account.to_string(), job.id, )); - - if let Some(funding_account) = job.funding_account { - msgs.push(build_free_funding_account_msg( - config.job_account_tracker_address.to_string(), - job.owner.to_string(), - funding_account.to_string(), - job.id, - )); - } } Ok(Response::new() @@ -585,7 +575,6 @@ pub fn evict_job( config: Config, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; - let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, job.owner.clone())?; let job_account_addr = job.account.clone(); if job.status != JobStatus::Pending { @@ -618,15 +607,13 @@ pub fn evict_job( )], )); - if !is_legacy_account(legacy_account, job_account_addr.clone()) { - // Free account - msgs.push(build_free_account_msg( - config.job_account_tracker_address.to_string(), - job.owner.to_string(), - job_account_addr.to_string(), - job.id, - )); - } + // Free account + msgs.push(build_free_account_msg( + config.job_account_tracker_address.to_string(), + job.owner.to_string(), + job_account_addr.to_string(), + job.id, + )); Ok(Response::new() .add_messages(msgs) diff --git a/contracts/warp-controller/src/migrate/legacy_account.rs b/contracts/warp-controller/src/migrate/legacy_account.rs deleted file mode 100644 index c7c7baf0..00000000 --- a/contracts/warp-controller/src/migrate/legacy_account.rs +++ /dev/null @@ -1,40 +0,0 @@ -use cosmwasm_std::{to_binary, DepsMut, MessageInfo, Order, Response, WasmMsg}; -use cw_storage_plus::Bound; - -use crate::{state::LEGACY_ACCOUNTS, ContractError}; -use controller::{Config, MigrateLegacyAccountsMsg}; - -pub fn migrate_legacy_accounts( - deps: DepsMut, - info: MessageInfo, - msg: MigrateLegacyAccountsMsg, - config: Config, -) -> Result { - if info.sender != config.owner { - return Err(ContractError::Unauthorized {}); - } - - let start_after = match msg.start_after { - None => None, - Some(s) => Some(deps.api.addr_validate(s.as_str())?), - }; - let start_after = start_after.map(Bound::exclusive); - - let account_keys: Result, _> = LEGACY_ACCOUNTS() - .keys(deps.storage, start_after, None, Order::Ascending) - .take(msg.limit as usize) - .collect(); - let account_keys = account_keys?; - let mut migration_msgs = vec![]; - - for account_key in account_keys { - let account_address = LEGACY_ACCOUNTS().load(deps.storage, account_key)?.account; - migration_msgs.push(WasmMsg::Migrate { - contract_addr: account_address.to_string(), - new_code_id: msg.warp_legacy_account_code_id.u64(), - msg: to_binary(&legacy_account::MigrateMsg {})?, - }) - } - - Ok(Response::new().add_messages(migration_msgs)) -} diff --git a/contracts/warp-controller/src/migrate/mod.rs b/contracts/warp-controller/src/migrate/mod.rs index d8df4a31..8a179a31 100644 --- a/contracts/warp-controller/src/migrate/mod.rs +++ b/contracts/warp-controller/src/migrate/mod.rs @@ -1,3 +1,2 @@ pub(crate) mod job; pub(crate) mod job_account; -pub(crate) mod legacy_account; diff --git a/contracts/warp-controller/src/query/account.rs b/contracts/warp-controller/src/query/account.rs deleted file mode 100644 index f5817723..00000000 --- a/contracts/warp-controller/src/query/account.rs +++ /dev/null @@ -1,40 +0,0 @@ -use cosmwasm_std::{Deps, Env, Order, StdResult}; -use cw_storage_plus::Bound; - -use crate::state::{LEGACY_ACCOUNTS, QUERY_PAGE_SIZE}; - -use controller::account::{ - LegacyAccountResponse, LegacyAccountsResponse, QueryLegacyAccountMsg, QueryLegacyAccountsMsg, -}; - -pub fn query_legacy_account( - deps: Deps, - _env: Env, - data: QueryLegacyAccountMsg, -) -> StdResult { - Ok(LegacyAccountResponse { - account: LEGACY_ACCOUNTS() - .load(deps.storage, deps.api.addr_validate(data.owner.as_str())?)?, - }) -} - -pub fn query_legacy_accounts( - deps: Deps, - _env: Env, - data: QueryLegacyAccountsMsg, -) -> StdResult { - let start_after = match data.start_after { - None => None, - Some(s) => Some(deps.api.addr_validate(s.as_str())?), - }; - let start_after = start_after.map(Bound::exclusive); - let infos = LEGACY_ACCOUNTS() - .range(deps.storage, start_after, None, Order::Ascending) - .take(data.limit.unwrap_or(QUERY_PAGE_SIZE) as usize) - .collect::>>()?; - let mut accounts = vec![]; - for tuple in infos { - accounts.push(tuple.1) - } - Ok(LegacyAccountsResponse { accounts }) -} diff --git a/contracts/warp-controller/src/query/mod.rs b/contracts/warp-controller/src/query/mod.rs index ee75d71c..b10bee4a 100644 --- a/contracts/warp-controller/src/query/mod.rs +++ b/contracts/warp-controller/src/query/mod.rs @@ -1,3 +1,2 @@ -pub(crate) mod account; pub(crate) mod controller; pub(crate) mod job; diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index e4be78f7..2a313e05 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -6,14 +6,10 @@ use cosmwasm_std::{ use crate::{ error::map_contract_error, execute::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}, - state::{JobQueue, CONFIG, LEGACY_ACCOUNTS, STATE}, - util::{ - legacy_account::is_legacy_account, - msg::{ - build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_take_funding_account_msg, build_taken_account_msg, - build_transfer_native_funds_msg, - }, + state::{JobQueue, CONFIG, STATE}, + util::msg::{ + build_account_execute_generic_msgs, build_account_withdraw_assets_msg, + build_take_funding_account_msg, build_taken_account_msg, build_transfer_native_funds_msg, }, ContractError, }; @@ -60,18 +56,13 @@ pub fn execute_job( let reward_plus_fee = finished_job.reward + total_fees; - let legacy_account = LEGACY_ACCOUNTS().may_load(deps.storage, finished_job.owner.clone())?; let job_account_addr = finished_job.account.clone(); let mut recurring_job_created = false; - // backwards compability with legacy accounts, funding account is job's account - let funding_account_addr = finished_job - .funding_account - .clone() - .unwrap_or(job_account_addr.clone()); - if finished_job.recurring { + let funding_account_addr = finished_job.funding_account.clone().unwrap(); + let operational_amount = deps .querier .query::(&QueryRequest::Bank(BankQuery::Balance { @@ -224,23 +215,23 @@ pub fn execute_job( } if recurring_job_created { - if !is_legacy_account(legacy_account, job_account_addr.clone()) { - // Take job account with the new job - msgs.push(build_taken_account_msg( - config.job_account_tracker_address.to_string(), - finished_job.owner.to_string(), - job_account_addr.to_string(), - new_job_id, - )); + let funding_account_addr = finished_job.funding_account.clone().unwrap(); - // take funding account with new job - msgs.push(build_take_funding_account_msg( - config.job_account_tracker_address.to_string(), - finished_job.owner.to_string(), - funding_account_addr.to_string(), - new_job_id, - )); - } + // Take job account with the new job + msgs.push(build_taken_account_msg( + config.job_account_tracker_address.to_string(), + finished_job.owner.to_string(), + job_account_addr.to_string(), + new_job_id, + )); + + // take funding account with new job + msgs.push(build_take_funding_account_msg( + config.job_account_tracker_address.to_string(), + finished_job.owner.to_string(), + funding_account_addr.to_string(), + new_job_id, + )); } else { // No new job created, account has been free in execute_job, no need to free here again // Job owner withdraw all assets that are listed from warp account to itself diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 4c58430a..816d5197 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -1,8 +1,7 @@ -use cosmwasm_std::{Addr, DepsMut, Env, Uint64}; +use cosmwasm_std::{DepsMut, Env, Uint64}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; use controller::{ - account::LegacyAccount, job::{Job, JobStatus, UpdateJobMsg}, Config, State, }; @@ -53,29 +52,6 @@ pub fn FINISHED_JOBS<'a>() -> IndexedMap<'a, u64, Job, JobIndexes<'a>> { IndexedMap::new("finished_jobs_v6", indexes) } -pub struct LegacyAccountIndexes<'a> { - pub account: UniqueIndex<'a, Addr, LegacyAccount>, -} - -impl IndexList for LegacyAccountIndexes<'_> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.account]; - Box::new(v.into_iter()) - } -} - -// !!! DEPRECATED !!! -// LEGACY_ACCOUNTS stores legacy account (all user's jobs share the same account, this is the old way of doing things before introducing job account) -// As of late 2023, we introduced job account meaning each job will have its own account, no more job sharing the same account -// We keep this for backward compatibility so user can withdraw from their old accounts -#[allow(non_snake_case)] -pub fn LEGACY_ACCOUNTS<'a>() -> IndexedMap<'a, Addr, LegacyAccount, LegacyAccountIndexes<'a>> { - let indexes = LegacyAccountIndexes { - account: UniqueIndex::new(|account| account.account.clone(), "accounts__account"), - }; - IndexedMap::new("accounts", indexes) -} - pub const QUERY_PAGE_SIZE: u32 = 50; pub const CONFIG: Item = Item::new("config"); pub const STATE: Item = Item::new("state"); diff --git a/contracts/warp-controller/src/util/legacy_account.rs b/contracts/warp-controller/src/util/legacy_account.rs deleted file mode 100644 index 86da41ec..00000000 --- a/contracts/warp-controller/src/util/legacy_account.rs +++ /dev/null @@ -1,8 +0,0 @@ -use controller::account::LegacyAccount; -use cosmwasm_std::Addr; - -pub fn is_legacy_account(legacy_account: Option, job_account_addr: Addr) -> bool { - legacy_account.map_or(false, |legacy_account| { - legacy_account.account == job_account_addr - }) -} diff --git a/contracts/warp-controller/src/util/mod.rs b/contracts/warp-controller/src/util/mod.rs index 5a7c7196..e87a1010 100644 --- a/contracts/warp-controller/src/util/mod.rs +++ b/contracts/warp-controller/src/util/mod.rs @@ -1,4 +1,3 @@ pub(crate) mod fee; pub(crate) mod filter; -pub(crate) mod legacy_account; pub(crate) mod msg; diff --git a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs index 1c25f770..b765ec16 100644 --- a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs +++ b/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs @@ -3,8 +3,8 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use job_account_tracker::{ - Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, InstantiateMsg, - QueryMsg, FundingAccountResponse, FundingAccountsResponse, + Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, + FundingAccountResponse, FundingAccountsResponse, InstantiateMsg, QueryMsg, }; fn main() { diff --git a/contracts/warp-legacy-account/.cargo/config b/contracts/warp-legacy-account/.cargo/config deleted file mode 100644 index b1f3d363..00000000 --- a/contracts/warp-legacy-account/.cargo/config +++ /dev/null @@ -1,4 +0,0 @@ -[alias] -wasm = "build --release --target wasm32-unknown-unknown" -unit-test = "test --lib" -schema = "run --example warp-legacy-account-schema" diff --git a/contracts/warp-legacy-account/.gitignore b/contracts/warp-legacy-account/.gitignore deleted file mode 100644 index 9095deaa..00000000 --- a/contracts/warp-legacy-account/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -# Build results -/target -/schema - -# Cargo+Git helper file (https://github.com/rust-lang/cargo/blob/0.44.1/src/cargo/sources/git/utils.rs#L320-L327) -.cargo-ok - -# Text file backups -**/*.rs.bk - -# macOS -.DS_Store - -# IDEs -*.iml -.idea diff --git a/contracts/warp-legacy-account/Cargo.toml b/contracts/warp-legacy-account/Cargo.toml deleted file mode 100644 index b4350b67..00000000 --- a/contracts/warp-legacy-account/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "warp-legacy-account" -version = "0.1.0" -authors = ["Terra Money "] -edition = "2021" - -exclude = [ - # Those files are rust-optimizer artifacts. You might want to commit them for convenience but they should not be part of the source code publication. - "contract.wasm", - "hash.txt", -] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib", "rlib"] - -[features] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] -# use library feature to disable all instantiate/execute/query exports -library = [] - -[package.metadata.scripts] -optimize = """docker run --rm -v "${process.cwd()}":/code \ - -v "${path.join(process.cwd(), "../../", "packages")}":/packages \ - --mount type=volume,source="${contract}_cache",target=/code/target \ - --mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \ - cosmwasm/rust-optimizer${process.env.TERRARIUM_ARCH_ARM64 ? "-arm64" : ""}:0.12.6 -""" - -[dependencies] -cosmwasm-std = "1.1" -cosmwasm-storage = "1.1" -cosmwasm-schema = "1.1" -base64 = "0.13.0" -cw-asset = "2.2" -cw-storage-plus = "0.16" -cw2 = "0.16" -cw20 = "0.16" -cw721 = "0.16.0" -cw-utils = "0.16" -controller = { path = "../../packages/controller", default-features = false, version = "*" } -legacy-account = { path = "../../packages/legacy-account", default-features = false, version = "*" } -schemars = "0.8" -thiserror = "1" -serde-json-wasm = "0.4.1" -json-codec-wasm = "0.1.0" -prost = "0.11.9" - -[dev-dependencies] -cw-multi-test = "0.16.0" -anyhow = "1.0.71" diff --git a/contracts/warp-legacy-account/README.md b/contracts/warp-legacy-account/README.md deleted file mode 100644 index 954383af..00000000 --- a/contracts/warp-legacy-account/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# CosmWasm Starter Pack - -This is a template to build smart contracts in Rust to run inside a -[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. -To understand the framework better, please read the overview in the -[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), -and dig into the [cosmwasm docs](https://www.cosmwasm.com). -This assumes you understand the theory and just want to get coding. - -## Creating a new repo from template - -Assuming you have a recent version of rust and cargo (v1.58.1+) installed -(via [rustup](https://rustup.rs/)), -then the following should get you a new repo to start a contract: - -Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. -Unless you did that before, run this line now: - -```sh -cargo install cargo-generate --features vendored-openssl -cargo install cargo-run-script -``` - -Now, use it to create your new contract. -Go to the folder in which you want to place it and run: - - -**Latest: 1.0.0-beta6** - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -```` - -**Older Version** - -Pass version as branch flag: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME -```` - -Example: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME -``` - -You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) -containing a simple working contract and build system that you can customize. - -## Create a Repo - -After generating, you have a initialized local git repo, but no commits, and no remote. -Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). -Then run the following: - -```sh -# this is needed to create a valid Cargo.lock file (see below) -cargo check -git branch -M main -git add . -git commit -m 'Initial Commit' -git remote add origin YOUR-GIT-URL -git push -u origin main -``` - -## CI Support - -We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) -and [Circle CI](.circleci/config.yml) in the generated project, so you can -get up and running with CI right away. - -One note is that the CI runs all `cargo` commands -with `--locked` to ensure it uses the exact same versions as you have locally. This also means -you must have an up-to-date `Cargo.lock` file, which is not auto-generated. -The first time you set up the project (or after adding any dep), you should ensure the -`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by -running `cargo check` or `cargo unit-test`. - -## Using your project - -Once you have your custom repo, you should check out [Developing](./Developing.md) to explain -more on how to run tests and develop code. Or go through the -[online tutorial](https://docs.cosmwasm.com/) to get a better feel -of how to develop. - -[Publishing](./Publishing.md) contains useful information on how to publish your contract -to the world, once you are ready to deploy it on a running blockchain. And -[Importing](./Importing.md) contains information about pulling in other contracts or crates -that have been published. - -Please replace this README file with information about your specific project. You can keep -the `Developing.md` and `Publishing.md` files as useful referenced, but please set some -proper description in the README. - -## Gitpod integration - -[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. - -Workspace contains: - - **rust**: for builds - - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client - - **jq**: shell JSON manipulation tool - -Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. - diff --git a/contracts/warp-legacy-account/examples/warp-legacy-account-schema.rs b/contracts/warp-legacy-account/examples/warp-legacy-account-schema.rs deleted file mode 100644 index cec9da8b..00000000 --- a/contracts/warp-legacy-account/examples/warp-legacy-account-schema.rs +++ /dev/null @@ -1,17 +0,0 @@ -use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; -use std::env::current_dir; -use std::fs::create_dir_all; - -use legacy_account::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; - -fn main() { - let mut out_dir = current_dir().unwrap(); - out_dir.push("schema"); - create_dir_all(&out_dir).unwrap(); - remove_schemas(&out_dir).unwrap(); - - export_schema(&schema_for!(InstantiateMsg), &out_dir); - export_schema(&schema_for!(ExecuteMsg), &out_dir); - export_schema(&schema_for!(QueryMsg), &out_dir); - export_schema(&schema_for!(Config), &out_dir); -} diff --git a/contracts/warp-legacy-account/meta/README.md b/contracts/warp-legacy-account/meta/README.md deleted file mode 100644 index 279d1db4..00000000 --- a/contracts/warp-legacy-account/meta/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# The meta folder - -This folder is ignored via the `.genignore` file. It contains meta files -that should not make it into the generated project. - -In particular, it is used for an AppVeyor CI script that runs on `cw-template` -itself (running the cargo-generate script, then testing the generated project). -The `.circleci` and `.github` directories contain scripts destined for any projects created from -this template. - -## Files - -- `appveyor.yml`: The AppVeyor CI configuration -- `test_generate.sh`: A script for generating a project from the template and - runnings builds and tests in it. This works almost like the CI script but - targets local UNIX-like dev environments. diff --git a/contracts/warp-legacy-account/meta/appveyor.yml b/contracts/warp-legacy-account/meta/appveyor.yml deleted file mode 100644 index 5da37f70..00000000 --- a/contracts/warp-legacy-account/meta/appveyor.yml +++ /dev/null @@ -1,61 +0,0 @@ -# This CI configuration tests the cw-template repository itself, -# not the resulting project. We want to ensure that -# 1. the template to project generation works -# 2. the template files are up to date -# -# We chose Appveyor for this task as it allows us to use an arbitrary config -# location. Furthermore it allows us to ship Circle CI and GitHub Actions configs -# generated for the resulting project. - -image: Ubuntu - -environment: - TOOLCHAIN: 1.58.1 - -services: - - docker - -cache: - - $HOME/.rustup/ -> meta/appveyor.yml - # For details about cargo caching see https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci - - $HOME/.cargo/bin/ -> meta/appveyor.yml - - $HOME/.cargo/registry/index/ -> meta/appveyor.yml - - $HOME/.cargo/registry/cache/ -> meta/appveyor.yml - - $HOME/.cargo/git/db/ -> meta/appveyor.yml - -install: - - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain "$TOOLCHAIN" -y - - source $HOME/.cargo/env - - rustc --version - - cargo --version - - rustup target add wasm32-unknown-unknown - - cargo install --features vendored-openssl cargo-generate || true - -build_script: - # No matter what is currently checked out by the CI (main, other branch, PR merge commit), - # we create a temporary local branch from that point with a constant name, which we need for - # cargo generate. - - git branch current-ci-checkout - - cd .. - - cargo generate --git cw-template --name testgen-ci --branch current-ci-checkout - - cd testgen-ci - - ls -lA - - cargo fmt -- --check - - cargo unit-test - - cargo wasm - - cargo schema - - docker build --pull -t "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" . - - \[ "${APPVEYOR_REPO_BRANCH}" = "main" \] && image_tag=latest || image_tag=${APPVEYOR_REPO_TAG_NAME} - - docker tag "cosmwasm/cw-gitpod-base:${APPVEYOR_REPO_COMMIT}" "cosmwasm/cw-gitpod-base:${image_tag}" - -on_success: - # publish docker image - - docker login --password-stdin -u "$DOCKER_USER" <<<"$DOCKER_PASS" - - docker push - - docker logout - -branches: -# whitelist long living branches and tags - only: - # - main - - /v\d+\.\d+\.\d+/ diff --git a/contracts/warp-legacy-account/meta/test_generate.sh b/contracts/warp-legacy-account/meta/test_generate.sh deleted file mode 100644 index b9aaa237..00000000 --- a/contracts/warp-legacy-account/meta/test_generate.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -o errexit -o nounset -o pipefail -command -v shellcheck > /dev/null && shellcheck "$0" - -REPO_ROOT="$(realpath "$(dirname "$0")/..")" - -TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/cw-template.XXXXXXXXX") -PROJECT_NAME="testgen-local" - -( - echo "Navigating to $TMP_DIR" - cd "$TMP_DIR" - - GIT_BRANCH=$(git -C "$REPO_ROOT" branch --show-current) - - echo "Generating project from local repository (branch $GIT_BRANCH) ..." - cargo generate --git "$REPO_ROOT" --name "$PROJECT_NAME" --branch "$GIT_BRANCH" - - ( - cd "$PROJECT_NAME" - echo "This is what was generated" - ls -lA - - # Check formatting - echo "Checking formatting ..." - cargo fmt -- --check - - # Debug builds first to fail fast - echo "Running unit tests ..." - cargo unit-test - echo "Creating schema ..." - cargo schema - - echo "Building wasm ..." - cargo wasm - ) -) diff --git a/contracts/warp-legacy-account/src/contract.rs b/contracts/warp-legacy-account/src/contract.rs deleted file mode 100644 index ba00f27f..00000000 --- a/contracts/warp-legacy-account/src/contract.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::state::CONFIG; -use crate::ContractError; -use controller::account::{AssetInfo, Cw721ExecuteMsg}; -use cosmwasm_std::CosmosMsg::Stargate; -use cosmwasm_std::{ - entry_point, to_binary, Addr, BankMsg, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, - Response, StdResult, Uint128, WasmMsg, -}; -use cw20::{BalanceResponse, Cw20ExecuteMsg}; -use cw721::{Cw721QueryMsg, OwnerOfResponse}; -use legacy_account::{ - Config, ExecuteMsg, IbcTransferMsg, InstantiateMsg, MigrateMsg, QueryMsg, TimeoutBlock, - WithdrawAssetsMsg, -}; -use prost::Message; - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn instantiate( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: InstantiateMsg, -) -> Result { - CONFIG.save( - deps.storage, - &Config { - owner: deps.api.addr_validate(&msg.owner)?, - warp_addr: info.sender, - }, - )?; - Ok(Response::new() - .add_attribute("action", "instantiate") - .add_attribute("contract_addr", env.contract.address) - .add_attribute("owner", msg.owner) - .add_attribute("funds", serde_json_wasm::to_string(&info.funds)?) - .add_attribute("cw_funds", serde_json_wasm::to_string(&msg.funds)?)) -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn execute( - deps: DepsMut, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.warp_addr { - return Err(ContractError::Unauthorized {}); - } - match msg { - ExecuteMsg::Generic(data) => Ok(Response::new() - .add_messages(data.msgs) - .add_attribute("action", "generic")), - ExecuteMsg::WithdrawAssets(data) => withdraw_assets(deps, env, info, data), - ExecuteMsg::IbcTransfer(data) => ibc_transfer(deps, env, info, data), - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { - match msg { - QueryMsg::Config => { - let config = CONFIG.load(deps.storage)?; - to_binary(&config) - } - } -} - -#[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { - Ok(Response::new()) -} - -pub fn ibc_transfer( - _deps: DepsMut, - env: Env, - _info: MessageInfo, - msg: IbcTransferMsg, -) -> Result { - let mut transfer_msg = msg.transfer_msg.clone(); - - if msg.timeout_block_delta.is_some() && msg.transfer_msg.timeout_block.is_some() { - let block = transfer_msg.timeout_block.unwrap(); - transfer_msg.timeout_block = Some(TimeoutBlock { - revision_number: Some(block.revision_number()), - revision_height: Some(env.block.height + msg.timeout_block_delta.unwrap()), - }) - } - - if msg.timeout_timestamp_seconds_delta.is_some() { - transfer_msg.timeout_timestamp = Some( - env.block - .time - .plus_seconds( - env.block.time.seconds() + msg.timeout_timestamp_seconds_delta.unwrap(), - ) - .nanos(), - ); - } - - Ok(Response::new().add_message(Stargate { - type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(), - value: transfer_msg.encode_to_vec().into(), - })) -} - -pub fn withdraw_assets( - deps: DepsMut, - env: Env, - info: MessageInfo, - data: WithdrawAssetsMsg, -) -> Result { - let config = CONFIG.load(deps.storage)?; - if info.sender != config.owner && info.sender != config.warp_addr { - return Err(ContractError::Unauthorized {}); - } - - let mut withdraw_msgs: Vec = vec![]; - - for asset_info in &data.asset_infos { - match asset_info { - AssetInfo::Native(denom) => { - let withdraw_native_msg = - withdraw_asset_native(deps.as_ref(), env.clone(), &config.owner, denom)?; - - match withdraw_native_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw20(addr) => { - let withdraw_cw20_msg = - withdraw_asset_cw20(deps.as_ref(), env.clone(), &config.owner, addr)?; - - match withdraw_cw20_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - AssetInfo::Cw721(addr, token_id) => { - let withdraw_cw721_msg = - withdraw_asset_cw721(deps.as_ref(), &config.owner, addr, token_id)?; - match withdraw_cw721_msg { - None => {} - Some(msg) => withdraw_msgs.push(msg), - } - } - } - } - - Ok(Response::new() - .add_messages(withdraw_msgs) - .add_attribute("action", "withdraw_assets") - .add_attribute("assets", serde_json_wasm::to_string(&data.asset_infos)?)) -} - -fn withdraw_asset_native( - deps: Deps, - env: Env, - owner: &Addr, - denom: &String, -) -> StdResult> { - let amount = deps.querier.query_balance(env.contract.address, denom)?; - - let res = if amount.amount > Uint128::zero() { - Some(CosmosMsg::Bank(BankMsg::Send { - to_address: owner.to_string(), - amount: vec![amount], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw20( - deps: Deps, - env: Env, - owner: &Addr, - token: &Addr, -) -> StdResult> { - let amount: BalanceResponse = deps.querier.query_wasm_smart( - token.to_string(), - &cw20::Cw20QueryMsg::Balance { - address: env.contract.address.to_string(), - }, - )?; - - let res = if amount.balance > Uint128::zero() { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw20ExecuteMsg::Transfer { - recipient: owner.to_string(), - amount: amount.balance, - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} - -fn withdraw_asset_cw721( - deps: Deps, - owner: &Addr, - token: &Addr, - token_id: &String, -) -> StdResult> { - let owner_query: OwnerOfResponse = deps.querier.query_wasm_smart( - token.to_string(), - &Cw721QueryMsg::OwnerOf { - token_id: token_id.to_string(), - include_expired: None, - }, - )?; - - let res = if owner_query.owner == *owner { - Some(CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: token.to_string(), - msg: to_binary(&Cw721ExecuteMsg::TransferNft { - recipient: owner.to_string(), - token_id: token_id.to_string(), - })?, - funds: vec![], - })) - } else { - None - }; - - Ok(res) -} diff --git a/contracts/warp-legacy-account/src/error.rs b/contracts/warp-legacy-account/src/error.rs deleted file mode 100644 index 85ae8cd0..00000000 --- a/contracts/warp-legacy-account/src/error.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::ContractError::{DecodeError, DeserializationError, SerializationError}; -use cosmwasm_std::StdError; -use thiserror::Error; - -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("Unauthorized")] - Unauthorized {}, - - #[error("Invalid fee")] - InvalidFee {}, - - #[error("Funds array in message does not match funds array in job.")] - FundsMismatch {}, - - #[error("Reward provided is smaller than minimum")] - RewardTooSmall {}, - - #[error("Invalid arguments")] - InvalidArguments {}, - - #[error("Custom Error val: {val:?}")] - CustomError { val: String }, - // Add any other custom errors you like here. - // Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. - #[error("Error deserializing data")] - DeserializationError {}, - - #[error("Error serializing data")] - SerializationError {}, - - #[error("Error decoding JSON result")] - DecodeError {}, - - #[error("Error resolving JSON path")] - ResolveError {}, - - #[error("Sub account already taken")] - SubAccountAlreadyTakenError {}, - - #[error("Sub account already free")] - SubAccountAlreadyFreeError {}, - - #[error("Sub account should be taken but it is free")] - SubAccountNotTakenError {}, -} - -impl From for ContractError { - fn from(_: serde_json_wasm::de::Error) -> Self { - DeserializationError {} - } -} - -impl From for ContractError { - fn from(_: serde_json_wasm::ser::Error) -> Self { - SerializationError {} - } -} - -impl From for ContractError { - fn from(_: json_codec_wasm::DecodeError) -> Self { - DecodeError {} - } -} - -impl From for ContractError { - fn from(_: base64::DecodeError) -> Self { - DecodeError {} - } -} diff --git a/contracts/warp-legacy-account/src/lib.rs b/contracts/warp-legacy-account/src/lib.rs deleted file mode 100644 index 90d6bfa8..00000000 --- a/contracts/warp-legacy-account/src/lib.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod contract; -mod error; -pub mod state; - -#[cfg(test)] -mod tests; - -pub use crate::error::ContractError; diff --git a/contracts/warp-legacy-account/src/state.rs b/contracts/warp-legacy-account/src/state.rs deleted file mode 100644 index 9483f694..00000000 --- a/contracts/warp-legacy-account/src/state.rs +++ /dev/null @@ -1,4 +0,0 @@ -use cw_storage_plus::Item; -use legacy_account::Config; - -pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/warp-legacy-account/src/tests.rs b/contracts/warp-legacy-account/src/tests.rs deleted file mode 100644 index 68871a06..00000000 --- a/contracts/warp-legacy-account/src/tests.rs +++ /dev/null @@ -1,327 +0,0 @@ -use crate::contract::{execute, instantiate}; -use crate::ContractError; -use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; -use cosmwasm_std::{ - to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, - IbcTimeoutBlock, Response, StakingMsg, Uint128, VoteOption, WasmMsg, -}; -use legacy_account::{ExecuteMsg, GenericMsg, InstantiateMsg}; - -#[test] -fn test_execute_controller() { - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info("vlad_controller", &[]); - - let _instantiate_res = instantiate( - deps.as_mut(), - env.clone(), - info.clone(), - InstantiateMsg { - owner: "vlad".to_string(), - funds: None, - }, - ); - - let execute_msg = ExecuteMsg::Generic(GenericMsg { - msgs: vec![ - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - }), - CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes, - }), - CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0, - }), - }), - CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default(), - }, - ], - }); - - let execute_res = execute(deps.as_mut(), env, info, execute_msg).unwrap(); - - assert_eq!( - execute_res, - Response::new() - .add_attribute("action", "generic") - .add_messages(vec![ - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }] - }), - CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes - }), - CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0 - }), - }), - CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default() - } - ]) - ) -} - -#[test] -fn test_execute_owner() { - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info("vlad_controller", &[]); - - let _instantiate_res = instantiate( - deps.as_mut(), - env.clone(), - info, - InstantiateMsg { - owner: "vlad".to_string(), - funds: None, - }, - ); - - let execute_msg = ExecuteMsg::Generic(GenericMsg { - msgs: vec![ - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - }), - CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes, - }), - CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0, - }), - }), - CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default(), - }, - ], - }); - - let info2 = mock_info("vlad", &[]); - - let execute_res = execute(deps.as_mut(), env, info2, execute_msg).unwrap(); - - assert_eq!( - execute_res, - Response::new() - .add_attribute("action", "generic") - .add_messages(vec![ - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }] - }), - CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes - }), - CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100) - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0 - }), - }), - CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default() - } - ]) - ) -} - -#[test] -fn test_execute_unauth() { - let mut deps = mock_dependencies(); - let env = mock_env(); - let info = mock_info("vlad_controller", &[]); - - let _instantiate_res = instantiate( - deps.as_mut(), - env.clone(), - info, - InstantiateMsg { - owner: "vlad".to_string(), - funds: None, - }, - ); - - let execute_msg = ExecuteMsg::Generic(GenericMsg { - msgs: vec![ - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: "contract".to_string(), - msg: to_binary("test").unwrap(), - funds: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - }), - CosmosMsg::Bank(BankMsg::Send { - to_address: "vlad2".to_string(), - amount: vec![Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }], - }), - CosmosMsg::Gov(GovMsg::Vote { - proposal_id: 0, - vote: VoteOption::Yes, - }), - CosmosMsg::Staking(StakingMsg::Delegate { - validator: "vladidator".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - }), - CosmosMsg::Distribution(DistributionMsg::SetWithdrawAddress { - address: "vladdress".to_string(), - }), - CosmosMsg::Ibc(IbcMsg::Transfer { - channel_id: "channel_vlad".to_string(), - to_address: "vlad3".to_string(), - amount: Coin { - denom: "coin".to_string(), - amount: Uint128::new(100), - }, - timeout: IbcTimeout::with_block(IbcTimeoutBlock { - revision: 0, - height: 0, - }), - }), - CosmosMsg::Stargate { - type_url: "utl".to_string(), - value: Default::default(), - }, - ], - }); - - let info2 = mock_info("vlad2", &[]); - - let execute_res = execute(deps.as_mut(), env, info2, execute_msg).unwrap_err(); - - assert_eq!(execute_res, ContractError::Unauthorized {}) -} diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index 376b99e6..b43fb8d8 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -51,33 +51,6 @@ pub enum Cw721ExecuteMsg { TransferNft { recipient: String, token_id: String }, } -#[cw_serde] -pub struct QueryLegacyAccountMsg { - pub owner: String, -} - -#[cw_serde] -pub struct QueryLegacyAccountsMsg { - pub start_after: Option, - pub limit: Option, -} - -#[cw_serde] -pub struct LegacyAccount { - pub owner: Addr, - pub account: Addr, -} - -#[cw_serde] -pub struct LegacyAccountResponse { - pub account: LegacyAccount, -} - -#[cw_serde] -pub struct LegacyAccountsResponse { - pub accounts: Vec, -} - #[cw_serde] pub enum AssetInfo { Native(String), diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index cb797bca..ad7fab1b 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -1,6 +1,3 @@ -use crate::account::{ - LegacyAccountResponse, LegacyAccountsResponse, QueryLegacyAccountMsg, QueryLegacyAccountsMsg, -}; use crate::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, JobResponse, JobsResponse, QueryJobMsg, QueryJobsMsg, UpdateJobMsg, @@ -100,7 +97,6 @@ pub enum ExecuteMsg { UpdateConfig(UpdateConfigMsg), - MigrateLegacyAccounts(MigrateLegacyAccountsMsg), MigrateFreeJobAccounts(MigrateJobAccountsMsg), MigrateTakenJobAccounts(MigrateJobAccountsMsg), @@ -136,13 +132,6 @@ pub struct UpdateConfigMsg { pub burn_fee_rate: Option, } -#[cw_serde] -pub struct MigrateLegacyAccountsMsg { - pub warp_legacy_account_code_id: Uint64, - pub start_after: Option, - pub limit: u8, -} - #[cw_serde] pub struct MigrateJobAccountsMsg { pub account_owner_addr: String, @@ -169,13 +158,6 @@ pub enum QueryMsg { #[returns(JobsResponse)] QueryJobs(QueryJobsMsg), - // For job account, please query it via the account tracker contract - // You can look at account tracker contract for more details - #[returns(LegacyAccountResponse)] - QueryLegacyAccount(QueryLegacyAccountMsg), - #[returns(LegacyAccountsResponse)] - QueryLegacyAccounts(QueryLegacyAccountsMsg), - #[returns(ConfigResponse)] QueryConfig(QueryConfigMsg), diff --git a/packages/legacy-account/.cargo/config b/packages/legacy-account/.cargo/config deleted file mode 100644 index c7f1d9fa..00000000 --- a/packages/legacy-account/.cargo/config +++ /dev/null @@ -1,4 +0,0 @@ -[alias] -wasm = "build --release --target wasm32-unknown-unknown" -unit-test = "test --lib" -schema = "run --example warp-protocol-schema" diff --git a/packages/legacy-account/Cargo.toml b/packages/legacy-account/Cargo.toml deleted file mode 100644 index 8d2455da..00000000 --- a/packages/legacy-account/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "legacy-account" -version = "0.1.0" -authors = ["Terra Money "] -edition = "2021" - -[features] -# for more explicit tests, cargo test --features=backtraces -backtraces = ["cosmwasm-std/backtraces"] - -[dependencies] -cosmwasm-std = "1.1" -cosmwasm-schema = "1.1" -schemars = "0.8" -serde = { version = "1", default-features = false, features = ["derive"] } -prost = "0.11.9" - -controller = { path = "../controller" } - -[dev-dependencies] -cw-multi-test = "0.16" diff --git a/packages/legacy-account/README.md b/packages/legacy-account/README.md deleted file mode 100644 index 954383af..00000000 --- a/packages/legacy-account/README.md +++ /dev/null @@ -1,106 +0,0 @@ -# CosmWasm Starter Pack - -This is a template to build smart contracts in Rust to run inside a -[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) module on all chains that enable it. -To understand the framework better, please read the overview in the -[cosmwasm repo](https://github.com/CosmWasm/cosmwasm/blob/master/README.md), -and dig into the [cosmwasm docs](https://www.cosmwasm.com). -This assumes you understand the theory and just want to get coding. - -## Creating a new repo from template - -Assuming you have a recent version of rust and cargo (v1.58.1+) installed -(via [rustup](https://rustup.rs/)), -then the following should get you a new repo to start a contract: - -Install [cargo-generate](https://github.com/ashleygwilliams/cargo-generate) and cargo-run-script. -Unless you did that before, run this line now: - -```sh -cargo install cargo-generate --features vendored-openssl -cargo install cargo-run-script -``` - -Now, use it to create your new contract. -Go to the folder in which you want to place it and run: - - -**Latest: 1.0.0-beta6** - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --name PROJECT_NAME -```` - -**Older Version** - -Pass version as branch flag: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch --name PROJECT_NAME -```` - -Example: - -```sh -cargo generate --git https://github.com/CosmWasm/cw-template.git --branch 0.16 --name PROJECT_NAME -``` - -You will now have a new folder called `PROJECT_NAME` (I hope you changed that to something else) -containing a simple working contract and build system that you can customize. - -## Create a Repo - -After generating, you have a initialized local git repo, but no commits, and no remote. -Go to a server (eg. github) and create a new upstream repo (called `YOUR-GIT-URL` below). -Then run the following: - -```sh -# this is needed to create a valid Cargo.lock file (see below) -cargo check -git branch -M main -git add . -git commit -m 'Initial Commit' -git remote add origin YOUR-GIT-URL -git push -u origin main -``` - -## CI Support - -We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) -and [Circle CI](.circleci/config.yml) in the generated project, so you can -get up and running with CI right away. - -One note is that the CI runs all `cargo` commands -with `--locked` to ensure it uses the exact same versions as you have locally. This also means -you must have an up-to-date `Cargo.lock` file, which is not auto-generated. -The first time you set up the project (or after adding any dep), you should ensure the -`Cargo.lock` file is updated, so the CI will test properly. This can be done simply by -running `cargo check` or `cargo unit-test`. - -## Using your project - -Once you have your custom repo, you should check out [Developing](./Developing.md) to explain -more on how to run tests and develop code. Or go through the -[online tutorial](https://docs.cosmwasm.com/) to get a better feel -of how to develop. - -[Publishing](./Publishing.md) contains useful information on how to publish your contract -to the world, once you are ready to deploy it on a running blockchain. And -[Importing](./Importing.md) contains information about pulling in other contracts or crates -that have been published. - -Please replace this README file with information about your specific project. You can keep -the `Developing.md` and `Publishing.md` files as useful referenced, but please set some -proper description in the README. - -## Gitpod integration - -[Gitpod](https://www.gitpod.io/) container-based development platform will be enabled on your project by default. - -Workspace contains: - - **rust**: for builds - - [wasmd](https://github.com/CosmWasm/wasmd): for local node setup and client - - **jq**: shell JSON manipulation tool - -Follow [Gitpod Getting Started](https://www.gitpod.io/docs/getting-started) and launch your workspace. - diff --git a/packages/legacy-account/examples/account-schema.rs b/packages/legacy-account/examples/account-schema.rs deleted file mode 100644 index b6ba6260..00000000 --- a/packages/legacy-account/examples/account-schema.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::env::current_dir; -use std::fs::create_dir_all; - -use controller::QueryMsg; -use controller::{ExecuteMsg, InstantiateMsg}; -use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; - -fn main() { - let mut out_dir = current_dir().unwrap(); - out_dir.push("schema"); - create_dir_all(&out_dir).unwrap(); - remove_schemas(&out_dir).unwrap(); - - export_schema(&schema_for!(InstantiateMsg), &out_dir); - export_schema(&schema_for!(ExecuteMsg), &out_dir); - export_schema(&schema_for!(QueryMsg), &out_dir); -} diff --git a/packages/legacy-account/src/lib.rs b/packages/legacy-account/src/lib.rs deleted file mode 100644 index a261a62c..00000000 --- a/packages/legacy-account/src/lib.rs +++ /dev/null @@ -1,95 +0,0 @@ -use controller::account::{AssetInfo, CwFund}; -use cosmwasm_schema::cw_serde; -use cosmwasm_std::{Addr, CosmosMsg}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[cw_serde] -pub struct Config { - pub owner: Addr, - pub warp_addr: Addr, -} - -#[cw_serde] -pub struct InstantiateMsg { - pub owner: String, - pub funds: Option>, -} - -#[cw_serde] -#[allow(clippy::large_enum_variant)] -pub enum ExecuteMsg { - Generic(GenericMsg), - WithdrawAssets(WithdrawAssetsMsg), - IbcTransfer(IbcTransferMsg), -} - -#[cw_serde] -pub struct GenericMsg { - pub msgs: Vec, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] -pub struct Coin { - #[prost(string, tag = "1")] - pub denom: String, - #[prost(string, tag = "2")] - pub amount: String, -} - -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] -pub struct TimeoutBlock { - #[prost(uint64, optional, tag = "1")] - pub revision_number: Option, - #[prost(uint64, optional, tag = "2")] - pub revision_height: Option, -} -#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, prost::Message)] -pub struct TransferMsg { - #[prost(string, tag = "1")] - pub source_port: String, - - #[prost(string, tag = "2")] - pub source_channel: String, - - #[prost(message, optional, tag = "3")] - pub token: Option, - - #[prost(string, tag = "4")] - pub sender: String, - - #[prost(string, tag = "5")] - pub receiver: String, - - #[prost(message, optional, tag = "6")] - pub timeout_block: Option, - - #[prost(uint64, optional, tag = "7")] - pub timeout_timestamp: Option, - - #[prost(string, tag = "8")] - pub memo: String, -} - -#[cw_serde] -pub struct IbcTransferMsg { - pub transfer_msg: TransferMsg, - pub timeout_block_delta: Option, - pub timeout_timestamp_seconds_delta: Option, -} - -#[cw_serde] -pub struct WithdrawAssetsMsg { - pub asset_infos: Vec, -} - -#[cw_serde] -pub struct ExecuteWasmMsg {} - -#[cw_serde] -pub enum QueryMsg { - Config, -} - -#[cw_serde] -pub struct MigrateMsg {} From da4a9be4f854c26c56bb8f725de7ad10a722d4a2 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 1 Dec 2023 22:23:56 +0100 Subject: [PATCH 096/133] refactor reply logic to not depend on job_id from msg.id, rather use it as a pattern match based on constants and extract job_id from message response --- contracts/warp-controller/src/contract.rs | 31 +++++++++++++------ .../warp-controller/src/execute/account.rs | 7 +++-- contracts/warp-controller/src/execute/job.rs | 11 +++++-- contracts/warp-controller/src/reply/job.rs | 27 +++++++++++++++- contracts/warp-controller/src/util/msg.rs | 1 + contracts/warp-job-account/src/tests.rs | 3 ++ packages/controller/src/account.rs | 13 ++++++-- 7 files changed, 75 insertions(+), 18 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 4522c91a..88d63c5d 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -21,8 +21,7 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { let state = State { - // start from 10, 0-9 reserved for reply logic - current_job_id: Uint64::from(10u64), + current_job_id: Uint64::one(), q: Uint64::zero(), }; @@ -82,7 +81,7 @@ pub fn instantiate( CONFIG.save(deps.storage, &config)?; let submsgs = vec![SubMsg { - id: 3, + id: REPLY_ID_INSTANTIATE_SUB_CONTRACTS, msg: build_instantiate_job_account_tracker_msg( config.owner.to_string(), env.contract.address.to_string(), @@ -179,16 +178,30 @@ pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result Result { let config = CONFIG.load(deps.storage)?; - // 0-10 reserved match msg.id { - 0 => reply::account::create_job_account_and_job(deps, env, msg, config), - 1 => reply::account::create_funding_account_and_job(deps, env, msg, config), - 2 => reply::account::create_funding_account(deps, env, msg, config), - 3 => reply::job::instantiate_sub_contracts(deps, env, msg, config), - _id => reply::job::execute_job(deps, env, msg, config), + REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB => { + reply::account::create_job_account_and_job(deps, env, msg, config) + } + REPLY_ID_CREATE_FUNDING_ACCOUNT_AND_JOB => { + reply::account::create_funding_account_and_job(deps, env, msg, config) + } + REPLY_ID_CREATE_FUNDING_ACCOUNT => { + reply::account::create_funding_account(deps, env, msg, config) + } + REPLY_ID_INSTANTIATE_SUB_CONTRACTS => { + reply::job::instantiate_sub_contracts(deps, env, msg, config) + } + REPLY_ID_EXECUTE_JOB => reply::job::execute_job(deps, env, msg, config), + _ => panic!(), } } diff --git a/contracts/warp-controller/src/execute/account.rs b/contracts/warp-controller/src/execute/account.rs index d192641e..552e7e7d 100644 --- a/contracts/warp-controller/src/execute/account.rs +++ b/contracts/warp-controller/src/execute/account.rs @@ -1,7 +1,10 @@ use controller::CreateFundingAccountMsg; use cosmwasm_std::{DepsMut, Env, MessageInfo, ReplyOn, Response, SubMsg, Uint64}; -use crate::{state::CONFIG, util::msg::build_instantiate_warp_account_msg, ContractError}; +use crate::{ + contract::REPLY_ID_CREATE_FUNDING_ACCOUNT, state::CONFIG, + util::msg::build_instantiate_warp_account_msg, ContractError, +}; pub fn create_funding_account( deps: DepsMut, @@ -12,7 +15,7 @@ pub fn create_funding_account( let config = CONFIG.load(deps.storage)?; let submsgs = vec![SubMsg { - id: 2, + id: REPLY_ID_CREATE_FUNDING_ACCOUNT, msg: build_instantiate_warp_account_msg( Uint64::from(0u64), // placeholder env.contract.address.to_string(), diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index ad0ae5fc..fc385298 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,3 +1,7 @@ +use crate::contract::{ + REPLY_ID_CREATE_FUNDING_ACCOUNT_AND_JOB, REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, + REPLY_ID_EXECUTE_JOB, +}; use crate::state::{JobQueue, STATE}; use crate::util::msg::{ build_account_execute_warp_msgs, build_free_funding_account_msg, build_take_funding_account_msg, @@ -168,7 +172,7 @@ pub fn create_job( None => { // Create account then create job in reply submsgs.push(SubMsg { - id: 0, + id: REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, msg: build_instantiate_warp_account_msg( job.id, env.contract.address.to_string(), @@ -269,7 +273,7 @@ pub fn create_job( None => { // Create funding account then create job in reply submsgs.push(SubMsg { - id: 1, + id: REPLY_ID_CREATE_FUNDING_ACCOUNT_AND_JOB, msg: build_instantiate_warp_account_msg( job.id, env.contract.address.to_string(), @@ -502,7 +506,7 @@ pub fn execute_job( match resolution { Ok(true) => { submsgs.push(SubMsg { - id: data.id.u64(), + id: REPLY_ID_EXECUTE_JOB, msg: CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job.account.to_string(), msg: to_binary(&job_account::ExecuteMsg::WarpMsgs(WarpMsgs { @@ -513,6 +517,7 @@ pub fn execute_job( vars, }), )?, + job_id: Some(data.id), }))?, funds: vec![], }), diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 2a313e05..129124b8 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -32,7 +32,32 @@ pub fn execute_job( SubMsgResult::Err(_) => JobStatus::Failed, }; - let job_id = msg.id; + let reply: cosmwasm_std::SubMsgResponse = msg + .result + .clone() + .into_result() + .map_err(StdError::generic_err)?; + + let warp_msgs_event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "warp_msgs") + }) + .ok_or_else(|| StdError::generic_err("cannot find `warp_msgs` event"))?; + + let job_id_str = warp_msgs_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "job_id") + .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? + .value; + + let job_id = job_id_str.as_str().parse::()?; let finished_job = JobQueue::finalize(&mut deps, env.clone(), job_id, new_status)?; diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index e2d149c6..b17d1cdc 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -217,6 +217,7 @@ pub fn build_account_execute_warp_msgs( contract_addr: account_addr, msg: to_binary(&job_account::ExecuteMsg::WarpMsgs(WarpMsgs { msgs: warp_msgs_for_account_to_execute, + job_id: None, })) .unwrap(), funds: vec![], diff --git a/contracts/warp-job-account/src/tests.rs b/contracts/warp-job-account/src/tests.rs index 0b0f86e8..d14bb94d 100644 --- a/contracts/warp-job-account/src/tests.rs +++ b/contracts/warp-job-account/src/tests.rs @@ -77,6 +77,7 @@ fn test_execute_controller() { value: Default::default(), }), ], + job_id: None, }); let execute_res = execute(deps.as_mut(), env, info, execute_msg).unwrap(); @@ -204,6 +205,7 @@ fn test_execute_owner() { value: Default::default(), }), ], + job_id: None, }); let info2 = mock_info("vlad", &[]); @@ -333,6 +335,7 @@ fn test_execute_unauth() { value: Default::default(), }), ], + job_id: None, }); let info2 = mock_info("vlad2", &[]); diff --git a/packages/controller/src/account.rs b/packages/controller/src/account.rs index b43fb8d8..14283ae0 100644 --- a/packages/controller/src/account.rs +++ b/packages/controller/src/account.rs @@ -1,6 +1,6 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::CosmosMsg::Stargate; -use cosmwasm_std::{to_binary, BankMsg, DepsMut, WasmMsg}; +use cosmwasm_std::{to_binary, BankMsg, DepsMut, Uint64, WasmMsg}; use cosmwasm_std::{Addr, CosmosMsg, Deps, Env, Response, StdError, StdResult, Uint128}; use cw20::{BalanceResponse, Cw20ExecuteMsg}; use cw721::{Cw721QueryMsg, OwnerOfResponse}; @@ -61,6 +61,7 @@ pub enum AssetInfo { #[cw_serde] pub struct WarpMsgs { pub msgs: Vec, + pub job_id: Option, } #[cw_serde] @@ -132,9 +133,15 @@ pub fn execute_warp_msgs( ) -> Result { let msgs = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, data.msgs, owner).unwrap(); - Ok(Response::new() + let mut resp = Response::new() .add_messages(msgs) - .add_attribute("action", "warp_msgs")) + .add_attribute("action", "warp_msgs"); + + if let Some(job_id) = data.job_id { + resp = resp.add_attribute("job_id", job_id); + } + + Ok(resp) } pub fn warp_msgs_to_cosmos_msgs( From 7f741599253258b0733605e96b2c2b7ace7b5a1c Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 8 Dec 2023 20:37:56 +0100 Subject: [PATCH 097/133] update deployment script --- tasks/deploy_warp.ts | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/tasks/deploy_warp.ts b/tasks/deploy_warp.ts index 31dbe0a0..d1432380 100644 --- a/tasks/deploy_warp.ts +++ b/tasks/deploy_warp.ts @@ -1,11 +1,10 @@ import task from "@terra-money/terrariums"; task(async ({ deployer, signer, refs }) => { - //account - deployer.buildContract("warp-account"); - deployer.optimizeContract("warp-account"); + deployer.buildContract("warp-controller"); + deployer.optimizeContract("warp-controller"); - const id = await deployer.storeCode("warp-account"); + const job_account_contract_id = await deployer.storeCode("warp-job-account"); await new Promise((resolve) => setTimeout(resolve, 10000)); await deployer.storeCode("warp-resolver"); @@ -17,35 +16,56 @@ task(async ({ deployer, signer, refs }) => { await deployer.storeCode("warp-controller"); await new Promise((resolve) => setTimeout(resolve, 10000)); + const job_account_tracker_id = await deployer.storeCode( + "warp-job-account-tracker" + ); + await new Promise((resolve) => setTimeout(resolve, 10000)); + const instantiateTemplatesMsg = { owner: signer.key.accAddress, fee_collector: signer.key.accAddress, templates: [], fee_denom: "uluna", - } + }; await deployer.instantiate("warp-templates", instantiateTemplatesMsg, { admin: signer.key.accAddress, }); await new Promise((resolve) => setTimeout(resolve, 10000)); - let resolver_address = await deployer.instantiate("warp-resolver", {}, { - admin: signer.key.accAddress, - }); + let resolver_address = await deployer.instantiate( + "warp-resolver", + {}, + { + admin: signer.key.accAddress, + } + ); await new Promise((resolve) => setTimeout(resolve, 10000)); const instantiateControllerMsg = { - warp_account_code_id: id, fee_denom: "uluna", + fee_collector: signer.key.accAddress, + warp_account_code_id: job_account_contract_id, + job_account_tracker_code_id: job_account_tracker_id, + minimum_reward: "10000", creation_fee: "5", cancellation_fee: "5", - minimum_reward: "10000", - resolver_address: resolver_address.address, + resolver_address: resolver_address, t_max: "86400", t_min: "86400", a_max: "10000", a_min: "10000", q_max: "10", + creation_fee_min: "500000", + creation_fee_max: "100000000", + burn_fee_min: "100000", + maintenance_fee_min: "50000", + maintenance_fee_max: "10000000", + duration_days_left: "10", + duration_days_right: "100", + queue_size_left: "5000", + queue_size_right: "50000", + burn_fee_rate: "25", }; await deployer.instantiate("warp-controller", instantiateControllerMsg, { From 77b6577d60a2d4ce92d48fbe7048598f503ddc4b Mon Sep 17 00:00:00 2001 From: simke9445 Date: Sat, 9 Dec 2023 18:01:14 +0100 Subject: [PATCH 098/133] remove query_free_account query --- .../warp-job-account-tracker/src/contract.rs | 3 --- .../src/query/account.rs | 27 +------------------ packages/job-account-tracker/src/lib.rs | 2 -- 3 files changed, 1 insertion(+), 31 deletions(-) diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index d17dc037..f801f771 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -82,9 +82,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryFirstFreeAccount(data) => { to_binary(&query::account::query_first_free_account(deps, data)?) } - QueryMsg::QueryFreeAccount(data) => { - to_binary(&query::account::query_free_account(deps, data)?) - } QueryMsg::QueryFundingAccounts(data) => { to_binary(&query::account::query_funding_accounts(deps, data)?) } diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-job-account-tracker/src/query/account.rs index d6a483af..cf9c84f5 100644 --- a/contracts/warp-job-account-tracker/src/query/account.rs +++ b/contracts/warp-job-account-tracker/src/query/account.rs @@ -6,8 +6,7 @@ use crate::state::{CONFIG, FREE_ACCOUNTS, FUNDING_ACCOUNTS_BY_USER, TAKEN_ACCOUN use job_account_tracker::{ Account, AccountResponse, AccountsResponse, ConfigResponse, FundingAccountResponse, FundingAccountsResponse, QueryFirstFreeAccountMsg, QueryFirstFreeFundingAccountMsg, - QueryFreeAccountMsg, QueryFreeAccountsMsg, QueryFundingAccountMsg, QueryFundingAccountsMsg, - QueryTakenAccountsMsg, + QueryFreeAccountsMsg, QueryFundingAccountMsg, QueryFundingAccountsMsg, QueryTakenAccountsMsg, }; const QUERY_LIMIT: u32 = 50; @@ -119,30 +118,6 @@ pub fn query_free_accounts(deps: Deps, data: QueryFreeAccountsMsg) -> StdResult< }) } -pub fn query_free_account(deps: Deps, data: QueryFreeAccountMsg) -> StdResult { - let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; - let maybe_free_account = FREE_ACCOUNTS - .prefix_range( - deps.storage, - Some(PrefixBound::inclusive(account_addr_ref)), - Some(PrefixBound::inclusive(account_addr_ref)), - Order::Ascending, - ) - .next(); - - let free_account = match maybe_free_account { - Some(Ok((account, last_job_id))) => Some(Account { - addr: account.1, - taken_by_job_id: Some(last_job_id), - }), - _ => None, - }; - - Ok(AccountResponse { - account: free_account, - }) -} - // funding accounts pub fn query_funding_account( diff --git a/packages/job-account-tracker/src/lib.rs b/packages/job-account-tracker/src/lib.rs index d181dc27..d9ec4b01 100644 --- a/packages/job-account-tracker/src/lib.rs +++ b/packages/job-account-tracker/src/lib.rs @@ -69,8 +69,6 @@ pub enum QueryMsg { QueryFreeAccounts(QueryFreeAccountsMsg), #[returns(AccountResponse)] QueryFirstFreeAccount(QueryFirstFreeAccountMsg), - #[returns(AccountResponse)] - QueryFreeAccount(QueryFreeAccountMsg), #[returns(FundingAccountResponse)] QueryFirstFreeFundingAccount(QueryFirstFreeFundingAccountMsg), #[returns(FundingAccountsResponse)] From 68ceb29e77d6425f33e8c568d95359895a3f30ee Mon Sep 17 00:00:00 2001 From: simke9445 Date: Sat, 9 Dec 2023 18:04:57 +0100 Subject: [PATCH 099/133] remove comment --- contracts/warp-job-account-tracker/src/contract.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-job-account-tracker/src/contract.rs index f801f771..48e10b61 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-job-account-tracker/src/contract.rs @@ -18,8 +18,6 @@ pub fn instantiate( CONFIG.save( deps.storage, &Config { - // owner: deps.api.addr_validate(&msg.owner)?, - // creator_addr: info.sender, admin: deps.api.addr_validate(&msg.admin)?, warp_addr: deps.api.addr_validate(&msg.warp_addr)?, }, From da18f3eedff81f1b2ec08ce498d3f9a213feca1c Mon Sep 17 00:00:00 2001 From: simke9445 Date: Sat, 9 Dec 2023 18:33:03 +0100 Subject: [PATCH 100/133] rename job-account to account everywhere --- Cargo.lock | 68 +++++++++---------- .../.cargo/config | 2 +- .../.gitignore | 0 .../Cargo.toml | 4 +- .../README.md | 0 .../examples/warp-account-tracker-schema.rs} | 2 +- .../meta/README.md | 0 .../meta/appveyor.yml | 0 .../meta/test_generate.sh | 0 .../src/contract.rs | 4 +- .../src/error.rs | 0 .../src/execute/account.rs | 2 +- .../src/execute/mod.rs | 0 .../src/integration_tests.rs | 58 ++++++++-------- .../src/lib.rs | 0 .../src/query/account.rs | 2 +- .../src/query/mod.rs | 0 .../src/state.rs | 2 +- .../.cargo/config | 2 +- .../.gitignore | 0 .../Cargo.toml | 4 +- .../README.md | 0 .../examples/warp-account-schema.rs} | 2 +- .../meta/README.md | 0 .../meta/appveyor.yml | 0 .../meta/test_generate.sh | 0 .../src/contract.rs | 2 +- .../src/error.rs | 0 .../src/lib.rs | 0 .../src/query/account.rs | 2 +- .../src/query/mod.rs | 0 .../src/state.rs | 2 +- .../src/tests.rs | 2 +- contracts/warp-controller/Cargo.toml | 4 +- contracts/warp-controller/src/contract.rs | 18 ++--- contracts/warp-controller/src/error.rs | 2 +- contracts/warp-controller/src/execute/job.rs | 56 +++++++-------- .../migrate/{job_account.rs => account.rs} | 36 +++++----- contracts/warp-controller/src/migrate/mod.rs | 2 +- .../warp-controller/src/reply/account.rs | 42 ++++++------ contracts/warp-controller/src/reply/job.rs | 22 +++--- contracts/warp-controller/src/util/msg.rs | 40 +++++------ .../Cargo.toml | 2 +- .../README.md | 0 .../src/lib.rs | 0 .../{job-account => account}/.cargo/config | 0 packages/{job-account => account}/Cargo.toml | 2 +- packages/{job-account => account}/README.md | 0 .../examples/account-schema.rs | 0 packages/{job-account => account}/src/lib.rs | 0 packages/controller/src/lib.rs | 12 ++-- refs.json | 4 +- tasks/deploy_warp.ts | 10 +-- 53 files changed, 206 insertions(+), 206 deletions(-) rename contracts/{warp-job-account-tracker => warp-account-tracker}/.cargo/config (61%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/.gitignore (100%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/Cargo.toml (90%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/README.md (100%) rename contracts/{warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs => warp-account-tracker/examples/warp-account-tracker-schema.rs} (97%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/meta/README.md (100%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/meta/appveyor.yml (100%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/meta/test_generate.sh (100%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/src/contract.rs (95%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/src/error.rs (100%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/src/execute/account.rs (99%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/src/execute/mod.rs (100%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/src/integration_tests.rs (86%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/src/lib.rs (100%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/src/query/account.rs (99%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/src/query/mod.rs (100%) rename contracts/{warp-job-account-tracker => warp-account-tracker}/src/state.rs (94%) rename contracts/{warp-job-account => warp-account}/.cargo/config (64%) rename contracts/{warp-job-account => warp-account}/.gitignore (100%) rename contracts/{warp-job-account => warp-account}/Cargo.toml (92%) rename contracts/{warp-job-account => warp-account}/README.md (100%) rename contracts/{warp-job-account/examples/warp-job-account-schema.rs => warp-account/examples/warp-account-schema.rs} (88%) rename contracts/{warp-job-account => warp-account}/meta/README.md (100%) rename contracts/{warp-job-account => warp-account}/meta/appveyor.yml (100%) rename contracts/{warp-job-account => warp-account}/meta/test_generate.sh (100%) rename contracts/{warp-job-account => warp-account}/src/contract.rs (96%) rename contracts/{warp-job-account => warp-account}/src/error.rs (100%) rename contracts/{warp-job-account => warp-account}/src/lib.rs (100%) rename contracts/{warp-job-account => warp-account}/src/query/account.rs (86%) rename contracts/{warp-job-account => warp-account}/src/query/mod.rs (100%) rename contracts/{warp-job-account => warp-account}/src/state.rs (76%) rename contracts/{warp-job-account => warp-account}/src/tests.rs (99%) rename contracts/warp-controller/src/migrate/{job_account.rs => account.rs} (58%) rename packages/{job-account-tracker => account-tracker}/Cargo.toml (91%) rename packages/{job-account-tracker => account-tracker}/README.md (100%) rename packages/{job-account-tracker => account-tracker}/src/lib.rs (100%) rename packages/{job-account => account}/.cargo/config (100%) rename packages/{job-account => account}/Cargo.toml (95%) rename packages/{job-account => account}/README.md (100%) rename packages/{job-account => account}/examples/account-schema.rs (100%) rename packages/{job-account => account}/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index abf7dcb6..ebe45581 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,28 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "account" +version = "0.1.0" +dependencies = [ + "controller", + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", + "prost 0.11.9", + "schemars", + "serde", +] + +[[package]] +name = "account-tracker" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-multi-test", +] + [[package]] name = "ahash" version = "0.7.6" @@ -564,28 +586,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" -[[package]] -name = "job-account" -version = "0.1.0" -dependencies = [ - "controller", - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", - "prost 0.11.9", - "schemars", - "serde", -] - -[[package]] -name = "job-account-tracker" -version = "0.1.0" -dependencies = [ - "cosmwasm-schema", - "cosmwasm-std", - "cw-multi-test", -] - [[package]] name = "json-codec-wasm" version = "0.1.0" @@ -1007,9 +1007,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] -name = "warp-controller" +name = "warp-account" version = "0.1.0" dependencies = [ + "account", + "anyhow", "base64", "controller", "cosmwasm-schema", @@ -1021,22 +1023,21 @@ dependencies = [ "cw-utils 0.16.0", "cw2 0.16.0", "cw20", - "job-account", - "job-account-tracker", + "cw721", "json-codec-wasm", - "resolver", + "prost 0.11.9", "schemars", "serde-json-wasm 0.4.1", "thiserror", ] [[package]] -name = "warp-job-account" +name = "warp-account-tracker" version = "0.1.0" dependencies = [ + "account-tracker", "anyhow", "base64", - "controller", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", @@ -1047,7 +1048,6 @@ dependencies = [ "cw2 0.16.0", "cw20", "cw721", - "job-account", "json-codec-wasm", "prost 0.11.9", "schemars", @@ -1056,11 +1056,13 @@ dependencies = [ ] [[package]] -name = "warp-job-account-tracker" +name = "warp-controller" version = "0.1.0" dependencies = [ - "anyhow", + "account", + "account-tracker", "base64", + "controller", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", @@ -1070,10 +1072,8 @@ dependencies = [ "cw-utils 0.16.0", "cw2 0.16.0", "cw20", - "cw721", - "job-account-tracker", "json-codec-wasm", - "prost 0.11.9", + "resolver", "schemars", "serde-json-wasm 0.4.1", "thiserror", diff --git a/contracts/warp-job-account-tracker/.cargo/config b/contracts/warp-account-tracker/.cargo/config similarity index 61% rename from contracts/warp-job-account-tracker/.cargo/config rename to contracts/warp-account-tracker/.cargo/config index b3ad083c..86e5ea3e 100644 --- a/contracts/warp-job-account-tracker/.cargo/config +++ b/contracts/warp-account-tracker/.cargo/config @@ -1,4 +1,4 @@ [alias] wasm = "build --release --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --example warp-job-account-tracker-schema" +schema = "run --example warp-account-tracker-schema" diff --git a/contracts/warp-job-account-tracker/.gitignore b/contracts/warp-account-tracker/.gitignore similarity index 100% rename from contracts/warp-job-account-tracker/.gitignore rename to contracts/warp-account-tracker/.gitignore diff --git a/contracts/warp-job-account-tracker/Cargo.toml b/contracts/warp-account-tracker/Cargo.toml similarity index 90% rename from contracts/warp-job-account-tracker/Cargo.toml rename to contracts/warp-account-tracker/Cargo.toml index ac23a59f..66bbc611 100644 --- a/contracts/warp-job-account-tracker/Cargo.toml +++ b/contracts/warp-account-tracker/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "warp-job-account-tracker" +name = "warp-account-tracker" version = "0.1.0" authors = ["Terra Money "] edition = "2021" @@ -40,7 +40,7 @@ cw2 = "0.16" cw20 = "0.16" cw721 = "0.16.0" cw-utils = "0.16" -job-account-tracker = { path = "../../packages/job-account-tracker", default-features = false, version = "*" } +account-tracker = { path = "../../packages/account-tracker", default-features = false, version = "*" } schemars = "0.8" thiserror = "1" serde-json-wasm = "0.4.1" diff --git a/contracts/warp-job-account-tracker/README.md b/contracts/warp-account-tracker/README.md similarity index 100% rename from contracts/warp-job-account-tracker/README.md rename to contracts/warp-account-tracker/README.md diff --git a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs b/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs similarity index 97% rename from contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs rename to contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs index b765ec16..5680d530 100644 --- a/contracts/warp-job-account-tracker/examples/warp-job-account-tracker-schema.rs +++ b/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs @@ -2,7 +2,7 @@ use std::env::current_dir; use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; -use job_account_tracker::{ +use account_tracker::{ Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FundingAccountResponse, FundingAccountsResponse, InstantiateMsg, QueryMsg, }; diff --git a/contracts/warp-job-account-tracker/meta/README.md b/contracts/warp-account-tracker/meta/README.md similarity index 100% rename from contracts/warp-job-account-tracker/meta/README.md rename to contracts/warp-account-tracker/meta/README.md diff --git a/contracts/warp-job-account-tracker/meta/appveyor.yml b/contracts/warp-account-tracker/meta/appveyor.yml similarity index 100% rename from contracts/warp-job-account-tracker/meta/appveyor.yml rename to contracts/warp-account-tracker/meta/appveyor.yml diff --git a/contracts/warp-job-account-tracker/meta/test_generate.sh b/contracts/warp-account-tracker/meta/test_generate.sh similarity index 100% rename from contracts/warp-job-account-tracker/meta/test_generate.sh rename to contracts/warp-account-tracker/meta/test_generate.sh diff --git a/contracts/warp-job-account-tracker/src/contract.rs b/contracts/warp-account-tracker/src/contract.rs similarity index 95% rename from contracts/warp-job-account-tracker/src/contract.rs rename to contracts/warp-account-tracker/src/contract.rs index 48e10b61..80ef2f7a 100644 --- a/contracts/warp-job-account-tracker/src/contract.rs +++ b/contracts/warp-account-tracker/src/contract.rs @@ -4,7 +4,7 @@ use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; use cw_utils::nonpayable; -use job_account_tracker::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use account_tracker::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( @@ -26,7 +26,7 @@ pub fn instantiate( Ok(Response::new() .add_attribute("action", "instantiate") .add_attribute("contract_addr", instantiated_account_addr.clone()) - .add_attribute("job_account_tracker", instantiated_account_addr) + .add_attribute("account_tracker", instantiated_account_addr) .add_attribute("admin", msg.admin) .add_attribute("warp_addr", msg.warp_addr)) } diff --git a/contracts/warp-job-account-tracker/src/error.rs b/contracts/warp-account-tracker/src/error.rs similarity index 100% rename from contracts/warp-job-account-tracker/src/error.rs rename to contracts/warp-account-tracker/src/error.rs diff --git a/contracts/warp-job-account-tracker/src/execute/account.rs b/contracts/warp-account-tracker/src/execute/account.rs similarity index 99% rename from contracts/warp-job-account-tracker/src/execute/account.rs rename to contracts/warp-account-tracker/src/execute/account.rs index 0c52f45a..6c4816f2 100644 --- a/contracts/warp-job-account-tracker/src/execute/account.rs +++ b/contracts/warp-account-tracker/src/execute/account.rs @@ -3,7 +3,7 @@ use crate::state::{ }; use crate::ContractError; use cosmwasm_std::{DepsMut, Response}; -use job_account_tracker::{ +use account_tracker::{ AddFundingAccountMsg, FreeAccountMsg, FreeFundingAccountMsg, FundingAccount, TakeAccountMsg, TakeFundingAccountMsg, }; diff --git a/contracts/warp-job-account-tracker/src/execute/mod.rs b/contracts/warp-account-tracker/src/execute/mod.rs similarity index 100% rename from contracts/warp-job-account-tracker/src/execute/mod.rs rename to contracts/warp-account-tracker/src/execute/mod.rs diff --git a/contracts/warp-job-account-tracker/src/integration_tests.rs b/contracts/warp-account-tracker/src/integration_tests.rs similarity index 86% rename from contracts/warp-job-account-tracker/src/integration_tests.rs rename to contracts/warp-account-tracker/src/integration_tests.rs index d6b0943e..8fe5b95a 100644 --- a/contracts/warp-job-account-tracker/src/integration_tests.rs +++ b/contracts/warp-account-tracker/src/integration_tests.rs @@ -3,7 +3,7 @@ mod tests { use anyhow::Result as AnyResult; use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; use cw_multi_test::{App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor}; - use job_account_tracker::{ + use account_tracker::{ Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FreeAccountMsg, InstantiateMsg, QueryConfigMsg, QueryFirstFreeAccountMsg, QueryFreeAccountsMsg, QueryMsg, QueryTakenAccountsMsg, TakeAccountMsg, @@ -39,24 +39,24 @@ mod tests { }) } - fn contract_warp_job_account_tracker() -> Box> { + fn contract_warp_account_tracker() -> Box> { let contract = ContractWrapper::new(execute, instantiate, query); Box::new(contract) } - fn init_warp_job_account_tracker( + fn init_warp_account_tracker( app: &mut App, - warp_job_account_tracker_contract_code_id: u64, + warp_account_tracker_contract_code_id: u64, ) -> Addr { app.instantiate_contract( - warp_job_account_tracker_contract_code_id, + warp_account_tracker_contract_code_id, Addr::unchecked(DUMMY_WARP_CONTROLLER_ADDR), &InstantiateMsg { admin: USER_1.to_string(), warp_addr: DUMMY_WARP_CONTROLLER_ADDR.to_string(), }, &[], - "warp_job_account_tracker", + "warp_account_tracker", None, ) .unwrap() @@ -73,17 +73,17 @@ mod tests { } #[test] - fn warp_job_account_tracker_contract_multi_test_account_management() { + fn warp_account_tracker_contract_multi_test_account_management() { let mut app = mock_app(); - let warp_job_account_tracker_contract_code_id = - app.store_code(contract_warp_job_account_tracker()); + let warp_account_tracker_contract_code_id = + app.store_code(contract_warp_account_tracker()); // Instantiate account - let warp_job_account_tracker_contract_addr = - init_warp_job_account_tracker(&mut app, warp_job_account_tracker_contract_code_id); + let warp_account_tracker_contract_addr = + init_warp_account_tracker(&mut app, warp_account_tracker_contract_code_id); assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryConfig(QueryConfigMsg {}) ), Ok(ConfigResponse { @@ -95,7 +95,7 @@ mod tests { ); assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryFirstFreeAccount(QueryFirstFreeAccountMsg { account_owner_addr: USER_1.to_string(), }) @@ -104,7 +104,7 @@ mod tests { ); assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, @@ -118,7 +118,7 @@ mod tests { ); assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, @@ -134,7 +134,7 @@ mod tests { // Mark first account as free let _ = app.execute_contract( Addr::unchecked(USER_1), - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), @@ -147,7 +147,7 @@ mod tests { assert_err( app.execute_contract( Addr::unchecked(USER_1), - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_1_ADDR.to_string(), @@ -161,7 +161,7 @@ mod tests { // Mark second account as free let _ = app.execute_contract( Addr::unchecked(USER_1), - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), @@ -173,7 +173,7 @@ mod tests { // Mark third account as free let _ = app.execute_contract( Addr::unchecked(USER_1), - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_3_ADDR.to_string(), @@ -185,7 +185,7 @@ mod tests { // Query first free account assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryFirstFreeAccount(QueryFirstFreeAccountMsg { account_owner_addr: USER_1.to_string(), }) @@ -201,7 +201,7 @@ mod tests { // Query free accounts assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, @@ -230,7 +230,7 @@ mod tests { // Query taken accounts assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, @@ -246,7 +246,7 @@ mod tests { // Take second account with job 1 let _ = app.execute_contract( Addr::unchecked(USER_1), - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &ExecuteMsg::TakeAccount(TakeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), @@ -259,7 +259,7 @@ mod tests { assert_err( app.execute_contract( Addr::unchecked(USER_1), - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &ExecuteMsg::TakeAccount(TakeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), @@ -273,7 +273,7 @@ mod tests { // Query free accounts assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, @@ -298,7 +298,7 @@ mod tests { // Query taken accounts assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, @@ -317,7 +317,7 @@ mod tests { // Free second account let _ = app.execute_contract( Addr::unchecked(USER_1), - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &ExecuteMsg::FreeAccount(FreeAccountMsg { account_owner_addr: USER_1.to_string(), account_addr: DUMMY_WARP_ACCOUNT_2_ADDR.to_string(), @@ -329,7 +329,7 @@ mod tests { // Query free accounts assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr.clone(), + warp_account_tracker_contract_addr.clone(), &QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, @@ -358,7 +358,7 @@ mod tests { // Query taken accounts assert_eq!( app.wrap().query_wasm_smart( - warp_job_account_tracker_contract_addr, + warp_account_tracker_contract_addr, &QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, diff --git a/contracts/warp-job-account-tracker/src/lib.rs b/contracts/warp-account-tracker/src/lib.rs similarity index 100% rename from contracts/warp-job-account-tracker/src/lib.rs rename to contracts/warp-account-tracker/src/lib.rs diff --git a/contracts/warp-job-account-tracker/src/query/account.rs b/contracts/warp-account-tracker/src/query/account.rs similarity index 99% rename from contracts/warp-job-account-tracker/src/query/account.rs rename to contracts/warp-account-tracker/src/query/account.rs index cf9c84f5..6144d5ce 100644 --- a/contracts/warp-job-account-tracker/src/query/account.rs +++ b/contracts/warp-account-tracker/src/query/account.rs @@ -3,7 +3,7 @@ use cw_storage_plus::{Bound, PrefixBound}; use crate::state::{CONFIG, FREE_ACCOUNTS, FUNDING_ACCOUNTS_BY_USER, TAKEN_ACCOUNTS}; -use job_account_tracker::{ +use account_tracker::{ Account, AccountResponse, AccountsResponse, ConfigResponse, FundingAccountResponse, FundingAccountsResponse, QueryFirstFreeAccountMsg, QueryFirstFreeFundingAccountMsg, QueryFreeAccountsMsg, QueryFundingAccountMsg, QueryFundingAccountsMsg, QueryTakenAccountsMsg, diff --git a/contracts/warp-job-account-tracker/src/query/mod.rs b/contracts/warp-account-tracker/src/query/mod.rs similarity index 100% rename from contracts/warp-job-account-tracker/src/query/mod.rs rename to contracts/warp-account-tracker/src/query/mod.rs diff --git a/contracts/warp-job-account-tracker/src/state.rs b/contracts/warp-account-tracker/src/state.rs similarity index 94% rename from contracts/warp-job-account-tracker/src/state.rs rename to contracts/warp-account-tracker/src/state.rs index 4409599a..47cacb36 100644 --- a/contracts/warp-job-account-tracker/src/state.rs +++ b/contracts/warp-account-tracker/src/state.rs @@ -1,6 +1,6 @@ use cosmwasm_std::{Addr, Uint64}; use cw_storage_plus::{Item, Map}; -use job_account_tracker::{Config, FundingAccount}; +use account_tracker::{Config, FundingAccount}; pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/warp-job-account/.cargo/config b/contracts/warp-account/.cargo/config similarity index 64% rename from contracts/warp-job-account/.cargo/config rename to contracts/warp-account/.cargo/config index 1fa27f1e..f4940a9d 100644 --- a/contracts/warp-job-account/.cargo/config +++ b/contracts/warp-account/.cargo/config @@ -1,4 +1,4 @@ [alias] wasm = "build --release --target wasm32-unknown-unknown" unit-test = "test --lib" -schema = "run --example warp-job-account-schema" +schema = "run --example warp-account-schema" diff --git a/contracts/warp-job-account/.gitignore b/contracts/warp-account/.gitignore similarity index 100% rename from contracts/warp-job-account/.gitignore rename to contracts/warp-account/.gitignore diff --git a/contracts/warp-job-account/Cargo.toml b/contracts/warp-account/Cargo.toml similarity index 92% rename from contracts/warp-job-account/Cargo.toml rename to contracts/warp-account/Cargo.toml index 9cefbc9d..94cd2c23 100644 --- a/contracts/warp-job-account/Cargo.toml +++ b/contracts/warp-account/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "warp-job-account" +name = "warp-account" version = "0.1.0" authors = ["Terra Money "] edition = "2021" @@ -41,7 +41,7 @@ cw20 = "0.16" cw721 = "0.16.0" cw-utils = "0.16" controller = { path = "../../packages/controller", default-features = false, version = "*" } -job-account = { path = "../../packages/job-account", default-features = false, version = "*" } +account = { path = "../../packages/account", default-features = false, version = "*" } schemars = "0.8" thiserror = "1" serde-json-wasm = "0.4.1" diff --git a/contracts/warp-job-account/README.md b/contracts/warp-account/README.md similarity index 100% rename from contracts/warp-job-account/README.md rename to contracts/warp-account/README.md diff --git a/contracts/warp-job-account/examples/warp-job-account-schema.rs b/contracts/warp-account/examples/warp-account-schema.rs similarity index 88% rename from contracts/warp-job-account/examples/warp-job-account-schema.rs rename to contracts/warp-account/examples/warp-account-schema.rs index 85c750f5..1e95cd6d 100644 --- a/contracts/warp-job-account/examples/warp-job-account-schema.rs +++ b/contracts/warp-account/examples/warp-account-schema.rs @@ -2,7 +2,7 @@ use std::env::current_dir; use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; -use job_account::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; +use account::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; fn main() { let mut out_dir = current_dir().unwrap(); diff --git a/contracts/warp-job-account/meta/README.md b/contracts/warp-account/meta/README.md similarity index 100% rename from contracts/warp-job-account/meta/README.md rename to contracts/warp-account/meta/README.md diff --git a/contracts/warp-job-account/meta/appveyor.yml b/contracts/warp-account/meta/appveyor.yml similarity index 100% rename from contracts/warp-job-account/meta/appveyor.yml rename to contracts/warp-account/meta/appveyor.yml diff --git a/contracts/warp-job-account/meta/test_generate.sh b/contracts/warp-account/meta/test_generate.sh similarity index 100% rename from contracts/warp-job-account/meta/test_generate.sh rename to contracts/warp-account/meta/test_generate.sh diff --git a/contracts/warp-job-account/src/contract.rs b/contracts/warp-account/src/contract.rs similarity index 96% rename from contracts/warp-job-account/src/contract.rs rename to contracts/warp-account/src/contract.rs index 2cb6658a..5ef27eb5 100644 --- a/contracts/warp-job-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -4,7 +4,7 @@ use controller::account::{execute_warp_msgs, warp_msgs_to_cosmos_msgs}; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; -use job_account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( diff --git a/contracts/warp-job-account/src/error.rs b/contracts/warp-account/src/error.rs similarity index 100% rename from contracts/warp-job-account/src/error.rs rename to contracts/warp-account/src/error.rs diff --git a/contracts/warp-job-account/src/lib.rs b/contracts/warp-account/src/lib.rs similarity index 100% rename from contracts/warp-job-account/src/lib.rs rename to contracts/warp-account/src/lib.rs diff --git a/contracts/warp-job-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs similarity index 86% rename from contracts/warp-job-account/src/query/account.rs rename to contracts/warp-account/src/query/account.rs index 98265862..bb0df58e 100644 --- a/contracts/warp-job-account/src/query/account.rs +++ b/contracts/warp-account/src/query/account.rs @@ -1,6 +1,6 @@ use crate::state::CONFIG; use cosmwasm_std::{Deps, StdResult}; -use job_account::ConfigResponse; +use account::ConfigResponse; pub fn query_config(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; diff --git a/contracts/warp-job-account/src/query/mod.rs b/contracts/warp-account/src/query/mod.rs similarity index 100% rename from contracts/warp-job-account/src/query/mod.rs rename to contracts/warp-account/src/query/mod.rs diff --git a/contracts/warp-job-account/src/state.rs b/contracts/warp-account/src/state.rs similarity index 76% rename from contracts/warp-job-account/src/state.rs rename to contracts/warp-account/src/state.rs index 9fbff4cf..dc761dde 100644 --- a/contracts/warp-job-account/src/state.rs +++ b/contracts/warp-account/src/state.rs @@ -1,5 +1,5 @@ use cw_storage_plus::Item; -use job_account::Config; +use account::Config; pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/warp-job-account/src/tests.rs b/contracts/warp-account/src/tests.rs similarity index 99% rename from contracts/warp-job-account/src/tests.rs rename to contracts/warp-account/src/tests.rs index d14bb94d..a1cfcd74 100644 --- a/contracts/warp-job-account/src/tests.rs +++ b/contracts/warp-account/src/tests.rs @@ -6,7 +6,7 @@ use cosmwasm_std::{ to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, IbcTimeoutBlock, Response, StakingMsg, Uint128, Uint64, VoteOption, WasmMsg, }; -use job_account::{ExecuteMsg, InstantiateMsg}; +use account::{ExecuteMsg, InstantiateMsg}; #[test] fn test_execute_controller() { diff --git a/contracts/warp-controller/Cargo.toml b/contracts/warp-controller/Cargo.toml index e669cfbf..5bd8c1b2 100644 --- a/contracts/warp-controller/Cargo.toml +++ b/contracts/warp-controller/Cargo.toml @@ -39,8 +39,8 @@ cw-storage-plus = "0.16" cw-utils = "0.16" cw2 = "0.16" cw20 = "0.16" -job-account = { path = "../../packages/job-account", default-features = false, version = "*" } -job-account-tracker = { path = "../../packages/job-account-tracker", default-features = false, version = "*" } +account = { path = "../../packages/account", default-features = false, version = "*" } +account-tracker = { path = "../../packages/account-tracker", default-features = false, version = "*" } controller = { path = "../../packages/controller", default-features = false, version = "*" } resolver = { path = "../../packages/resolver", default-features = false, version = "*" } schemars = "0.8" diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 88d63c5d..f26ce446 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -7,7 +7,7 @@ use cw_utils::{must_pay, nonpayable}; use crate::{ execute, migrate, query, reply, state::{CONFIG, STATE}, - util::msg::build_instantiate_job_account_tracker_msg, + util::msg::build_instantiate_account_tracker_msg, ContractError, }; @@ -39,7 +39,7 @@ pub fn instantiate( cancellation_fee_percentage: msg.cancellation_fee, resolver_address: deps.api.addr_validate(&msg.resolver_address)?, // placeholder, will be updated in reply - job_account_tracker_address: deps.api.addr_validate(&msg.resolver_address)?, + account_tracker_address: deps.api.addr_validate(&msg.resolver_address)?, t_max: msg.t_max, t_min: msg.t_min, a_max: msg.a_max, @@ -82,10 +82,10 @@ pub fn instantiate( let submsgs = vec![SubMsg { id: REPLY_ID_INSTANTIATE_SUB_CONTRACTS, - msg: build_instantiate_job_account_tracker_msg( + msg: build_instantiate_account_tracker_msg( config.owner.to_string(), env.contract.address.to_string(), - msg.job_account_tracker_code_id.u64(), + msg.account_tracker_code_id.u64(), ), gas_limit: None, reply_on: ReplyOn::Always, @@ -131,13 +131,13 @@ pub fn execute( nonpayable(&info).unwrap(); execute::controller::update_config(deps, env, info, data, config) } - ExecuteMsg::MigrateFreeJobAccounts(data) => { + ExecuteMsg::MigrateFreeAccounts(data) => { nonpayable(&info).unwrap(); - migrate::job_account::migrate_free_job_accounts(deps.as_ref(), env, info, data, config) + migrate::account::migrate_free_accounts(deps.as_ref(), env, info, data, config) } - ExecuteMsg::MigrateTakenJobAccounts(data) => { + ExecuteMsg::MigrateTakenAccounts(data) => { nonpayable(&info).unwrap(); - migrate::job_account::migrate_taken_job_accounts(deps.as_ref(), env, info, data, config) + migrate::account::migrate_taken_accounts(deps.as_ref(), env, info, data, config) } ExecuteMsg::MigratePendingJobs(data) => { @@ -190,7 +190,7 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { - reply::account::create_job_account_and_job(deps, env, msg, config) + reply::account::create_account_and_job(deps, env, msg, config) } REPLY_ID_CREATE_FUNDING_ACCOUNT_AND_JOB => { reply::account::create_funding_account_and_job(deps, env, msg, config) diff --git a/contracts/warp-controller/src/error.rs b/contracts/warp-controller/src/error.rs index b4b9c15c..1f749643 100644 --- a/contracts/warp-controller/src/error.rs +++ b/contracts/warp-controller/src/error.rs @@ -43,7 +43,7 @@ pub enum ContractError { AccountAlreadyExists {}, #[error("Job account tracker already exists")] - JobAccountTrackerAlreadyExists {}, + AccountTrackerAlreadyExists {}, #[error("Account cannot create an account")] AccountCannotCreateAccount {}, diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index fc385298..617e8185 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -26,7 +26,7 @@ use crate::util::{ }; use controller::{account::CwFund, Config}; -use job_account_tracker::{AccountResponse, FundingAccountResponse}; +use account_tracker::{AccountResponse, FundingAccountResponse}; use resolver::QueryHydrateMsgsMsg; use super::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}; @@ -56,7 +56,7 @@ pub fn create_job( let state = STATE.load(deps.storage)?; let job_owner = info.sender.clone(); - let job_account_tracker_address_ref = &config.job_account_tracker_address.to_string(); + let account_tracker_address_ref = &config.account_tracker_address.to_string(); let _validate_conditions_and_variables: Option = deps.querier.query_wasm_smart( &config.resolver_address, @@ -135,10 +135,10 @@ pub fn create_job( }, )?; - let job_account_resp: AccountResponse = deps.querier.query_wasm_smart( - job_account_tracker_address_ref, - &job_account_tracker::QueryMsg::QueryFirstFreeAccount( - job_account_tracker::QueryFirstFreeAccountMsg { + let account_resp: AccountResponse = deps.querier.query_wasm_smart( + account_tracker_address_ref, + &account_tracker::QueryMsg::QueryFirstFreeAccount( + account_tracker::QueryFirstFreeAccountMsg { account_owner_addr: job_owner.to_string(), }, ), @@ -149,9 +149,9 @@ pub fn create_job( if let Some(funding_account_addr) = data.funding_account { // fetch funding account and check if it exists, throw otherwise funding_account_resp = deps.querier.query_wasm_smart( - job_account_tracker_address_ref, - &job_account_tracker::QueryMsg::QueryFundingAccount( - job_account_tracker::QueryFundingAccountMsg { + account_tracker_address_ref, + &account_tracker::QueryMsg::QueryFundingAccount( + account_tracker::QueryFundingAccountMsg { account_addr: funding_account_addr.to_string(), account_owner_addr: info.sender.to_string(), }, @@ -159,16 +159,16 @@ pub fn create_job( )?; } else { funding_account_resp = deps.querier.query_wasm_smart( - job_account_tracker_address_ref, - &job_account_tracker::QueryMsg::QueryFirstFreeFundingAccount( - job_account_tracker::QueryFirstFreeFundingAccountMsg { + account_tracker_address_ref, + &account_tracker::QueryMsg::QueryFirstFreeFundingAccount( + account_tracker::QueryFirstFreeFundingAccountMsg { account_owner_addr: job_owner.to_string(), }, ), )?; } - match job_account_resp.account { + match account_resp.account { None => { // Create account then create job in reply submsgs.push(SubMsg { @@ -235,7 +235,7 @@ pub fn create_job( // Take account msgs.push(build_taken_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), job_owner.to_string(), available_account_addr.to_string(), job.id, @@ -309,7 +309,7 @@ pub fn create_job( // Take account msgs.push(build_take_funding_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), job_owner.to_string(), available_account_addr.to_string(), job.id, @@ -358,7 +358,7 @@ pub fn delete_job( fee_denom_paid_amount: Uint128, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; - let job_account_addr = job.account.clone(); + let account_addr = job.account.clone(); if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); @@ -395,15 +395,15 @@ pub fn delete_job( // Free account msgs.push(build_free_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), job.owner.to_string(), - job_account_addr.to_string(), + account_addr.to_string(), job.id, )); if let Some(funding_account) = job.funding_account { msgs.push(build_free_funding_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), job.owner.to_string(), funding_account.to_string(), job.id, @@ -418,7 +418,7 @@ pub fn delete_job( // Job owner withdraw all assets that are listed from warp account to itself msgs.push(build_account_withdraw_assets_msg( - job_account_addr.to_string(), + account_addr.to_string(), job.assets_to_withdraw, )); @@ -474,7 +474,7 @@ pub fn execute_job( config: Config, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; - let job_account_addr = job.account.clone(); + let account_addr = job.account.clone(); if job.status != JobStatus::Pending { return Err(ContractError::JobNotActive {}); @@ -509,7 +509,7 @@ pub fn execute_job( id: REPLY_ID_EXECUTE_JOB, msg: CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job.account.to_string(), - msg: to_binary(&job_account::ExecuteMsg::WarpMsgs(WarpMsgs { + msg: to_binary(&account::ExecuteMsg::WarpMsgs(WarpMsgs { msgs: deps.querier.query_wasm_smart( config.resolver_address, &resolver::QueryMsg::QueryHydrateMsgs(QueryHydrateMsgsMsg { @@ -547,15 +547,15 @@ pub fn execute_job( // Free account msgs.push(build_free_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), job.owner.to_string(), - job_account_addr.to_string(), + account_addr.to_string(), job.id, )); if let Some(funding_account) = job.funding_account { msgs.push(build_free_funding_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), job.owner.to_string(), funding_account.to_string(), job.id, @@ -580,7 +580,7 @@ pub fn evict_job( config: Config, ) -> Result { let job = JobQueue::get(&deps, data.id.into())?; - let job_account_addr = job.account.clone(); + let account_addr = job.account.clone(); if job.status != JobStatus::Pending { return Err(ContractError::Unauthorized {}); @@ -614,9 +614,9 @@ pub fn evict_job( // Free account msgs.push(build_free_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), job.owner.to_string(), - job_account_addr.to_string(), + account_addr.to_string(), job.id, )); diff --git a/contracts/warp-controller/src/migrate/job_account.rs b/contracts/warp-controller/src/migrate/account.rs similarity index 58% rename from contracts/warp-controller/src/migrate/job_account.rs rename to contracts/warp-controller/src/migrate/account.rs index c972be54..0a95b03c 100644 --- a/contracts/warp-controller/src/migrate/job_account.rs +++ b/contracts/warp-controller/src/migrate/account.rs @@ -1,25 +1,25 @@ use cosmwasm_std::{to_binary, Deps, Env, MessageInfo, Response, WasmMsg}; use crate::ContractError; -use controller::{Config, MigrateJobAccountsMsg}; -use job_account_tracker::{ +use controller::{Config, MigrateAccountsMsg}; +use account_tracker::{ AccountsResponse, MigrateMsg, QueryFreeAccountsMsg, QueryTakenAccountsMsg, }; -pub fn migrate_free_job_accounts( +pub fn migrate_free_accounts( deps: Deps, _env: Env, info: MessageInfo, - msg: MigrateJobAccountsMsg, + msg: MigrateAccountsMsg, config: Config, ) -> Result { if info.sender != config.owner { return Err(ContractError::Unauthorized {}); } - let free_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( - config.job_account_tracker_address, - &job_account_tracker::QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + let free_accounts: AccountsResponse = deps.querier.query_wasm_smart( + config.account_tracker_address, + &account_tracker::QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { account_owner_addr: msg.account_owner_addr, start_after: msg.start_after, limit: Some(msg.limit as u32), @@ -27,10 +27,10 @@ pub fn migrate_free_job_accounts( )?; let mut migration_msgs = vec![]; - for job_account in free_job_accounts.accounts { + for account in free_accounts.accounts { migration_msgs.push(WasmMsg::Migrate { - contract_addr: job_account.addr.to_string(), - new_code_id: msg.warp_job_account_code_id.u64(), + contract_addr: account.addr.to_string(), + new_code_id: msg.warp_account_code_id.u64(), msg: to_binary(&MigrateMsg {})?, }); } @@ -38,20 +38,20 @@ pub fn migrate_free_job_accounts( Ok(Response::new().add_messages(migration_msgs)) } -pub fn migrate_taken_job_accounts( +pub fn migrate_taken_accounts( deps: Deps, _env: Env, info: MessageInfo, - msg: MigrateJobAccountsMsg, + msg: MigrateAccountsMsg, config: Config, ) -> Result { if info.sender != config.owner { return Err(ContractError::Unauthorized {}); } - let taken_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( - config.job_account_tracker_address, - &job_account_tracker::QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { + let taken_accounts: AccountsResponse = deps.querier.query_wasm_smart( + config.account_tracker_address, + &account_tracker::QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { account_owner_addr: msg.account_owner_addr, start_after: msg.start_after, limit: Some(msg.limit as u32), @@ -59,10 +59,10 @@ pub fn migrate_taken_job_accounts( )?; let mut migration_msgs = vec![]; - for job_account in taken_job_accounts.accounts { + for account in taken_accounts.accounts { migration_msgs.push(WasmMsg::Migrate { - contract_addr: job_account.addr.to_string(), - new_code_id: msg.warp_job_account_code_id.u64(), + contract_addr: account.addr.to_string(), + new_code_id: msg.warp_account_code_id.u64(), msg: to_binary(&MigrateMsg {})?, }); } diff --git a/contracts/warp-controller/src/migrate/mod.rs b/contracts/warp-controller/src/migrate/mod.rs index 8a179a31..6f297081 100644 --- a/contracts/warp-controller/src/migrate/mod.rs +++ b/contracts/warp-controller/src/migrate/mod.rs @@ -1,2 +1,2 @@ pub(crate) mod job; -pub(crate) mod job_account; +pub(crate) mod account; diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 7b83a3a6..23f3b955 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -15,7 +15,7 @@ use crate::{ ContractError, }; -pub fn create_job_account_and_job( +pub fn create_account_and_job( mut deps: DepsMut, env: Env, msg: Reply, @@ -23,7 +23,7 @@ pub fn create_job_account_and_job( ) -> Result { let reply = msg.result.into_result().map_err(StdError::generic_err)?; - let job_account_event = reply + let account_event = reply .events .iter() .find(|event| { @@ -34,7 +34,7 @@ pub fn create_job_account_and_job( }) .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; - let job_id_str = job_account_event + let job_id_str = account_event .attributes .iter() .cloned() @@ -43,7 +43,7 @@ pub fn create_job_account_and_job( .value; let job_id = job_id_str.as_str().parse::()?; - let owner = job_account_event + let owner = account_event .attributes .iter() .cloned() @@ -51,8 +51,8 @@ pub fn create_job_account_and_job( .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? .value; - let job_account_addr = deps.api.addr_validate( - &job_account_event + let account_addr = deps.api.addr_validate( + &account_event .attributes .iter() .cloned() @@ -62,7 +62,7 @@ pub fn create_job_account_and_job( )?; let native_funds: Vec = serde_json_wasm::from_str( - &job_account_event + &account_event .attributes .iter() .cloned() @@ -72,7 +72,7 @@ pub fn create_job_account_and_job( )?; let cw_funds: Option> = serde_json_wasm::from_str( - &job_account_event + &account_event .attributes .iter() .cloned() @@ -82,7 +82,7 @@ pub fn create_job_account_and_job( )?; let account_msgs: Option> = serde_json_wasm::from_str( - &job_account_event + &account_event .attributes .iter() .cloned() @@ -92,7 +92,7 @@ pub fn create_job_account_and_job( )?; let mut job = JobQueue::get(&deps, job_id)?; - job.account = job_account_addr.clone(); + job.account = account_addr.clone(); JobQueue::sync(&mut deps, env, job.clone())?; let mut msgs: Vec = vec![]; @@ -106,14 +106,14 @@ pub fn create_job_account_and_job( .addr_validate(&cw20_fund.contract_addr)? .to_string(), owner.clone(), - job_account_addr.clone().to_string(), + account_addr.clone().to_string(), cw20_fund.amount, ), CwFund::Cw721(cw721_fund) => build_transfer_cw721_msg( deps.api .addr_validate(&cw721_fund.contract_addr)? .to_string(), - job_account_addr.clone().to_string(), + account_addr.clone().to_string(), cw721_fund.token_id.clone(), ), }) @@ -123,25 +123,25 @@ pub fn create_job_account_and_job( if let Some(account_msgs) = account_msgs { // Account execute msgs msgs.push(build_account_execute_warp_msgs( - job_account_addr.to_string(), + account_addr.to_string(), account_msgs, )); } // Take job account msgs.push(build_taken_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), job.owner.to_string(), - job_account_addr.to_string(), + account_addr.to_string(), job.id, )); Ok(Response::new() .add_messages(msgs) - .add_attribute("action", "create_job_account_and_job_reply") + .add_attribute("action", "create_account_and_job_reply") // .add_attribute("job_id", value) .add_attribute("owner", owner) - .add_attribute("job_account_address", job_account_addr) + .add_attribute("account_address", account_addr) .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?) .add_attribute( "cw_funds", @@ -210,7 +210,7 @@ pub fn create_funding_account_and_job( JobQueue::sync(&mut deps, env, job.clone())?; let msgs: Vec = vec![build_take_funding_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), job.owner.to_string(), funding_account_addr.to_string(), job.id, @@ -218,7 +218,7 @@ pub fn create_funding_account_and_job( Ok(Response::new() .add_messages(msgs) - .add_attribute("action", "create_job_account_and_job_reply") + .add_attribute("action", "create_account_and_job_reply") // .add_attribute("job_id", value) .add_attribute("owner", owner) .add_attribute("funding_account_address", funding_account_addr) @@ -273,14 +273,14 @@ pub fn create_funding_account( )?; let msgs: Vec = vec![build_add_funding_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), owner.to_string(), funding_account_addr.to_string(), )]; Ok(Response::new() .add_messages(msgs) - .add_attribute("action", "create_job_account_reply") + .add_attribute("action", "create_account_reply") .add_attribute("owner", owner) .add_attribute("funding_account_address", funding_account_addr) .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 129124b8..8df13573 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -81,7 +81,7 @@ pub fn execute_job( let reward_plus_fee = finished_job.reward + total_fees; - let job_account_addr = finished_job.account.clone(); + let account_addr = finished_job.account.clone(); let mut recurring_job_created = false; @@ -244,15 +244,15 @@ pub fn execute_job( // Take job account with the new job msgs.push(build_taken_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), finished_job.owner.to_string(), - job_account_addr.to_string(), + account_addr.to_string(), new_job_id, )); // take funding account with new job msgs.push(build_take_funding_account_msg( - config.job_account_tracker_address.to_string(), + config.account_tracker_address.to_string(), finished_job.owner.to_string(), funding_account_addr.to_string(), new_job_id, @@ -261,7 +261,7 @@ pub fn execute_job( // No new job created, account has been free in execute_job, no need to free here again // Job owner withdraw all assets that are listed from warp account to itself msgs.push(build_account_withdraw_assets_msg( - job_account_addr.to_string(), + account_addr.to_string(), finished_job.assets_to_withdraw, )); @@ -291,7 +291,7 @@ pub fn instantiate_sub_contracts( let reply: cosmwasm_std::SubMsgResponse = msg.result.into_result().map_err(StdError::generic_err)?; - let job_account_tracker_instantiate_event = reply + let account_tracker_instantiate_event = reply .events .iter() .find(|event| { @@ -302,17 +302,17 @@ pub fn instantiate_sub_contracts( }) .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; - let job_account_tracker_addr = deps.api.addr_validate( - &job_account_tracker_instantiate_event + let account_tracker_addr = deps.api.addr_validate( + &account_tracker_instantiate_event .attributes .iter() .cloned() - .find(|attr| attr.key == "job_account_tracker") - .ok_or_else(|| StdError::generic_err("cannot find `job_account_tracker` attribute"))? + .find(|attr| attr.key == "account_tracker") + .ok_or_else(|| StdError::generic_err("cannot find `account_tracker` attribute"))? .value, )?; - config.job_account_tracker_address = job_account_tracker_addr; + config.account_tracker_address = account_tracker_addr; CONFIG.save(deps.storage, &config)?; diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index b17d1cdc..c5006442 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -4,13 +4,13 @@ use controller::account::{ AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, WarpMsg, WarpMsgs, WithdrawAssetsMsg, }; -use job_account_tracker::{ +use account_tracker::{ AddFundingAccountMsg, FreeAccountMsg, FreeFundingAccountMsg, TakeAccountMsg, TakeFundingAccountMsg, }; #[allow(clippy::too_many_arguments)] -pub fn build_instantiate_job_account_tracker_msg( +pub fn build_instantiate_account_tracker_msg( admin_addr: String, controller_addr: String, code_id: u64, @@ -18,7 +18,7 @@ pub fn build_instantiate_job_account_tracker_msg( CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(admin_addr.clone()), code_id, - msg: to_binary(&job_account_tracker::InstantiateMsg { + msg: to_binary(&account_tracker::InstantiateMsg { admin: admin_addr, warp_addr: controller_addr, }) @@ -41,7 +41,7 @@ pub fn build_instantiate_warp_account_msg( CosmosMsg::Wasm(WasmMsg::Instantiate { admin: Some(admin_addr), code_id, - msg: to_binary(&job_account::InstantiateMsg { + msg: to_binary(&account::InstantiateMsg { owner: account_owner.clone(), job_id, native_funds: native_funds.clone(), @@ -55,14 +55,14 @@ pub fn build_instantiate_warp_account_msg( } pub fn build_free_account_msg( - job_account_tracker_addr: String, + account_tracker_addr: String, account_owner_addr: String, account_addr: String, last_job_id: Uint64, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: job_account_tracker_addr, - msg: to_binary(&job_account_tracker::ExecuteMsg::FreeAccount( + contract_addr: account_tracker_addr, + msg: to_binary(&account_tracker::ExecuteMsg::FreeAccount( FreeAccountMsg { account_owner_addr, account_addr, @@ -75,14 +75,14 @@ pub fn build_free_account_msg( } pub fn build_taken_account_msg( - job_account_tracker_addr: String, + account_tracker_addr: String, account_owner_addr: String, account_addr: String, job_id: Uint64, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: job_account_tracker_addr, - msg: to_binary(&job_account_tracker::ExecuteMsg::TakeAccount( + contract_addr: account_tracker_addr, + msg: to_binary(&account_tracker::ExecuteMsg::TakeAccount( TakeAccountMsg { account_owner_addr, account_addr, @@ -95,14 +95,14 @@ pub fn build_taken_account_msg( } pub fn build_free_funding_account_msg( - job_account_tracker_addr: String, + account_tracker_addr: String, account_owner_addr: String, account_addr: String, job_id: Uint64, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: job_account_tracker_addr, - msg: to_binary(&job_account_tracker::ExecuteMsg::FreeFundingAccount( + contract_addr: account_tracker_addr, + msg: to_binary(&account_tracker::ExecuteMsg::FreeFundingAccount( FreeFundingAccountMsg { account_owner_addr, account_addr, @@ -115,14 +115,14 @@ pub fn build_free_funding_account_msg( } pub fn build_take_funding_account_msg( - job_account_tracker_addr: String, + account_tracker_addr: String, account_owner_addr: String, account_addr: String, job_id: Uint64, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: job_account_tracker_addr, - msg: to_binary(&job_account_tracker::ExecuteMsg::TakeFundingAccount( + contract_addr: account_tracker_addr, + msg: to_binary(&account_tracker::ExecuteMsg::TakeFundingAccount( TakeFundingAccountMsg { account_owner_addr, account_addr, @@ -135,13 +135,13 @@ pub fn build_take_funding_account_msg( } pub fn build_add_funding_account_msg( - job_account_tracker_addr: String, + account_tracker_addr: String, account_owner_addr: String, account_addr: String, ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: job_account_tracker_addr, - msg: to_binary(&job_account_tracker::ExecuteMsg::AddFundingAccount( + contract_addr: account_tracker_addr, + msg: to_binary(&account_tracker::ExecuteMsg::AddFundingAccount( AddFundingAccountMsg { account_owner_addr, account_addr, @@ -215,7 +215,7 @@ pub fn build_account_execute_warp_msgs( ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: account_addr, - msg: to_binary(&job_account::ExecuteMsg::WarpMsgs(WarpMsgs { + msg: to_binary(&account::ExecuteMsg::WarpMsgs(WarpMsgs { msgs: warp_msgs_for_account_to_execute, job_id: None, })) diff --git a/packages/job-account-tracker/Cargo.toml b/packages/account-tracker/Cargo.toml similarity index 91% rename from packages/job-account-tracker/Cargo.toml rename to packages/account-tracker/Cargo.toml index c7e71053..2cef1079 100644 --- a/packages/job-account-tracker/Cargo.toml +++ b/packages/account-tracker/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "job-account-tracker" +name = "account-tracker" version = "0.1.0" authors = ["Terra Money "] edition = "2021" diff --git a/packages/job-account-tracker/README.md b/packages/account-tracker/README.md similarity index 100% rename from packages/job-account-tracker/README.md rename to packages/account-tracker/README.md diff --git a/packages/job-account-tracker/src/lib.rs b/packages/account-tracker/src/lib.rs similarity index 100% rename from packages/job-account-tracker/src/lib.rs rename to packages/account-tracker/src/lib.rs diff --git a/packages/job-account/.cargo/config b/packages/account/.cargo/config similarity index 100% rename from packages/job-account/.cargo/config rename to packages/account/.cargo/config diff --git a/packages/job-account/Cargo.toml b/packages/account/Cargo.toml similarity index 95% rename from packages/job-account/Cargo.toml rename to packages/account/Cargo.toml index 753f2e0a..1398fe84 100644 --- a/packages/job-account/Cargo.toml +++ b/packages/account/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "job-account" +name = "account" version = "0.1.0" authors = ["Terra Money "] edition = "2021" diff --git a/packages/job-account/README.md b/packages/account/README.md similarity index 100% rename from packages/job-account/README.md rename to packages/account/README.md diff --git a/packages/job-account/examples/account-schema.rs b/packages/account/examples/account-schema.rs similarity index 100% rename from packages/job-account/examples/account-schema.rs rename to packages/account/examples/account-schema.rs diff --git a/packages/job-account/src/lib.rs b/packages/account/src/lib.rs similarity index 100% rename from packages/job-account/src/lib.rs rename to packages/account/src/lib.rs diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index ad7fab1b..0cc7a10e 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -21,7 +21,7 @@ pub struct Config { // By querying job account tracker contract // We know all accounts owned by that user and each account's availability // For more detail, please refer to job account tracker contract - pub job_account_tracker_address: Addr, + pub account_tracker_address: Addr, pub resolver_address: Addr, // maximum time for evictions pub t_max: Uint64, @@ -61,7 +61,7 @@ pub struct InstantiateMsg { pub fee_denom: String, pub fee_collector: Option, pub warp_account_code_id: Uint64, - pub job_account_tracker_code_id: Uint64, + pub account_tracker_code_id: Uint64, pub minimum_reward: Uint128, pub creation_fee: Uint64, pub cancellation_fee: Uint64, @@ -97,8 +97,8 @@ pub enum ExecuteMsg { UpdateConfig(UpdateConfigMsg), - MigrateFreeJobAccounts(MigrateJobAccountsMsg), - MigrateTakenJobAccounts(MigrateJobAccountsMsg), + MigrateFreeAccounts(MigrateAccountsMsg), + MigrateTakenAccounts(MigrateAccountsMsg), MigratePendingJobs(MigrateJobsMsg), MigrateFinishedJobs(MigrateJobsMsg), @@ -133,9 +133,9 @@ pub struct UpdateConfigMsg { } #[cw_serde] -pub struct MigrateJobAccountsMsg { +pub struct MigrateAccountsMsg { pub account_owner_addr: String, - pub warp_job_account_code_id: Uint64, + pub warp_account_code_id: Uint64, pub start_after: Option, pub limit: u8, } diff --git a/refs.json b/refs.json index 96ce95bd..18ec8085 100644 --- a/refs.json +++ b/refs.json @@ -23,10 +23,10 @@ "codeId": "9263", "address": "terra17xm2ewyg60y7eypnwav33fwm23hxs3qyd8qk9tnntj4d0rp2vvhsgkpwwp" }, - "warp-job-account": { + "warp-account": { "codeId": "12123" }, - "warp-job-account-tracker": { + "warp-account-tracker": { "codeId": "12122", "address": "terra1zzgg30ygltd5s3xtescfquwmm2jktaq28t37f2j9h5wwswpxtyyspugek8" } diff --git a/tasks/deploy_warp.ts b/tasks/deploy_warp.ts index d1432380..60618455 100644 --- a/tasks/deploy_warp.ts +++ b/tasks/deploy_warp.ts @@ -4,7 +4,7 @@ task(async ({ deployer, signer, refs }) => { deployer.buildContract("warp-controller"); deployer.optimizeContract("warp-controller"); - const job_account_contract_id = await deployer.storeCode("warp-job-account"); + const account_contract_id = await deployer.storeCode("warp-account"); await new Promise((resolve) => setTimeout(resolve, 10000)); await deployer.storeCode("warp-resolver"); @@ -16,8 +16,8 @@ task(async ({ deployer, signer, refs }) => { await deployer.storeCode("warp-controller"); await new Promise((resolve) => setTimeout(resolve, 10000)); - const job_account_tracker_id = await deployer.storeCode( - "warp-job-account-tracker" + const account_tracker_id = await deployer.storeCode( + "warp-account-tracker" ); await new Promise((resolve) => setTimeout(resolve, 10000)); @@ -45,8 +45,8 @@ task(async ({ deployer, signer, refs }) => { const instantiateControllerMsg = { fee_denom: "uluna", fee_collector: signer.key.accAddress, - warp_account_code_id: job_account_contract_id, - job_account_tracker_code_id: job_account_tracker_id, + warp_account_code_id: account_contract_id, + account_tracker_code_id: account_tracker_id, minimum_reward: "10000", creation_fee: "5", cancellation_fee: "5", From 3fc719b0664aa04c25969a8fd7b999efc11c8426 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Sat, 9 Dec 2023 18:38:17 +0100 Subject: [PATCH 101/133] update comments --- packages/controller/src/job.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index c262b737..0fed5ed4 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -15,6 +15,8 @@ pub struct Job { // As job creator can have infinite job accounts, each job account can only be used by up to 1 active job // So each job's fund is isolated pub account: Addr, + // Funding account is an optionally provided account from which job fees and rewards are deducted from, used only in case + // of recurring jobs - if a user doesn't provide a funding account, one is created on the fly pub funding_account: Option, pub last_update_time: Uint64, pub name: String, @@ -47,8 +49,6 @@ pub enum JobStatus { Evicted, } -// Create a job using job account, if job account does not exist, create it -// Each job account will only be used for 1 job, therefore we achieve funds isolation #[cw_serde] pub struct Execution { pub condition: String, From a1496cd5eff39dd31538680462b4fe64ca0b5274 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Sat, 9 Dec 2023 18:57:41 +0100 Subject: [PATCH 102/133] fmt --- .../examples/warp-account-tracker-schema.rs | 2 +- .../warp-account-tracker/src/contract.rs | 2 +- .../src/execute/account.rs | 2 +- .../src/integration_tests.rs | 9 +++--- contracts/warp-account-tracker/src/state.rs | 2 +- .../examples/warp-account-schema.rs | 2 +- contracts/warp-account/src/contract.rs | 2 +- contracts/warp-account/src/query/account.rs | 2 +- contracts/warp-account/src/tests.rs | 2 +- contracts/warp-controller/src/execute/job.rs | 2 +- .../warp-controller/src/migrate/account.rs | 4 +-- contracts/warp-controller/src/migrate/mod.rs | 2 +- contracts/warp-controller/src/util/msg.rs | 32 ++++++++----------- 13 files changed, 29 insertions(+), 36 deletions(-) diff --git a/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs b/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs index 5680d530..f0054f49 100644 --- a/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs +++ b/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs @@ -1,11 +1,11 @@ use std::env::current_dir; use std::fs::create_dir_all; -use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use account_tracker::{ Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FundingAccountResponse, FundingAccountsResponse, InstantiateMsg, QueryMsg, }; +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; fn main() { let mut out_dir = current_dir().unwrap(); diff --git a/contracts/warp-account-tracker/src/contract.rs b/contracts/warp-account-tracker/src/contract.rs index 80ef2f7a..4bd3e3cb 100644 --- a/contracts/warp-account-tracker/src/contract.rs +++ b/contracts/warp-account-tracker/src/contract.rs @@ -1,10 +1,10 @@ use crate::state::CONFIG; use crate::{execute, query, ContractError}; +use account_tracker::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; use cw_utils::nonpayable; -use account_tracker::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( diff --git a/contracts/warp-account-tracker/src/execute/account.rs b/contracts/warp-account-tracker/src/execute/account.rs index 6c4816f2..db9f8af1 100644 --- a/contracts/warp-account-tracker/src/execute/account.rs +++ b/contracts/warp-account-tracker/src/execute/account.rs @@ -2,11 +2,11 @@ use crate::state::{ FREE_ACCOUNTS, FUNDING_ACCOUNTS_BY_USER, TAKEN_ACCOUNTS, TAKEN_FUNDING_ACCOUNT_BY_JOB, }; use crate::ContractError; -use cosmwasm_std::{DepsMut, Response}; use account_tracker::{ AddFundingAccountMsg, FreeAccountMsg, FreeFundingAccountMsg, FundingAccount, TakeAccountMsg, TakeFundingAccountMsg, }; +use cosmwasm_std::{DepsMut, Response}; pub fn taken_account(deps: DepsMut, data: TakeAccountMsg) -> Result { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; diff --git a/contracts/warp-account-tracker/src/integration_tests.rs b/contracts/warp-account-tracker/src/integration_tests.rs index 8fe5b95a..242533de 100644 --- a/contracts/warp-account-tracker/src/integration_tests.rs +++ b/contracts/warp-account-tracker/src/integration_tests.rs @@ -1,13 +1,13 @@ #[cfg(test)] mod tests { - use anyhow::Result as AnyResult; - use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; - use cw_multi_test::{App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor}; use account_tracker::{ Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FreeAccountMsg, InstantiateMsg, QueryConfigMsg, QueryFirstFreeAccountMsg, QueryFreeAccountsMsg, QueryMsg, QueryTakenAccountsMsg, TakeAccountMsg, }; + use anyhow::Result as AnyResult; + use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; + use cw_multi_test::{App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor}; use crate::{ contract::{execute, instantiate, query}, @@ -75,8 +75,7 @@ mod tests { #[test] fn warp_account_tracker_contract_multi_test_account_management() { let mut app = mock_app(); - let warp_account_tracker_contract_code_id = - app.store_code(contract_warp_account_tracker()); + let warp_account_tracker_contract_code_id = app.store_code(contract_warp_account_tracker()); // Instantiate account let warp_account_tracker_contract_addr = diff --git a/contracts/warp-account-tracker/src/state.rs b/contracts/warp-account-tracker/src/state.rs index 47cacb36..2e10e009 100644 --- a/contracts/warp-account-tracker/src/state.rs +++ b/contracts/warp-account-tracker/src/state.rs @@ -1,6 +1,6 @@ +use account_tracker::{Config, FundingAccount}; use cosmwasm_std::{Addr, Uint64}; use cw_storage_plus::{Item, Map}; -use account_tracker::{Config, FundingAccount}; pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/warp-account/examples/warp-account-schema.rs b/contracts/warp-account/examples/warp-account-schema.rs index 1e95cd6d..74b58663 100644 --- a/contracts/warp-account/examples/warp-account-schema.rs +++ b/contracts/warp-account/examples/warp-account-schema.rs @@ -1,8 +1,8 @@ use std::env::current_dir; use std::fs::create_dir_all; -use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; use account::{Config, ExecuteMsg, InstantiateMsg, QueryMsg}; +use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; fn main() { let mut out_dir = current_dir().unwrap(); diff --git a/contracts/warp-account/src/contract.rs b/contracts/warp-account/src/contract.rs index 5ef27eb5..79438124 100644 --- a/contracts/warp-account/src/contract.rs +++ b/contracts/warp-account/src/contract.rs @@ -1,10 +1,10 @@ use crate::state::CONFIG; use crate::{query, ContractError}; +use account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use controller::account::{execute_warp_msgs, warp_msgs_to_cosmos_msgs}; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, }; -use account::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; #[cfg_attr(not(feature = "library"), entry_point)] pub fn instantiate( diff --git a/contracts/warp-account/src/query/account.rs b/contracts/warp-account/src/query/account.rs index bb0df58e..74bd5671 100644 --- a/contracts/warp-account/src/query/account.rs +++ b/contracts/warp-account/src/query/account.rs @@ -1,6 +1,6 @@ use crate::state::CONFIG; -use cosmwasm_std::{Deps, StdResult}; use account::ConfigResponse; +use cosmwasm_std::{Deps, StdResult}; pub fn query_config(deps: Deps) -> StdResult { let config = CONFIG.load(deps.storage)?; diff --git a/contracts/warp-account/src/tests.rs b/contracts/warp-account/src/tests.rs index a1cfcd74..3d1375e1 100644 --- a/contracts/warp-account/src/tests.rs +++ b/contracts/warp-account/src/tests.rs @@ -1,12 +1,12 @@ use crate::contract::{execute, instantiate}; use crate::ContractError; +use account::{ExecuteMsg, InstantiateMsg}; use controller::account::{WarpMsg, WarpMsgs}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{ to_binary, BankMsg, Coin, CosmosMsg, DistributionMsg, GovMsg, IbcMsg, IbcTimeout, IbcTimeoutBlock, Response, StakingMsg, Uint128, Uint64, VoteOption, WasmMsg, }; -use account::{ExecuteMsg, InstantiateMsg}; #[test] fn test_execute_controller() { diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 617e8185..733bd43a 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -25,8 +25,8 @@ use crate::util::{ }, }; -use controller::{account::CwFund, Config}; use account_tracker::{AccountResponse, FundingAccountResponse}; +use controller::{account::CwFund, Config}; use resolver::QueryHydrateMsgsMsg; use super::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee}; diff --git a/contracts/warp-controller/src/migrate/account.rs b/contracts/warp-controller/src/migrate/account.rs index 0a95b03c..2f6ee7c1 100644 --- a/contracts/warp-controller/src/migrate/account.rs +++ b/contracts/warp-controller/src/migrate/account.rs @@ -1,10 +1,8 @@ use cosmwasm_std::{to_binary, Deps, Env, MessageInfo, Response, WasmMsg}; use crate::ContractError; +use account_tracker::{AccountsResponse, MigrateMsg, QueryFreeAccountsMsg, QueryTakenAccountsMsg}; use controller::{Config, MigrateAccountsMsg}; -use account_tracker::{ - AccountsResponse, MigrateMsg, QueryFreeAccountsMsg, QueryTakenAccountsMsg, -}; pub fn migrate_free_accounts( deps: Deps, diff --git a/contracts/warp-controller/src/migrate/mod.rs b/contracts/warp-controller/src/migrate/mod.rs index 6f297081..d882e236 100644 --- a/contracts/warp-controller/src/migrate/mod.rs +++ b/contracts/warp-controller/src/migrate/mod.rs @@ -1,2 +1,2 @@ -pub(crate) mod job; pub(crate) mod account; +pub(crate) mod job; diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index c5006442..9272f44c 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -1,13 +1,13 @@ use cosmwasm_std::{to_binary, BankMsg, Coin, CosmosMsg, Uint128, Uint64, WasmMsg}; -use controller::account::{ - AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, WarpMsg, WarpMsgs, - WithdrawAssetsMsg, -}; use account_tracker::{ AddFundingAccountMsg, FreeAccountMsg, FreeFundingAccountMsg, TakeAccountMsg, TakeFundingAccountMsg, }; +use controller::account::{ + AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, WarpMsg, WarpMsgs, + WithdrawAssetsMsg, +}; #[allow(clippy::too_many_arguments)] pub fn build_instantiate_account_tracker_msg( @@ -62,13 +62,11 @@ pub fn build_free_account_msg( ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: account_tracker_addr, - msg: to_binary(&account_tracker::ExecuteMsg::FreeAccount( - FreeAccountMsg { - account_owner_addr, - account_addr, - last_job_id, - }, - )) + msg: to_binary(&account_tracker::ExecuteMsg::FreeAccount(FreeAccountMsg { + account_owner_addr, + account_addr, + last_job_id, + })) .unwrap(), funds: vec![], }) @@ -82,13 +80,11 @@ pub fn build_taken_account_msg( ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: account_tracker_addr, - msg: to_binary(&account_tracker::ExecuteMsg::TakeAccount( - TakeAccountMsg { - account_owner_addr, - account_addr, - job_id, - }, - )) + msg: to_binary(&account_tracker::ExecuteMsg::TakeAccount(TakeAccountMsg { + account_owner_addr, + account_addr, + job_id, + })) .unwrap(), funds: vec![], }) From 0f8ab1dce61f5a80fa6be4bee8982324199278fe Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Dec 2023 11:49:57 +0100 Subject: [PATCH 103/133] update templates to work with executions --- contracts/warp-templates/src/contract.rs | 10 ++++------ packages/templates/src/template.rs | 8 +++----- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/contracts/warp-templates/src/contract.rs b/contracts/warp-templates/src/contract.rs index 298d4776..bf12f934 100644 --- a/contracts/warp-templates/src/contract.rs +++ b/contracts/warp-templates/src/contract.rs @@ -118,10 +118,9 @@ pub fn submit_template( id: state.current_template_id, owner: info.sender.clone(), name: data.name.clone(), - msg: data.msg.clone(), + executions: data.executions.clone(), formatted_str: data.formatted_str.clone(), vars: data.vars.clone(), - condition: data.condition.clone(), }; TEMPLATES.save(deps.storage, state.current_template_id.u64(), &msg_template)?; @@ -146,7 +145,7 @@ pub fn submit_template( .add_attribute("id", state.current_template_id) .add_attribute("owner", info.sender) .add_attribute("name", data.name) - .add_attribute("msg", data.msg) + .add_attribute("executions", serde_json_wasm::to_string(&data.executions)?) .add_attribute("formatted_str", data.formatted_str) .add_attribute("vars", serde_json_wasm::to_string(&data.vars)?)) } @@ -177,10 +176,9 @@ pub fn edit_template( id: t.id, owner: t.owner, name: data.name.unwrap_or(t.name), - msg: t.msg, + executions: t.executions, formatted_str: t.formatted_str, vars: t.vars, - condition: t.condition, }), })?; @@ -189,7 +187,7 @@ pub fn edit_template( .add_attribute("id", t.id) .add_attribute("owner", info.sender) .add_attribute("name", t.name) - .add_attribute("msg", t.msg) + .add_attribute("executions", serde_json_wasm::to_string(&t.executions)?) .add_attribute("formatted_str", t.formatted_str) .add_attribute("vars", serde_json_wasm::to_string(&t.vars)?)) } diff --git a/packages/templates/src/template.rs b/packages/templates/src/template.rs index 90f6ed32..0806f49d 100644 --- a/packages/templates/src/template.rs +++ b/packages/templates/src/template.rs @@ -1,6 +1,6 @@ +use controller::job::Execution; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Uint64}; -use resolver::condition::Condition; use resolver::variable::Variable; //msg templates @@ -10,16 +10,14 @@ pub struct Template { pub owner: Addr, pub name: String, pub vars: Vec, - pub msg: String, - pub condition: Option, + pub executions: Vec, pub formatted_str: String, } #[cw_serde] pub struct SubmitTemplateMsg { pub name: String, - pub msg: String, - pub condition: Option, + pub executions: Vec, pub formatted_str: String, pub vars: Vec, } From aaeaed000d91f1a81007d7b1185fd07752fb1e6d Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Dec 2023 12:10:13 +0100 Subject: [PATCH 104/133] new testnet refs --- refs.json | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/refs.json b/refs.json index 18ec8085..b6128599 100644 --- a/refs.json +++ b/refs.json @@ -9,26 +9,22 @@ }, "testnet": { "warp-account": { - "codeId": "9626" + "codeId": "12318" }, "warp-controller": { - "codeId": "12126", - "address": "terra1fqcfh8vpqsl7l5yjjtq5wwu6sv989txncq5fa756tv7lywqexraq5vnjvt" + "codeId": "12321", + "address": "terra1sylnrt9rkv7lraqvxssdzwmhm804qjtlp8ssjc502538ykyhdn5sns9edd" }, "warp-resolver": { - "codeId": "12124", - "address": "terra1lxfx6n792aw3hg47tchmyuhv5t30f334gus67pc250qx5zljadws65elnf" + "codeId": "12319", + "address": "terra1yr249ds5f24u72cnyspdu0vghlkxyfanjj5kyagx9q4fm6nlsads89f9l7" }, "warp-templates": { - "codeId": "9263", - "address": "terra17xm2ewyg60y7eypnwav33fwm23hxs3qyd8qk9tnntj4d0rp2vvhsgkpwwp" - }, - "warp-account": { - "codeId": "12123" + "codeId": "12320", + "address": "terra1g8v5syvvfcsdlwd5yyguujlsf6vnadhdxghmnwu3ah6q8m7vn0jq0hcgwt" }, "warp-account-tracker": { - "codeId": "12122", - "address": "terra1zzgg30ygltd5s3xtescfquwmm2jktaq28t37f2j9h5wwswpxtyyspugek8" + "codeId": "12322" } }, "mainnet": { From 7941150b042967ad3937d420a96bf5c974466fae Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Dec 2023 12:12:27 +0100 Subject: [PATCH 105/133] add account tracker addr --- refs.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/refs.json b/refs.json index b6128599..d2c34726 100644 --- a/refs.json +++ b/refs.json @@ -24,7 +24,8 @@ "address": "terra1g8v5syvvfcsdlwd5yyguujlsf6vnadhdxghmnwu3ah6q8m7vn0jq0hcgwt" }, "warp-account-tracker": { - "codeId": "12322" + "codeId": "12322", + "address": "terra1lhxshdp748xs56v83rsegwpmyuqxf2uszdr3wuazjrfq2wza2kyslq800h" } }, "mainnet": { From aeb71598fce1484e645716571261e4b782f8427d Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Dec 2023 14:35:03 +0100 Subject: [PATCH 106/133] migrate + more descriptive errors --- contracts/warp-controller/src/contract.rs | 8 +------- contracts/warp-controller/src/error.rs | 3 +++ contracts/warp-controller/src/execute/job.rs | 4 ++++ packages/controller/src/lib.rs | 1 - refs.json | 2 +- tasks/migrate_warp.ts | 1 - 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index f26ce446..4b2824be 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -168,13 +168,7 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult { } #[cfg_attr(not(feature = "library"), entry_point)] -pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> Result { - let mut config = CONFIG.load(deps.storage)?; - - config.warp_account_code_id = msg.warp_account_code_id; - - CONFIG.save(deps.storage, &config)?; - +pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result { Ok(Response::new()) } diff --git a/contracts/warp-controller/src/error.rs b/contracts/warp-controller/src/error.rs index 1f749643..67eb2a06 100644 --- a/contracts/warp-controller/src/error.rs +++ b/contracts/warp-controller/src/error.rs @@ -15,6 +15,9 @@ pub enum ContractError { #[error("Insufficient funds to pay for reward and fee.")] InsufficientFundsToPayForRewardAndFee {}, + #[error("Insufficient operational funds.")] + InsufficientOperationalFunds {}, + #[error("Insufficient funds to pay for fee.")] InsufficientFundsToPayForFee {}, diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 733bd43a..117aab6d 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -74,6 +74,10 @@ pub fn create_job( let total_fees = creation_fee + maintenance_fee + burn_fee; if data.operational_amount > fee_denom_paid_amount { + return Err(ContractError::InsufficientOperationalFunds {}); + } + + if data.reward + total_fees > fee_denom_paid_amount { return Err(ContractError::InsufficientFundsToPayForRewardAndFee {}); } diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 0cc7a10e..c8889396 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -183,5 +183,4 @@ pub struct StateResponse { #[cw_serde] pub struct MigrateMsg { - pub warp_account_code_id: Uint64, } diff --git a/refs.json b/refs.json index d2c34726..28367cfd 100644 --- a/refs.json +++ b/refs.json @@ -12,7 +12,7 @@ "codeId": "12318" }, "warp-controller": { - "codeId": "12321", + "codeId": "12328", "address": "terra1sylnrt9rkv7lraqvxssdzwmhm804qjtlp8ssjc502538ykyhdn5sns9edd" }, "warp-resolver": { diff --git a/tasks/migrate_warp.ts b/tasks/migrate_warp.ts index 7207d7c6..347b7c7a 100644 --- a/tasks/migrate_warp.ts +++ b/tasks/migrate_warp.ts @@ -15,7 +15,6 @@ task(async ({ deployer, signer, refs, network }) => { contract.address!, parseInt(contract.codeId!), { - warp_account_code_id: "12123" } ); From b746914ce405edc8b87c169fd55528d51676fe20 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Dec 2023 15:31:03 +0100 Subject: [PATCH 107/133] fix querying funding accounts response --- .../warp-account-tracker/src/query/account.rs | 20 +++++++++++++------ packages/controller/src/lib.rs | 3 +-- refs.json | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/contracts/warp-account-tracker/src/query/account.rs b/contracts/warp-account-tracker/src/query/account.rs index 6144d5ce..ce922dca 100644 --- a/contracts/warp-account-tracker/src/query/account.rs +++ b/contracts/warp-account-tracker/src/query/account.rs @@ -143,7 +143,12 @@ pub fn query_funding_accounts( ) -> StdResult { let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; - let funding_accounts = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref)?; + let resp = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref); + + let funding_accounts = match resp { + Ok(funding_accounts) => funding_accounts, + Err(_) => vec![], + }; Ok(FundingAccountsResponse { funding_accounts }) } @@ -154,12 +159,15 @@ pub fn query_first_free_funding_account( ) -> StdResult { let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; - let funding_accounts = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref)?; + let resp = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref); - let funding_account = funding_accounts - .iter() - .find(|fa| fa.taken_by_job_ids.is_empty()) - .cloned(); + let funding_account = match resp { + Ok(funding_accounts) => funding_accounts + .iter() + .find(|fa| fa.taken_by_job_ids.is_empty()) + .cloned(), + Err(_) => None, + }; Ok(FundingAccountResponse { funding_account }) } diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index c8889396..a6abef5e 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -182,5 +182,4 @@ pub struct StateResponse { } #[cw_serde] -pub struct MigrateMsg { -} +pub struct MigrateMsg {} diff --git a/refs.json b/refs.json index 28367cfd..2507802c 100644 --- a/refs.json +++ b/refs.json @@ -24,7 +24,7 @@ "address": "terra1g8v5syvvfcsdlwd5yyguujlsf6vnadhdxghmnwu3ah6q8m7vn0jq0hcgwt" }, "warp-account-tracker": { - "codeId": "12322", + "codeId": "12329", "address": "terra1lhxshdp748xs56v83rsegwpmyuqxf2uszdr3wuazjrfq2wza2kyslq800h" } }, From fca3f6c20d519d8a40dad25fd2d60bf789da192a Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Dec 2023 17:16:21 +0100 Subject: [PATCH 108/133] minor --- contracts/warp-account-tracker/src/contract.rs | 2 +- .../warp-account-tracker/src/execute/account.rs | 4 ++-- contracts/warp-controller/src/execute/job.rs | 4 ++-- contracts/warp-controller/src/reply/account.rs | 15 +++++++-------- contracts/warp-controller/src/reply/job.rs | 4 ++-- contracts/warp-controller/src/util/msg.rs | 2 +- refs.json | 4 ++-- 7 files changed, 17 insertions(+), 18 deletions(-) diff --git a/contracts/warp-account-tracker/src/contract.rs b/contracts/warp-account-tracker/src/contract.rs index 4bd3e3cb..d1a254d8 100644 --- a/contracts/warp-account-tracker/src/contract.rs +++ b/contracts/warp-account-tracker/src/contract.rs @@ -46,7 +46,7 @@ pub fn execute( match msg { ExecuteMsg::TakeAccount(data) => { nonpayable(&info).unwrap(); - execute::account::taken_account(deps, data) + execute::account::take_account(deps, data) } ExecuteMsg::FreeAccount(data) => { nonpayable(&info).unwrap(); diff --git a/contracts/warp-account-tracker/src/execute/account.rs b/contracts/warp-account-tracker/src/execute/account.rs index db9f8af1..3103ee54 100644 --- a/contracts/warp-account-tracker/src/execute/account.rs +++ b/contracts/warp-account-tracker/src/execute/account.rs @@ -8,7 +8,7 @@ use account_tracker::{ }; use cosmwasm_std::{DepsMut, Response}; -pub fn taken_account(deps: DepsMut, data: TakeAccountMsg) -> Result { +pub fn take_account(deps: DepsMut, data: TakeAccountMsg) -> Result { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; FREE_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); @@ -21,7 +21,7 @@ pub fn taken_account(deps: DepsMut, data: TakeAccountMsg) -> Result Date: Mon, 11 Dec 2023 17:35:37 +0100 Subject: [PATCH 109/133] refactor mutable deps to deps.storage instaed --- contracts/warp-controller/src/execute/job.rs | 33 ++++++++-------- .../warp-controller/src/reply/account.rs | 12 +++--- contracts/warp-controller/src/reply/job.rs | 6 +-- contracts/warp-controller/src/state.rs | 38 ++++++++++--------- refs.json | 2 +- 5 files changed, 48 insertions(+), 43 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index f6f7c77e..5428f290 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -34,7 +34,7 @@ use super::fee::{compute_burn_fee, compute_creation_fee, compute_maintenance_fee const MAX_TEXT_LENGTH: usize = 280; pub fn create_job( - mut deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, data: CreateJobMsg, @@ -110,7 +110,7 @@ pub fn create_job( .checked_sub(data.reward + total_fees)?; let mut job = JobQueue::add( - &mut deps, + deps.storage, Job { id: state.current_job_id, prev_id: None, @@ -196,7 +196,7 @@ pub fn create_job( let available_account_addr = &available_account.addr; // Update job.account from placeholder value to job account job.account = available_account_addr.clone(); - JobQueue::sync(&mut deps, env.clone(), job.clone())?; + JobQueue::sync(deps.storage, env.clone(), job.clone())?; if !native_funds_minus_operational_amount.is_empty() { // Fund account in native coins @@ -300,7 +300,7 @@ pub fn create_job( let available_account_addr = &available_account.account_addr; // Update funding_account from placeholder value to funding account job.funding_account = Some(available_account_addr.clone()); - JobQueue::sync(&mut deps, env, job.clone())?; + JobQueue::sync(deps.storage, env, job.clone())?; // Fund account in native coins msgs.push(build_transfer_native_funds_msg( @@ -354,14 +354,14 @@ pub fn create_job( } pub fn delete_job( - mut deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, data: DeleteJobMsg, config: Config, fee_denom_paid_amount: Uint128, ) -> Result { - let job = JobQueue::get(&deps, data.id.into())?; + let job = JobQueue::get(deps.storage, data.id.into())?; let account_addr = job.account.clone(); if job.status != JobStatus::Pending { @@ -372,7 +372,7 @@ pub fn delete_job( return Err(ContractError::Unauthorized {}); } - let _new_job = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Cancelled)?; + let _new_job = JobQueue::finalize(deps.storage, env, job.id.into(), JobStatus::Cancelled)?; let fee = job.reward * Uint128::from(config.cancellation_fee_percentage) / Uint128::new(100); if fee > fee_denom_paid_amount { @@ -435,12 +435,12 @@ pub fn delete_job( } pub fn update_job( - mut deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, data: UpdateJobMsg, ) -> Result { - let job = JobQueue::get(&deps, data.id.into())?; + let job = JobQueue::get(deps.storage, data.id.into())?; if info.sender != job.owner { return Err(ContractError::Unauthorized {}); @@ -454,7 +454,7 @@ pub fn update_job( return Err(ContractError::NameTooShort {}); } - let job = JobQueue::update(&mut deps, env, data)?; + let job = JobQueue::update(deps.storage, env, data)?; Ok(Response::new() .add_attribute("action", "update_job") @@ -471,13 +471,13 @@ pub fn update_job( } pub fn execute_job( - mut deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, data: ExecuteJobMsg, config: Config, ) -> Result { - let job = JobQueue::get(&deps, data.id.into())?; + let job = JobQueue::get(deps.storage, data.id.into())?; let account_addr = job.account.clone(); if job.status != JobStatus::Pending { @@ -537,7 +537,7 @@ pub fn execute_job( Err(e) => { attrs.push(Attribute::new("job_condition_status", "invalid")); attrs.push(Attribute::new("error", e.to_string())); - JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Failed)?; + JobQueue::finalize(deps.storage, env, job.id.into(), JobStatus::Failed)?; break; } } @@ -577,13 +577,13 @@ pub fn execute_job( } pub fn evict_job( - mut deps: DepsMut, + deps: DepsMut, env: Env, info: MessageInfo, data: EvictJobMsg, config: Config, ) -> Result { - let job = JobQueue::get(&deps, data.id.into())?; + let job = JobQueue::get(deps.storage, data.id.into())?; let account_addr = job.account.clone(); if job.status != JobStatus::Pending { @@ -599,7 +599,8 @@ pub fn evict_job( let mut msgs = vec![]; // Job will be evicted - let job_status = JobQueue::finalize(&mut deps, env, job.id.into(), JobStatus::Evicted)?.status; + let job_status = + JobQueue::finalize(deps.storage, env, job.id.into(), JobStatus::Evicted)?.status; // Controller sends eviction reward to evictor msgs.push(build_transfer_native_funds_msg( diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 8139d6cd..b5629c35 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -15,7 +15,7 @@ use crate::{ }; pub fn create_account_and_job( - mut deps: DepsMut, + deps: DepsMut, env: Env, msg: Reply, config: Config, @@ -90,9 +90,9 @@ pub fn create_account_and_job( .value, )?; - let mut job = JobQueue::get(&deps, job_id)?; + let mut job = JobQueue::get(deps.storage, job_id)?; job.account = account_addr.clone(); - JobQueue::sync(&mut deps, env, job.clone())?; + JobQueue::sync(deps.storage, env, job.clone())?; let mut msgs: Vec = vec![]; @@ -149,7 +149,7 @@ pub fn create_account_and_job( } pub fn create_funding_account_and_job( - mut deps: DepsMut, + deps: DepsMut, env: Env, msg: Reply, config: Config, @@ -204,9 +204,9 @@ pub fn create_funding_account_and_job( .value, )?; - let mut job = JobQueue::get(&deps, job_id)?; + let mut job = JobQueue::get(deps.storage, job_id)?; job.funding_account = Some(funding_account_addr.clone()); - JobQueue::sync(&mut deps, env, job.clone())?; + JobQueue::sync(deps.storage, env, job.clone())?; let msgs: Vec = vec![build_take_funding_account_msg( config.account_tracker_address.to_string(), diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 59d8f0e7..6bfe5675 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -20,7 +20,7 @@ use controller::{ }; pub fn execute_job( - mut deps: DepsMut, + deps: DepsMut, env: Env, msg: Reply, config: Config, @@ -59,7 +59,7 @@ pub fn execute_job( let job_id = job_id_str.as_str().parse::()?; - let finished_job = JobQueue::finalize(&mut deps, env.clone(), job_id, new_status)?; + let finished_job = JobQueue::finalize(deps.storage, env.clone(), job_id, new_status)?; let res_attrs = match msg.result { SubMsgResult::Err(e) => vec![Attribute::new( @@ -170,7 +170,7 @@ pub fn execute_job( operational_amount.checked_sub(finished_job.reward + total_fees)?; let new_job = JobQueue::add( - &mut deps, + deps.storage, Job { id: new_job_id, prev_id: Some(finished_job.id), diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 816d5197..c78e7f91 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{DepsMut, Env, Uint64}; +use cosmwasm_std::{Env, Storage, Uint64}; use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; use controller::{ @@ -59,16 +59,16 @@ pub const STATE: Item = Item::new("state"); pub struct JobQueue; impl JobQueue { - pub fn add(deps: &mut DepsMut, job: Job) -> Result { - let state = STATE.load(deps.storage)?; + pub fn add(storage: &mut dyn Storage, job: Job) -> Result { + let state: State = STATE.load(storage)?; - let job = PENDING_JOBS().update(deps.storage, state.current_job_id.u64(), |s| match s { + let job = PENDING_JOBS().update(storage, state.current_job_id.u64(), |s| match s { None => Ok(job), Some(_) => Err(ContractError::JobAlreadyExists {}), })?; STATE.save( - deps.storage, + storage, &State { current_job_id: state.current_job_id.checked_add(Uint64::new(1))?, q: state.q.checked_add(Uint64::new(1))?, @@ -78,14 +78,14 @@ impl JobQueue { Ok(job) } - pub fn get(deps: &DepsMut, job_id: u64) -> Result { - let job = PENDING_JOBS().load(deps.storage, job_id)?; + pub fn get(storage: &dyn Storage, job_id: u64) -> Result { + let job = PENDING_JOBS().load(storage, job_id)?; Ok(job) } - pub fn sync(deps: &mut DepsMut, env: Env, job: Job) -> Result { - PENDING_JOBS().update(deps.storage, job.id.u64(), |j| match j { + pub fn sync(storage: &mut dyn Storage, env: Env, job: Job) -> Result { + PENDING_JOBS().update(storage, job.id.u64(), |j| match j { None => Err(ContractError::JobDoesNotExist {}), Some(job) => Ok(Job { id: job.id, @@ -111,8 +111,12 @@ impl JobQueue { }) } - pub fn update(deps: &mut DepsMut, _env: Env, data: UpdateJobMsg) -> Result { - PENDING_JOBS().update(deps.storage, data.id.u64(), |h| match h { + pub fn update( + storage: &mut dyn Storage, + _env: Env, + data: UpdateJobMsg, + ) -> Result { + PENDING_JOBS().update(storage, data.id.u64(), |h| match h { None => Err(ContractError::JobDoesNotExist {}), Some(job) => Ok(Job { id: job.id, @@ -139,7 +143,7 @@ impl JobQueue { } pub fn finalize( - deps: &mut DepsMut, + storage: &mut dyn Storage, env: Env, job_id: u64, status: JobStatus, @@ -148,7 +152,7 @@ impl JobQueue { return Err(ContractError::Unauthorized {}); } - let job = PENDING_JOBS().load(deps.storage, job_id)?; + let job = PENDING_JOBS().load(storage, job_id)?; let new_job = Job { id: job.id, @@ -172,16 +176,16 @@ impl JobQueue { operational_amount: job.operational_amount, }; - FINISHED_JOBS().update(deps.storage, job_id, |j| match j { + FINISHED_JOBS().update(storage, job_id, |j| match j { None => Ok(new_job.clone()), Some(_) => Err(ContractError::JobAlreadyFinished {}), })?; - PENDING_JOBS().remove(deps.storage, job_id)?; + PENDING_JOBS().remove(storage, job_id)?; - let state = STATE.load(deps.storage)?; + let state = STATE.load(storage)?; STATE.save( - deps.storage, + storage, &State { current_job_id: state.current_job_id, q: state.q.checked_sub(Uint64::new(1))?, diff --git a/refs.json b/refs.json index 8188dc4e..5ec4fa81 100644 --- a/refs.json +++ b/refs.json @@ -12,7 +12,7 @@ "codeId": "12318" }, "warp-controller": { - "codeId": "12333", + "codeId": "12334", "address": "terra1sylnrt9rkv7lraqvxssdzwmhm804qjtlp8ssjc502538ykyhdn5sns9edd" }, "warp-resolver": { From c0d06e6835dcd987849aa2a0986aaefc62d7df4d Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 11 Dec 2023 18:35:38 +0100 Subject: [PATCH 110/133] refactor --- contracts/warp-controller/src/state.rs | 14 +++++++++----- refs.json | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index c78e7f91..8d170984 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -85,9 +85,9 @@ impl JobQueue { } pub fn sync(storage: &mut dyn Storage, env: Env, job: Job) -> Result { - PENDING_JOBS().update(storage, job.id.u64(), |j| match j { + let res = PENDING_JOBS().update(storage, job.id.u64(), |j| match j { None => Err(ContractError::JobDoesNotExist {}), - Some(job) => Ok(Job { + Some(_) => Ok(Job { id: job.id, prev_id: job.prev_id, owner: job.owner, @@ -108,7 +108,9 @@ impl JobQueue { created_at_time: Uint64::from(env.block.time.seconds()), funding_account: job.funding_account, }), - }) + })?; + + Ok(res) } pub fn update( @@ -116,7 +118,7 @@ impl JobQueue { _env: Env, data: UpdateJobMsg, ) -> Result { - PENDING_JOBS().update(storage, data.id.u64(), |h| match h { + let job = PENDING_JOBS().update(storage, data.id.u64(), |h| match h { None => Err(ContractError::JobDoesNotExist {}), Some(job) => Ok(Job { id: job.id, @@ -139,7 +141,9 @@ impl JobQueue { funding_account: job.funding_account, operational_amount: job.operational_amount, }), - }) + })?; + + Ok(job) } pub fn finalize( diff --git a/refs.json b/refs.json index 5ec4fa81..29f103eb 100644 --- a/refs.json +++ b/refs.json @@ -12,7 +12,7 @@ "codeId": "12318" }, "warp-controller": { - "codeId": "12334", + "codeId": "12339", "address": "terra1sylnrt9rkv7lraqvxssdzwmhm804qjtlp8ssjc502538ykyhdn5sns9edd" }, "warp-resolver": { From 31afa7f51676cf59e907eda9cee3747d3fdfe3f3 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 12 Dec 2023 13:57:45 +0100 Subject: [PATCH 111/133] expose warp_msgs_to_cosmos_msgs in resolver --- contracts/warp-resolver/src/contract.rs | 20 ++++++++++++++++++-- packages/resolver/src/lib.rs | 9 ++++++++- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/contracts/warp-resolver/src/contract.rs b/contracts/warp-resolver/src/contract.rs index 5a22d42d..951090e7 100644 --- a/contracts/warp-resolver/src/contract.rs +++ b/contracts/warp-resolver/src/contract.rs @@ -4,7 +4,7 @@ use crate::util::variable::{ vars_valid, }; use crate::ContractError; -use controller::account::WarpMsg; +use controller::account::{warp_msgs_to_cosmos_msgs, WarpMsg}; use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult, }; @@ -17,7 +17,7 @@ use resolver::{ ExecuteResolveConditionMsg, ExecuteSimulateQueryMsg, ExecuteValidateJobCreationMsg, InstantiateMsg, MigrateMsg, QueryApplyVarFnMsg, QueryHydrateMsgsMsg, QueryHydrateVarsMsg, QueryMsg, QueryResolveConditionMsg, QueryValidateJobCreationMsg, SimulateQueryMsg, - SimulateResponse, + SimulateResponse, WarpMsgsToCosmosMsgsMsg, }; #[cfg_attr(not(feature = "library"), entry_point)] @@ -49,9 +49,25 @@ pub fn execute( } ExecuteMsg::ExecuteApplyVarFn(data) => execute_apply_var_fn(deps, env, info, data), ExecuteMsg::ExecuteHydrateMsgs(data) => execute_hydrate_msgs(deps, env, info, data), + ExecuteMsg::WarpMsgsToCosmosMsgs(data) => { + execute_warp_msgs_to_cosmos_msgs(deps, env, info, data) + } } } +fn execute_warp_msgs_to_cosmos_msgs( + deps: DepsMut, + env: Env, + _info: MessageInfo, + msg: WarpMsgsToCosmosMsgsMsg, +) -> Result { + let result = warp_msgs_to_cosmos_msgs(deps.as_ref(), env, msg.msgs, &msg.owner)?; + + Ok(Response::new() + .add_attribute("action", "warp_msgs_to_cosmos_msgs") + .add_attribute("response", serde_json_wasm::to_string(&result)?)) +} + pub fn execute_simulate_query( deps: DepsMut, env: Env, diff --git a/packages/resolver/src/lib.rs b/packages/resolver/src/lib.rs index ad5abe6d..34f76f74 100644 --- a/packages/resolver/src/lib.rs +++ b/packages/resolver/src/lib.rs @@ -6,7 +6,7 @@ use controller::{ job::{Execution, ExternalInput, JobStatus}, }; use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::QueryRequest; +use cosmwasm_std::{Addr, QueryRequest}; #[cw_serde] pub struct InstantiateMsg {} @@ -18,6 +18,7 @@ pub enum ExecuteMsg { ExecuteResolveCondition(ExecuteResolveConditionMsg), ExecuteApplyVarFn(ExecuteApplyVarFnMsg), ExecuteHydrateMsgs(ExecuteHydrateMsgsMsg), + WarpMsgsToCosmosMsgs(WarpMsgsToCosmosMsgsMsg), } #[derive(QueryResponses)] @@ -40,6 +41,12 @@ pub enum QueryMsg { #[cw_serde] pub struct MigrateMsg {} +#[cw_serde] +pub struct WarpMsgsToCosmosMsgsMsg { + pub msgs: Vec, + pub owner: Addr, +} + #[cw_serde] pub struct ExecuteSimulateQueryMsg { pub query: QueryRequest, From d194ecd574aa81588e8064576f9381b15b698553 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 12 Dec 2023 14:50:27 +0100 Subject: [PATCH 112/133] rename account to job_account --- .../warp-account-tracker/src/contract.rs | 20 +++++----- .../src/execute/account.rs | 30 +++++++-------- .../src/integration_tests.rs | 38 +++++++++---------- .../warp-account-tracker/src/query/account.rs | 34 +++++++++-------- contracts/warp-account-tracker/src/state.rs | 4 +- contracts/warp-controller/src/contract.rs | 8 ++-- contracts/warp-controller/src/execute/job.rs | 16 ++++---- .../warp-controller/src/migrate/account.rs | 20 +++++----- .../warp-controller/src/reply/account.rs | 7 ++-- contracts/warp-controller/src/reply/job.rs | 5 ++- contracts/warp-controller/src/util/fee.rs | 4 ++ contracts/warp-controller/src/util/msg.rs | 32 +++++++++------- packages/account-tracker/src/lib.rs | 22 +++++------ packages/controller/src/lib.rs | 4 +- refs.json | 4 +- 15 files changed, 132 insertions(+), 116 deletions(-) diff --git a/contracts/warp-account-tracker/src/contract.rs b/contracts/warp-account-tracker/src/contract.rs index d1a254d8..0896d423 100644 --- a/contracts/warp-account-tracker/src/contract.rs +++ b/contracts/warp-account-tracker/src/contract.rs @@ -44,13 +44,13 @@ pub fn execute( } match msg { - ExecuteMsg::TakeAccount(data) => { + ExecuteMsg::TakeJobAccount(data) => { nonpayable(&info).unwrap(); - execute::account::take_account(deps, data) + execute::account::take_job_account(deps, data) } - ExecuteMsg::FreeAccount(data) => { + ExecuteMsg::FreeJobAccount(data) => { nonpayable(&info).unwrap(); - execute::account::free_account(deps, data) + execute::account::free_job_account(deps, data) } ExecuteMsg::TakeFundingAccount(data) => { nonpayable(&info).unwrap(); @@ -71,14 +71,14 @@ pub fn execute( pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), - QueryMsg::QueryTakenAccounts(data) => { - to_binary(&query::account::query_taken_accounts(deps, data)?) + QueryMsg::QueryTakenJobAccounts(data) => { + to_binary(&query::account::query_taken_job_accounts(deps, data)?) } - QueryMsg::QueryFreeAccounts(data) => { - to_binary(&query::account::query_free_accounts(deps, data)?) + QueryMsg::QueryFreeJobAccounts(data) => { + to_binary(&query::account::query_free_job_accounts(deps, data)?) } - QueryMsg::QueryFirstFreeAccount(data) => { - to_binary(&query::account::query_first_free_account(deps, data)?) + QueryMsg::QueryFirstFreeJobAccount(data) => { + to_binary(&query::account::query_first_free_job_account(deps, data)?) } QueryMsg::QueryFundingAccounts(data) => { to_binary(&query::account::query_funding_accounts(deps, data)?) diff --git a/contracts/warp-account-tracker/src/execute/account.rs b/contracts/warp-account-tracker/src/execute/account.rs index 3103ee54..c07e2c56 100644 --- a/contracts/warp-account-tracker/src/execute/account.rs +++ b/contracts/warp-account-tracker/src/execute/account.rs @@ -1,18 +1,18 @@ use crate::state::{ - FREE_ACCOUNTS, FUNDING_ACCOUNTS_BY_USER, TAKEN_ACCOUNTS, TAKEN_FUNDING_ACCOUNT_BY_JOB, + FREE_JOB_ACCOUNTS, FUNDING_ACCOUNTS_BY_USER, TAKEN_FUNDING_ACCOUNT_BY_JOB, TAKEN_JOB_ACCOUNTS, }; use crate::ContractError; use account_tracker::{ - AddFundingAccountMsg, FreeAccountMsg, FreeFundingAccountMsg, FundingAccount, TakeAccountMsg, - TakeFundingAccountMsg, + AddFundingAccountMsg, FreeFundingAccountMsg, FreeJobAccountMsg, FundingAccount, + TakeFundingAccountMsg, TakeJobAccountMsg, }; use cosmwasm_std::{DepsMut, Response}; -pub fn take_account(deps: DepsMut, data: TakeAccountMsg) -> Result { +pub fn take_job_account(deps: DepsMut, data: TakeJobAccountMsg) -> Result { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; - FREE_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); - TAKEN_ACCOUNTS.update( + FREE_JOB_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); + TAKEN_JOB_ACCOUNTS.update( deps.storage, (account_owner_ref, account_addr_ref), |s| match s { @@ -21,16 +21,16 @@ pub fn take_account(deps: DepsMut, data: TakeAccountMsg) -> Result Result { +pub fn free_job_account(deps: DepsMut, data: FreeJobAccountMsg) -> Result { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; - TAKEN_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); - FREE_ACCOUNTS.update( + TAKEN_JOB_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); + FREE_JOB_ACCOUNTS.update( deps.storage, (account_owner_ref, account_addr_ref), |s| match s { @@ -40,7 +40,7 @@ pub fn free_account(deps: DepsMut, data: FreeAccountMsg) -> Result StdResult { Ok(ConfigResponse { config }) } -pub fn query_first_free_account( +pub fn query_first_free_job_account( deps: Deps, - data: QueryFirstFreeAccountMsg, + data: QueryFirstFreeJobAccountMsg, ) -> StdResult { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; - let maybe_free_account = FREE_ACCOUNTS + let maybe_free_job_account = FREE_JOB_ACCOUNTS .prefix_range( deps.storage, Some(PrefixBound::inclusive(account_owner_ref)), @@ -29,7 +30,7 @@ pub fn query_first_free_account( Order::Ascending, ) .next(); - let free_account = match maybe_free_account { + let free_job_account = match maybe_free_job_account { Some(Ok((account, last_job_id))) => Some(Account { addr: account.1, taken_by_job_id: Some(last_job_id), @@ -37,19 +38,19 @@ pub fn query_first_free_account( _ => None, }; Ok(AccountResponse { - account: free_account, + account: free_job_account, }) } -pub fn query_taken_accounts( +pub fn query_taken_job_accounts( deps: Deps, - data: QueryTakenAccountsMsg, + data: QueryTakenJobAccountsMsg, ) -> StdResult { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let iter = match data.start_after { Some(start_after) => { let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; - TAKEN_ACCOUNTS.range( + TAKEN_JOB_ACCOUNTS.range( deps.storage, Some(Bound::exclusive(( account_owner_ref, @@ -59,7 +60,7 @@ pub fn query_taken_accounts( Order::Descending, ) } - None => TAKEN_ACCOUNTS.prefix_range( + None => TAKEN_JOB_ACCOUNTS.prefix_range( deps.storage, Some(PrefixBound::inclusive(account_owner_ref)), Some(PrefixBound::inclusive(account_owner_ref)), @@ -81,12 +82,15 @@ pub fn query_taken_accounts( }) } -pub fn query_free_accounts(deps: Deps, data: QueryFreeAccountsMsg) -> StdResult { +pub fn query_free_job_accounts( + deps: Deps, + data: QueryFreeJobAccountsMsg, +) -> StdResult { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let iter = match data.start_after { Some(start_after) => { let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; - FREE_ACCOUNTS.range( + FREE_JOB_ACCOUNTS.range( deps.storage, Some(Bound::exclusive(( account_owner_ref, @@ -96,7 +100,7 @@ pub fn query_free_accounts(deps: Deps, data: QueryFreeAccountsMsg) -> StdResult< Order::Descending, ) } - None => FREE_ACCOUNTS.prefix_range( + None => FREE_JOB_ACCOUNTS.prefix_range( deps.storage, Some(PrefixBound::inclusive(account_owner_ref)), Some(PrefixBound::inclusive(account_owner_ref)), diff --git a/contracts/warp-account-tracker/src/state.rs b/contracts/warp-account-tracker/src/state.rs index 2e10e009..3b947346 100644 --- a/contracts/warp-account-tracker/src/state.rs +++ b/contracts/warp-account-tracker/src/state.rs @@ -5,10 +5,10 @@ use cw_storage_plus::{Item, Map}; pub const CONFIG: Item = Item::new("config"); // Key is the (account owner address, account address), value is the ID of the pending job currently using it -pub const TAKEN_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("taken_accounts"); +pub const TAKEN_JOB_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("taken_job_accounts"); // Key is the (account owner address, account address), value is id of the last job which reserved it -pub const FREE_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("free_accounts"); +pub const FREE_JOB_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("free_job_accounts"); // owner address -> funding_account[] // - user can have multiple funding accounts diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 4b2824be..c448658b 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -131,13 +131,13 @@ pub fn execute( nonpayable(&info).unwrap(); execute::controller::update_config(deps, env, info, data, config) } - ExecuteMsg::MigrateFreeAccounts(data) => { + ExecuteMsg::MigrateFreeJobAccounts(data) => { nonpayable(&info).unwrap(); - migrate::account::migrate_free_accounts(deps.as_ref(), env, info, data, config) + migrate::account::migrate_free_job_accounts(deps.as_ref(), env, info, data, config) } - ExecuteMsg::MigrateTakenAccounts(data) => { + ExecuteMsg::MigrateTakenJobAccounts(data) => { nonpayable(&info).unwrap(); - migrate::account::migrate_taken_accounts(deps.as_ref(), env, info, data, config) + migrate::account::migrate_taken_job_accounts(deps.as_ref(), env, info, data, config) } ExecuteMsg::MigratePendingJobs(data) => { diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 5428f290..d03141a4 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -19,8 +19,8 @@ use cosmwasm_std::{ use crate::util::{ fee::deduct_from_native_funds, msg::{ - build_account_withdraw_assets_msg, build_free_account_msg, - build_instantiate_warp_account_msg, build_take_account_msg, build_transfer_cw20_msg, + build_account_withdraw_assets_msg, build_free_job_account_msg, + build_instantiate_warp_account_msg, build_take_job_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, build_transfer_native_funds_msg, }, }; @@ -141,8 +141,8 @@ pub fn create_job( let account_resp: AccountResponse = deps.querier.query_wasm_smart( account_tracker_address_ref, - &account_tracker::QueryMsg::QueryFirstFreeAccount( - account_tracker::QueryFirstFreeAccountMsg { + &account_tracker::QueryMsg::QueryFirstFreeJobAccount( + account_tracker::QueryFirstFreeJobAccountMsg { account_owner_addr: job_owner.to_string(), }, ), @@ -238,7 +238,7 @@ pub fn create_job( } // Take account - msgs.push(build_take_account_msg( + msgs.push(build_take_job_account_msg( config.account_tracker_address.to_string(), job_owner.to_string(), available_account_addr.to_string(), @@ -398,7 +398,7 @@ pub fn delete_job( )); // Free account - msgs.push(build_free_account_msg( + msgs.push(build_free_job_account_msg( config.account_tracker_address.to_string(), job.owner.to_string(), account_addr.to_string(), @@ -550,7 +550,7 @@ pub fn execute_job( )); // Free account - msgs.push(build_free_account_msg( + msgs.push(build_free_job_account_msg( config.account_tracker_address.to_string(), job.owner.to_string(), account_addr.to_string(), @@ -618,7 +618,7 @@ pub fn evict_job( )); // Free account - msgs.push(build_free_account_msg( + msgs.push(build_free_job_account_msg( config.account_tracker_address.to_string(), job.owner.to_string(), account_addr.to_string(), diff --git a/contracts/warp-controller/src/migrate/account.rs b/contracts/warp-controller/src/migrate/account.rs index 2f6ee7c1..8bbe4a11 100644 --- a/contracts/warp-controller/src/migrate/account.rs +++ b/contracts/warp-controller/src/migrate/account.rs @@ -1,10 +1,12 @@ use cosmwasm_std::{to_binary, Deps, Env, MessageInfo, Response, WasmMsg}; use crate::ContractError; -use account_tracker::{AccountsResponse, MigrateMsg, QueryFreeAccountsMsg, QueryTakenAccountsMsg}; +use account_tracker::{ + AccountsResponse, MigrateMsg, QueryFreeJobAccountsMsg, QueryTakenJobAccountsMsg, +}; use controller::{Config, MigrateAccountsMsg}; -pub fn migrate_free_accounts( +pub fn migrate_free_job_accounts( deps: Deps, _env: Env, info: MessageInfo, @@ -15,9 +17,9 @@ pub fn migrate_free_accounts( return Err(ContractError::Unauthorized {}); } - let free_accounts: AccountsResponse = deps.querier.query_wasm_smart( + let free_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( config.account_tracker_address, - &account_tracker::QueryMsg::QueryFreeAccounts(QueryFreeAccountsMsg { + &account_tracker::QueryMsg::QueryFreeJobAccounts(QueryFreeJobAccountsMsg { account_owner_addr: msg.account_owner_addr, start_after: msg.start_after, limit: Some(msg.limit as u32), @@ -25,7 +27,7 @@ pub fn migrate_free_accounts( )?; let mut migration_msgs = vec![]; - for account in free_accounts.accounts { + for account in free_job_accounts.accounts { migration_msgs.push(WasmMsg::Migrate { contract_addr: account.addr.to_string(), new_code_id: msg.warp_account_code_id.u64(), @@ -36,7 +38,7 @@ pub fn migrate_free_accounts( Ok(Response::new().add_messages(migration_msgs)) } -pub fn migrate_taken_accounts( +pub fn migrate_taken_job_accounts( deps: Deps, _env: Env, info: MessageInfo, @@ -47,9 +49,9 @@ pub fn migrate_taken_accounts( return Err(ContractError::Unauthorized {}); } - let taken_accounts: AccountsResponse = deps.querier.query_wasm_smart( + let taken_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( config.account_tracker_address, - &account_tracker::QueryMsg::QueryTakenAccounts(QueryTakenAccountsMsg { + &account_tracker::QueryMsg::QueryTakenJobAccounts(QueryTakenJobAccountsMsg { account_owner_addr: msg.account_owner_addr, start_after: msg.start_after, limit: Some(msg.limit as u32), @@ -57,7 +59,7 @@ pub fn migrate_taken_accounts( )?; let mut migration_msgs = vec![]; - for account in taken_accounts.accounts { + for account in taken_job_accounts.accounts { migration_msgs.push(WasmMsg::Migrate { contract_addr: account.addr.to_string(), new_code_id: msg.warp_account_code_id.u64(), diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index b5629c35..9a393dec 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -8,8 +8,9 @@ use controller::{ use crate::{ state::JobQueue, util::msg::{ - build_account_execute_warp_msgs, build_add_funding_account_msg, build_take_account_msg, - build_take_funding_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, + build_account_execute_warp_msgs, build_add_funding_account_msg, + build_take_funding_account_msg, build_take_job_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, }, ContractError, }; @@ -128,7 +129,7 @@ pub fn create_account_and_job( } // Take job account - msgs.push(build_take_account_msg( + msgs.push(build_take_job_account_msg( config.account_tracker_address.to_string(), job.owner.to_string(), account_addr.to_string(), diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 6bfe5675..53d7588b 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -9,7 +9,8 @@ use crate::{ state::{JobQueue, CONFIG, STATE}, util::msg::{ build_account_execute_generic_msgs, build_account_withdraw_assets_msg, - build_take_account_msg, build_take_funding_account_msg, build_transfer_native_funds_msg, + build_take_funding_account_msg, build_take_job_account_msg, + build_transfer_native_funds_msg, }, ContractError, }; @@ -243,7 +244,7 @@ pub fn execute_job( let funding_account_addr = finished_job.funding_account.clone().unwrap(); // Take job account with the new job - msgs.push(build_take_account_msg( + msgs.push(build_take_job_account_msg( config.account_tracker_address.to_string(), finished_job.owner.to_string(), account_addr.to_string(), diff --git a/contracts/warp-controller/src/util/fee.rs b/contracts/warp-controller/src/util/fee.rs index d507819f..fdfdb9f5 100644 --- a/contracts/warp-controller/src/util/fee.rs +++ b/contracts/warp-controller/src/util/fee.rs @@ -13,5 +13,9 @@ pub fn deduct_from_native_funds( deducted_amount = Uint128::zero(); } } + + // Filter out coins with an amount of zero + funds.retain(|coin| !coin.amount.is_zero()); + funds } diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 930bf30c..526faa45 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -1,8 +1,8 @@ use cosmwasm_std::{to_binary, BankMsg, Coin, CosmosMsg, Uint128, Uint64, WasmMsg}; use account_tracker::{ - AddFundingAccountMsg, FreeAccountMsg, FreeFundingAccountMsg, TakeAccountMsg, - TakeFundingAccountMsg, + AddFundingAccountMsg, FreeFundingAccountMsg, FreeJobAccountMsg, TakeFundingAccountMsg, + TakeJobAccountMsg, }; use controller::account::{ AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, WarpMsg, WarpMsgs, @@ -54,7 +54,7 @@ pub fn build_instantiate_warp_account_msg( }) } -pub fn build_free_account_msg( +pub fn build_free_job_account_msg( account_tracker_addr: String, account_owner_addr: String, account_addr: String, @@ -62,17 +62,19 @@ pub fn build_free_account_msg( ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: account_tracker_addr, - msg: to_binary(&account_tracker::ExecuteMsg::FreeAccount(FreeAccountMsg { - account_owner_addr, - account_addr, - last_job_id, - })) + msg: to_binary(&account_tracker::ExecuteMsg::FreeJobAccount( + FreeJobAccountMsg { + account_owner_addr, + account_addr, + last_job_id, + }, + )) .unwrap(), funds: vec![], }) } -pub fn build_take_account_msg( +pub fn build_take_job_account_msg( account_tracker_addr: String, account_owner_addr: String, account_addr: String, @@ -80,11 +82,13 @@ pub fn build_take_account_msg( ) -> CosmosMsg { CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: account_tracker_addr, - msg: to_binary(&account_tracker::ExecuteMsg::TakeAccount(TakeAccountMsg { - account_owner_addr, - account_addr, - job_id, - })) + msg: to_binary(&account_tracker::ExecuteMsg::TakeJobAccount( + TakeJobAccountMsg { + account_owner_addr, + account_addr, + job_id, + }, + )) .unwrap(), funds: vec![], }) diff --git a/packages/account-tracker/src/lib.rs b/packages/account-tracker/src/lib.rs index d9ec4b01..85d14755 100644 --- a/packages/account-tracker/src/lib.rs +++ b/packages/account-tracker/src/lib.rs @@ -17,22 +17,22 @@ pub struct InstantiateMsg { #[cw_serde] #[allow(clippy::large_enum_variant)] pub enum ExecuteMsg { - TakeAccount(TakeAccountMsg), - FreeAccount(FreeAccountMsg), + TakeJobAccount(TakeJobAccountMsg), + FreeJobAccount(FreeJobAccountMsg), TakeFundingAccount(TakeFundingAccountMsg), FreeFundingAccount(FreeFundingAccountMsg), AddFundingAccount(AddFundingAccountMsg), } #[cw_serde] -pub struct TakeAccountMsg { +pub struct TakeJobAccountMsg { pub account_owner_addr: String, pub account_addr: String, pub job_id: Uint64, } #[cw_serde] -pub struct FreeAccountMsg { +pub struct FreeJobAccountMsg { pub account_owner_addr: String, pub account_addr: String, pub last_job_id: Uint64, @@ -64,11 +64,11 @@ pub enum QueryMsg { #[returns(ConfigResponse)] QueryConfig(QueryConfigMsg), #[returns(AccountsResponse)] - QueryTakenAccounts(QueryTakenAccountsMsg), + QueryTakenJobAccounts(QueryTakenJobAccountsMsg), #[returns(AccountsResponse)] - QueryFreeAccounts(QueryFreeAccountsMsg), + QueryFreeJobAccounts(QueryFreeJobAccountsMsg), #[returns(AccountResponse)] - QueryFirstFreeAccount(QueryFirstFreeAccountMsg), + QueryFirstFreeJobAccount(QueryFirstFreeJobAccountMsg), #[returns(FundingAccountResponse)] QueryFirstFreeFundingAccount(QueryFirstFreeFundingAccountMsg), #[returns(FundingAccountsResponse)] @@ -86,14 +86,14 @@ pub struct ConfigResponse { } #[cw_serde] -pub struct QueryTakenAccountsMsg { +pub struct QueryTakenJobAccountsMsg { pub account_owner_addr: String, pub start_after: Option, pub limit: Option, } #[cw_serde] -pub struct QueryFreeAccountsMsg { +pub struct QueryFreeJobAccountsMsg { pub account_owner_addr: String, pub start_after: Option, pub limit: Option, @@ -118,12 +118,12 @@ pub struct AccountsResponse { } #[cw_serde] -pub struct QueryFirstFreeAccountMsg { +pub struct QueryFirstFreeJobAccountMsg { pub account_owner_addr: String, } #[cw_serde] -pub struct QueryFreeAccountMsg { +pub struct QueryFreeJobAccountMsg { pub account_addr: String, } diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index a6abef5e..94651907 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -97,8 +97,8 @@ pub enum ExecuteMsg { UpdateConfig(UpdateConfigMsg), - MigrateFreeAccounts(MigrateAccountsMsg), - MigrateTakenAccounts(MigrateAccountsMsg), + MigrateFreeJobAccounts(MigrateAccountsMsg), + MigrateTakenJobAccounts(MigrateAccountsMsg), MigratePendingJobs(MigrateJobsMsg), MigrateFinishedJobs(MigrateJobsMsg), diff --git a/refs.json b/refs.json index 29f103eb..6fe56253 100644 --- a/refs.json +++ b/refs.json @@ -12,11 +12,11 @@ "codeId": "12318" }, "warp-controller": { - "codeId": "12339", + "codeId": "12369", "address": "terra1sylnrt9rkv7lraqvxssdzwmhm804qjtlp8ssjc502538ykyhdn5sns9edd" }, "warp-resolver": { - "codeId": "12319", + "codeId": "12368", "address": "terra1yr249ds5f24u72cnyspdu0vghlkxyfanjj5kyagx9q4fm6nlsads89f9l7" }, "warp-templates": { From cbc5ae9a15fdd9d26c3ffd63742f30ff8c84c394 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 12 Dec 2023 22:51:34 +0100 Subject: [PATCH 113/133] redesign accounts-tracker for better handling of accounts data and storage --- .../examples/warp-account-tracker-schema.rs | 7 +- .../warp-account-tracker/src/contract.rs | 23 +- contracts/warp-account-tracker/src/error.rs | 3 + .../src/execute/account.rs | 238 ++++++------ .../src/integration_tests.rs | 156 ++++---- .../warp-account-tracker/src/query/account.rs | 357 ++++++++++++------ contracts/warp-account-tracker/src/state.rs | 21 +- contracts/warp-controller/src/contract.rs | 16 +- .../warp-controller/src/execute/account.rs | 32 +- contracts/warp-controller/src/execute/job.rs | 8 +- .../warp-controller/src/migrate/account.rs | 46 +-- .../warp-controller/src/reply/account.rs | 66 +--- contracts/warp-controller/src/util/msg.rs | 21 +- packages/account-tracker/src/lib.rs | 89 +++-- packages/controller/src/lib.rs | 3 +- refs.json | 4 +- tasks/deploy_warp.ts | 4 +- tasks/migrate_warp.ts | 3 +- 18 files changed, 566 insertions(+), 531 deletions(-) diff --git a/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs b/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs index f0054f49..a5558f03 100644 --- a/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs +++ b/contracts/warp-account-tracker/examples/warp-account-tracker-schema.rs @@ -2,8 +2,8 @@ use std::env::current_dir; use std::fs::create_dir_all; use account_tracker::{ - Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, - FundingAccountResponse, FundingAccountsResponse, InstantiateMsg, QueryMsg, + Account, AccountsResponse, Config, ConfigResponse, ExecuteMsg, FundingAccountResponse, + FundingAccountsResponse, InstantiateMsg, JobAccountResponse, JobAccountsResponse, QueryMsg, }; use cosmwasm_schema::{export_schema, remove_schemas, schema_for}; @@ -18,9 +18,10 @@ fn main() { export_schema(&schema_for!(QueryMsg), &out_dir); export_schema(&schema_for!(Config), &out_dir); export_schema(&schema_for!(AccountsResponse), &out_dir); - export_schema(&schema_for!(AccountResponse), &out_dir); export_schema(&schema_for!(FundingAccountResponse), &out_dir); export_schema(&schema_for!(FundingAccountsResponse), &out_dir); + export_schema(&schema_for!(JobAccountResponse), &out_dir); + export_schema(&schema_for!(JobAccountsResponse), &out_dir); export_schema(&schema_for!(ConfigResponse), &out_dir); export_schema(&schema_for!(Account), &out_dir); } diff --git a/contracts/warp-account-tracker/src/contract.rs b/contracts/warp-account-tracker/src/contract.rs index 0896d423..ea689168 100644 --- a/contracts/warp-account-tracker/src/contract.rs +++ b/contracts/warp-account-tracker/src/contract.rs @@ -60,10 +60,6 @@ pub fn execute( nonpayable(&info).unwrap(); execute::account::free_funding_account(deps, data) } - ExecuteMsg::AddFundingAccount(data) => { - nonpayable(&info).unwrap(); - execute::account::add_funding_account(deps, data) - } } } @@ -71,15 +67,7 @@ pub fn execute( pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { match msg { QueryMsg::QueryConfig(_) => to_binary(&query::account::query_config(deps)?), - QueryMsg::QueryTakenJobAccounts(data) => { - to_binary(&query::account::query_taken_job_accounts(deps, data)?) - } - QueryMsg::QueryFreeJobAccounts(data) => { - to_binary(&query::account::query_free_job_accounts(deps, data)?) - } - QueryMsg::QueryFirstFreeJobAccount(data) => { - to_binary(&query::account::query_first_free_job_account(deps, data)?) - } + QueryMsg::QueryAccounts(data) => to_binary(&query::account::query_accounts(deps, data)?), QueryMsg::QueryFundingAccounts(data) => { to_binary(&query::account::query_funding_accounts(deps, data)?) } @@ -89,6 +77,15 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { QueryMsg::QueryFirstFreeFundingAccount(data) => to_binary( &query::account::query_first_free_funding_account(deps, data)?, ), + QueryMsg::QueryJobAccounts(data) => { + to_binary(&query::account::query_job_accounts(deps, data)?) + } + QueryMsg::QueryJobAccount(data) => { + to_binary(&query::account::query_job_account(deps, data)?) + } + QueryMsg::QueryFirstFreeJobAccount(data) => { + to_binary(&query::account::query_first_free_job_account(deps, data)?) + } } } diff --git a/contracts/warp-account-tracker/src/error.rs b/contracts/warp-account-tracker/src/error.rs index 65fa0305..fe78cc67 100644 --- a/contracts/warp-account-tracker/src/error.rs +++ b/contracts/warp-account-tracker/src/error.rs @@ -49,6 +49,9 @@ pub enum ContractError { #[error("Account not found")] AccountNotFound {}, + + #[error("Invalid account type")] + InvalidAccountType {}, } impl From for ContractError { diff --git a/contracts/warp-account-tracker/src/execute/account.rs b/contracts/warp-account-tracker/src/execute/account.rs index c07e2c56..1d025cad 100644 --- a/contracts/warp-account-tracker/src/execute/account.rs +++ b/contracts/warp-account-tracker/src/execute/account.rs @@ -1,16 +1,37 @@ use crate::state::{ - FREE_JOB_ACCOUNTS, FUNDING_ACCOUNTS_BY_USER, TAKEN_FUNDING_ACCOUNT_BY_JOB, TAKEN_JOB_ACCOUNTS, + ACCOUNTS, FREE_FUNDING_ACCOUNTS, FREE_JOB_ACCOUNTS, TAKEN_FUNDING_ACCOUNTS, TAKEN_JOB_ACCOUNTS, }; use crate::ContractError; use account_tracker::{ - AddFundingAccountMsg, FreeFundingAccountMsg, FreeJobAccountMsg, FundingAccount, - TakeFundingAccountMsg, TakeJobAccountMsg, + Account, AccountType, FreeFundingAccountMsg, FreeJobAccountMsg, TakeFundingAccountMsg, + TakeJobAccountMsg, }; -use cosmwasm_std::{DepsMut, Response}; +use cosmwasm_std::{DepsMut, Response, Uint64}; pub fn take_job_account(deps: DepsMut, data: TakeJobAccountMsg) -> Result { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + + // Attempt to load the account; if it doesn't exist, create a new one + let account = ACCOUNTS.update( + deps.storage, + (account_owner_ref, account_addr_ref), + |s| -> Result { + match s { + Some(account) => Ok(account), + None => Ok(Account { + account_type: AccountType::Job, + owner_addr: account_owner_ref.clone(), + account_addr: account_addr_ref.clone(), + }), + } + }, + )?; + + if account.account_type != AccountType::Job { + return Err(ContractError::InvalidAccountType {}); + } + FREE_JOB_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); TAKEN_JOB_ACCOUNTS.update( deps.storage, @@ -20,15 +41,37 @@ pub fn take_job_account(deps: DepsMut, data: TakeJobAccountMsg) -> Result Err(ContractError::AccountAlreadyTakenError {}), }, )?; + Ok(Response::new() .add_attribute("action", "take_job_account") .add_attribute("account_addr", data.account_addr) - .add_attribute("job_id", data.job_id)) + .add_attribute("job_id", data.job_id.to_string())) } pub fn free_job_account(deps: DepsMut, data: FreeJobAccountMsg) -> Result { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + + // Attempt to load the account; if it doesn't exist, create a new one + let account = ACCOUNTS.update( + deps.storage, + (account_owner_ref, account_addr_ref), + |s| -> Result { + match s { + Some(account) => Ok(account), + None => Ok(Account { + account_type: AccountType::Job, + owner_addr: account_owner_ref.clone(), + account_addr: account_addr_ref.clone(), + }), + } + }, + )?; + + if account.account_type != AccountType::Job { + return Err(ContractError::InvalidAccountType {}); + } + TAKEN_JOB_ACCOUNTS.remove(deps.storage, (account_owner_ref, account_addr_ref)); FREE_JOB_ACCOUNTS.update( deps.storage, @@ -39,6 +82,7 @@ pub fn free_job_account(deps: DepsMut, data: FreeJobAccountMsg) -> Result Err(ContractError::AccountAlreadyFreeError {}), }, )?; + Ok(Response::new() .add_attribute("action", "free_job_account") .add_attribute("account_addr", data.account_addr)) @@ -48,51 +92,40 @@ pub fn take_funding_account( deps: DepsMut, data: TakeFundingAccountMsg, ) -> Result { - let account_owner_addr_ref = deps.api.addr_validate(&data.account_owner_addr)?; + let account_owner_addr_ref = &deps.api.addr_validate(&data.account_owner_addr)?; let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; - // prevent taking job accounts as funding accounts - if TAKEN_JOB_ACCOUNTS.has(deps.storage, (&account_owner_addr_ref, account_addr_ref)) - || FREE_JOB_ACCOUNTS.has(deps.storage, (&account_owner_addr_ref, account_addr_ref)) - { - return Err(ContractError::AccountAlreadyTakenError {}); - } + // Attempt to load the account; if it doesn't exist, create a new one + let account = ACCOUNTS.update( + deps.storage, + (account_owner_addr_ref, account_addr_ref), + |s| -> Result { + match s { + Some(account) => Ok(account), + None => Ok(Account { + account_type: AccountType::Job, + owner_addr: account_owner_addr_ref.clone(), + account_addr: account_addr_ref.clone(), + }), + } + }, + )?; - TAKEN_FUNDING_ACCOUNT_BY_JOB.update(deps.storage, data.job_id.u64(), |s| match s { - // value is a dummy data because there is no built in support for set in cosmwasm - None => Ok(account_addr_ref.clone()), - Some(_) => Err(ContractError::AccountAlreadyTakenError {}), - })?; + if account.account_type != AccountType::Funding { + return Err(ContractError::InvalidAccountType {}); + } - FUNDING_ACCOUNTS_BY_USER.update( + FREE_FUNDING_ACCOUNTS.remove(deps.storage, (account_owner_addr_ref, account_addr_ref)); + TAKEN_FUNDING_ACCOUNTS.update( deps.storage, - &account_owner_addr_ref, - |accounts_opt| -> Result, ContractError> { - match accounts_opt { - None => { - // No funding accounts exist for this user, create a new vec - Ok(vec![FundingAccount { - account_addr: account_addr_ref.clone(), - taken_by_job_ids: vec![data.job_id], - }]) - } - Some(mut accounts) => { - // Check if a funding account with the specified address already exists - if let Some(funding_account) = accounts - .iter_mut() - .find(|acc| acc.account_addr == account_addr_ref.clone()) - { - // Funding account exists, update its job_ids - funding_account.taken_by_job_ids.push(data.job_id); - } else { - // Funding account does not exist, add a new one - accounts.push(FundingAccount { - account_addr: account_addr_ref.clone(), - taken_by_job_ids: vec![data.job_id], - }); - } - Ok(accounts) + (account_owner_addr_ref, account_addr_ref), + |ids| -> Result, ContractError> { + match ids { + Some(mut id_list) => { + id_list.push(data.job_id); + Ok(id_list) } + None => Ok(vec![data.job_id]), } }, )?; @@ -107,87 +140,58 @@ pub fn free_funding_account( deps: DepsMut, data: FreeFundingAccountMsg, ) -> Result { - let account_owner_addr_ref = deps.api.addr_validate(&data.account_owner_addr)?; - let account_addr_ref = deps.api.addr_validate(&data.account_addr)?; + let account_owner_addr_ref = &deps.api.addr_validate(&data.account_owner_addr)?; + let account_addr_ref = &deps.api.addr_validate(&data.account_addr)?; - TAKEN_FUNDING_ACCOUNT_BY_JOB.remove(deps.storage, data.job_id.u64()); - - FUNDING_ACCOUNTS_BY_USER.update( + // Attempt to load the account; if it doesn't exist, create a new one + let account = ACCOUNTS.update( deps.storage, - &account_owner_addr_ref, - |accounts_opt| -> Result, ContractError> { - match accounts_opt { - Some(mut accounts) => { - // Find the funding account with the specified address - if let Some(funding_account) = accounts - .iter_mut() - .find(|acc| acc.account_addr == account_addr_ref) - { - // Remove the specified job ID - funding_account - .taken_by_job_ids - .retain(|&id| id != data.job_id); - - Ok(accounts) - } else { - Err(ContractError::AccountNotFound {}) - } - } - None => Err(ContractError::AccountNotFound {}), + (account_owner_addr_ref, account_addr_ref), + |s| -> Result { + match s { + Some(account) => Ok(account), + None => Ok(Account { + account_type: AccountType::Job, + owner_addr: account_owner_addr_ref.clone(), + account_addr: account_addr_ref.clone(), + }), } }, )?; - Ok(Response::new() - .add_attribute("action", "free_funding_account") - .add_attribute("account_addr", data.account_addr) - .add_attribute("job_id", data.job_id.to_string())) -} - -pub fn add_funding_account( - deps: DepsMut, - data: AddFundingAccountMsg, -) -> Result { - let account_owner_addr_ref = deps.api.addr_validate(&data.account_owner_addr)?; - let account_addr_ref = deps.api.addr_validate(&data.account_addr)?; - - // prevent adding job accounts as funding accounts - if TAKEN_JOB_ACCOUNTS.has(deps.storage, (&account_owner_addr_ref, &account_addr_ref)) - || FREE_JOB_ACCOUNTS.has(deps.storage, (&account_owner_addr_ref, &account_addr_ref)) - { - return Err(ContractError::AccountAlreadyTakenError {}); + if account.account_type != AccountType::Funding { + return Err(ContractError::InvalidAccountType {}); } - FUNDING_ACCOUNTS_BY_USER.update( - deps.storage, - &account_owner_addr_ref, - |accounts_opt| -> Result, ContractError> { - match accounts_opt { - Some(mut accounts) => { - if accounts - .iter_mut() - .any(|acc| acc.account_addr == account_addr_ref.clone()) - { - // account already exists, do nothing - Ok(accounts) - } else { - accounts.push(FundingAccount { - account_addr: account_addr_ref.clone(), - taken_by_job_ids: vec![], - }); - - Ok(accounts) - } - } - None => Ok(vec![FundingAccount { - account_addr: account_addr_ref.clone(), - taken_by_job_ids: vec![], - }]), - } - }, - )?; + // Retrieve current job IDs for the funding account + let mut job_ids = + TAKEN_FUNDING_ACCOUNTS.load(deps.storage, (account_owner_addr_ref, account_addr_ref))?; + + // Remove the specified job ID + job_ids.retain(|&id| id != data.job_id); + + // Update or remove the entry in TAKEN_FUNDING_ACCOUNTS based on the updated list + if job_ids.is_empty() { + TAKEN_FUNDING_ACCOUNTS.remove(deps.storage, (account_owner_addr_ref, account_addr_ref)); + FREE_FUNDING_ACCOUNTS.update( + deps.storage, + (account_owner_addr_ref, account_addr_ref), + |s| match s { + None => Ok(vec![data.job_id]), + Some(_) => Err(ContractError::AccountAlreadyFreeError {}), + }, + )?; + } else { + // Update the entry in TAKEN_FUNDING_ACCOUNTS + TAKEN_FUNDING_ACCOUNTS.save( + deps.storage, + (account_owner_addr_ref, account_addr_ref), + &job_ids, + )?; + } Ok(Response::new() - .add_attribute("action", "add_funding_account") - .add_attribute("account_addr", data.account_addr)) + .add_attribute("action", "free_funding_account") + .add_attribute("account_addr", data.account_addr) + .add_attribute("job_id", data.job_id.to_string())) } diff --git a/contracts/warp-account-tracker/src/integration_tests.rs b/contracts/warp-account-tracker/src/integration_tests.rs index c170e29f..90faf8a3 100644 --- a/contracts/warp-account-tracker/src/integration_tests.rs +++ b/contracts/warp-account-tracker/src/integration_tests.rs @@ -1,9 +1,9 @@ #[cfg(test)] mod tests { use account_tracker::{ - Account, AccountResponse, AccountsResponse, Config, ConfigResponse, ExecuteMsg, - FreeJobAccountMsg, InstantiateMsg, QueryConfigMsg, QueryFirstFreeJobAccountMsg, - QueryFreeJobAccountsMsg, QueryMsg, QueryTakenJobAccountsMsg, TakeJobAccountMsg, + AccountStatus, Config, ConfigResponse, ExecuteMsg, FreeJobAccountMsg, InstantiateMsg, + JobAccount, JobAccountResponse, JobAccountsResponse, QueryConfigMsg, + QueryFirstFreeJobAccountMsg, QueryJobAccountsMsg, QueryMsg, TakeJobAccountMsg, }; use anyhow::Result as AnyResult; use cosmwasm_std::{Addr, Coin, Empty, Uint128, Uint64}; @@ -99,33 +99,35 @@ mod tests { account_owner_addr: USER_1.to_string(), }) ), - Ok(AccountResponse { account: None }) + Ok(JobAccountResponse { job_account: None }) ); assert_eq!( app.wrap().query_wasm_smart( warp_account_tracker_contract_addr.clone(), - &QueryMsg::QueryFreeJobAccounts(QueryFreeJobAccountsMsg { + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, - limit: None + limit: None, + account_status: AccountStatus::Free }) ), - Ok(AccountsResponse { - accounts: vec![], + Ok(JobAccountsResponse { + job_accounts: vec![], total_count: 0 }) ); assert_eq!( app.wrap().query_wasm_smart( warp_account_tracker_contract_addr.clone(), - &QueryMsg::QueryTakenJobAccounts(QueryTakenJobAccountsMsg { + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, - limit: None + limit: None, + account_status: AccountStatus::Taken }) ), - Ok(AccountsResponse { - accounts: vec![], + Ok(JobAccountsResponse { + job_accounts: vec![], total_count: 0 }) ); @@ -189,10 +191,11 @@ mod tests { account_owner_addr: USER_1.to_string(), }) ), - Ok(AccountResponse { - account: Some(Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - taken_by_job_id: Some(DUMMY_JOB_1_ID) + Ok(JobAccountResponse { + job_account: Some(JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free }) }) ); @@ -201,26 +204,30 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_account_tracker_contract_addr.clone(), - &QueryMsg::QueryFreeJobAccounts(QueryFreeJobAccountsMsg { + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, - limit: None + limit: None, + account_status: AccountStatus::Free }) ), - Ok(AccountsResponse { - accounts: vec![ - Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), - taken_by_job_id: Some(DUMMY_JOB_1_ID) + Ok(JobAccountsResponse { + job_accounts: vec![ + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free }, - Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), - taken_by_job_id: Some(DUMMY_JOB_2_ID) + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), + taken_by_job_id: DUMMY_JOB_2_ID, + account_status: AccountStatus::Free + }, + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free }, - Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - taken_by_job_id: Some(DUMMY_JOB_1_ID) - } ], total_count: 3 }) @@ -230,14 +237,15 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_account_tracker_contract_addr.clone(), - &QueryMsg::QueryTakenJobAccounts(QueryTakenJobAccountsMsg { + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, - limit: None + limit: None, + account_status: AccountStatus::Taken }) ), - Ok(AccountsResponse { - accounts: vec![], + Ok(JobAccountsResponse { + job_accounts: vec![], total_count: 0 }) ); @@ -273,22 +281,25 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_account_tracker_contract_addr.clone(), - &QueryMsg::QueryFreeJobAccounts(QueryFreeJobAccountsMsg { + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, - limit: None + limit: None, + account_status: AccountStatus::Free }) ), - Ok(AccountsResponse { - accounts: vec![ - Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), - taken_by_job_id: Some(DUMMY_JOB_1_ID) + Ok(JobAccountsResponse { + job_accounts: vec![ + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }, + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free }, - Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - taken_by_job_id: Some(DUMMY_JOB_1_ID) - } ], total_count: 2 }) @@ -298,16 +309,18 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_account_tracker_contract_addr.clone(), - &QueryMsg::QueryTakenJobAccounts(QueryTakenJobAccountsMsg { + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, - limit: None + limit: None, + account_status: AccountStatus::Taken }) ), - Ok(AccountsResponse { - accounts: vec![Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), - taken_by_job_id: Some(DUMMY_JOB_1_ID) + Ok(JobAccountsResponse { + job_accounts: vec![JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Taken },], total_count: 1 }) @@ -329,26 +342,30 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_account_tracker_contract_addr.clone(), - &QueryMsg::QueryFreeJobAccounts(QueryFreeJobAccountsMsg { + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, - limit: None + limit: None, + account_status: AccountStatus::Free }) ), - Ok(AccountsResponse { - accounts: vec![ - Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), - taken_by_job_id: Some(DUMMY_JOB_1_ID) + Ok(JobAccountsResponse { + job_accounts: vec![ + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free + }, + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free }, - Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_2_ADDR), - taken_by_job_id: Some(DUMMY_JOB_1_ID) + JobAccount { + account_addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_3_ADDR), + taken_by_job_id: DUMMY_JOB_1_ID, + account_status: AccountStatus::Free }, - Account { - addr: Addr::unchecked(DUMMY_WARP_ACCOUNT_1_ADDR), - taken_by_job_id: Some(DUMMY_JOB_1_ID) - } ], total_count: 3 }) @@ -358,14 +375,15 @@ mod tests { assert_eq!( app.wrap().query_wasm_smart( warp_account_tracker_contract_addr, - &QueryMsg::QueryTakenJobAccounts(QueryTakenJobAccountsMsg { + &QueryMsg::QueryJobAccounts(QueryJobAccountsMsg { account_owner_addr: USER_1.to_string(), start_after: None, - limit: None + limit: None, + account_status: AccountStatus::Taken }) ), - Ok(AccountsResponse { - accounts: vec![], + Ok(JobAccountsResponse { + job_accounts: vec![], total_count: 0 }) ); diff --git a/contracts/warp-account-tracker/src/query/account.rs b/contracts/warp-account-tracker/src/query/account.rs index 1e8839fd..94f19269 100644 --- a/contracts/warp-account-tracker/src/query/account.rs +++ b/contracts/warp-account-tracker/src/query/account.rs @@ -1,13 +1,17 @@ use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::{Bound, PrefixBound}; -use crate::state::{CONFIG, FREE_JOB_ACCOUNTS, FUNDING_ACCOUNTS_BY_USER, TAKEN_JOB_ACCOUNTS}; +use crate::state::{ + ACCOUNTS, CONFIG, FREE_FUNDING_ACCOUNTS, FREE_JOB_ACCOUNTS, TAKEN_FUNDING_ACCOUNTS, + TAKEN_JOB_ACCOUNTS, +}; use account_tracker::{ - Account, AccountResponse, AccountsResponse, ConfigResponse, FundingAccountResponse, - FundingAccountsResponse, QueryFirstFreeFundingAccountMsg, QueryFirstFreeJobAccountMsg, - QueryFreeJobAccountsMsg, QueryFundingAccountMsg, QueryFundingAccountsMsg, - QueryTakenJobAccountsMsg, + Account, AccountStatus, AccountsResponse, ConfigResponse, FundingAccount, + FundingAccountResponse, FundingAccountsResponse, JobAccount, JobAccountResponse, + JobAccountsResponse, QueryAccountsMsg, QueryFirstFreeFundingAccountMsg, + QueryFirstFreeJobAccountMsg, QueryFundingAccountMsg, QueryFundingAccountsMsg, + QueryJobAccountMsg, QueryJobAccountsMsg, }; const QUERY_LIMIT: u32 = 50; @@ -17,161 +21,268 @@ pub fn query_config(deps: Deps) -> StdResult { Ok(ConfigResponse { config }) } -pub fn query_first_free_job_account( - deps: Deps, - data: QueryFirstFreeJobAccountMsg, -) -> StdResult { +pub fn query_accounts(deps: Deps, data: QueryAccountsMsg) -> StdResult { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; - let maybe_free_job_account = FREE_JOB_ACCOUNTS - .prefix_range( + let start_after = data + .start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let iter = match start_after { + Some(start_after_addr) => ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive((account_owner_ref, &start_after_addr))), + None, + Order::Ascending, + ), + None => ACCOUNTS.prefix_range( deps.storage, Some(PrefixBound::inclusive(account_owner_ref)), Some(PrefixBound::inclusive(account_owner_ref)), Order::Ascending, - ) - .next(); - let free_job_account = match maybe_free_job_account { - Some(Ok((account, last_job_id))) => Some(Account { - addr: account.1, - taken_by_job_id: Some(last_job_id), + ), + }; + + let accounts = iter + .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) + .map(|item| item.map(|(_, account)| account)) + .collect::>>()?; + + Ok(AccountsResponse { accounts }) +} + +pub fn query_funding_account( + deps: Deps, + data: QueryFundingAccountMsg, +) -> StdResult { + let account_owner_addr_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; + + let funding_account = match TAKEN_FUNDING_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_ids)) => Some(FundingAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_ids: job_ids, + account_status: AccountStatus::Taken, }), - _ => None, + Ok(None) => { + match FREE_FUNDING_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_ids)) => Some(FundingAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_ids: job_ids, + account_status: AccountStatus::Free, + }), + Ok(None) => None, + Err(err) => return Err(err), + } + } + Err(err) => return Err(err), }; - Ok(AccountResponse { - account: free_job_account, + + Ok(FundingAccountResponse { funding_account }) +} + +pub fn query_first_free_funding_account( + deps: Deps, + data: QueryFirstFreeFundingAccountMsg, +) -> StdResult { + let resp = query_funding_accounts( + deps, + QueryFundingAccountsMsg { + account_owner_addr: data.account_owner_addr, + account_status: AccountStatus::Free, + start_after: None, + limit: Some(1), + }, + )?; + + Ok(FundingAccountResponse { + funding_account: resp.funding_accounts.first().cloned(), }) } -pub fn query_taken_job_accounts( +pub fn query_funding_accounts( deps: Deps, - data: QueryTakenJobAccountsMsg, -) -> StdResult { + data: QueryFundingAccountsMsg, +) -> StdResult { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; - let iter = match data.start_after { - Some(start_after) => { - let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; - TAKEN_JOB_ACCOUNTS.range( + let status = data.account_status; + + let iter = match status { + AccountStatus::Free => match data.start_after { + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + FREE_FUNDING_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Ascending, + ) + } + None => FREE_FUNDING_ACCOUNTS.prefix_range( deps.storage, - Some(Bound::exclusive(( - account_owner_ref, - start_after_account_addr, - ))), - None, - Order::Descending, - ) - } - None => TAKEN_JOB_ACCOUNTS.prefix_range( - deps.storage, - Some(PrefixBound::inclusive(account_owner_ref)), - Some(PrefixBound::inclusive(account_owner_ref)), - Order::Descending, - ), + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ), + }, + AccountStatus::Taken => match data.start_after { + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + TAKEN_FUNDING_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Ascending, + ) + } + None => TAKEN_FUNDING_ACCOUNTS.prefix_range( + deps.storage, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ), + }, }; - let accounts = iter + + let funding_accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { - item.map(|(account, job_id)| Account { - addr: account.1, - taken_by_job_id: Some(job_id), + item.map(|(account, job_ids)| FundingAccount { + account_addr: account.1, + taken_by_job_ids: job_ids, + account_status: status.clone(), }) }) - .collect::>>()?; - Ok(AccountsResponse { - total_count: accounts.len() as u32, - accounts, + .collect::>>()?; + + Ok(FundingAccountsResponse { + funding_accounts: funding_accounts.clone(), + total_count: funding_accounts.len() as u32, }) } -pub fn query_free_job_accounts( - deps: Deps, - data: QueryFreeJobAccountsMsg, -) -> StdResult { +pub fn query_job_accounts(deps: Deps, data: QueryJobAccountsMsg) -> StdResult { let account_owner_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; - let iter = match data.start_after { - Some(start_after) => { - let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; - FREE_JOB_ACCOUNTS.range( + let status = data.account_status; + + let iter = match status { + AccountStatus::Free => match data.start_after { + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + + FREE_JOB_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Ascending, + ) + } + None => FREE_JOB_ACCOUNTS.prefix_range( deps.storage, - Some(Bound::exclusive(( - account_owner_ref, - start_after_account_addr, - ))), - None, - Order::Descending, - ) - } - None => FREE_JOB_ACCOUNTS.prefix_range( - deps.storage, - Some(PrefixBound::inclusive(account_owner_ref)), - Some(PrefixBound::inclusive(account_owner_ref)), - Order::Descending, - ), + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ), + }, + AccountStatus::Taken => match data.start_after { + Some(start_after) => { + let start_after_account_addr = &deps.api.addr_validate(start_after.as_str())?; + + TAKEN_JOB_ACCOUNTS.range( + deps.storage, + Some(Bound::exclusive(( + account_owner_ref, + start_after_account_addr, + ))), + None, + Order::Ascending, + ) + } + None => TAKEN_JOB_ACCOUNTS.prefix_range( + deps.storage, + Some(PrefixBound::inclusive(account_owner_ref)), + Some(PrefixBound::inclusive(account_owner_ref)), + Order::Ascending, + ), + }, }; - let accounts = iter + + let job_accounts = iter .take(data.limit.unwrap_or(QUERY_LIMIT) as usize) .map(|item| { - item.map(|(account, last_job_id)| Account { - addr: account.1, - taken_by_job_id: Some(last_job_id), + item.map(|(account, job_id)| JobAccount { + account_addr: account.1, + taken_by_job_id: job_id, + account_status: status.clone(), }) }) - .collect::>>()?; - Ok(AccountsResponse { - total_count: accounts.len() as u32, - accounts, - }) -} - -// funding accounts + .collect::>>()?; -pub fn query_funding_account( - deps: Deps, - data: QueryFundingAccountMsg, -) -> StdResult { - let account_addr_ref = deps.api.addr_validate(data.account_addr.as_str())?; - let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; - - let funding_accounts = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref)?; - - Ok(FundingAccountResponse { - funding_account: funding_accounts - .iter() - .find(|fa| fa.account_addr == account_addr_ref.clone()) - .cloned(), + Ok(JobAccountsResponse { + job_accounts: job_accounts.clone(), + total_count: job_accounts.len() as u32, }) } -pub fn query_funding_accounts( - deps: Deps, - data: QueryFundingAccountsMsg, -) -> StdResult { - let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; - - let resp = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref); +pub fn query_job_account(deps: Deps, data: QueryJobAccountMsg) -> StdResult { + let account_owner_addr_ref = &deps.api.addr_validate(data.account_owner_addr.as_str())?; + let account_addr_ref = &deps.api.addr_validate(data.account_addr.as_str())?; - let funding_accounts = match resp { - Ok(funding_accounts) => funding_accounts, - Err(_) => vec![], + let job_account = match TAKEN_JOB_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_id)) => Some(JobAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_id: job_id, + account_status: AccountStatus::Taken, + }), + Ok(None) => { + match FREE_JOB_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_id)) => Some(JobAccount { + account_addr: account_addr_ref.clone(), + taken_by_job_id: job_id, + account_status: AccountStatus::Free, + }), + Ok(None) => None, + Err(err) => return Err(err), + } + } + Err(err) => return Err(err), }; - Ok(FundingAccountsResponse { funding_accounts }) + Ok(JobAccountResponse { job_account }) } -pub fn query_first_free_funding_account( +pub fn query_first_free_job_account( deps: Deps, - data: QueryFirstFreeFundingAccountMsg, -) -> StdResult { - let account_owner_addr_ref = deps.api.addr_validate(data.account_owner_addr.as_str())?; - - let resp = FUNDING_ACCOUNTS_BY_USER.load(deps.storage, &account_owner_addr_ref); - - let funding_account = match resp { - Ok(funding_accounts) => funding_accounts - .iter() - .find(|fa| fa.taken_by_job_ids.is_empty()) - .cloned(), - Err(_) => None, - }; + data: QueryFirstFreeJobAccountMsg, +) -> StdResult { + let resp = query_job_accounts( + deps, + QueryJobAccountsMsg { + account_owner_addr: data.account_owner_addr, + account_status: AccountStatus::Free, + start_after: None, + limit: Some(1), + }, + )?; - Ok(FundingAccountResponse { funding_account }) + Ok(JobAccountResponse { + job_account: resp.job_accounts.first().cloned(), + }) } diff --git a/contracts/warp-account-tracker/src/state.rs b/contracts/warp-account-tracker/src/state.rs index 3b947346..77b501a3 100644 --- a/contracts/warp-account-tracker/src/state.rs +++ b/contracts/warp-account-tracker/src/state.rs @@ -1,19 +1,22 @@ -use account_tracker::{Config, FundingAccount}; +use account_tracker::{Account, Config}; use cosmwasm_std::{Addr, Uint64}; use cw_storage_plus::{Item, Map}; pub const CONFIG: Item = Item::new("config"); +// Key is the (account owner address, account address), value is the account struct +pub const ACCOUNTS: Map<(&Addr, &Addr), Account> = Map::new("accounts"); + +// Key is the (account owner address, account address), value is a vector of IDs of the jobs currently using it +pub const TAKEN_FUNDING_ACCOUNTS: Map<(&Addr, &Addr), Vec> = + Map::new("taken_funding_accounts"); + +// Key is the (account owner address, account address), value is id of the last job which reserved it (vec[last_job_id]) +pub const FREE_FUNDING_ACCOUNTS: Map<(&Addr, &Addr), Vec> = + Map::new("free_funding_accounts"); + // Key is the (account owner address, account address), value is the ID of the pending job currently using it pub const TAKEN_JOB_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("taken_job_accounts"); // Key is the (account owner address, account address), value is id of the last job which reserved it pub const FREE_JOB_ACCOUNTS: Map<(&Addr, &Addr), Uint64> = Map::new("free_job_accounts"); - -// owner address -> funding_account[] -// - user can have multiple funding accounts -// - a job can be assigned to only one funding account -// - funding account can fund multiple jobs -pub const FUNDING_ACCOUNTS_BY_USER: Map<&Addr, Vec> = - Map::new("funding_accounts_by_user"); -pub const TAKEN_FUNDING_ACCOUNT_BY_JOB: Map = Map::new("funding_account_by_job"); diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index c448658b..7f0972ca 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -131,13 +131,9 @@ pub fn execute( nonpayable(&info).unwrap(); execute::controller::update_config(deps, env, info, data, config) } - ExecuteMsg::MigrateFreeJobAccounts(data) => { + ExecuteMsg::MigrateAccounts(data) => { nonpayable(&info).unwrap(); - migrate::account::migrate_free_job_accounts(deps.as_ref(), env, info, data, config) - } - ExecuteMsg::MigrateTakenJobAccounts(data) => { - nonpayable(&info).unwrap(); - migrate::account::migrate_taken_job_accounts(deps.as_ref(), env, info, data, config) + migrate::account::migrate_accounts(deps.as_ref(), env, info, data, config) } ExecuteMsg::MigratePendingJobs(data) => { @@ -174,9 +170,8 @@ pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result Result { @@ -189,9 +184,6 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { reply::account::create_funding_account_and_job(deps, env, msg, config) } - REPLY_ID_CREATE_FUNDING_ACCOUNT => { - reply::account::create_funding_account(deps, env, msg, config) - } REPLY_ID_INSTANTIATE_SUB_CONTRACTS => { reply::job::instantiate_sub_contracts(deps, env, msg, config) } diff --git a/contracts/warp-controller/src/execute/account.rs b/contracts/warp-controller/src/execute/account.rs index 552e7e7d..09365e70 100644 --- a/contracts/warp-controller/src/execute/account.rs +++ b/contracts/warp-controller/src/execute/account.rs @@ -1,10 +1,7 @@ use controller::CreateFundingAccountMsg; -use cosmwasm_std::{DepsMut, Env, MessageInfo, ReplyOn, Response, SubMsg, Uint64}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint64}; -use crate::{ - contract::REPLY_ID_CREATE_FUNDING_ACCOUNT, state::CONFIG, - util::msg::build_instantiate_warp_account_msg, ContractError, -}; +use crate::{state::CONFIG, util::msg::build_instantiate_warp_account_msg, ContractError}; pub fn create_funding_account( deps: DepsMut, @@ -14,22 +11,17 @@ pub fn create_funding_account( ) -> Result { let config = CONFIG.load(deps.storage)?; - let submsgs = vec![SubMsg { - id: REPLY_ID_CREATE_FUNDING_ACCOUNT, - msg: build_instantiate_warp_account_msg( - Uint64::from(0u64), // placeholder - env.contract.address.to_string(), - config.warp_account_code_id.u64(), - info.sender.to_string(), - info.funds, - None, - None, - ), - gas_limit: None, - reply_on: ReplyOn::Always, - }]; + let msgs = vec![build_instantiate_warp_account_msg( + Uint64::from(0u64), // placeholder + env.contract.address.to_string(), + config.warp_account_code_id.u64(), + info.sender.to_string(), + info.funds, + None, + None, + )]; Ok(Response::new() .add_attribute("action", "create_funding_account") - .add_submessages(submsgs)) + .add_messages(msgs)) } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index d03141a4..5f3c5293 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -25,7 +25,7 @@ use crate::util::{ }, }; -use account_tracker::{AccountResponse, FundingAccountResponse}; +use account_tracker::{FundingAccountResponse, JobAccountResponse}; use controller::{account::CwFund, Config}; use resolver::QueryHydrateMsgsMsg; @@ -139,7 +139,7 @@ pub fn create_job( }, )?; - let account_resp: AccountResponse = deps.querier.query_wasm_smart( + let job_account_resp: JobAccountResponse = deps.querier.query_wasm_smart( account_tracker_address_ref, &account_tracker::QueryMsg::QueryFirstFreeJobAccount( account_tracker::QueryFirstFreeJobAccountMsg { @@ -172,7 +172,7 @@ pub fn create_job( )?; } - match account_resp.account { + match job_account_resp.job_account { None => { // Create account then create job in reply submsgs.push(SubMsg { @@ -193,7 +193,7 @@ pub fn create_job( attrs.push(Attribute::new("action", "create_account_and_job")); } Some(available_account) => { - let available_account_addr = &available_account.addr; + let available_account_addr = &available_account.account_addr; // Update job.account from placeholder value to job account job.account = available_account_addr.clone(); JobQueue::sync(deps.storage, env.clone(), job.clone())?; diff --git a/contracts/warp-controller/src/migrate/account.rs b/contracts/warp-controller/src/migrate/account.rs index 8bbe4a11..e4ef9fac 100644 --- a/contracts/warp-controller/src/migrate/account.rs +++ b/contracts/warp-controller/src/migrate/account.rs @@ -1,12 +1,10 @@ use cosmwasm_std::{to_binary, Deps, Env, MessageInfo, Response, WasmMsg}; use crate::ContractError; -use account_tracker::{ - AccountsResponse, MigrateMsg, QueryFreeJobAccountsMsg, QueryTakenJobAccountsMsg, -}; +use account_tracker::{AccountsResponse, MigrateMsg, QueryAccountsMsg}; use controller::{Config, MigrateAccountsMsg}; -pub fn migrate_free_job_accounts( +pub fn migrate_accounts( deps: Deps, _env: Env, info: MessageInfo, @@ -17,9 +15,9 @@ pub fn migrate_free_job_accounts( return Err(ContractError::Unauthorized {}); } - let free_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( + let accounts: AccountsResponse = deps.querier.query_wasm_smart( config.account_tracker_address, - &account_tracker::QueryMsg::QueryFreeJobAccounts(QueryFreeJobAccountsMsg { + &account_tracker::QueryMsg::QueryAccounts(QueryAccountsMsg { account_owner_addr: msg.account_owner_addr, start_after: msg.start_after, limit: Some(msg.limit as u32), @@ -27,41 +25,9 @@ pub fn migrate_free_job_accounts( )?; let mut migration_msgs = vec![]; - for account in free_job_accounts.accounts { + for account in accounts.accounts { migration_msgs.push(WasmMsg::Migrate { - contract_addr: account.addr.to_string(), - new_code_id: msg.warp_account_code_id.u64(), - msg: to_binary(&MigrateMsg {})?, - }); - } - - Ok(Response::new().add_messages(migration_msgs)) -} - -pub fn migrate_taken_job_accounts( - deps: Deps, - _env: Env, - info: MessageInfo, - msg: MigrateAccountsMsg, - config: Config, -) -> Result { - if info.sender != config.owner { - return Err(ContractError::Unauthorized {}); - } - - let taken_job_accounts: AccountsResponse = deps.querier.query_wasm_smart( - config.account_tracker_address, - &account_tracker::QueryMsg::QueryTakenJobAccounts(QueryTakenJobAccountsMsg { - account_owner_addr: msg.account_owner_addr, - start_after: msg.start_after, - limit: Some(msg.limit as u32), - }), - )?; - - let mut migration_msgs = vec![]; - for account in taken_job_accounts.accounts { - migration_msgs.push(WasmMsg::Migrate { - contract_addr: account.addr.to_string(), + contract_addr: account.account_addr.to_string(), new_code_id: msg.warp_account_code_id.u64(), msg: to_binary(&MigrateMsg {})?, }); diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 9a393dec..6c0586a1 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -8,9 +8,8 @@ use controller::{ use crate::{ state::JobQueue, util::msg::{ - build_account_execute_warp_msgs, build_add_funding_account_msg, - build_take_funding_account_msg, build_take_job_account_msg, build_transfer_cw20_msg, - build_transfer_cw721_msg, + build_account_execute_warp_msgs, build_take_funding_account_msg, + build_take_job_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, }, ContractError, }; @@ -224,64 +223,3 @@ pub fn create_funding_account_and_job( .add_attribute("funding_account_address", funding_account_addr) .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) } - -pub fn create_funding_account( - deps: DepsMut, - _env: Env, - msg: Reply, - config: Config, -) -> Result { - let reply = msg.result.into_result().map_err(StdError::generic_err)?; - - let funding_account_event = reply - .events - .iter() - .find(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "action" && attr.value == "instantiate") - }) - .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; - - let owner = funding_account_event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "owner") - .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? - .value; - - let funding_account_addr = deps.api.addr_validate( - &funding_account_event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "contract_addr") - .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? - .value, - )?; - - let native_funds: Vec = serde_json_wasm::from_str( - &funding_account_event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "native_funds") - .ok_or_else(|| StdError::generic_err("cannot find `native_funds` attribute"))? - .value, - )?; - - let msgs: Vec = vec![build_add_funding_account_msg( - config.account_tracker_address.to_string(), - owner.to_string(), - funding_account_addr.to_string(), - )]; - - Ok(Response::new() - .add_messages(msgs) - .add_attribute("action", "create_funding_account_reply") - .add_attribute("owner", owner) - .add_attribute("funding_account_address", funding_account_addr) - .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) -} diff --git a/contracts/warp-controller/src/util/msg.rs b/contracts/warp-controller/src/util/msg.rs index 526faa45..facbe1ed 100644 --- a/contracts/warp-controller/src/util/msg.rs +++ b/contracts/warp-controller/src/util/msg.rs @@ -1,8 +1,7 @@ use cosmwasm_std::{to_binary, BankMsg, Coin, CosmosMsg, Uint128, Uint64, WasmMsg}; use account_tracker::{ - AddFundingAccountMsg, FreeFundingAccountMsg, FreeJobAccountMsg, TakeFundingAccountMsg, - TakeJobAccountMsg, + FreeFundingAccountMsg, FreeJobAccountMsg, TakeFundingAccountMsg, TakeJobAccountMsg, }; use controller::account::{ AssetInfo, CwFund, FundTransferMsgs, TransferFromMsg, TransferNftMsg, WarpMsg, WarpMsgs, @@ -134,24 +133,6 @@ pub fn build_take_funding_account_msg( }) } -pub fn build_add_funding_account_msg( - account_tracker_addr: String, - account_owner_addr: String, - account_addr: String, -) -> CosmosMsg { - CosmosMsg::Wasm(WasmMsg::Execute { - contract_addr: account_tracker_addr, - msg: to_binary(&account_tracker::ExecuteMsg::AddFundingAccount( - AddFundingAccountMsg { - account_owner_addr, - account_addr, - }, - )) - .unwrap(), - funds: vec![], - }) -} - pub fn build_transfer_cw20_msg( cw20_token_contract_addr: String, owner_addr: String, diff --git a/packages/account-tracker/src/lib.rs b/packages/account-tracker/src/lib.rs index 85d14755..29f4154e 100644 --- a/packages/account-tracker/src/lib.rs +++ b/packages/account-tracker/src/lib.rs @@ -1,6 +1,19 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; use cosmwasm_std::{Addr, Uint64}; +#[cw_serde] +pub enum AccountType { + Funding, + Job, +} + +#[cw_serde] +pub struct Account { + pub account_type: AccountType, + pub owner_addr: Addr, + pub account_addr: Addr, +} + #[cw_serde] pub struct Config { pub admin: Addr, @@ -21,7 +34,6 @@ pub enum ExecuteMsg { FreeJobAccount(FreeJobAccountMsg), TakeFundingAccount(TakeFundingAccountMsg), FreeFundingAccount(FreeFundingAccountMsg), - AddFundingAccount(AddFundingAccountMsg), } #[cw_serde] @@ -64,17 +76,19 @@ pub enum QueryMsg { #[returns(ConfigResponse)] QueryConfig(QueryConfigMsg), #[returns(AccountsResponse)] - QueryTakenJobAccounts(QueryTakenJobAccountsMsg), - #[returns(AccountsResponse)] - QueryFreeJobAccounts(QueryFreeJobAccountsMsg), - #[returns(AccountResponse)] + QueryAccounts(QueryAccountsMsg), + #[returns(JobAccountsResponse)] + QueryJobAccounts(QueryJobAccountsMsg), + #[returns(JobAccountResponse)] + QueryJobAccount(QueryJobAccountMsg), + #[returns(JobAccountResponse)] QueryFirstFreeJobAccount(QueryFirstFreeJobAccountMsg), - #[returns(FundingAccountResponse)] - QueryFirstFreeFundingAccount(QueryFirstFreeFundingAccountMsg), #[returns(FundingAccountsResponse)] QueryFundingAccounts(QueryFundingAccountsMsg), #[returns(FundingAccountResponse)] QueryFundingAccount(QueryFundingAccountMsg), + #[returns(FundingAccountResponse)] + QueryFirstFreeFundingAccount(QueryFirstFreeFundingAccountMsg), } #[cw_serde] @@ -86,44 +100,63 @@ pub struct ConfigResponse { } #[cw_serde] -pub struct QueryTakenJobAccountsMsg { +pub struct AccountsResponse { + pub accounts: Vec, +} + +#[cw_serde] +pub struct QueryAccountsMsg { pub account_owner_addr: String, pub start_after: Option, pub limit: Option, } #[cw_serde] -pub struct QueryFreeJobAccountsMsg { +pub enum AccountStatus { + Free, + Taken, +} + +#[cw_serde] +pub struct QueryJobAccountsMsg { pub account_owner_addr: String, + pub account_status: AccountStatus, pub start_after: Option, pub limit: Option, } #[cw_serde] -pub struct Account { - pub addr: Addr, - pub taken_by_job_id: Option, +pub struct QueryFirstFreeJobAccountMsg { + pub account_owner_addr: String, } #[cw_serde] -pub struct FundingAccount { +pub struct QueryJobAccountMsg { + pub account_owner_addr: String, + pub account_addr: String, +} + +#[cw_serde] +pub struct JobAccount { pub account_addr: Addr, - pub taken_by_job_ids: Vec, // List of job IDs using this account + pub taken_by_job_id: Uint64, + pub account_status: AccountStatus, } #[cw_serde] -pub struct AccountsResponse { - pub accounts: Vec, +pub struct JobAccountsResponse { + pub job_accounts: Vec, pub total_count: u32, } #[cw_serde] -pub struct QueryFirstFreeJobAccountMsg { - pub account_owner_addr: String, +pub struct JobAccountResponse { + pub job_account: Option, } #[cw_serde] -pub struct QueryFreeJobAccountMsg { +pub struct QueryFundingAccountMsg { + pub account_owner_addr: String, pub account_addr: String, } @@ -133,24 +166,24 @@ pub struct QueryFirstFreeFundingAccountMsg { } #[cw_serde] -pub struct QueryFundingAccountMsg { +pub struct QueryFundingAccountsMsg { pub account_owner_addr: String, - pub account_addr: String, + pub account_status: AccountStatus, + pub start_after: Option, + pub limit: Option, } #[cw_serde] -pub struct QueryFundingAccountsMsg { - pub account_owner_addr: String, +pub struct FundingAccount { + pub account_addr: Addr, + pub taken_by_job_ids: Vec, + pub account_status: AccountStatus, } #[cw_serde] pub struct FundingAccountsResponse { pub funding_accounts: Vec, -} - -#[cw_serde] -pub struct AccountResponse { - pub account: Option, + pub total_count: u32, } #[cw_serde] diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 94651907..f5835468 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -97,8 +97,7 @@ pub enum ExecuteMsg { UpdateConfig(UpdateConfigMsg), - MigrateFreeJobAccounts(MigrateAccountsMsg), - MigrateTakenJobAccounts(MigrateAccountsMsg), + MigrateAccounts(MigrateAccountsMsg), MigratePendingJobs(MigrateJobsMsg), MigrateFinishedJobs(MigrateJobsMsg), diff --git a/refs.json b/refs.json index 6fe56253..47aec2a5 100644 --- a/refs.json +++ b/refs.json @@ -12,7 +12,7 @@ "codeId": "12318" }, "warp-controller": { - "codeId": "12369", + "codeId": "12371", "address": "terra1sylnrt9rkv7lraqvxssdzwmhm804qjtlp8ssjc502538ykyhdn5sns9edd" }, "warp-resolver": { @@ -24,7 +24,7 @@ "address": "terra1g8v5syvvfcsdlwd5yyguujlsf6vnadhdxghmnwu3ah6q8m7vn0jq0hcgwt" }, "warp-account-tracker": { - "codeId": "12330", + "codeId": "12370", "address": "terra1lhxshdp748xs56v83rsegwpmyuqxf2uszdr3wuazjrfq2wza2kyslq800h" } }, diff --git a/tasks/deploy_warp.ts b/tasks/deploy_warp.ts index 60618455..70ff0cbf 100644 --- a/tasks/deploy_warp.ts +++ b/tasks/deploy_warp.ts @@ -16,9 +16,7 @@ task(async ({ deployer, signer, refs }) => { await deployer.storeCode("warp-controller"); await new Promise((resolve) => setTimeout(resolve, 10000)); - const account_tracker_id = await deployer.storeCode( - "warp-account-tracker" - ); + const account_tracker_id = await deployer.storeCode("warp-account-tracker"); await new Promise((resolve) => setTimeout(resolve, 10000)); const instantiateTemplatesMsg = { diff --git a/tasks/migrate_warp.ts b/tasks/migrate_warp.ts index 347b7c7a..df59eac1 100644 --- a/tasks/migrate_warp.ts +++ b/tasks/migrate_warp.ts @@ -14,8 +14,7 @@ task(async ({ deployer, signer, refs, network }) => { signer.key.accAddress, contract.address!, parseInt(contract.codeId!), - { - } + {} ); try { From 64d9e78fae1f740471545725bacc04ecc62f9699 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 12 Dec 2023 23:14:42 +0100 Subject: [PATCH 114/133] new contracts --- refs.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/refs.json b/refs.json index 47aec2a5..73845004 100644 --- a/refs.json +++ b/refs.json @@ -9,22 +9,22 @@ }, "testnet": { "warp-account": { - "codeId": "12318" + "codeId": "12374" }, "warp-controller": { - "codeId": "12371", - "address": "terra1sylnrt9rkv7lraqvxssdzwmhm804qjtlp8ssjc502538ykyhdn5sns9edd" + "codeId": "12377", + "address": "terra15g0v6w0na27lud72lqm6ks4xup4ypj765ydkl9zfcxde9lpzm4lsns80pk" }, "warp-resolver": { - "codeId": "12368", - "address": "terra1yr249ds5f24u72cnyspdu0vghlkxyfanjj5kyagx9q4fm6nlsads89f9l7" + "codeId": "12375", + "address": "terra1f2ah2ftzagfrcw2wp9l8el7cjtr6n7q55w5l8hvz537wa6nejeuq2rxytl" }, "warp-templates": { - "codeId": "12320", - "address": "terra1g8v5syvvfcsdlwd5yyguujlsf6vnadhdxghmnwu3ah6q8m7vn0jq0hcgwt" + "codeId": "12376", + "address": "terra1w0gn3y7hmshmqp7juxejfh09rll55yr9g32wdc5usrrajfktu7rqed6rpv" }, "warp-account-tracker": { - "codeId": "12370", + "codeId": "12378", "address": "terra1lhxshdp748xs56v83rsegwpmyuqxf2uszdr3wuazjrfq2wza2kyslq800h" } }, From 560d465d5c06e83d42ae5f5361d193fa7fae2541 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 13 Dec 2023 05:21:00 +0100 Subject: [PATCH 115/133] clean controller config + new deploy contracts --- .../src/execute/account.rs | 1 - contracts/warp-controller/src/contract.rs | 32 ++++------ contracts/warp-controller/src/error.rs | 16 ++--- .../warp-controller/src/execute/controller.rs | 64 +++++++++---------- contracts/warp-controller/src/execute/fee.rs | 4 +- contracts/warp-controller/src/execute/job.rs | 4 +- packages/controller/src/lib.rs | 45 ++++--------- refs.json | 16 ++--- tasks/deploy_warp.ts | 12 +--- 9 files changed, 78 insertions(+), 116 deletions(-) diff --git a/contracts/warp-account-tracker/src/execute/account.rs b/contracts/warp-account-tracker/src/execute/account.rs index 1d025cad..a3bc6db6 100644 --- a/contracts/warp-account-tracker/src/execute/account.rs +++ b/contracts/warp-account-tracker/src/execute/account.rs @@ -77,7 +77,6 @@ pub fn free_job_account(deps: DepsMut, data: FreeJobAccountMsg) -> Result Ok(data.last_job_id), Some(_) => Err(ContractError::AccountAlreadyFreeError {}), }, diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 7f0972ca..cefe6e17 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -35,46 +35,40 @@ pub fn instantiate( .addr_validate(&msg.fee_collector.unwrap_or_else(|| info.sender.to_string()))?, warp_account_code_id: msg.warp_account_code_id, minimum_reward: msg.minimum_reward, - creation_fee_percentage: msg.creation_fee, - cancellation_fee_percentage: msg.cancellation_fee, resolver_address: deps.api.addr_validate(&msg.resolver_address)?, // placeholder, will be updated in reply account_tracker_address: deps.api.addr_validate(&msg.resolver_address)?, - t_max: msg.t_max, - t_min: msg.t_min, - a_max: msg.a_max, - a_min: msg.a_min, - q_max: msg.q_max, creation_fee_min: msg.creation_fee_min, creation_fee_max: msg.creation_fee_max, burn_fee_min: msg.burn_fee_min, maintenance_fee_min: msg.maintenance_fee_min, maintenance_fee_max: msg.maintenance_fee_max, - duration_days_left: msg.duration_days_left, - duration_days_right: msg.duration_days_right, + duration_days_min: msg.duration_days_min, + duration_days_max: msg.duration_days_max, queue_size_left: msg.queue_size_left, queue_size_right: msg.queue_size_right, burn_fee_rate: msg.burn_fee_rate, + cancellation_fee_rate: msg.cancellation_fee_rate, }; - if config.a_max < config.a_min { - return Err(ContractError::MaxFeeUnderMinFee {}); + if config.creation_fee_max < config.creation_fee_min { + return Err(ContractError::CreationMaxFeeUnderMinFee {}); } - if config.t_max < config.t_min { - return Err(ContractError::MaxTimeUnderMinTime {}); + if config.maintenance_fee_max < config.maintenance_fee_min { + return Err(ContractError::MaintenanceMaxFeeUnderMinFee {}); } - if config.minimum_reward < config.a_min { - return Err(ContractError::RewardSmallerThanFee {}); + if config.duration_days_max < config.duration_days_min { + return Err(ContractError::DurationMaxDaysUnderMinDays {}); } - if config.creation_fee_percentage.u64() > 100 { - return Err(ContractError::CreationFeeTooHigh {}); + if config.cancellation_fee_rate.u64() > 100 { + return Err(ContractError::CancellationFeeTooHigh {}); } - if config.cancellation_fee_percentage.u64() > 100 { - return Err(ContractError::CancellationFeeTooHigh {}); + if config.burn_fee_rate.u128() > 100 { + return Err(ContractError::BurnFeeTooHigh {}); } STATE.save(deps.storage, &state)?; diff --git a/contracts/warp-controller/src/error.rs b/contracts/warp-controller/src/error.rs index 67eb2a06..e121741d 100644 --- a/contracts/warp-controller/src/error.rs +++ b/contracts/warp-controller/src/error.rs @@ -66,8 +66,8 @@ pub enum ContractError { #[error("Cancellation fee too high")] CancellationFeeTooHigh {}, - #[error("Creation fee too high")] - CreationFeeTooHigh {}, + #[error("Burn fee too high")] + BurnFeeTooHigh {}, #[error("Custom Error val: {val:?}")] CustomError { val: String }, @@ -82,14 +82,14 @@ pub enum ContractError { #[error("Error decoding JSON result")] DecodeError {}, - #[error("Max eviction fee smaller than minimum eviction fee.")] - MaxFeeUnderMinFee {}, + #[error("Creation max fee smaller than minimum fee.")] + CreationMaxFeeUnderMinFee {}, - #[error("Max eviction time smaller than minimum eviction time.")] - MaxTimeUnderMinTime {}, + #[error("Maintenance max fee smaller than minimum fee.")] + MaintenanceMaxFeeUnderMinFee {}, - #[error("Job reward smaller than eviction fee.")] - RewardSmallerThanFee {}, + #[error("Max duration days smaller than minimum duration days.")] + DurationMaxDaysUnderMinDays {}, #[error("Eviction period not elapsed.")] EvictionPeriodNotElapsed {}, diff --git a/contracts/warp-controller/src/execute/controller.rs b/contracts/warp-controller/src/execute/controller.rs index d4d09b03..fe0dab92 100644 --- a/contracts/warp-controller/src/execute/controller.rs +++ b/contracts/warp-controller/src/execute/controller.rs @@ -25,39 +25,49 @@ pub fn update_config( Some(data) => deps.api.addr_validate(data.as_str())?, }; config.minimum_reward = data.minimum_reward.unwrap_or(config.minimum_reward); - config.creation_fee_percentage = data - .creation_fee_percentage - .unwrap_or(config.creation_fee_percentage); - config.cancellation_fee_percentage = data - .cancellation_fee_percentage - .unwrap_or(config.cancellation_fee_percentage); + config.cancellation_fee_rate = data + .cancellation_fee_rate + .unwrap_or(config.cancellation_fee_rate); - config.a_max = data.a_max.unwrap_or(config.a_max); - config.a_min = data.a_min.unwrap_or(config.a_min); - config.t_max = data.t_max.unwrap_or(config.t_max); - config.t_min = data.t_min.unwrap_or(config.t_min); - config.q_max = data.q_max.unwrap_or(config.q_max); + config.creation_fee_min = data.creation_fee_min.unwrap_or(config.creation_fee_min); + config.creation_fee_max = data.creation_fee_max.unwrap_or(config.creation_fee_max); + config.burn_fee_min = data.burn_fee_min.unwrap_or(config.burn_fee_min); + config.maintenance_fee_min = data + .maintenance_fee_min + .unwrap_or(config.maintenance_fee_min); + config.maintenance_fee_max = data + .maintenance_fee_max + .unwrap_or(config.maintenance_fee_max); + config.duration_days_min = data.duration_days_min.unwrap_or(config.duration_days_min); + config.duration_days_max = data.duration_days_max.unwrap_or(config.duration_days_max); + config.queue_size_left = data.queue_size_left.unwrap_or(config.queue_size_left); + config.queue_size_right = data.queue_size_right.unwrap_or(config.queue_size_right); + config.burn_fee_rate = data.burn_fee_rate.unwrap_or(config.burn_fee_rate); - if config.a_max < config.a_min { - return Err(ContractError::MaxFeeUnderMinFee {}); + if config.burn_fee_rate.u128() > 100 { + return Err(ContractError::BurnFeeTooHigh {}); } - if config.t_max < config.t_min { - return Err(ContractError::MaxTimeUnderMinTime {}); + if config.creation_fee_max < config.creation_fee_min { + return Err(ContractError::CreationMaxFeeUnderMinFee {}); } - if config.minimum_reward < config.a_min { - return Err(ContractError::RewardSmallerThanFee {}); + if config.maintenance_fee_max < config.maintenance_fee_min { + return Err(ContractError::MaintenanceMaxFeeUnderMinFee {}); } - if config.creation_fee_percentage.u64() > 100 { - return Err(ContractError::CreationFeeTooHigh {}); + if config.duration_days_max < config.duration_days_min { + return Err(ContractError::DurationMaxDaysUnderMinDays {}); } - if config.cancellation_fee_percentage.u64() > 100 { + if config.cancellation_fee_rate.u64() > 100 { return Err(ContractError::CancellationFeeTooHigh {}); } + if config.burn_fee_rate.u128() > 100 { + return Err(ContractError::BurnFeeTooHigh {}); + } + CONFIG.save(deps.storage, &config)?; Ok(Response::new() @@ -65,17 +75,5 @@ pub fn update_config( .add_attribute("config_owner", config.owner) .add_attribute("config_fee_collector", config.fee_collector) .add_attribute("config_minimum_reward", config.minimum_reward) - .add_attribute( - "config_creation_fee_percentage", - config.creation_fee_percentage, - ) - .add_attribute( - "config_cancellation_fee_percentage", - config.cancellation_fee_percentage, - ) - .add_attribute("config_a_max", config.a_max) - .add_attribute("config_a_min", config.a_min) - .add_attribute("config_t_max", config.t_max) - .add_attribute("config_t_min", config.t_min) - .add_attribute("config_q_max", config.q_max)) + .add_attribute("config_cancellation_fee_rate", config.cancellation_fee_rate)) } diff --git a/contracts/warp-controller/src/execute/fee.rs b/contracts/warp-controller/src/execute/fee.rs index e4ceede0..a9677d09 100644 --- a/contracts/warp-controller/src/execute/fee.rs +++ b/contracts/warp-controller/src/execute/fee.rs @@ -20,9 +20,9 @@ pub fn compute_creation_fee(queue_size: Uint64, config: &Config) -> Uint128 { } pub fn compute_maintenance_fee(duration_days: Uint64, config: &Config) -> Uint128 { - let x1 = Uint128::from(config.duration_days_left); + let x1 = Uint128::from(config.duration_days_min); let y1 = config.maintenance_fee_min; - let x2 = Uint128::from(config.duration_days_right); + let x2 = Uint128::from(config.duration_days_max); let y2 = config.maintenance_fee_max; let dd = Uint128::from(duration_days); diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 5f3c5293..b5dcb59f 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -374,7 +374,7 @@ pub fn delete_job( let _new_job = JobQueue::finalize(deps.storage, env, job.id.into(), JobStatus::Cancelled)?; - let fee = job.reward * Uint128::from(config.cancellation_fee_percentage) / Uint128::new(100); + let fee = job.reward * Uint128::from(config.cancellation_fee_rate) / Uint128::new(100); if fee > fee_denom_paid_amount { return Err(ContractError::InsufficientFundsToPayForFee {}); } @@ -590,7 +590,7 @@ pub fn evict_job( return Err(ContractError::Unauthorized {}); } - let eviction_fee = config.a_max; + let eviction_fee = config.maintenance_fee_min; if (env.block.time.seconds() - job.created_at_time.u64()) < (job.duration_days.u64() * 86400) { return Err(ContractError::EvictionPeriodNotElapsed {}); diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index f5835468..093c2431 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -16,31 +16,20 @@ pub struct Config { pub fee_collector: Addr, pub warp_account_code_id: Uint64, pub minimum_reward: Uint128, - pub creation_fee_percentage: Uint64, - pub cancellation_fee_percentage: Uint64, + pub cancellation_fee_rate: Uint64, // By querying job account tracker contract // We know all accounts owned by that user and each account's availability // For more detail, please refer to job account tracker contract pub account_tracker_address: Addr, pub resolver_address: Addr, - // maximum time for evictions - pub t_max: Uint64, - // minimum time for evictions - pub t_min: Uint64, - // maximum fee for evictions - pub a_max: Uint128, - // minimum fee for evictions - pub a_min: Uint128, - // maximum length of queue modifier for evictions - pub q_max: Uint64, pub creation_fee_min: Uint128, pub creation_fee_max: Uint128, pub burn_fee_min: Uint128, pub maintenance_fee_min: Uint128, pub maintenance_fee_max: Uint128, // duration_days fn interval [left, right] - pub duration_days_left: Uint64, - pub duration_days_right: Uint64, + pub duration_days_min: Uint64, + pub duration_days_max: Uint64, // queue_size fn interval [left, right] pub queue_size_left: Uint64, pub queue_size_right: Uint64, @@ -63,22 +52,16 @@ pub struct InstantiateMsg { pub warp_account_code_id: Uint64, pub account_tracker_code_id: Uint64, pub minimum_reward: Uint128, - pub creation_fee: Uint64, - pub cancellation_fee: Uint64, + pub cancellation_fee_rate: Uint64, pub resolver_address: String, - pub t_max: Uint64, - pub t_min: Uint64, - pub a_max: Uint128, - pub a_min: Uint128, - pub q_max: Uint64, pub creation_fee_min: Uint128, pub creation_fee_max: Uint128, pub burn_fee_min: Uint128, pub maintenance_fee_min: Uint128, pub maintenance_fee_max: Uint128, // duration_days fn interval [left, right] - pub duration_days_left: Uint64, - pub duration_days_right: Uint64, + pub duration_days_min: Uint64, + pub duration_days_max: Uint64, // queue_size fn interval [left, right] pub queue_size_left: Uint64, pub queue_size_right: Uint64, @@ -110,24 +93,18 @@ pub struct UpdateConfigMsg { pub owner: Option, pub fee_collector: Option, pub minimum_reward: Option, - pub creation_fee_percentage: Option, - pub cancellation_fee_percentage: Option, - pub t_max: Option, - pub t_min: Option, - pub a_max: Option, - pub a_min: Option, - pub q_max: Option, + pub cancellation_fee_rate: Option, pub creation_fee_min: Option, pub creation_fee_max: Option, pub burn_fee_min: Option, pub maintenance_fee_min: Option, pub maintenance_fee_max: Option, // duration_days fn interval [left, right] - pub duration_days_left: Option, - pub duration_days_right: Option, + pub duration_days_min: Option, + pub duration_days_max: Option, // queue_size fn interval [left, right] - pub queue_size_left: Option, - pub queue_size_right: Option, + pub queue_size_left: Option, + pub queue_size_right: Option, pub burn_fee_rate: Option, } diff --git a/refs.json b/refs.json index 73845004..094b4f34 100644 --- a/refs.json +++ b/refs.json @@ -9,22 +9,22 @@ }, "testnet": { "warp-account": { - "codeId": "12374" + "codeId": "12384" }, "warp-controller": { - "codeId": "12377", - "address": "terra15g0v6w0na27lud72lqm6ks4xup4ypj765ydkl9zfcxde9lpzm4lsns80pk" + "codeId": "12387", + "address": "terra1h6qvvjkkv2yvr66hkds85esfqm9qe0d7s4svk6hnsj09xdwxxg8q7j7jvu" }, "warp-resolver": { - "codeId": "12375", - "address": "terra1f2ah2ftzagfrcw2wp9l8el7cjtr6n7q55w5l8hvz537wa6nejeuq2rxytl" + "codeId": "12385", + "address": "terra1qfkyljtyvkjccwvxejwhdfaxnkwwt67upnyljwe768dku7lchztqxn94d0" }, "warp-templates": { - "codeId": "12376", - "address": "terra1w0gn3y7hmshmqp7juxejfh09rll55yr9g32wdc5usrrajfktu7rqed6rpv" + "codeId": "12386", + "address": "terra1wergw3euhfxz8qwp3zc2s8ppyarksvw9p3e8mty4yg9ggpzutrds0yn2ku" }, "warp-account-tracker": { - "codeId": "12378", + "codeId": "12388", "address": "terra1lhxshdp748xs56v83rsegwpmyuqxf2uszdr3wuazjrfq2wza2kyslq800h" } }, diff --git a/tasks/deploy_warp.ts b/tasks/deploy_warp.ts index 70ff0cbf..49855c40 100644 --- a/tasks/deploy_warp.ts +++ b/tasks/deploy_warp.ts @@ -46,21 +46,15 @@ task(async ({ deployer, signer, refs }) => { warp_account_code_id: account_contract_id, account_tracker_code_id: account_tracker_id, minimum_reward: "10000", - creation_fee: "5", - cancellation_fee: "5", + cancellation_fee_rate: "5", resolver_address: resolver_address, - t_max: "86400", - t_min: "86400", - a_max: "10000", - a_min: "10000", - q_max: "10", creation_fee_min: "500000", creation_fee_max: "100000000", burn_fee_min: "100000", maintenance_fee_min: "50000", maintenance_fee_max: "10000000", - duration_days_left: "10", - duration_days_right: "100", + duration_days_min: "10", + duration_days_max: "100", queue_size_left: "5000", queue_size_right: "50000", burn_fee_rate: "25", From ed68d214e660f5550ee8311a9abf0fbd6cfea2dc Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 13 Dec 2023 05:25:21 +0100 Subject: [PATCH 116/133] update account-tracker address --- refs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/refs.json b/refs.json index 094b4f34..a7702a5c 100644 --- a/refs.json +++ b/refs.json @@ -25,7 +25,7 @@ }, "warp-account-tracker": { "codeId": "12388", - "address": "terra1lhxshdp748xs56v83rsegwpmyuqxf2uszdr3wuazjrfq2wza2kyslq800h" + "address": "terra1dg2wm2ftljtydvmkwf6y2vavtjcz496um22tx548zvvupndv8gws0k9daq" } }, "mainnet": { From 50aa5b62982b13e5fe145e0bb7981767b447f5d6 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Wed, 13 Dec 2023 21:56:54 +0100 Subject: [PATCH 117/133] final polish + fixes --- .../src/execute/account.rs | 4 +- contracts/warp-controller/src/contract.rs | 19 +- contracts/warp-controller/src/error.rs | 3 + contracts/warp-controller/src/execute/job.rs | 224 ++++++++---------- contracts/warp-controller/src/migrate/job.rs | 2 - contracts/warp-controller/src/reply/job.rs | 20 +- contracts/warp-controller/src/state.rs | 7 +- packages/controller/src/job.rs | 25 +- refs.json | 18 +- 9 files changed, 136 insertions(+), 186 deletions(-) diff --git a/contracts/warp-account-tracker/src/execute/account.rs b/contracts/warp-account-tracker/src/execute/account.rs index a3bc6db6..a6a99d67 100644 --- a/contracts/warp-account-tracker/src/execute/account.rs +++ b/contracts/warp-account-tracker/src/execute/account.rs @@ -102,7 +102,7 @@ pub fn take_funding_account( match s { Some(account) => Ok(account), None => Ok(Account { - account_type: AccountType::Job, + account_type: AccountType::Funding, owner_addr: account_owner_addr_ref.clone(), account_addr: account_addr_ref.clone(), }), @@ -150,7 +150,7 @@ pub fn free_funding_account( match s { Some(account) => Ok(account), None => Ok(Account { - account_type: AccountType::Job, + account_type: AccountType::Funding, owner_addr: account_owner_addr_ref.clone(), account_addr: account_addr_ref.clone(), }), diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index cefe6e17..cc61aeac 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -2,7 +2,7 @@ use cosmwasm_std::{ entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, ReplyOn, Response, StdResult, SubMsg, Uint64, }; -use cw_utils::{must_pay, nonpayable}; +use cw_utils::nonpayable; use crate::{ execute, migrate, query, reply, @@ -97,21 +97,8 @@ pub fn execute( ) -> Result { let config = CONFIG.load(deps.storage)?; match msg { - ExecuteMsg::CreateJob(data) => { - // IBC denoms can be passed alongside native, can't use must_pay - let fee_denom_paid_amount = info - .funds - .iter() - .find(|f| f.denom == config.fee_denom) - .unwrap() - .amount; - - execute::job::create_job(deps, env, info, data, config, fee_denom_paid_amount) - } - ExecuteMsg::DeleteJob(data) => { - let fee_denom_paid_amount = must_pay(&info, &config.fee_denom).unwrap(); - execute::job::delete_job(deps, env, info, data, config, fee_denom_paid_amount) - } + ExecuteMsg::CreateJob(data) => execute::job::create_job(deps, env, info, data, config), + ExecuteMsg::DeleteJob(data) => execute::job::delete_job(deps, env, info, data, config), ExecuteMsg::UpdateJob(data) => execute::job::update_job(deps, env, info, data), ExecuteMsg::ExecuteJob(data) => { nonpayable(&info).unwrap(); diff --git a/contracts/warp-controller/src/error.rs b/contracts/warp-controller/src/error.rs index e121741d..dec1202d 100644 --- a/contracts/warp-controller/src/error.rs +++ b/contracts/warp-controller/src/error.rs @@ -12,6 +12,9 @@ pub enum ContractError { #[error("Unauthorized")] Unauthorized {}, + #[error("Funding account not provided for recurring job")] + FundingAccountMissingForRecurringJob {}, + #[error("Insufficient funds to pay for reward and fee.")] InsufficientFundsToPayForRewardAndFee {}, diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index b5dcb59f..2d6789da 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,13 +1,11 @@ -use crate::contract::{ - REPLY_ID_CREATE_FUNDING_ACCOUNT_AND_JOB, REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, - REPLY_ID_EXECUTE_JOB, -}; +use crate::contract::{REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, REPLY_ID_EXECUTE_JOB}; use crate::state::{JobQueue, STATE}; use crate::util::msg::{ - build_account_execute_warp_msgs, build_free_funding_account_msg, build_take_funding_account_msg, + build_account_execute_generic_msgs, build_account_execute_warp_msgs, + build_free_funding_account_msg, build_take_funding_account_msg, }; use crate::ContractError; -use controller::account::{AssetInfo, WarpMsgs}; +use controller::account::WarpMsgs; use controller::job::{ CreateJobMsg, DeleteJobMsg, EvictJobMsg, ExecuteJobMsg, Execution, Job, JobStatus, UpdateJobMsg, }; @@ -25,7 +23,7 @@ use crate::util::{ }, }; -use account_tracker::{FundingAccountResponse, JobAccountResponse}; +use account_tracker::{FundingAccount, FundingAccountResponse, JobAccountResponse}; use controller::{account::CwFund, Config}; use resolver::QueryHydrateMsgsMsg; @@ -39,7 +37,6 @@ pub fn create_job( info: MessageInfo, data: CreateJobMsg, config: Config, - fee_denom_paid_amount: Uint128, ) -> Result { if data.name.len() > MAX_TEXT_LENGTH { return Err(ContractError::NameTooLong {}); @@ -73,42 +70,30 @@ pub fn create_job( let total_fees = creation_fee + maintenance_fee + burn_fee; - if data.operational_amount > fee_denom_paid_amount { - return Err(ContractError::InsufficientOperationalFunds {}); + if data.funding_account.is_none() && data.recurring { + return Err(ContractError::FundingAccountMissingForRecurringJob {}); } - if data.reward + total_fees > fee_denom_paid_amount { - return Err(ContractError::InsufficientFundsToPayForRewardAndFee {}); - } + // ignore operational_amount when funding_account is provided + let operational_amount = if data.funding_account.is_some() { + Uint128::zero() + } else { + data.operational_amount + }; // Reward and fee will always be in native denom let native_funds_minus_operational_amount = deduct_from_native_funds( info.funds.clone(), config.fee_denom.clone(), - data.operational_amount, + operational_amount, ); let mut submsgs = vec![]; let mut msgs = vec![]; let mut attrs = vec![]; - // Job owner sends reward to controller when it calls create_job - // Reward stays at controller, no need to send it elsewhere - msgs.push( - // Job owner sends fee to controller when it calls create_job - // Controller sends fee to fee collector - build_transfer_native_funds_msg( - config.fee_collector.to_string(), - vec![Coin::new(total_fees.u128(), config.fee_denom.clone())], - ), - ); - let state = STATE.load(deps.storage)?; - let operational_amount_minus_reward_and_fee = data - .operational_amount - .checked_sub(data.reward + total_fees)?; - let mut job = JobQueue::add( deps.storage, Job { @@ -134,8 +119,6 @@ pub fn create_job( created_at_time: Uint64::from(env.block.time.seconds()), // placeholder, will be updated later on funding_account: None, - // needs to have reward and total_fees subtracted from it (reward is sent to controller, fees are sent to fee collector) - operational_amount: operational_amount_minus_reward_and_fee, }, )?; @@ -148,30 +131,6 @@ pub fn create_job( ), )?; - let funding_account_resp: FundingAccountResponse; - - if let Some(funding_account_addr) = data.funding_account { - // fetch funding account and check if it exists, throw otherwise - funding_account_resp = deps.querier.query_wasm_smart( - account_tracker_address_ref, - &account_tracker::QueryMsg::QueryFundingAccount( - account_tracker::QueryFundingAccountMsg { - account_addr: funding_account_addr.to_string(), - account_owner_addr: info.sender.to_string(), - }, - ), - )?; - } else { - funding_account_resp = deps.querier.query_wasm_smart( - account_tracker_address_ref, - &account_tracker::QueryMsg::QueryFirstFreeFundingAccount( - account_tracker::QueryFirstFreeFundingAccountMsg { - account_owner_addr: job_owner.to_string(), - }, - ), - )?; - } - match job_account_resp.job_account { None => { // Create account then create job in reply @@ -272,81 +231,94 @@ pub fn create_job( } } - if data.recurring { - match funding_account_resp.funding_account { - None => { - // Create funding account then create job in reply - submsgs.push(SubMsg { - id: REPLY_ID_CREATE_FUNDING_ACCOUNT_AND_JOB, - msg: build_instantiate_warp_account_msg( - job.id, - env.contract.address.to_string(), - config.warp_account_code_id.u64(), - info.sender.to_string(), - vec![Coin::new( - operational_amount_minus_reward_and_fee.u128(), - config.fee_denom, - )], - None, - None, - ), - gas_limit: None, - reply_on: ReplyOn::Always, - }); + let mut funding_account: Option = None; + + if let Some(funding_account_addr) = data.funding_account { + // fetch funding account and check if it exists, throw otherwise + let funding_account_resp: FundingAccountResponse = deps.querier.query_wasm_smart( + account_tracker_address_ref, + &account_tracker::QueryMsg::QueryFundingAccount( + account_tracker::QueryFundingAccountMsg { + account_addr: funding_account_addr.to_string(), + account_owner_addr: info.sender.to_string(), + }, + ), + )?; + + funding_account = funding_account_resp.funding_account; + } - attrs.push(Attribute::new("action", "create_funding_account_and_job")); + match funding_account { + None => { + // exit only applies for recurring jobs, otherwise funds are in controller + if data.recurring { + return Err(ContractError::FundingAccountMissingForRecurringJob {}); } - Some(available_account) => { - let available_account_addr = &available_account.account_addr; - // Update funding_account from placeholder value to funding account - job.funding_account = Some(available_account_addr.clone()); - JobQueue::sync(deps.storage, env, job.clone())?; + } + Some(available_account) => { + let available_account_addr = &available_account.account_addr; + // Update funding_account from placeholder value to funding account + job.funding_account = Some(available_account_addr.clone()); + JobQueue::sync(deps.storage, env.clone(), job.clone())?; - // Fund account in native coins - msgs.push(build_transfer_native_funds_msg( - available_account_addr.to_string(), + // transfer reward + fees to controller from funding account + msgs.push(build_account_execute_generic_msgs( + job.funding_account.clone().unwrap().to_string(), + vec![build_transfer_native_funds_msg( + env.contract.address.to_string(), vec![Coin::new( - operational_amount_minus_reward_and_fee.u128(), - config.fee_denom, + total_fees.u128() + data.reward.u128(), + config.fee_denom.clone(), )], - )); + )], + )); - // Take account - msgs.push(build_take_funding_account_msg( - config.account_tracker_address.to_string(), - job_owner.to_string(), - available_account_addr.to_string(), - job.id, - )); + // Take account + msgs.push(build_take_funding_account_msg( + config.account_tracker_address.to_string(), + job_owner.to_string(), + available_account_addr.to_string(), + job.id, + )); - attrs.push(Attribute::new("action", "create_job")); - attrs.push(Attribute::new("job_id", job.id)); - attrs.push(Attribute::new("job_owner", job.owner)); - attrs.push(Attribute::new("job_name", job.name)); - attrs.push(Attribute::new( - "job_status", - serde_json_wasm::to_string(&job.status)?, - )); - attrs.push(Attribute::new( - "job_executions", - serde_json_wasm::to_string(&job.executions)?, - )); - attrs.push(Attribute::new("job_reward", job.reward)); - attrs.push(Attribute::new("job_creation_fee", creation_fee.to_string())); - attrs.push(Attribute::new( - "job_maintenance_fee", - maintenance_fee.to_string(), - )); - attrs.push(Attribute::new("job_burn_fee", burn_fee.to_string())); - attrs.push(Attribute::new("job_total_fees", total_fees.to_string())); - attrs.push(Attribute::new( - "job_last_updated_time", - job.last_update_time, - )); - } + attrs.push(Attribute::new("action", "create_job")); + attrs.push(Attribute::new("job_id", job.id)); + attrs.push(Attribute::new("job_owner", job.owner)); + attrs.push(Attribute::new("job_name", job.name)); + attrs.push(Attribute::new( + "job_status", + serde_json_wasm::to_string(&job.status)?, + )); + attrs.push(Attribute::new( + "job_executions", + serde_json_wasm::to_string(&job.executions)?, + )); + attrs.push(Attribute::new("job_reward", job.reward)); + attrs.push(Attribute::new("job_creation_fee", creation_fee.to_string())); + attrs.push(Attribute::new( + "job_maintenance_fee", + maintenance_fee.to_string(), + )); + attrs.push(Attribute::new("job_burn_fee", burn_fee.to_string())); + attrs.push(Attribute::new("job_total_fees", total_fees.to_string())); + attrs.push(Attribute::new( + "job_last_updated_time", + job.last_update_time, + )); } } + // Job owner sends reward to controller when it calls create_job + // Reward stays at controller, no need to send it elsewhere + msgs.push( + // Job owner sends fee to controller when it calls create_job + // Controller sends fee to fee collector + build_transfer_native_funds_msg( + config.fee_collector.to_string(), + vec![Coin::new(total_fees.u128(), config.fee_denom)], + ), + ); + Ok(Response::new() .add_submessages(submsgs) .add_messages(msgs) @@ -359,7 +331,6 @@ pub fn delete_job( info: MessageInfo, data: DeleteJobMsg, config: Config, - fee_denom_paid_amount: Uint128, ) -> Result { let job = JobQueue::get(deps.storage, data.id.into())?; let account_addr = job.account.clone(); @@ -375,9 +346,6 @@ pub fn delete_job( let _new_job = JobQueue::finalize(deps.storage, env, job.id.into(), JobStatus::Cancelled)?; let fee = job.reward * Uint128::from(config.cancellation_fee_rate) / Uint128::new(100); - if fee > fee_denom_paid_amount { - return Err(ContractError::InsufficientFundsToPayForFee {}); - } let mut msgs = vec![]; @@ -412,12 +380,6 @@ pub fn delete_job( funding_account.to_string(), job.id, )); - - // withdraws all native funds from funding account - msgs.push(build_account_withdraw_assets_msg( - funding_account.to_string(), - vec![AssetInfo::Native(config.fee_denom)], - )); } // Job owner withdraw all assets that are listed from warp account to itself @@ -567,8 +529,8 @@ pub fn execute_job( } Ok(Response::new() - .add_submessages(submsgs) .add_messages(msgs) + .add_submessages(submsgs) .add_attribute("action", "execute_job") .add_attribute("executor", info.sender) .add_attribute("job_id", job.id) diff --git a/contracts/warp-controller/src/migrate/job.rs b/contracts/warp-controller/src/migrate/job.rs index cb230d90..90e4175c 100644 --- a/contracts/warp-controller/src/migrate/job.rs +++ b/contracts/warp-controller/src/migrate/job.rs @@ -105,7 +105,6 @@ pub fn migrate_pending_jobs( created_at_time: old_job.last_update_time, // TODO: update to old_job.funding_account funding_account: None, - operational_amount: old_job.reward, }, )?; } @@ -175,7 +174,6 @@ pub fn migrate_finished_jobs( created_at_time: old_job.last_update_time, // TODO: update to old_job.funding_account funding_account: None, - operational_amount: old_job.reward, }, )?; } diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 53d7588b..1411b6c8 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -15,7 +15,6 @@ use crate::{ ContractError, }; use controller::{ - account::AssetInfo, job::{Job, JobStatus}, Config, }; @@ -110,6 +109,7 @@ pub fn execute_job( "failed_invalid_job_status", )); } else { + // vars are updated to next job iteration let new_vars: String = deps.querier.query_wasm_smart( config.resolver_address.clone(), &resolver::QueryMsg::QueryApplyVarFn(resolver::QueryApplyVarFnMsg { @@ -120,6 +120,8 @@ pub fn execute_job( )?; let should_terminate_job: bool; + + // check if terminate condition is true with updated vars match finished_job.terminate_condition.clone() { Some(terminate_condition) => { let resolution: StdResult = deps.querier.query_wasm_smart( @@ -167,9 +169,6 @@ pub fn execute_job( if !should_terminate_job { recurring_job_created = true; - let operational_amount_minus_reward_and_fee = - operational_amount.checked_sub(finished_job.reward + total_fees)?; - let new_job = JobQueue::add( deps.storage, Job { @@ -187,7 +186,6 @@ pub fn execute_job( vars: new_vars, recurring: finished_job.recurring, reward: finished_job.reward, - operational_amount: operational_amount_minus_reward_and_fee, assets_to_withdraw: finished_job.assets_to_withdraw.clone(), duration_days: finished_job.duration_days, created_at_time: Uint64::from(env.block.time.seconds()), @@ -243,7 +241,7 @@ pub fn execute_job( if recurring_job_created { let funding_account_addr = finished_job.funding_account.clone().unwrap(); - // Take job account with the new job + // Take job account with the new job, previously freed in execute_job msgs.push(build_take_job_account_msg( config.account_tracker_address.to_string(), finished_job.owner.to_string(), @@ -251,7 +249,7 @@ pub fn execute_job( new_job_id, )); - // take funding account with new job + // take funding account with new job, previously freed in execute_job msgs.push(build_take_funding_account_msg( config.account_tracker_address.to_string(), finished_job.owner.to_string(), @@ -265,14 +263,6 @@ pub fn execute_job( account_addr.to_string(), finished_job.assets_to_withdraw, )); - - // withdraw all funds if funding acc exists - if let Some(acc) = finished_job.funding_account { - msgs.push(build_account_withdraw_assets_msg( - acc.to_string(), - vec![AssetInfo::Native(config.fee_denom)], - )); - } } Ok(Response::new() diff --git a/contracts/warp-controller/src/state.rs b/contracts/warp-controller/src/state.rs index 8d170984..abeab1f2 100644 --- a/contracts/warp-controller/src/state.rs +++ b/contracts/warp-controller/src/state.rs @@ -102,7 +102,6 @@ impl JobQueue { vars: job.vars, recurring: job.recurring, reward: job.reward, - operational_amount: job.operational_amount, assets_to_withdraw: job.assets_to_withdraw, duration_days: job.duration_days, created_at_time: Uint64::from(env.block.time.seconds()), @@ -115,7 +114,7 @@ impl JobQueue { pub fn update( storage: &mut dyn Storage, - _env: Env, + env: Env, data: UpdateJobMsg, ) -> Result { let job = PENDING_JOBS().update(storage, data.id.u64(), |h| match h { @@ -125,7 +124,7 @@ impl JobQueue { prev_id: job.prev_id, owner: job.owner, account: job.account, - last_update_time: job.last_update_time, + last_update_time: Uint64::new(env.block.time.seconds()), name: data.name.unwrap_or(job.name), description: data.description.unwrap_or(job.description), labels: data.labels.unwrap_or(job.labels), @@ -139,7 +138,6 @@ impl JobQueue { duration_days: job.duration_days, created_at_time: job.created_at_time, funding_account: job.funding_account, - operational_amount: job.operational_amount, }), })?; @@ -177,7 +175,6 @@ impl JobQueue { duration_days: job.duration_days, created_at_time: job.created_at_time, funding_account: job.funding_account, - operational_amount: job.operational_amount, }; FINISHED_JOBS().update(storage, job_id, |j| match j { diff --git a/packages/controller/src/job.rs b/packages/controller/src/job.rs index 0fed5ed4..42969271 100644 --- a/packages/controller/src/job.rs +++ b/packages/controller/src/job.rs @@ -11,12 +11,13 @@ pub struct Job { // Exist if job is the follow up job of a recurring job pub prev_id: Option, pub owner: Addr, - // Warp account this job is associated with, job will be executed in the context of it and pay protocol fee from it - // As job creator can have infinite job accounts, each job account can only be used by up to 1 active job - // So each job's fund is isolated + // Warp account this job is associated with, job will be executed in the context of it and + // pay protocol fee from it. As job creator can have infinite job accounts, each job account + // can only be used by up to 1 active job, so each job's fund is isolated pub account: Addr, - // Funding account is an optionally provided account from which job fees and rewards are deducted from, used only in case - // of recurring jobs - if a user doesn't provide a funding account, one is created on the fly + // Funding account from which job fees and rewards are deducted. + // - required for recurring jobs + // - optionally provided for one time jobs pub funding_account: Option, pub last_update_time: Uint64, pub name: String, @@ -30,7 +31,9 @@ pub struct Job { pub duration_days: Uint64, pub created_at_time: Uint64, pub reward: Uint128, - pub operational_amount: Uint128, + // Acts like a lifecycle method - called on job termination. + // For withdrawing assets on each job execution (recurring jobs), + // use WithdrawAssets warp msg pub assets_to_withdraw: Vec, } @@ -60,16 +63,26 @@ pub struct CreateJobMsg { pub name: String, pub description: String, pub labels: Vec, + // exit condition for recurring jobs pub terminate_condition: Option, pub executions: Vec, pub vars: String, pub recurring: bool, pub reward: Uint128, + // without funding account: operational_amount needs to equal total_fees + reward + // with funding account: ignored, can be set to 0 pub operational_amount: Uint128, pub duration_days: Uint64, + // Acts like a lifecycle method - called on job termination. + // For withdrawing assets on each job execution (recurring jobs), + // use WithdrawAssets warp msg pub assets_to_withdraw: Option>, + // messages that are executed via job-account when the job is created pub account_msgs: Option>, pub cw_funds: Option>, + // Funding account from which job fees and rewards are deducted. + // - required for recurring jobs + // - optionally provided for one time jobs pub funding_account: Option, } diff --git a/refs.json b/refs.json index a7702a5c..b173e3ad 100644 --- a/refs.json +++ b/refs.json @@ -9,23 +9,23 @@ }, "testnet": { "warp-account": { - "codeId": "12384" + "codeId": "12397" }, "warp-controller": { - "codeId": "12387", - "address": "terra1h6qvvjkkv2yvr66hkds85esfqm9qe0d7s4svk6hnsj09xdwxxg8q7j7jvu" + "codeId": "12409", + "address": "terra16zlwx7yeugjpzvdw2cs8v7zghssmh95thrwuzjf56u4g6xu0udpqqctnxa" }, "warp-resolver": { - "codeId": "12385", - "address": "terra1qfkyljtyvkjccwvxejwhdfaxnkwwt67upnyljwe768dku7lchztqxn94d0" + "codeId": "12398", + "address": "terra12sm42c7g0733zullajvpczazpgtj4w0ndma3yta38840d3sqg5cswv0drc" }, "warp-templates": { - "codeId": "12386", - "address": "terra1wergw3euhfxz8qwp3zc2s8ppyarksvw9p3e8mty4yg9ggpzutrds0yn2ku" + "codeId": "12399", + "address": "terra1cp3lf3qf5gtmv30stprmrt7ye03j5t6nazhe45rzk6nuz2ka83ks9an6gy" }, "warp-account-tracker": { - "codeId": "12388", - "address": "terra1dg2wm2ftljtydvmkwf6y2vavtjcz496um22tx548zvvupndv8gws0k9daq" + "codeId": "12401", + "address": "terra15evxsh7hxdpwvpd6c6t9hn3q6r6q6pfxq8nz27kpp56nusqfyeuqpc0jjp" } }, "mainnet": { From 571b7408374afecacd41114ddc692d9f675218f1 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 15 Dec 2023 16:27:41 +0100 Subject: [PATCH 118/133] register funding account as free when created --- .../src/execute/account.rs | 9 ++- contracts/warp-controller/src/contract.rs | 8 ++- .../warp-controller/src/execute/account.rs | 32 +++++---- .../warp-controller/src/reply/account.rs | 69 ++++++++++++++++++- refs.json | 4 +- 5 files changed, 101 insertions(+), 21 deletions(-) diff --git a/contracts/warp-account-tracker/src/execute/account.rs b/contracts/warp-account-tracker/src/execute/account.rs index a6a99d67..75f951a6 100644 --- a/contracts/warp-account-tracker/src/execute/account.rs +++ b/contracts/warp-account-tracker/src/execute/account.rs @@ -163,8 +163,13 @@ pub fn free_funding_account( } // Retrieve current job IDs for the funding account - let mut job_ids = - TAKEN_FUNDING_ACCOUNTS.load(deps.storage, (account_owner_addr_ref, account_addr_ref))?; + let mut job_ids = match TAKEN_FUNDING_ACCOUNTS + .may_load(deps.storage, (account_owner_addr_ref, account_addr_ref)) + { + Ok(Some(job_ids)) => job_ids, + Ok(None) => vec![], + Err(err) => return Err(ContractError::Std(err)), + }; // Remove the specified job ID job_ids.retain(|&id| id != data.job_id); diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index cc61aeac..0e5423cb 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -151,8 +151,9 @@ pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result Result { @@ -165,6 +166,9 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { reply::account::create_funding_account_and_job(deps, env, msg, config) } + REPLY_ID_CREATE_FUNDING_ACCOUNT => { + reply::account::create_funding_account(deps, env, msg, config) + } REPLY_ID_INSTANTIATE_SUB_CONTRACTS => { reply::job::instantiate_sub_contracts(deps, env, msg, config) } diff --git a/contracts/warp-controller/src/execute/account.rs b/contracts/warp-controller/src/execute/account.rs index 09365e70..552e7e7d 100644 --- a/contracts/warp-controller/src/execute/account.rs +++ b/contracts/warp-controller/src/execute/account.rs @@ -1,7 +1,10 @@ use controller::CreateFundingAccountMsg; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Uint64}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, ReplyOn, Response, SubMsg, Uint64}; -use crate::{state::CONFIG, util::msg::build_instantiate_warp_account_msg, ContractError}; +use crate::{ + contract::REPLY_ID_CREATE_FUNDING_ACCOUNT, state::CONFIG, + util::msg::build_instantiate_warp_account_msg, ContractError, +}; pub fn create_funding_account( deps: DepsMut, @@ -11,17 +14,22 @@ pub fn create_funding_account( ) -> Result { let config = CONFIG.load(deps.storage)?; - let msgs = vec![build_instantiate_warp_account_msg( - Uint64::from(0u64), // placeholder - env.contract.address.to_string(), - config.warp_account_code_id.u64(), - info.sender.to_string(), - info.funds, - None, - None, - )]; + let submsgs = vec![SubMsg { + id: REPLY_ID_CREATE_FUNDING_ACCOUNT, + msg: build_instantiate_warp_account_msg( + Uint64::from(0u64), // placeholder + env.contract.address.to_string(), + config.warp_account_code_id.u64(), + info.sender.to_string(), + info.funds, + None, + None, + ), + gas_limit: None, + reply_on: ReplyOn::Always, + }]; Ok(Response::new() .add_attribute("action", "create_funding_account") - .add_messages(msgs)) + .add_submessages(submsgs)) } diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index 6c0586a1..b617b42a 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -1,4 +1,4 @@ -use cosmwasm_std::{Coin, CosmosMsg, DepsMut, Env, Reply, Response, StdError}; +use cosmwasm_std::{Coin, CosmosMsg, DepsMut, Env, Reply, Response, StdError, Uint64}; use controller::{ account::{CwFund, WarpMsg}, @@ -8,8 +8,9 @@ use controller::{ use crate::{ state::JobQueue, util::msg::{ - build_account_execute_warp_msgs, build_take_funding_account_msg, - build_take_job_account_msg, build_transfer_cw20_msg, build_transfer_cw721_msg, + build_account_execute_warp_msgs, build_free_funding_account_msg, + build_take_funding_account_msg, build_take_job_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, }, ContractError, }; @@ -223,3 +224,65 @@ pub fn create_funding_account_and_job( .add_attribute("funding_account_address", funding_account_addr) .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) } + +pub fn create_funding_account( + deps: DepsMut, + _env: Env, + msg: Reply, + config: Config, +) -> Result { + let reply = msg.result.into_result().map_err(StdError::generic_err)?; + + let funding_account_event = reply + .events + .iter() + .find(|event| { + event + .attributes + .iter() + .any(|attr| attr.key == "action" && attr.value == "instantiate") + }) + .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; + + let owner = funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "owner") + .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? + .value; + + let funding_account_addr = deps.api.addr_validate( + &funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "contract_addr") + .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? + .value, + )?; + + let native_funds: Vec = serde_json_wasm::from_str( + &funding_account_event + .attributes + .iter() + .cloned() + .find(|attr| attr.key == "native_funds") + .ok_or_else(|| StdError::generic_err("cannot find `native_funds` attribute"))? + .value, + )?; + + let msgs: Vec = vec![build_free_funding_account_msg( + config.account_tracker_address.to_string(), + owner.to_string(), + funding_account_addr.to_string(), + Uint64::from(0u64), // placeholder, + )]; + + Ok(Response::new() + .add_messages(msgs) + .add_attribute("action", "create_funding_account_reply") + .add_attribute("owner", owner) + .add_attribute("funding_account_address", funding_account_addr) + .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) +} diff --git a/refs.json b/refs.json index b173e3ad..4ed8cc89 100644 --- a/refs.json +++ b/refs.json @@ -12,7 +12,7 @@ "codeId": "12397" }, "warp-controller": { - "codeId": "12409", + "codeId": "12426", "address": "terra16zlwx7yeugjpzvdw2cs8v7zghssmh95thrwuzjf56u4g6xu0udpqqctnxa" }, "warp-resolver": { @@ -24,7 +24,7 @@ "address": "terra1cp3lf3qf5gtmv30stprmrt7ye03j5t6nazhe45rzk6nuz2ka83ks9an6gy" }, "warp-account-tracker": { - "codeId": "12401", + "codeId": "12428", "address": "terra15evxsh7hxdpwvpd6c6t9hn3q6r6q6pfxq8nz27kpp56nusqfyeuqpc0jjp" } }, From afded955cf46662e62c7a12d960534a072727b6d Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 18 Dec 2023 15:31:12 +0100 Subject: [PATCH 119/133] revert to execute_job submsg id as job_id not static --- contracts/warp-controller/src/contract.rs | 8 +++--- contracts/warp-controller/src/execute/job.rs | 4 +-- contracts/warp-controller/src/reply/job.rs | 27 +------------------- refs.json | 2 +- 4 files changed, 8 insertions(+), 33 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index 0e5423cb..fde87f1b 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -21,7 +21,8 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { let state = State { - current_job_id: Uint64::one(), + // first 10 slots reserved for reply calls + current_job_id: Uint64::new(10u64), q: Uint64::zero(), }; @@ -149,11 +150,11 @@ pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result Result { @@ -172,7 +173,6 @@ pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> Result { reply::job::instantiate_sub_contracts(deps, env, msg, config) } - REPLY_ID_EXECUTE_JOB => reply::job::execute_job(deps, env, msg, config), - _ => panic!(), + _ => reply::job::execute_job(deps, env, msg, config), } } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 2d6789da..1f3780fc 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -1,4 +1,4 @@ -use crate::contract::{REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB, REPLY_ID_EXECUTE_JOB}; +use crate::contract::REPLY_ID_CREATE_JOB_ACCOUNT_AND_JOB; use crate::state::{JobQueue, STATE}; use crate::util::msg::{ build_account_execute_generic_msgs, build_account_execute_warp_msgs, @@ -472,7 +472,7 @@ pub fn execute_job( match resolution { Ok(true) => { submsgs.push(SubMsg { - id: REPLY_ID_EXECUTE_JOB, + id: data.id.u64(), msg: CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: job.account.to_string(), msg: to_binary(&account::ExecuteMsg::WarpMsgs(WarpMsgs { diff --git a/contracts/warp-controller/src/reply/job.rs b/contracts/warp-controller/src/reply/job.rs index 1411b6c8..ee4880eb 100644 --- a/contracts/warp-controller/src/reply/job.rs +++ b/contracts/warp-controller/src/reply/job.rs @@ -32,32 +32,7 @@ pub fn execute_job( SubMsgResult::Err(_) => JobStatus::Failed, }; - let reply: cosmwasm_std::SubMsgResponse = msg - .result - .clone() - .into_result() - .map_err(StdError::generic_err)?; - - let warp_msgs_event = reply - .events - .iter() - .find(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "action" && attr.value == "warp_msgs") - }) - .ok_or_else(|| StdError::generic_err("cannot find `warp_msgs` event"))?; - - let job_id_str = warp_msgs_event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "job_id") - .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? - .value; - - let job_id = job_id_str.as_str().parse::()?; + let job_id = msg.id; let finished_job = JobQueue::finalize(deps.storage, env.clone(), job_id, new_status)?; diff --git a/refs.json b/refs.json index 4ed8cc89..af6d5143 100644 --- a/refs.json +++ b/refs.json @@ -12,7 +12,7 @@ "codeId": "12397" }, "warp-controller": { - "codeId": "12426", + "codeId": "12472", "address": "terra16zlwx7yeugjpzvdw2cs8v7zghssmh95thrwuzjf56u4g6xu0udpqqctnxa" }, "warp-resolver": { From e000eabd6064b4ab9013fe57366e4d4f114fd47b Mon Sep 17 00:00:00 2001 From: simke9445 Date: Mon, 18 Dec 2023 15:43:54 +0100 Subject: [PATCH 120/133] new testnet contracts --- refs.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/refs.json b/refs.json index af6d5143..d5bd232d 100644 --- a/refs.json +++ b/refs.json @@ -9,23 +9,23 @@ }, "testnet": { "warp-account": { - "codeId": "12397" + "codeId": "12473" }, "warp-controller": { - "codeId": "12472", - "address": "terra16zlwx7yeugjpzvdw2cs8v7zghssmh95thrwuzjf56u4g6xu0udpqqctnxa" + "codeId": "12476", + "address": "terra1r6crnz2j0zkcclhtzqm74l5n7p3pk27fk3uy4cg3pkr45y3pzjtsg3zzmt" }, "warp-resolver": { - "codeId": "12398", - "address": "terra12sm42c7g0733zullajvpczazpgtj4w0ndma3yta38840d3sqg5cswv0drc" + "codeId": "12474", + "address": "terra1xpewwp0fput9xx8wyk5cglketkke27kgyeykzscafk4sgna4jp5qgzwmmz" }, "warp-templates": { - "codeId": "12399", - "address": "terra1cp3lf3qf5gtmv30stprmrt7ye03j5t6nazhe45rzk6nuz2ka83ks9an6gy" + "codeId": "12475", + "address": "terra1pn6v2jx4m95rdwq3jrjg7dgf3fjv0w506s45jdhskgzc9s5l23dsn2y7pw" }, "warp-account-tracker": { - "codeId": "12428", - "address": "terra15evxsh7hxdpwvpd6c6t9hn3q6r6q6pfxq8nz27kpp56nusqfyeuqpc0jjp" + "codeId": "12477", + "address": "terra1qgexzlztyl49q8at7y6jxjagan8afmkg0aake0z36tq90ckmf2psh39dsy" } }, "mainnet": { From aeb629b6b9d8376455c4a0904695899fc16af356 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 9 Jan 2024 17:09:55 +0100 Subject: [PATCH 121/133] add free funding account in evict_job --- contracts/warp-controller/src/execute/job.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 1f3780fc..d7a61cc6 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -587,6 +587,15 @@ pub fn evict_job( job.id, )); + if let Some(funding_account) = job.funding_account { + msgs.push(build_free_funding_account_msg( + config.account_tracker_address.to_string(), + job.owner.to_string(), + funding_account.to_string(), + job.id, + )); + } + Ok(Response::new() .add_messages(msgs) .add_attribute("action", "evict_job") From c01733e8e69a0ab4c7bdc59d0ced0a03ad2eb8e1 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Tue, 9 Jan 2024 17:15:46 +0100 Subject: [PATCH 122/133] fix state cleanup on evict_job --- contracts/warp-controller/src/execute/job.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index d7a61cc6..1db33b47 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -570,9 +570,9 @@ pub fn evict_job( vec![Coin::new(eviction_fee.u128(), config.fee_denom.clone())], )); - // Controller sends execution reward minus eviction reward back to account + // Controller sends execution reward minus eviction reward back to owner msgs.push(build_transfer_native_funds_msg( - job.account.to_string(), + job.owner.to_string(), vec![Coin::new( (job.reward - eviction_fee).u128(), config.fee_denom.clone(), @@ -596,6 +596,12 @@ pub fn evict_job( )); } + // Job owner withdraw all assets that are listed from warp account to itself + msgs.push(build_account_withdraw_assets_msg( + account_addr.to_string(), + job.assets_to_withdraw, + )); + Ok(Response::new() .add_messages(msgs) .add_attribute("action", "evict_job") From ae8c020c8a2a07ae54fd3b4b8d7d25a49794edac Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 12 Jan 2024 15:13:17 +0100 Subject: [PATCH 123/133] fix vulnerability with operational amount when funding account is not provided --- contracts/warp-controller/src/execute/job.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 1db33b47..6b6f9afc 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -74,6 +74,23 @@ pub fn create_job( return Err(ContractError::FundingAccountMissingForRecurringJob {}); } + if data.funding_account.is_none() { + if data.operational_amount < total_fees + data.reward { + return Err(ContractError::InsufficientOperationalFunds {}); + } + + let fee_denom_paid_amount = info + .funds + .iter() + .find(|f| f.denom == config.fee_denom) + .unwrap() + .amount; + + if fee_denom_paid_amount < data.operational_amount { + return Err(ContractError::InsufficientFundsToPayForRewardAndFee {}); + } + } + // ignore operational_amount when funding_account is provided let operational_amount = if data.funding_account.is_some() { Uint128::zero() From b82d86133eb2742d61b65c246e4c0bb86f9053b2 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 19 Jan 2024 17:55:46 +0100 Subject: [PATCH 124/133] remove account_msgs usage in reply --- .../warp-controller/src/reply/account.rs | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index b617b42a..a590c23c 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -1,16 +1,12 @@ use cosmwasm_std::{Coin, CosmosMsg, DepsMut, Env, Reply, Response, StdError, Uint64}; -use controller::{ - account::{CwFund, WarpMsg}, - Config, -}; +use controller::{account::CwFund, Config}; use crate::{ state::JobQueue, util::msg::{ - build_account_execute_warp_msgs, build_free_funding_account_msg, - build_take_funding_account_msg, build_take_job_account_msg, build_transfer_cw20_msg, - build_transfer_cw721_msg, + build_free_funding_account_msg, build_take_funding_account_msg, build_take_job_account_msg, + build_transfer_cw20_msg, build_transfer_cw721_msg, }, ContractError, }; @@ -81,16 +77,6 @@ pub fn create_account_and_job( .value, )?; - let account_msgs: Option> = serde_json_wasm::from_str( - &account_event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "account_msgs") - .ok_or_else(|| StdError::generic_err("cannot find `account_msgs` attribute"))? - .value, - )?; - let mut job = JobQueue::get(deps.storage, job_id)?; job.account = account_addr.clone(); JobQueue::sync(deps.storage, env, job.clone())?; @@ -120,14 +106,6 @@ pub fn create_account_and_job( } } - if let Some(account_msgs) = account_msgs { - // Account execute msgs - msgs.push(build_account_execute_warp_msgs( - account_addr.to_string(), - account_msgs, - )); - } - // Take job account msgs.push(build_take_job_account_msg( config.account_tracker_address.to_string(), From 7f046ac9e65e8613a9e12f079631489ebc1eed5a Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 25 Jan 2024 19:12:06 +0100 Subject: [PATCH 125/133] add account-tracker admin override --- .../warp-account-tracker/src/contract.rs | 4 ++- .../src/execute/config.rs | 28 +++++++++++++++++++ .../warp-account-tracker/src/execute/mod.rs | 1 + packages/account-tracker/src/lib.rs | 6 ++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 contracts/warp-account-tracker/src/execute/config.rs diff --git a/contracts/warp-account-tracker/src/contract.rs b/contracts/warp-account-tracker/src/contract.rs index ea689168..99732274 100644 --- a/contracts/warp-account-tracker/src/contract.rs +++ b/contracts/warp-account-tracker/src/contract.rs @@ -1,3 +1,4 @@ +use crate::execute::config::update_config; use crate::state::CONFIG; use crate::{execute, query, ContractError}; use account_tracker::{Config, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; @@ -34,7 +35,7 @@ pub fn instantiate( #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, - _env: Env, + env: Env, info: MessageInfo, msg: ExecuteMsg, ) -> Result { @@ -60,6 +61,7 @@ pub fn execute( nonpayable(&info).unwrap(); execute::account::free_funding_account(deps, data) } + ExecuteMsg::UpdateConfig(data) => update_config(deps, env, info, data), } } diff --git a/contracts/warp-account-tracker/src/execute/config.rs b/contracts/warp-account-tracker/src/execute/config.rs new file mode 100644 index 00000000..ed0640f4 --- /dev/null +++ b/contracts/warp-account-tracker/src/execute/config.rs @@ -0,0 +1,28 @@ +use account_tracker::UpdateConfigMsg; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; + +use crate::{state::CONFIG, ContractError}; + +pub fn update_config( + deps: DepsMut, + _env: Env, + info: MessageInfo, + data: UpdateConfigMsg, +) -> Result { + let mut config = CONFIG.load(deps.storage)?; + + if info.sender != config.admin { + return Err(ContractError::Unauthorized {}); + } + + config.admin = match data.admin { + None => config.admin, + Some(data) => deps.api.addr_validate(data.as_str())?, + }; + + CONFIG.save(deps.storage, &config)?; + + Ok(Response::new() + .add_attribute("action", "update_config") + .add_attribute("config_admin", config.admin)) +} diff --git a/contracts/warp-account-tracker/src/execute/mod.rs b/contracts/warp-account-tracker/src/execute/mod.rs index d937534a..6038438e 100644 --- a/contracts/warp-account-tracker/src/execute/mod.rs +++ b/contracts/warp-account-tracker/src/execute/mod.rs @@ -1 +1,2 @@ pub(crate) mod account; +pub(crate) mod config; diff --git a/packages/account-tracker/src/lib.rs b/packages/account-tracker/src/lib.rs index 29f4154e..028100ab 100644 --- a/packages/account-tracker/src/lib.rs +++ b/packages/account-tracker/src/lib.rs @@ -34,6 +34,12 @@ pub enum ExecuteMsg { FreeJobAccount(FreeJobAccountMsg), TakeFundingAccount(TakeFundingAccountMsg), FreeFundingAccount(FreeFundingAccountMsg), + UpdateConfig(UpdateConfigMsg), +} + +#[cw_serde] +pub struct UpdateConfigMsg { + pub admin: Option, } #[cw_serde] From 86e8e2a51a55313749c3e43241aadee81c5416c5 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 25 Jan 2024 19:29:50 +0100 Subject: [PATCH 126/133] add config validations --- contracts/warp-controller/src/contract.rs | 13 ++++++++++++- contracts/warp-controller/src/error.rs | 6 ++++++ contracts/warp-controller/src/execute/controller.rs | 12 +++++++++++- contracts/warp-controller/src/execute/job.rs | 4 ++++ packages/controller/src/lib.rs | 3 +++ 5 files changed, 36 insertions(+), 2 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index fde87f1b..aeda93c0 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -46,6 +46,7 @@ pub fn instantiate( maintenance_fee_max: msg.maintenance_fee_max, duration_days_min: msg.duration_days_min, duration_days_max: msg.duration_days_max, + duration_days_limit: msg.duration_days_limit, queue_size_left: msg.queue_size_left, queue_size_right: msg.queue_size_right, burn_fee_rate: msg.burn_fee_rate, @@ -60,7 +61,7 @@ pub fn instantiate( return Err(ContractError::MaintenanceMaxFeeUnderMinFee {}); } - if config.duration_days_max < config.duration_days_min { + if config.duration_days_max <= config.duration_days_min { return Err(ContractError::DurationMaxDaysUnderMinDays {}); } @@ -72,6 +73,16 @@ pub fn instantiate( return Err(ContractError::BurnFeeTooHigh {}); } + if config.queue_size_right <= config.queue_size_left { + return Err(ContractError::QueueSizeRightUnderQueueSizeLeft {}); + } + + if config.duration_days_max > config.duration_days_limit + || config.duration_days_min > config.duration_days_limit + { + return Err(ContractError::DurationDaysLimit {}); + } + STATE.save(deps.storage, &state)?; CONFIG.save(deps.storage, &config)?; diff --git a/contracts/warp-controller/src/error.rs b/contracts/warp-controller/src/error.rs index dec1202d..3e5f136f 100644 --- a/contracts/warp-controller/src/error.rs +++ b/contracts/warp-controller/src/error.rs @@ -36,6 +36,9 @@ pub enum ContractError { #[error("Name cannot exceed 280 characters")] NameTooLong {}, + #[error("Duration days exceeds limit.")] + DurationDaysLimit {}, + #[error("Attempting to distribute more rewards than received from the action")] DistributingMoreRewardThanReceived {}, @@ -94,6 +97,9 @@ pub enum ContractError { #[error("Max duration days smaller than minimum duration days.")] DurationMaxDaysUnderMinDays {}, + #[error("Queue size right smaller than queue size left.")] + QueueSizeRightUnderQueueSizeLeft {}, + #[error("Eviction period not elapsed.")] EvictionPeriodNotElapsed {}, diff --git a/contracts/warp-controller/src/execute/controller.rs b/contracts/warp-controller/src/execute/controller.rs index fe0dab92..67b21f20 100644 --- a/contracts/warp-controller/src/execute/controller.rs +++ b/contracts/warp-controller/src/execute/controller.rs @@ -56,7 +56,7 @@ pub fn update_config( return Err(ContractError::MaintenanceMaxFeeUnderMinFee {}); } - if config.duration_days_max < config.duration_days_min { + if config.duration_days_max <= config.duration_days_min { return Err(ContractError::DurationMaxDaysUnderMinDays {}); } @@ -68,6 +68,16 @@ pub fn update_config( return Err(ContractError::BurnFeeTooHigh {}); } + if config.queue_size_right <= config.queue_size_left { + return Err(ContractError::QueueSizeRightUnderQueueSizeLeft {}); + } + + if config.duration_days_max > config.duration_days_limit + || config.duration_days_min > config.duration_days_limit + { + return Err(ContractError::DurationDaysLimit {}); + } + CONFIG.save(deps.storage, &config)?; Ok(Response::new() diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 6b6f9afc..2a71e5c7 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -50,6 +50,10 @@ pub fn create_job( return Err(ContractError::RewardTooSmall {}); } + if data.duration_days > config.duration_days_limit { + return Err(ContractError::DurationDaysLimit {}); + } + let state = STATE.load(deps.storage)?; let job_owner = info.sender.clone(); diff --git a/packages/controller/src/lib.rs b/packages/controller/src/lib.rs index 093c2431..23eb3073 100644 --- a/packages/controller/src/lib.rs +++ b/packages/controller/src/lib.rs @@ -30,6 +30,7 @@ pub struct Config { // duration_days fn interval [left, right] pub duration_days_min: Uint64, pub duration_days_max: Uint64, + pub duration_days_limit: Uint64, // queue_size fn interval [left, right] pub queue_size_left: Uint64, pub queue_size_right: Uint64, @@ -62,6 +63,7 @@ pub struct InstantiateMsg { // duration_days fn interval [left, right] pub duration_days_min: Uint64, pub duration_days_max: Uint64, + pub duration_days_limit: Uint64, // queue_size fn interval [left, right] pub queue_size_left: Uint64, pub queue_size_right: Uint64, @@ -102,6 +104,7 @@ pub struct UpdateConfigMsg { // duration_days fn interval [left, right] pub duration_days_min: Option, pub duration_days_max: Option, + pub duration_days_limit: Option, // queue_size fn interval [left, right] pub queue_size_left: Option, pub queue_size_right: Option, From ae4f47cdbd5d2a12af5f63ce86e6572f5bb4bdd3 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 25 Jan 2024 19:31:33 +0100 Subject: [PATCH 127/133] remove repeated ops --- contracts/warp-controller/src/execute/controller.rs | 4 ---- contracts/warp-controller/src/execute/job.rs | 2 -- 2 files changed, 6 deletions(-) diff --git a/contracts/warp-controller/src/execute/controller.rs b/contracts/warp-controller/src/execute/controller.rs index 67b21f20..54eeef56 100644 --- a/contracts/warp-controller/src/execute/controller.rs +++ b/contracts/warp-controller/src/execute/controller.rs @@ -64,10 +64,6 @@ pub fn update_config( return Err(ContractError::CancellationFeeTooHigh {}); } - if config.burn_fee_rate.u128() > 100 { - return Err(ContractError::BurnFeeTooHigh {}); - } - if config.queue_size_right <= config.queue_size_left { return Err(ContractError::QueueSizeRightUnderQueueSizeLeft {}); } diff --git a/contracts/warp-controller/src/execute/job.rs b/contracts/warp-controller/src/execute/job.rs index 2a71e5c7..36963c83 100644 --- a/contracts/warp-controller/src/execute/job.rs +++ b/contracts/warp-controller/src/execute/job.rs @@ -113,8 +113,6 @@ pub fn create_job( let mut msgs = vec![]; let mut attrs = vec![]; - let state = STATE.load(deps.storage)?; - let mut job = JobQueue::add( deps.storage, Job { From 1b4a7fb014afd99df7d9f201c696b33898930853 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Thu, 25 Jan 2024 19:39:07 +0100 Subject: [PATCH 128/133] remove unused code --- contracts/warp-controller/src/contract.rs | 4 - .../warp-controller/src/reply/account.rs | 80 +------------------ 2 files changed, 2 insertions(+), 82 deletions(-) diff --git a/contracts/warp-controller/src/contract.rs b/contracts/warp-controller/src/contract.rs index aeda93c0..33d6fdfb 100644 --- a/contracts/warp-controller/src/contract.rs +++ b/contracts/warp-controller/src/contract.rs @@ -163,7 +163,6 @@ pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> Result Result { reply::account::create_account_and_job(deps, env, msg, config) } - REPLY_ID_CREATE_FUNDING_ACCOUNT_AND_JOB => { - reply::account::create_funding_account_and_job(deps, env, msg, config) - } REPLY_ID_CREATE_FUNDING_ACCOUNT => { reply::account::create_funding_account(deps, env, msg, config) } diff --git a/contracts/warp-controller/src/reply/account.rs b/contracts/warp-controller/src/reply/account.rs index a590c23c..5cfedb1e 100644 --- a/contracts/warp-controller/src/reply/account.rs +++ b/contracts/warp-controller/src/reply/account.rs @@ -5,8 +5,8 @@ use controller::{account::CwFund, Config}; use crate::{ state::JobQueue, util::msg::{ - build_free_funding_account_msg, build_take_funding_account_msg, build_take_job_account_msg, - build_transfer_cw20_msg, build_transfer_cw721_msg, + build_free_funding_account_msg, build_take_job_account_msg, build_transfer_cw20_msg, + build_transfer_cw721_msg, }, ContractError, }; @@ -127,82 +127,6 @@ pub fn create_account_and_job( )) } -pub fn create_funding_account_and_job( - deps: DepsMut, - env: Env, - msg: Reply, - config: Config, -) -> Result { - let reply = msg.result.into_result().map_err(StdError::generic_err)?; - - let funding_account_event = reply - .events - .iter() - .find(|event| { - event - .attributes - .iter() - .any(|attr| attr.key == "action" && attr.value == "instantiate") - }) - .ok_or_else(|| StdError::generic_err("cannot find `instantiate` event"))?; - - let job_id_str = funding_account_event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "job_id") - .ok_or_else(|| StdError::generic_err("cannot find `job_id` attribute"))? - .value; - let job_id = job_id_str.as_str().parse::()?; - - let owner = funding_account_event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "owner") - .ok_or_else(|| StdError::generic_err("cannot find `owner` attribute"))? - .value; - - let funding_account_addr = deps.api.addr_validate( - &funding_account_event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "contract_addr") - .ok_or_else(|| StdError::generic_err("cannot find `contract_addr` attribute"))? - .value, - )?; - - let native_funds: Vec = serde_json_wasm::from_str( - &funding_account_event - .attributes - .iter() - .cloned() - .find(|attr| attr.key == "native_funds") - .ok_or_else(|| StdError::generic_err("cannot find `native_funds` attribute"))? - .value, - )?; - - let mut job = JobQueue::get(deps.storage, job_id)?; - job.funding_account = Some(funding_account_addr.clone()); - JobQueue::sync(deps.storage, env, job.clone())?; - - let msgs: Vec = vec![build_take_funding_account_msg( - config.account_tracker_address.to_string(), - job.owner.to_string(), - funding_account_addr.to_string(), - job.id, - )]; - - Ok(Response::new() - .add_messages(msgs) - .add_attribute("action", "create_funding_account_and_job_reply") - .add_attribute("job_id", job_id.to_string()) - .add_attribute("owner", owner) - .add_attribute("funding_account_address", funding_account_addr) - .add_attribute("native_funds", serde_json_wasm::to_string(&native_funds)?)) -} - pub fn create_funding_account( deps: DepsMut, _env: Env, From da7bbe54cf9d658e44723f70f78ad620c108225d Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 26 Jan 2024 16:46:41 +0100 Subject: [PATCH 129/133] add v2 changelog.md --- V2CHANGELOG.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 V2CHANGELOG.md diff --git a/V2CHANGELOG.md b/V2CHANGELOG.md new file mode 100644 index 00000000..33c6d947 --- /dev/null +++ b/V2CHANGELOG.md @@ -0,0 +1,119 @@ +# V2 Changelog + +## Major Updates + +- **User’s Warp-Account Removed:** + - Replaced by job accounts. + - `create_job` now creates a job account on the fly in the same transaction. + - Users can provide funds in `info.funds` that are relayed to the job account. + - Job account is used as a job session throughout job execution. + - In case of recurring jobs, the same job account is kept through time. + - Required for stateful jobs like trading strategies. + +- **Multiple Funding Accounts:** + - Users can create and manage multiple funding accounts. + - Funding account is used for distributing rewards to keepers and paying fees. + - Otherwise, fees are subtracted from `info.funds`. + - Useful for recurring jobs to provide fees and topups on the side. + +- **New Fee Mechanism:** + - Introduces a more dynamic and flexible fee calculation system. + - Creation Fee: Calculated based on the queue size. + - Maintenance Fee: Determined based on the duration in days. + - Burn Fee: Computed from the job reward. + - `total_fees = creation_fee + maintenance_fee + burn_fee` + - `job cost = total_fees + reward` + +- **New Contract Warp-Account-Tracker:** + - Used for management of job accounts and funding accounts. + - Holds state for taken and free accounts by job_id. + +## API Changes + +### Removed +- `create_job.msgs` +- `create_job.condition` +- `create_job.requeue_on_evict` + +### Added +- `create_job.executions` + - Array of executions that operate like a switch. + - Single execution contains msgs (warp msgs) and condition. + - On job execution, the first execution condition that returns true top-down is taken. +- **Job Accounts with WarpMsg Struct:** + - Job accounts now operate with WarpMsg struct. + - Previously, warp accounts worked only with cosmos msgs. + - WarpMsg:Generic is equivalent to a cosmos msg. + - Added support for WarpMsg:WithdrawAssets and WarpMsg:IbcTransfer. + - Extensible messaging standard within warp in case custom message formats are needed in the future. +- `create_job.operational_amount` + - Without funding account: `operational_amount` needs to equal `total_fees + reward`. + - With funding account: Ignored, can be set to 0. +- `create_job.duration_days` + - Defines job length of stay in the warp queue. + - Maintenance fee paid upfront for it based on fee calculations. +- `create_job.cw_funds` + - Optionally passed list of cw20 and cw721 funds to be sent from user to job account. +- `create_job.funding_account` + - Optionally attached funding account from which job fees and rewards are deducted. + - Required for recurring jobs. + - Optionally provided for one-time jobs. +- `create_job.account_msgs` + - Messages that are executed via job-account on job creation. + - Useful for deploying funds to money markets to earn APR while the job waits for execution. +- **Controller.create_funding_account API:** + - Creates a new free funding account for the user. +- **FnValue StringValue Support:** + - Static variable can be initialized with an `init_fn`. + - FnValue now supports `StringValue`. + +## Fee functions + +### Creation Fee + +The creation fee (`f(qs)`) is a piecewise function depending on the queue size (`qs`). + +``` +f(qs) = + y1, if qs < x1 + slope * qs + y1 - slope * x1, if x1 <= qs < x2 + y2, if qs >= x2 +``` + +Where: +- `x1` = `config.queue_size_left` +- `x2` = `config.queue_size_right` +- `y1` = `config.creation_fee_min` +- `y2` = `config.creation_fee_max` +- `slope` = `(y2 - y1) / (x2 - x1)` + +### Maintenance Fee + +The maintenance fee (`g(dd)`) is structured similarly, based on the duration in days (`dd`). + +``` +g(dd) = + y1, if dd < x1 + slope * dd + y1 - slope * x1, if x1 <= dd < x2 + y2, if dd >= x2 +``` + +Where: +- `x1` = `config.duration_days_min` +- `x2` = `config.duration_days_max` +- `slope` = `(y2 - y1) / (x2 - x1)` + +### Burn Fee + +The burn fee (`h(job_reward)`) is calculated as the maximum between the `calculated_fee` and `min_fee`. + +``` +h(job_reward) = + max(calculated_fee, min_fee) +``` + +Where: +- `calculated_fee` = `job_reward * config.burn_fee_rate / 100` +- `min_fee` = `config.burn_fee_min` + + \ No newline at end of file From 80d8ba472bdb2d66ef38bf5762ec6a5e2db35650 Mon Sep 17 00:00:00 2001 From: simke9445 Date: Fri, 2 Feb 2024 15:20:04 +0100 Subject: [PATCH 130/133] update deployment script + new testnet contracts --- refs.json | 18 +++++++++--------- tasks/deploy_warp.ts | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/refs.json b/refs.json index d5bd232d..f662f32d 100644 --- a/refs.json +++ b/refs.json @@ -9,23 +9,23 @@ }, "testnet": { "warp-account": { - "codeId": "12473" + "codeId": "12858" }, "warp-controller": { - "codeId": "12476", - "address": "terra1r6crnz2j0zkcclhtzqm74l5n7p3pk27fk3uy4cg3pkr45y3pzjtsg3zzmt" + "codeId": "12861", + "address": "terra1mmsl3mxq9n8a6dgye05pn0qlup7r24e2vyjkqgpe32pv3ehjgnes0jz5nc" }, "warp-resolver": { - "codeId": "12474", - "address": "terra1xpewwp0fput9xx8wyk5cglketkke27kgyeykzscafk4sgna4jp5qgzwmmz" + "codeId": "12859", + "address": "terra1kjv3e7v7m03kk8lrjqr2j604vusxrpxadg6xjz89jucladh5m5gqqag8q7" }, "warp-templates": { - "codeId": "12475", - "address": "terra1pn6v2jx4m95rdwq3jrjg7dgf3fjv0w506s45jdhskgzc9s5l23dsn2y7pw" + "codeId": "12860", + "address": "terra155wp5wwvquqzg30r6luu4e9d95p7pexe3xjszhflcsqe5gpayd6smz5w6k" }, "warp-account-tracker": { - "codeId": "12477", - "address": "terra1qgexzlztyl49q8at7y6jxjagan8afmkg0aake0z36tq90ckmf2psh39dsy" + "codeId": "12862", + "address": "terra15yefd9r33wad527jrxphef8r0jr7n4chg4ehgq0lmrwsfsflaajq5ps2jz" } }, "mainnet": { diff --git a/tasks/deploy_warp.ts b/tasks/deploy_warp.ts index 49855c40..50391fcb 100644 --- a/tasks/deploy_warp.ts +++ b/tasks/deploy_warp.ts @@ -55,6 +55,7 @@ task(async ({ deployer, signer, refs }) => { maintenance_fee_max: "10000000", duration_days_min: "10", duration_days_max: "100", + duration_days_limit: "180", queue_size_left: "5000", queue_size_right: "50000", burn_fee_rate: "25", From 8d62cea5cd905e9e81c0d5e9b8de325b87278e1e Mon Sep 17 00:00:00 2001 From: Vlad J Date: Thu, 8 Feb 2024 08:58:43 -0500 Subject: [PATCH 131/133] Halborn audit --- ...curity_Assessment_Report_Halborn_Final.pdf | Bin 0 -> 1317309 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Terra_Warp_Contracts_CosmWasm_Smart_Contract_Security_Assessment_Report_Halborn_Final.pdf diff --git a/Terra_Warp_Contracts_CosmWasm_Smart_Contract_Security_Assessment_Report_Halborn_Final.pdf b/Terra_Warp_Contracts_CosmWasm_Smart_Contract_Security_Assessment_Report_Halborn_Final.pdf new file mode 100644 index 0000000000000000000000000000000000000000..0ba8c25f48a306c0386d598d307b912392728802 GIT binary patch literal 1317309 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

Yc;UM(kDN( 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�qnzw}+Y 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�&^r$0ykPnMlTifSr*M$cbxwcC)`i>-3 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%0D%S#YJBdSi$z2*KxC-ewaF~C2Z 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%0A6WhQS$4g9jFz%>d~s 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%0D0R@j8dH^uDzC)kc^s24K_B&3;k7V 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%0D#QUoQ{$el>w{gw{>iJg*f0M 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