On-chain infrastructure for GistPin — a location-aware gist platform built on the Stellar / Soroban blockchain.
The contracts handle registering gists as verifiable blockchain records, organizing them by geographic location, and supplying metadata for off-chain indexers.
| Layer | Technology |
|---|---|
| Language | Rust |
| Smart Contract Framework | Soroban SDK |
| Build Tools | cargo, stellar-cli |
| Target | wasm32-unknown-unknown |
| License | MIT |
contracts/
├── src/
│ └── lib.rs # GistRegistry contract
├── Cargo.toml
└── README.md
All contracts live in a single crate for now, with flexibility to split into separate packages as complexity grows.
Each gist record tracks:
| Field | Type | Description |
|---|---|---|
gist_id |
u64 |
Auto-incremented identifier |
author |
Option<Address> |
Optional author address (None = anonymous) |
location_cell |
String |
Coarse geographic cell (e.g. H3 or geohash) |
content_hash |
String |
Content hash pointer (e.g. IPFS CID) |
created_at |
u64 |
Ledger timestamp at creation |
expires_at |
u64 |
Ledger timestamp after which the gist is expired |
| Method | Returns | Description |
|---|---|---|
initialize(admin) |
Result<(), GistError> |
Set the moderator address. Callable once, at deploy time |
get_admin() |
Option<Address> |
The configured moderator, if initialized |
post_gist(author, location_cell, content_hash, ttl_secs) |
Result<u64, GistError> |
Register a new gist; returns its gist_id |
get_gist(gist_id) |
Option<Gist> |
Retrieve a gist record by id (expired/hidden records are still returned) |
is_active(gist_id) |
bool |
Exists, not expired, and not hidden |
list_gists_by_cell(location_cell, cursor, limit) |
Vec<Gist> |
Paginated list of active gists in a cell |
edit_gist(gist_id, new_content_hash) |
Result<(), GistError> |
Replace the content pointer — author only |
delete_gist(gist_id) |
Result<(), GistError> |
Delete your own gist — author only |
hide_gist(gist_id) |
Result<(), GistError> |
Soft-hide a gist — moderator only |
unhide_gist(gist_id) |
Result<(), GistError> |
Reverse a hide — moderator only |
remove_gist(gist_id) |
Result<(), GistError> |
Permanently delete — moderator only |
report_gist(gist_id) |
Result<u32, GistError> |
Flag for off-chain review (anyone); returns the new count |
report_count(gist_id) |
u32 |
Reports filed against a gist |
Posting works without initialize; only moderator actions require it.
post_gist supports both modes:
- Signed (
author = Some(addr)) —addr.require_auth()is enforced, so a gist can never be attributed to an address that did not authorize the call. Signed posts are cooldown-limited. - Anonymous (
author = None) — no authorization required and no cooldown (there is no on-chain identity to rate-limit; anonymous abuse is handled off-chain by the API layer).
| Rule | Value |
|---|---|
Default TTL (when ttl_secs is None) |
24 hours |
| Maximum TTL | 7 days |
| Cooldown per (author, cell) | 60 seconds |
Expired gists are excluded from list_gists_by_cell but remain retrievable via
get_gist; use is_active to test expiry.
| Action | Who | Effect |
|---|---|---|
edit_gist / delete_gist |
the gist's author (require_auth) |
Anonymous gists are immutable — no provable owner |
hide_gist / unhide_gist |
moderator (require_auth) |
Soft, reversible. Excluded from listings; still returned by get_gist with hidden = true, so moderation stays auditable |
remove_gist |
moderator (require_auth) |
Permanent and irreversible — prefer hide_gist unless the content must not persist |
report_gist |
anyone | Advisory only; never hides content on its own |
| Error | Code | Raised when |
|---|---|---|
TtlZero |
1 | ttl_secs is 0 |
TtlTooLong |
2 | ttl_secs exceeds the 7-day maximum |
CooldownActive |
3 | The author posted in this cell < 60s ago |
NotFound |
4 | No gist exists with that id |
NotAuthorized |
5 | Caller is neither the author nor the moderator |
AlreadyInitialized |
6 | initialize was called twice |
NotInitialized |
7 | A moderator action ran before initialize |
AnonymousImmutable |
8 | Edit/delete attempted on an anonymous gist |
Authorization failures surface as a trapped require_auth (a panic), not a
GistError — that is Soroban's standard behaviour.
list_gists_by_cell pages over a secondary index of gist ids per cell, so
cost scales with the results in that cell rather than the total number of
gists. cursor is a zero-based offset into that index (not a gist id).
Every state change publishes an event with a single Symbol topic (the event
name) so off-chain indexers can reconcile. Keep gist_posted in sync with the
backend's GIST_POSTED_EVENT constant.
| Topic | Data payload | Emitted by |
|---|---|---|
gist_posted |
the full Gist record |
post_gist |
gist_edited |
the updated Gist record |
edit_gist |
gist_deleted |
gist_id: u64 |
delete_gist |
gist_hidden |
gist_id: u64 |
hide_gist |
gist_unhidden |
gist_id: u64 |
unhide_gist |
gist_removed |
gist_id: u64 |
remove_gist |
gist_reported |
(gist_id: u64, count: u32) |
report_gist |
The Gist record carries gist_id, author, location_cell, content_hash,
created_at, expires_at, hidden.
Build, deploy and initialize on testnet:
./scripts/deploy-testnet.sh <identity> [admin-address]The script runs the tests, builds the release WASM, deploys, and calls
initialize to set the moderator. Prerequisites (Rust wasm32 target, the
Stellar CLI, and a funded identity) are listed at the top of the script.
| Network | Contract ID | Moderator |
|---|---|---|
| testnet | not yet deployed | — |
Record the contract id here after deploying, then set
CONTRACT_ID_GIST_REGISTRYin the backend environment to take it out of mock mode.
- Set
CONTRACT_ID_GIST_REGISTRYto the deployed id. - Update
soroban.service.ts—post_gistnow takes a 4th argument (ttl_secs: Option<u64>) and returns aResult. -
list_gists_by_cell'scursoris au32offset into the cell index, no longer a gist id. - Decode the
Gistpayload's newexpires_atandhiddenfields. - Subscribe to the moderation/edit events above, not just
gist_posted.
| Contract | Purpose |
|---|---|
| Tipping | Tip mechanisms for gist authors |
| Staking | Stakeholder systems |
| Moderation | On-chain content moderation |
- Rust (≥ 1.70) — install via rustup
wasm32-unknown-unknowntargetstellar-cli— install guide
rustup target add wasm32-unknown-unknowncargo install --locked stellar-cli --features optcargo build --target wasm32-unknown-unknown --releasecargo teststellar contract deploy \
--wasm target/wasm32-unknown-unknown/release/gistpin_contracts.wasm \
--network testnet \
--source <your-identity>- Modifications to contract interfaces require prior discussion via a linked issue and design documentation.
- Public functions should remain compact and well-documented.
- New functionality must be accompanied by test coverage.