Skip to content

Commit

Permalink
Merge pull request #54 from CosmWasm/get-all-allowances-for-subkeys
Browse files Browse the repository at this point in the history
Add new query to return all allowances on subkeys
  • Loading branch information
ethanfrey authored Aug 26, 2020
2 parents 4877106 + 225348f commit dbcbc9f
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 5 deletions.
3 changes: 2 additions & 1 deletion contracts/cw1-subkeys/examples/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -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);
}
108 changes: 108 additions & 0 deletions contracts/cw1-subkeys/schema/all_allowances_response.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
32 changes: 32 additions & 0 deletions contracts/cw1-subkeys/schema/query_msg.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
124 changes: 120 additions & 4 deletions contracts/cw1-subkeys/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};

Expand Down Expand Up @@ -207,6 +207,9 @@ pub fn query<S: Storage, A: Api, Q: Querier>(
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)?)
}
}
}

Expand Down Expand Up @@ -259,6 +262,51 @@ fn can_send<S: Storage, A: Api, Q: Querier>(
}
}

const MAX_LIMIT: u32 = 30;
const DEFAULT_LIMIT: u32 = 10;

fn calc_limit(request: Option<u32>) -> 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<HumanAddr>) -> Option<Vec<u8>> {
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<S: Storage, A: Api, Q: Querier>(
deps: &Extern<S, A, Q>,
start_after: Option<HumanAddr>,
limit: Option<u32>,
) -> StdResult<AllAllowancesResponse> {
let limit = calc_limit(limit);
let range_start = calc_range_start(start_after);

let api = &deps.api;
let res: StdResult<Vec<AllowanceInfo>> = 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::*;
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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"));
Expand Down
20 changes: 20 additions & 0 deletions contracts/cw1-subkeys/src/msg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = Empty>
Expand Down Expand Up @@ -54,4 +56,22 @@ where
sender: HumanAddr,
msg: CosmosMsg<T>,
},
/// Gets all Allowances for this contract
/// Returns AllAllowancesResponse
AllAllowances {
start_after: Option<HumanAddr>,
limit: Option<u32>,
},
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct AllAllowancesResponse {
pub allowances: Vec<AllowanceInfo>,
}

#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct AllowanceInfo {
pub spender: HumanAddr,
pub balance: Balance,
pub expires: Expiration,
}

0 comments on commit dbcbc9f

Please sign in to comment.