[LWDM] feat(coin-tester): add the NEAR coin-tester - #20297
Conversation
Web Tools Build Status
|
There was a problem hiding this comment.
Pull request overview
Adds a new deterministic NEAR coin-tester module and the minimal shared framework hooks needed to run the same scenario through both the legacy account bridge and the generic coin-framework bridge.
Changes:
- Introduces
@ledgerhq/coin-tester-near(near-sandbox-backed) with an MSW indexer stub, local signer, and a full send + staking lifecycle scenario. - Adds NEAR bridge API intent mapping (
withdraw→finalize_unstake) and wires it into the coin-module loader registry. - Fixes generic framework optimistic-op fee parsing for large BigNumber magnitudes and updates
createTransactiondefaults to support NEAR’s nonce semantics.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
pnpm-lock.yaml |
Adds dependency graph entries for the new NEAR coin-tester package and near-sandbox transitive deps. |
package.json |
Adds coin:tester:near script to run the new tester workspace. |
libs/ledger-live-common/src/families/near/bridge/api.ts |
Adds NEAR-specific intent-type mapping so the generic framework can build withdraw operations. |
libs/ledger-live-common/src/families/near/bridge/api.test.ts |
Unit tests for the NEAR bridge API intent mapping and default export wiring. |
libs/ledger-live-common/src/coin-modules/loaders.ts |
Registers NEAR loadBridgeApi so the generic framework can resolve computeIntentType. |
libs/ledger-live-common/src/bridge/generic-coin-framework/utils.ts |
Fixes fee conversion for optimistic ops by avoiding exponential notation when converting to BigInt. |
libs/ledger-live-common/src/bridge/generic-coin-framework/utils.test.ts |
Adds regression coverage for the BigNumber 1e21 boundary that previously broke BigInt parsing. |
libs/ledger-live-common/src/bridge/generic-coin-framework/createTransaction.ts |
Treats NEAR like Cardano regarding default nonce/sequence expectations (signable default tx). |
libs/coin-tester-modules/coin-tester-near/tsconfig.json |
TypeScript config for the new coin-tester workspace. |
libs/coin-tester-modules/coin-tester-near/tsconfig.build.json |
Build-time TS config excluding tests and test folders. |
libs/coin-tester-modules/coin-tester-near/src/stakingPool.ts |
Downloads/verifies a pinned staking-pool WASM and deploys/pings it in the sandbox. |
libs/coin-tester-modules/coin-tester-near/src/signer.ts |
Implements local signers compatible with both legacy bridge and generic framework signing contracts. |
libs/coin-tester-modules/coin-tester-near/src/scenarii/near.ts |
Defines the deterministic NEAR scenario (2 transfers + stake/unstake/withdraw), plus sandbox/indexer lifecycle. |
libs/coin-tester-modules/coin-tester-near/src/scenarii.test.ts |
Runs the same NEAR scenario for both strategies (legacy and generic-adapter). |
libs/coin-tester-modules/coin-tester-near/src/sandbox.ts |
Starts near-sandbox on a free port, provides RPC helpers, account funding, and fast-forwarding. |
libs/coin-tester-modules/coin-tester-near/src/indexer.ts |
MSW stubs for indexer endpoints (history/stats/deposits/validators) backed by sandbox state. |
libs/coin-tester-modules/coin-tester-near/src/helpers.ts |
Bridge wiring for both strategies and legacy-bridge adaptation to GenericTransaction. |
libs/coin-tester-modules/coin-tester-near/src/fixtures.ts |
Deterministic constants + LiveConfig coin-config pointing to sandbox and MSW indexer host. |
libs/coin-tester-modules/coin-tester-near/README.md |
Documentation on running the tester and what it covers. |
libs/coin-tester-modules/coin-tester-near/package.json |
Defines the new workspace, deps, scripts, and exports for coin-tester-near. |
libs/coin-tester-modules/coin-tester-near/jest.config.ts |
Jest config for node-based integration tests via SWC. |
libs/coin-tester-modules/coin-tester-near/CHANGELOG.md |
Initializes changelog for the new package. |
libs/coin-tester-modules/coin-tester-near/.unimportedrc.json |
Adds legacy unimported configuration for the new package. |
libs/coin-tester-modules/coin-tester-near/.gitignore |
Ignores the downloaded staking pool WASM cache directory. |
.github/workflows/test-coin-tester.yml |
Adds near to the coin-tester workflow matrix env list. |
.changeset/near-coin-tester.md |
Changeset bumping @ledgerhq/coin-tester-near and @ledgerhq/live-common. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Rsdoctor Bundle Diff AnalysisFound 7 projects in monorepo, 7 projects with changes. 📊 Quick Summary
📋 Detailed Reports (Click to expand)📁 desktop-mainPath:
📁 desktop-preloaderPath:
📁 desktop-rendererPath:
📁 desktop-webviewDappPreloaderPath:
📁 desktop-webviewPreloaderPath:
📁 desktop-workersPath:
📁 mobilePath:
Generated by Rsdoctor GitHub Action |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 26 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (3)
libs/coin-tester-modules/coin-tester-near/src/indexer.ts:146
Number(...)can yieldNaNwhenlimitis present but not numeric (e.g.?limit=foo). In that case the>= limitcheck inhistoryForwill never be true and the stub may return an unbounded history. Safer to default to 25 unless the parsed limit is finite.
const limit = Number(new URL(request.url).searchParams.get("limit") ?? 25);
const data = await historyFor(rpc, String(params.address), limit);
libs/coin-tester-modules/coin-tester-near/src/sandbox.ts:55
- The RPC helper always calls
response.json()without checkingresponse.ok. If the sandbox returns a non-2xx response or a non-JSON body, this will throw a low-signal error and can also keep the socket busy. Consider handling HTTP errors explicitly and surfacing status + body for easier debugging.
const rpc = async <T>(method: string, params: unknown): Promise<T> => {
const response = await fetch(rpcUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: "coin-tester", method, params }),
});
const body = (await response.json()) as { result?: T; error?: unknown };
if (body.error) {
throw new Error(`near-sandbox ${method}: ${JSON.stringify(body.error)}`);
}
return body.result as T;
};
libs/coin-tester-modules/coin-tester-near/src/indexer.ts:211
- This constant allowlist is recreated on every unhandled request. Prefer a module-level
Setand.has()for constant membership checks (clearer intent and avoids repeated array allocation).
server.listen({
onUnhandledRequest: request => {
const hostname = new URL(request.url).hostname;
// Allow requests to the local sandbox node to pass through to the real node.
if (["127.0.0.1", "localhost"].includes(hostname)) return;
throw new Error(`Unhandled request: ${request.method} ${request.url}`);
},
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
libs/coin-tester-modules/coin-tester-near/src/sandbox.ts:116
tearDown()usesPromise.race([sandbox.tearDown(), timeout]). If the timeout wins,sandbox.tearDown()keeps running in the background and may later reject, which would become an unhandled promise rejection (and can fail the Jest process). Wrapsandbox.tearDown()with a.catch(...)before racing so any late rejection is handled.
async tearDown() {
// The node occasionally keeps a socket open and never resolves; the temp home is disposable,
// so a stuck shutdown must not hold the whole suite hostage.
let timer: NodeJS.Timeout | undefined;
await Promise.race([
sandbox.tearDown(),
new Promise<void>(resolve => {
timer = setTimeout(() => {
console.warn("coin-tester-near: sandbox teardown timed out, continuing");
resolve();
}, 15_000);
}),
]);
60ebfda to
35df985
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
libs/ledger-live-common/src/families/near/bridge/api.ts:14
JSON.stringify(mode)can itself throw (e.g. whenmodeis abigint) or returnundefined(e.g.symbol), which would either mask the intended error or produce an unhelpful message. Sincemodeisunknownhere, it’s safer to stringify with a fallback toString(mode).
if (typeof mode !== "string") {
throw new TypeError(`Unsupported transaction mode: ${JSON.stringify(mode)}`);
}
35df985 to
0e716a3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
libs/coin-tester-modules/coin-tester-near/src/sandbox.ts:112
Promise.race([sandbox.tearDown(), timeout])will reject ifsandbox.tearDown()throws, and in that case the timeout handle is never cleared. Since the intent here is to avoid teardown issues holding the suite hostage, it’s safer to (1) swallow/console.warnteardown failures and (2) clear the timer in afinallyblock so it’s always cleaned up.
await Promise.race([
sandbox.tearDown(),
new Promise<void>(resolve => {
timer = setTimeout(() => {
console.warn("coin-tester-near: sandbox teardown timed out, continuing");
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (4)
libs/ledger-live-common/src/families/near/bridge/api.ts:24
- Same as above: the unsupported-mode error message would be clearer if it included the family name (e.g.
Unsupported NEAR transaction mode: ...) for consistency with other bridge APIs.
return "finalize_unstake";
default:
throw new Error(`Unsupported transaction mode: ${mode}`);
}
libs/ledger-live-common/src/families/near/bridge/api.ts:14
- The thrown error message here doesn't include the family name, unlike the other bridge APIs (e.g.
Unsupported Solana transaction mode: ...infamilies/solana/bridge/api.ts). IncludingNEARin the message would make framework-side debugging much easier.
This issue also appears on line 21 of the same file.
if (typeof mode !== "string") {
throw new TypeError(`Unsupported transaction mode: ${JSON.stringify(mode)}`);
}
libs/coin-tester-modules/coin-tester-near/package.json:38
- The package's
exportsmap is missing the standard./*conditional export used by the other coin-tester modules (e.g.coin-tester-solana,coin-tester-stellar). Without it, consumers relying on the@ledgerhq/sourcecondition (and the require/default split) won't resolve subpath imports consistently.
"exports": {
"./lib/*": "./lib/*.js",
"./lib-es/*": "./lib-es/*.js",
"./package.json": "./package.json"
},
libs/coin-tester-modules/coin-tester-near/src/sandbox.ts:116
Promise.racedoesn't cancel the losing promise. Ifsandbox.tearDown()eventually rejects after the timeout branch wins, it can surface as an unhandled rejection and fail the Jest run. Attaching a.catchprevents that while keeping the timeout behavior.
await Promise.race([
sandbox.tearDown(),
new Promise<void>(resolve => {
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (3)
pnpm-lock.yaml:14295
- The lockfile includes changes unrelated to adding the NEAR coin-tester (e.g., the
@apollo/server@4.12.2deprecatedmetadata text changed while the package version stayed the same). This suggests the lockfile was regenerated with different tooling/metadata and adds noise that makes dependency reviews harder. Please try to regenerate the lockfile so it only reflects the new workspace/deps introduced by this PR (or explain why these extra deltas are unavoidable).
pnpm-lock.yaml:22880 - The lockfile changes
@storybook/addon-react-native-web@0.0.29’speerDependencies.webpackfrom'*'to'^5.89.0'without any corresponding version bump in the dependency itself. This is another unrelated lockfile metadata delta that looks like a lockfile regeneration artifact; please try to keeppnpm-lock.yamlchanges limited to what’s required for the new NEAR tester deps.
pnpm-lock.yaml:25225 - The lockfile changes
any-observable@0.3.0’speerDependencies.rxjsfrom'*'to'^5.5.10'(package version unchanged). Unless this is a known pnpm/registry metadata normalization, it’s unrelated churn; please keep lockfile diffs scoped to the NEAR tester addition if possible.
|



📝 Description
Adds
@ledgerhq/coin-tester-near, a deterministic integration tester for the NEAR family. It boots a localnear-sandboxnode and runs one scenario through both the legacy account bridge and the generic coin-framework bridge, so the two surfaces are compared against each other on every run.The scenario covers a transfer to an existing named account, a transfer to a fresh implicit account, and the full staking cycle: delegate, undelegate, and withdraw after the unlock period.
No Docker.
near-sandboxdownloads a nativeneardbuild for the host platform, so it runs on the standard CI runner and on a laptop with no extra setup.staking_pool.wasmis pinned to a commit and verified against its sha256, so neither upstream nor a stale cache can change the contract the scenario delegates to.Three changes outside the tester package are needed for the generic-adapter half of the run:
families/near/bridge/api.ts(new): NEAR names its staking modesstake/unstake/withdraw, while the framework's default whitelist knowsstake/unstake/finalize_unstakeand throws on anything else. Without the mapping,withdrawcannot be built through the framework at all.generic-coin-framework/createTransaction.ts: NEAR joins the Cardano branch, since a NEAR nonce belongs to an access key rather than the account, so the default transaction must be signable without callers setting a sequence.generic-coin-framework/utils.ts:buildOptimisticOperationconverted fees withBigNumber.toString(), which switches to exponential notation from 1e21 up and cannot be parsed byBigInt. At NEAR's 24 decimals a staking fee crosses that threshold at 0.0015, so every staked operation threw. Now usestoFixed(), with a regression test asserting the boundary directly.The last one is a shared-framework fix affecting any family with a large-magnitude fee. It is bundled here rather than split out because the tester is red without it.
What the first end-to-end run caught
Four contract mismatches that unit tests had missed, all wrong assumptions about the indexer contract rather than bugs in the coin module:
/v3/statsmust be stubbedgetGasPrice's node-RPC fallback only covers a response without agas_price, not a request that throwsstaking-depositsreturns a bare array{ data }envelopeloadBridgeApiis requiredcomputeIntentTypefrom the loader registry, not from the callerfinality: "final"while a new account is first visible only optimisticallyNotes for reviewers
bridge/api.ts(30 lines) plus two small framework edits.families/near/bridge/api.tsgets thebridge/api.test.tsthat the other nine families already have.continue-on-error: true, so it reports without gating the merge.developautomatically once that merges.🔗 Context