diff --git a/Cargo.toml b/Cargo.toml index b4adfcb4..f100f957 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,8 @@ members = [ "contracts/multisig/interact", "contracts/mystery-box", "contracts/mystery-box/meta", + "contracts/nft-escrow", + "contracts/nft-escrow/meta", "contracts/nft-minter", "contracts/nft-minter/meta", "contracts/nft-storage-prepay", diff --git a/contracts/nft-escrow/.gitignore b/contracts/nft-escrow/.gitignore new file mode 100644 index 00000000..2c76bc98 --- /dev/null +++ b/contracts/nft-escrow/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +*/target/ + +# The mxpy output +/output*/ diff --git a/contracts/nft-escrow/Cargo.toml b/contracts/nft-escrow/Cargo.toml new file mode 100644 index 00000000..1631740b --- /dev/null +++ b/contracts/nft-escrow/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "nft-escrow" +version = "0.0.0" +authors = [ "you",] +edition = "2018" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies.multiversx-sc] +version = "0.43.4" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.43.4" diff --git a/contracts/nft-escrow/meta/Cargo.toml b/contracts/nft-escrow/meta/Cargo.toml new file mode 100644 index 00000000..cf1cbcfd --- /dev/null +++ b/contracts/nft-escrow/meta/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nft-escrow-meta" +version = "0.0.0" +edition = "2018" +publish = false +authors = [ "you",] + +[dev-dependencies] + +[dependencies.nft-escrow] +path = ".." + +[dependencies.multiversx-sc-meta] +version = "0.43.4" diff --git a/contracts/nft-escrow/meta/src/main.rs b/contracts/nft-escrow/meta/src/main.rs new file mode 100644 index 00000000..1400efa8 --- /dev/null +++ b/contracts/nft-escrow/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta::cli_main::(); +} diff --git a/contracts/nft-escrow/multiversx.json b/contracts/nft-escrow/multiversx.json new file mode 100644 index 00000000..73655396 --- /dev/null +++ b/contracts/nft-escrow/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/contracts/nft-escrow/mxsc-template.toml b/contracts/nft-escrow/mxsc-template.toml new file mode 100644 index 00000000..441addd7 --- /dev/null +++ b/contracts/nft-escrow/mxsc-template.toml @@ -0,0 +1,18 @@ +name = "empty" +contract_trait = "EmptyContract" +src_file = "empty.rs" +rename_pairs = [ + [ + "blockchain.set_current_dir_from_workspace(\"contracts/examples/empty\");", + "// blockchain.set_current_dir_from_workspace(\"relative path to your workspace, if applicable\");", + ], +] +files_include = [ + "meta", + "scenarios", + "src", + "tests", + "wasm/Cargo.toml", + "Cargo.toml", + "multiversx.json", +] diff --git a/contracts/nft-escrow/scenarios/accept.scen.json b/contracts/nft-escrow/scenarios/accept.scen.json new file mode 100644 index 00000000..2ed7bd29 --- /dev/null +++ b/contracts/nft-escrow/scenarios/accept.scen.json @@ -0,0 +1,254 @@ +{ + "name": "buy nft", + "steps": [ + { + "step": "externalSteps", + "path": "escrow.scen.json" + }, + { + "step": "scCall", + "id": "accept-offer-not-exists", + "tx": { + "from": "address:second", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "accept", + "arguments": [ + "2" + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "4", + "message": "str:Offer does not exist", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "accept-offer-only-wanted", + "tx": { + "from": "address:first", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "accept", + "arguments": [ + "1" + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "4", + "message": "str:Can not accept this offer", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "accept-other-token", + "tx": { + "from": "address:second", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "accept", + "arguments": [ + "1" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:SEMIFUNG-123456", + "value": "1000", + "nonce": "1" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "4", + "message": "str:NFT does not match", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "accept-other-nonce", + "tx": { + "from": "address:second", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "accept", + "arguments": [ + "1" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:NFT2-654321", + "value": "1", + "nonce": "1" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "4", + "message": "str:NFT does not match", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "accept-other-amount", + "tx": { + "from": "address:second", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "accept", + "arguments": [ + "1" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:NFT2-654321", + "value": "2", + "nonce": "2" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "4", + "message": "str:NFT does not match", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "accept", + "tx": { + "from": "address:second", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "accept", + "arguments": [ + "1" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:NFT2-654321", + "value": "1", + "nonce": "2" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "0", + "gas": "*", + "refund": "*" + } + }, + { + "step": "checkState", + "accounts": { + "address:first": { + "nonce": "6", + "balance": "0", + "esdt": { + "str:FUNG-123456": "1000", + "str:SEMIFUNG-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1000" + } + ] + }, + "str:NFT-123456": { + "instances": [ + { + "nonce": "2", + "balance": "1" + } + ] + }, + "str:NFT2-654321": { + "instances": [ + { + "nonce": "2", + "balance": "1" + } + ] + } + } + }, + "address:second": { + "nonce": "5", + "balance": "0", + "esdt": { + "str:FUNG-123456": "1000", + "str:SEMIFUNG-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1000" + } + ] + }, + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1" + } + ] + }, + "str:NFT2-654321": { + "instances": [ + { + "nonce": "1", + "balance": "1" + }, + { + "nonce": "2", + "balance": "1" + } + ] + } + } + }, + "sc:nft-escrow": { + "nonce": "0", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "instances": [] + } + }, + "storage": { + "str:lastOfferId": "1" + }, + "code": "file:../output/nft-escrow.wasm", + "owner": "address:owner" + }, + "+": "" + } + } + ] +} diff --git a/contracts/nft-escrow/scenarios/cancel.scen.json b/contracts/nft-escrow/scenarios/cancel.scen.json new file mode 100644 index 00000000..23c7495b --- /dev/null +++ b/contracts/nft-escrow/scenarios/cancel.scen.json @@ -0,0 +1,121 @@ +{ + "name": "buy nft", + "steps": [ + { + "step": "externalSteps", + "path": "escrow.scen.json" + }, + { + "step": "scCall", + "id": "cancel-offer-not-exists", + "tx": { + "from": "address:first", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "cancel", + "arguments": [ + "2" + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "4", + "message": "str:Offer does not exist", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "cancel-offer-only-creator", + "tx": { + "from": "address:second", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "cancel", + "arguments": [ + "1" + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "4", + "message": "str:Only the offer creator can cancel it", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "cancel", + "tx": { + "from": "address:first", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "cancel", + "arguments": [ + "1" + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "0", + "gas": "*", + "refund": "*" + } + }, + { + "step": "checkState", + "accounts": { + "address:first": { + "nonce": "7", + "balance": "0", + "esdt": { + "str:FUNG-123456": "1000", + "str:SEMIFUNG-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1000" + } + ] + }, + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1" + }, + { + "nonce": "2", + "balance": "1" + } + ] + } + } + }, + "sc:nft-escrow": { + "nonce": "0", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "instances": [] + } + }, + "storage": { + "str:lastOfferId": "1" + }, + "code": "file:../output/nft-escrow.wasm", + "owner": "address:owner" + }, + "+": "" + } + } + ] +} diff --git a/contracts/nft-escrow/scenarios/escrow.scen.json b/contracts/nft-escrow/scenarios/escrow.scen.json new file mode 100644 index 00000000..8a6281b4 --- /dev/null +++ b/contracts/nft-escrow/scenarios/escrow.scen.json @@ -0,0 +1,226 @@ +{ + "name": "buy nft", + "steps": [ + { + "step": "externalSteps", + "path": "init.scen.json" + }, + { + "step": "scCall", + "id": "escrow-fungible", + "tx": { + "from": "address:first", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "escrow", + "arguments": [ + "str:NFT2-654321", + "2", + "address:second" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:FUNG-123456", + "value": "1000" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "4", + "message": "str:ESDT is not an NFT", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "escrow-semi-fungible", + "tx": { + "from": "address:first", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "escrow", + "arguments": [ + "str:NFT2-654321", + "2", + "address:second" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:SEMIFUNG-123456", + "value": "1000", + "nonce": "1" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "4", + "message": "str:ESDT is not an NFT", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "escrow-wanted-not-nft", + "tx": { + "from": "address:first", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "escrow", + "arguments": [ + "str:NFT2-654321", + "0", + "address:second" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:NFT-123456", + "value": "1", + "nonce": "1" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "message": "str:Wanted ESDT is not an NFT", + "status": "4", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "escrow-same-as-caller", + "tx": { + "from": "address:first", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "escrow", + "arguments": [ + "str:NFT2-654321", + "2", + "address:first" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:NFT-123456", + "value": "1", + "nonce": "1" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "message": "str:Wanted address should not be the same as the caller", + "status": "4", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scCall", + "id": "escrow", + "tx": { + "from": "address:first", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "escrow", + "arguments": [ + "str:NFT2-654321", + "2", + "address:second" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:NFT-123456", + "value": "1", + "nonce": "1" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": ["1"], + "status": "0", + "gas": "*", + "refund": "*" + } + }, + { + "step": "checkState", + "accounts": { + "address:first": { + "nonce": "5", + "balance": "0", + "esdt": { + "str:FUNG-123456": "1000", + "str:SEMIFUNG-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1000" + } + ] + }, + "str:NFT-123456": { + "instances": [ + { + "nonce": "2", + "balance": "1" + } + ] + } + } + }, + "sc:nft-escrow": { + "nonce": "0", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1" + } + ] + } + }, + "storage": { + "str:createdOffers|address:first|``.len": "1", + "str:createdOffers|address:first|``.item|u32:1": "1", + "str:createdOffers|address:first|``.index|u32:1": "1", + + "str:wantedOffers|address:second|``.len": "1", + "str:wantedOffers|address:second|``.item|u32:1": "1", + "str:wantedOffers|address:second|``.index|u32:1": "1", + + "str:offers|u32:1": { + "01-creator": "address:first", + "02-nft": "nested:str:NFT-123456", + "03-nonce": "u64:1", + "04-wanted_nft": "nested:str:NFT2-654321", + "05-wanted_nonce": "u64:2", + "06-wanted_address": "address:second" + }, + "str:lastOfferId": "1" + }, + "code": "file:../output/nft-escrow.wasm", + "owner": "address:owner" + }, + "+": "" + } + } + ] +} diff --git a/contracts/nft-escrow/scenarios/init.scen.json b/contracts/nft-escrow/scenarios/init.scen.json new file mode 100644 index 00000000..9ea50e1c --- /dev/null +++ b/contracts/nft-escrow/scenarios/init.scen.json @@ -0,0 +1,105 @@ +{ + "name": "init", + "steps": [ + { + "step": "setState", + "accounts": { + "address:owner": { + "nonce": "0", + "balance": "0" + }, + "address:first": { + "nonce": "0", + "balance": "0", + "esdt": { + "str:FUNG-123456": "1000", + "str:SEMIFUNG-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1000" + } + ] + }, + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1" + }, + { + "nonce": "2", + "balance": "1" + } + ] + } + } + }, + "address:second": { + "nonce": "0", + "balance": "0", + "esdt": { + "str:FUNG-123456": "1000", + "str:SEMIFUNG-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1000" + } + ] + }, + "str:NFT2-654321": { + "instances": [ + { + "nonce": "1", + "balance": "1" + }, + { + "nonce": "2", + "balance": "2" + } + ] + } + } + } + }, + "newAddresses": [ + { + "creatorAddress": "address:owner", + "creatorNonce": "0", + "newAddress": "sc:nft-escrow" + } + ] + }, + { + "step": "scDeploy", + "id": "deploy", + "tx": { + "from": "address:owner", + "contractCode": "file:../output/nft-escrow.wasm", + "arguments": [], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "0", + "logs": [], + "gas": "*", + "refund": "*" + } + }, + { + "step": "checkState", + "accounts": { + "sc:nft-escrow": { + "nonce": "0", + "balance": "0", + "storage": {}, + "code": "file:../output/nft-escrow.wasm" + }, + "+": "" + } + } + ] +} diff --git a/contracts/nft-escrow/scenarios/views.scen.json b/contracts/nft-escrow/scenarios/views.scen.json new file mode 100644 index 00000000..10d35128 --- /dev/null +++ b/contracts/nft-escrow/scenarios/views.scen.json @@ -0,0 +1,209 @@ +{ + "name": "buy nft", + "steps": [ + { + "step": "externalSteps", + "path": "escrow.scen.json" + }, + { + "step": "scQuery", + "id": "getCreatedOffersFirst", + "tx": { + "to": "sc:nft-escrow", + "function": "getCreatedOffers", + "arguments": [ + "address:first" + ] + }, + "expect": { + "out": [ + "1", + { + "01-creator": "address:first", + "02-nft": "nested:str:NFT-123456", + "03-nonce": "u64:1", + "04-wanted_nft": "nested:str:NFT2-654321", + "05-wanted_nonce": "u64:2", + "06-wanted_address": "address:second" + } + ] + } + }, + { + "step": "scQuery", + "id": "getCreatedOffersSecond", + "tx": { + "to": "sc:nft-escrow", + "function": "getCreatedOffers", + "arguments": [ + "address:second" + ] + }, + "expect": { + "out": [] + } + }, + { + "step": "scQuery", + "id": "getWantedOffersFirst", + "tx": { + "to": "sc:nft-escrow", + "function": "getWantedOffers", + "arguments": [ + "address:first" + ] + }, + "expect": { + "out": [] + } + }, + { + "step": "scQuery", + "id": "getWantedOffersSecond", + "tx": { + "to": "sc:nft-escrow", + "function": "getWantedOffers", + "arguments": [ + "address:second" + ] + }, + "expect": { + "out": [ + "1", + { + "01-creator": "address:first", + "02-nft": "nested:str:NFT-123456", + "03-nonce": "u64:1", + "04-wanted_nft": "nested:str:NFT2-654321", + "05-wanted_nonce": "u64:2", + "06-wanted_address": "address:second" + } + ] + } + }, + { + "step": "scCall", + "id": "escrow", + "tx": { + "from": "address:first", + "to": "sc:nft-escrow", + "egldValue": "0", + "function": "escrow", + "arguments": [ + "str:NFT2-654321", + "1", + "address:second" + ], + "esdtValue": [ + { + "tokenIdentifier": "str:NFT-123456", + "value": "1", + "nonce": "2" + } + ], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": ["2"], + "status": "0", + "gas": "*", + "refund": "*" + } + }, + { + "step": "scQuery", + "id": "getCreatedOffersFirstAgain", + "tx": { + "to": "sc:nft-escrow", + "function": "getCreatedOffers", + "arguments": [ + "address:first" + ] + }, + "expect": { + "out": [ + "1", + { + "01-creator": "address:first", + "02-nft": "nested:str:NFT-123456", + "03-nonce": "u64:1", + "04-wanted_nft": "nested:str:NFT2-654321", + "05-wanted_nonce": "u64:2", + "06-wanted_address": "address:second" + }, + "2", + { + "01-creator": "address:first", + "02-nft": "nested:str:NFT-123456", + "03-nonce": "u64:2", + "04-wanted_nft": "nested:str:NFT2-654321", + "05-wanted_nonce": "u64:1", + "06-wanted_address": "address:second" + } + ] + } + }, + { + "step": "scQuery", + "id": "getCreatedOffersSecond", + "tx": { + "to": "sc:nft-escrow", + "function": "getCreatedOffers", + "arguments": [ + "address:second" + ] + }, + "expect": { + "out": [] + } + }, + { + "step": "scQuery", + "id": "getWantedOffersFirst", + "tx": { + "to": "sc:nft-escrow", + "function": "getWantedOffers", + "arguments": [ + "address:first" + ] + }, + "expect": { + "out": [] + } + }, + { + "step": "scQuery", + "id": "getWantedOffersSecond", + "tx": { + "to": "sc:nft-escrow", + "function": "getWantedOffers", + "arguments": [ + "address:second" + ] + }, + "expect": { + "out": [ + "1", + { + "01-creator": "address:first", + "02-nft": "nested:str:NFT-123456", + "03-nonce": "u64:1", + "04-wanted_nft": "nested:str:NFT2-654321", + "05-wanted_nonce": "u64:2", + "06-wanted_address": "address:second" + }, + "2", + { + "01-creator": "address:first", + "02-nft": "nested:str:NFT-123456", + "03-nonce": "u64:2", + "04-wanted_nft": "nested:str:NFT2-654321", + "05-wanted_nonce": "u64:1", + "06-wanted_address": "address:second" + } + ] + } + } + ] +} diff --git a/contracts/nft-escrow/src/lib.rs b/contracts/nft-escrow/src/lib.rs new file mode 100644 index 00000000..dc975728 --- /dev/null +++ b/contracts/nft-escrow/src/lib.rs @@ -0,0 +1,186 @@ +#![no_std] + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +#[derive(TypeAbi, TopEncode, TopDecode)] +pub struct Offer { + pub creator: ManagedAddress, + pub nft: TokenIdentifier, + pub nonce: u64, + pub wanted_nft: TokenIdentifier, + pub wanted_nonce: u64, + pub wanted_address: ManagedAddress, +} + +#[multiversx_sc::contract] +pub trait NftEscrowContract { + #[init] + fn init(&self) {} + + #[payable("*")] + #[endpoint] + fn escrow( + &self, + wanted_nft: TokenIdentifier, + wanted_nonce: u64, + wanted_address: ManagedAddress, + ) -> u32 { + let payment = self.call_value().single_esdt(); + + require!( + payment.token_nonce > 0 && payment.amount == 1, + "ESDT is not an NFT" + ); + require!(wanted_nonce > 0, "Wanted ESDT is not an NFT"); + + let creator = self.blockchain().get_caller(); + + require!( + creator != wanted_address, + "Wanted address should not be the same as the caller" + ); + + let offer_id = self.last_offer_id().update(|v| { + *v += 1; + + *v + }); + + self.created_offers(&creator).insert(offer_id); + self.wanted_offers(&wanted_address).insert(offer_id); + + let offer = Offer { + creator, + nft: payment.token_identifier, + nonce: payment.token_nonce, + wanted_nft, + wanted_nonce, + wanted_address, + }; + + self.offers(offer_id).set(offer); + + offer_id + } + + #[endpoint] + fn cancel(&self, offer_id: u32) { + let offers_mapper = self.offers(offer_id); + + require!(!offers_mapper.is_empty(), "Offer does not exist"); + + let caller = self.blockchain().get_caller(); + + let offer = offers_mapper.get(); + + require!( + offer.creator == caller, + "Only the offer creator can cancel it" + ); + + self.created_offers(&caller).swap_remove(&offer_id); + self.wanted_offers(&offer.wanted_address) + .swap_remove(&offer_id); + + self.offers(offer_id).clear(); + + self.send().direct_esdt( + &offer.creator, + &offer.nft, + offer.nonce, + &BigUint::from(1u64), + ); + } + + #[payable("*")] + #[endpoint] + fn accept(&self, offer_id: u32) { + let offers_mapper = self.offers(offer_id); + + require!(!offers_mapper.is_empty(), "Offer does not exist"); + + let offer = offers_mapper.get(); + + let caller = self.blockchain().get_caller(); + + require!(offer.wanted_address == caller, "Can not accept this offer"); + + let payment = self.call_value().single_esdt(); + + require!( + payment.token_identifier == offer.wanted_nft + && payment.token_nonce == offer.wanted_nonce + && payment.amount == 1, + "NFT does not match" + ); + + self.created_offers(&offer.creator).swap_remove(&offer_id); + self.wanted_offers(&offer.wanted_address) + .swap_remove(&offer_id); + + self.offers(offer_id).clear(); + + self.send().direct_esdt( + &offer.creator, + &payment.token_identifier, + payment.token_nonce, + &payment.amount, + ); + self.send().direct_esdt( + &offer.wanted_address, + &offer.nft, + offer.nonce, + &BigUint::from(1u64), + ); + } + + #[view(getCreatedOffers)] + fn get_created_offers( + &self, + address: ManagedAddress, + ) -> MultiValueEncoded>> { + let mut result = MultiValueEncoded::new(); + + for offer_id in self.created_offers(&address).iter() { + result.push(self.get_offer_result(offer_id)); + } + + result + } + + #[view(getWantedOffers)] + fn get_wanted_offers( + &self, + address: ManagedAddress, + ) -> MultiValueEncoded>> { + let mut result = MultiValueEncoded::new(); + + for offer_id in self.wanted_offers(&address).iter() { + result.push(self.get_offer_result(offer_id)); + } + + result + } + + fn get_offer_result(&self, offer_id: u32) -> MultiValue2> { + let offer = self.offers(offer_id).get(); + + MultiValue2::from((offer_id, offer)) + } + + #[view] + #[storage_mapper("createdOffers")] + fn created_offers(&self, address: &ManagedAddress) -> UnorderedSetMapper; + + #[view] + #[storage_mapper("wantedOffers")] + fn wanted_offers(&self, address: &ManagedAddress) -> UnorderedSetMapper; + + #[view] + #[storage_mapper("offers")] + fn offers(&self, id: u32) -> SingleValueMapper>; + + #[storage_mapper("lastOfferId")] + fn last_offer_id(&self) -> SingleValueMapper; +} diff --git a/contracts/nft-escrow/tests/nft_escrow_scenario_go_test.rs b/contracts/nft-escrow/tests/nft_escrow_scenario_go_test.rs new file mode 100644 index 00000000..8b43a59f --- /dev/null +++ b/contracts/nft-escrow/tests/nft_escrow_scenario_go_test.rs @@ -0,0 +1,30 @@ +use multiversx_sc_scenario::*; + +fn world() -> ScenarioWorld { + ScenarioWorld::vm_go() +} + +#[test] +fn accept_go() { + world().run("scenarios/accept.scen.json"); +} + +#[test] +fn cancel_go() { + world().run("scenarios/cancel.scen.json"); +} + +#[test] +fn escrow_go() { + world().run("scenarios/escrow.scen.json"); +} + +#[test] +fn init_go() { + world().run("scenarios/init.scen.json"); +} + +#[test] +fn views_go() { + world().run("scenarios/views.scen.json"); +} diff --git a/contracts/nft-escrow/tests/nft_escrow_scenario_rs_test.rs b/contracts/nft-escrow/tests/nft_escrow_scenario_rs_test.rs new file mode 100644 index 00000000..b78c1d9f --- /dev/null +++ b/contracts/nft-escrow/tests/nft_escrow_scenario_rs_test.rs @@ -0,0 +1,35 @@ +use multiversx_sc_scenario::*; + +fn world() -> ScenarioWorld { + todo!() +} + +#[test] +#[ignore = "not supported"] +fn accept_rs() { + world().run("scenarios/accept.scen.json"); +} + +#[test] +#[ignore = "not supported"] +fn cancel_rs() { + world().run("scenarios/cancel.scen.json"); +} + +#[test] +#[ignore = "not supported"] +fn escrow_rs() { + world().run("scenarios/escrow.scen.json"); +} + +#[test] +#[ignore = "not supported"] +fn init_rs() { + world().run("scenarios/init.scen.json"); +} + +#[test] +#[ignore = "not supported"] +fn views_rs() { + world().run("scenarios/views.scen.json"); +} diff --git a/contracts/nft-escrow/wasm/Cargo.lock b/contracts/nft-escrow/wasm/Cargo.lock new file mode 100644 index 00000000..cce0feda --- /dev/null +++ b/contracts/nft-escrow/wasm/Cargo.lock @@ -0,0 +1,219 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" + +[[package]] +name = "multiversx-sc" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406939660d0c79dd191c6677f4b048df873a95f4531d8abafc9cdbe282bf1725" +dependencies = [ + "bitflags", + "hashbrown", + "hex-literal", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f1e15b46c17b87c0c7cdd79b041a4abd7f3a2b45f3c993f6ce38c0f233e82b6" +dependencies = [ + "arrayvec", + "multiversx-sc-codec-derive", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7bc0762cd6d88f8bc54805bc652b042a61cd7fbc2d0a325010f088b78fb2ac" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e006240993963b482fe0682ae49b2d07255495e3c86706925d119137376cdfc" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.43.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e721d1bc80de2ede4099a9040519486c3c1139cb0287d8fc4f9fc3e8a3f19e" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nft-escrow" +version = "0.0.0" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nft-escrow-wasm" +version = "0.0.0" +dependencies = [ + "multiversx-sc-wasm-adapter", + "nft-escrow", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro2" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" diff --git a/contracts/nft-escrow/wasm/Cargo.toml b/contracts/nft-escrow/wasm/Cargo.toml new file mode 100644 index 00000000..4e7db800 --- /dev/null +++ b/contracts/nft-escrow/wasm/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "nft-escrow-wasm" +version = "0.0.0" +edition = "2018" +publish = false +authors = [ "you",] + +[lib] +crate-type = [ "cdylib",] + +[workspace] +members = [ ".",] + +[dev-dependencies] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" + +[dependencies.nft-escrow] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.43.4" diff --git a/contracts/nft-escrow/wasm/src/lib.rs b/contracts/nft-escrow/wasm/src/lib.rs new file mode 100644 index 00000000..7d6e71c8 --- /dev/null +++ b/contracts/nft-escrow/wasm/src/lib.rs @@ -0,0 +1,36 @@ +// Code generated by the multiversx-sc multi-contract system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Endpoints: 8 +// Async Callback (empty): 1 +// Total number of exported functions: 10 + +#![no_std] + +// Configuration that works with rustc < 1.73.0. +// TODO: Recommended rustc version: 1.73.0 or newer. +#![feature(lang_items)] + +multiversx_sc_wasm_adapter::allocator!(); +multiversx_sc_wasm_adapter::panic_handler!(); + +multiversx_sc_wasm_adapter::endpoints! { + nft_escrow + ( + init => init + escrow => escrow + cancel => cancel + accept => accept + getCreatedOffers => get_created_offers + getWantedOffers => get_wanted_offers + created_offers => created_offers + wanted_offers => wanted_offers + offers => offers + ) +} + +multiversx_sc_wasm_adapter::async_callback_empty! {}