Skip to content

Commit 3ea52cd

Browse files
committed
chore: add unreserve endpoint to asset contract
1 parent f7a8913 commit 3ea52cd

File tree

4 files changed

+296
-1
lines changed

4 files changed

+296
-1
lines changed

contracts/asset/src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ pub enum ContractError {
1616
#[error("Reserved asset: {id}")]
1717
ReservedAsset { id: String }, // e.g. listing is reserved
1818

19+
#[error("Reservation not found: {id}")]
20+
ReservationNotFound { id: String },
21+
1922
#[error("Invalid listing price: {price}")]
2023
InvalidListingPrice { price: u128 },
2124

contracts/asset/src/execute.rs

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,49 @@ where
155155
.add_attribute("reserver", reservation.reserver.to_string())
156156
.add_attribute("reserved_until", reservation.reserved_until.to_string()))
157157
}
158+
pub fn unreserve<TNftExtension, TCustomResponseMsg>(
159+
deps: DepsMut,
160+
env: &Env,
161+
info: &MessageInfo,
162+
id: String,
163+
delist: bool,
164+
) -> Result<Response<TCustomResponseMsg>, ContractError>
165+
where
166+
TNftExtension: Cw721State,
167+
TCustomResponseMsg: CustomMsg,
168+
{
169+
let asset_config = AssetConfig::<TNftExtension>::default();
170+
171+
let mut listing = asset_config
172+
.listings
173+
.may_load(deps.storage, &id)?
174+
.ok_or_else(|| ContractError::ListingNotFound { id: id.clone() })?;
175+
176+
let reserved = listing
177+
.reserved
178+
.as_ref()
179+
.ok_or_else(|| ContractError::ReservationNotFound { id: id.clone() })?;
180+
181+
if reserved.reserver != info.sender {
182+
return Err(ContractError::Unauthorized {});
183+
}
184+
185+
let response = Response::<TCustomResponseMsg>::default()
186+
.add_attribute("action", "unreserve")
187+
.add_attribute("id", id.clone())
188+
.add_attribute("collection", env.contract.address.clone())
189+
.add_attribute("reserver", info.sender.to_string());
190+
191+
if delist {
192+
asset_config.listings.remove(deps.storage, &id)?;
193+
return Ok(response.add_attribute("delisted", "true"));
194+
}
195+
196+
listing.reserved = None;
197+
asset_config.listings.save(deps.storage, &id, &listing)?;
198+
199+
Ok(response.add_attribute("delisted", "false"))
200+
}
158201
pub fn buy<TNftExtension, TCustomResponseMsg>(
159202
deps: DepsMut,
160203
_env: &Env,
@@ -1224,3 +1267,234 @@ fn test_reserve() {
12241267
);
12251268
}
12261269
}
1270+
1271+
#[test]
1272+
fn test_unreserve() {
1273+
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
1274+
use cosmwasm_std::{Coin, Empty};
1275+
1276+
// reserver can remove reservation while keeping listing active
1277+
{
1278+
let mut deps = mock_dependencies();
1279+
let env = mock_env();
1280+
let owner_addr = deps.api.addr_make("owner");
1281+
let reserver_addr = deps.api.addr_make("reserver");
1282+
let nft_info = NftInfo {
1283+
owner: owner_addr.clone(),
1284+
approvals: vec![],
1285+
token_uri: None,
1286+
extension: Empty {},
1287+
};
1288+
expect_ok(AssetConfig::<Empty>::default().cw721_config.nft_info.save(
1289+
deps.as_mut().storage,
1290+
"token-1",
1291+
&nft_info,
1292+
));
1293+
1294+
let reservation = Reserve {
1295+
reserver: reserver_addr.clone(),
1296+
reserved_until: Expiration::AtHeight(env.block.height + 10),
1297+
};
1298+
1299+
expect_ok(AssetConfig::<Empty>::default().listings.save(
1300+
deps.as_mut().storage,
1301+
"token-1",
1302+
&ListingInfo {
1303+
id: "token-1".to_string(),
1304+
seller: owner_addr.clone(),
1305+
price: Coin::new(100 as u128, "uxion"),
1306+
reserved: Some(reservation.clone()),
1307+
marketplace_fee_bps: None,
1308+
marketplace_fee_recipient: None,
1309+
},
1310+
));
1311+
1312+
let response = expect_ok(unreserve::<Empty, Empty>(
1313+
deps.as_mut(),
1314+
&env,
1315+
&message_info(&reserver_addr, &[]),
1316+
"token-1".to_string(),
1317+
false,
1318+
));
1319+
1320+
let attrs: Vec<(String, String)> = response
1321+
.attributes
1322+
.iter()
1323+
.map(|attr| (attr.key.clone(), attr.value.clone()))
1324+
.collect();
1325+
assert_eq!(
1326+
attrs,
1327+
vec![
1328+
("action".to_string(), "unreserve".to_string()),
1329+
("id".to_string(), "token-1".to_string()),
1330+
("collection".to_string(), env.contract.address.to_string()),
1331+
("reserver".to_string(), reserver_addr.to_string()),
1332+
("delisted".to_string(), "false".to_string()),
1333+
],
1334+
);
1335+
1336+
let stored = expect_ok(
1337+
AssetConfig::<Empty>::default()
1338+
.listings
1339+
.load(deps.as_ref().storage, "token-1"),
1340+
);
1341+
assert!(stored.reserved.is_none());
1342+
}
1343+
1344+
// reserver can delist when requested
1345+
{
1346+
let mut deps = mock_dependencies();
1347+
let env = mock_env();
1348+
let owner_addr = deps.api.addr_make("owner");
1349+
let reserver_addr = deps.api.addr_make("reserver");
1350+
let nft_info = NftInfo {
1351+
owner: owner_addr.clone(),
1352+
approvals: vec![],
1353+
token_uri: None,
1354+
extension: Empty {},
1355+
};
1356+
expect_ok(AssetConfig::<Empty>::default().cw721_config.nft_info.save(
1357+
deps.as_mut().storage,
1358+
"token-2",
1359+
&nft_info,
1360+
));
1361+
1362+
expect_ok(AssetConfig::<Empty>::default().listings.save(
1363+
deps.as_mut().storage,
1364+
"token-2",
1365+
&ListingInfo {
1366+
id: "token-2".to_string(),
1367+
seller: owner_addr.clone(),
1368+
price: Coin::new(150 as u128, "uxion"),
1369+
reserved: Some(Reserve {
1370+
reserver: reserver_addr.clone(),
1371+
reserved_until: Expiration::AtHeight(env.block.height + 10),
1372+
}),
1373+
marketplace_fee_bps: None,
1374+
marketplace_fee_recipient: None,
1375+
},
1376+
));
1377+
1378+
let response = expect_ok(unreserve::<Empty, Empty>(
1379+
deps.as_mut(),
1380+
&env,
1381+
&message_info(&reserver_addr, &[]),
1382+
"token-2".to_string(),
1383+
true,
1384+
));
1385+
1386+
let attrs: Vec<(String, String)> = response
1387+
.attributes
1388+
.iter()
1389+
.map(|attr| (attr.key.clone(), attr.value.clone()))
1390+
.collect();
1391+
assert_eq!(
1392+
attrs,
1393+
vec![
1394+
("action".to_string(), "unreserve".to_string()),
1395+
("id".to_string(), "token-2".to_string()),
1396+
("collection".to_string(), env.contract.address.to_string()),
1397+
("reserver".to_string(), reserver_addr.to_string()),
1398+
("delisted".to_string(), "true".to_string()),
1399+
],
1400+
);
1401+
1402+
let stored = expect_ok(
1403+
AssetConfig::<Empty>::default()
1404+
.listings
1405+
.may_load(deps.as_ref().storage, "token-2"),
1406+
);
1407+
assert!(stored.is_none());
1408+
}
1409+
1410+
// non-reserver cannot unreserve
1411+
{
1412+
let mut deps = mock_dependencies();
1413+
let env = mock_env();
1414+
let owner_addr = deps.api.addr_make("owner");
1415+
let reserver_addr = deps.api.addr_make("reserver");
1416+
let intruder_addr = deps.api.addr_make("intruder");
1417+
let nft_info = NftInfo {
1418+
owner: owner_addr.clone(),
1419+
approvals: vec![],
1420+
token_uri: None,
1421+
extension: Empty {},
1422+
};
1423+
expect_ok(AssetConfig::<Empty>::default().cw721_config.nft_info.save(
1424+
deps.as_mut().storage,
1425+
"token-3",
1426+
&nft_info,
1427+
));
1428+
1429+
expect_ok(AssetConfig::<Empty>::default().listings.save(
1430+
deps.as_mut().storage,
1431+
"token-3",
1432+
&ListingInfo {
1433+
id: "token-3".to_string(),
1434+
seller: owner_addr.clone(),
1435+
price: Coin::new(200 as u128, "uxion"),
1436+
reserved: Some(Reserve {
1437+
reserver: reserver_addr.clone(),
1438+
reserved_until: Expiration::AtHeight(env.block.height + 10),
1439+
}),
1440+
marketplace_fee_bps: None,
1441+
marketplace_fee_recipient: None,
1442+
},
1443+
));
1444+
1445+
let err = expect_err(unreserve::<Empty, Empty>(
1446+
deps.as_mut(),
1447+
&env,
1448+
&message_info(&intruder_addr, &[]),
1449+
"token-3".to_string(),
1450+
false,
1451+
));
1452+
assert_eq!(err, ContractError::Unauthorized {});
1453+
}
1454+
1455+
// cannot unreserve when listing not reserved
1456+
{
1457+
let mut deps = mock_dependencies();
1458+
let env = mock_env();
1459+
let owner_addr = deps.api.addr_make("owner");
1460+
let reserver_addr = deps.api.addr_make("reserver");
1461+
let nft_info = NftInfo {
1462+
owner: owner_addr.clone(),
1463+
approvals: vec![],
1464+
token_uri: None,
1465+
extension: Empty {},
1466+
};
1467+
expect_ok(AssetConfig::<Empty>::default().cw721_config.nft_info.save(
1468+
deps.as_mut().storage,
1469+
"token-4",
1470+
&nft_info,
1471+
));
1472+
1473+
expect_ok(AssetConfig::<Empty>::default().listings.save(
1474+
deps.as_mut().storage,
1475+
"token-4",
1476+
&ListingInfo {
1477+
id: "token-4".to_string(),
1478+
seller: owner_addr.clone(),
1479+
price: Coin::new(250 as u128, "uxion"),
1480+
reserved: None,
1481+
marketplace_fee_bps: None,
1482+
marketplace_fee_recipient: None,
1483+
},
1484+
));
1485+
1486+
let err = expect_err(unreserve::<Empty, Empty>(
1487+
deps.as_mut(),
1488+
&env,
1489+
&message_info(&reserver_addr, &[]),
1490+
"token-4".to_string(),
1491+
false,
1492+
));
1493+
assert_eq!(
1494+
err,
1495+
ContractError::ReservationNotFound {
1496+
id: "token-4".to_string()
1497+
}
1498+
);
1499+
}
1500+
}

contracts/asset/src/msg.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ pub enum AssetExtensionExecuteMsg {
1818
token_id: String,
1919
reservation: Reserve,
2020
},
21+
UnReserve {
22+
token_id: String,
23+
delist: Option<bool>,
24+
},
2125
Delist {
2226
token_id: String,
2327
},

contracts/asset/src/traits.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::marker::PhantomData;
22

33
use crate::{
44
error::ContractError,
5-
execute::{buy, delist, list, reserve},
5+
execute::{buy, delist, list, reserve, unreserve},
66
msg::{AssetExtensionExecuteMsg, AssetExtensionQueryMsg},
77
plugin::PluggableAsset,
88
state::{AssetConfig, Reserve},
@@ -168,6 +168,17 @@ pub trait SellableAsset<
168168
) -> Result<Response<TCustomResponseMsg>, ContractError> {
169169
reserve::<TNftExtension, TCustomResponseMsg>(deps, env, info, id, reservation)
170170
}
171+
fn unreserve(
172+
&self,
173+
deps: DepsMut,
174+
env: &Env,
175+
info: &MessageInfo,
176+
id: String,
177+
delist: bool,
178+
) -> Result<Response<TCustomResponseMsg>, ContractError> {
179+
unreserve::<TNftExtension, TCustomResponseMsg>(deps, env, info, id, delist)
180+
}
181+
171182
fn buy(
172183
&self,
173184
deps: DepsMut,
@@ -268,6 +279,9 @@ where
268279
token_id,
269280
reservation,
270281
} => Ok(self.reserve(deps, env, info, token_id, reservation)?),
282+
AssetExtensionExecuteMsg::UnReserve { token_id, delist } => {
283+
Ok(self.unreserve(deps, env, info, token_id, delist.unwrap_or(false))?)
284+
}
271285
AssetExtensionExecuteMsg::Delist { token_id } => {
272286
Ok(self.delist(deps, env, info, token_id)?)
273287
}

0 commit comments

Comments
 (0)