watchWallet and signed notifications
Signed deposit and transfer lifecycle events with checkpointing.
Status and source boundary
Proposed target SDK behavior for product review. It is not an available SDK, new standalone Streams endpoint or proof that a combined event backend exists. Incoming deposit lifecycle adapts current standalone Streams contracts. Outgoing transfer lifecycle requires a separate proposed transaction-event source; current Streams is not claimed to provide it.
Business action#
Receive signed deposit and outgoing-transfer lifecycle notifications through the same wallet business facade used to send assets. The SDK hides registration, subscription, cursor, bounded reconnect and replay mechanics. It never exposes an endpoint catalog as the product interface.
Proposed calls#
watchWallet(
input: WatchWalletInput,
onEvent: (event: WalletEvent) => Promise<void>,
options?: { signal?: AbortSignal },
): Promise<void>
verifyWebhook(input: {
rawBody: Uint8Array;
signature: string;
timestamp: string;
keyId: string;
version: string;
}): Promise<VerifiedWalletEvent>
TypeScript WatchWalletInput uses walletId, network, durable consumerId, optional assetScope and optional event kinds. verifyWebhook is server-only and validates exact raw bytes before JSON parsing. Its signing algorithm/key-discovery details remain subject to security design and interoperability tests; the contract does not invent a source-verified algorithm.
async def watch_wallet(
input: WatchWalletInput,
on_event: Callable[[WalletEvent], Awaitable[None]],
) -> None: ...
async def verify_webhook(
input: VerifyWebhookInput,
) -> VerifiedWalletEvent: ...
Python uses wallet_id, network, consumer_id, optional asset_scope/event_kinds, and raw request bytes for verification. Normal handler return permits checkpoint advance; exception or cancellation does not.
WatchWallet(
ctx context.Context,
input *WatchWalletInput,
handler func(context.Context, *WalletEvent) error,
) error
VerifyWebhook(
ctx context.Context,
input *VerifyWebhookInput,
) (*VerifiedWalletEvent, error)
Go requires a non-nil handler and RawBody []byte. A nil handler result permits checkpoint advance; error/canceled context does not.
Browser consumers use an authenticated wallet-scoped channel and read permission. Read-only sessions may watch but cannot send. No tenant credential or webhook secret enters browser configuration. If direct browser events carry signatures, validation uses published asymmetric event-authentication keys; confidential verification stays backend-only.
Event contract#
WalletEvent {
eventId, eventKind: incomingDeposit | outgoingTransfer, direction,
tenantId, walletId, network, address, asset, rawAmount,
lifecycleStatus, statusSequence, occurredAt, observedAt,
transferId?, operationId?, requestId?, transactionId?,
correlationId, previousEventId?, reversionReason?,
authentication: { keyId, version, signedAt }
}
eventId is stable for one notification. correlationId groups one economic movement across send-operation and network-observation sources, preventing it from being credited both as an outgoing transfer and an incoming deposit. Direction and event kind are explicit. Exact quantities are canonical unsigned base-unit integers. Transaction hash/signature, when present, is correlation evidence and not finality proof.
Event authentication uses service event-authentication keys, never wallet fund-signing keys. Verification authenticates sender/integrity and binds exact raw envelope bytes, tenant, wallet, event ID, timestamp, key ID and version. It then validates the parsed event against the expected tenant/wallet subscription. An invalid signature, malformed envelope, impossible version, cross-tenant/wallet binding or malicious replay with altered bytes is denied before handler invocation and before checkpoint advance.
Signature freshness and key rotation must not erase legitimate retained-history delivery. The accepted design publishes a verification-key overlap and maximum delivery/replay window at least as long as event retention/recovery. An envelope outside that supported window returns a typed unverifiable-history error and requires authenticated resync; it is not silently accepted or dropped.
A valid duplicate eventId is ordinary at-least-once redelivery, not an attack. If the application already has a committed processed-event receipt, it performs no new business effect and may acknowledge the duplicate. It never skips an earlier unprocessed gap merely because a later duplicate is known.
Signature verification does not prove transaction inclusion, successful execution or finality. Lifecycle status and sequence carry only the source evidence stated by the event. Reorg/finality corrections append new events with correlation to prior history; they never edit or delete the prior notification.
Processing and recovery#
The application handler resolves only after atomically committing its business effect and processed-eventId receipt in the application's own store. Only then may the SDK advance its separate checkpoint. There is no cross-system transaction between application storage and the SDK/source checkpoint. A crash between those commits produces redelivery; the receipt makes it harmless. External effects require application idempotency or a transactional outbox.
Handler failure advances no checkpoint. Canceling the watch stops that consumer and acknowledges no unfinished event. One slow consumer does not block another. Resume starts at the durable consumer checkpoint. If retention cannot cover a gap, return resyncRequired or unverifiableHistory and stop false-success progression; never jump silently to current events.
await sdk.watchWallet(
{ walletId, network, consumerId, eventKinds: ["incomingDeposit", "outgoingTransfer"], signal },
async event => {
await application.commitEffectAndProcessedEvent(event);
},
);
For webhook delivery, retain the exact raw request bytes until verification completes:
const event = await sdk.verifyWebhook({ rawBody, signature, timestamp, keyId, version });
await application.commitEffectAndProcessedEvent(event);
Mode and authorization boundary#
Both embedded and server wallets may emit deposit and transfer events. The event identifies mode-neutral wallet/transaction facts; it grants no authority. Embedded sends require owner-grant/approval. Server-wallet sends require service identity, policy, transaction limit and configured approvers. An authenticated end user never receives treasury authority from an event subscription.
Acceptance#
- Positive: deposits and outgoing transfers arrive with stable event/correlation identity, authentic envelope binding and lifecycle corrections; successful processing advances only that consumer.
- Positive: legitimate duplicate delivery produces no repeated business effect and may be acknowledged after the committed receipt is confirmed.
- Negative: invalid signature, changed raw bytes, stale unsupported envelope, cross-tenant/wallet binding or altered replay reaches no handler and advances no checkpoint.
- Negative: handler failure, crash-window redelivery or slow consumers create neither false success nor a silent gap.
- Negative: verified sender/integrity is never labelled chain execution/finality, and wallet signing keys never authenticate event transport.
See SDK model and business stories, sendTransaction, Transactions, and getOperation.