diff --git a/Architecture.md b/Architecture.md index d4ccc966c..040d2d404 100644 --- a/Architecture.md +++ b/Architecture.md @@ -80,7 +80,7 @@ This design makes it **impossible** to implement conflicting extensions: ```rust // ✅ This works - using Enumerable impl NonFungibleToken for MyContract { - type ContractType = Enumerable; + type ContractType = Compose<(Enumerable,)>; // ... implementations } impl NonFungibleEnumerable for MyContract { @@ -199,7 +199,9 @@ pub struct MyToken; #[contractimpl(contracttrait)] impl FungibleToken for MyToken { - type ContractType = Base; + // `Compose` resolves the list of contract types (the behavior-overriding + // ones, not additive extensions like `Burnable`) to the right one. + type ContractType = Compose<(Base,)>; // The macro fills in every method body, no manual overrides needed. // Alternatively, custom overrides can be provided here (optional) } diff --git a/examples/fungible-allowlist/src/contract.rs b/examples/fungible-allowlist/src/contract.rs index e64c2ecbb..932bd15de 100644 --- a/examples/fungible-allowlist/src/contract.rs +++ b/examples/fungible-allowlist/src/contract.rs @@ -13,7 +13,7 @@ use stellar_macros::only_role; use stellar_tokens::fungible::{ allowlist::{AllowList, FungibleAllowList}, burnable::FungibleBurnable, - Base, FungibleToken, + Base, Compose, FungibleToken, }; #[contract] @@ -46,7 +46,7 @@ impl ExampleContract { #[contractimpl(contracttrait)] impl FungibleToken for ExampleContract { - type ContractType = AllowList; + type ContractType = Compose<(AllowList,)>; } #[contractimpl(contracttrait)] diff --git a/examples/fungible-blocklist/src/contract.rs b/examples/fungible-blocklist/src/contract.rs index 2759e691c..d19e8760a 100644 --- a/examples/fungible-blocklist/src/contract.rs +++ b/examples/fungible-blocklist/src/contract.rs @@ -14,7 +14,7 @@ use stellar_macros::only_role; use stellar_tokens::fungible::{ blocklist::{BlockList, FungibleBlockList}, burnable::FungibleBurnable, - Base, FungibleToken, + Base, Compose, FungibleToken, }; #[contract] @@ -52,7 +52,7 @@ impl ExampleContract { #[contractimpl(contracttrait)] impl FungibleToken for ExampleContract { - type ContractType = BlockList; + type ContractType = Compose<(BlockList,)>; } #[contractimpl(contracttrait)] diff --git a/examples/fungible-capped/src/contract.rs b/examples/fungible-capped/src/contract.rs index aa8c89b54..7392341f0 100644 --- a/examples/fungible-capped/src/contract.rs +++ b/examples/fungible-capped/src/contract.rs @@ -10,7 +10,7 @@ use soroban_sdk::{contract, contractimpl, Address, Env, MuxedAddress, String}; use stellar_tokens::fungible::{ capped::{check_cap, set_cap}, - Base, FungibleToken, + Base, Compose, FungibleToken, }; #[contract] @@ -30,5 +30,5 @@ impl ExampleContract { #[contractimpl(contracttrait)] impl FungibleToken for ExampleContract { - type ContractType = Base; + type ContractType = Compose<(Base,)>; } diff --git a/examples/fungible-governor-timelock/token/src/token.rs b/examples/fungible-governor-timelock/token/src/token.rs index 5a961bf8b..fb1f4d35b 100644 --- a/examples/fungible-governor-timelock/token/src/token.rs +++ b/examples/fungible-governor-timelock/token/src/token.rs @@ -2,7 +2,7 @@ use soroban_sdk::{contract, contractimpl, Address, Env, MuxedAddress, String}; use stellar_access::ownable::{set_owner, Ownable}; use stellar_governance::votes::Votes; use stellar_macros::only_owner; -use stellar_tokens::fungible::{votes::FungibleVotes, Base, FungibleToken}; +use stellar_tokens::fungible::{votes::FungibleVotes, Base, Compose, FungibleToken}; #[contract] pub struct TokenContract; @@ -27,7 +27,7 @@ impl TokenContract { #[contractimpl(contracttrait)] impl FungibleToken for TokenContract { - type ContractType = FungibleVotes; + type ContractType = Compose<(FungibleVotes,)>; } #[contractimpl(contracttrait)] diff --git a/examples/fungible-governor/token/src/token.rs b/examples/fungible-governor/token/src/token.rs index 5a961bf8b..fb1f4d35b 100644 --- a/examples/fungible-governor/token/src/token.rs +++ b/examples/fungible-governor/token/src/token.rs @@ -2,7 +2,7 @@ use soroban_sdk::{contract, contractimpl, Address, Env, MuxedAddress, String}; use stellar_access::ownable::{set_owner, Ownable}; use stellar_governance::votes::Votes; use stellar_macros::only_owner; -use stellar_tokens::fungible::{votes::FungibleVotes, Base, FungibleToken}; +use stellar_tokens::fungible::{votes::FungibleVotes, Base, Compose, FungibleToken}; #[contract] pub struct TokenContract; @@ -27,7 +27,7 @@ impl TokenContract { #[contractimpl(contracttrait)] impl FungibleToken for TokenContract { - type ContractType = FungibleVotes; + type ContractType = Compose<(FungibleVotes,)>; } #[contractimpl(contracttrait)] diff --git a/examples/fungible-pausable/src/contract.rs b/examples/fungible-pausable/src/contract.rs index 8cd104da3..823163f95 100644 --- a/examples/fungible-pausable/src/contract.rs +++ b/examples/fungible-pausable/src/contract.rs @@ -15,7 +15,7 @@ use soroban_sdk::{ }; use stellar_contract_utils::pausable::{self as pausable, Pausable}; use stellar_macros::when_not_paused; -use stellar_tokens::fungible::{burnable::FungibleBurnable, Base, FungibleToken}; +use stellar_tokens::fungible::{burnable::FungibleBurnable, Base, Compose, FungibleToken}; pub const OWNER: Symbol = symbol_short!("OWNER"); @@ -90,7 +90,7 @@ impl Pausable for ExampleContract { #[contractimpl] impl FungibleToken for ExampleContract { - type ContractType = Base; + type ContractType = Compose<(Base,)>; fn total_supply(e: &Env) -> i128 { Self::ContractType::total_supply(e) diff --git a/examples/fungible-vault/src/contract.rs b/examples/fungible-vault/src/contract.rs index 01e6e9d5c..de1cdd8e8 100644 --- a/examples/fungible-vault/src/contract.rs +++ b/examples/fungible-vault/src/contract.rs @@ -2,7 +2,7 @@ use soroban_sdk::{contract, contractimpl, Address, Env, MuxedAddress, String}; use stellar_tokens::{ - fungible::{Base, FungibleToken}, + fungible::{Base, Compose, FungibleToken}, vault::{FungibleVault, Vault}, }; @@ -29,7 +29,7 @@ impl ExampleContract { #[contractimpl(contracttrait)] impl FungibleToken for ExampleContract { - type ContractType = Vault; + type ContractType = Compose<(Vault,)>; // Allows override of decimals and other base functions. diff --git a/examples/fungible-votes/src/contract.rs b/examples/fungible-votes/src/contract.rs index b66aa5441..1e1d29a83 100644 --- a/examples/fungible-votes/src/contract.rs +++ b/examples/fungible-votes/src/contract.rs @@ -3,7 +3,7 @@ use stellar_access::ownable::{set_owner, Ownable}; use stellar_governance::votes::Votes; use stellar_macros::only_owner; use stellar_tokens::fungible::{ - burnable::FungibleBurnable, votes::FungibleVotes, Base, FungibleToken, + burnable::FungibleBurnable, votes::FungibleVotes, Base, Compose, FungibleToken, }; #[contract] @@ -24,7 +24,7 @@ impl ExampleContract { #[contractimpl(contracttrait)] impl FungibleToken for ExampleContract { - type ContractType = FungibleVotes; + type ContractType = Compose<(FungibleVotes,)>; } #[contractimpl(contracttrait)] diff --git a/examples/nft-access-control/src/contract.rs b/examples/nft-access-control/src/contract.rs index 3d96ac413..9fb301687 100644 --- a/examples/nft-access-control/src/contract.rs +++ b/examples/nft-access-control/src/contract.rs @@ -5,7 +5,9 @@ use soroban_sdk::{contract, contractimpl, vec, Address, Env, String, Symbol, Vec}; use stellar_access::access_control::{set_admin, AccessControl}; use stellar_macros::{has_any_role, has_role, only_admin, only_any_role, only_role}; -use stellar_tokens::non_fungible::{burnable::NonFungibleBurnable, Base, NonFungibleToken}; +use stellar_tokens::non_fungible::{ + burnable::NonFungibleBurnable, Base, Compose, NonFungibleToken, +}; #[contract] pub struct ExampleContract; @@ -46,7 +48,7 @@ impl ExampleContract { #[contractimpl(contracttrait)] impl NonFungibleToken for ExampleContract { - type ContractType = Base; + type ContractType = Compose<(Base,)>; } // for this contract, the `burn*` functions are only meant to be called by diff --git a/examples/nft-consecutive/src/contract.rs b/examples/nft-consecutive/src/contract.rs index a7ecee399..3d7785b6c 100644 --- a/examples/nft-consecutive/src/contract.rs +++ b/examples/nft-consecutive/src/contract.rs @@ -7,7 +7,7 @@ use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String}; use stellar_tokens::non_fungible::{ burnable::NonFungibleBurnable, consecutive::{Consecutive, NonFungibleConsecutive}, - Base, ContractOverrides, NonFungibleToken, + Base, Compose, ContractOverrides, NonFungibleToken, }; #[contracttype] @@ -39,7 +39,7 @@ impl ExampleContract { // `#[contractimpl(contracttrait)]` macro. #[contractimpl(contracttrait)] impl NonFungibleToken for ExampleContract { - type ContractType = Consecutive; + type ContractType = Compose<(Consecutive,)>; fn balance(e: &Env, owner: Address) -> u32 { Self::ContractType::balance(e, &owner) diff --git a/examples/nft-enumerable/src/contract.rs b/examples/nft-enumerable/src/contract.rs index 94b0a6e8a..0c224eaae 100644 --- a/examples/nft-enumerable/src/contract.rs +++ b/examples/nft-enumerable/src/contract.rs @@ -8,7 +8,7 @@ use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String}; use stellar_tokens::non_fungible::{ burnable::NonFungibleBurnable, enumerable::{Enumerable, NonFungibleEnumerable}, - Base, NonFungibleToken, + Base, Compose, NonFungibleToken, }; #[contracttype] @@ -36,7 +36,7 @@ impl ExampleContract { #[contractimpl(contracttrait)] impl NonFungibleToken for ExampleContract { - type ContractType = Enumerable; + type ContractType = Compose<(Enumerable,)>; } #[contractimpl(contracttrait)] diff --git a/examples/nft-royalties/src/contract.rs b/examples/nft-royalties/src/contract.rs index 99a5ef80a..6ec359b04 100644 --- a/examples/nft-royalties/src/contract.rs +++ b/examples/nft-royalties/src/contract.rs @@ -7,7 +7,9 @@ use soroban_sdk::{contract, contractimpl, symbol_short, Address, Env, String, Symbol, Vec}; use stellar_access::access_control::{self as access_control, AccessControl}; use stellar_macros::{only_admin, only_role}; -use stellar_tokens::non_fungible::{royalties::NonFungibleRoyalties, Base, NonFungibleToken}; +use stellar_tokens::non_fungible::{ + royalties::NonFungibleRoyalties, Base, Compose, NonFungibleToken, +}; #[contract] pub struct ExampleContract; @@ -53,7 +55,7 @@ impl ExampleContract { #[contractimpl(contracttrait)] impl NonFungibleToken for ExampleContract { - type ContractType = Base; + type ContractType = Compose<(Base,)>; } #[contractimpl(contracttrait)] diff --git a/examples/nft-sequential-minting/src/contract.rs b/examples/nft-sequential-minting/src/contract.rs index 3e9aba7e2..df3be767a 100644 --- a/examples/nft-sequential-minting/src/contract.rs +++ b/examples/nft-sequential-minting/src/contract.rs @@ -3,7 +3,9 @@ //! Demonstrates an example usage of the NFT default base implementation. use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, String}; -use stellar_tokens::non_fungible::{burnable::NonFungibleBurnable, Base, NonFungibleToken}; +use stellar_tokens::non_fungible::{ + burnable::NonFungibleBurnable, Base, Compose, NonFungibleToken, +}; #[contracttype] pub enum DataKey { @@ -30,7 +32,7 @@ impl ExampleContract { #[contractimpl(contracttrait)] impl NonFungibleToken for ExampleContract { - type ContractType = Base; + type ContractType = Compose<(Base,)>; } #[contractimpl(contracttrait)] diff --git a/examples/rwa/token/src/contract.rs b/examples/rwa/token/src/contract.rs index 610334064..d29e4e180 100644 --- a/examples/rwa/token/src/contract.rs +++ b/examples/rwa/token/src/contract.rs @@ -7,7 +7,7 @@ use stellar_access::access_control::{self as access_control, AccessControl}; use stellar_contract_utils::pausable::{self as pausable, Pausable}; use stellar_macros::{only_admin, only_role}; use stellar_tokens::{ - fungible::{Base, FungibleToken}, + fungible::{Base, Compose, FungibleToken}, rwa::{RWAToken, RWA}, }; @@ -54,7 +54,7 @@ impl Pausable for RWATokenContract { #[contractimpl(contracttrait)] impl FungibleToken for RWATokenContract { - type ContractType = RWA; + type ContractType = Compose<(RWA,)>; } #[contractimpl(contracttrait)] diff --git a/packages/tokens/src/fungible/extensions/burnable/mod.rs b/packages/tokens/src/fungible/extensions/burnable/mod.rs index 8cd699498..0b5e2f07a 100644 --- a/packages/tokens/src/fungible/extensions/burnable/mod.rs +++ b/packages/tokens/src/fungible/extensions/burnable/mod.rs @@ -33,8 +33,9 @@ use crate::fungible::{overrides::BurnableOverrides, FungibleToken}; /// /// The burn logic is selected automatically based on the `ContractType` set /// on the `FungibleToken` implementation. If the contract uses -/// `type ContractType = AllowList`, burning checks the allowlist; with -/// `type ContractType = Base` it uses the vanilla behavior, and so on. There +/// `type ContractType = Compose<(AllowList,)>`, burning checks the allowlist; +/// with `type ContractType = Compose<(Base,)>` it uses the vanilla behavior, +/// and so on. There /// is no need to interact with the override machinery, it works in the /// background. #[contracttrait] diff --git a/packages/tokens/src/fungible/extensions/combinations/mod.rs b/packages/tokens/src/fungible/extensions/combinations/mod.rs new file mode 100644 index 000000000..4515c2357 --- /dev/null +++ b/packages/tokens/src/fungible/extensions/combinations/mod.rs @@ -0,0 +1,109 @@ +//! # Contract Type Composition for Fungible Token. +//! +//! A contract selects exactly one `ContractType` on its +//! [`crate::fungible::FungibleToken`] implementation, and that single slot +//! decides all overridable behavior. The contract type is selected uniformly +//! with [`Compose`], by listing the contract types that override the `Base` +//! behavior. [`Compose`] resolves the list to the curated contract type at +//! compile time. +//! +//! Note that [`Compose`] only selects the `ContractType`; it says nothing +//! about extensions that add new functionality without overriding the base +//! behavior. Those (e.g. [`crate::fungible::burnable::FungibleBurnable`] or +//! the [`crate::fungible::capped`] helpers) have no contract type and are +//! simply implemented alongside; there is no `Compose<(Burnable,)>`. +//! +//! Usage: +//! +//! ```ignore +//! #[contractimpl(contracttrait)] +//! impl FungibleToken for MyToken { +//! type ContractType = Compose<(AllowList,)>; +//! } +//! +//! #[contractimpl(contracttrait)] +//! impl FungibleAllowList for MyToken { +//! // ... +//! } +//! ``` +//! +//! No multi-type combinations are curated for fungible tokens yet; every +//! valid list currently holds a single contract type. Invalid lists do not +//! compile: `AllowList` and `BlockList` are mutually exclusive, and +//! implementing an extension trait the list does not back (e.g. +//! [`crate::fungible::allowlist::FungibleAllowList`] without `AllowList` in +//! the list) is rejected by that trait's bound. + +use crate::{ + fungible::{ + extensions::{allowlist::AllowList, blocklist::BlockList, votes::FungibleVotes}, + Base, ContractOverrides, + }, + rwa::RWA, + vault::Vault, +}; + +/// Resolves a list of contract types to the combined contract type, e.g. +/// `Compose<(AllowList,)>`. +/// +/// This is shorthand for `::Out`; refer to [`Composable`] for +/// the valid lists. +pub type Compose = ::Out; + +/// Type-level lookup backing [`Compose`]: each valid contract type list +/// resolves to its combined contract type through the `Out` associated type. +/// +/// Single contract types resolve to themselves (with or without the +/// one-element tuple form). Mutually exclusive contract types (e.g. +/// `AllowList` and `BlockList`) have no implementation, so combining them +/// does not compile. +#[diagnostic::on_unimplemented( + message = "`{Self}` is not a valid contract type combination", + note = "valid single contract types: `Base`, `AllowList`, `BlockList`, `RWA`, `Vault`, \ + `FungibleVotes`", + note = "no multi-type combinations are curated for fungible tokens yet", + note = "`AllowList` and `BlockList` are mutually exclusive" +)] +pub trait Composable { + type Out: ContractOverrides; +} + +// Single contract types resolve to themselves. The bare form and the +// one-element tuple form are equivalent, so a stray trailing comma does not +// change the meaning. +impl Composable for Base { + type Out = Base; +} +impl Composable for (Base,) { + type Out = Base; +} +impl Composable for AllowList { + type Out = AllowList; +} +impl Composable for (AllowList,) { + type Out = AllowList; +} +impl Composable for BlockList { + type Out = BlockList; +} +impl Composable for (BlockList,) { + type Out = BlockList; +} +impl Composable for RWA { + type Out = RWA; +} +impl Composable for (RWA,) { + type Out = RWA; +} +impl Composable for Vault { + type Out = Vault; +} +impl Composable for (Vault,) { + type Out = Vault; +} +impl Composable for FungibleVotes { + type Out = FungibleVotes; +} +impl Composable for (FungibleVotes,) { + type Out = FungibleVotes; +} diff --git a/packages/tokens/src/fungible/extensions/mod.rs b/packages/tokens/src/fungible/extensions/mod.rs index 7b9304053..3af97ab36 100644 --- a/packages/tokens/src/fungible/extensions/mod.rs +++ b/packages/tokens/src/fungible/extensions/mod.rs @@ -2,4 +2,5 @@ pub mod allowlist; pub mod blocklist; pub mod burnable; pub mod capped; +pub mod combinations; pub mod votes; diff --git a/packages/tokens/src/fungible/mod.rs b/packages/tokens/src/fungible/mod.rs index 9ba144ae6..22f9cef31 100644 --- a/packages/tokens/src/fungible/mod.rs +++ b/packages/tokens/src/fungible/mod.rs @@ -74,7 +74,9 @@ mod utils; #[cfg(test)] mod test; -pub use extensions::{allowlist, blocklist, burnable, capped, votes}; +pub use extensions::{ + allowlist, blocklist, burnable, capped, combinations, combinations::Compose, votes, +}; pub use overrides::{Base, ContractOverrides}; use soroban_sdk::{ contracterror, contractevent, contracttrait, Address, Env, MuxedAddress, String, @@ -116,10 +118,17 @@ pub use utils::{sac_admin_generic, sac_admin_wrapper}; /// [`crate::rwa::RWAToken`]) trait, incompatible with /// [`crate::fungible::allowlist::AllowList`] trait and /// [`crate::fungible::blocklist::BlockList`] trait. +/// * [`crate::vault::Vault`] (enabling the compatibility and overrides for +/// [`crate::vault::FungibleVault`]) trait. +/// * [`crate::fungible::votes::FungibleVotes`] (enabling the compatibility and +/// overrides for [`stellar_governance::votes::Votes`]) trait. +/// +/// The contract type is selected with +/// [`crate::fungible::combinations::Compose`]; invalid combinations are +/// rejected at compile time. /// -/// The default implementations of this trait for `Base`, `Allowlist`, -/// `Blocklist` and `RWA` can be found by navigating to: -/// `ContractType::{method_name}`. +/// The default implementations of this trait for each contract type can be +/// found by navigating to: `ContractType::{method_name}`. /// /// For example, the implementation of [`FungibleToken::transfer`] for the /// `Allowlist` contract type can be found at @@ -128,10 +137,13 @@ pub use utils::{sac_admin_generic, sac_admin_wrapper}; pub trait FungibleToken { /// Helper type that allows some of the functionality of the base trait to /// be overridden based on the extensions implemented. - /// [`crate::fungible::Base`] should be used as the type when - /// not using - /// [`crate::fungible::allowlist::AllowList`] or - /// [`crate::fungible::blocklist::BlockList`] extensions. + /// The contract type is selected with + /// [`crate::fungible::combinations::Compose`], by listing the contract + /// types that override the `Base` behavior: `Compose<(Base,)>` for the + /// vanilla case, `Compose<(AllowList,)>` for an allowlist token, and so + /// on. Extensions that add functionality without overriding behavior + /// (e.g. [`crate::fungible::burnable::FungibleBurnable`]) have no + /// contract type and do not appear in the list. type ContractType: ContractOverrides; /// Returns the total amount of tokens in circulation. diff --git a/packages/tokens/src/fungible/overrides.rs b/packages/tokens/src/fungible/overrides.rs index 2943af9d3..7a7bdd44f 100644 --- a/packages/tokens/src/fungible/overrides.rs +++ b/packages/tokens/src/fungible/overrides.rs @@ -25,7 +25,7 @@ use soroban_sdk::{Address, Env, MuxedAddress, String}; /// ```rust /// #[contractimpl(contracttrait)] /// impl FungibleToken for ExampleContract { -/// type ContractType = Base; +/// type ContractType = Compose<(Base,)>; /// } /// ``` pub trait ContractOverrides { diff --git a/packages/tokens/src/non_fungible/extensions/burnable/mod.rs b/packages/tokens/src/non_fungible/extensions/burnable/mod.rs index b13ba45ab..18c9cda5f 100644 --- a/packages/tokens/src/non_fungible/extensions/burnable/mod.rs +++ b/packages/tokens/src/non_fungible/extensions/burnable/mod.rs @@ -34,8 +34,9 @@ use soroban_sdk::{contractevent, contracttrait, Address, Env}; /// /// The burn logic is selected automatically based on the `ContractType` set /// on the `NonFungibleToken` implementation. If the contract uses -/// `type ContractType = Consecutive`, burning uses the consecutive bookkeeping; -/// with `type ContractType = Base` it uses the vanilla behavior, and so on. +/// `type ContractType = Compose<(Consecutive,)>`, burning uses the consecutive +/// bookkeeping; with `type ContractType = Compose<(Base,)>` it uses the vanilla +/// behavior, and so on. /// There is no need to interact with the override machinery, it works in the /// background. #[contracttrait] diff --git a/packages/tokens/src/non_fungible/extensions/combinations/mod.rs b/packages/tokens/src/non_fungible/extensions/combinations/mod.rs new file mode 100644 index 000000000..d28f43a27 --- /dev/null +++ b/packages/tokens/src/non_fungible/extensions/combinations/mod.rs @@ -0,0 +1,90 @@ +//! # Contract Type Composition for Non-Fungible Token. +//! +//! A contract selects exactly one `ContractType` on its +//! [`crate::non_fungible::NonFungibleToken`] implementation, and that single +//! slot decides all overridable behavior. The contract type is selected +//! uniformly with [`Compose`], by listing the contract types that override +//! the `Base` behavior. +//! +//! Note that [`Compose`] only selects the `ContractType`; it says nothing +//! about extensions that add new functionality without overriding the base +//! behavior. Those (e.g. [`crate::non_fungible::burnable::NonFungibleBurnable`] +//! or [`crate::non_fungible::royalties::NonFungibleRoyalties`]) have no +//! contract type and are simply implemented alongside; there is no +//! `Compose<(Burnable,)>`. +//! +//! Usage: +//! +//! ```ignore +//! #[contractimpl(contracttrait)] +//! impl NonFungibleToken for MyToken { +//! type ContractType = Compose<(Enumerable,)>; +//! } +//! +//! #[contractimpl(contracttrait)] +//! impl NonFungibleEnumerable for MyToken {} +//! ``` +//! +//! No multi-type combinations are curated for non-fungible tokens yet; every +//! valid list currently holds a single contract type. Invalid lists do not +//! compile: `Enumerable` and `Consecutive` are mutually exclusive, and +//! implementing an extension trait the list does not back (e.g. +//! [`crate::non_fungible::enumerable::NonFungibleEnumerable`] without +//! `Enumerable` in the list) is rejected by that trait's bound. + +use crate::non_fungible::{ + extensions::{consecutive::Consecutive, enumerable::Enumerable, votes::NonFungibleVotes}, + Base, ContractOverrides, +}; + +/// Resolves a list of contract types to the combined contract type, e.g. +/// `Compose<(Enumerable,)>`. +/// +/// This is shorthand for `::Out`; refer to [`Composable`] for +/// the valid lists. +pub type Compose = ::Out; + +/// Type-level lookup backing [`Compose`]: each valid contract type list +/// resolves to its contract type through the `Out` associated type. +/// +/// Single contract types resolve to themselves (with or without the +/// one-element tuple form). Mutually exclusive contract types (e.g. +/// `Enumerable` and `Consecutive`) have no implementation, so combining them +/// does not compile. +#[diagnostic::on_unimplemented( + message = "`{Self}` is not a valid contract type combination", + note = "valid single contract types: `Base`, `Enumerable`, `Consecutive`, `NonFungibleVotes`", + note = "no multi-type combinations are curated for non-fungible tokens yet", + note = "`Enumerable` and `Consecutive` are mutually exclusive" +)] +pub trait Composable { + type Out: ContractOverrides; +} + +// Single contract types resolve to themselves. The bare form and the +// one-element tuple form are equivalent, so a stray trailing comma does not +// change the meaning. +impl Composable for Base { + type Out = Base; +} +impl Composable for (Base,) { + type Out = Base; +} +impl Composable for Enumerable { + type Out = Enumerable; +} +impl Composable for (Enumerable,) { + type Out = Enumerable; +} +impl Composable for Consecutive { + type Out = Consecutive; +} +impl Composable for (Consecutive,) { + type Out = Consecutive; +} +impl Composable for NonFungibleVotes { + type Out = NonFungibleVotes; +} +impl Composable for (NonFungibleVotes,) { + type Out = NonFungibleVotes; +} diff --git a/packages/tokens/src/non_fungible/extensions/mod.rs b/packages/tokens/src/non_fungible/extensions/mod.rs index c8bbbf7f6..76f926fee 100644 --- a/packages/tokens/src/non_fungible/extensions/mod.rs +++ b/packages/tokens/src/non_fungible/extensions/mod.rs @@ -1,4 +1,5 @@ pub mod burnable; +pub mod combinations; pub mod consecutive; pub mod enumerable; pub mod royalties; diff --git a/packages/tokens/src/non_fungible/mod.rs b/packages/tokens/src/non_fungible/mod.rs index f6d625ce6..13ddd724a 100644 --- a/packages/tokens/src/non_fungible/mod.rs +++ b/packages/tokens/src/non_fungible/mod.rs @@ -71,7 +71,9 @@ mod utils; #[cfg(test)] mod test; -pub use extensions::{burnable, consecutive, enumerable, royalties, votes}; +pub use extensions::{ + burnable, combinations, combinations::Compose, consecutive, enumerable, royalties, votes, +}; pub use overrides::{Base, ContractOverrides}; // ################## TRAIT ################## use soroban_sdk::{contracterror, contractevent, contracttrait, Address, Env, String}; @@ -100,19 +102,22 @@ pub use utils::sequential; /// compatibility and overrides for /// [`crate::non_fungible::extensions::enumerable::NonFungibleEnumerable`]) /// trait, incompatible with -/// [`crate::non_fungible::extensions::burnable::NonFungibleBurnable`]) and -/// [`crate::non_fungible::extensions::consecutive::NonFungibleConsecutive`] -/// trait. +/// [`crate::non_fungible::extensions::consecutive::Consecutive`] trait. /// * [`crate::non_fungible::extensions::consecutive::Consecutive`] (enabling /// the compatibility and overrides for /// [`crate::non_fungible::extensions::consecutive::NonFungibleConsecutive`]) /// trait, incompatible with -/// [`crate::non_fungible::extensions::burnable::NonFungibleBurnable`]) and -/// [`crate::non_fungible::extensions::enumerable::NonFungibleEnumerable`] +/// [`crate::non_fungible::extensions::enumerable::Enumerable`] trait. +/// * [`crate::non_fungible::extensions::votes::NonFungibleVotes`] (enabling the +/// compatibility and overrides for [`stellar_governance::votes::Votes`]) /// trait. /// -/// The default implementations of this trait for `Base`, `Enumerable`, and -/// `Consecutive` can be found by navigating to `ContractType::{method_name}`. +/// The contract type is selected with +/// [`crate::non_fungible::combinations::Compose`]; invalid combinations are +/// rejected at compile time. +/// +/// The default implementations of this trait for each contract type can be +/// found by navigating to `ContractType::{method_name}`. /// For example, the implementation of [`NonFungibleToken::transfer`] for the /// `Enumerable` contract type can be found at /// [`crate::non_fungible::extensions::enumerable::Enumerable::transfer`]. @@ -120,11 +125,14 @@ pub use utils::sequential; pub trait NonFungibleToken { /// Helper type that allows some of the functionality of the base trait to /// be overridden based on the extensions implemented. - /// [`crate::non_fungible::Base`] should be used as the type when - /// not using - /// [`crate::non_fungible::extensions::enumerable::Enumerable`] or - /// [`crate::non_fungible::extensions::consecutive::Consecutive`] - /// extensions. + /// The contract type is selected with + /// [`crate::non_fungible::combinations::Compose`], by listing the + /// contract types that override the `Base` behavior: `Compose<(Base,)>` + /// for the vanilla case, `Compose<(Enumerable,)>` for an enumerable + /// token, and so on. Extensions that add functionality without + /// overriding behavior (e.g. + /// [`crate::non_fungible::burnable::NonFungibleBurnable`]) have no + /// contract type and do not appear in the list. type ContractType: ContractOverrides; /// Returns the number of tokens owned by `account`. diff --git a/packages/tokens/src/non_fungible/overrides.rs b/packages/tokens/src/non_fungible/overrides.rs index ab368f2df..dd4c84b3a 100644 --- a/packages/tokens/src/non_fungible/overrides.rs +++ b/packages/tokens/src/non_fungible/overrides.rs @@ -26,7 +26,7 @@ use soroban_sdk::{Address, Env, String}; /// ```rust /// #[contractimpl(contracttrait)] /// impl NonFungibleToken for ExampleContract { -/// type ContractType = Consecutive; +/// type ContractType = Compose<(Consecutive,)>; /// } /// ``` pub trait ContractOverrides {