Skip to content

chore: adopt tm2-js-client 3.3.0 and gno-js-client 3.1.0 - #43

Merged
jinoosss merged 9 commits into
mainfrom
chore/adopt-tm2-3-3-and-gno-3-1
Sep 29, 2026
Merged

jinoosss merged 9 commits into
mainfrom
chore/adopt-tm2-3-3-and-gno-3-1

Conversation

@jinoosss

@jinoosss jinoosss commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Tracks the latest main of gnolang/tm2-js-client, gnolang/gno-js-client and onbloc/adena-wallet.

@gnolang/tm2-js-client 1.3.3 → 3.3.0 and @gnolang/gno-js-client 1.4.5 → 3.1.0. That crosses two majors on each side, so the changes below are the breakages that had to be absorbed plus the drift that had accumulated against the wallet.

Dependencies

Package Before After
@gnolang/tm2-js-client 1.3.3 3.3.0
@gnolang/gno-js-client 1.4.5 3.1.0
@bufbuild/protobuf 2.2.3 (dev) 2.15.0 (runtime)
long undeclared, bundled removed
@noble/curves, @noble/hashes, @scure/bip32, @scure/bip39, ethereum-cryptography pinned, unused removed
@web3auth/openlogin-adapter 8.12.4, unused removed
  • @bufbuild/protobuf was misplaced. The vendored gno event protos import @bufbuild/protobuf/wire at runtime while the package was a devDependency, so tsup inlined a copy of the library into bin/, at a version (2.2.3) that did not match what the gno / tm2 clients resolve. It is now a real dependency pinned to 2.15.0 and stays external in the bundle.
  • long was a phantom dependency. Nothing declared it; it resolved only because protobufjs happened to be in the tree. The regenerated protos don't need it, and bin/index.js no longer references it.
  • The crypto pins had gone stale. They were added to dedupe the crypto stack of tm2-js-client 1.x. tm2 3.x resolves the @noble / @scure 2.x line through @cosmjs 0.39, so keeping 1.x pins as direct dependencies only forced consumers to install a second copy. Nothing in src/ imports them.

After the change bin/index.js requires exactly the declared dependencies and nothing else.

Encoding

  • core/types/proto/gno/event.ts regenerated with ts-proto 2.12.4, matching the upstream clients: Long → bigint, and decode now guards its recursion depth instead of overflowing the stack.
  • bytes_delta was encoded with the wrong wire type. It used proto int64 (plain varint). TM2 amino encodes Go int64 through binary.PutVarint, which is zig-zag — the proto sint64 wire type, the same one upstream uses for TxFee.gas_wanted, MsgEnablePackage.pkg_height and MsgCreateSession.expires_at. This matters in practice because StorageUnlockEvent.BytesDelta is negative on the chain. Fixed to sint64 and pinned by a test that asserts the bytes.
  • All nine message endpoints are now supported. MsgEnablePackage, MsgRejectPackage, MsgCreateSession, MsgRevokeSession and MsgRevokeAllSessions get make*Message builders and round-trip through encodeTransactionMessage / decodeTransactionMessage, matching the wallet's EMessageType.
  • mapTxToTransactionParams reads the now-bigint fee.gas_wanted.

fee_delta / fee_refund stay string. Upstream they are std.Coin, which looks like it should be an embedded message, but tm2/pkg/std/coin.go defines MarshalAmino() (string, error) returning "<amount><denom>" — so the wire value really is a string, and adena-wallet's parser reads it with parseTokenAmount.

No overlap with @gnolang/tm2-rpc. It reaches us only transitively through tm2-js-client, and nothing in src/ imports it — the same position gno-js-client moved to in #273, which dropped its direct tm2-rpc runtime dependency in favour of Tm2Client / constructRequestError re-exported from tm2-js-client. tm2-rpc does export Event / EventAttribute, but those are the decoded-RPC read model (@type plus an index signature, no codec) and tm2-js-client does not re-export them, so they are not reachable by, or interchangeable with, this SDK's protobuf codecs.

Verified unchanged: encodeTransaction, and the SignedDocument fee shape, which already matched the Ledger-compatible payload tm2 3.0.0 switched to ({ amount: [{ amount, denom }], gas }) and the single-coin gas_fee the builder produces.

Provider

JSONRPCProvider builds its RPC client asynchronously since tm2 3.x and its constructor is now protected, so instances come from JSONRPCProvider.create(). connectProvider and setNetwork became async as a result; connect and switchNetwork already were.

Adena injection API

The wallet's multisig surface changed in adena-wallet after this SDK's multisig support landed. Shapes below were read off the extension's implementation, not its declared types, which are themselves stale in places.

  • createMultisigTransaction takes { messages, fee: { gasFee, gasWanted }, memo?, networkInfo? } — not { chain_id, msgs, fee: { amount, gas } } — and its response data is { tx } only.
  • Signing and broadcasting take a MultisigTransactionDocument ({ tx, chainId, accountNumber, sequence }). Since createMultisigTransaction no longer returns the account context, callers assemble it from getAccount / getNetwork; the README shows how.
  • Collected signatures key their public key under @type, not type.
  • TransactionParams gained optional networkInfo; DoContract and BroadcastMultisigTransaction gained ContractOptions.
  • AdenaWallet gained version and Sign, and lost SignDocument — the extension does not inject it, and the SDK never called it.

createMultisigAccount was checked and is unchanged: it still returns { multisigConfig, multisigAddress }.

Tooling

  • Jest transpiles the ESM-only packages tm2 3.x pulls in (uuid 14, @noble/* 2.x, @scure/* 2.x, readonly-date-esm) via a new tsconfig.spec.json. Without it every suite that touches a provider fails to parse.
  • .nvmrc → 24.13.0, the runtime the upstream clients test against.

Consumer note: tm2-js-client's CJS build requires ESM-only uuid, so require-ing this SDK now needs Node 22.12+. ESM consumers are unaffected.

Missing functions and types

A sweep of the public surface against WalletProvider, window.adena and the barrel files turned up things that were never reachable. Fixed in the second commit.

  • AdenaSDK.getNetwork() did not exist. core/methods/get-network.ts, GetNetworkResponse, two test files and both provider implementations were all in place, and the README documented adenaSDK.getNetwork(), but the SDK class never wired it up. Added, with a delegation test that asserts every WalletProvider method is reachable through AdenaSDK so this cannot recur.
  • core/types/methods was never re-exported. Around 30 option and response types had no public name — SignTransactionOptions, BroadcastTransactionOptions, SwitchNetworkOptions, GetAccountResponse, IsConnectedResponse, SignMultisigTransactionOptions (which this PR changes), and so on. Callers could not annotate an AdenaSDK result.
  • ConnectionState was declared but not exported, so AdenaSDK.getConnectionState() returned an enum callers could not name or compare against.
  • LOGIN_PROVIDER was export declare const, which emits no JavaScript. It is a real object now that the module is exported.
  • Barrels skipped files: core/methods (get-social-user-profile), core/types/methods (get-connection-state, get-social-user-profile), core/utils (network.utils, provider.utils — the isTM2WalletProvider / isGnoSocialWalletProvider guards). core/constants had no barrel at all, so GNO_ADDRESS_PREFIX and DEFAULT_RPC_URL were unreachable.
  • README: new WalletProvider() cannot work — WalletProvider is the interface. The example now uses AdenaWalletProvider.

Four names collide between core/types/methods and providers/adena-wallet/types. The bare name is now the SDK-level WalletResponse shape that AdenaSDK returns; the raw window.adena shape keeps an Adena prefix:

Bare name (SDK response) Prefixed (raw window.adena)
AddEstablishResponse AdenaAddEstablishResponse
CreateMultisigAccountResponse AdenaCreateMultisigAccountResponse
CreateMultisigTransactionResponse AdenaCreateMultisigTransactionResponse
SignMultisigTransactionResponse AdenaSignMultisigTransactionResponse

The public surface goes from 136 to 183 exports with nothing removed (verified by diffing the generated bin/index.d.ts export list before and after).

Also checked and found complete: every WalletResponse*Type enum member the wallet imports from this SDK, the nine MsgEndpoint entries, and all eleven symbols the wallet actually imports from @adena-wallet/sdk (AdenaWallet, WalletMessageInfo, StorageDepositEvent, StorageUnlockEvent, WalletResponse, WalletResponseType, WalletResponseStatus and the four response-type enums).

Downstream migration required in adena-wallet

packages/adena-extension/src/common/provider/gno/event-parser.ts decodes real chain events through this SDK's proto codec and reads the result with the Long API:

const decodedEvent = parseProto(event.value, StorageDepositEvent.decode);
const bytesDelta = decodedEvent.bytes_delta.toInt(); // Long

bytes_delta is a bigint now, which has no .toInt(). That line becomes Number(decodedEvent.bytes_delta) when the wallet picks up this version. It is the only place the wallet touches a field this PR retyped.

That file is also why the sint64 fix matters in production rather than in principle: it is a live decode path for /tm.StorageDepositEvent and /tm.StorageUnlockEvent, and StorageUnlockEvent.bytes_delta is negative on the chain. Decoding a zig-zag varint as a plain int64 returns double the value for a positive delta and a wrong value entirely for a negative one.

Follow-up (not in this PR)

Three parts of window.adena still have no SDK wrapper. These are feature additions rather than drift, so they are not in this PR:

TransactionResultCommit / TransactionResultSync in core/types/transaction.types.ts are hand-rolled and no longer describe what either provider returns. Both hand back tm2's BroadcastTxCommitResult, where ResponseBase.Events is a JSON string of tm2-rpc Event[] (adaptResponseBase does JSON.stringify(rb.events)) and Error is a { [key: string]: string } map — not the TransactionEvent[] and { log: string } the SDK declares, and height is missing. It type-checks only because the union's loosest member, TransactionResult, requires nothing but hash. Pre-existing — ABCIResponseBase.Events was already string | null in tm2-js-client 1.3.3 — and fixing it changes a public type, so it is flagged rather than folded in.

GnoWalletProvider also connects a plain tm2 JSONRPCProvider, so GnoWallet.getProvider() hands back a provider without the gno-specific methods its type advertises. Pre-existing; switching it to GnoJSONRPCProvider would be a behavior change.

Verification

  • tsc --noEmit clean
  • yarn lint clean
  • jest from the repo root (as CI runs it) and from packages/sdk: 31 suites, 87 tests passing — up from 28/78, with new coverage for the added message types, the sint64 fix and AdenaSDK provider delegation
  • yarn build clean

Track the latest `main` of gnolang/tm2-js-client, gnolang/gno-js-client and
onbloc/adena-wallet.

Dependencies
- `@gnolang/tm2-js-client` 1.3.3 -> 3.3.0, `@gnolang/gno-js-client` 1.4.5 -> 3.1.0.
- `@bufbuild/protobuf` moves from devDependencies to dependencies at 2.15.0, the
  version the gno / tm2 clients resolve. The vendored gno event protos import
  `@bufbuild/protobuf/wire` at runtime, so it was being bundled into the output
  as a second copy of the library.
- Drop `long`. It was never declared, only reachable through `protobufjs`, and
  the regenerated protos no longer need it.
- Drop the unused `@noble/*`, `@scure/*` and `ethereum-cryptography` pins. They
  deduped the crypto stack of tm2-js-client 1.x; tm2 3.x resolves the 2.x line
  through `@cosmjs` 0.39, so the pins only forced a second copy on consumers.
  Also drop `@web3auth/openlogin-adapter`, superseded by `@web3auth/auth-adapter`.

Encoding
- Regenerate `core/types/proto/gno/event.ts` with ts-proto 2.12.4, matching the
  upstream clients: `Long` becomes `bigint` and `decode` guards its recursion
  depth.
- Fix `bytes_delta` to encode as `sint64`, not `int64`. TM2 amino encodes Go
  `int64` as a zig-zag varint, which is the proto `sint64` wire type, and
  `StorageUnlockEvent.BytesDelta` is negative on the chain.
- Support every message endpoint the wallet sends: `MsgEnablePackage`,
  `MsgRejectPackage`, `MsgCreateSession`, `MsgRevokeSession` and
  `MsgRevokeAllSessions` now have `make*Message` builders and round-trip
  through `encodeTransactionMessage` / `decodeTransactionMessage`.
- `mapTxToTransactionParams` reads the now-`bigint` `fee.gas_wanted`.

Provider
- `JSONRPCProvider` builds its RPC client asynchronously since tm2 3.x and its
  constructor is protected, so `connectProvider` and `setNetwork` are async.

Adena injection API
- Align the multisig types with the wallet's current surface:
  `createMultisigTransaction` takes `{ messages, fee: { gasFee, gasWanted } }`
  and returns `{ tx }` only; signing takes a `MultisigTransactionDocument`
  assembled from `getAccount` / `getNetwork`; collected signatures key their
  public key under `@type`.
- Add `version`, `Sign` and `ContractOptions`; remove `SignDocument`, which the
  extension does not inject.

Tooling
- Jest transpiles the ESM-only packages tm2 3.x pulls in (`uuid`,
  `@noble/*`, `@scure/*`, `readonly-date-esm`) through `tsconfig.spec.json`.
  Note that a CJS consumer now needs Node 22.12+ to `require` the SDK, since
  tm2-js-client's CJS build requires ESM-only `uuid`.
- `.nvmrc` moves to 24.13.0, the runtime the upstream clients test against.
Audit of the public surface against `WalletProvider`, `window.adena` and the
barrel files, after the client upgrade.

- `AdenaSDK.getNetwork()` was missing. `core/methods/get-network.ts`, its
  response type, its tests and both provider implementations were all in place,
  and the README documented it, but the SDK class never wired it up.
- `core/types/methods` was never re-exported, so ~30 option and response types
  a caller needs to annotate `AdenaSDK` calls had no public name, including
  `SignTransactionOptions` and `SignMultisigTransactionOptions`. Four names
  collide with `providers/adena-wallet/types`; the bare name is now the
  SDK-level `WalletResponse` shape that `AdenaSDK` returns, and the raw
  `window.adena` shape is exported with an `Adena` prefix.
- `ConnectionState` was declared but not exported, so
  `AdenaSDK.getConnectionState()` returned an enum callers could not name.
- `LOGIN_PROVIDER` was an `export declare const`, which emits nothing. It is a
  real object now, since the module it lives in is exported.
- Barrels that skipped files: `core/methods` (`get-social-user-profile`),
  `core/types/methods` (`get-connection-state`, `get-social-user-profile`),
  `core/utils` (`network.utils`, `provider.utils` — the `isTM2WalletProvider` /
  `isGnoSocialWalletProvider` guards), and `core/constants`, which had no
  barrel at all.
- README: `new WalletProvider()` cannot work, `WalletProvider` is the interface.
  Use `AdenaWalletProvider`.

The public surface goes from 136 to 183 exports with nothing removed.

@junghoon-vans junghoon-vans 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.

The asynchronous provider creation introduces a failed network-switch path where the reported chain no longer matches the wallet's active provider. Please make the transition atomic and cover a rejected RPC connection.

`JSONRPCProvider.create` performs a version-detection round trip through
`Tm2Client.connect`, so it rejects for an unreachable node. `setNetwork`
assigned `currentChainId` before awaiting it and installed the provider only
after it resolved, so a switch to a refused RPC rejected, left the previous
provider active, and had `getNetwork()` report the network the wallet was not
on. The synchronous constructor this replaced installed the provider before
returning, so the window is new.

- `connectProviderTo(rpcUrl)` builds the provider for an explicit endpoint and
  mutates nothing until it resolves. `connectProvider` now delegates to it.
- `setNetwork` connects first and assigns `currentChainId` only on success, so
  a rejection leaves the reported network and the active provider in agreement.
- Overlapping switches are tagged, and a slower earlier one no longer commits
  its chain id over a newer one that already landed. `disconnectProvider`
  clears the tag so an in-flight switch cannot commit after disconnect.
- `switchNetwork` returns `NETWORK_TIMEOUT` instead of rejecting, matching how
  its other failure paths report.

Covers the refused connection, the listener that must not fire, recovery once
the endpoint is reachable, and the overlapping switch. Three of the five fail
against the previous implementation.
@jinoosss

Copy link
Copy Markdown
Member Author

Confirmed and fixed in b0cd780 — thanks, the reproduction was exact.

JSONRPCProvider.create goes through Tm2Client.connect, which awaits detectVersion (a node status query), so it really does reject for an unreachable endpoint. And you're right that the window is new rather than pre-existing: the old synchronous constructor installed the provider before returning, so currentChainId and the active provider could never disagree.

Atomic transition. setNetwork now connects first and assigns currentChainId only after wallet.connect() has run:

private async setNetwork(network: NetworkInfo): Promise<void> {
  const request = Symbol(network.chainId);
  this.pendingNetworkRequest = request;

  try {
    await this.connectProviderTo(network.rpcUrl);
  } catch (error) {
    if (this.pendingNetworkRequest === request) {
      this.pendingNetworkRequest = null;
    }
    throw error;
  }

  if (this.pendingNetworkRequest !== request) {
    return;
  }
  this.pendingNetworkRequest = null;

  this.currentChainId = network.chainId;
  this.triggerNetworkCallback(this.currentChainId);
}

connectProviderTo(rpcUrl) takes the endpoint explicitly instead of reading it back off currentNetwork, so nothing is mutated until the factory resolves. connectProvider() keeps its signature and delegates to it.

Overlapping switches. Tagged each call, so a slower earlier switch that lands after a newer one no longer commits its chain id over it. disconnectProvider clears the tag too, so an in-flight switch cannot commit after a disconnect.

Failure reporting. switchNetwork returned a rejected promise where its other failure paths return a response; it now returns NETWORK_TIMEOUT.

Coverage — src/core/__tests__/providers/switch-network-atomicity.test.ts, five cases: refused connection keeps the reported chain and the active provider in sync, the listener does not fire on failure, recovery once the endpoint is reachable again, and the overlapping switch. Three of the five fail against the previous implementation, and the overlap case fails if the request tag is removed, so they are load-bearing rather than decorative.

32 suites / 92 tests passing; lint, tsc --noEmit and build clean.

@junghoon-vans junghoon-vans 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.

The refused-RPC path is fixed, but overlapping switches can still install a stale provider after the newer switch commits. The new overlap test needs to assert both reported and active networks.

b0cd780 checked the tag after `connectProviderTo` had already run
`wallet.connect(provider)`, so a superseded switch still replaced the active
provider once its factory resolved. B first, C resolving first, then B, left
`{ reported: 'C', active: 'B' }`.

`setNetwork` now builds the provider itself and does the ownership check
before installing it, so the check, `wallet.connect()` and the `currentChainId`
assignment sit in one synchronous block with no await between them. A
superseded or disconnected switch drops its provider instead of installing it.
`connectProviderTo` is gone; `connectProvider` inlines the same create-then-
install and re-checks the wallet, which `disconnect()` can clear mid-flight.

The overlap tests now assert the active provider alongside the reported chain,
in both settle orders, plus one asserting a lost switch never reaches
`wallet.connect`. All three fail against b0cd780.
@jinoosss

Copy link
Copy Markdown
Member Author

Right on both counts — fixed in dc54a01.

setNetwork now builds the provider itself and checks ownership before installing it, so the tag check, wallet.connect() and the currentChainId assignment sit in one synchronous block with no await between them. A superseded switch drops its provider instead of installing it. connectProviderTo is gone.

The overlap tests now assert the active provider alongside the reported chain, in both settle orders, plus one asserting a lost switch never reaches wallet.connect. All three fail against b0cd780 with your { reported: 'C', active: 'B' }.

32 suites / 94 tests; lint, tsc, build clean.

@junghoon-vans junghoon-vans 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.

The overlapping switchNetwork() fix is verified. A concurrently pending connect() can still install its older provider after a successful switch and make the active RPC disagree with getNetwork().

`connectProvider()` built and installed its provider without taking a ticket,
so an initial `connect()` for A that resolved after a `switchNetwork(B)` had
committed replaced the active provider and left `{ reported: 'B', active: 'A' }`.

Both entry points now go through `applyConnection(rpcUrl, network)`, which
takes the ticket, checks it after the factory resolves, and only then installs
the provider and selects the network — still with no await in between. A
superseded attempt drops its provider and reports whether the newer attempt
has already landed. `setNetwork` is a thin wrapper that passes the target
network; `connectProvider` passes null to keep the current selection.

Covers both orderings of a `connect()` racing a `switchNetwork()`. The
reviewer's ordering fails against dc54a01.
@jinoosss

Copy link
Copy Markdown
Member Author

Fixed in 5e85258 — connectProvider() was the one path still installing without a ticket.

Both entry points now go through applyConnection(rpcUrl, network): take the ticket, check it after the factory resolves, then install the provider and select the network with no await in between. A superseded attempt drops its provider. setNetwork passes the target network, connectProvider passes null to keep the current selection.

Two tests added for connect() racing switchNetwork(), both settle orders. Your ordering fails against dc54a01 with active: 'https://a.example'.

One residual I'd rather flag than paper over: if the older connect() resolves before the newer switch lands, it returns false, so ConnectionManager briefly marks DISCONNECTED even though the switch then connects. The provider/chain invariant holds throughout — only the connection-state label is stale. Happy to have connect() await the winning attempt if you want that closed too.

32 suites / 96 tests; lint, tsc, build clean.

@junghoon-vans junghoon-vans 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.

The stale-provider race is fixed and the new concurrency tests pass. A superseded connect() can still leave the SDK connection manager permanently DISCONNECTED after a successful network switch, blocking subsequent SDK methods.

A superseded attempt guessed its result from `pendingNetworkRequest === null &&
wallet !== null`, which is false while the attempt that took over is still in
flight. `ConnectionManager.connectTM2Wallet` turns that into DISCONNECTED, and
`AdenaSDK.getWalletProvider()` then rejects every provider method — even though
the switch that superseded it connected fine.

Each attempt now registers its outcome next to its ticket, and a superseded one
awaits that instead of guessing. It resolves true once the winner installs its
provider, false if the winner fails. `pendingConnection` always belongs to a
later attempt at that point, so it cannot wait on itself; `disconnectProvider`
clears it with the ticket.

A superseded attempt no longer settles before the winner does, so the two
overlap tests that awaited it mid-flight now flush instead.

Adds the SDK-level assertion for the reported order: reconnect, switch, resolve
the older connect first, then the switch. `getConnectionState()` stays
CONNECTED and `getNetwork()` reports the switched network. It fails against
5e85258 with DISCONNECTED (0).
@jinoosss

Copy link
Copy Markdown
Member Author

Closed in 95ff854 — this was the residual I flagged, and you're right that it's consumer-visible.

The superseded attempt was guessing its result from pendingNetworkRequest === null && wallet !== null, which is false while the winner is still in flight. Each attempt now registers its outcome next to its ticket, and a superseded one awaits that instead: true once the winner installs its provider, false if the winner fails. pendingConnection always belongs to a later attempt at that point, so it can't wait on itself, and disconnectProvider clears it with the ticket.

Side effect worth noting: a superseded attempt no longer settles before the winner does, so two overlap tests that awaited it mid-flight now flush instead.

Added the SDK-level assertion for your order — reconnect, switch, resolve the older A connect first, then B. getConnectionState() stays CONNECTED, getNetwork() reports B, provider methods keep working. It fails against 5e85258 with Expected: 2, Received: 0.

33 suites / 99 tests; lint, tsc, build clean.

@junghoon-vans junghoon-vans 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.

The superseded reconnect now waits for a successful switch, and that path passes. If the winning switch fails, the old provider remains active but the SDK connection manager is marked DISCONNECTED; subsequent SDK methods are still blocked.

A superseded attempt returned the winner's result verbatim, so a winning switch
that failed reported false and `ConnectionManager.connectTM2Wallet` marked the
wallet DISCONNECTED — even though the provider installed before the race was
still the active one and `getNetwork()` still answered.

The question the caller asks is whether the wallet is connected, not whether the
winner succeeded. `hasActiveProvider` tracks whether a provider is installed,
set alongside `wallet.connect()` and cleared by `disconnectProvider`, and a
superseded attempt falls back to it when the winner fails.

Provider-level coverage splits into the two outcomes: a failing winner with a
provider already installed resolves true and leaves the wallet on it, and one
with nothing installed still resolves false. Adds the SDK-level assertion for
the reported order — reconnect, switch, resolve the older connect, reject the
switch — where the state stays CONNECTED, `getNetwork()` reports the original
network and the switch returns NETWORK_TIMEOUT. Both fail against 95ff854.
@jinoosss

Copy link
Copy Markdown
Member Author

Fixed in e8cf2fc.

I had the superseded attempt return the winner's result verbatim, which answers the wrong question — the caller asks whether the wallet is connected, not whether the winner succeeded. hasActiveProvider now tracks whether a provider is installed (set alongside wallet.connect(), cleared by disconnectProvider), and a superseded attempt falls back to it when the winner fails.

Provider-level coverage split into both outcomes: a failing winner with a provider already installed resolves true and leaves the wallet on it, one with nothing installed still resolves false.

Added the SDK-level assertion for your order — reconnect, switch, resolve the older A connect, reject B. State stays CONNECTED, getNetwork() reports A, switch returns NETWORK_TIMEOUT. Against 95ff854 that one gives Expected: 2, Received: 0, and the provider-level one Expected: true, Received: false.

33 suites / 101 tests; lint, tsc, build clean.

@junghoon-vans junghoon-vans 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.

The previously reported failed-winning-switch path is fixed. An older superseded connect factory can still reject after the newer switch succeeds, causing the SDK connection state to become ERROR while the active provider is healthy.

@junghoon-vans junghoon-vans 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.

Verified the superseded connection failure fix on the PR branch. A stale A factory rejection no longer marks the SDK ERROR after B connects, in both settlement orders. The previous regression now yields an active/reported B and CONNECTED state. yarn lint, tsc --noEmit, all 103 tests, and yarn build pass.

The constructor kicks off `connectWallet()` without awaiting when `isSession`
is set. That was safe while the provider was built by a synchronous
constructor, but tm2-js-client 3.x rejects for an unreachable node and
`connectTM2Wallet` rethrows after recording ERROR, so constructing an
`AdenaSDK` with a session and a node that is down leaks an unhandled rejection
— a process-level crash in Node, an `unhandledrejection` event in a browser.

The call site now catches and logs, matching how the awaited `connect()` path
already reports. The connection state is unchanged: ERROR, as before.

The regression test fails against d35ec05, where the rejection escapes and
takes the suite down with it.
@jinoosss

Copy link
Copy Markdown
Member Author

Pulled d35ec05 and verified it independently: your two tests fail against e8cf2fc with the not connect wallet symptom you described, and the supersedingConnectionOutcome() extraction is the right shape — the catch now rethrows only while the attempt still owns the ticket.

While auditing the rest of the async-factory blast radius I found one more spot this PR opened, after your approval. ConnectionManager's constructor starts connectWallet() without awaiting when isSession is set:

if (this.config.isSession) {
  this.stateManager.loadState();
  this.connectWallet();        // floating
}

That was safe while the provider came from a synchronous constructor. Now the factory rejects for an unreachable node and connectTM2Wallet rethrows after recording ERROR, so new AdenaSDK(provider, { isSession: true }) against a node that is down leaks an unhandled rejection — a crash in Node, an unhandledrejection event in a browser. Jest surfaces it by failing the whole suite, which is how the regression test catches it.

Fixed in b5b6bf2: the call site catches and logs, matching the awaited connect() path. Connection state is unchanged at ERROR. The test fails against d35ec05.

34 suites / 105 tests; lint, tsc, build clean. Worth a re-approve since this landed after yours.

@junghoon-vans junghoon-vans 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.

Checked the additional session-restore fix. An unreachable RPC no longer leaks an unhandled rejection from the constructor; the SDK records ERROR, and I exercised that path in the built package. Type-check, build, lint, and all 105 tests pass.

@jinoosss
jinoosss merged commit bf9d99a into main Sep 29, 2026
3 checks passed
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.

3 participants