diff --git a/contracts/cw1-subkeys/examples/schema.rs b/contracts/cw1-subkeys/examples/schema.rs index fa69da81f..bd528e6af 100644 --- a/contracts/cw1-subkeys/examples/schema.rs +++ b/contracts/cw1-subkeys/examples/schema.rs @@ -3,7 +3,7 @@ use std::fs::create_dir_all; use cosmwasm_schema::{export_schema, export_schema_with_title, remove_schemas, schema_for}; -use cw1_subkeys::msg::{HandleMsg, QueryMsg}; +use cw1_subkeys::msg::{AllAllowancesResponse, HandleMsg, QueryMsg}; use cw1_subkeys::state::Allowance; use cw1_whitelist::msg::{AdminListResponse, InitMsg}; @@ -18,4 +18,5 @@ fn main() { export_schema_with_title(&mut schema_for!(QueryMsg), &out_dir, "QueryMsg"); export_schema(&schema_for!(Allowance), &out_dir); export_schema(&schema_for!(AdminListResponse), &out_dir); + export_schema(&schema_for!(AllAllowancesResponse), &out_dir); } diff --git a/contracts/cw1-subkeys/schema/all_allowances_response.json b/contracts/cw1-subkeys/schema/all_allowances_response.json new file mode 100644 index 000000000..1cfbc42d3 --- /dev/null +++ b/contracts/cw1-subkeys/schema/all_allowances_response.json @@ -0,0 +1,108 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AllAllowancesResponse", + "type": "object", + "required": [ + "allowances" + ], + "properties": { + "allowances": { + "type": "array", + "items": { + "$ref": "#/definitions/AllowanceInfo" + } + } + }, + "definitions": { + "AllowanceInfo": { + "type": "object", + "required": [ + "balance", + "expires", + "spender" + ], + "properties": { + "balance": { + "$ref": "#/definitions/Balance" + }, + "expires": { + "$ref": "#/definitions/Expiration" + }, + "spender": { + "$ref": "#/definitions/HumanAddr" + } + } + }, + "Balance": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Expiration": { + "anyOf": [ + { + "description": "AtHeight will expire when `env.block.height` >= height", + "type": "object", + "required": [ + "at_height" + ], + "properties": { + "at_height": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "AtTime will expire when `env.block.time` >= time", + "type": "object", + "required": [ + "at_time" + ], + "properties": { + "at_time": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + { + "description": "Never will never expire. Used to express the empty variant", + "type": "object", + "required": [ + "never" + ], + "properties": { + "never": { + "type": "object" + } + } + } + ] + }, + "HumanAddr": { + "type": "string" + }, + "Uint128": { + "type": "string" + } + } +} diff --git a/contracts/cw1-subkeys/schema/query_msg.json b/contracts/cw1-subkeys/schema/query_msg.json index 132423e29..d57372d2e 100644 --- a/contracts/cw1-subkeys/schema/query_msg.json +++ b/contracts/cw1-subkeys/schema/query_msg.json @@ -57,6 +57,38 @@ } } } + }, + { + "description": "Gets all Allowances for this contract Returns AllAllowancesResponse", + "type": "object", + "required": [ + "all_allowances" + ], + "properties": { + "all_allowances": { + "type": "object", + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "anyOf": [ + { + "$ref": "#/definitions/HumanAddr" + }, + { + "type": "null" + } + ] + } + } + } + } } ], "definitions": { diff --git a/contracts/cw1-subkeys/src/contract.rs b/contracts/cw1-subkeys/src/contract.rs index 4e61a5064..4d0822066 100644 --- a/contracts/cw1-subkeys/src/contract.rs +++ b/contracts/cw1-subkeys/src/contract.rs @@ -2,8 +2,8 @@ use schemars::JsonSchema; use std::fmt; use cosmwasm_std::{ - log, to_binary, Api, BankMsg, Binary, Coin, CosmosMsg, Empty, Env, Extern, HandleResponse, - HumanAddr, InitResponse, Querier, StdError, StdResult, Storage, + log, to_binary, Api, BankMsg, Binary, CanonicalAddr, Coin, CosmosMsg, Empty, Env, Extern, + HandleResponse, HumanAddr, InitResponse, Order, Querier, StdError, StdResult, Storage, }; use cw0::Expiration; use cw1::CanSendResponse; @@ -14,7 +14,7 @@ use cw1_whitelist::{ }; use cw2::{set_contract_version, ContractVersion}; -use crate::msg::{HandleMsg, QueryMsg}; +use crate::msg::{AllAllowancesResponse, AllowanceInfo, HandleMsg, QueryMsg}; use crate::state::{allowances, allowances_read, Allowance}; use std::ops::{AddAssign, Sub}; @@ -207,6 +207,9 @@ pub fn query( QueryMsg::AdminList {} => to_binary(&query_admin_list(deps)?), QueryMsg::Allowance { spender } => to_binary(&query_allowance(deps, spender)?), QueryMsg::CanSend { sender, msg } => to_binary(&query_can_send(deps, sender, msg)?), + QueryMsg::AllAllowances { start_after, limit } => { + to_binary(&query_all_allowances(deps, start_after, limit)?) + } } } @@ -259,6 +262,51 @@ fn can_send( } } +const MAX_LIMIT: u32 = 30; +const DEFAULT_LIMIT: u32 = 10; + +fn calc_limit(request: Option) -> usize { + request.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize +} + +// this will set the first key after the provided key, by appending a 1 byte +fn calc_range_start(start_after: Option) -> Option> { + match start_after { + Some(human) => { + let mut v = Vec::from(human.0); + v.push(1); + Some(v) + } + None => None, + } +} + +// return a list of all allowances here +pub fn query_all_allowances( + deps: &Extern, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = calc_limit(limit); + let range_start = calc_range_start(start_after); + + let api = &deps.api; + let res: StdResult> = allowances_read(&deps.storage) + .range(range_start.as_deref(), None, Order::Ascending) + .take(limit) + .map(|item| { + item.and_then(|(k, allow)| { + Ok(AllowanceInfo { + spender: api.human_address(&CanonicalAddr::from(k))?, + balance: allow.balance, + expires: allow.expires, + }) + }) + }) + .collect(); + Ok(AllAllowancesResponse { allowances: res? }) +} + #[cfg(test)] mod tests { use super::*; @@ -297,7 +345,7 @@ mod tests { } #[test] - fn query_allowances() { + fn query_allowance_works() { let mut deps = mock_dependencies(20, &coins(1111, "token1")); let owner = HumanAddr::from("admin0001"); @@ -351,6 +399,74 @@ mod tests { assert_eq!(allowance, Allowance::default(),); } + #[test] + fn query_all_allowances_works() { + let mut deps = mock_dependencies(20, &coins(1111, "token1")); + + let owner = HumanAddr::from("admin0001"); + let admins = vec![owner.clone(), HumanAddr::from("admin0002")]; + + let spender1 = HumanAddr::from("spender0001"); + let spender2 = HumanAddr::from("spender0002"); + let spender3 = HumanAddr::from("spender0003"); + let initial_spenders = vec![spender1.clone(), spender2.clone(), spender3.clone()]; + + // Same allowances for all spenders, for simplicity + let initial_allowances = coins(1234, "mytoken"); + let expires_later = Expiration::AtHeight(12345); + let initial_expirations = vec![ + Expiration::Never {}, + Expiration::Never {}, + expires_later.clone(), + ]; + + let env = mock_env(owner, &[]); + setup_test_case( + &mut deps, + &env, + &admins, + &initial_spenders, + &initial_allowances, + &initial_expirations, + ); + + // let's try pagination + let allowances = query_all_allowances(&deps, None, Some(2)) + .unwrap() + .allowances; + assert_eq!(2, allowances.len()); + assert_eq!( + allowances[0], + AllowanceInfo { + spender: spender1, + balance: Balance(initial_allowances.clone()), + expires: Expiration::Never {} + } + ); + assert_eq!( + allowances[1], + AllowanceInfo { + spender: spender2.clone(), + balance: Balance(initial_allowances.clone()), + expires: Expiration::Never {} + } + ); + + // now continue from after the last one + let allowances = query_all_allowances(&deps, Some(spender2), Some(2)) + .unwrap() + .allowances; + assert_eq!(1, allowances.len()); + assert_eq!( + allowances[0], + AllowanceInfo { + spender: spender3, + balance: Balance(initial_allowances.clone()), + expires: expires_later, + } + ); + } + #[test] fn update_admins_and_query() { let mut deps = mock_dependencies(20, &coins(1111, "token1")); diff --git a/contracts/cw1-subkeys/src/msg.rs b/contracts/cw1-subkeys/src/msg.rs index 28f664ae5..e6bc10420 100644 --- a/contracts/cw1-subkeys/src/msg.rs +++ b/contracts/cw1-subkeys/src/msg.rs @@ -5,6 +5,8 @@ use std::fmt; use cosmwasm_std::{Coin, CosmosMsg, Empty, HumanAddr}; use cw0::Expiration; +use crate::balance::Balance; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum HandleMsg @@ -54,4 +56,22 @@ where sender: HumanAddr, msg: CosmosMsg, }, + /// Gets all Allowances for this contract + /// Returns AllAllowancesResponse + AllAllowances { + start_after: Option, + limit: Option, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct AllAllowancesResponse { + pub allowances: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct AllowanceInfo { + pub spender: HumanAddr, + pub balance: Balance, + pub expires: Expiration, }