diff --git a/Cargo.lock b/Cargo.lock index 2ba866a41..8d7dd54a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2626,6 +2626,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0870c84016d4b481be5c9f323c24f65e31e901ae618f0e80f4308fb00de1d2d" +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fixed-hash" version = "0.8.0" @@ -4273,6 +4279,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nft-utils" +version = "0.2.0" +dependencies = [ + "borsh", + "demo-stf", + "sov-modules-api", + "sov-nft-module", + "sov-rollup-interface", + "sov-sequencer", + "tokio", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -4982,6 +5001,49 @@ dependencies = [ "serde", ] +[[package]] +name = "postgres" +version = "0.19.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7915b33ed60abc46040cbcaa25ffa1c7ec240668e0477c4f3070786f5916d451" +dependencies = [ + "bytes", + "fallible-iterator", + "futures-util", + "log", + "tokio", + "tokio-postgres", +] + +[[package]] +name = "postgres-protocol" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b6c5ef183cd3ab4ba005f1ca64c21e8bd97ce4699cfea9e8d9a2c4958ca520" +dependencies = [ + "base64 0.21.4", + "byteorder", + "bytes", + "fallible-iterator", + "hmac", + "md-5", + "memchr", + "rand 0.8.5", + "sha2 0.10.7", + "stringprep", +] + +[[package]] +name = "postgres-types" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2234cdee9408b523530a9b6d2d6b373d1db34f6a8e51dc03ded1828d7fb67c" +dependencies = [ + "bytes", + "fallible-iterator", + "postgres-protocol", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -7441,6 +7503,7 @@ dependencies = [ "anyhow", "borsh", "jsonrpsee", + "postgres", "schemars", "serde", "serde_json", @@ -7451,6 +7514,8 @@ dependencies = [ "sov-rollup-interface", "sov-state", "tempfile", + "tokio", + "tracing", ] [[package]] @@ -7728,6 +7793,17 @@ dependencies = [ "precomputed-hash", ] +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.9.3" @@ -8138,6 +8214,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-postgres" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d340244b32d920260ae7448cb72b6e238bddc3d4f7603394e7dd46ed8e48f5b8" +dependencies = [ + "async-trait", + "byteorder", + "bytes", + "fallible-iterator", + "futures-channel", + "futures-util", + "log", + "parking_lot 0.12.1", + "percent-encoding", + "phf", + "pin-project-lite", + "postgres-protocol", + "postgres-types", + "rand 0.8.5", + "socket2 0.5.4", + "tokio", + "tokio-util", + "whoami", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -8830,6 +8932,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wildmatch" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 729d60289..aa556b226 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,9 @@ members = [ "utils/zk-cycle-macros", "utils/zk-cycle-utils", + "utils/nft-utils", "utils/bashtestmd", + "utils/nft-utils", "module-system/sov-cli", "module-system/sov-modules-stf-template", diff --git a/examples/demo-prover/methods/guest-celestia/Cargo.lock b/examples/demo-prover/methods/guest-celestia/Cargo.lock index b4574b135..da5807d78 100644 --- a/examples/demo-prover/methods/guest-celestia/Cargo.lock +++ b/examples/demo-prover/methods/guest-celestia/Cargo.lock @@ -1854,3 +1854,8 @@ dependencies = [ "quote", "syn 2.0.37", ] + +[[patch.unused]] +name = "cc" +version = "1.0.79" +source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/examples/demo-prover/methods/guest-mock/Cargo.lock b/examples/demo-prover/methods/guest-mock/Cargo.lock index d4302d1f0..db5de049c 100644 --- a/examples/demo-prover/methods/guest-mock/Cargo.lock +++ b/examples/demo-prover/methods/guest-mock/Cargo.lock @@ -1272,3 +1272,8 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[patch.unused]] +name = "cc" +version = "1.0.79" +source = "git+https://github.com/rust-lang/cc-rs?rev=e5bbdfa#e5bbdfa1fa468c028cb38fee6c35a3cf2e5a2736" diff --git a/examples/demo-rollup/Cargo.toml b/examples/demo-rollup/Cargo.toml index b014a7afc..2814df35e 100644 --- a/examples/demo-rollup/Cargo.toml +++ b/examples/demo-rollup/Cargo.toml @@ -79,7 +79,7 @@ native = ["anyhow", "jsonrpsee", "serde", "serde_json", "tracing", "tokio", "tra "sov-state/native", "sov-cli", "clap", "sov-celestia-adapter/native", "sov-db", "sov-sequencer", "sov-stf-runner/native", "sov-modules-api/native", "sov-rollup-interface/native"] bench = ["native", "async-trait", "borsh", "hex"] - +offchain = ["demo-stf/offchain"] [[bench]] name = "rollup_bench" diff --git a/examples/demo-stf/Cargo.toml b/examples/demo-stf/Cargo.toml index 17babeb8f..28a93152e 100644 --- a/examples/demo-stf/Cargo.toml +++ b/examples/demo-stf/Cargo.toml @@ -51,6 +51,7 @@ rand = "0.8" [features] default = [] +offchain = ["sov-nft-module/offchain"] experimental = ["sov-evm/experimental", "reth-primitives"] native = [ "sov-stf-runner/native", diff --git a/full-node/sov-sequencer/src/utils.rs b/full-node/sov-sequencer/src/utils.rs index a4a828ee9..56a42311e 100644 --- a/full-node/sov-sequencer/src/utils.rs +++ b/full-node/sov-sequencer/src/utils.rs @@ -38,6 +38,39 @@ impl SimpleClient { Ok(()) } + /// Sends multiple transactions to the sequencer for immediate publication. + pub async fn send_transactions( + &self, + txs: Vec, + chunk_size: Option, + ) -> Result<(), anyhow::Error> { + let serialized_txs: Vec> = txs + .into_iter() + .map(|tx| tx.try_to_vec()) + .collect::>()?; + + match chunk_size { + Some(batch_size) => { + for chunk in serialized_txs.chunks(batch_size) { + let response: String = self + .http_client + .request("sequencer_publishBatch", chunk.to_vec()) + .await?; + info!("publish batch response for chunk: {:?}", response); + } + } + None => { + let response: String = self + .http_client + .request("sequencer_publishBatch", serialized_txs) + .await?; + info!("publish batch response: {:?}", response); + } + } + + Ok(()) + } + /// Get a reference to the underlying [`HttpClient`] pub fn http(&self) -> &HttpClient { &self.http_client diff --git a/full-node/sov-stf-runner/src/batch_builder.rs b/full-node/sov-stf-runner/src/batch_builder.rs index 47c935c52..e6b3cd009 100644 --- a/full-node/sov-stf-runner/src/batch_builder.rs +++ b/full-node/sov-stf-runner/src/batch_builder.rs @@ -125,14 +125,14 @@ where current_batch_size += tx_len; } - if txs.is_empty() { - bail!("No valid transactions are available"); - } - for (tx, err) in dismissed { warn!("Transaction 0x{} was dismissed: {:?}", hex::encode(tx), err); } + if txs.is_empty() { + bail!("No valid transactions are available"); + } + Ok(txs) } } diff --git a/module-system/module-implementations/sov-nft-module/Cargo.toml b/module-system/module-implementations/sov-nft-module/Cargo.toml index a063098d0..d1f4e0d9f 100644 --- a/module-system/module-implementations/sov-nft-module/Cargo.toml +++ b/module-system/module-implementations/sov-nft-module/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "sov-nft-module" +description = "A Sovereign SDK module for managing non-fungible tokens" version = { workspace = true } edition = { workspace = true } authors = { workspace = true } @@ -21,6 +22,10 @@ sov-modules-api = { path = "../../sov-modules-api" } sov-modules-macros = {path = "../../sov-modules-macros"} sov-state = { path = "../../sov-state" } +postgres = { version = "0.19.7", optional = true } +tokio = { version = "1.32.0", features=["full"], optional = true } +tracing = { workspace = true, optional = true } + [dev-dependencies] sov-rollup-interface = { path = "../../../rollup-interface" } sov-data-generators = { path = "../../utils/sov-data-generators" } @@ -29,5 +34,6 @@ sov-nft-module = { version = "*", features = ["native"], path = "." } [features] default = [] +offchain = ["postgres","tokio","tracing"] native = ["serde_json", "jsonrpsee", "schemars", "sov-state/native", "sov-modules-api/native", ] test = ["native"] diff --git a/module-system/module-implementations/sov-nft-module/offchain_readme.md b/module-system/module-implementations/sov-nft-module/offchain_readme.md new file mode 100644 index 000000000..3a4345ddf --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/offchain_readme.md @@ -0,0 +1,60 @@ +## Offchain testing + +### Introduction +This readme outlines the steps to demonstrate the offchain processing functionality that is part of the `sov-nft-module` + +### Steps +* Install postgres on your system +* Start the postgres terminal +* Create the tables necessary for offchain processing + +```bash +psql postgres -f sovereign/module-system/module-implementations/sov-nft-module/src/init_db.sql +``` +* The above command runs the `init_db.sql` script which creates 3 tables + * `collections` - tracking the NFT collections that have been created, their supply and other info + * `nfts` - tracking the individual NFTs, the `token_uri` pointing to offchain state and other info + * `top_owners` - tracks the number of NFTs of each collection that a user owns + * running the following query can show the top owners for a specific collection + ```sql + SELECT owner, count + FROM top_owners + WHERE collection_address = ( + SELECT collection_address + FROM collections + WHERE collection_name = 'your_collection_name' + ) + ORDER BY count DESC + LIMIT 5; + ``` + * running the following query can show the largest owner for each collection + ```sql + SELECT + collection_name, + owner, + count + FROM ( + SELECT c.collection_name, t.owner, t.count, + RANK() OVER (PARTITION BY t.collection_address ORDER BY t.count DESC) as rank + FROM top_owners t + INNER JOIN collections c ON c.collection_address = t.collection_address + ) sub + WHERE rank = 1; + ``` + +* Run the demo rollup in offchain mode +```bash +rm -rf demo_data; POSTGRES_CONNECTION_STRING="postgresql://username:password@localhost/postgres" cargo run --features offchain -- --da-layer mock +``` +* Explanation of the above command + * `rm -rf demo_data` is to wipe the rollup state. For testing its better to start with clean state + * `POSTGRES_CONNECTION_STRING` is to allow the offchain component of the `sov-nft-module` to connect to postgres instance + * `--features offchain` is necessary to enable offchain processing. Without the feature, the functions are no-ops + * `--da-layer mock` is used to run an in-memory local DA layer +* Run the NFT minting script +```bash +$ cd sovereign/utils/nft-utils +$ cargo run +``` + * The above script creates 3 NFT collections, mints some NFTs to each collection + * The tables can be explored by connecting to postgres and running sample queries from above \ No newline at end of file diff --git a/module-system/module-implementations/sov-nft-module/src/call.rs b/module-system/module-implementations/sov-nft-module/src/call.rs index 095942e78..028d62498 100644 --- a/module-system/module-implementations/sov-nft-module/src/call.rs +++ b/module-system/module-implementations/sov-nft-module/src/call.rs @@ -2,6 +2,7 @@ use anyhow::Result; use sov_modules_api::{CallResponse, Context, WorkingSet}; use crate::address::UserAddress; +use crate::offchain::{update_collection, update_nft}; use crate::{Collection, CollectionAddress, Nft, NftIdentifier, NonFungibleToken, TokenId}; #[cfg_attr( @@ -87,6 +88,7 @@ impl NonFungibleToken { )?; self.collections .set(&collection_address, &collection, working_set); + update_collection(&collection); Ok(CallResponse::default()) } @@ -107,6 +109,7 @@ impl NonFungibleToken { collection.set_collection_uri(collection_uri); self.collections .set(&collection_address, collection.inner(), working_set); + update_collection(collection.inner()); Ok(CallResponse::default()) } @@ -126,6 +129,7 @@ impl NonFungibleToken { collection.freeze(); self.collections .set(&collection_address, collection.inner(), working_set); + update_collection(collection.inner()); Ok(CallResponse::default()) } @@ -165,6 +169,9 @@ impl NonFungibleToken { self.collections .set(&collection_address, collection.inner(), working_set); + update_collection(collection.inner()); + update_nft(&new_nft, None); + Ok(CallResponse::default()) } @@ -178,12 +185,14 @@ impl NonFungibleToken { ) -> Result { let mut owned_nft = Nft::get_owned_nft(nft_id, collection_address, &self.nfts, context, working_set)?; + let original_owner = owned_nft.inner().get_owner().clone(); owned_nft.set_owner(to); self.nfts.set( &NftIdentifier(nft_id, collection_address.clone()), owned_nft.inner(), working_set, ); + update_nft(owned_nft.inner(), Some(original_owner.clone())); Ok(CallResponse::default()) } @@ -211,10 +220,11 @@ impl NonFungibleToken { mutable_nft.update_token_uri(&uri); } self.nfts.set( - &NftIdentifier(token_id, collection_address), + &NftIdentifier(token_id, collection_address.clone()), mutable_nft.inner(), working_set, ); + update_nft(mutable_nft.inner(), None); Ok(CallResponse::default()) } } diff --git a/module-system/module-implementations/sov-nft-module/src/init_db.sql b/module-system/module-implementations/sov-nft-module/src/init_db.sql new file mode 100644 index 000000000..ed89fddad --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/src/init_db.sql @@ -0,0 +1,50 @@ +-- Drop existing tables if they exist +DROP TABLE IF EXISTS top_owners CASCADE; +DROP TABLE IF EXISTS nfts CASCADE; +DROP TABLE IF EXISTS collections CASCADE; + +-- Create collection table +CREATE TABLE collections +( + collection_address TEXT PRIMARY KEY, + collection_name TEXT NOT NULL, + creator_address TEXT NOT NULL, + frozen BOOLEAN NOT NULL, + metadata_url TEXT, + supply BIGINT NOT NULL +); + +-- Create index on creator_address to quickly find collections by a creator +CREATE INDEX idx_creator ON collections (creator_address); + +-- Create nft table +CREATE TABLE nfts +( + collection_address TEXT NOT NULL, + nft_id BIGINT NOT NULL, + metadata_url TEXT, + owner TEXT NOT NULL, + frozen BOOLEAN NOT NULL, + PRIMARY KEY (collection_address, nft_id) +); + +-- Create index on owner to quickly find NFTs owned by a particular address +CREATE INDEX idx_nft_owner ON nfts (owner); + +-- Create index on collection_address to quickly find NFTs belonging to a particular collection +CREATE INDEX idx_nft_collection ON nfts (collection_address); + +-- Create top_owners table +CREATE TABLE top_owners +( + owner TEXT NOT NULL, + collection_address TEXT NOT NULL, + count BIGINT NOT NULL, + PRIMARY KEY (owner, collection_address) +); + +-- Create index on collection_address to quickly find top owners in a particular collection +CREATE INDEX idx_top_owners_collection ON top_owners (collection_address); + +-- Create index on count to quickly find top owners by count (optional) +CREATE INDEX idx_top_owners_count ON top_owners (count); diff --git a/module-system/module-implementations/sov-nft-module/src/lib.rs b/module-system/module-implementations/sov-nft-module/src/lib.rs index 65df31d34..b1d26129b 100644 --- a/module-system/module-implementations/sov-nft-module/src/lib.rs +++ b/module-system/module-implementations/sov-nft-module/src/lib.rs @@ -16,6 +16,9 @@ mod query; pub use query::*; use serde::{Deserialize, Serialize}; use sov_modules_api::{CallResponse, Context, Error, Module, ModuleInfo, StateMap, WorkingSet}; +mod offchain; +#[cfg(feature = "offchain")] +mod sql; /// Utility functions. pub mod utils; diff --git a/module-system/module-implementations/sov-nft-module/src/offchain.rs b/module-system/module-implementations/sov-nft-module/src/offchain.rs new file mode 100644 index 000000000..a2d6a192c --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/src/offchain.rs @@ -0,0 +1,123 @@ +#[cfg(feature = "offchain")] +use postgres::NoTls; +use sov_modules_macros::offchain; + +#[cfg(feature = "offchain")] +use crate::sql::*; +#[cfg(feature = "offchain")] +use crate::utils::get_collection_address; +#[cfg(feature = "offchain")] +use crate::CollectionAddress; +use crate::{Collection, Nft, OwnerAddress}; + +/// Syncs a collection to the corresponding table "collections" in postgres +#[offchain] +pub fn update_collection(collection: &Collection) { + // Extract the necessary metadata from the collection + let collection_name = collection.get_name(); + let creator_address = collection.get_creator(); + let frozen = collection.is_frozen(); + let metadata_url = collection.get_collection_uri(); + let supply = collection.get_supply(); + let collection_address: CollectionAddress = + get_collection_address(collection_name, creator_address.as_ref()); + let collection_address_str = collection_address.to_string(); + let creator_address_str = creator_address.to_string(); + // postgres insert + tokio::task::block_in_place(|| { + if let Ok(conn_string) = std::env::var("POSTGRES_CONNECTION_STRING") { + match postgres::Client::connect(&conn_string, NoTls) { + Ok(mut client) => { + let result = client.execute( + INSERT_OR_UPDATE_COLLECTION, + &[ + &collection_address_str, + &collection_name, + &creator_address_str, + &frozen, + &metadata_url, + &(supply as i64), + ], + ); + if let Err(e) = result { + tracing::error!("Failed to execute query: {}", e); + } + } + Err(e) => { + tracing::error!("Failed to connect to the database: {}", e); + } + } + } else { + tracing::error!("Environment variable POSTGRES_CONNECTION_STRING is not set"); + } + }) +} + +/// Syncs an NFT to the corresponding table "nfts" in postgres +/// Additionally, this function also has logic to track the counts of NFTs held by each user +/// in each collection. +#[offchain] +pub fn update_nft(nft: &Nft, old_owner: Option>) { + let collection_address = nft.get_collection_address().to_string(); + let nft_id = nft.get_token_id(); + let new_owner_str = nft.get_owner().to_string(); + let frozen = nft.is_frozen(); + let metadata_url = nft.get_token_uri(); + let old_owner_address = old_owner.map(|x| x.to_string()); + + tokio::task::block_in_place(|| { + if let Ok(conn_string) = std::env::var("POSTGRES_CONNECTION_STRING") { + let mut client = postgres::Client::connect(&conn_string, NoTls).unwrap(); + + // Check current owner in the database for the NFT + let rows = client + .query( + QUERY_OWNER_FROM_NFTS, + &[&collection_address, &(nft_id as i64)], + ) + .unwrap(); + + let db_owner: Option = rows.get(0).map(|row| row.get(0)); + + // Handle ownership change logic for top_owners table + if let Some(db_owner_str) = db_owner { + if old_owner_address.is_none() { + // This means it's a mint operation but the NFT already exists in the table. + // Do nothing as we shouldn't increment in this scenario. + } else if old_owner_address.as_ref() != Some(&new_owner_str) { + // Transfer occurred + + // Decrement count for the database owner (which would be the old owner in a transfer scenario) + let _ = client.execute( + DECREMENT_COUNT_FOR_OLD_OWNER, + &[&db_owner_str, &collection_address], + ); + + // Increment count for new owner + let _ = client.execute( + INCREMENT_OR_UPDATE_COUNT_FOR_NEW_OWNER, + &[&new_owner_str, &collection_address], + ); + } + } else if old_owner_address.is_none() { + // Mint operation, and NFT doesn't exist in the database. Increment for the new owner. + let _ = client.execute( + INCREMENT_OR_UPDATE_COUNT_FOR_NEW_OWNER, + &[&new_owner_str, &collection_address], + ); + } + + // Update NFT information after handling top_owners logic + let _ = client.execute( + INSERT_OR_UPDATE_NFT, + &[ + &collection_address, + &(nft_id as i64), + &metadata_url, + &new_owner_str, + &frozen, + ], + ); + } + }) +} diff --git a/module-system/module-implementations/sov-nft-module/src/sql.rs b/module-system/module-implementations/sov-nft-module/src/sql.rs new file mode 100644 index 000000000..6b521a877 --- /dev/null +++ b/module-system/module-implementations/sov-nft-module/src/sql.rs @@ -0,0 +1,30 @@ +pub const INSERT_OR_UPDATE_COLLECTION: &str = "INSERT INTO collections (\ + collection_address, collection_name, creator_address,\ + frozen, metadata_url, supply)\ + VALUES ($1, $2, $3, $4, $5, $6)\ + ON CONFLICT (collection_address)\ + DO UPDATE SET collection_name = EXCLUDED.collection_name,\ + creator_address = EXCLUDED.creator_address,\ + frozen = EXCLUDED.frozen,\ + metadata_url = EXCLUDED.metadata_url,\ + supply = EXCLUDED.supply"; + +pub const QUERY_OWNER_FROM_NFTS: &str = + "SELECT owner FROM nfts WHERE collection_address = $1 AND nft_id = $2"; + +pub const DECREMENT_COUNT_FOR_OLD_OWNER: &str = "UPDATE top_owners SET count = count - 1 \ + WHERE owner = $1 AND collection_address = $2 AND count > 0"; + +pub const INCREMENT_OR_UPDATE_COUNT_FOR_NEW_OWNER: &str = + "INSERT INTO top_owners (owner, collection_address, count) VALUES ($1, $2, 1) \ + ON CONFLICT (owner, collection_address) \ + DO UPDATE SET count = top_owners.count + 1"; + +pub const INSERT_OR_UPDATE_NFT: &str = "INSERT INTO nfts (\ + collection_address, nft_id, metadata_url,\ + owner, frozen)\ + VALUES ($1, $2, $3, $4, $5)\ + ON CONFLICT (collection_address, nft_id)\ + DO UPDATE SET metadata_url = EXCLUDED.metadata_url,\ + owner = EXCLUDED.owner,\ + frozen = EXCLUDED.frozen"; diff --git a/module-system/sov-modules-api/src/transaction.rs b/module-system/sov-modules-api/src/transaction.rs index 1810fbd86..bad6da527 100644 --- a/module-system/sov-modules-api/src/transaction.rs +++ b/module-system/sov-modules-api/src/transaction.rs @@ -6,7 +6,9 @@ use crate::PrivateKey; use crate::{Context, Signature}; /// A Transaction object that is compatible with the module-system/sov-default-stf. -#[derive(Debug, PartialEq, Eq, Clone, borsh::BorshDeserialize, borsh::BorshSerialize)] +#[derive( + Debug, PartialEq, Eq, Clone, borsh::BorshDeserialize, borsh::BorshSerialize, serde::Serialize, +)] pub struct Transaction { signature: C::Signature, pub_key: C::PublicKey, diff --git a/module-system/sov-modules-macros/src/lib.rs b/module-system/sov-modules-macros/src/lib.rs index 40a942af5..c5bd5d1ca 100644 --- a/module-system/sov-modules-macros/src/lib.rs +++ b/module-system/sov-modules-macros/src/lib.rs @@ -18,6 +18,7 @@ mod manifest; mod module_call_json_schema; mod module_info; mod new_types; +mod offchain; #[cfg(feature = "native")] mod rpc; @@ -29,10 +30,11 @@ use dispatch::genesis::GenesisMacro; use dispatch::message_codec::MessageCodec; use module_call_json_schema::derive_module_call_json_schema; use new_types::address_type_helper; +use offchain::offchain_generator; use proc_macro::TokenStream; #[cfg(feature = "native")] use rpc::ExposeRpcMacro; -use syn::{parse_macro_input, DeriveInput}; +use syn::{parse_macro_input, DeriveInput, ItemFn}; #[proc_macro_derive(ModuleInfo, attributes(state, module, address, gas))] pub fn module_info(input: TokenStream) -> TokenStream { @@ -263,3 +265,38 @@ pub fn address_type(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as DeriveInput); handle_macro_error(address_type_helper(input)) } + +/// The offchain macro is used to annotate functions that should only be executed by the rollup +/// when the "offchain" feature flag is passed. The macro produces one of two functions depending on +/// the presence flag. +/// "offchain" feature enabled: function is present as defined +/// "offchain" feature absent: function body is replaced with an empty definition +/// +/// The idea here is that offchain computation is optionally enabled for a full node and is not +/// part of chain state and does not impact consensus, prover or anything else. +/// +/// ## Example +/// ``` +/// use sov_modules_macros::offchain; +/// #[offchain] +/// fn redis_insert(count: u64){ +/// println!("Inserting {} to redis", count); +/// } +/// ``` +/// +/// This is exactly equivalent to hand-writing +///``` +/// #[cfg(feature = "offchain")] +/// fn redis_insert(count: u64){ +/// println!("Inserting {} to redis", count); +/// } +/// +/// #[cfg(not(feature = "offchain"))] +/// fn redis_insert(count: u64){ +/// } +///``` +#[proc_macro_attribute] +pub fn offchain(_attr: TokenStream, item: TokenStream) -> TokenStream { + let input = parse_macro_input!(item as ItemFn); + handle_macro_error(offchain_generator(input)) +} diff --git a/module-system/sov-modules-macros/src/offchain.rs b/module-system/sov-modules-macros/src/offchain.rs new file mode 100644 index 000000000..274021ad9 --- /dev/null +++ b/module-system/sov-modules-macros/src/offchain.rs @@ -0,0 +1,31 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::ItemFn; + +pub fn offchain_generator(function: ItemFn) -> Result { + let visibility = &function.vis; + let name = &function.sig.ident; + let inputs = &function.sig.inputs; + let output = &function.sig.output; + let block = &function.block; + let generics = &function.sig.generics; + let where_clause = &function.sig.generics.where_clause; + let asyncness = &function.sig.asyncness; + + let output = quote! { + // The "real" function + #[cfg(feature = "offchain")] + #visibility #asyncness fn #name #generics(#inputs) #output #where_clause { + #block + } + + // The no-op function + #[cfg(not(feature = "offchain"))] + #[allow(unused_variables)] + #visibility #asyncness fn #name #generics(#inputs) #output #where_clause { + // Do nothing. Should be optimized away + } + }; + + Ok(output.into()) +} diff --git a/packages_to_publish.yml b/packages_to_publish.yml index cf287870f..2143d509a 100644 --- a/packages_to_publish.yml +++ b/packages_to_publish.yml @@ -17,6 +17,7 @@ - sov-prover-incentives - sov-chain-state - sov-blob-storage +- sov-nft-module # Adapters - sov-risc0-adapter diff --git a/utils/nft-utils/Cargo.toml b/utils/nft-utils/Cargo.toml new file mode 100644 index 000000000..6375f1ee6 --- /dev/null +++ b/utils/nft-utils/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "nft-utils" +authors = { workspace = true } +description = "Utils for NFTs" +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +rust-version = { workspace = true } +version = { workspace = true } +readme = "README.md" +resolver = "2" +autotests = false +publish = false + +[dependencies] +borsh = { workspace = true } +tokio = { workspace = true } + +sov-nft-module = { path = "../../module-system/module-implementations/sov-nft-module", features = ["native"]} +sov-sequencer = { path = "../../full-node/sov-sequencer"} +demo-stf = {path = "../../examples/demo-stf"} +sov-rollup-interface = {path = "../../rollup-interface"} + +sov-modules-api = {path = "../../module-system/sov-modules-api", features = ["native"]} diff --git a/utils/nft-utils/src/lib.rs b/utils/nft-utils/src/lib.rs new file mode 100644 index 000000000..bb280d598 --- /dev/null +++ b/utils/nft-utils/src/lib.rs @@ -0,0 +1,201 @@ +use borsh::ser::BorshSerialize; +use demo_stf::runtime::RuntimeCall; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::default_signature::private_key::DefaultPrivateKey; +use sov_modules_api::transaction::Transaction; +use sov_modules_api::{Address, PrivateKey}; +use sov_nft_module::utils::get_collection_address; +use sov_nft_module::{CallMessage, CollectionAddress, UserAddress}; +use sov_rollup_interface::mocks::MockDaSpec; + +fn get_collection_metadata_url(base_url: &str, collection_address: &str) -> String { + format!("{}/collection/{}", base_url, collection_address) +} + +fn get_nft_metadata_url(base_url: &str, collection_address: &str, nft_id: u64) -> String { + format!("{}/nft/{}/{}", base_url, collection_address, nft_id) +} + +/// Convenience and readability wrapper for build_create_collection_transaction +pub fn build_create_collection_transactions( + creator_pk: &DefaultPrivateKey, + start_nonce: &mut u64, + base_uri: &str, + collections: &[&str], +) -> Vec> { + collections + .iter() + .map(|&collection_name| { + let tx = build_create_collection_transaction( + creator_pk, + *start_nonce, + collection_name, + base_uri, + ); + *start_nonce += 1; + tx + }) + .collect() +} + +/// Constructs a transaction to create a new NFT collection. +/// +/// # Arguments +/// +/// * `signer`: The private key used for signing the transaction. +/// * `nonce`: The nonce to be used for the transaction. +/// * `collection_name`: The name of the collection to be created. +/// +/// # Returns +/// +/// Returns a signed transaction for creating a new NFT collection. +pub fn build_create_collection_transaction( + signer: &DefaultPrivateKey, + nonce: u64, + collection_name: &str, + base_uri: &str, +) -> Transaction { + let collection_address = get_collection_address::( + collection_name, + signer.default_address().as_ref(), + ); + + let collection_uri = get_collection_metadata_url(base_uri, &collection_address.to_string()); + let create_collection_message = RuntimeCall::::nft( + CallMessage::::CreateCollection { + name: collection_name.to_string(), + collection_uri, + }, + ); + Transaction::::new_signed_tx( + signer, + create_collection_message.try_to_vec().unwrap(), + nonce, + ) +} + +/// Convenience and readability wrapper for build_mint_nft_transaction +pub fn build_mint_transactions( + creator_pk: &DefaultPrivateKey, + start_nonce: &mut u64, + collection: &str, + start_nft_id: &mut u64, + num: usize, + base_uri: &str, + owner_pk: &DefaultPrivateKey, +) -> Vec> { + (0..num) + .map(|_| { + let tx = build_mint_nft_transaction( + creator_pk, + *start_nonce, + collection, + *start_nft_id, + base_uri, + &owner_pk.default_address(), + ); + *start_nft_id += 1; + *start_nonce += 1; + tx + }) + .collect() +} + +/// Constructs a transaction to mint a new NFT. +/// +/// # Arguments +/// +/// * `signer`: The private key used for signing the transaction. +/// * `nonce`: The nonce to be used for the transaction. +/// * `collection_name`: The name of the collection to which the NFT belongs. +/// * `token_id`: The unique identifier for the new NFT. +/// * `owner`: The address of the user to whom the NFT will be minted. +/// +/// # Returns +/// +/// Returns a signed transaction for minting a new NFT to a specified user. +pub fn build_mint_nft_transaction( + signer: &DefaultPrivateKey, + nonce: u64, + collection_name: &str, + token_id: u64, + base_uri: &str, + owner: &Address, +) -> Transaction { + let collection_address = get_collection_address::( + collection_name, + signer.default_address().as_ref(), + ); + let token_uri = get_nft_metadata_url(base_uri, &collection_address.to_string(), token_id); + let mint_nft_message = + RuntimeCall::::nft(CallMessage::::MintNft { + collection_name: collection_name.to_string(), + token_uri, + token_id, + owner: UserAddress::new(owner), + frozen: false, + }); + Transaction::::new_signed_tx( + signer, + mint_nft_message.try_to_vec().unwrap(), + nonce, + ) +} + +/// Convenience and readability wrapper for build_transfer_nft_transaction +pub fn build_transfer_transactions( + signer: &DefaultPrivateKey, + start_nonce: &mut u64, + collection_address: &CollectionAddress, + nft_ids: Vec, +) -> Vec> { + nft_ids + .into_iter() + .map(|nft_id| { + let new_owner = DefaultPrivateKey::generate().default_address(); + let tx = build_transfer_nft_transaction( + signer, + *start_nonce, + collection_address, + nft_id, + &new_owner, + ); + *start_nonce += 1; + tx + }) + .collect() +} + +/// Constructs a transaction to transfer an NFT to another user. +/// +/// # Arguments +/// +/// * `signer`: The private key used for signing the transaction. +/// * `nonce`: The nonce to be used for the transaction. +/// * `collection_address`: The address of the collection to which the NFT belongs. +/// * `token_id`: The unique identifier for the NFT being transferred. +/// * `to`: The address of the user to whom the NFT will be transferred. +/// +/// # Returns +/// +/// Returns a signed transaction for transferring an NFT to a specified user. +pub fn build_transfer_nft_transaction( + signer: &DefaultPrivateKey, + nonce: u64, + collection_address: &CollectionAddress, + token_id: u64, + to: &Address, +) -> Transaction { + let transfer_message = RuntimeCall::::nft(CallMessage::< + DefaultContext, + >::TransferNft { + collection_address: collection_address.clone(), + token_id, + to: UserAddress::new(to), + }); + Transaction::::new_signed_tx( + signer, + transfer_message.try_to_vec().unwrap(), + nonce, + ) +} diff --git a/utils/nft-utils/src/main.rs b/utils/nft-utils/src/main.rs new file mode 100644 index 000000000..15d01ae71 --- /dev/null +++ b/utils/nft-utils/src/main.rs @@ -0,0 +1,105 @@ +use std::thread; +use std::time::Duration; + +use nft_utils::{ + build_create_collection_transactions, build_mint_transactions, build_transfer_transactions, +}; +use sov_modules_api::default_context::DefaultContext; +use sov_modules_api::default_signature::private_key::DefaultPrivateKey; +use sov_nft_module::utils::get_collection_address; +use sov_sequencer::utils::SimpleClient; + +const COLLECTION_1: &str = "Sovereign Squirrel Syndicate"; +const COLLECTION_2: &str = "Celestial Dolphins"; +const COLLECTION_3: &str = "Risky Rhinos"; + +const DUMMY_URL: &str = "http://foobar.storage"; + +const PK1: [u8; 32] = [ + 199, 23, 116, 41, 227, 173, 69, 178, 7, 24, 164, 151, 88, 149, 52, 187, 102, 167, 163, 248, 38, + 86, 207, 66, 87, 81, 56, 66, 211, 150, 208, 155, +]; +const PK2: [u8; 32] = [ + 92, 136, 187, 3, 235, 27, 9, 215, 232, 93, 24, 78, 85, 255, 234, 60, 152, 21, 139, 246, 151, + 129, 152, 227, 231, 204, 38, 84, 159, 129, 71, 143, +]; +const PK3: [u8; 32] = [ + 233, 139, 68, 72, 169, 252, 229, 117, 72, 144, 47, 191, 13, 42, 32, 107, 190, 52, 102, 210, + 161, 208, 245, 116, 93, 84, 37, 87, 171, 44, 30, 239, +]; + +#[tokio::main] +async fn main() { + let creator_pk = DefaultPrivateKey::try_from(&PK1[..]).unwrap(); + let owner_1_pk = DefaultPrivateKey::try_from(&PK2[..]).unwrap(); + let owner_2_pk = DefaultPrivateKey::try_from(&PK3[..]).unwrap(); + + let client = SimpleClient::new("localhost", 12345).await.unwrap(); + + let mut nonce = 0; + let collections = [COLLECTION_1, COLLECTION_2, COLLECTION_3]; + let transactions = + build_create_collection_transactions(&creator_pk, &mut nonce, DUMMY_URL, &collections); + client.send_transactions(transactions, None).await.unwrap(); + + // sleep is necessary because of how the sequencer currently works + // without the sleep, there is a concurrency issue and some transactions would be ignored + // TODO: remove after https://github.com/Sovereign-Labs/sovereign-sdk/issues/949 is fixed + thread::sleep(Duration::from_millis(1000)); + + let mut nft_id = 1; + let mut transactions = build_mint_transactions( + &creator_pk, + &mut nonce, + COLLECTION_1, + &mut nft_id, + 15, + DUMMY_URL, + &owner_1_pk, + ); + + transactions.extend(build_mint_transactions( + &creator_pk, + &mut nonce, + COLLECTION_1, + &mut nft_id, + 5, + DUMMY_URL, + &owner_2_pk, + )); + let mut nft_id = 1; + transactions.extend(build_mint_transactions( + &creator_pk, + &mut nonce, + COLLECTION_2, + &mut nft_id, + 20, + DUMMY_URL, + &owner_1_pk, + )); + + client + .send_transactions(transactions.clone(), None) + .await + .unwrap(); + // TODO: remove after https://github.com/Sovereign-Labs/sovereign-sdk/issues/949 is fixed + thread::sleep(Duration::from_millis(3000)); + + let collection_1_address = get_collection_address::( + COLLECTION_1, + creator_pk.default_address().as_ref(), + ); + + let mut owner_1_nonce = 0; + let nft_ids_to_transfer: Vec = (1..=6).collect(); + transactions = build_transfer_transactions( + &owner_1_pk, + &mut owner_1_nonce, + &collection_1_address, + nft_ids_to_transfer, + ); + client + .send_transactions(transactions.clone(), None) + .await + .unwrap(); +}