Skip to content

Commit 760ffe8

Browse files
authored
Merge pull request #665 from smartdev-stack/feat/soroban-pause-royalty-assets-docs-645-646-647-659
Soroban: pausable + custom-asset royalties, contract docs, mint ownership check
2 parents d3135db + 3bcff58 commit 760ffe8

13 files changed

Lines changed: 753 additions & 5 deletions

contracts/README.md

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# ClipCash NFT Soroban Contract
2+
3+
Source: [`contracts/nft-contract/src`](./nft-contract/src). Deployed on Stellar via Soroban. Contract metadata: `name = "ClipCash NFT Contract"`, `version = "1.0.0"`.
4+
5+
This document is the contract ABI reference for frontend/integrator developers. For the backend REST API that wraps these calls (metadata upload, transaction preparation, signature verification), see [`docs/api-contract.md § 6 NFT Minting`](../docs/api-contract.md#6-nft-minting).
6+
7+
## Table of Contents
8+
9+
1. [Data Types](#1-data-types)
10+
2. [Errors](#2-errors)
11+
3. [Public Functions](#3-public-functions)
12+
4. [Events](#4-events)
13+
5. [Usage Examples](#5-usage-examples)
14+
15+
---
16+
17+
## 1. Data Types
18+
19+
### `TokenData`
20+
21+
Stored per minted token (`token_id`).
22+
23+
| Field | Type | Description |
24+
|---|---|---|
25+
| `owner` | `Address` | Current holder of the token |
26+
| `is_soulbound` | `bool` | If `true`, the token can never be transferred or approved |
27+
| `creator` | `Address` | Original minter / recipient — royalty payments go here |
28+
| `clip_id` | `String` | ClipCash clip identifier this token represents |
29+
| `content_uri` | `String` | IPFS/Arweave metadata URI |
30+
| `created_at` | `u64` | Ledger timestamp (seconds) at mint time |
31+
32+
---
33+
34+
## 2. Errors
35+
36+
All fallible functions return `Result<T, Error>`. `Error` is a `#[contracterror]` enum (`u32` codes):
37+
38+
| Code | Variant | Meaning |
39+
|---|---|---|
40+
| 1 | `Unauthorized` | Caller is not the token owner / an approved spender |
41+
| 2 | `TokenNotFound` | No token exists for the given `token_id` |
42+
| 3 | `AlreadyInitialized` | `initialize` called more than once |
43+
| 4 | `NotInitialized` | Admin-gated call made before `initialize` |
44+
| 5 | `SoulboundTokenNotTransferable` | Attempted transfer/approve of a soulbound token |
45+
| 6 | `InvalidTokenId` | `mint` called with a `token_id` that already exists |
46+
| 7 | `InvalidRoyaltyBps` | Royalty value outside 0–10 000 BPS |
47+
| 8 | `ContractPaused` | Contract is paused; mint/transfer/approve rejected |
48+
| 9 | `UnsupportedAsset` | Asset contract address is not on the royalty allow-list |
49+
50+
---
51+
52+
## 3. Public Functions
53+
54+
### Admin & Lifecycle
55+
56+
#### `initialize(admin: Address) -> Result<(), Error>`
57+
One-time setup. Sets the contract admin. Fails with `AlreadyInitialized` if called again.
58+
59+
#### `pause() -> Result<(), Error>`
60+
Pauses the contract — `mint`, `transfer`, `transfer_from`, and `approve` all reject with `ContractPaused` until unpaused. **Admin only.** Emits `paused`.
61+
62+
#### `unpause() -> Result<(), Error>`
63+
Restores normal operation. **Admin only.** Emits `unpaused`.
64+
65+
#### `is_paused() -> bool`
66+
Returns the current pause state. Read-only, no auth required.
67+
68+
### Minting & Ownership
69+
70+
#### `mint(to: Address, token_id: u64, clip_id: String, content_uri: String, is_soulbound: bool) -> Result<(), Error>`
71+
Mints a new token to `to`. **Admin only** (`admin.require_auth()`); rejected while paused. Fails with `InvalidTokenId` if `token_id` is already minted. The minting recipient (`to`) is recorded as the token's `creator` for royalty purposes. Emits `mint`.
72+
73+
#### `owner_of(token_id: u64) -> Option<Address>`
74+
Current owner of a token, or `None` if it doesn't exist.
75+
76+
#### `get_token_data(token_id: u64) -> Option<TokenData>`
77+
Full stored record for a token.
78+
79+
#### `is_soulbound(token_id: u64) -> bool`
80+
Whether the token is non-transferable. Returns `false` for unknown tokens.
81+
82+
#### `get_creator(token_id: u64) -> Option<Address>`
83+
The original minting recipient (used as the royalty recipient).
84+
85+
#### `balance_of(owner: Address) -> u64`
86+
Number of tokens held by `owner`.
87+
88+
#### `total_supply() -> u64`
89+
Total tokens ever minted.
90+
91+
### Transfers & Approvals
92+
93+
#### `transfer(from: Address, to: Address, token_id: u64) -> Result<(), Error>`
94+
Transfers `token_id` from `from` to `to`. Requires `from.require_auth()`. Rejected while paused, for soulbound tokens (`SoulboundTokenNotTransferable`), or if `from` isn't the current owner. Emits `transfer`.
95+
96+
#### `approve(owner: Address, spender: Address, token_id: u64) -> Result<(), Error>`
97+
Authorizes `spender` to transfer `token_id` on `owner`'s behalf. Requires `owner.require_auth()`. Rejected while paused or for soulbound tokens. Emits `approve`.
98+
99+
#### `get_approved(token_id: u64) -> Option<Address>`
100+
Currently approved spender for a token, if any.
101+
102+
#### `transfer_from(spender: Address, from: Address, to: Address, token_id: u64) -> Result<(), Error>`
103+
Transfers `token_id` from `from` to `to` on behalf of an approved `spender` (or the owner). Requires `spender.require_auth()`. Rejected while paused or for soulbound tokens. Clears any existing approval. Emits `transfer`.
104+
105+
### Royalties
106+
107+
#### `set_default_royalty_bps(bps: u32) -> Result<(), Error>`
108+
Sets the default royalty rate applied on secondary sales, in basis points (1 BPS = 0.01%; max `10_000` = 100%). **Admin only.** Fails with `InvalidRoyaltyBps` above the max.
109+
110+
#### `get_default_royalty_bps() -> Option<u32>`
111+
Currently configured default royalty, or `None` if never set.
112+
113+
#### `set_default_royalty_asset(asset: Address) -> Result<(), Error>`
114+
Sets the Stellar Asset Contract (SAC) address royalties are paid in by default (e.g. the native XLM SAC or a USDC SAC). The asset must already be on the allow-list via `add_supported_asset`. **Admin only.** Fails with `UnsupportedAsset` otherwise.
115+
116+
#### `get_default_royalty_asset() -> Option<Address>`
117+
Currently configured default royalty asset, if any.
118+
119+
#### `add_supported_asset(asset: Address) -> Result<(), Error>`
120+
Adds an asset contract address to the admin-approved allow-list of assets that may be used for royalty payouts. **Admin only.**
121+
122+
#### `remove_supported_asset(asset: Address) -> Result<(), Error>`
123+
Removes an asset from the royalty allow-list. **Admin only.**
124+
125+
#### `is_supported_asset(asset: Address) -> bool`
126+
Whether `asset` is currently on the allow-list. Read-only.
127+
128+
#### `pay_royalty(payer: Address, token_id: u64, asset: Address, amount: i128) -> Result<i128, Error>`
129+
Pays the royalty owed on a sale of `token_id` to the token's creator, in `asset`. Requires `payer.require_auth()` and a sufficient `asset` balance. `asset` must be on the allow-list (`UnsupportedAsset` otherwise). The transferred amount is `amount * royalty_bps / 10_000`, using the contract's configured default royalty rate. Returns the amount actually transferred. Emits `royalty_paid`.
130+
131+
---
132+
133+
## 4. Events
134+
135+
All events are published via `env.events().publish(topics, data)`.
136+
137+
| Event | Topics | Data | Emitted by |
138+
|---|---|---|---|
139+
| `mint` | `("mint", to: Address)` | `(token_id: u64, is_soulbound: bool)` | `mint` |
140+
| `transfer` | `("transfer", from: Address, to: Address)` | `token_id: u64` | `transfer`, `transfer_from` |
141+
| `approve` | `("approve", owner: Address, spender: Address)` | `token_id: u64` | `approve` |
142+
| `paused` | `("paused", admin: Address)` | `()` | `pause` |
143+
| `unpaused` | `("unpaused", admin: Address)` | `()` | `unpause` |
144+
| `royalty_paid` | `("royalty_paid", payer: Address, recipient: Address)` | `(asset: Address, token_id: u64, amount: i128)` | `pay_royalty` |
145+
146+
---
147+
148+
## 5. Usage Examples
149+
150+
Examples use the [Stellar CLI](https://developers.stellar.org/docs/tools/developer-tools/cli/stellar-cli) (`stellar contract invoke`) against testnet. Replace `$CONTRACT_ID` with the deployed contract address (see `GET /nfts/contract/info` in the backend API).
151+
152+
### Mint a clip as an NFT (admin)
153+
154+
```bash
155+
stellar contract invoke \
156+
--id $CONTRACT_ID \
157+
--source admin \
158+
--network testnet \
159+
-- mint \
160+
--to GC6XOTK6L6LGBKIWH3IRUZPVUY4COGEMW4J5YINOSPKO27YKTUUHTZF3 \
161+
--token_id 42 \
162+
--clip_id "clip-42" \
163+
--content_uri "ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG" \
164+
--is_soulbound false
165+
```
166+
167+
### Pause / unpause the contract (admin, emergency response)
168+
169+
```bash
170+
stellar contract invoke --id $CONTRACT_ID --source admin --network testnet -- pause
171+
stellar contract invoke --id $CONTRACT_ID --source admin --network testnet -- is_paused
172+
stellar contract invoke --id $CONTRACT_ID --source admin --network testnet -- unpause
173+
```
174+
175+
### Configure royalties to pay out in USDC instead of XLM
176+
177+
```bash
178+
# USDC SAC address on testnet (issuer-specific — see Stellar Asset List for the current address)
179+
USDC_SAC=CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA
180+
181+
stellar contract invoke --id $CONTRACT_ID --source admin --network testnet \
182+
-- add_supported_asset --asset $USDC_SAC
183+
184+
stellar contract invoke --id $CONTRACT_ID --source admin --network testnet \
185+
-- set_default_royalty_asset --asset $USDC_SAC
186+
187+
stellar contract invoke --id $CONTRACT_ID --source admin --network testnet \
188+
-- set_default_royalty_bps --bps 1000 # 10%
189+
```
190+
191+
### Pay a royalty on a secondary sale
192+
193+
```bash
194+
stellar contract invoke --id $CONTRACT_ID --source buyer --network testnet \
195+
-- pay_royalty \
196+
--payer GBUYERADDRESS... \
197+
--token_id 42 \
198+
--asset $USDC_SAC \
199+
--amount 5000000 # 500.0000 USDC (7 decimals) sale price
200+
```
201+
202+
### Query ownership and royalty state
203+
204+
```bash
205+
stellar contract invoke --id $CONTRACT_ID --network testnet -- owner_of --token_id 42
206+
stellar contract invoke --id $CONTRACT_ID --network testnet -- get_default_royalty_bps
207+
stellar contract invoke --id $CONTRACT_ID --network testnet -- get_default_royalty_asset
208+
stellar contract invoke --id $CONTRACT_ID --network testnet -- is_supported_asset --asset $USDC_SAC
209+
```

0 commit comments

Comments
 (0)