Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
423 changes: 369 additions & 54 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ cosmwasm-std = { version = "2.2.2", features = ["stargate", "cosmwasm_2_1"]
cw2 = "2.0.0"
cw-storage-plus = "2.0.0"
cw-utils = "2.0.0"
cw721 = "0.20.0"
hex = "0.4"
sha2 = { version = "0.10.8", features = ["oid"]}
thiserror = "1"
Expand Down
25 changes: 25 additions & 0 deletions contracts/asset/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "asset"
version = "0.1.0"
edition = "2024"
description = "Primary Asset Contract implementation for XION network"
license = "Apache-2.0"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
cw-storage-plus = { workspace = true }
cw2 = { workspace = true }
cw721 = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }

[features]
default = []
library = []
asset_base = []
crossmint = []
123 changes: 123 additions & 0 deletions contracts/asset/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Asset Contract

A CosmWasm `cw721`-compatible asset contract for the XION network that layers marketplace
functionality and a pluggable execution pipeline on top of NFT collections. The library exposes
traits that make it easy to extend vanilla `cw721` contracts with listing, reservation, and plugin
logic without rewriting the core token implementation.

## Core Types and Traits

- `AssetContract` / `DefaultAssetContract`
- Thin wrapper around the canonical `cw721` storage helpers (`Cw721Config`) plus marketplace
indices (`IndexedMap` for listings and plugin registry).
- `DefaultAssetContract` picks `AssetExtensionExecuteMsg` as the extension message so you get the
marketplace verbs (list, reserve, delist, buy) out of the box.

- `SellableAsset`
- Trait that adds four high-level marketplace entry points: `list`, `reserve`, `delist`, and `buy`.
- Each method wires through the shared `AssetConfig` helpers defined in `execute.rs`, handling
ownership checks, price validation, and state transitions.
- Implemented for `AssetContract`, so adopting the trait is as simple as embedding the contract
struct in your project.

- `PluggableAsset`
- Trait that wraps `cw721::Cw721Execute::execute` with a plugin pipeline (`execute_pluggable`).
- Hooks (`on_list_plugin`, `on_buy_plugin`, etc.) run before the base action and can mutate a
`PluginCtx` shared across plugins. The returned `Response` from plugins is merged back into the
main execution result, allowing plugins to enqueue messages, attributes, or data.
- `DefaultAssetContract` implements the trait using `DefaultXionAssetContext`, giving you sensible
defaults while still allowing custom contexts if you implement the trait yourself.

## Messages and State

- `AssetExtensionExecuteMsg` provides the marketplace verbs clients call via the `cw721` execute
route. These are automatically dispatched through `SellableAsset` when `DefaultAssetContract`
handles `execute_extension`.
- `Reserve` captures an optional reservation window (`Expiration`) and address, used to gate buys.
- `ListingInfo` stores price, seller, reservation data, and marketplace fee settings. The contract
fetches NFT metadata directly from the `cw721` state when needed instead of duplicating it in the
listing storage.
- `AssetConfig` centralizes the contract's storage maps and exposes helper constructors so you can
use custom storage keys when embedding the contract inside another crate.

## Plugin System

`plugin.rs` includes a `Plugin` enum and a default plugin module:

- Price guards (`ExactPrice`, `MinimumPrice`).
- Temporal restrictions (`NotBefore`, `NotAfter`, `TimeLock`).
- Access control (`AllowedMarketplaces`, `RequiresProof`).
- Currency allow-listing (`AllowedCurrencies`).
- Royalty payouts (`Royalty`).

The provided `default_plugins` module contains ready-to-use helpers that enforce the relevant rules
and can enqueue `BankMsg::Send` payouts (e.g., royalties) or raise errors to abort the action.
Register plugins per collection with `AssetConfig::collection_plugins` and they will be invoked by
`execute_pluggable` automatically.

## Using the Library

1. **Instantiate `cw721` normally**
```rust
pub type InstantiateMsg = asset::msg::InstantiateMsg<MyCollectionExtension>;
```
Use the standard `cw721` instantiate flow; the asset contract reuses `Cw721InstantiateMsg`.

2. **Embed the contract**
```rust
use asset::traits::{AssetContract, DefaultAssetContract};

pub struct AppContract {
asset: DefaultAssetContract<'static, MyNftExtension, MyNftMsg, MyCollectionExtension, MyCollectionMsg>,
}

impl Default for AppContract {
fn default() -> Self {
Self { asset: AssetContract::default() }
}
}
```

3. **Expose execute entry points**
```rust
use asset::traits::{PluggableAsset, SellableAsset};

pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: asset::msg::ExecuteMsg<MyNftMsg, MyCollectionMsg, asset::msg::AssetExtensionExecuteMsg>,
) -> Result<Response, ContractError> {
Ok(APP_CONTRACT.asset.execute_pluggable(deps, &env, &info, msg)?)
}
```
The `PluggableAsset` trait forwards marketplace operations to the relevant hooks and finally to
the base `cw721` implementation.

4. **Dispatch marketplace operations**
```rust
use asset::msg::AssetExtensionExecuteMsg;
use cosmwasm_std::{Coin, to_json_binary, CosmosMsg};

let list = CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: collection_addr.into(),
funds: vec![],
msg: to_json_binary(&AssetExtensionExecuteMsg::List {
token_id: "token-1".into(),
price: Coin::new(1_000_000u128, "uxion"),
reservation: None,
})?,
});
```
Similar patterns apply for `Reserve`, `Buy` (attach payment funds), and `Delist` messages.

## Feature Flags

- `asset_base` (default): ships the standard marketplace + plugin behavior.
- `crossmint`: alternative configuration for cross-minting scenarios (mutually exclusive with
`asset_base`). Ensure only one of these is enabled at a time.

## Testing

`src/test.rs` demonstrates how to wire mocks for the `SellableAsset` entry points and validate
plugin flows. Use it as a reference when building integration tests around custom plugin behavior.
80 changes: 80 additions & 0 deletions contracts/asset/src/contracts/asset_base.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#[cfg(feature = "asset_base")]
use crate::msg::AssetExtensionQueryMsg;
// Default implementation of the xion asset standard showing how to set up a contract
// to use the default trait XionAssetExecuteExtension
use crate::plugin::PluggableAsset;

Check warning on line 5 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused import: `crate::plugin::PluggableAsset`

Check failure on line 5 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Lints

unused import: `crate::plugin::PluggableAsset`
use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult};

Check warning on line 6 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused imports: `Binary`, `DepsMut`, `Deps`, `Env`, `MessageInfo`, `Response`, and `StdResult`

Check failure on line 6 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Lints

unused imports: `Binary`, `DepsMut`, `Deps`, `Env`, `MessageInfo`, `Response`, and `StdResult`
use cw721::{
DefaultOptionalCollectionExtension, DefaultOptionalCollectionExtensionMsg,
DefaultOptionalNftExtension, DefaultOptionalNftExtensionMsg, traits::Cw721Execute,

Check warning on line 9 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused import: `traits::Cw721Execute`

Check failure on line 9 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Lints

unused import: `traits::Cw721Execute`
};

use crate::{
CONTRACT_NAME, CONTRACT_VERSION,

Check warning on line 13 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Test Suite

unused imports: `AssetContract`, `AssetExtensionExecuteMsg`, `CONTRACT_NAME`, `CONTRACT_VERSION`, `ExecuteMsg`, `InstantiateMsg`, and `error::ContractResult`

Check failure on line 13 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Lints

unused imports: `AssetContract`, `AssetExtensionExecuteMsg`, `CONTRACT_NAME`, `CONTRACT_VERSION`, `ExecuteMsg`, `InstantiateMsg`, and `error::ContractResult`
error::ContractResult,
msg::{AssetExtensionExecuteMsg, ExecuteMsg, InstantiateMsg},
traits::{AssetContract, DefaultAssetContract},
};
type AssetBaseContract<'a> = DefaultAssetContract<

Check warning on line 18 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Test Suite

type alias `AssetBaseContract` is never used

Check failure on line 18 in contracts/asset/src/contracts/asset_base.rs

View workflow job for this annotation

GitHub Actions / Lints

type alias `AssetBaseContract` is never used
'a,
DefaultOptionalNftExtension,
DefaultOptionalNftExtensionMsg,
DefaultOptionalCollectionExtension,
DefaultOptionalCollectionExtensionMsg,
>;

#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)]
#[cfg(feature = "asset_base")]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg<DefaultOptionalCollectionExtensionMsg>,
) -> ContractResult<Response> {
let contract: AssetBaseContract<'static> = AssetContract::default();

contract
.instantiate_with_version(deps, &env, &info, msg, CONTRACT_NAME, CONTRACT_VERSION)
.map_err(Into::into)
}

#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)]
#[cfg(feature = "asset_base")]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg<
DefaultOptionalNftExtensionMsg,
DefaultOptionalCollectionExtensionMsg,
AssetExtensionExecuteMsg,
>,
) -> ContractResult<Response> {
let contract: AssetBaseContract<'static> = AssetContract::default();

contract
.execute_pluggable(deps, &env, &info, msg)
.map_err(Into::into)
}

#[cfg_attr(not(feature = "library"), cosmwasm_std::entry_point)]
#[cfg(feature = "asset_base")]
pub fn query(
deps: Deps,
env: Env,
msg: cw721::msg::Cw721QueryMsg<
DefaultOptionalNftExtension,
DefaultOptionalCollectionExtension,
AssetExtensionQueryMsg,
>,
) -> StdResult<Binary> {
use cw721::traits::Cw721Query;

use crate::error::ContractError;

let contract: AssetBaseContract<'static> = AssetContract::default();

contract
.query(deps, &env, msg)
.map_err(|err| ContractError::from(err).into())
}
1 change: 1 addition & 0 deletions contracts/asset/src/contracts/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod asset_base;
58 changes: 58 additions & 0 deletions contracts/asset/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum ContractError {
// Generic errors
#[error("{0}")]
Std(#[from] cosmwasm_std::StdError),

#[error("Unauthorized")]
Unauthorized {},

#[error("Listing already exists: {id}")]
ListingAlreadyExists { id: String },

#[error("Listing not found: {id}")]
ListingNotFound { id: String },

#[error("Reserved asset: {id}")]
ReservedAsset { id: String }, // e.g. listing is reserved

#[error("Invalid listing price: {price}")]
InvalidListingPrice { price: u128 },

#[error("Invalid payment: {price} {denom}")]
InvalidPayment { price: u128, denom: String },

#[error("Insufficient funds")]
InsufficientFunds {},

#[error("No payment")]
NoPayment {},

#[error("Plugin error: {msg}")]
PluginError { msg: String },

#[error("Invalid marketplace fee: {bps}, {recipient}")]
InvalidMarketplaceFee { bps: u16, recipient: String },
}

impl From<ContractError> for cw721::error::Cw721ContractError {
fn from(value: ContractError) -> Self {
cw721::error::Cw721ContractError::Std(cosmwasm_std::StdError::generic_err(
value.to_string(),
))
}
}

impl From<cw721::error::Cw721ContractError> for ContractError {
fn from(value: cw721::error::Cw721ContractError) -> Self {
ContractError::Std(cosmwasm_std::StdError::generic_err(value.to_string()))
}
}

impl From<ContractError> for cosmwasm_std::StdError {
fn from(value: ContractError) -> Self {
cosmwasm_std::StdError::generic_err(value.to_string())
}
}

pub type ContractResult<T> = Result<T, ContractError>;
Loading
Loading