diff --git a/cosmwasm-contracts/cw721-membership/schema/cw721-membership.json b/cosmwasm-contracts/cw721-membership/schema/cw721-membership.json index 028028d167..175af1790f 100644 --- a/cosmwasm-contracts/cw721-membership/schema/cw721-membership.json +++ b/cosmwasm-contracts/cw721-membership/schema/cw721-membership.json @@ -126,12 +126,6 @@ "$ref": "#/definitions/MembershipConfig" } }, - "owner": { - "type": [ - "string", - "null" - ] - }, "trade_royalties_addr": { "type": [ "string", @@ -160,13 +154,13 @@ "subscribe": { "type": "object", "required": [ - "channel_addr", + "channel_id", "membership_kind", "recipient_addr" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" }, "membership_kind": { "type": "integer", @@ -242,15 +236,17 @@ "update_channel_mint_platform_fee": { "type": "object", "required": [ - "channel_addr", - "mint_royalties" + "channel_id" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" }, "mint_royalties": { - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint16", "minimum": 0.0 } @@ -288,11 +284,11 @@ "withdraw_mint_funds": { "type": "object", "required": [ - "channel_addr" + "channel_id" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" }, "destination_addr": { "type": [ @@ -497,11 +493,49 @@ "channel": { "type": "object", "required": [ - "channel_addr" + "channel_id" ], "properties": { - "channel_addr": { + "channel_id": { + "$ref": "#/definitions/Uint64" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "channels_by_owner" + ], + "properties": { + "channels_by_owner": { + "type": "object", + "required": [ + "owner_address" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner_address": { "type": "string" + }, + "start_after": { + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] } } } @@ -529,11 +563,11 @@ "channel_funds": { "type": "object", "required": [ - "channel_addr" + "channel_id" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" } } } @@ -549,12 +583,12 @@ "subscription": { "type": "object", "required": [ - "channel_addr", + "channel_id", "sub_addr" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" }, "sub_addr": { "type": "string" @@ -749,6 +783,10 @@ "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" } } }, @@ -1192,6 +1230,20 @@ } } }, + "channels_by_owner": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Uint64", + "type": "array", + "items": { + "$ref": "#/definitions/Uint64" + }, + "definitions": { + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } + }, "config": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", diff --git a/cosmwasm-contracts/cw721-membership/schema/raw/execute.json b/cosmwasm-contracts/cw721-membership/schema/raw/execute.json index 881be1b6af..59af0bd9cc 100644 --- a/cosmwasm-contracts/cw721-membership/schema/raw/execute.json +++ b/cosmwasm-contracts/cw721-membership/schema/raw/execute.json @@ -87,12 +87,6 @@ "$ref": "#/definitions/MembershipConfig" } }, - "owner": { - "type": [ - "string", - "null" - ] - }, "trade_royalties_addr": { "type": [ "string", @@ -121,13 +115,13 @@ "subscribe": { "type": "object", "required": [ - "channel_addr", + "channel_id", "membership_kind", "recipient_addr" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" }, "membership_kind": { "type": "integer", @@ -203,15 +197,17 @@ "update_channel_mint_platform_fee": { "type": "object", "required": [ - "channel_addr", - "mint_royalties" + "channel_id" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" }, "mint_royalties": { - "type": "integer", + "type": [ + "integer", + "null" + ], "format": "uint16", "minimum": 0.0 } @@ -249,11 +245,11 @@ "withdraw_mint_funds": { "type": "object", "required": [ - "channel_addr" + "channel_id" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" }, "destination_addr": { "type": [ diff --git a/cosmwasm-contracts/cw721-membership/schema/raw/query.json b/cosmwasm-contracts/cw721-membership/schema/raw/query.json index fe8b23c9d7..1949fcd429 100644 --- a/cosmwasm-contracts/cw721-membership/schema/raw/query.json +++ b/cosmwasm-contracts/cw721-membership/schema/raw/query.json @@ -74,11 +74,49 @@ "channel": { "type": "object", "required": [ - "channel_addr" + "channel_id" ], "properties": { - "channel_addr": { + "channel_id": { + "$ref": "#/definitions/Uint64" + } + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "channels_by_owner" + ], + "properties": { + "channels_by_owner": { + "type": "object", + "required": [ + "owner_address" + ], + "properties": { + "limit": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner_address": { "type": "string" + }, + "start_after": { + "anyOf": [ + { + "$ref": "#/definitions/Uint64" + }, + { + "type": "null" + } + ] } } } @@ -106,11 +144,11 @@ "channel_funds": { "type": "object", "required": [ - "channel_addr" + "channel_id" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" } } } @@ -126,12 +164,12 @@ "subscription": { "type": "object", "required": [ - "channel_addr", + "channel_id", "sub_addr" ], "properties": { - "channel_addr": { - "type": "string" + "channel_id": { + "$ref": "#/definitions/Uint64" }, "sub_addr": { "type": "string" @@ -326,6 +364,10 @@ "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" + }, + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" } } } diff --git a/cosmwasm-contracts/cw721-membership/schema/raw/response_to_channels_by_owner.json b/cosmwasm-contracts/cw721-membership/schema/raw/response_to_channels_by_owner.json new file mode 100644 index 0000000000..86c8cf8940 --- /dev/null +++ b/cosmwasm-contracts/cw721-membership/schema/raw/response_to_channels_by_owner.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Array_of_Uint64", + "type": "array", + "items": { + "$ref": "#/definitions/Uint64" + }, + "definitions": { + "Uint64": { + "description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```", + "type": "string" + } + } +} diff --git a/cosmwasm-contracts/cw721-membership/src/contract.rs b/cosmwasm-contracts/cw721-membership/src/contract.rs index f5d74d4ab6..2bb2b55155 100644 --- a/cosmwasm-contracts/cw721-membership/src/contract.rs +++ b/cosmwasm-contracts/cw721-membership/src/contract.rs @@ -3,8 +3,8 @@ use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; use cosmwasm_schema::cw_serde; use cosmwasm_std::{ - Addr, BankMsg, Binary, Coin, CosmosMsg, Order, Response, StdResult, Storage, Timestamp, - Uint128, Uint64, + Addr, BankMsg, Binary, Coin, CosmosMsg, Order, OverflowError, Response, StdResult, Storage, + Timestamp, Uint128, Uint64, }; use cw2981_royalties::msg::{CheckRoyaltiesResponse, Cw2981QueryMsg, RoyaltiesInfoResponse}; use cw721::{ @@ -20,6 +20,8 @@ use sylvia::{contract, entry_points}; pub type NftExtension = Metadata; pub const DEFAULT_LIMIT: usize = 10; +pub const MAX_DURATION_SECONDS: u64 = 2 * 365 * 24 * 60 * 60; // 2 "years" + // MAX_LIMIT is infinity, query gas restrictions should handle this, this allows protected nodes to return big batches if needed pub struct Cw721MembershipContract { @@ -27,12 +29,12 @@ pub struct Cw721MembershipContract { pub(crate) num_tokens: Item<'static, Uint64>, pub(crate) num_channels: Item<'static, Uint64>, pub(crate) channels: Map<'static, u64, Channel>, - pub(crate) channels_by_addr: Map<'static, Addr, u64>, + pub(crate) channels_by_owner: Map<'static, (Addr, u64), ()>, pub(crate) nfts: Map<'static, (u64, u64), Nft>, - pub(crate) by_owner: Map<'static, (Addr, u64, u64), ()>, - pub(crate) by_expiration: Map<'static, (String, u64, u64), ()>, + pub(crate) nfts_by_owner: Map<'static, (Addr, u64, u64), ()>, + pub(crate) nfts_by_expiration: Map<'static, (String, u64, u64), ()>, pub(crate) mint_platform_fees: Map<'static, String, Uint128>, - pub(crate) mint_funds: Map<'static, (Addr, String), Uint128>, + pub(crate) mint_funds: Map<'static, (u64, String), Uint128>, } #[cw_serde] @@ -68,8 +70,8 @@ pub struct ChannelFundsResponse { #[cw_serde] pub struct Channel { memberships_config: Vec, - mint_royalties_per10k: u16, // 0-10000 = 0%-100% - trade_royalties_per10k: u16, // 0-10000 = 0%-100% + mint_royalties_per10k: Option, // 0-10000 = 0%-100% + trade_royalties_per10k: u16, // 0-10000 = 0%-100% trade_royalties_addr: Addr, next_index: Uint64, owner_addr: Addr, @@ -122,12 +124,12 @@ impl Cw721MembershipContract { Self { config: Item::new("config"), channels: Map::new("channels"), - channels_by_addr: Map::new("channels_by_addr"), + channels_by_owner: Map::new("channels_by_owner"), nfts: Map::new("nfts"), num_tokens: Item::new("num_tokens"), num_channels: Item::new("num_channels"), - by_owner: Map::new("by_owner"), - by_expiration: Map::new("by_expiration"), + nfts_by_owner: Map::new("nfts_by_owner"), + nfts_by_expiration: Map::new("nfts_by_expiration"), mint_platform_fees: Map::new("mint_platform_fees"), mint_funds: Map::new("mint_funds"), } @@ -171,30 +173,20 @@ impl Cw721MembershipContract { trade_royalties_per10k: u16, // 0-10000 = 0%-100% trade_royalties_addr: Option, ) -> Result { - let config = self.config.load(ctx.deps.storage)?; + assert_valid_tiers(&memberships_config)?; let owner_addr = ctx.info.sender; - let channel_id_opt = self - .channels_by_addr - .may_load(ctx.deps.storage, owner_addr.to_owned())?; - - let channel_id: u64 = match channel_id_opt { - Some(_) => Err(ContractError::ChannelExists), - None => { - let channel_id = self - .num_channels - .update::<_, ContractError>(ctx.deps.storage, |num_channels| { - Ok(num_channels.checked_add(Uint64::one())?) - })?; - self.channels_by_addr.save( - ctx.deps.storage, - owner_addr.to_owned(), - &channel_id.u64(), - )?; - Ok(channel_id.u64()) - } - }?; + let channel_id = self + .num_channels + .update::<_, ContractError>(ctx.deps.storage, |num_channels| { + Ok(num_channels.checked_add(Uint64::one())?) + })?; + self.channels_by_owner.save( + ctx.deps.storage, + (owner_addr.to_owned(), channel_id.u64()), + &(), + )?; let royalties_addr = match trade_royalties_addr { Some(royalties_addr) => ctx.deps.api.addr_validate(royalties_addr.as_str())?, @@ -209,15 +201,17 @@ impl Cw721MembershipContract { None => Ok(Channel { memberships_config, next_index: Uint64::one(), - mint_royalties_per10k: config.mint_royalties_per10k_default, + mint_royalties_per10k: None, trade_royalties_per10k, - owner_addr: owner_addr, + owner_addr, trade_royalties_addr: royalties_addr, }), }, )?; - Ok(Response::default().add_attribute("channel_id", channel_id.to_string())) + Ok(Response::default() + .add_attribute("action", "create_channel") + .add_attribute("channel_id", channel_id.to_string())) } #[msg(exec)] @@ -228,10 +222,8 @@ impl Cw721MembershipContract { memberships_config: Option>, trade_royalties_per10k: Option, trade_royalties_addr: Option, - owner: Option, + // owner: Option, TODO: allow transfer ) -> Result { - let new_owner = owner; - self.channels .update(ctx.deps.storage, id.into(), |channel| match channel { Some(mut channel) => { @@ -242,6 +234,7 @@ impl Cw721MembershipContract { let mut changed = false; if let Some(memberships_config) = memberships_config { + assert_valid_tiers(&memberships_config)?; channel.memberships_config = memberships_config; changed = true; } @@ -257,12 +250,6 @@ impl Cw721MembershipContract { changed = true; } - if let Some(new_owner) = new_owner { - let new_owner = ctx.deps.api.addr_validate(new_owner.as_str())?; - channel.owner_addr = new_owner; - changed = true; - } - if !changed { return Err(ContractError::NoChanges); } @@ -278,19 +265,12 @@ impl Cw721MembershipContract { pub fn subscribe( &self, ctx: ExecCtx, - channel_addr: String, + channel_id: Uint64, recipient_addr: String, membership_kind: u8, ) -> Result { let recipient_addr = ctx.deps.api.addr_validate(recipient_addr.as_str())?; - let unchecked_channel_addr = Addr::unchecked(channel_addr.as_str()); // we don't need to validate the address because we won't find a channel if the address is invalid - let channel_id = self - .channels_by_addr - .load(ctx.deps.storage, unchecked_channel_addr.to_owned()) - .map_err(|_| ContractError::UnknownChannelAddress)?; - let channel_addr = unchecked_channel_addr; - let mut channel = self .channels .load(ctx.deps.storage, channel_id.into()) @@ -303,14 +283,16 @@ impl Cw721MembershipContract { assert_exact_funds(&ctx, membership.price.to_owned())?; + let config = self.config.load(ctx.deps.storage)?; + + let mint_royalties = channel + .mint_royalties_per10k + .unwrap_or(config.mint_royalties_per10k_default); + let royalties_amount = membership .price .amount - .checked_mul(Uint128::from( - self.config - .load(ctx.deps.storage)? - .mint_royalties_per10k_default, - ))? + .checked_mul(Uint128::from(mint_royalties))? .checked_div(Uint128::from(10000u16))?; self.mint_platform_fees.update::<_, ContractError>( ctx.deps.storage, @@ -325,7 +307,7 @@ impl Cw721MembershipContract { let creator_amount = membership.price.amount.checked_sub(royalties_amount)?; self.mint_funds.update::<_, ContractError>( ctx.deps.storage, - (channel_addr.to_owned(), membership.price.denom.to_owned()), + (channel_id.u64(), membership.price.denom.to_owned()), |funds| { Ok(funds .unwrap_or(Uint128::zero()) @@ -350,7 +332,7 @@ impl Cw721MembershipContract { )?; // save in index - self.by_owner.save( + self.nfts_by_owner.save( ctx.deps.storage, ( recipient_addr.to_owned(), @@ -359,8 +341,8 @@ impl Cw721MembershipContract { ), &(), )?; - let expiry = nft.start_time.plus_seconds(nft.duration_seconds.u64()); - self.by_expiration.save( + let expiry = checked_expiry(nft.start_time, nft.duration_seconds)?; + self.nfts_by_expiration.save( ctx.deps.storage, ( recipient_addr.to_string() + &channel_id.to_string(), @@ -379,9 +361,10 @@ impl Cw721MembershipContract { Ok(num_tokens.checked_add(Uint64::one())?) })?; - // we need these for efficient indexing + // we need this for efficient indexing let token_id = format_token_id(channel_id.into(), nft_index.into()); - let nft_info = self.internal_nft_info(ctx.deps.storage, channel_id, nft_index.u64())?; + let nft_info = + self.internal_nft_info(ctx.deps.storage, channel_id.u64(), nft_index.u64())?; let json_nft_info = serde_json::to_string(&nft_info).map_err(|_| ContractError::SerializationError)?; @@ -440,19 +423,14 @@ impl Cw721MembershipContract { pub fn update_channel_mint_platform_fee( &self, ctx: ExecCtx, - channel_addr: String, - mint_royalties: u16, + channel_id: Uint64, + mint_royalties: Option, ) -> Result { let config = self.config.load(ctx.deps.storage)?; if ctx.info.sender != config.admin_addr { return Err(ContractError::Unauthorized); } - let unchecked_channel_addr = Addr::unchecked(channel_addr.as_str()); // we don't need to validate the address because we won't find a channel if the address is invalid - let channel_id = self - .channels_by_addr - .load(ctx.deps.storage, unchecked_channel_addr.to_owned()) - .map_err(|_| ContractError::UnknownChannelAddress)?; let mut channel = self .channels .load(ctx.deps.storage, channel_id.into()) @@ -504,20 +482,12 @@ impl Cw721MembershipContract { pub fn withdraw_mint_funds( &self, ctx: ExecCtx, - channel_addr: String, + channel_id: Uint64, destination_addr: Option, ) -> Result { - let unchecked_channel_addr = Addr::unchecked(channel_addr.as_str()); // we don't need to validate the address because we won't find a channel if the address is invalid - let channel_id = self - .channels_by_addr - .load(ctx.deps.storage, unchecked_channel_addr.to_owned()) - .map_err(|_| ContractError::UnknownChannelAddress)?; - if !self.channels.has(ctx.deps.storage, channel_id.into()) { - return Err(ContractError::ChannelNotFound); - } - let channel_addr = unchecked_channel_addr; + let channel = self.channels.load(ctx.deps.storage, channel_id.into())?; - if ctx.info.sender != channel_addr { + if ctx.info.sender != channel.owner_addr { return Err(ContractError::Unauthorized); } @@ -528,7 +498,7 @@ impl Cw721MembershipContract { let coins: Result, ContractError> = self .mint_funds - .prefix(channel_addr.to_owned()) + .prefix(channel_id.u64()) .range(ctx.deps.storage, None, None, Order::Ascending) .map(|item| { let (denom, amount) = item?; @@ -542,7 +512,7 @@ impl Cw721MembershipContract { })); self.mint_funds - .prefix(channel_addr) + .prefix(channel_id.u64()) .clear(ctx.deps.storage, None); Ok(response) @@ -560,13 +530,9 @@ impl Cw721MembershipContract { pub fn channel( &self, ctx: QueryCtx, - channel_addr: String, + channel_id: Uint64, ) -> Result { - let unchecked_channel_addr = Addr::unchecked(channel_addr.as_str()); // we don't need to validate the address because we won't find a channel if the address is invalid - let channel_id = self - .channels_by_addr - .load(ctx.deps.storage, unchecked_channel_addr.to_owned()) - .map_err(|_| ContractError::UnknownChannelAddress)?; + let config = self.config.load(ctx.deps.storage)?; let channel = self .channels @@ -577,12 +543,41 @@ impl Cw721MembershipContract { id: channel_id.into(), owner_addr: channel.owner_addr, memberships_config: channel.memberships_config, - mint_royalties_per10k: channel.mint_royalties_per10k, + mint_royalties_per10k: channel + .mint_royalties_per10k + .unwrap_or(config.mint_royalties_per10k_default), trade_royalties_per10k: channel.trade_royalties_per10k, trade_royalties_addr: channel.trade_royalties_addr, }) } + #[msg(query)] + pub fn channels_by_owner( + &self, + ctx: QueryCtx, + owner_address: String, + start_after: Option, + limit: Option, + ) -> Result, ContractError> { + let limit = input_limit_into_range_limit(limit); + let channel_ids: Result, ContractError> = self + .channels_by_owner + .prefix(Addr::unchecked(owner_address)) + .range( + ctx.deps.storage, + start_after.map(|sa| Bound::exclusive(sa)), + None, + Order::Ascending, + ) + .map(|r| { + let (id, _) = r?; + Ok(Uint64::from(id)) + }) + .take(limit) + .collect(); + Ok(channel_ids?) + } + #[msg(query)] pub fn admin_funds(&self, ctx: QueryCtx) -> Result { let funds: Result, ContractError> = self @@ -600,13 +595,11 @@ impl Cw721MembershipContract { pub fn channel_funds( &self, ctx: QueryCtx, - channel_addr: String, + channel_id: Uint64, ) -> Result { - let unchecked_channel_addr = Addr::unchecked(channel_addr.as_str()); // we don't need to validate the address because we won't find a channel if the address is invalid - let channel_addr = unchecked_channel_addr; let funds: Result, ContractError> = self .mint_funds - .prefix(channel_addr.to_owned()) + .prefix(channel_id.u64()) .range(ctx.deps.storage, None, None, Order::Ascending) .map(|item| { let (denom, amount) = item?; @@ -621,19 +614,14 @@ impl Cw721MembershipContract { &self, ctx: QueryCtx, sub_addr: String, - channel_addr: String, + channel_id: Uint64, ) -> Result { let sub_addr = Addr::unchecked(sub_addr.as_str()); - let unchecked_channel_addr = Addr::unchecked(channel_addr.as_str()); - let channel_id = self - .channels_by_addr - .load(ctx.deps.storage, unchecked_channel_addr.to_owned()) - .map_err(|_| ContractError::UnknownChannelAddress)?; let mut subscription: Option = None; let mut level: u16 = 0; // only iterate over unexpired tokens for entry in self - .by_expiration + .nfts_by_expiration .sub_prefix(sub_addr.to_string() + &channel_id.to_string()) .range( ctx.deps.storage, @@ -649,12 +637,12 @@ impl Cw721MembershipContract { // we don't need to check that we're past start time since it can't be in the future - let expiration = nft.start_time.plus_seconds(nft.duration_seconds.into()); + let expiration = checked_expiry(nft.start_time, nft.duration_seconds)?; if expiration <= ctx.env.block.time { continue; } - level += 1; + level = level.checked_add(1).ok_or(ContractError::InternalError)?; match &subscription { Some(value) => { @@ -768,7 +756,7 @@ impl Cw721MembershipContract { return Err(ContractError::Unauthorized); } self.nfts.remove(ctx.deps.storage, (channel_id, nft_index)); - self.by_owner + self.nfts_by_owner .remove(ctx.deps.storage, (ctx.info.sender, channel_id, nft_index)); self.num_tokens .update::<_, ContractError>(ctx.deps.storage, |num_tokens| { @@ -926,17 +914,11 @@ impl Cw721MembershipContract { limit: Option, ) -> Result { let owner_addr = ctx.deps.api.addr_validate(owner.as_str())?; - let limit = limit.map(|limit| limit as usize).unwrap_or(DEFAULT_LIMIT); - let min_bound = match start_after { - Some(start_after) => { - let (channel_id, nft_index) = parse_token_id(&start_after)?; - Some(Bound::exclusive((channel_id, nft_index))) - } - None => None, - }; + let limit = input_limit_into_range_limit(limit); + let min_bound = token_id_start_after_into_bound(&start_after)?; let tokens: Result, _> = self - .by_owner + .nfts_by_owner .sub_prefix(owner_addr) .range(ctx.deps.storage, min_bound, None, Order::Ascending) .take(limit) @@ -958,15 +940,8 @@ impl Cw721MembershipContract { start_after: Option, limit: Option, ) -> Result { - let limit = limit.map(|limit| limit as usize).unwrap_or(DEFAULT_LIMIT); - - let min_bound = match start_after { - Some(start_after) => { - let (channel_id, nft_index) = parse_token_id(&start_after)?; - Some(Bound::exclusive((channel_id, nft_index))) - } - None => None, - }; + let limit = input_limit_into_range_limit(limit); + let min_bound = token_id_start_after_into_bound(&start_after)?; let tokens: Result, _> = self .nfts @@ -1001,8 +976,6 @@ impl Cw721MembershipContract { ) -> Result, ContractError> { let nft = self.nfts.load(storage, (channel_id, nft_index))?; - // TODO: improve info - Ok(NftInfoResponse { token_uri: None, extension: NftExtension { @@ -1060,30 +1033,27 @@ impl Cw721MembershipContract { None => Err(ContractError::NftNotFound), })?; - self.by_owner + self.nfts_by_owner .remove(ctx.deps.storage, (ctx.info.sender, channel_id, nft_index)); - self.by_owner.save( + self.nfts_by_owner.save( ctx.deps.storage, (recipient.to_owned(), channel_id, nft_index), &(), )?; - let expiry = nft - .start_time - .plus_seconds(nft.duration_seconds.u64()) - .seconds(); - self.by_expiration.remove( + let expiry_seconds = checked_expiry(nft.start_time, nft.duration_seconds)?.seconds(); + self.nfts_by_expiration.remove( ctx.deps.storage, ( sender.to_string() + &channel_id.to_string(), - expiry, + expiry_seconds, nft_index, ), ); - self.by_expiration.save( + self.nfts_by_expiration.save( ctx.deps.storage, ( recipient.to_string() + &channel_id.to_string(), - expiry, + expiry_seconds, nft_index, ), &(), @@ -1093,7 +1063,7 @@ impl Cw721MembershipContract { } } -pub fn assert_exact_funds(ctx: &ExecCtx, amount: Coin) -> Result { +pub fn assert_exact_funds(ctx: &ExecCtx, amount: Coin) -> Result<(), ContractError> { let mut total_funds_amount = Uint128::zero(); for coin in ctx.info.funds.iter() { if coin.denom == amount.denom { @@ -1105,18 +1075,74 @@ pub fn assert_exact_funds(ctx: &ExecCtx, amount: Coin) -> Result Result<(u64, u64), ContractError> { let bytes = URL_SAFE_NO_PAD.decode(token_id)?; - let (nft_index, nft_index_len) = + let (channel_id, channel_id_len) = u64::decode_var(bytes.as_slice()).ok_or(ContractError::InvalidTokenId)?; - let (channel_id, _) = - u64::decode_var(&bytes[nft_index_len..]).ok_or(ContractError::InvalidTokenId)?; + let (nft_index, _) = + u64::decode_var(&bytes[channel_id_len..]).ok_or(ContractError::InvalidTokenId)?; Ok((channel_id, nft_index)) } pub fn format_token_id(channel_id: u64, nft_index: u64) -> String { - URL_SAFE_NO_PAD.encode([nft_index.encode_var_vec(), channel_id.encode_var_vec()].concat()) + URL_SAFE_NO_PAD.encode([channel_id.encode_var_vec(), nft_index.encode_var_vec()].concat()) +} + +pub fn checked_expiry( + start: Timestamp, + duration_seconds: Uint64, +) -> Result { + let start_nanos = Uint64::from(start.nanos()); + let durations_nanos = duration_seconds.checked_mul(Uint64::from(1_000_000_000u64))?; + let end_nanos = start_nanos.checked_add(durations_nanos)?; + Ok(Timestamp::from_nanos(end_nanos.u64())) +} + +pub fn assert_valid_tiers(tiers: &Vec) -> Result<(), ContractError> { + for tier in tiers.iter() { + if tier.price.amount.is_zero() { + return Err(ContractError::InvalidTiers); + } + if tier.price.denom.is_empty() { + return Err(ContractError::InvalidTiers); + } + if tier.duration_seconds.is_zero() + || tier.duration_seconds > Uint64::from(MAX_DURATION_SECONDS) + { + return Err(ContractError::InvalidTiers); + } + if tier.display_name.is_empty() { + return Err(ContractError::InvalidTiers); + } + if tier.description.is_empty() { + return Err(ContractError::InvalidTiers); + } + if tier.nft_name_prefix.is_empty() { + return Err(ContractError::InvalidTiers); + } + if tier.nft_image_uri.is_empty() { + return Err(ContractError::InvalidTiers); + } + } + Ok(()) +} + +pub fn token_id_start_after_into_bound( + start_after: &Option, +) -> Result>, ContractError> { + let bound = match start_after { + Some(start_after) => { + let (channel_id, nft_index) = parse_token_id(&start_after)?; + Some(Bound::exclusive((channel_id, nft_index))) + } + None => None, + }; + Ok(bound) +} + +pub fn input_limit_into_range_limit(limit: Option) -> usize { + limit.map(|limit| limit as usize).unwrap_or(DEFAULT_LIMIT) } diff --git a/cosmwasm-contracts/cw721-membership/src/error.rs b/cosmwasm-contracts/cw721-membership/src/error.rs index 456ce16058..d79421ec32 100644 --- a/cosmwasm-contracts/cw721-membership/src/error.rs +++ b/cosmwasm-contracts/cw721-membership/src/error.rs @@ -53,4 +53,10 @@ pub enum ContractError { #[error("No changes.")] NoChanges, + + #[error("Expiry overflow.")] + ExpiryOverflow, + + #[error("Invalid tiers.")] + InvalidTiers, } diff --git a/cosmwasm-contracts/cw721-membership/src/multitest.rs b/cosmwasm-contracts/cw721-membership/src/multitest.rs index 27dbc13477..0206ff665a 100644 --- a/cosmwasm-contracts/cw721-membership/src/multitest.rs +++ b/cosmwasm-contracts/cw721-membership/src/multitest.rs @@ -1,4 +1,6 @@ -use cosmwasm_std::{from_json, Addr, BalanceResponse, BankQuery, Coin, Timestamp, Uint128, Uint64}; +use cosmwasm_std::{ + from_json, Addr, BalanceResponse, BankQuery, Coin, Event, Timestamp, Uint128, Uint64, +}; use cw2981_royalties::msg::{CheckRoyaltiesResponse, Cw2981QueryMsg, RoyaltiesInfoResponse}; use cw721::{NftInfoResponse, TokensResponse}; use cw721_metadata_onchain::{Metadata, Trait}; @@ -115,7 +117,12 @@ fn basic_full_flow() { .call(channel_owner) .unwrap(); - let channel_response = contract.channel(channel_owner.to_string()).unwrap(); + let ev_values = get_events_values(res.events, "wasm".to_string(), "channel_id".to_string()); + assert_eq!(ev_values.len(), 1); + let channel_id = Uint64::from(ev_values[0].parse::().unwrap()); + assert_eq!(channel_id, Uint64::from(1u64)); + + let channel_response = contract.channel(channel_id).unwrap(); assert_eq!(channel_response.memberships_config, memberships_config); assert_eq!(channel_response.mint_royalties_per10k, mint_royalties); assert_eq!( @@ -128,11 +135,11 @@ fn basic_full_flow() { ); contract - .update_channel(channel_response.id, None, Some(trade_royalties), None, None) + .update_channel(channel_response.id, None, Some(trade_royalties), None) .call(channel_owner) .unwrap(); - let channel_response = contract.channel(channel_owner.to_string()).unwrap(); + let channel_response = contract.channel(channel_id).unwrap(); assert_eq!(channel_response.owner_addr, channel_owner); assert_eq!(channel_response.memberships_config, memberships_config); assert_eq!(channel_response.mint_royalties_per10k, mint_royalties); @@ -145,7 +152,7 @@ fn basic_full_flow() { // ------- mint a nft contract - .subscribe(channel_owner.to_string(), sub_user.to_string(), 0) + .subscribe(channel_response.id, sub_user.to_string(), 0) .with_funds(&[Coin { denom: "utori".to_string(), amount: Uint128::from(1000000u32), @@ -294,7 +301,7 @@ fn basic_full_flow() { }); let subscription = contract - .subscription(sub_user.to_string(), channel_owner.to_string()) + .subscription(sub_user.to_string(), channel_response.id) .unwrap(); assert_eq!( subscription, @@ -313,7 +320,7 @@ fn basic_full_flow() { }); let subscription = contract - .subscription(sub_user.to_string(), channel_owner.to_string()) + .subscription(sub_user.to_string(), channel_response.id) .unwrap(); assert_eq!( subscription, @@ -325,7 +332,7 @@ fn basic_full_flow() { // check withdraw contract - .withdraw_mint_funds(channel_owner.to_string(), Some(channel_vault.to_string())) + .withdraw_mint_funds(channel_id, Some(channel_vault.to_string())) .call(channel_owner) .unwrap(); let channel_balance = get_balance(channel_vault.to_string()); @@ -340,3 +347,17 @@ fn basic_full_flow() { let platform_balance = get_balance(platform_vault.to_string()); assert_eq!(platform_balance.amount.amount, Uint128::from(50000u32)); } + +fn get_events_values(ev: Vec, ty: String, key: String) -> Vec { + ev.iter() + .filter(|v| v.ty == ty) + .map(|v| { + v.attributes + .iter() + .find(|vv| vv.key == key) + .unwrap() + .value + .clone() + }) + .collect() +} diff --git a/package.json b/package.json index a422cc5d88..6446f2350d 100644 --- a/package.json +++ b/package.json @@ -81,12 +81,12 @@ "electron-store": "^8.1.0", "eslint-config-universe": "^12.0.0", "ethers": "^5.7.2", - "expo": "~50.0.15", + "expo": "~50.0.17", "expo-av": "~13.10.5", "expo-blur": "~12.9.2", "expo-clipboard": "~5.0.1", "expo-document-picker": "~11.10.1", - "expo-file-system": "~16.0.8", + "expo-file-system": "~16.0.9", "expo-font": "~11.10.3", "expo-image-picker": "^14.7.1", "expo-linear-gradient": "~12.7.2", diff --git a/packages/contracts-clients/cw721-membership/Cw721Membership.client.ts b/packages/contracts-clients/cw721-membership/Cw721Membership.client.ts index ccea6a42c8..b84125f0b8 100644 --- a/packages/contracts-clients/cw721-membership/Cw721Membership.client.ts +++ b/packages/contracts-clients/cw721-membership/Cw721Membership.client.ts @@ -6,26 +6,35 @@ import { CosmWasmClient, SigningCosmWasmClient, ExecuteResult } from "@cosmjs/cosmwasm-stargate"; import { StdFee } from "@cosmjs/amino"; -import { InstantiateMsg, ExecuteMsg, ExecMsg, Uint64, Uint128, Binary, MembershipConfig, Coin, QueryMsg, QueryMsg1, Cw2981QueryMsg, AdminFundsResponse, Expiration, Timestamp, AllNftInfoResponseForMetadata, OwnerOfResponse, Approval, NftInfoResponseForMetadata, Metadata, Trait, TokensResponse, Addr, ChannelResponse, ChannelFundsResponse, Config, ContractInfoResponse, Cw2981Response, CheckRoyaltiesResponse, RoyaltiesInfoResponse, NumTokensResponse, SubscriptionResponse, Subscription } from "./Cw721Membership.types"; +import { InstantiateMsg, ExecuteMsg, ExecMsg, Uint64, Uint128, Binary, MembershipConfig, Coin, QueryMsg, QueryMsg1, Cw2981QueryMsg, AdminFundsResponse, Expiration, Timestamp, AllNftInfoResponseForMetadata, OwnerOfResponse, Approval, NftInfoResponseForMetadata, Metadata, Trait, TokensResponse, Addr, ChannelResponse, ChannelFundsResponse, ArrayOfUint64, Config, ContractInfoResponse, Cw2981Response, CheckRoyaltiesResponse, RoyaltiesInfoResponse, NumTokensResponse, SubscriptionResponse, Subscription } from "./Cw721Membership.types"; export interface Cw721MembershipReadOnlyInterface { contractAddress: string; config: () => Promise; channel: ({ - channelAddr + channelId }: { - channelAddr: string; + channelId: Uint64; }) => Promise; + channelsByOwner: ({ + limit, + ownerAddress, + startAfter + }: { + limit?: number; + ownerAddress: string; + startAfter?: Uint64; + }) => Promise; adminFunds: () => Promise; channelFunds: ({ - channelAddr + channelId }: { - channelAddr: string; + channelId: Uint64; }) => Promise; subscription: ({ - channelAddr, + channelId, subAddr }: { - channelAddr: string; + channelId: Uint64; subAddr: string; }) => Promise; extension: ({ @@ -80,6 +89,7 @@ export class Cw721MembershipQueryClient implements Cw721MembershipReadOnlyInterf this.contractAddress = contractAddress; this.config = this.config.bind(this); this.channel = this.channel.bind(this); + this.channelsByOwner = this.channelsByOwner.bind(this); this.adminFunds = this.adminFunds.bind(this); this.channelFunds = this.channelFunds.bind(this); this.subscription = this.subscription.bind(this); @@ -99,13 +109,30 @@ export class Cw721MembershipQueryClient implements Cw721MembershipReadOnlyInterf }); }; channel = async ({ - channelAddr + channelId }: { - channelAddr: string; + channelId: Uint64; }): Promise => { return this.client.queryContractSmart(this.contractAddress, { channel: { - channel_addr: channelAddr + channel_id: channelId + } + }); + }; + channelsByOwner = async ({ + limit, + ownerAddress, + startAfter + }: { + limit?: number; + ownerAddress: string; + startAfter?: Uint64; + }): Promise => { + return this.client.queryContractSmart(this.contractAddress, { + channels_by_owner: { + limit, + owner_address: ownerAddress, + start_after: startAfter } }); }; @@ -115,26 +142,26 @@ export class Cw721MembershipQueryClient implements Cw721MembershipReadOnlyInterf }); }; channelFunds = async ({ - channelAddr + channelId }: { - channelAddr: string; + channelId: Uint64; }): Promise => { return this.client.queryContractSmart(this.contractAddress, { channel_funds: { - channel_addr: channelAddr + channel_id: channelId } }); }; subscription = async ({ - channelAddr, + channelId, subAddr }: { - channelAddr: string; + channelId: Uint64; subAddr: string; }): Promise => { return this.client.queryContractSmart(this.contractAddress, { subscription: { - channel_addr: channelAddr, + channel_id: channelId, sub_addr: subAddr } }); @@ -246,22 +273,20 @@ export interface Cw721MembershipInterface extends Cw721MembershipReadOnlyInterfa updateChannel: ({ id, membershipsConfig, - owner, tradeRoyaltiesAddr, tradeRoyaltiesPer10k }: { id: Uint64; membershipsConfig?: MembershipConfig[]; - owner?: string; tradeRoyaltiesAddr?: string; tradeRoyaltiesPer10k?: number; }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; subscribe: ({ - channelAddr, + channelId, membershipKind, recipientAddr }: { - channelAddr: string; + channelId: Uint64; membershipKind: number; recipientAddr: string; }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; @@ -281,11 +306,11 @@ export interface Cw721MembershipInterface extends Cw721MembershipReadOnlyInterfa symbol?: string; }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; updateChannelMintPlatformFee: ({ - channelAddr, + channelId, mintRoyalties }: { - channelAddr: string; - mintRoyalties: number; + channelId: Uint64; + mintRoyalties?: number; }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; withdrawMintPlatformFee: ({ destinationAddr @@ -293,10 +318,10 @@ export interface Cw721MembershipInterface extends Cw721MembershipReadOnlyInterfa destinationAddr?: string; }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; withdrawMintFunds: ({ - channelAddr, + channelId, destinationAddr }: { - channelAddr: string; + channelId: Uint64; destinationAddr?: string; }, fee?: number | StdFee | "auto", memo?: string, _funds?: Coin[]) => Promise; transferNft: ({ @@ -363,13 +388,11 @@ export class Cw721MembershipClient extends Cw721MembershipQueryClient implements updateChannel = async ({ id, membershipsConfig, - owner, tradeRoyaltiesAddr, tradeRoyaltiesPer10k }: { id: Uint64; membershipsConfig?: MembershipConfig[]; - owner?: string; tradeRoyaltiesAddr?: string; tradeRoyaltiesPer10k?: number; }, fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { @@ -377,24 +400,23 @@ export class Cw721MembershipClient extends Cw721MembershipQueryClient implements update_channel: { id, memberships_config: membershipsConfig, - owner, trade_royalties_addr: tradeRoyaltiesAddr, trade_royalties_per10k: tradeRoyaltiesPer10k } }, fee, memo, _funds); }; subscribe = async ({ - channelAddr, + channelId, membershipKind, recipientAddr }: { - channelAddr: string; + channelId: Uint64; membershipKind: number; recipientAddr: string; }, fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { return await this.client.execute(this.sender, this.contractAddress, { subscribe: { - channel_addr: channelAddr, + channel_id: channelId, membership_kind: membershipKind, recipient_addr: recipientAddr } @@ -427,15 +449,15 @@ export class Cw721MembershipClient extends Cw721MembershipQueryClient implements }, fee, memo, _funds); }; updateChannelMintPlatformFee = async ({ - channelAddr, + channelId, mintRoyalties }: { - channelAddr: string; - mintRoyalties: number; + channelId: Uint64; + mintRoyalties?: number; }, fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { return await this.client.execute(this.sender, this.contractAddress, { update_channel_mint_platform_fee: { - channel_addr: channelAddr, + channel_id: channelId, mint_royalties: mintRoyalties } }, fee, memo, _funds); @@ -452,15 +474,15 @@ export class Cw721MembershipClient extends Cw721MembershipQueryClient implements }, fee, memo, _funds); }; withdrawMintFunds = async ({ - channelAddr, + channelId, destinationAddr }: { - channelAddr: string; + channelId: Uint64; destinationAddr?: string; }, fee: number | StdFee | "auto" = "auto", memo?: string, _funds?: Coin[]): Promise => { return await this.client.execute(this.sender, this.contractAddress, { withdraw_mint_funds: { - channel_addr: channelAddr, + channel_id: channelId, destination_addr: destinationAddr } }, fee, memo, _funds); diff --git a/packages/contracts-clients/cw721-membership/Cw721Membership.types.ts b/packages/contracts-clients/cw721-membership/Cw721Membership.types.ts index 8c8d2497d4..0ec427afa0 100644 --- a/packages/contracts-clients/cw721-membership/Cw721Membership.types.ts +++ b/packages/contracts-clients/cw721-membership/Cw721Membership.types.ts @@ -25,14 +25,13 @@ export type ExecMsg = { update_channel: { id: Uint64; memberships_config?: MembershipConfig[] | null; - owner?: string | null; trade_royalties_addr?: string | null; trade_royalties_per10k?: number | null; [k: string]: unknown; }; } | { subscribe: { - channel_addr: string; + channel_id: Uint64; membership_kind: number; recipient_addr: string; [k: string]: unknown; @@ -49,8 +48,8 @@ export type ExecMsg = { }; } | { update_channel_mint_platform_fee: { - channel_addr: string; - mint_royalties: number; + channel_id: Uint64; + mint_royalties?: number | null; [k: string]: unknown; }; } | { @@ -60,7 +59,7 @@ export type ExecMsg = { }; } | { withdraw_mint_funds: { - channel_addr: string; + channel_id: Uint64; destination_addr?: string | null; [k: string]: unknown; }; @@ -106,7 +105,14 @@ export type QueryMsg1 = { }; } | { channel: { - channel_addr: string; + channel_id: Uint64; + [k: string]: unknown; + }; +} | { + channels_by_owner: { + limit?: number | null; + owner_address: string; + start_after?: Uint64 | null; [k: string]: unknown; }; } | { @@ -115,12 +121,12 @@ export type QueryMsg1 = { }; } | { channel_funds: { - channel_addr: string; + channel_id: Uint64; [k: string]: unknown; }; } | { subscription: { - channel_addr: string; + channel_id: Uint64; sub_addr: string; [k: string]: unknown; }; @@ -234,6 +240,7 @@ export interface ChannelResponse { export interface ChannelFundsResponse { funds: Coin[]; } +export type ArrayOfUint64 = Uint64[]; export interface Config { admin_addr: Addr; description: string; diff --git a/packages/hooks/feed/usePremiumChannel.ts b/packages/hooks/feed/usePremiumChannel.ts index 084fceec45..affc6a61f7 100644 --- a/packages/hooks/feed/usePremiumChannel.ts +++ b/packages/hooks/feed/usePremiumChannel.ts @@ -1,22 +1,24 @@ import { useQuery } from "@tanstack/react-query"; +import { parseUserId } from "@/networks"; import { mustGetCw721MembershipQueryClient } from "@/utils/feed/client"; -export const usePremiumChannel = ( +const usePremiumChannel = ( networkId: string | undefined, - channelAddress: string | undefined, + channelId: string | undefined, + enabled?: boolean, ) => { return useQuery( - ["feed-premium-channel", networkId, channelAddress], + ["feed-premium-channel", networkId, channelId], async () => { - if (!networkId || !channelAddress) { + if (!networkId || !channelId) { return null; } const client = await mustGetCw721MembershipQueryClient(networkId); try { - const channel = await client.channel({ channelAddr: channelAddress }); + const channel = await client.channel({ channelId }); return channel; } catch (error) { if ( @@ -29,8 +31,46 @@ export const usePremiumChannel = ( } }, { - enabled: !!networkId && !!channelAddress, + enabled: (enabled ?? true) && !!networkId && !!channelId, staleTime: Infinity, }, ); }; + +const usePremiumChannelsByOwner = ( + userId: string | undefined, + enabled?: boolean, +) => { + return useQuery( + ["feed-premium-channels", userId], + async () => { + if (!userId) { + return []; + } + const [, userAddress] = parseUserId(userId); + if (!userAddress) { + return []; + } + + const client = await mustGetCw721MembershipQueryClient(userId); + + const channels = await client.channelsByOwner({ + ownerAddress: userAddress, + }); + return channels; + }, + { + enabled: (enabled ?? true) && !!userId, + staleTime: Infinity, + }, + ); +}; + +export const useMainPremiumChannel = ( + userId: string | undefined, + enabled?: boolean, +) => { + const [network] = parseUserId(userId); + const { data: channels } = usePremiumChannelsByOwner(userId, enabled); + return usePremiumChannel(network?.id, channels?.[0], enabled); +}; diff --git a/packages/hooks/feed/usePremiumSubscription.ts b/packages/hooks/feed/usePremiumSubscription.ts index d39affca61..a761d4c86e 100644 --- a/packages/hooks/feed/usePremiumSubscription.ts +++ b/packages/hooks/feed/usePremiumSubscription.ts @@ -1,5 +1,7 @@ import { useQuery } from "@tanstack/react-query"; +import { useMainPremiumChannel } from "./usePremiumChannel"; + import { parseUserId } from "@/networks"; import { mustGetCw721MembershipQueryClient } from "@/utils/feed/client"; @@ -7,25 +9,26 @@ export const usePremiumSubscription = ( channelUserId: string | undefined, subUserId: string | undefined, ) => { + const { data: channel } = useMainPremiumChannel(channelUserId); return useQuery( ["premium-is-subscribed", channelUserId, subUserId], async () => { - const [network, channelAddr] = parseUserId(channelUserId); + const [network] = parseUserId(channelUserId); const [, subAddr] = parseUserId(subUserId); - if (!network || !channelAddr || !subAddr) { + if (!channel || !network || !subAddr) { return null; } const client = await mustGetCw721MembershipQueryClient(network.id); const result = await client.subscription({ - channelAddr, + channelId: channel.id, subAddr, }); return result; }, - { staleTime: Infinity }, + { staleTime: Infinity, enabled: !!channel }, ); }; diff --git a/packages/screens/UserPublicProfile/components/UPPIntro.tsx b/packages/screens/UserPublicProfile/components/UPPIntro.tsx index 716882e520..d16a1d5090 100644 --- a/packages/screens/UserPublicProfile/components/UPPIntro.tsx +++ b/packages/screens/UserPublicProfile/components/UPPIntro.tsx @@ -21,7 +21,7 @@ import { SocialButton } from "@/components/buttons/SocialButton"; import { SocialButtonSecondary } from "@/components/buttons/SocialButtonSecondary"; import { ProfileButton } from "@/components/hub/ProfileButton"; import { UserAvatarWithFrame } from "@/components/images/AvatarWithFrame"; -import { usePremiumChannel } from "@/hooks/feed/usePremiumChannel"; +import { useMainPremiumChannel } from "@/hooks/feed/usePremiumChannel"; import { usePremiumIsSubscribed } from "@/hooks/feed/usePremiumIsSubscribed"; import { useMaxResolution } from "@/hooks/useMaxResolution"; import { useNSUserInfo } from "@/hooks/useNSUserInfo"; @@ -56,7 +56,7 @@ export const UPPIntro: React.FC<{ const [network, userAddress] = parseUserId(userId); const { width } = useMaxResolution(); const { width: windowWidth } = useWindowDimensions(); - const { data: premiumChannel } = usePremiumChannel(network?.id, userAddress); + const { data: premiumChannel } = useMainPremiumChannel(userId); const networkHasPremiumFeature = !!getNetworkFeature( network?.id, NetworkFeature.CosmWasmPremiumFeed, diff --git a/packages/screens/UserPublicProfile/components/modals/PremiumSubscriptionModal.tsx b/packages/screens/UserPublicProfile/components/modals/PremiumSubscriptionModal.tsx index d758aff880..b3b230fac6 100644 --- a/packages/screens/UserPublicProfile/components/modals/PremiumSubscriptionModal.tsx +++ b/packages/screens/UserPublicProfile/components/modals/PremiumSubscriptionModal.tsx @@ -10,7 +10,7 @@ import { UserAvatarWithFrame } from "@/components/images/AvatarWithFrame"; import ModalBase from "@/components/modals/ModalBase"; import { SpacerColumn } from "@/components/spacer"; import { useFeedbacks } from "@/context/FeedbacksProvider"; -import { usePremiumChannel } from "@/hooks/feed/usePremiumChannel"; +import { useMainPremiumChannel } from "@/hooks/feed/usePremiumChannel"; import { useNSUserInfo } from "@/hooks/useNSUserInfo"; import useSelectedWallet from "@/hooks/useSelectedWallet"; import { getUserId, parseUserId } from "@/networks"; @@ -26,8 +26,8 @@ export const PremiumSubscriptionModal: React.FC<{ userId: string; }> = ({ onClose, isVisible, userId }) => { const { metadata } = useNSUserInfo(userId); - const [network, channelAddress] = parseUserId(userId); - const { data: channel } = usePremiumChannel(network?.id, channelAddress); + const [network, channelOwnerAddress] = parseUserId(userId); + const { data: channel } = useMainPremiumChannel(userId); const selectedWallet = useSelectedWallet(); const { wrapWithFeedback } = useFeedbacks(); @@ -62,7 +62,7 @@ export const PremiumSubscriptionModal: React.FC<{ ); await client.subscribe( { - channelAddr: channelAddress, + channelId: channel.id, membershipKind: selectedItemIndex, recipientAddr: selectedWallet.address, }, @@ -97,7 +97,7 @@ export const PremiumSubscriptionModal: React.FC<{ @@ -105,7 +105,7 @@ export const PremiumSubscriptionModal: React.FC<{ {metadata?.tokenId ? metadata?.public_name : DEFAULT_NAME} - @{metadata?.tokenId ? metadata.tokenId : channelAddress} + @{metadata?.tokenId ? metadata.tokenId : channelOwnerAddress} diff --git a/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx b/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx index 39f3e50cb9..705955c7f6 100644 --- a/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx +++ b/packages/screens/UserPublicProfile/components/modals/SubscriptionSetupModal.tsx @@ -20,7 +20,7 @@ import { ChannelResponse, MembershipConfig, } from "@/contracts-clients/cw721-membership"; -import { usePremiumChannel } from "@/hooks/feed/usePremiumChannel"; +import { useMainPremiumChannel } from "@/hooks/feed/usePremiumChannel"; import useSelectedWallet from "@/hooks/useSelectedWallet"; import { getNativeCurrency, getNetworkFeature, parseUserId } from "@/networks"; import { NetworkFeature } from "@/networks/features"; @@ -45,10 +45,10 @@ export const SubscriptionSetupModal: React.FC<{ isVisible: boolean; onClose: () => void; }> = ({ userId, isVisible, onClose }) => { - const [network, channelAddress] = parseUserId(userId); + const [network] = parseUserId(userId); const networkId = network?.id; - const { data: channel } = usePremiumChannel(networkId, channelAddress); + const { data: channel } = useMainPremiumChannel(userId); if (!networkId || channel === undefined) { return null; diff --git a/yarn.lock b/yarn.lock index 5fb9fb0290..f118d52c27 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3728,14 +3728,14 @@ __metadata: languageName: node linkType: hard -"@expo/cli@npm:0.17.8": - version: 0.17.8 - resolution: "@expo/cli@npm:0.17.8" +"@expo/cli@npm:0.17.10": + version: 0.17.10 + resolution: "@expo/cli@npm:0.17.10" dependencies: "@babel/runtime": ^7.20.0 "@expo/code-signing-certificates": 0.0.5 "@expo/config": ~8.5.0 - "@expo/config-plugins": ~7.8.0 + "@expo/config-plugins": ~7.9.0 "@expo/devcert": ^1.0.0 "@expo/env": ~0.2.2 "@expo/image-utils": ^0.4.0 @@ -3744,7 +3744,7 @@ __metadata: "@expo/osascript": ^2.0.31 "@expo/package-manager": ^1.1.1 "@expo/plist": ^0.1.0 - "@expo/prebuild-config": 6.7.4 + "@expo/prebuild-config": 6.8.1 "@expo/rudder-sdk-node": 1.1.1 "@expo/spawn-async": 1.5.0 "@expo/xcpretty": ^4.3.0 @@ -3810,7 +3810,7 @@ __metadata: ws: ^8.12.1 bin: expo-internal: build/bin/cli - checksum: 1feb62d1a8b55db9acac01ba13a02ed70783a1a9fed60ac22aafc196e8a055cb9a7594c9be5d4c3755c5dda2ccff563e404ccfbc039276289190146467bd5b3f + checksum: acd3fd40dc9c6da2755463aca1ab085e01b28ca9efe824f90f75b80765f15ce6023e506232fab4e453b8a43131a0233e2db38869caa2cd7ca2d9e2980d0d9662 languageName: node linkType: hard @@ -3824,7 +3824,32 @@ __metadata: languageName: node linkType: hard -"@expo/config-plugins@npm:7.8.4, @expo/config-plugins@npm:~7.8.0, @expo/config-plugins@npm:~7.8.2": +"@expo/config-plugins@npm:7.9.1, @expo/config-plugins@npm:~7.9.0": + version: 7.9.1 + resolution: "@expo/config-plugins@npm:7.9.1" + dependencies: + "@expo/config-types": ^50.0.0-alpha.1 + "@expo/fingerprint": ^0.6.0 + "@expo/json-file": ~8.3.0 + "@expo/plist": ^0.1.0 + "@expo/sdk-runtime-versions": ^1.0.0 + "@react-native/normalize-color": ^2.0.0 + chalk: ^4.1.2 + debug: ^4.3.1 + find-up: ~5.0.0 + getenv: ^1.0.0 + glob: 7.1.6 + resolve-from: ^5.0.0 + semver: ^7.5.3 + slash: ^3.0.0 + slugify: ^1.6.6 + xcode: ^3.0.1 + xml2js: 0.6.0 + checksum: b685c76a9f01af779494058f5206f7663888fa47191217a1ead24f4bd56315dab5c0865e920e7141f2fd1bf1872c3b973c9987a90c6c02156c1a9e78a2b673e7 + languageName: node + linkType: hard + +"@expo/config-plugins@npm:~7.8.0, @expo/config-plugins@npm:~7.8.2": version: 7.8.4 resolution: "@expo/config-plugins@npm:7.8.4" dependencies: @@ -3856,7 +3881,26 @@ __metadata: languageName: node linkType: hard -"@expo/config@npm:8.5.4, @expo/config@npm:~8.5.0": +"@expo/config@npm:8.5.6": + version: 8.5.6 + resolution: "@expo/config@npm:8.5.6" + dependencies: + "@babel/code-frame": ~7.10.4 + "@expo/config-plugins": ~7.9.0 + "@expo/config-types": ^50.0.0 + "@expo/json-file": ^8.2.37 + getenv: ^1.0.0 + glob: 7.1.6 + require-from-string: ^2.0.2 + resolve-from: ^5.0.0 + semver: 7.5.3 + slugify: ^1.3.4 + sucrase: 3.34.0 + checksum: 869d5018e724f67908d6294b02c0b79925f6683d0430076f36c6b51f6f02ba9c45d53b2ed4882e3fbcb0e30448c62a58e91dda85f4878d74b5b69a33d761a035 + languageName: node + linkType: hard + +"@expo/config@npm:~8.5.0": version: 8.5.4 resolution: "@expo/config@npm:8.5.4" dependencies: @@ -3968,9 +4012,9 @@ __metadata: languageName: node linkType: hard -"@expo/metro-config@npm:0.17.6": - version: 0.17.6 - resolution: "@expo/metro-config@npm:0.17.6" +"@expo/metro-config@npm:0.17.7": + version: 0.17.7 + resolution: "@expo/metro-config@npm:0.17.7" dependencies: "@babel/core": ^7.20.0 "@babel/generator": ^7.20.5 @@ -3994,7 +4038,7 @@ __metadata: sucrase: 3.34.0 peerDependencies: "@react-native/babel-preset": "*" - checksum: 6ff4625b7238cddbdb18ca13a0d69e0618cb12d3984122fb05cbdf913b0c2c12ba7cefe8bc7dbf26a25b42b3be01f1bf8be2f5ae8e91425b5c4b3427f91797b7 + checksum: 2e1ff484e286a2d66bbbf925c42609ef347cb84ee66fcb567ddad7effc3f833d448ae955c98624926b7895c1eeaedd100cb4e325973c40288183131b39e4d76e languageName: node linkType: hard @@ -4098,6 +4142,26 @@ __metadata: languageName: node linkType: hard +"@expo/prebuild-config@npm:6.8.1": + version: 6.8.1 + resolution: "@expo/prebuild-config@npm:6.8.1" + dependencies: + "@expo/config": ~8.5.0 + "@expo/config-plugins": ~7.9.0 + "@expo/config-types": ^50.0.0-alpha.1 + "@expo/image-utils": ^0.4.0 + "@expo/json-file": ^8.2.37 + debug: ^4.3.1 + fs-extra: ^9.0.0 + resolve-from: ^5.0.0 + semver: 7.5.3 + xml2js: 0.6.0 + peerDependencies: + expo-modules-autolinking: ">=0.8.1" + checksum: 8874e73fd44224ce7dd554308ec652763c9d64d870be0390fd09a59b90ad2e5dbc2492d8b9cf2b50d880983c657ccb53541774ca988cf993611f9690779c9a7f + languageName: node + linkType: hard + "@expo/rudder-sdk-node@npm:1.1.1": version: 1.1.1 resolution: "@expo/rudder-sdk-node@npm:1.1.1" @@ -7858,9 +7922,9 @@ __metadata: languageName: node linkType: hard -"babel-preset-expo@npm:~10.0.1": - version: 10.0.1 - resolution: "babel-preset-expo@npm:10.0.1" +"babel-preset-expo@npm:~10.0.2": + version: 10.0.2 + resolution: "babel-preset-expo@npm:10.0.2" dependencies: "@babel/plugin-proposal-decorators": ^7.12.9 "@babel/plugin-transform-export-namespace-from": ^7.22.11 @@ -7871,7 +7935,7 @@ __metadata: "@react-native/babel-preset": ^0.73.18 babel-plugin-react-native-web: ~0.18.10 react-refresh: 0.14.0 - checksum: 3786192e3531e7cc261a65fbaec015edb1399b4cabea4fed2456bb6c2fdf3f48ed97283626e8e76a5554cf8ca9df55c83d6309932696ab82a81a4b9848d406fe + checksum: c0436a8dd5fd570408331021c08f066467bade9f7c3024f552a7ace5798b3153bdfc9c028371122e6125c091844fc3d2ff11c771b6cc96d84e8468bc4fba3797 languageName: node linkType: hard @@ -11090,12 +11154,12 @@ __metadata: languageName: node linkType: hard -"expo-file-system@npm:~16.0.8": - version: 16.0.8 - resolution: "expo-file-system@npm:16.0.8" +"expo-file-system@npm:~16.0.9": + version: 16.0.9 + resolution: "expo-file-system@npm:16.0.9" peerDependencies: expo: "*" - checksum: d69baf26f4463a144841f5994e280417f3225573e73d3be51be52cbac01863cc69750d7aa978b6ec965de3766b9ddfe9963b35cf24d6add16b5136268a01acbe + checksum: 88444c3772baf2a0000e8131287f653fc15b98dc0ea2048d4c95af78077bd5227aa4e47d4b04fb0c40b8160fbf1d4ccb8260d63f70c3f16881c95e8889ef75ca languageName: node linkType: hard @@ -11246,19 +11310,19 @@ __metadata: languageName: node linkType: hard -"expo@npm:~50.0.15": - version: 50.0.15 - resolution: "expo@npm:50.0.15" +"expo@npm:~50.0.17": + version: 50.0.17 + resolution: "expo@npm:50.0.17" dependencies: "@babel/runtime": ^7.20.0 - "@expo/cli": 0.17.8 - "@expo/config": 8.5.4 - "@expo/config-plugins": 7.8.4 - "@expo/metro-config": 0.17.6 + "@expo/cli": 0.17.10 + "@expo/config": 8.5.6 + "@expo/config-plugins": 7.9.1 + "@expo/metro-config": 0.17.7 "@expo/vector-icons": ^14.0.0 - babel-preset-expo: ~10.0.1 + babel-preset-expo: ~10.0.2 expo-asset: ~9.0.2 - expo-file-system: ~16.0.8 + expo-file-system: ~16.0.9 expo-font: ~11.10.3 expo-keep-awake: ~12.8.2 expo-modules-autolinking: 1.10.3 @@ -11267,7 +11331,7 @@ __metadata: whatwg-url-without-unicode: 8.0.0-3 bin: expo: bin/cli - checksum: d03afd04ba150ede3a565118ca6bea63bb51f6b5310190e5e78f7597f518a0c95b5f34c9bd11d93d980cd1c375887f7ee671911c0bfd57b2b35ed68eebcaa011 + checksum: a41254c18b86bb012e8e6152f0d8aa4a92d1b209209bdd9f672f82e1bdc65546a3c4bfee22f82185ad01811427873a1b6574208f38a64063cb6a9ad2f74127af languageName: node linkType: hard @@ -19270,14 +19334,14 @@ __metadata: eslint-config-universe: ^12.0.0 eslint-plugin-react-hooks: ^4.6.0 ethers: ^5.7.2 - expo: ~50.0.15 + expo: ~50.0.17 expo-av: ~13.10.5 expo-blur: ~12.9.2 expo-clipboard: ~5.0.1 expo-dev-client: ~3.3.11 expo-doctor: ^1.1.3 expo-document-picker: ~11.10.1 - expo-file-system: ~16.0.8 + expo-file-system: ~16.0.9 expo-font: ~11.10.3 expo-image-picker: ^14.7.1 expo-linear-gradient: ~12.7.2