Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,138 @@ the next page, and page queries are pinned to one subgraph block snapshot.
It preserves the historical `pageSize` behavior for compatibility, while
`getAnnouncementsPageUsingSubgraph` is the typed cursor-based API.

### Streaming announcements for a user from a subgraph

Use `scanAnnouncementsForUserUsingSubgraph` when you want to fetch one bounded
page at a time, scan it locally for a user, and surface matches incrementally.

```ts
import { scanAnnouncementsForUserUsingSubgraph } from "@scopelift/stealth-address-sdk";

for await (const batch of scanAnnouncementsForUserUsingSubgraph({
subgraphUrl: "https://your-subgraph.example/api",
fromBlock: 12345678,
toBlock: 12349999,
spendingPublicKey: "0xUserSpendingPublicKey",
viewingPrivateKey: "0xUserViewingPrivateKey",
})) {
console.log(batch.announcements);
}
```

Batches are sorted newest to oldest within each scanned page. The overall
stream follows subgraph cursor order, so consumers that need a single global
ordering should re-sort after accumulation.

Each yielded batch includes:

- `announcements`: the matches found in that scanned page
- `scannedCount`: the number of candidate announcements scanned in that page
- `nextCursor`: the resume token for the next older page, when another page exists
- `snapshotBlock`: the fixed subgraph snapshot block for the full bounded scan

Persist `nextCursor` and `snapshotBlock` from any yielded batch if you want to
resume the scan later from the next older page.

If you use `includeList` or `excludeList`, provide `clientParams` or call the
helper through `createStealthClient(...)` so the SDK can resolve transaction
senders for those filters.

Historical scan work is pinned to one `snapshotBlock`. Complete the historical
scan against that fixed snapshot before you start live watching.

### Watching live announcements for a user

Use `watchAnnouncementsForUser` when you want to process live announcement
batches after historical catch-up is complete.

```ts
import {
ERC5564_CONTRACT_ADDRESS,
VALID_SCHEME_ID,
createStealthClient,
} from "@scopelift/stealth-address-sdk";

const stealthClient = createStealthClient({
chainId: 11155111,
rpcUrl: process.env.RPC_URL!,
});

const snapshotBlock = 12349999n;

const unwatch = await stealthClient.watchAnnouncementsForUser({
ERC5564Address: ERC5564_CONTRACT_ADDRESS,
args: {
schemeId: BigInt(VALID_SCHEME_ID.SCHEME_ID_1),
caller: "0xYourCallingContractAddress",
},
fromBlock: snapshotBlock + 1n,
spendingPublicKey: "0xUserSpendingPublicKey",
viewingPrivateKey: "0xUserViewingPrivateKey",
handleLogsForUser: async (logs, meta) => {
console.log("live matches", logs, meta);
},
onHeartbeat: meta => {
console.log("watch heartbeat", meta.observedBlock.toString());
},
onError: error => {
console.error("watch handler failed", error);
},
});
```

`handleLogsForUser` receives a second `meta` argument with:

- `fromBlock`
- `observedBlock`
- `pollTimestamp`
- `rawLogCount`
- `relevantLogCount`

If you also provide `onHeartbeat`, the SDK calls it whenever the watcher
observes a chain head while polling. That lets the app distinguish "watch is
alive but there were no matching logs" from "watch appears stalled" without
adding a separate head-polling loop.

The SDK ignores `handleLogsForUser`'s return value, but it awaits any returned
promise before considering that batch processed. Watched batches are processed
one at a time in arrival order. If one batch takes a long time to finish, later
batches wait behind it.

If watched batch processing fails:

- the SDK reports that failure once through `onError` when provided
- if `onError` is omitted, the SDK logs the error to `console.error`
- the watcher stays alive for later batches
- the failed batch is not replayed automatically
- if `onError` also throws, that secondary failure is swallowed so the watcher
can continue
- if both `onError` and fallback logging fail, that reporting failure is
swallowed as a last resort so the watcher can continue processing later
batches

Calling `unwatch()` stops future watched batches, but it does not cancel a batch
that is already in flight.

### Recommended scan -> watch handoff

The clean no-gap/no-dup handoff is:

1. Fetch historical pages with `getAnnouncementsPageUsingSubgraph(...)` or
stream them with `scanAnnouncementsForUserUsingSubgraph(...)`.
2. Reuse the first page's `snapshotBlock` for every later historical page in
that catch-up sequence.
3. Persist and dedupe the historical matches locally.
4. Only after historical catch-up completes, start
`watchAnnouncementsForUser(...)` at `fromBlock: snapshotBlock + 1n`.

That boundary keeps historical reads pinned to one frozen subgraph view and
starts live watching strictly after that snapshot, so the normal path avoids
gaps and avoids duplicate delivery.

See the runnable React/TanStack composition example in
`examples/composeAnnouncementHistoryAndWatch`.

## License

[MIT](/LICENSE) License
Binary file modified bun.lockb
Binary file not shown.
9 changes: 9 additions & 0 deletions examples/composeAnnouncementHistoryAndWatch/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
VITE_CHAIN_ID='11155111'
VITE_RPC_URL='https://your-rpc.example'
VITE_SUBGRAPH_URL='https://your-subgraph.example/api'
VITE_SPENDING_PUBLIC_KEY='0xYourSpendingPublicKey'
VITE_VIEWING_PRIVATE_KEY='0xYourViewingPrivateKey'
VITE_CALLER='0xYourCallingContractAddress'
VITE_FROM_BLOCK='12345678'
VITE_PAGE_SIZE='100'
VITE_POLLING_INTERVAL='1000'
18 changes: 18 additions & 0 deletions examples/composeAnnouncementHistoryAndWatch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Compose Announcement History And Watch Example

This example shows the recommended `scan -> watch` composition for a user inbox:

- keep a merged inbox in memory for the current page session
- catch up paged history with `getAnnouncementsPageUsingSubgraph(...)`
- filter each page locally with `getAnnouncementsForUser(...)`
- only start `watchAnnouncementsForUser(...)` after catch-up completes
- start live watching from `snapshotBlock + 1n`
- use watch heartbeat metadata to show the latest observed block without a
separate head poll

Run it with:

```bash
bun install
bun run dev
```
Loading
Loading