Skip to content

Add SIP-030 discovery and listeners to the Sats Connect Wallet API - #263

Draft
aryzing wants to merge 1 commit into
developfrom
feat/stx-sip-listeners
Draft

aryzing wants to merge 1 commit into
developfrom
feat/stx-sip-listeners

Conversation

@aryzing

@aryzing aryzing commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Package pre-release info

Built at (UTC): 2026-10-03T18:16:12.120Z

npm install sats-connect@4.3.0-0a6bd22 -E
bun install sats-connect@4.3.0-0a6bd22 -E

Summary

Companion to Core/SDK #131, extension #2265, mobile #3007, and Stacks Connect #515.

Xverse's injected provider exposes SIP-030 listen directly: neither Stacks Connect nor Sats Connect is required to call the native wallet API. This PR closes the top-level Sats Connect integration gap.

  • Pin the verified published @sats-connect/core@0.19.0-d1718be prerelease in the manifest and lockfile. This exposes typed stx_getNetworks, the two SIP event contracts, and the named listen export through the existing Core re-export.
  • Add synchronous Wallet.listen(event, callback) for stx_networkChange and stx_accountChange to the default Wallet API. Honor the instance's selected provider (or adopt its saved default) without selection, approval or unlock prompts.
  • Forward to a single adapter instance when it supports listen; otherwise use the injected provider's native listener through Core. Preserve callback receiver, bare native payloads and the exact unlisten function.
  • Fail explicitly for missing selection/unsupported native listeners. Never synthesize account data from legacy events or call stx_getAccounts automatically.
  • Leave existing Wallet.addListener, positional/object calling conventions, and RPC response payloads unchanged.
  • Document named/default/direct-provider usage and Xverse's deliberate deprecated Gaia placeholders: all-zero hex key and https://gaia.invalid, for software and hardware accounts. Sats Connect does not replace another wallet's Gaia fields.
  • Run runtime and compile-time regression coverage in PR CI.

Validation

  • Clean npm ci --ignore-scripts --no-audit --no-fund against published dependencies.
  • npm run check-types: source and positive/negative API type fixtures.
  • npm run test:listeners: build/type declarations and 9 passing public-artifact tests.
  • Prettier checks on implementation, manifests, new documentation and test files; git diff --check.
  • Confirmed the runtime regressions fail against the pre-change implementation.

Tests cover saved/instance provider routing, both event payloads, independent cleanup and receiver forwarding, adapters and native fallback, unsupported providers, no implicit UI/RPC calls, named exports and discovery, and unchanged legacy callbacks.

Rollout

Draft pending stable dependency rollout: temporarily use SDK 0.19.0-d1718be, then replace its manifest/lockfile pin with stable 0.19.0 once available before a production release. No local tarball paths or guessed published versions.

Keep the already-planned, unpublished sats-connect release version 4.3.0; no existing stable version is overwritten.

@netlify

netlify Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for sats-connect canceled.

Name Link
🔨 Latest commit 58a9357
🔍 Latest deploy log https://app.netlify.com/projects/sats-connect/deploys/6ac14638b48eac0008801616

@victorkirov victorkirov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review (round 1): two blocking issues. @sats-connect/core is pinned to an unmerged prerelease with no release guard, and Wallet.listen throws on every wallet that doesn't support SIP-030 yet while the documented examples don't handle it. Suggestions are inline.

Comment thread src/index.ts
public listen: Listen = (event, callback) => {
const providerId = this.providerId ?? getDefaultProvider();
if (!providerId) {
throw new Error('Select a wallet provider before registering SIP-030 listeners.');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Wallet.listen throws on wallets without SIP-030 support, and the documented examples don't handle it

Wallet.listen throws synchronously when no provider is selected, and Core throws when the provider has no native listen. That covers every Xverse version shipped before this rollout, plus the Unisat and Fordefi adapters. The sibling addListener deliberately logs and returns a no-op so that apps don't crash when sats-connect is ahead of the installed wallet (see its comment). The SIP030_LISTENERS.md examples and the README call Wallet.listen with no try/catch, so an app that registers on mount (in a React effect or at page init) will throw for first-time visitors and for users on older wallets. Smallest fix: either match addListener (console.error and return () => {}), or keep the throw, document it, and show try/catch or a capability check in the examples.

Comment thread src/index.ts
}
this.providerId = providerId;

const Adapter = this.defaultAdapters[providerId];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Adapter listen dispatch does the same thing as the Core fallback

The only in-tree adapter that has listen is XverseAdapter, and its implementation is (e, cb) => listen(e, cb, this.id), which is exactly what line 129 already does. Fordefi and Unisat fall through, and defaultAdapters is private with no setter, so consumers can't plug in another implementation. The branch adds an adapter construction per call, and its tests have to mutate the private field to reach it (tests/listeners.test.mjs:146,166). Consider dropping lines 122-125 and calling listenProvider directly. (It does mirror the adapter-dispatch shape of request/addListener, hence a suggestion only.)

Comment thread SIP030_LISTENERS.md

Xverse suppresses account events while locked without triggering unlock/approval prompts. Connected origins without read permission for the selected account receive `[]`; missing Stacks address/public key also yields `[]`. This does not change existing explicit account requests or legacy events.

## Dependency rollout

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] PR-process notes committed as permanent docs

The "Dependency rollout" and "Validation" sections and the "companion to sats-connect-core#131" opener describe this PR rather than the library, and they go stale as soon as Core 0.19.0 ships. The README links to this file as the usage doc. Consider moving those sections to the PR description and keeping only the usage, provider-selection and Gaia-policy content.

Comment thread tests/listeners.test.mjs
assert.deepEqual(calls, ['stx_networkChange', 'stx_accountChange', 'stx_networkChange']);
});

test('delegates to an adapter on one instance and retains its native receiver', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Tests cover an unreachable adapter configuration and miss the real non-Xverse adapters

The adapter-delegation and withAdapter tests inject custom defaultAdapters, which production code can't do. No test calls Wallet.listen with a real defaultAdapters entry that lacks listen (for example unisat or a Fordefi provider), and that is the path most non-Xverse users take. Consider replacing the private-field cases with a setDefaultProvider('unisat') case that asserts the chosen failure behaviour.

@victorkirov

Copy link
Copy Markdown
Member

[agent] Decisions this PR makes that I reviewed and agree with, at 58a9357. This is not the approval of the PR; that follows separately.

New entry point

  • Adds Wallet.listen as a second public event API next to the legacy Wallet.addListener. It is typed with Core's Listen/ListenEventMap, accepts only stx_networkChange and stx_accountChange, and passes the wallet's native payloads (a bare accounts array, a networks object) and its unlisten function through unchanged. (not taken: Extending addListener with the SIP-030 event names; wrapping the SIP payloads in the legacy {type, ...} envelope)

Assumption

  • Forwards each wallet's stx_accountChange payload unchanged and documents, in SIP030_LISTENERS.md, that Xverse sends real addresses and public keys but placeholder Gaia values (an all-zero gaiaAppKey and https://gaia.invalid). Sats Connect does not fill in, strip or flag Gaia fields for any wallet. (not taken: Normalising the event in the SDK, for example removing Gaia fields or marking placeholders; documenting no Gaia policy and leaving it to each wallet)

Failure handling

  • Makes Wallet.listen fail synchronously when no provider is known, instead of prompting. It works as follows: it uses the instance's selected provider, or falls back to the saved default and pins that default onto the instance. If neither exists it throws 'Select a wallet provider…'. It never opens the selector, approval or unlock UI, and it never polls stx_getAccounts or translates legacy events when the wallet lacks native listen. (not taken: Opening the wallet selector the way Wallet.request does; returning a no-op unsubscribe; synthesizing events from legacy addListener or stx_getAccounts)

Consistency

  • Adds a second test setup: a node:test suite (tests/listeners.test.mjs) that runs against the built dist with hand-made window/localStorage/document globals, plus a compile-only type fixture. The PR beta-publish workflow now requires check-types and test:listeners to pass, while the existing jest config stays unused in CI. (not taken: Writing the tests in the repo's existing jest/ts-jest setup and enabling npm test in CI)
  • Routes listen past the adapter layer when the adapter lacks listen. It works as follows: if the selected provider's adapter defines listen, that is used; otherwise Core's listen is called directly on the injected window provider for that id. Wallet.request, by contrast, always goes through an adapter or BaseAdapter. (not taken: Requiring every adapter to implement listen and throwing when it doesn't; routing through BaseAdapter the same way request does)

@victorkirov

Copy link
Copy Markdown
Member

[agent] Update to the decisions I reviewed, at 58a9357: only what changed since my previous decisions comment. Everything else there still stands. This is not the approval of the PR.

Now agreed

Consistency

  • Adds a second test setup: a node:test suite (tests/listeners.test.mjs) that runs against the built dist with hand-made window/localStorage/document globals, plus a compile-only type fixture. The PR beta-publish workflow now requires check-types and test:listeners to pass, while the existing jest config stays unused in CI. (not taken: Writing the tests in the repo's existing jest/ts-jest setup and enabling npm test in CI) — new

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.

2 participants