Repository navigation
chore: adopt tm2-js-client 3.3.0 and gno-js-client 3.1.0 - #43
Conversation
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
left a comment
There was a problem hiding this comment.
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.
|
Confirmed and fixed in b0cd780 — thanks, the reproduction was exact.
Atomic transition. 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);
}
Overlapping switches. Tagged each call, so a slower earlier switch that lands after a newer one no longer commits its chain id over it. Failure reporting. Coverage — 32 suites / 92 tests passing; lint, |
junghoon-vans
left a comment
There was a problem hiding this comment.
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.
|
Right on both counts — fixed in dc54a01.
The overlap tests now assert the active provider alongside the reported chain, in both settle orders, plus one asserting a lost switch never reaches 32 suites / 94 tests; lint, tsc, build clean. |
junghoon-vans
left a comment
There was a problem hiding this comment.
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.
|
Fixed in 5e85258 — Both entry points now go through Two tests added for One residual I'd rather flag than paper over: if the older 32 suites / 96 tests; lint, tsc, build clean. |
junghoon-vans
left a comment
There was a problem hiding this comment.
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).
|
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 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. 33 suites / 99 tests; lint, tsc, build clean. |
junghoon-vans
left a comment
There was a problem hiding this comment.
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.
|
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. 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, 33 suites / 101 tests; lint, tsc, build clean. |
junghoon-vans
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
|
Pulled d35ec05 and verified it independently: your two tests fail against e8cf2fc with the While auditing the rest of the async-factory blast radius I found one more spot this PR opened, after your approval. 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 Fixed in b5b6bf2: the call site catches and logs, matching the awaited 34 suites / 105 tests; lint, tsc, build clean. Worth a re-approve since this landed after yours. |
junghoon-vans
left a comment
There was a problem hiding this comment.
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.
Tracks the latest
mainof gnolang/tm2-js-client, gnolang/gno-js-client andonbloc/adena-wallet.@gnolang/tm2-js-client1.3.3 → 3.3.0 and@gnolang/gno-js-client1.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
@gnolang/tm2-js-client@gnolang/gno-js-client@bufbuild/protobuflong@noble/curves,@noble/hashes,@scure/bip32,@scure/bip39,ethereum-cryptography@web3auth/openlogin-adapter@bufbuild/protobufwas misplaced. The vendored gno event protos import@bufbuild/protobuf/wireat runtime while the package was a devDependency, so tsup inlined a copy of the library intobin/, 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.longwas a phantom dependency. Nothing declared it; it resolved only becauseprotobufjshappened to be in the tree. The regenerated protos don't need it, andbin/index.jsno longer references it.@noble/@scure2.x line through@cosmjs0.39, so keeping 1.x pins as direct dependencies only forced consumers to install a second copy. Nothing insrc/imports them.After the change
bin/index.jsrequires exactly the declared dependencies and nothing else.Encoding
core/types/proto/gno/event.tsregenerated with ts-proto 2.12.4, matching the upstream clients:Long→bigint, anddecodenow guards its recursion depth instead of overflowing the stack.bytes_deltawas encoded with the wrong wire type. It used protoint64(plain varint). TM2 amino encodes Goint64throughbinary.PutVarint, which is zig-zag — the protosint64wire type, the same one upstream uses forTxFee.gas_wanted,MsgEnablePackage.pkg_heightandMsgCreateSession.expires_at. This matters in practice becauseStorageUnlockEvent.BytesDeltais negative on the chain. Fixed tosint64and pinned by a test that asserts the bytes.MsgEnablePackage,MsgRejectPackage,MsgCreateSession,MsgRevokeSessionandMsgRevokeAllSessionsgetmake*Messagebuilders and round-trip throughencodeTransactionMessage/decodeTransactionMessage, matching the wallet'sEMessageType.mapTxToTransactionParamsreads the now-bigintfee.gas_wanted.fee_delta/fee_refundstaystring. Upstream they arestd.Coin, which looks like it should be an embedded message, buttm2/pkg/std/coin.godefinesMarshalAmino() (string, error)returning"<amount><denom>"— so the wire value really is a string, andadena-wallet's parser reads it withparseTokenAmount.No overlap with
@gnolang/tm2-rpc. It reaches us only transitively through tm2-js-client, and nothing insrc/imports it — the same position gno-js-client moved to in #273, which dropped its direct tm2-rpc runtime dependency in favour ofTm2Client/constructRequestErrorre-exported from tm2-js-client. tm2-rpc does exportEvent/EventAttribute, but those are the decoded-RPC read model (@typeplus 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 theSignedDocumentfee shape, which already matched the Ledger-compatible payload tm2 3.0.0 switched to ({ amount: [{ amount, denom }], gas }) and the single-coingas_feethe builder produces.Provider
JSONRPCProviderbuilds its RPC client asynchronously since tm2 3.x and its constructor is now protected, so instances come fromJSONRPCProvider.create().connectProviderandsetNetworkbecame async as a result;connectandswitchNetworkalready were.Adena injection API
The wallet's multisig surface changed in
adena-walletafter 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.createMultisigTransactiontakes{ messages, fee: { gasFee, gasWanted }, memo?, networkInfo? }— not{ chain_id, msgs, fee: { amount, gas } }— and its response data is{ tx }only.MultisigTransactionDocument({ tx, chainId, accountNumber, sequence }). SincecreateMultisigTransactionno longer returns the account context, callers assemble it fromgetAccount/getNetwork; the README shows how.@type, nottype.TransactionParamsgained optionalnetworkInfo;DoContractandBroadcastMultisigTransactiongainedContractOptions.AdenaWalletgainedversionandSign, and lostSignDocument— the extension does not inject it, and the SDK never called it.createMultisigAccountwas checked and is unchanged: it still returns{ multisigConfig, multisigAddress }.Tooling
uuid14,@noble/*2.x,@scure/*2.x,readonly-date-esm) via a newtsconfig.spec.json. Without it every suite that touches a provider fails to parse..nvmrc→ 24.13.0, the runtime the upstream clients test against.Missing functions and types
A sweep of the public surface against
WalletProvider,window.adenaand 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 documentedadenaSDK.getNetwork(), but the SDK class never wired it up. Added, with a delegation test that asserts everyWalletProvidermethod is reachable throughAdenaSDKso this cannot recur.core/types/methodswas 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 anAdenaSDKresult.ConnectionStatewas declared but not exported, soAdenaSDK.getConnectionState()returned an enum callers could not name or compare against.LOGIN_PROVIDERwasexport declare const, which emits no JavaScript. It is a real object now that the module is exported.core/methods(get-social-user-profile),core/types/methods(get-connection-state,get-social-user-profile),core/utils(network.utils,provider.utils— theisTM2WalletProvider/isGnoSocialWalletProviderguards).core/constantshad no barrel at all, soGNO_ADDRESS_PREFIXandDEFAULT_RPC_URLwere unreachable.new WalletProvider()cannot work —WalletProvideris the interface. The example now usesAdenaWalletProvider.Four names collide between
core/types/methodsandproviders/adena-wallet/types. The bare name is now the SDK-levelWalletResponseshape thatAdenaSDKreturns; the rawwindow.adenashape keeps anAdenaprefix:window.adena)AddEstablishResponseAdenaAddEstablishResponseCreateMultisigAccountResponseAdenaCreateMultisigAccountResponseCreateMultisigTransactionResponseAdenaCreateMultisigTransactionResponseSignMultisigTransactionResponseAdenaSignMultisigTransactionResponseThe public surface goes from 136 to 183 exports with nothing removed (verified by diffing the generated
bin/index.d.tsexport list before and after).Also checked and found complete: every
WalletResponse*Typeenum member the wallet imports from this SDK, the nineMsgEndpointentries, and all eleven symbols the wallet actually imports from@adena-wallet/sdk(AdenaWallet,WalletMessageInfo,StorageDepositEvent,StorageUnlockEvent,WalletResponse,WalletResponseType,WalletResponseStatusand the four response-type enums).Downstream migration required in
adena-walletpackages/adena-extension/src/common/provider/gno/event-parser.tsdecodes real chain events through this SDK's proto codec and reads the result with theLongAPI:bytes_deltais abigintnow, which has no.toInt(). That line becomesNumber(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
sint64fix matters in production rather than in principle: it is a live decode path for/tm.StorageDepositEventand/tm.StorageUnlockEvent, andStorageUnlockEvent.bytes_deltais negative on the chain. Decoding a zig-zag varint as a plainint64returns 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.adenastill have no SDK wrapper. These are feature additions rather than drift, so they are not in this PR:Sign(amino signing) — noAdenaSDKmethod.cosmos.*— the SDK ships the types (added in feat: add cosmos types and multi-chain establish #42) but no wrapper methods.version— now on theAdenaWallettype, but not surfaced throughAdenaWalletProvider.TransactionResultCommit/TransactionResultSyncincore/types/transaction.types.tsare hand-rolled and no longer describe what either provider returns. Both hand back tm2'sBroadcastTxCommitResult, whereResponseBase.Eventsis a JSON string of tm2-rpcEvent[](adaptResponseBasedoesJSON.stringify(rb.events)) andErroris a{ [key: string]: string }map — not theTransactionEvent[]and{ log: string }the SDK declares, andheightis missing. It type-checks only because the union's loosest member,TransactionResult, requires nothing buthash. Pre-existing —ABCIResponseBase.Eventswas alreadystring | nullin tm2-js-client 1.3.3 — and fixing it changes a public type, so it is flagged rather than folded in.GnoWalletProvideralso connects a plain tm2JSONRPCProvider, soGnoWallet.getProvider()hands back a provider without the gno-specific methods its type advertises. Pre-existing; switching it toGnoJSONRPCProviderwould be a behavior change.Verification
tsc --noEmitcleanyarn lintcleanjestfrom the repo root (as CI runs it) and frompackages/sdk: 31 suites, 87 tests passing — up from 28/78, with new coverage for the added message types, thesint64fix andAdenaSDKprovider delegationyarn buildclean