Skip to content
Open
Show file tree
Hide file tree
Changes from all 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"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please remove this addition. it is not necessary. if it breaks again, you would need to actually fix the packahe.json entry points (exports)

}
}
}
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