The tagging feature allows users to organize and categorize their savings goals, bills, and insurance policies using custom string labels (tags). This enables better grouping, filtering, and analytics across all financial entities.
Tags have been added to the following structs:
-
Bill (
bill_payments/src/lib.rs)- Added
tags: Vec<String>field - Tags are preserved when bills are archived
- Tags are copied to recurring bills
- Added
-
ArchivedBill (
bill_payments/src/lib.rs)- Added
tags: Vec<String>field - Tags are preserved from the original bill
- Added
-
SavingsGoal (
savings_goals/src/lib.rs)- Added
tags: Vec<String>field
- Added
-
InsurancePolicy (
insurance/src/lib.rs)- Added
tags: Vec<String>field
- Added
Each contract provides owner-only functions to manage tags:
pub fn add_tags_to_bill(
env: Env,
caller: Address,
bill_id: u32,
tags: Vec<String>,
) -> Result<(), Error>
pub fn remove_tags_from_bill(
env: Env,
caller: Address,
bill_id: u32,
tags: Vec<String>,
) -> Result<(), Error>pub fn add_tags_to_goal(
env: Env,
caller: Address,
goal_id: u32,
tags: Vec<String>,
)
pub fn remove_tags_from_goal(
env: Env,
caller: Address,
goal_id: u32,
tags: Vec<String>,
)pub fn add_tags_to_policy(
env: Env,
caller: Address,
policy_id: u32,
tags: Vec<String>,
)
pub fn remove_tags_from_policy(
env: Env,
caller: Address,
policy_id: u32,
tags: Vec<String>,
)All contracts enforce consistent validation and normalization rules to ensure safe indexing and predictable off-chain search behavior:
- Tags cannot be empty (at least one tag must be provided)
- Each tag must be between 1 and 32 characters in length
- Allowed character set:
[a-z0-9-_](lowercase letters, digits, hyphens, underscores) - Uppercase letters are automatically normalized to lowercase
- Duplicate tags are allowed
Tags are normalized before storage using the following rules:
- Case normalization: All uppercase letters (A-Z) are converted to lowercase (a-z)
- Character validation: Only alphanumeric characters, hyphens (-), and underscores (_) are allowed
- Length validation: Tags must be 1-32 characters after normalization
- Rejection: Tags with invalid characters (e.g., @, !, #, spaces) are rejected with an error
Tag validation is centralized in remitwise-common:
canonicalize_tags_checked(env, tags) -> Result<Vec<String>, TagError>— preferred for untrusted or indexer-supplied input. Returns typed errors (Empty,TooLong,InvalidChar { position }) and short-circuits on the first violation.canonicalize_tags(env, tags, on_invalid_char)— legacy panic-based wrapper retained for backward compatibility. New contract code should prefer the checked variant and mapTagErrorto each crate's contract errors (e.g.SavingsGoalError::InvalidTagContent).
TAG_MAX_LEN (32) in remitwise-common is the single source of truth for maximum tag length.
"URGENT-1_Tag"→"urgent-1_tag"(normalized)"Monthly-Bill"→"monthly-bill"(normalized)"invalid@tag!"→ rejected (invalid characters)""→ rejected (empty)"a" * 33→ rejected (too long)
Tag operations emit events for tracking and analytics:
tags_add: Emitted when tags are added to a bill- Data:
(bill_id, owner, tags)
- Data:
tags_rem: Emitted when tags are removed from a bill- Data:
(bill_id, owner, tags)
- Data:
tags_add: Emitted when tags are added to a goal- Data:
(goal_id, owner, tags)
- Data:
tags_rem: Emitted when tags are removed from a goal- Data:
(goal_id, owner, tags)
- Data:
tags_add: Emitted when tags are added to a policy- Data:
(policy_id, owner, tags)
- Data:
tags_rem: Emitted when tags are removed from a policy- Data:
(policy_id, owner, tags)
- Data:
- Only the owner of an entity (bill, goal, or policy) can add or remove tags
- All tag operations require authentication via
caller.require_auth() - Unauthorized attempts will result in a panic or error
- Tags are stored as part of the entity struct
- Tags are included in all query results (paginated and single-entity queries)
- Tags persist across entity lifecycle (e.g., when bills are archived)
- Tags are copied to recurring bills when a bill is paid
let tags = vec![
String::from_str(&env, "utilities"),
String::from_str(&env, "monthly"),
String::from_str(&env, "high-priority")
];
client.add_tags_to_bill(&owner, &bill_id, &tags);let tags_to_remove = vec![
String::from_str(&env, "old-tag")
];
client.remove_tags_from_goal(&owner, &goal_id, &tags_to_remove);let tags = vec![
String::from_str(&env, "health"),
String::from_str(&env, "family")
];
client.add_tags_to_policy(&owner, &policy_id, &tags);Tags are automatically included in all paginated query results:
get_unpaid_billsget_all_bills_for_ownerget_archived_billsget_active_policiesget_goals
When bills are archived, their tags are preserved in the ArchivedBill struct.
When a recurring bill is paid and a new bill is created, the tags are copied to the new bill.
Error::EmptyTags(14): Panics when trying to add/remove an empty tag listError::InvalidTag(13): Panics when a tag is invalid (empty or > 32 characters)Error::InvalidTagContent(19): Panics when a tag contains invalid characters (not in [a-z0-9-_])Error::Unauthorized(5): Panics when a non-owner tries to modify tagsError::BillNotFound(1): Panics when the bill doesn't exist
These contracts use panics for error handling:
- "Tags cannot be empty": When trying to add/remove an empty tag list
- "Tag must be between 1 and 32 characters": When a tag is invalid
SavingsGoalError::InvalidTagContent: When a tag contains invalid characters- "Only the [entity] owner can [add/remove] tags": When unauthorized
- "[Entity] not found": When the entity doesn't exist
Potential improvements for the tagging system:
- Tag-based Filtering: Add query functions to filter entities by tags
- Tag Analytics: Aggregate statistics by tag (e.g., total amount by tag)
- Tag Suggestions: Auto-suggest tags based on entity names or patterns
- Tag Limits: Enforce maximum number of tags per entity
- Predefined Tags: Support for system-defined tag categories
- Tag Search Index: On-chain search index for tag-based queries