Stellar Soroban smart contracts for minting video clips as NFTs with royalty support.
ClipCashNFT lets creators mint their best clips as NFTs on the Stellar blockchain. Each token stores a metadata URI (IPFS / Arweave) on-chain and supports EIP-2981-style royalties so creators earn on every secondary sale.
clips-contract/
├── clips_nft/
│ ├── src/
│ │ └── lib.rs # ClipCashNFT contract
│ └── Cargo.toml
├── Cargo.toml # Workspace manifest
├── Makefile # Build / test helpers
├── CONTRIBUTING.md
└── README.md
| Tool | Version |
|---|---|
| Rust | 1.74+ |
| wasm32-unknown-unknown target | — |
| Stellar CLI (optional, for deployment) | 22+ |
# Install Rust wasm target
rustup target add wasm32-unknown-unknown
# Install Stellar CLI (optional)
cargo install --locked stellar-cli# Check
make check
# Run tests
make test
# Build release WASM
make build| Key | Type | Description |
|---|---|---|
Admin |
Address |
Contract owner / admin |
NextTokenId |
u32 |
Auto-increment token ID counter |
Paused |
bool |
Pause flag |
Token(token_id) |
TokenData |
Packed owner address and clip_id |
Metadata(token_id) |
String |
Metadata URI (IPFS / Arweave) |
Royalty(token_id) |
Royalty |
Royalty config for the token |
ClipIdMinted(clip_id) |
TokenId |
Prevents double-minting same clip |
Signer |
BytesN<32> |
Backend Ed25519 public key |
The contract uses compact enum keys and u32 identifiers for token and clip indexes.
This avoids string-based storage keys in hot mint paths.
Estimated mint storage operations:
instancereads: 4 (Admin,NextTokenId,Paused,Signer)instancewrites: 1 (NextTokenId)persistentreads: 1 (ClipIdMinteddedup check)persistentwrites: 2 (TokenData,ClipIdMinted)
Estimated persistent writes per mint: 2.
Initialize the contract and set the admin.
- Signature:
init(env: Env, admin: Address) - Auth: —
- Parameters:
admin: TheAddressthat will be the contract administrator.
- Returns:
()
Register (or rotate) the backend Ed25519 public key used to verify clip ownership before minting.
- Signature:
set_signer(env: Env, admin: Address, pubkey: BytesN<32>) -> Result<(), Error> - Auth:
admin - Parameters:
admin: The contract adminAddress.pubkey: 32-byte Ed25519 public key of the trusted backend signer.
- Returns:
Result<(), Error>
Return the currently registered backend signer public key, if any.
- Signature:
get_signer(env: Env) -> Option<BytesN<32>> - Auth: —
- Returns:
Option<BytesN<32>>
Pause the contract. Blocks mint and transfer until unpaused.
- Signature:
pause(env: Env, admin: Address) -> Result<(), Error> - Auth:
admin - Parameters:
admin: The contract adminAddress.
- Returns:
Result<(), Error>
Unpause the contract, re-enabling mint and transfer.
- Signature:
unpause(env: Env, admin: Address) -> Result<(), Error> - Auth:
admin - Parameters:
admin: The contract adminAddress.
- Returns:
Result<(), Error>
Returns true if the contract is currently paused.
- Signature:
is_paused(env: Env) -> bool - Auth: —
- Returns:
bool
Mint a new NFT for a video clip. Requires a valid Ed25519 signature from the registered backend signer.
- Signature:
mint(env: Env, admin: Address, to: Address, clip_id: u32, metadata_uri: String, royalty: Royalty, signature: BytesN<64>) -> Result<TokenId, Error> - Auth:
admin - Parameters:
admin: The contract adminAddress.to:Addressthat will own the NFT.clip_id:u32unique off-chain clip identifier.metadata_uri:String(IPFS or Arweave URI).royalty:Royaltyconfiguration for secondary sales.signature:BytesN<64>Ed25519 signature from the backend signer.
- Returns:
Result<TokenId, Error>
Transfer NFT ownership from from to to.
- Signature:
transfer(env: Env, from: Address, to: Address, token_id: TokenId) -> Result<(), Error> - Auth:
from - Parameters:
from: Current ownerAddress.to: New ownerAddress.token_id:TokenId(u32) to transfer.
- Returns:
Result<(), Error>
Burn (destroy) an NFT. Only the current owner may burn.
- Signature:
burn(env: Env, owner: Address, token_id: TokenId) -> Result<(), Error> - Auth:
owner - Parameters:
owner: Current ownerAddress.token_id:TokenId(u32) to destroy.
- Returns:
Result<(), Error>
Returns the owner of a given token ID.
- Signature:
owner_of(env: Env, token_id: TokenId) -> Result<Address, Error> - Auth: —
- Parameters:
token_id:TokenId(u32).
- Returns:
Result<Address, Error>
Returns the metadata URI for a given token ID.
- Signature:
token_uri(env: Env, token_id: TokenId) -> Result<String, Error> - Auth: —
- Parameters:
token_id:TokenId(u32).
- Returns:
Result<String, Error>
Look up the on-chain token ID for a given clip_id.
- Signature:
clip_token_id(env: Env, clip_id: u32) -> Result<TokenId, Error> - Auth: —
- Parameters:
clip_id: The off-chain clip identifier (u32).
- Returns:
Result<TokenId, Error>
Returns the stored Royalty struct for a token.
- Signature:
get_royalty(env: Env, token_id: TokenId) -> Result<Royalty, Error> - Auth: —
- Parameters:
token_id:TokenId(u32).
- Returns:
Result<Royalty, Error>
Returns the royalty receiver, amount, and payment asset for a given sale price.
- Signature:
royalty_info(env: Env, token_id: TokenId, sale_price: i128) -> Result<RoyaltyInfo, Error> - Auth: —
- Parameters:
token_id:TokenId(u32).sale_price: The sale price in the asset's smallest unit (i128).
- Returns:
Result<RoyaltyInfo, Error>
Pay royalties for a token sale using the asset configured in the royalty (handles SEP-0041 assets).
- Signature:
pay_royalty(env: Env, payer: Address, token_id: TokenId, sale_price: i128) -> Result<(), Error> - Auth:
payer - Parameters:
payer: Address making the payment.token_id:TokenId(u32).sale_price: The sale price (i128).
- Returns:
Result<(), Error>
Update the royalty configuration for a token. Admin only.
- Signature:
set_royalty(env: Env, admin: Address, token_id: TokenId, new_royalty: Royalty) -> Result<(), Error> - Auth:
admin - Parameters:
admin: The contract adminAddress.token_id:TokenId(u32).new_royalty: The updatedRoyaltyconfig.
- Returns:
Result<(), Error>
Returns the total number of minted tokens.
- Signature:
total_supply(env: Env) -> u32 - Auth: —
- Returns:
u32
Returns true if the token exists.
- Signature:
exists(env: Env, token_id: TokenId) -> bool - Auth: —
- Parameters:
token_id:TokenId(u32).
- Returns:
bool
| Topic | Data type | Emitted by | Description |
|---|---|---|---|
"mint" |
MintEvent |
mint() |
Emitted when a new NFT is minted. |
"paused" |
() |
pause() |
Emitted when the contract is paused. |
"unpaused" |
() |
unpause() |
Emitted when the contract is unpaused. |
"royalty" |
(TokenId, Address, i128, Address) |
pay_royalty() |
Emitted when a royalty is paid for a SEP-0041 asset. Data: (token_id, receiver, amount, asset_address). |
MintEvent fields:
to:Addressclip_id:u32token_id:TokenId(u32)metadata_uri:String
TokenData
pub struct TokenData {
pub owner: Address,
pub clip_id: u32,
}Royalty
pub struct Royalty {
pub recipient: Address,
pub basis_points: u32,
pub asset_address: Option<Address>,
}RoyaltyInfo
pub struct RoyaltyInfo {
pub receiver: Address,
pub royalty_amount: i128,
pub asset_address: Option<Address>,
}// 1. Initialize and Set Signer
client.init(&admin);
client.set_signer(&admin, &backend_pubkey);
// 2. Mint
let token_id = client.mint(
&admin,
&creator,
&42u32, // clip_id
&String::from_str(&env, "ipfs://QmXyz..."), // metadata URI
&Royalty { recipient: creator.clone(), basis_points: 500, asset_address: None }, // 5% XLM
&signature, // Ed25519 signature from backend
);
// 3. Query
let owner = client.owner_of(&token_id);
let uri = client.token_uri(&token_id);
let supply = client.total_supply();
// 4. Royalty for a 1 XLM sale (in stroops: 10_000_000)
let info = client.royalty_info(&token_id, &10_000_000i128);
// info.royalty_amount == 500_000 stroops (5%)
// info.receiver == creator
// 5. Transfer
client.transfer(&creator, &buyer, &token_id);
// 6. Burn
client.burn(&buyer, &token_id);Royalties follow the EIP-2981 pattern adapted for Soroban:
royalty_amount = sale_price × basis_points / 10_000
basis_pointsrange:0–10_000(0 % – 100 %)- Marketplaces call
royalty_info(token_id, sale_price)to get the exact amount to forward toreceiverbefore crediting the seller.
MIT