Skip to content

Commit 9a0cf71

Browse files
committed
feat: make sure deductions are made in sellable buy logic
1 parent a14fe72 commit 9a0cf71

File tree

4 files changed

+216
-13
lines changed

4 files changed

+216
-13
lines changed

contracts/asset/src/execute.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -641,12 +641,13 @@ fn test_buy() {
641641
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
642642
use cosmwasm_std::{Empty, coin, coins};
643643

644-
// successful buy transfers ownership, pays seller, removes listing, and emits attributes
644+
// successful buy transfers ownership, pays seller, pays royalties, removes listing, and emits attributes
645645
{
646646
let mut deps = mock_dependencies();
647647
let env = mock_env();
648648
let seller_addr = deps.api.addr_make("seller");
649649
let buyer_addr = deps.api.addr_make("buyer");
650+
let owner_addr = deps.api.addr_make("owner");
650651
let nft_info = NftInfo {
651652
owner: seller_addr.clone(),
652653
approvals: vec![],
@@ -682,7 +683,7 @@ fn test_buy() {
682683
&message_info(&buyer_addr, &[price.clone()]),
683684
"token-1".to_string(),
684685
None,
685-
vec![(seller_addr.to_string(), coin(10 as u128, "uxion"), "marketplace_fee".to_string())],
686+
vec![(owner_addr.to_string(), coin(10 as u128, "uxion"), "royalties".to_string())],
686687
)
687688
.unwrap();
688689

@@ -694,7 +695,7 @@ fn test_buy() {
694695
assert_eq!(
695696
attrs,
696697
vec![
697-
("marketplace_fee".to_string(), "100".to_string()),
698+
("marketplace_fee".to_string(), "1".to_string()),
698699
("action".to_string(), "buy".to_string()),
699700
("id".to_string(), "token-1".to_string()),
700701
("price".to_string(), price.amount.to_string()),
@@ -715,7 +716,7 @@ fn test_buy() {
715716
amount: coins(1 as u128, "uxion"),
716717
}),CosmosMsg::Bank(BankMsg::Send {
717718
to_address: seller_addr.to_string(),
718-
amount: coins(99 as u128, "uxion"),
719+
amount: coins(89 as u128, "uxion"),
719720
})],
720721
);
721722

contracts/asset/src/plugin.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ use crate::{
2020
msg::AssetExtensionExecuteMsg,
2121
plugin,
2222
state::{AssetConfig, Reserve},
23-
traits::{AssetContract, DefaultAssetContract},
23+
traits::{AssetContract, DefaultAssetContract, SellableAsset},
2424
};
2525

2626
/// Shared context passed through the pipeline, mutated by plugins.
@@ -438,6 +438,62 @@ where
438438
TNftExtensionMsg: Cw721CustomMsg,
439439
TNftExtensionMsg: StateFactory<TNftExtension>,
440440
{
441+
fn execute_pluggable(
442+
&self,
443+
deps: DepsMut,
444+
env: &Env,
445+
info: &MessageInfo,
446+
msg: Cw721ExecuteMsg<TNftExtensionMsg, TCollectionExtensionMsg, AssetExtensionExecuteMsg>,
447+
) -> Result<Response<Empty>, Cw721ContractError> {
448+
let plugin_response: Response<Empty>;
449+
let plugin_ctx_deductions: Vec<(String, Coin, String)>;
450+
{
451+
let mut plugin_ctx = Self::get_plugin_ctx(deps.as_ref(), env, info);
452+
453+
match &msg {
454+
Cw721ExecuteMsg::TransferNft {
455+
recipient,
456+
token_id,
457+
} => self.on_transfer_plugin(&recipient, &token_id, &mut plugin_ctx)?,
458+
Cw721ExecuteMsg::UpdateExtension { msg } => {
459+
self.on_update_extension_plugin(&msg, &mut plugin_ctx)?
460+
}
461+
_ => true,
462+
};
463+
plugin_response = plugin_ctx.response;
464+
plugin_ctx_deductions = plugin_ctx.deductions.clone();
465+
};
466+
let mut response = match &msg {
467+
Cw721ExecuteMsg::UpdateExtension { msg: extension_msg } => {
468+
match extension_msg {
469+
AssetExtensionExecuteMsg::Buy { token_id, recipient } => {
470+
self.buy(deps, env, info, (*token_id).clone(), (*recipient).clone(), plugin_ctx_deductions)?
471+
},
472+
_ => self.execute(deps, env, info, msg)?,
473+
}
474+
},
475+
_ => self.execute(deps, env, info, msg)?,
476+
};
477+
478+
response.messages.extend(plugin_response.messages);
479+
response.events.extend(plugin_response.events);
480+
response.attributes.extend(plugin_response.attributes);
481+
482+
if let Some(plugin_data) = plugin_response.data {
483+
match &mut response.data {
484+
Some(existing) => {
485+
let mut combined = Vec::with_capacity(existing.len() + plugin_data.len());
486+
combined.extend_from_slice(existing.as_slice());
487+
combined.extend_from_slice(plugin_data.as_slice());
488+
*existing = Binary::from(combined);
489+
}
490+
None => response.data = Some(plugin_data),
491+
}
492+
}
493+
494+
Ok(response)
495+
}
496+
441497
fn on_update_extension_plugin<'a>(
442498
&self,
443499
msg: &AssetExtensionExecuteMsg,

contracts/asset/src/test.rs

Lines changed: 151 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ mod asset_pluggable_tests {
451451
let token_id = "token-1".to_string();
452452
let price = Coin::new(100u128, "uxion");
453453

454-
let result = contract.on_list_plugin(&token_id, &price, &None, &None,&mut ctx);
454+
let result = contract.on_list_plugin(&token_id, &price, &None, &None, &mut ctx);
455455

456456
assert_eq!(
457457
result.expect_err("expected min price error").to_string(),
@@ -1154,3 +1154,153 @@ mod query_test {
11541154
}));
11551155
}
11561156
}
1157+
1158+
#[cfg(test)]
1159+
mod asset_pluggable_sellable_test {
1160+
use std::collections::HashMap;
1161+
1162+
use crate::{
1163+
msg::AssetExtensionExecuteMsg,
1164+
plugin::{PluggableAsset, Plugin},
1165+
state::{AssetConfig, ListingInfo},
1166+
traits::DefaultAssetContract,
1167+
};
1168+
use cosmwasm_std::{
1169+
BankMsg, Coin, CosmosMsg, Empty,
1170+
testing::{message_info, mock_dependencies, mock_env},
1171+
};
1172+
use cw721::{msg::Cw721ExecuteMsg, state::NftInfo};
1173+
1174+
#[test]
1175+
fn buy_deducts_marketplace_and_royalty_fees() {
1176+
let mut deps = mock_dependencies();
1177+
let mut contract: DefaultAssetContract<'static, Empty, Empty, Empty, Empty> =
1178+
Default::default();
1179+
1180+
let seller = deps.api.addr_make("seller");
1181+
let buyer = deps.api.addr_make("buyer");
1182+
let marketplace = deps.api.addr_make("marketplace");
1183+
let royalty_recipient = deps.api.addr_make("artist");
1184+
1185+
let price = Coin::new(1_000u128, "uxion");
1186+
1187+
let listing = ListingInfo {
1188+
id: "token-1".to_string(),
1189+
price: price.clone(),
1190+
seller: seller.clone(),
1191+
reserved: None,
1192+
nft_info: NftInfo {
1193+
owner: seller.clone(),
1194+
approvals: vec![],
1195+
token_uri: None,
1196+
extension: Empty::default(),
1197+
},
1198+
marketplace_fee_bps: Some(1_000),
1199+
marketplace_fee_recipient: Some(marketplace.clone()),
1200+
};
1201+
1202+
contract
1203+
.config
1204+
.listings
1205+
.save(deps.as_mut().storage, "token-1", &listing)
1206+
.unwrap();
1207+
contract
1208+
.config
1209+
.cw721_config
1210+
.nft_info
1211+
.save(deps.as_mut().storage, "token-1", &listing.nft_info)
1212+
.unwrap();
1213+
contract
1214+
.config
1215+
.collection_plugins
1216+
.save(
1217+
deps.as_mut().storage,
1218+
"Royalty",
1219+
&Plugin::Royalty {
1220+
bps: 500,
1221+
recipient: royalty_recipient.clone(),
1222+
on_primary: true,
1223+
},
1224+
)
1225+
.unwrap();
1226+
1227+
let env = mock_env();
1228+
let info = message_info(&buyer, &[price.clone()]);
1229+
1230+
let res = contract
1231+
.execute_pluggable(
1232+
deps.as_mut(),
1233+
&env,
1234+
&info,
1235+
Cw721ExecuteMsg::UpdateExtension {
1236+
msg: AssetExtensionExecuteMsg::Buy {
1237+
token_id: "token-1".to_string(),
1238+
recipient: None,
1239+
},
1240+
},
1241+
)
1242+
.unwrap();
1243+
1244+
assert_eq!(res.messages.len(), 3);
1245+
1246+
let mut marketplace_paid = None;
1247+
let mut seller_paid = None;
1248+
let mut royalty_paid = None;
1249+
1250+
for msg in &res.messages {
1251+
match &msg.msg {
1252+
CosmosMsg::Bank(BankMsg::Send { to_address, amount }) => {
1253+
let coin = amount
1254+
.first()
1255+
.cloned()
1256+
.expect("send message must include funds");
1257+
if *to_address == marketplace.to_string() {
1258+
marketplace_paid = Some(coin);
1259+
} else if *to_address == seller.to_string() {
1260+
seller_paid = Some(coin);
1261+
} else if *to_address == royalty_recipient.to_string() {
1262+
royalty_paid = Some(coin);
1263+
} else {
1264+
panic!("unexpected recipient {}", to_address);
1265+
}
1266+
}
1267+
other => panic!("unexpected message: {:?}", other),
1268+
}
1269+
}
1270+
1271+
assert_eq!(
1272+
marketplace_paid.expect("marketplace fee"),
1273+
Coin::new(100u128, "uxion"),
1274+
);
1275+
assert_eq!(
1276+
royalty_paid.expect("royalty fee"),
1277+
Coin::new(50u128, "uxion"),
1278+
);
1279+
assert_eq!(
1280+
seller_paid.expect("seller payment"),
1281+
Coin::new(850u128, "uxion"),
1282+
);
1283+
1284+
let attrs: HashMap<_, _> = res
1285+
.attributes
1286+
.iter()
1287+
.map(|attr| (attr.key.clone(), attr.value.clone()))
1288+
.collect();
1289+
assert_eq!(attrs.get("marketplace_fee"), Some(&"100".to_string()));
1290+
assert_eq!(
1291+
attrs.get("royalty_amount"),
1292+
Some(&Coin::new(50u128, "uxion").to_string()),
1293+
);
1294+
assert_eq!(
1295+
attrs.get("royalty_recipient"),
1296+
Some(&royalty_recipient.to_string()),
1297+
);
1298+
1299+
let stored_nft = AssetConfig::<Empty>::default()
1300+
.cw721_config
1301+
.nft_info
1302+
.load(deps.as_ref().storage, "token-1")
1303+
.unwrap();
1304+
assert_eq!(stored_nft.owner, buyer);
1305+
}
1306+
}

contracts/asset/src/traits.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ impl<
188188
TCollectionExtension,
189189
TCollectionExtensionMsg,
190190
TExtentionMsg,
191-
TCustomResponseMsg,
192191
>
193192
SellableAsset<
194193
'a,
@@ -197,7 +196,7 @@ impl<
197196
TCollectionExtension,
198197
TCollectionExtensionMsg,
199198
TExtentionMsg,
200-
TCustomResponseMsg,
199+
Empty,
201200
>
202201
for AssetContract<
203202
'a,
@@ -214,7 +213,6 @@ where
214213
TCollectionExtension: FromAttributesState + ToAttributesState,
215214
TCollectionExtensionMsg: StateFactory<TCollectionExtension> + Cw721CustomMsg,
216215
TCollectionExtensionMsg: Default,
217-
TCustomResponseMsg: CustomMsg,
218216
{
219217
}
220218

@@ -224,15 +222,14 @@ impl<
224222
TNftExtensionMsg,
225223
TCollectionExtension,
226224
TCollectionExtensionMsg,
227-
TCustomResponseMsg,
228225
>
229226
Cw721Execute<
230227
TNftExtension,
231228
TNftExtensionMsg,
232229
TCollectionExtension,
233230
TCollectionExtensionMsg,
234231
AssetExtensionExecuteMsg,
235-
TCustomResponseMsg,
232+
Empty,
236233
>
237234
for DefaultAssetContract<
238235
'a,
@@ -244,7 +241,6 @@ impl<
244241
where
245242
TNftExtension: Cw721State,
246243
TNftExtensionMsg: StateFactory<TNftExtension> + Cw721CustomMsg,
247-
TCustomResponseMsg: CustomMsg,
248244
TCollectionExtensionMsg: StateFactory<TCollectionExtension> + Cw721CustomMsg,
249245
TCollectionExtension: Cw721State,
250246
TCollectionExtension: FromAttributesState + ToAttributesState,
@@ -256,7 +252,7 @@ where
256252
env: &Env,
257253
info: &MessageInfo,
258254
msg: AssetExtensionExecuteMsg,
259-
) -> Result<Response<TCustomResponseMsg>, cw721::error::Cw721ContractError> {
255+
) -> Result<Response<Empty>, cw721::error::Cw721ContractError> {
260256
match msg {
261257
AssetExtensionExecuteMsg::List {
262258
token_id,

0 commit comments

Comments
 (0)