@@ -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+ }
158201pub 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+ }
0 commit comments