diff --git a/Cargo.lock b/Cargo.lock index a2e1cd41fe..60f615f584 100755 --- a/Cargo.lock +++ b/Cargo.lock @@ -2300,6 +2300,23 @@ dependencies = [ "nft-storage-prepay", ] +[[package]] +name = "nft-subscription" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "multiversx-sc-modules", + "multiversx-sc-scenario", +] + +[[package]] +name = "nft-subscription-meta" +version = "0.0.0" +dependencies = [ + "multiversx-sc-meta", + "nft-subscription", +] + [[package]] name = "nibble_vec" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3e2a20a57d..ef92a8a7b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,8 @@ members = [ "contracts/examples/multisig/interact", "contracts/examples/nft-minter", "contracts/examples/nft-minter/meta", + "contracts/examples/nft-subscription", + "contracts/examples/nft-subscription/meta", "contracts/examples/nft-storage-prepay", "contracts/examples/nft-storage-prepay/meta", "contracts/examples/order-book/factory", diff --git a/contracts/examples/nft-subscription/.gitignore b/contracts/examples/nft-subscription/.gitignore new file mode 100644 index 0000000000..9494cb146e --- /dev/null +++ b/contracts/examples/nft-subscription/.gitignore @@ -0,0 +1,7 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +*/target/ + +# The mxpy output +output diff --git a/contracts/examples/nft-subscription/Cargo.toml b/contracts/examples/nft-subscription/Cargo.toml new file mode 100644 index 0000000000..f97cd8b819 --- /dev/null +++ b/contracts/examples/nft-subscription/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nft-subscription" +version = "0.0.0" +authors = ["Thouny "] +edition = "2021" +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies.multiversx-sc] +version = "0.43.4" +path = "../../../framework/base" + +[dependencies.multiversx-sc-modules] +version = "0.43.4" +path = "../../../contracts/modules" + +[dev-dependencies.multiversx-sc-scenario] +version = "0.43.4" +path = "../../../framework/scenario" diff --git a/contracts/examples/nft-subscription/meta/Cargo.toml b/contracts/examples/nft-subscription/meta/Cargo.toml new file mode 100644 index 0000000000..5eed1a06b6 --- /dev/null +++ b/contracts/examples/nft-subscription/meta/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "nft-subscription-meta" +version = "0.0.0" +authors = ["Thouny "] +edition = "2021" +publish = false + +[dependencies.nft-subscription] +path = ".." + +[dependencies.multiversx-sc-meta] +version = "0.43.4" +path = "../../../../framework/meta" diff --git a/contracts/examples/nft-subscription/meta/src/main.rs b/contracts/examples/nft-subscription/meta/src/main.rs new file mode 100644 index 0000000000..27c3b0f0ad --- /dev/null +++ b/contracts/examples/nft-subscription/meta/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + multiversx_sc_meta::cli_main::(); +} diff --git a/contracts/examples/nft-subscription/multiversx.json b/contracts/examples/nft-subscription/multiversx.json new file mode 100644 index 0000000000..7365539625 --- /dev/null +++ b/contracts/examples/nft-subscription/multiversx.json @@ -0,0 +1,3 @@ +{ + "language": "rust" +} \ No newline at end of file diff --git a/contracts/examples/nft-subscription/scenarios/init.scen.json b/contracts/examples/nft-subscription/scenarios/init.scen.json new file mode 100644 index 0000000000..40a0f7da51 --- /dev/null +++ b/contracts/examples/nft-subscription/scenarios/init.scen.json @@ -0,0 +1,78 @@ +{ + "name": "init", + "steps": [ + { + "step": "setState", + "accounts": { + "address:owner": { + "nonce": "0", + "balance": "0" + }, + "address:buyer": { + "nonce": "0", + "balance": "1000" + } + }, + "newAddresses": [ + { + "creatorAddress": "address:owner", + "creatorNonce": "0", + "newAddress": "sc:nft-subscription" + } + ] + }, + { + "step": "scDeploy", + "id": "deploy", + "tx": { + "from": "address:owner", + "contractCode": "file:../output/nft-subscription.wasm", + "arguments": [], + "gasLimit": "10,000,000", + "gasPrice": "0" + }, + "expect": { + "out": [], + "status": "0", + "logs": [], + "gas": "*", + "refund": "*" + } + }, + { + "step": "checkState", + "accounts": { + "sc:nft-subscription": { + "nonce": "0", + "balance": "0", + "storage": {}, + "code": "file:../output/nft-subscription.wasm" + }, + "+": "" + } + }, + { + "step": "setState", + "accounts": { + "sc:nft-subscription": { + "nonce": "0", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "lastNonce": "0", + "roles": [ + "ESDTRoleNFTCreate", + "ESDTRoleNFTUpdateAttributes" + ] + } + }, + "storage": { + "str:tokenId": "str:NFT-123456" + }, + "code": "file:../output/nft-subscription.wasm", + "owner": "address:owner" + } + } + } + ] +} diff --git a/contracts/examples/nft-subscription/scenarios/mint_nft.scen.json b/contracts/examples/nft-subscription/scenarios/mint_nft.scen.json new file mode 100644 index 0000000000..b51d81fb3c --- /dev/null +++ b/contracts/examples/nft-subscription/scenarios/mint_nft.scen.json @@ -0,0 +1,92 @@ +{ + "name": "mint and update nft", + "steps": [ + { + "step": "externalSteps", + "path": "init.scen.json" + }, + { + "step": "scCall", + "id": "create-NFT-1", + "tx": { + "from": "address:owner", + "to": "sc:nft-subscription", + "function": "mint", + "arguments": [], + "gasLimit": "20,000,000", + "gasPrice": "0" + } + }, + { + "step": "checkState", + "accounts": { + "address:owner": { + "nonce": "2", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1", + "creator": "sc:nft-subscription", + "attributes": { + "0-expiration": "u64:0", + "1-attributes": "nested:str:common" + } + } + ] + } + } + }, + "+": "" + } + }, + { + "step": "scCall", + "id": "update-NFT-1", + "tx": { + "from": "address:owner", + "to": "sc:nft-subscription", + "esdtValue": [ + { + "tokenIdentifier": "str:NFT-123456", + "nonce": "1", + "value": "1" + } + ], + "function": "update_attributes", + "arguments": [ + "str:rare" + ], + "gasLimit": "20,000,000", + "gasPrice": "0" + } + }, + { + "step": "checkState", + "accounts": { + "address:owner": { + "nonce": "3", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1", + "creator": "sc:nft-subscription", + "attributes": { + "0-expiration": "u64:0", + "1-attributes": "nested:str:rare" + } + } + ] + } + } + }, + "+": "" + } + } + ] +} diff --git a/contracts/examples/nft-subscription/scenarios/test_subscription.scen.json b/contracts/examples/nft-subscription/scenarios/test_subscription.scen.json new file mode 100644 index 0000000000..7948b03bcc --- /dev/null +++ b/contracts/examples/nft-subscription/scenarios/test_subscription.scen.json @@ -0,0 +1,215 @@ +{ + "name": "subscription", + "steps": [ + { + "step": "externalSteps", + "path": "mint_nft.scen.json" + }, + { + "step": "setState", + "currentBlockInfo": { + "blockTimestamp": "1" + } + }, + { + "step": "scCall", + "id": "add-subscription", + "tx": { + "from": "address:owner", + "to": "sc:nft-subscription", + "esdtValue": [ + { + "tokenIdentifier": "str:NFT-123456", + "nonce": "1", + "value": "1" + } + ], + "function": "renew", + "arguments": [ + "2" + ], + "gasLimit": "20,000,000", + "gasPrice": "0" + } + }, + { + "step": "checkState", + "accounts": { + "address:owner": { + "nonce": "*", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1", + "creator": "sc:nft-subscription", + "attributes": { + "0-expiration": "u64:3", + "1-attributes": "nested:str:rare" + } + } + ] + } + } + }, + "+": "" + } + }, + { + "step": "setState", + "currentBlockInfo": { + "blockTimestamp": "2" + } + }, + { + "step": "scCall", + "id": "renew-subscription-not-ended", + "tx": { + "from": "address:owner", + "to": "sc:nft-subscription", + "esdtValue": [ + { + "tokenIdentifier": "str:NFT-123456", + "nonce": "1", + "value": "1" + } + ], + "function": "renew", + "arguments": [ + "3" + ], + "gasLimit": "20,000,000", + "gasPrice": "0" + } + }, + { + "step": "checkState", + "accounts": { + "address:owner": { + "nonce": "*", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1", + "creator": "sc:nft-subscription", + "attributes": { + "0-expiration": "u64:6", + "1-attributes": "nested:str:rare" + } + } + ] + } + } + }, + "+": "" + } + }, + { + "step": "setState", + "currentBlockInfo": { + "blockTimestamp": "10" + } + }, + { + "step": "scCall", + "id": "renew-subscription-already-ended", + "tx": { + "from": "address:owner", + "to": "sc:nft-subscription", + "esdtValue": [ + { + "tokenIdentifier": "str:NFT-123456", + "nonce": "1", + "value": "1" + } + ], + "function": "renew", + "arguments": [ + "5" + ], + "gasLimit": "20,000,000", + "gasPrice": "0" + } + }, + { + "step": "checkState", + "accounts": { + "address:owner": { + "nonce": "*", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1", + "creator": "sc:nft-subscription", + "attributes": { + "0-expiration": "u64:15", + "1-attributes": "nested:str:rare" + } + } + ] + } + } + }, + "+": "" + } + }, + { + "step": "setState", + "currentBlockInfo": { + "blockTimestamp": "11" + } + }, + { + "step": "scCall", + "id": "cancel-subscription", + "tx": { + "from": "address:owner", + "to": "sc:nft-subscription", + "esdtValue": [ + { + "tokenIdentifier": "str:NFT-123456", + "nonce": "1", + "value": "1" + } + ], + "function": "cancel", + "arguments": [], + "gasLimit": "20,000,000", + "gasPrice": "0" + } + }, + { + "step": "checkState", + "accounts": { + "address:owner": { + "nonce": "*", + "balance": "0", + "esdt": { + "str:NFT-123456": { + "instances": [ + { + "nonce": "1", + "balance": "1", + "creator": "sc:nft-subscription", + "attributes": { + "0-expiration": "u64:0", + "1-attributes": "nested:str:rare" + } + } + ] + } + } + }, + "+": "" + } + } + ] +} \ No newline at end of file diff --git a/contracts/examples/nft-subscription/src/lib.rs b/contracts/examples/nft-subscription/src/lib.rs new file mode 100644 index 0000000000..b9c4fcdb8d --- /dev/null +++ b/contracts/examples/nft-subscription/src/lib.rs @@ -0,0 +1,88 @@ +#![no_std] + +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +use multiversx_sc_modules::{default_issue_callbacks, subscription}; + +#[multiversx_sc::contract] +pub trait NftSubscription: + default_issue_callbacks::DefaultIssueCallbacksModule + subscription::SubscriptionModule +{ + #[init] + fn init(&self) {} + + #[endpoint] + fn issue(&self) { + self.token_id().issue_and_set_all_roles( + EsdtTokenType::NonFungible, + self.call_value().egld_value().clone_value(), + ManagedBuffer::from(b"Subscription"), + ManagedBuffer::from(b"SUB"), + 0, + None, + ) + } + + #[endpoint] + fn mint(&self) { + let nonce = self.create_subscription_nft( + self.token_id().get_token_id_ref(), + &BigUint::from(1u8), + &ManagedBuffer::new(), + &BigUint::from(0u8), + &ManagedBuffer::new(), + 0, + ManagedBuffer::from(b"common"), + &ManagedVec::new(), + ); + self.send().direct_esdt( + &self.blockchain().get_caller(), + self.token_id().get_token_id_ref(), + nonce, + &BigUint::from(1u8), + ); + } + + #[payable("*")] + #[endpoint] + fn update_attributes(&self, attributes: ManagedBuffer) { + let (id, nonce, _) = self.call_value().single_esdt().into_tuple(); + self.update_subscription_attributes::(&id, nonce, attributes); + self.send().direct_esdt( + &self.blockchain().get_caller(), + &id, + nonce, + &BigUint::from(1u8), + ); + } + + #[payable("*")] + #[endpoint] + fn renew(&self, duration: u64) { + let (id, nonce, _) = self.call_value().single_esdt().into_tuple(); + self.renew_subscription::(&id, nonce, duration); + self.send().direct_esdt( + &self.blockchain().get_caller(), + &id, + nonce, + &BigUint::from(1u8), + ); + } + + #[payable("*")] + #[endpoint] + fn cancel(&self) { + let (id, nonce, _) = self.call_value().single_esdt().into_tuple(); + self.cancel_subscription::(&id, nonce); + self.send().direct_esdt( + &self.blockchain().get_caller(), + &id, + nonce, + &BigUint::from(1u8), + ); + } + + #[storage_mapper("tokenId")] + fn token_id(&self) -> NonFungibleTokenMapper; +} diff --git a/contracts/examples/nft-subscription/tests/nft_subscription_scenario_rs_test.rs b/contracts/examples/nft-subscription/tests/nft_subscription_scenario_rs_test.rs new file mode 100644 index 0000000000..d37faa2bca --- /dev/null +++ b/contracts/examples/nft-subscription/tests/nft_subscription_scenario_rs_test.rs @@ -0,0 +1,23 @@ +use multiversx_sc_scenario::*; + +fn world() -> ScenarioWorld { + todo!() +} + +#[test] +#[ignore = "not supported"] +fn test_subscription_rs() { + world().run("scenarios/test_subscription.scen.json"); +} + +#[test] +#[ignore = "not supported"] +fn mint_nft_rs() { + world().run("scenarios/mint_nft.scen.json"); +} + +#[test] +#[ignore = "not supported"] +fn init_rs() { + world().run("scenarios/init.scen.json"); +} diff --git a/contracts/examples/nft-subscription/tests/scenario_go_test.rs b/contracts/examples/nft-subscription/tests/scenario_go_test.rs new file mode 100644 index 0000000000..504c7992b0 --- /dev/null +++ b/contracts/examples/nft-subscription/tests/scenario_go_test.rs @@ -0,0 +1,20 @@ +use multiversx_sc_scenario::*; + +fn world() -> ScenarioWorld { + ScenarioWorld::vm_go() +} + +#[test] +fn test_subscription_go() { + world().run("scenarios/test_subscription.scen.json"); +} + +#[test] +fn mint_nft_go() { + world().run("scenarios/mint_nft.scen.json"); +} + +#[test] +fn init_go() { + world().run("scenarios/init.scen.json"); +} diff --git a/contracts/examples/nft-subscription/wasm/Cargo.lock b/contracts/examples/nft-subscription/wasm/Cargo.lock new file mode 100644 index 0000000000..f618f1a62d --- /dev/null +++ b/contracts/examples/nft-subscription/wasm/Cargo.lock @@ -0,0 +1,217 @@ +# 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" +dependencies = [ + "bitflags", + "hashbrown", + "hex-literal", + "multiversx-sc-codec", + "multiversx-sc-derive", + "num-traits", +] + +[[package]] +name = "multiversx-sc-codec" +version = "0.18.1" +dependencies = [ + "arrayvec", + "multiversx-sc-codec-derive", +] + +[[package]] +name = "multiversx-sc-codec-derive" +version = "0.18.1" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "multiversx-sc-derive" +version = "0.43.4" +dependencies = [ + "hex", + "proc-macro2", + "quote", + "radix_trie", + "syn", +] + +[[package]] +name = "multiversx-sc-modules" +version = "0.43.4" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "multiversx-sc-wasm-adapter" +version = "0.43.4" +dependencies = [ + "multiversx-sc", +] + +[[package]] +name = "nft-subscription" +version = "0.0.0" +dependencies = [ + "multiversx-sc", + "multiversx-sc-modules", +] + +[[package]] +name = "nft-subscription-wasm" +version = "0.0.0" +dependencies = [ + "multiversx-sc-wasm-adapter", + "nft-subscription", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[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/examples/nft-subscription/wasm/Cargo.toml b/contracts/examples/nft-subscription/wasm/Cargo.toml new file mode 100644 index 0000000000..4cfb2cf9a4 --- /dev/null +++ b/contracts/examples/nft-subscription/wasm/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "nft-subscription-wasm" +version = "0.0.0" +authors = ["Thouny "] +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[profile.release] +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" + +[dependencies.nft-subscription] +path = ".." + +[dependencies.multiversx-sc-wasm-adapter] +version = "0.43.4" +path = "../../../../framework/wasm-adapter" + +[workspace] +members = ["."] diff --git a/contracts/examples/nft-subscription/wasm/src/lib.rs b/contracts/examples/nft-subscription/wasm/src/lib.rs new file mode 100644 index 0000000000..76a79ce530 --- /dev/null +++ b/contracts/examples/nft-subscription/wasm/src/lib.rs @@ -0,0 +1,33 @@ +// Code generated by the multiversx-sc multi-contract system. DO NOT EDIT. + +//////////////////////////////////////////////////// +////////////////// AUTO-GENERATED ////////////////// +//////////////////////////////////////////////////// + +// Init: 1 +// Endpoints: 5 +// Async Callback: 1 +// Total number of exported functions: 7 + +#![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_subscription + ( + init => init + issue => issue + mint => mint + update_attributes => update_attributes + renew => renew + cancel => cancel + ) +} + +multiversx_sc_wasm_adapter::async_callback! { nft_subscription } diff --git a/contracts/modules/src/lib.rs b/contracts/modules/src/lib.rs index 3229e4f742..b1973e3fc0 100644 --- a/contracts/modules/src/lib.rs +++ b/contracts/modules/src/lib.rs @@ -12,6 +12,7 @@ pub mod ongoing_operation; pub mod only_admin; pub mod pause; pub mod staking; +pub mod subscription; pub mod token_merge; pub mod transfer_role_proxy; pub mod users; diff --git a/contracts/modules/src/subscription.rs b/contracts/modules/src/subscription.rs new file mode 100644 index 0000000000..3f142abbe4 --- /dev/null +++ b/contracts/modules/src/subscription.rs @@ -0,0 +1,154 @@ +multiversx_sc::imports!(); +multiversx_sc::derive_imports!(); + +/// Standard smart contract module for managing a Subscription NFT. +/// Adaptation of the EIP-5643 for MultiversX, more here https://eips.ethereum.org/EIPS/eip-5643 +/// +/// This standard is an extension of the MultiversX NFT standard. +/// It proposes an additional interface for NFTs to be used as recurring, expirable subscriptions. +/// The interface includes functions to renew and cancel the subscription. +/// +/// Since the NFT standard only has one field for adding arbitrary data (attributes), +/// The module also provides functions for creating NFTs with subscription as well as for reading and updating attributes +/// This allows developers to add additional data to the subscription expiration +/// +/// Developers should be careful when interacting with custom attributes at the same time as subscription +/// They should exclusively use the functions from this module +/// The use of the generic function for updating nft attributes might result in data loss +/// +/// The module provides functions for: +/// * creating a subscription nft +/// * updating custom attributes +/// * getting custom attributes +/// * renewing a subscription +/// * cancelling a subscription +/// * getting the expiration +/// +#[derive(TypeAbi, TopEncode, TopDecode)] +pub struct SubscriptionAttributes { + pub expiration: u64, + pub attributes: T, +} + +#[multiversx_sc::module] +pub trait SubscriptionModule { + // ** NFT and Attributes + + fn create_subscription_nft( + &self, + token_id: &TokenIdentifier, + amount: &BigUint, + name: &ManagedBuffer, + royalties: &BigUint, + hash: &ManagedBuffer, + duration: u64, + attributes: T, + uris: &ManagedVec, + ) -> u64 { + let subscription_attributes = SubscriptionAttributes:: { + expiration: self.blockchain().get_block_timestamp() + duration, + attributes, + }; + + self.send().esdt_nft_create( + token_id, + amount, + name, + royalties, + hash, + &subscription_attributes, + uris, + ) + } + + fn update_subscription_attributes( + &self, + id: &TokenIdentifier, + nonce: u64, + attributes: T, + ) { + let subscription_attributes = SubscriptionAttributes:: { + expiration: self.get_subscription::(id, nonce), + attributes, + }; + + self.send() + .nft_update_attributes(id, nonce, &subscription_attributes); + } + + // @dev should only be called if the nft is owned by the contract + fn get_subscription_attributes( + &self, + id: &TokenIdentifier, + nonce: u64, + ) -> T { + let subscription_attributes: SubscriptionAttributes = + self.blockchain().get_token_attributes(id, nonce); + + subscription_attributes.attributes + } + + // ** Subscription + + #[event("subscriptionUpdate")] + fn subscription_update_event( + &self, + #[indexed] token_id: &ManagedBuffer, + #[indexed] token_nonce: u64, + #[indexed] expiration: u64, + ); + + fn renew_subscription( + &self, + id: &TokenIdentifier, + nonce: u64, + duration: u64, + ) { + let time = self.blockchain().get_block_timestamp(); + let mut subscription_attributes: SubscriptionAttributes = + self.blockchain().get_token_attributes(id, nonce); + let expiration = subscription_attributes.expiration; + + subscription_attributes.expiration = if expiration > time { + expiration + duration + } else { + time + duration + }; + + self.send() + .nft_update_attributes(id, nonce, &subscription_attributes); + + self.subscription_update_event( + id.as_managed_buffer(), + nonce, + subscription_attributes.expiration, + ); + } + + fn cancel_subscription( + &self, + id: &TokenIdentifier, + nonce: u64, + ) { + let mut subscription_attributes: SubscriptionAttributes = + self.blockchain().get_token_attributes(id, nonce); + subscription_attributes.expiration = 0; + + self.send() + .nft_update_attributes(id, nonce, &subscription_attributes); + + self.subscription_update_event(id.as_managed_buffer(), nonce, 0); + } + + // @dev should only be called if the nft is owned by the contract + fn get_subscription( + &self, + id: &TokenIdentifier, + nonce: u64, + ) -> u64 { + let subscription_attributes: SubscriptionAttributes = + self.blockchain().get_token_attributes(id, nonce); + + subscription_attributes.expiration + } +}