Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
6 changes: 6 additions & 0 deletions .changeset/near-coin-tester.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ledgerhq/coin-tester-near": minor
"@ledgerhq/live-common": minor
---

Add the NEAR coin-tester
2 changes: 1 addition & 1 deletion .github/workflows/test-coin-tester.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ permissions:
contents: read

env:
COIN_TESTER_CURRENCIES: "evm polkadot solana bitcoin tezos tron xrp stellar multiversx cosmos cardano"
COIN_TESTER_CURRENCIES: "evm polkadot solana bitcoin tezos tron xrp stellar multiversx cosmos cardano near"

jobs:
is-affected:
Expand Down
3 changes: 3 additions & 0 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@
},
"./libs/coin-tester-modules/coin-tester-multiversx": {
"entry": ["src/scenarii/multiversx.ts"]
},
"./libs/coin-tester-modules/coin-tester-near": {
"entry": ["src/scenarii/near.ts"]
}
}
}
2 changes: 2 additions & 0 deletions libs/coin-tester-modules/coin-tester-near/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# staking-pool wasm, downloaded on first run
.cache/
1 change: 1 addition & 0 deletions libs/coin-tester-modules/coin-tester-near/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# @ledgerhq/coin-tester-near
53 changes: 53 additions & 0 deletions libs/coin-tester-modules/coin-tester-near/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# @ledgerhq/coin-tester-near

> [!NOTE]
> **Status: STABLE** — Production-ready; API is considered stable.

Deterministic integration tester for the NEAR family. Starts a local `near-sandbox` node and runs
one scenario through both the legacy account bridge and the generic coin-framework bridge
(Alpaca/`CoinModuleApi`), so the two are compared against each other on every run.

## Run

```sh
pnpm coin:tester:near start
```

No Docker required: `near-sandbox` downloads a native binary for the host platform on first run
(Darwin arm64 and Linux x86_64 are both published). The coin module must be built first
(`pnpm --filter @ledgerhq/coin-near build`).

## Coverage

- transfers to an existing named account and to a fresh implicit account
- staking: delegate, undelegate, and withdraw after the unlock period
- the account's balance breakdown and operation history after each transaction

## How it works

**Node.** `near-sandbox` is a build of `neard` carrying the `sandbox` feature, which adds the
`sandbox_fast_forward` RPC. Genesis is left untouched — shrinking `epoch_length` to speed up the
staking unlock stops the node from ever becoming ready, so block height is advanced explicitly
instead.

The node sometimes keeps a socket open and never completes its shutdown, which holds the event loop
open long after the assertions are done. Teardown is therefore bounded by a timeout, and the run uses
`--forceExit`, as the polkadot and stellar testers do for the same reason. `near-sandbox` exposes no
handle on the process, so there is nothing to kill directly; the RPC port is picked per run, so a
leftover node never collides with the next one.

**Staking pool.** NEAR staking runs through a contract rather than the protocol, so the scenario
deploys `staking_pool.wasm` from `near/core-contracts` and delegates to it. The download is pinned to
a commit and checked against its sha256, so neither upstream nor a stale cache can swap the contract
out from under the run. The pool logs a
`minimum_stake` failure once per epoch because it also tries to become a protocol validator with far
less than the seat price; that is expected and does not affect delegation.

**Indexer.** The coin module reads its history, gas price, staking deposits and validator list from
an indexer that no local node provides, so those four endpoints are served by an `msw` stub built
from the sandbox's own state. `/v3/stats` is stubbed too, despite `getGasPrice` having a node-RPC
fallback: that fallback only covers a response without a `gas_price`, not a request that throws, and
an unstubbed host fails DNS resolution long before it is reached.

**Signing.** The local signer signs `sha256` of the Borsh-serialized transaction, which is what the
device app does. Signing the raw payload produces a signature the network rejects at broadcast.
32 changes: 32 additions & 0 deletions libs/coin-tester-modules/coin-tester-near/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Config } from "jest";

const esmDeps = ["ky"];

const config: Config = {
testEnvironment: "node",
setupFilesAfterEnv: ["@ledgerhq/wallet-framework-test-setup"],
transform: {
"^.+\\.(t|j)sx?$": [
"@swc/jest",
{
jsc: {
target: "esnext",
},
},
],
[`node_modules[\\\\|/].pnpm[\\\\|/](${esmDeps.join("|")}).+\\.(js|jsx|mjs)$`]: [
"@swc/jest",
{
jsc: {
target: "esnext",
},
},
],
},
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
transformIgnorePatterns: [`node_modules/.pnpm/(?!(${esmDeps.join("|")}))`],
reporters: ["default", ...(process.env.CI ? ["github-actions"] : [])],
};

export default config;
78 changes: 78 additions & 0 deletions libs/coin-tester-modules/coin-tester-near/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"name": "@ledgerhq/coin-tester-near",
"version": "0.1.0",
"private": true,
"description": "Ledger NEAR Coin Tester",
"main": "src/scenarii.test.ts",
"keywords": [
"Ledger",
"LedgerWallet",
"NEAR",
"Testing"
],
"repository": {
"type": "git",
"url": "https://github.com/LedgerHQ/ledger-live.git"
},
"bugs": {
"url": "https://github.com/LedgerHQ/ledger-live/issues"
},
"homepage": "https://github.com/LedgerHQ/ledger-live/tree/develop/libs/coin-tester-modules/coin-tester-near",
"typesVersions": {
"*": {
"lib/*": [
"lib/*"
],
"lib-es/*": [
"lib-es/*"
],
"*": [
"lib/*"
]
}
},
"exports": {
"./lib/*": "./lib/*.js",
"./lib-es/*": "./lib-es/*.js",
"./package.json": "./package.json"
},
"license": "Apache-2.0",
"scripts": {
"clean": "rimraf lib lib-es",
"build": "tsc --project tsconfig.build.json && tsc --project tsconfig.build.json -m esnext --moduleResolution bundler --outDir lib-es",
"prewatch": "pnpm build",
"watch": "tsc --watch --project tsconfig.build.json",
"watch:es": "tsc --watch --project tsconfig.build.json -m esnext --moduleResolution bundler --outDir lib-es",
"lint": "oxlint src",
"lint:fix": "oxfmt src && oxlint src --fix --quiet",
"format": "oxfmt src",
"format:check": "oxfmt src --check",
"typecheck": "tsc --noEmit --customConditions node",
"start": "pnpm jest --runTestsByPath src/*.test.ts --runInBand --forceExit",
"unimported": "pnpm knip --directory ../../.. -W libs/coin-tester-modules/coin-tester-near"
},
Comment thread
pawell24 marked this conversation as resolved.
"dependencies": {
"@ledgerhq/coin-near": "workspace:^",
"@ledgerhq/coin-tester": "workspace:^",
"@ledgerhq/ledger-wallet-framework": "workspace:^",
"@ledgerhq/live-common": "workspace:^",
"@ledgerhq/live-config": "workspace:^",
"@ledgerhq/types-live": "workspace:^",
"bignumber.js": "^9",
"msw": "catalog:",
"near-api-js": "^3.0.2",
"near-sandbox": "^0.3.2"
},
"devDependencies": {
"@ledgerhq/wallet-framework-test-setup": "workspace:^",
"@swc/core": "catalog:",
"@swc/jest": "catalog:",
"@types/jest": "catalog:",
"@types/node": "catalog:",
"jest": "catalog:",
"oxfmt": "catalog:",
"oxlint": "catalog:",
"rimraf": "^5",
"typescript": "catalog:"
}
}
88 changes: 88 additions & 0 deletions libs/coin-tester-modules/coin-tester-near/src/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import BigNumber from "bignumber.js";
import type { Account } from "@ledgerhq/types-live";
import { decodeAccountId } from "@ledgerhq/ledger-wallet-framework/account";
import { getCryptoCurrencyById } from "@ledgerhq/ledger-wallet-framework/currencies";
import {
getDerivationScheme,
runDerivationScheme,
} from "@ledgerhq/ledger-wallet-framework/derivation";
import { DEFAULT_ACCOUNT_ID } from "near-sandbox";

export const NETWORK_ID = "sandbox";

/** Every account the scenario touches is a subaccount of the genesis account. */
export const SENDER_ID = `sender.${DEFAULT_ACCOUNT_ID}`;
export const NAMED_RECIPIENT_ID = `recipient.${DEFAULT_ACCOUNT_ID}`;
export const POOL_ID = `pool.${DEFAULT_ACCOUNT_ID}`;

/**
* A 64-character hex address. NEAR creates such an account implicitly on first transfer, which is
* the branch that costs more than a transfer to an existing account.
*/
export const IMPLICIT_RECIPIENT_ID =
"4e7de0a21d8a20f970c86b6edf407906d7ba9e205979c3268270eef80a286e2d";

export const NEAR = 10n ** 24n;

export const SENDER_BALANCE = 200n * NEAR;
export const RECIPIENT_BALANCE = 10n * NEAR;
export const POOL_BALANCE = 50n * NEAR;

/** Base URL the coin config points at; msw intercepts it, nothing listens on it. */
export const INDEXER_URL = "http://indexer.coin-tester-near.local";

/** Four epochs of 500 blocks, the pool's `NUM_EPOCHS_TO_UNLOCK`, plus a margin. */
export const EPOCHS_TO_UNLOCK_BLOCKS = 2500;

export const coinConfig = (rpcUrl: string) => () => ({
status: { type: "active" as const },
infra: {
API_NEAR_PRIVATE_NODE: rpcUrl,
API_NEAR_PUBLIC_NODE: rpcUrl,
API_NEAR_INDEXER: INDEXER_URL,
API_NEARBLOCKS_INDEXER: INDEXER_URL,
},
});

export const NEAR_CURRENCY = getCryptoCurrencyById("near");

/** The account the scenario starts from, before the first sync fills it in. */
export const makeAccount = (address: string): Account => {
const id = `js:2:${NEAR_CURRENCY.id}:${address}:`;
const { derivationMode, xpubOrAddress } = decodeAccountId(id);
const scheme = getDerivationScheme({ derivationMode, currency: NEAR_CURRENCY });
const index = 0;

return {
type: "Account",
id,
xpub: xpubOrAddress,
subAccounts: [],
seedIdentifier: xpubOrAddress,
used: true,
swapHistory: [],
derivationMode,
currency: NEAR_CURRENCY,
index,
nfts: [],
freshAddress: xpubOrAddress,
freshAddressPath: runDerivationScheme(scheme, NEAR_CURRENCY, {
account: index,
node: 0,
address: 0,
}),
creationDate: new Date(),
lastSyncDate: new Date(0),
blockHeight: 0,
balance: new BigNumber(0),
spendableBalance: new BigNumber(0),
operationsCount: 0,
operations: [],
pendingOperations: [],
balanceHistoryCache: {
HOUR: { latestDate: null, balances: [] },
DAY: { latestDate: null, balances: [] },
WEEK: { latestDate: null, balances: [] },
},
} as unknown as Account;
};
Loading
Loading