Skip to content

Commit

Permalink
make cw721 claims paginatable with backwards compatibility
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Oct 31, 2024
1 parent 83e9a98 commit 815717d
Show file tree
Hide file tree
Showing 25 changed files with 512 additions and 342 deletions.
20 changes: 17 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ dao-proposal-multiple-v241 = { package = "dao-proposal-multiple", version = "=2.
dao-proposal-single-v241 = { package = "dao-proposal-single", version = "=2.4.1" }
dao-voting-cw4-v241 = { package = "dao-voting-cw4", version = "=2.4.1" }
dao-voting-v241 = { package = "dao-voting", version = "=2.4.1" }

# v2.5.0 dependencies.
cw721-controllers-v250 = { package = "cw721-controllers", version = "=2.5.0" }
82 changes: 4 additions & 78 deletions ci/integration-tests/src/tests/dao_voting_cw721_staked_test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cosm_orc::orchestrator::{ExecReq, SigningKey};
use cosm_orc::orchestrator::SigningKey;
use cosmwasm_std::{Binary, Empty, Uint128};
use cw_utils::Duration;
use test_context::test_context;
Expand Down Expand Up @@ -126,7 +126,9 @@ pub fn claim_nfts(chain: &mut Chain, sender: &SigningKey) {
.execute(
CONTRACT_NAME,
"claim_nfts",
&module::msg::ExecuteMsg::ClaimNfts {},
&module::msg::ExecuteMsg::ClaimNfts {
token_ids: Some(vec![]),
},
sender,
vec![],
)
Expand Down Expand Up @@ -189,79 +191,3 @@ fn cw721_stake_tokens(chain: &mut Chain) {
let voting_power = query_voting_power(chain, &user_addr, None);
assert_eq!(voting_power, Uint128::zero());
}

#[test_context(Chain)]
#[test]
#[ignore]
fn cw721_stake_max_claims_works(chain: &mut Chain) {
use module::state::MAX_CLAIMS;

let user_addr = chain.users["user1"].account.address.clone();
let user_key = chain.users["user1"].key.clone();

let CommonTest { module, .. } =
setup_test(chain, Some(Duration::Height(1)), &user_key, &user_addr);

// Create `MAX_CLAIMS` claims.

// batch_size * 3 = the number of msgs to be batched per tx.
// We cant batch all of the msgs under a single tx because we hit MAX_BLOCK_GAS limits.
let batch_size = 10;
let mut total_msgs = 0;

let mut reqs = vec![];
for i in 0..MAX_CLAIMS {
let token_id = i.to_string();

reqs.push(ExecReq {
contract_name: CW721_NAME.to_string(),
msg: Box::new(cw721_base::ExecuteMsg::Mint::<Empty, Empty> {
token_id: token_id.clone(),
owner: user_addr.to_string(),
token_uri: None,
extension: Empty::default(),
}),
funds: vec![],
});

reqs.push(ExecReq {
contract_name: CW721_NAME.to_string(),
msg: Box::new(cw721::Cw721ExecuteMsg::SendNft {
contract: module.to_string(),
token_id: token_id.clone(),
msg: Binary::default(),
}),
funds: vec![],
});

reqs.push(ExecReq {
contract_name: CONTRACT_NAME.to_string(),
msg: Box::new(module::msg::ExecuteMsg::Unstake {
token_ids: vec![token_id],
}),
funds: vec![],
});

if (i != 0 && i % batch_size == 0) || i == MAX_CLAIMS - 1 {
total_msgs += reqs.len();

chain
.orc
.execute_batch("batch_cw721_stake_max_claims", reqs, &user_key)
.unwrap();

reqs = vec![];
}
}

assert_eq!(total_msgs as u64, MAX_CLAIMS * 3);

chain
.orc
.poll_for_n_blocks(1, core::time::Duration::from_millis(20_000), false)
.unwrap();

// If this works, we're golden. Other tests make sure that the
// NFTs get returned as a result of this.
claim_nfts(chain, &user_key);
}
1 change: 0 additions & 1 deletion contracts/voting/dao-voting-cw721-roles/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ dao-cw721-extensions = { workspace = true }
dao-dao-macros = { workspace = true }
dao-interface = { workspace = true }
cw721-base = { workspace = true, features = ["library"] }
cw721-controllers = { workspace = true }
cw-ownable = { workspace = true }
cw721 = { workspace = true }
cw-utils = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions contracts/voting/dao-voting-cw721-staked/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ cw-hooks = { workspace = true }
cw721 = { workspace = true }
cw721-base = { workspace = true, features = ["library"] }
cw721-controllers = { workspace = true }
cw721-controllers-v250 = { workspace = true }
cw-utils = { workspace = true }
cw2 = { workspace = true }
dao-dao-macros = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,25 @@
"additionalProperties": false
},
{
"description": "Claim NFTs that have been unstaked for the specified duration.",
"description": "Claim NFTs that have been unstaked for the specified duration. If none are provided, it attempts to claim all legacy claims. If token IDs are provided, only those are claimed. If an empty vector is provided, it attempts to claim all non-legacy claims.",
"type": "object",
"required": [
"claim_nfts"
],
"properties": {
"claim_nfts": {
"type": "object",
"properties": {
"token_ids": {
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
},
Expand Down Expand Up @@ -525,6 +536,20 @@
"properties": {
"address": {
"type": "string"
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
Expand Down
94 changes: 75 additions & 19 deletions contracts/voting/dao-voting-cw721-staked/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ use dao_voting::threshold::{
use crate::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, NftContract, QueryMsg};
use crate::state::{
register_staked_nft, register_unstaked_nfts, Config, ACTIVE_THRESHOLD, CONFIG, DAO, HOOKS,
INITIAL_NFTS, MAX_CLAIMS, NFT_BALANCES, NFT_CLAIMS, STAKED_NFTS_PER_OWNER, TOTAL_STAKED_NFTS,
INITIAL_NFTS, LEGACY_NFT_CLAIMS, NFT_BALANCES, NFT_CLAIMS, STAKED_NFTS_PER_OWNER,
TOTAL_STAKED_NFTS,
};
use crate::ContractError;

Expand Down Expand Up @@ -204,7 +205,7 @@ pub fn execute(
match msg {
ExecuteMsg::ReceiveNft(msg) => execute_stake(deps, env, info, msg),
ExecuteMsg::Unstake { token_ids } => execute_unstake(deps, env, info, token_ids),
ExecuteMsg::ClaimNfts {} => execute_claim_nfts(deps, env, info),
ExecuteMsg::ClaimNfts { token_ids } => execute_claim_nfts(deps, env, info, token_ids),
ExecuteMsg::UpdateConfig { duration } => execute_update_config(info, deps, duration),
ExecuteMsg::AddHook { addr } => execute_add_hook(deps, info, addr),
ExecuteMsg::RemoveHook { addr } => execute_remove_hook(deps, info, addr),
Expand Down Expand Up @@ -314,13 +315,6 @@ pub fn execute_unstake(
}

Some(duration) => {
let outstanding_claims = NFT_CLAIMS
.query_claims(deps.as_ref(), &info.sender)?
.nft_claims;
if outstanding_claims.len() + token_ids.len() > MAX_CLAIMS as usize {
return Err(ContractError::TooManyClaims {});
}

// Out of gas here is fine - just try again with fewer
// tokens.
NFT_CLAIMS.create_nft_claims(
Expand All @@ -343,22 +337,57 @@ pub fn execute_claim_nfts(
deps: DepsMut,
env: Env,
info: MessageInfo,
token_ids: Option<Vec<String>>,
) -> Result<Response, ContractError> {
let nfts = NFT_CLAIMS.claim_nfts(deps.storage, &info.sender, &env.block)?;
if nfts.is_empty() {
return Err(ContractError::NothingToClaim {});
}
let token_ids = match token_ids {
// attempt to claim all legacy NFTs
None => {
// claim all legacy NFTs
let legacy_nfts =
LEGACY_NFT_CLAIMS.claim_nfts(deps.storage, &info.sender, &env.block)?;

if legacy_nfts.is_empty() {
return Err(ContractError::NothingToClaim {});
}

legacy_nfts
}
// attempt to claim non-legacy NFTs
Some(token_ids) => {
let token_ids = if token_ids.is_empty() {
// query all NFT claims if none are provided
NFT_CLAIMS
.query_claims(deps.as_ref(), &info.sender, None, None)?
.nft_claims
.into_iter()
.filter(|nft| nft.release_at.is_expired(&env.block))
.map(|nft| nft.token_id)
.collect::<Vec<_>>()
} else {
// use provided NFTs if any
token_ids
};

if token_ids.is_empty() {
return Err(ContractError::NothingToClaim {});
}

NFT_CLAIMS.claim_nfts(deps.storage, &info.sender, &token_ids, &env.block)?;

token_ids
}
};

let config = CONFIG.load(deps.storage)?;

let msgs = nfts
let msgs = token_ids
.into_iter()
.map(|nft| -> StdResult<CosmosMsg> {
.map(|token_id| -> StdResult<CosmosMsg> {
Ok(WasmMsg::Execute {
contract_addr: config.nft_address.to_string(),
msg: to_json_binary(&cw721::Cw721ExecuteMsg::TransferNft {
recipient: info.sender.to_string(),
token_id: nft,
token_id,
})?,
funds: vec![],
}
Expand Down Expand Up @@ -485,7 +514,11 @@ pub fn query(deps: Deps, env: Env, msg: QueryMsg) -> StdResult<Binary> {
QueryMsg::Dao {} => query_dao(deps),
QueryMsg::Info {} => query_info(deps),
QueryMsg::IsActive {} => query_is_active(deps, env),
QueryMsg::NftClaims { address } => query_nft_claims(deps, address),
QueryMsg::NftClaims {
address,
start_after,
limit,
} => query_nft_claims(deps, address, start_after, limit),
QueryMsg::Hooks {} => query_hooks(deps),
QueryMsg::StakedNfts {
address,
Expand Down Expand Up @@ -606,8 +639,31 @@ pub fn query_dao(deps: Deps) -> StdResult<Binary> {
to_json_binary(&dao)
}

pub fn query_nft_claims(deps: Deps, address: String) -> StdResult<Binary> {
to_json_binary(&NFT_CLAIMS.query_claims(deps, &deps.api.addr_validate(&address)?)?)
pub fn query_nft_claims(
deps: Deps,
address: String,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<Binary> {
let addr = deps.api.addr_validate(&address)?;

// load all legacy claims since it does not support pagination
let legacy_claims = LEGACY_NFT_CLAIMS
.query_claims(deps, &addr)?
.nft_claims
.into_iter()
.map(|c| cw721_controllers::NftClaim::new(c.token_id, c.release_at))
.collect::<Vec<_>>();

// paginate all new claims
let claims = NFT_CLAIMS
.query_claims(deps, &addr, start_after.as_ref(), limit)?
.nft_claims;

// combine legacy and new claims
let nft_claims = legacy_claims.into_iter().chain(claims).collect();

to_json_binary(&cw721_controllers::NftClaimsResponse { nft_claims })
}

pub fn query_hooks(deps: Deps) -> StdResult<Binary> {
Expand Down
6 changes: 3 additions & 3 deletions contracts/voting/dao-voting-cw721-staked/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub enum ContractError {
#[error(transparent)]
UnstakingDurationError(#[from] dao_voting::duration::UnstakingDurationError),

#[error(transparent)]
NftClaimError(#[from] cw721_controllers::NftClaimError),

#[error("Can not stake that which has already been staked")]
AlreadyStaked {},

Expand All @@ -44,9 +47,6 @@ pub enum ContractError {
#[error("Can not unstake that which you have not staked (unstaking {token_id})")]
NotStaked { token_id: String },

#[error("Too many outstanding claims. Claim some tokens before unstaking more.")]
TooManyClaims {},

#[error("Unauthorized")]
Unauthorized {},

Expand Down
Loading

0 comments on commit 815717d

Please sign in to comment.