The canonical blockchain→app event contract for the DIG Network.
dig-events-protocol is the ONE ecosystem definition of the wallet/chain event taxonomy that the DIG
node/wallet engine EMITS and apps (dig-app) SUBSCRIBE to. It owns only the CONTRACT — the wire types
and the two shape traits — so a second implementation matches the first byte-for-byte. The machinery
that produces, persists, and streams events (the event bus, the catch-up delta store, live
subscriptions) stays in the engine (dig-wallet-backend).
WalletEvent— the event enum the engine emits (11 variants, serdetag = "type"snake_case), withkind()andmatches()for subscription filtering.EventKind— the kind discriminant; anEnumSet<EventKind>is the subscription FILTER (serialized as a snake_case list).Cursor+EmittedEvent— the monotonic delivery cursor and the envelope that flows over the live stream and is returned by catch-up.SyncLifecycle/SyncStatus— the tri-state sync snapshot.WalletId/Amount/AssetId— the payload newtypes.EventEmitter+CatchUp+filter_events— the emit/backfill shape traits and the shared filtering rule.
Serialized as JSON with #[serde(tag = "type", rename_all = "snake_case")] — type discriminant is the variant name in snake_case.
| Variant | Wire Type | Fields | Summary |
|---|---|---|---|
FundsReceived |
funds_received |
wallet_id: WalletId, asset: Option<AssetId>, amount: Amount, coin_id: String, confirmed_height: u32 |
Inbound value landed for wallet |
FundsSent |
funds_sent |
wallet_id: WalletId, asset: Option<AssetId>, amount: Amount, tx_id: String, confirmed_height: u32 |
Outbound value confirmed for wallet |
CoinStateChanged |
coin_state_changed |
coin_id: String, spent: bool, created_height: Option<u32>, spent_height: Option<u32> |
Tracked coin's spent/created state changed |
Confirmation |
confirmation |
tx_id: String, height: u32 |
Submitted transaction confirmed on-chain |
TransactionFailed |
transaction_failed |
tx_id: String, error: String |
Submitted transaction failed (rejected/never confirmed) |
NewTip |
new_tip |
height: u32, header_hash: String |
New chain tip observed |
SyncProgress |
sync_progress |
wallet_id: WalletId, state: SyncLifecycle, peak_height: u32, target_height: u32 |
Sync progress advanced for wallet |
CatInfo |
cat_info |
asset_id: AssetId, name: Option<String> |
CAT metadata became available |
DidInfo |
did_info |
launcher_id: String |
DID metadata became available |
NftData |
nft_data |
launcher_id: String |
NFT data became available |
Derivation |
derivation |
wallet_id: WalletId, index: u32 |
New HD receive address became active |
EnumSet<EventKind> is the subscription filter; serializes as a snake_case list on the wire (e.g., ["funds_received","funds_sent"]):
pub enum EventKind {
FundsReceived, // inbound value
FundsSent, // outbound value confirmed
CoinStateChanged, // coin state (spent/created) changed
Confirmation, // transaction confirmed
TransactionFailed, // transaction failed
NewTip, // new chain tip
SyncProgress, // sync progress advanced
CatInfo, // CAT metadata resolved
DidInfo, // DID metadata resolved
NftData, // NFT metadata resolved
Derivation, // new HD derivation active
}Usage: event.kind() returns the event's EventKind; event.matches(filter) checks if it passes a filter.
/// Monotonic per-instance sequence number stamped on delivered events.
pub struct Cursor(pub u64);
/// The envelope that flows over live stream AND catch-up backfill.
pub struct EmittedEvent {
pub cursor: Cursor, // monotonic sequence (u64 on wire)
pub event: WalletEvent, // the payload
}Subscription Model:
- Live stream: Engine emits
EmittedEvents in real-time; subscriber remembers the lastCursor. - On gap (disconnect/lag): Subscriber calls
catch_up(since: Cursor, filter: EventKind)ONCE to backfill missed events. - Resume live: After backfill, resume the live stream from the last backfill cursor.
/// Tri-state sync snapshot (wire: "idle" / "syncing" / "synced").
pub enum SyncLifecycle {
Idle, // not started / no peer
Syncing, // actively catching up
Synced, // caught up and tracking live
}
pub struct SyncStatus {
pub state: SyncLifecycle, // current state
pub peak_height: u32, // height processed to
pub target_height: u32, // chain tip being synced toward
}Delivered via WalletEvent::SyncProgress.
Three thin newtypes so the wire is a bare value, not a tagged object:
pub struct WalletId(pub u32); // wallet identifier (wire: bare u32)
pub struct Amount(pub u64); // on-chain value in mojos (wire: ALWAYS a decimal string; JS = BigInt)
pub struct AssetId(pub String); // CAT tail hash, hex (wire: bare string)Asset field semantics:
Some(AssetId(…))= CAT (colored coin)None= native XCH
EventEmitter — what the engine implements; producers call publish():
pub trait EventEmitter {
fn publish(&self, event: WalletEvent) -> Cursor;
}CatchUp — what a subscriber calls once to backfill:
pub trait CatchUp {
type Error;
async fn catch_up(
&self,
since: Cursor,
filter: EnumSet<EventKind>,
) -> Result<Vec<EmittedEvent>, Self::Error>;
}Returns every EmittedEvent with cursor strictly > since, optionally narrowed to filter, in cursor order.
Shared, drift-free rule applied on BOTH live and catch-up:
pub fn filter_events(
events: impl IntoIterator<Item = EmittedEvent>,
filter: EnumSet<EventKind>,
) -> Vec<EmittedEvent>Retains only events whose kind() is in the filter, preserving cursor order. Ensures live and catch-up deliver identical filtered views.
- Serialization: JSON via
serde - Event tagging:
#[serde(tag = "type", rename_all = "snake_case")] - Kind filter list:
["event_kind_one","event_kind_two"](snake_case, sorted) - Cursor: Bare
u64on wire, monotonic increasing per instance - Amounts:
u64in mojos (smallest indivisible unit); serialized ALWAYS as a decimal string (every value, small or large) so a JS/TS consumer reads it as onebigint(BigInt(str)) — one code path, notypeofbranch, and no precision loss pastNumber.MAX_SAFE_INTEGER. Deserialization accepts the canonical decimal string and, leniently, a bare JSON number. - Timestamps: None on the wire itself (events carry heights, not absolute times; time mapping is app-layer)
use dig_events_protocol::{EnumSet, EventKind, WalletEvent, WalletId, Amount};
// Build a subscription filter — only funds movement.
let filter = EventKind::FundsReceived | EventKind::FundsSent;
let event = WalletEvent::FundsReceived {
wallet_id: WalletId(1),
asset: None, // native XCH
amount: Amount(5_000),
coin_id: "abcd".into(),
confirmed_height: 100,
};
assert!(event.matches(filter));Implement EventEmitter on your event bus and CatchUp on your delta store to produce a conformant
event source; consumers depend only on this crate.
[dependencies]
dig-events-protocol = "0.1"A pure leaf crate: serde + enumset + async-trait only. No dig-* dependency, no runtime.
The wire format is frozen by golden-JSON conformance KATs (tests/conformance.rs): every event
variant, the envelope, and the kind list round-trip against byte-stable fixtures. A change that alters
the wire shape breaks a KAT.
See SPEC.md for the normative contract.
Apache-2.0 OR MIT.