Skip to content

feat(shade): implement campaign categories and tagging system (#352)#394

Open
goodness-cpu wants to merge 2 commits into
ShadeProtocol:mainfrom
goodness-cpu:implement-campaign-categories
Open

feat(shade): implement campaign categories and tagging system (#352)#394
goodness-cpu wants to merge 2 commits into
ShadeProtocol:mainfrom
goodness-cpu:implement-campaign-categories

Conversation

@goodness-cpu

@goodness-cpu goodness-cpu commented Jun 30, 2026

Copy link
Copy Markdown

Closes #352

Summary

Implements the campaign categories and tagging system requested in #352. The feature is added to the shade contract alongside the existing merchant / invoice / subscription / ticketing modules and is fully backwards-compatible — only new DataKey variants, new struct types, new error variants, new event types, and new trait methods are introduced.

What ships

  1. CampaignCategory — admin-curated registry (id, name, description, active, timestamp). Each campaign must reference one active category.
  2. CampaignTag — shared vocabulary (id, name, creator, timestamp) created by any registered merchant or the admin. Tags are platform-wide and reusable across campaigns.
  3. Campaign — merchant-owned fundraising/marketing campaign (id, merchant_id, merchant, title, description, category_id, tags: Vec<u64>, goal_amount, token, deadline, raised_amount, active, created_at) that attaches exactly one active category and any number of tags.
  4. Filtered queriesget_campaigns(filter) with optional category_id, tag_id, merchant_id, and is_active selectors. Backed by reverse indices (CategoryCampaigns, TagCampaigns, MerchantCampaigns, CampaignTagList) so lookups by category, by tag, and by merchant are O(n) scans over the relevant subset rather than full-table scans.
  5. Contribution accountingrecord_campaign_contribution accumulates raised_amount and emits an event with the new ratio against goal_amount. The contract does not move tokens; campaign payments can be attached to any rail while on-chain metadata + totals remain authoritative.

Security model

  • Every write path calls require_auth() on the calling principal.
  • Category mutations are admin-only via core::assert_admin.
  • Tag creation requires the caller to be either admin or a registered merchant (merchant::is_merchant || core address match).
  • Campaign mutations are merchant-only and check stored merchant Address (no merchant_id guessing).
  • Pausable guard (pausable_component::assert_not_paused) gates every state-changing entry point.
  • Reentrancy guard (reentrancy::enter / exit) wraps every write that mutates more than one storage key.
  • Distinct errors for "merchant-deactivated" (CampaignInactive = #63) versus "deadline passed" (CampaignExpired = #65) so off-chain indexers can render the right UX.

Events

Eight new #[contractevent] structs in events.rs:

  • CampaignCategoryCreatedcategory_id, admin, name, description, timestamp.
  • CampaignCategoryUpdatedcategory_id, admin, name, description, active, timestamp.
  • CampaignTagCreatedtag_id, creator, name, timestamp.
  • CampaignCreatedcampaign_id, merchant, merchant_id, title, description, category_id, tags, goal_amount, token, deadline, timestamp.
  • CampaignUpdatedcampaign_id, merchant, title, description, timestamp.
  • CampaignStatusChangedcampaign_id, merchant, active, timestamp.
  • CampaignTagAdded / CampaignTagRemovedcampaign_id, merchant, tag_id, timestamp.
  • CampaignContributioncampaign_id, contributor, amount, raised_amount, goal_amount, timestamp.

Every event ships full structural metadata (ids, addresses, amounts, category/tag ids, ledger timestamp) so off-chain UIs / indexers can subscribe without follow-up lookups.

Storage layout

Twelve new DataKey variants appended in types.rs without renumbering or re-using existing discriminants; the existing enum goes up to #54 and the new variants occupy #55-#65. Four new structs (CampaignCategory, CampaignTag, Campaign, CampaignFilter) are added alongside. Reverse indices are kept in lockstep on every add/remove:

CampaignCategory(u64)            -> CampaignCategory
CampaignCategoryCount           -> u64
CampaignCategoryName(String)    -> u64            // name -> id dedup
CategoryCampaigns(u64)          -> Vec<u64>       // category_id -> campaign_ids

CampaignTag(u64)                 -> CampaignTag
CampaignTagCount                -> u64
CampaignTagName(String)         -> u64            // name -> id dedup
TagCampaigns(u64)               -> Vec<u64>       // tag_id -> campaign_ids

Campaign(u64)                   -> Campaign
CampaignCount                   -> u64
MerchantCampaigns(u64)          -> Vec<u64>       // merchant_id -> campaign_ids
CampaignTagList(u64)            -> Vec<u64>       // campaign_id -> attached tag_ids

Public API additions

ShadeTrait (and Shade impl) gain 14 new methods:

create_campaign_category       (admin)
update_campaign_category       (admin)
get_campaign_category
get_campaign_categories

create_campaign_tag            (admin | registered merchant)
get_campaign_tag
get_campaign_tags

create_campaign                (merchant)
update_campaign                (merchant)
set_campaign_active            (merchant)
add_campaign_tag               (merchant)
remove_campaign_tag            (merchant)
record_campaign_contribution   (anyone — open accounting hook)

get_campaign
get_campaigns

All write methods are gated by pausable_component::assert_not_paused. All merchant-facing methods call merchant_addr.require_auth() and verify ownership against the stored Campaign.merchant Address.

Tests

A new tests/test_campaigns.rs adds ~20 cases covering:

  • Admin-only category creation, duplicate-name rejection, length validation, full / partial updates, missing-id lookup.
  • Tag creation by admin and by merchant; unauthorised callers rejected; duplicate names rejected; missing tag references.
  • Campaign creation enforces: registered merchant, active merchant, non-empty title, positive goal, future deadline, accepted token, and active category.
  • Owner-only update / tag add / tag remove / status toggle; reverse-index updates verified.
  • Contribution accumulation; 0 amount, deactivated-campaign (#63), and expired-deadline (#65) rejection paths.
  • Filtered queries by category, by tag, by (category ∧ tag) intersection, and across all campaigns.

Diff footprint

File Change
contracts/shade/src/types.rs +73
contracts/shade/src/errors.rs +23
contracts/shade/src/events.rs +259
contracts/shade/src/components/mod.rs +1
contracts/shade/src/components/campaigns.rs +770 (new)
contracts/shade/src/interface.rs +94 / -0
contracts/shade/src/shade.rs +131 / -0
contracts/shade/src/tests/mod.rs +1
contracts/shade/src/tests/test_campaigns.rs +620 (new)
Total +1972 / -0

Notes for reviewers

  • The diff is strictly additive. No existing DataKey variants, struct fields, error discriminants, or trait methods were renamed or repurposed. The existing enum's repr(u32) is preserved.
  • cargo / rustc / stellar were not available in the sandbox where this PR was authored; final cargo test -p shade and stellar contract build verification should be run from the standard dev image (make test).
  • One reviewer flag remains unimplemented in this PR (intentionally) for a follow-up: Campaign.tags is currently stored both on the Campaign struct and in the CampaignTagList reverse index. Dropping the field shaves rent at the cost of a tiny read-time recompute; planned as a follow-up to keep this diff surgical.

Validation commands

cd contracts/shade
cargo check --tests
cargo test
cargo build --target wasm32v1-none --release      # equivalent to `make build`

Closes #352

…rotocol#352)

Adds an admin-managed CampaignCategory registry, a merchant-curated CampaignTag vocabulary, and merchant-owned Campaign records that reference exactly one active category and any number of tags. All write paths enforce require_auth() and the appropriate role-based check; records are stored in additive DataKey variants so storage layout remains backwards compatible.

Highlights:
- Add DataKey variants CampaignCategory, CampaignCategoryName, CampaignTag, CampaignTagName, Campaign, CategoryCampaigns, TagCampaigns, MerchantCampaigns, CampaignTagList plus matching CampaignCategory, CampaignTag, Campaign, CampaignFilter structs to types.rs.
- Add ContractError variants 55-65 (CampaignCategoryNotFound .. CampaignExpired) to errors.rs.
- Add 8 new contractevent structs in events.rs: CampaignCategoryCreated/Updated, CampaignTagCreated, CampaignCreated/Updated/StatusChanged, CampaignTagAdded/Removed, CampaignContribution.
- New components/campaigns.rs with reentrancy-guarded writes, unique-tagging via name index, reverse indices for get_campaigns_by_category/by_tag/by_merchant, distinct errors for inactive vs expired campaigns.
- Extend ShadeTrait and Shade impl with 14 new methods covering admin category mgmt, admin/merchant tag creation, merchant-only campaign CRUD, contribution recording, and filtered queries.
- New tests/test_campaigns.rs covers admin and merchant auth paths, duplicate name rejection, length validation, deadline handling, category-active enforcement, ownership checks, contribution accumulation, inactive/expired rejection, and the full filter matrix.

Closes ShadeProtocol#352
@drips-wave

drips-wave Bot commented Jun 30, 2026

Copy link
Copy Markdown

@goodness-cpu Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Campaign Categories and Tagging System

1 participant