diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93fffa30f0..cdbe3e79d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ permissions: pull-requests: read env: - SOCKET_SECURITY_MODE: monitor # Options: monitor (non-blocking) or block (fails on vulnerabilities) + SOCKET_SECURITY_MODE: monitor # Options: monitor (non-blocking) or block (fails on vulnerabilities) jobs: # Detect which file groups changed so downstream jobs can skip when irrelevant. @@ -36,6 +36,7 @@ jobs: with: filters: | source: + - '.github/workflows/ci.yml' - 'modules/**' - 'package.json' - 'yarn.lock' @@ -62,30 +63,37 @@ jobs: strategy: fail-fast: false matrix: - node-version: [22.x, 24.x] + node-version: [22.x, 24.x, 26.x] experimental: [false] include: - # Node's Current release line — not yet an LTS. Failures here are - # surfaced via PR comment but do not block merge (see all-checks). - - node-version: 26.x + - node-version: 27-nightly experimental: true steps: - - uses: socketdev/action@937f824ec476dfd164d4a4d9995751427b0be143 # v1.3.0 + - id: socket-firewall + continue-on-error: ${{ matrix.experimental }} + uses: socketdev/action@937f824ec476dfd164d4a4d9995751427b0be143 # v1.3.0 with: mode: firewall-free - - uses: actions/checkout@v6 + - id: checkout + continue-on-error: ${{ matrix.experimental }} + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 - name: Setup node:${{ matrix.node-version }} + id: setup-node + continue-on-error: ${{ matrix.experimental }} + timeout-minutes: 15 uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - name: Build Info + id: build-info + continue-on-error: ${{ matrix.experimental }} run: | echo "node $(node --version)" echo "npm $(npm --version)" @@ -95,12 +103,15 @@ jobs: echo "head ref $GITHUB_HEAD_REF" - name: Fetch Base Ref + id: fetch-base-ref + continue-on-error: ${{ matrix.experimental }} run: | git fetch origin ${GITHUB_BASE_REF:-master} - name: restore lerna dependencies id: lerna-cache - uses: actions/cache@v5 + continue-on-error: ${{ matrix.experimental }} + uses: actions/cache/restore@v5 with: path: | node_modules @@ -108,13 +119,32 @@ jobs: key: ${{ runner.os }}-node${{matrix.node-version}}-${{ hashFiles('yarn.lock') }}-${{ hashFiles('tsconfig.packages.json') }}-${{ hashFiles('**/package.json') }} - name: Install Packages + id: install-packages if: steps.lerna-cache.outputs.cache-hit != 'true' || contains( github.event.pull_request.labels.*.name, 'SKIP_CACHE') + continue-on-error: ${{ matrix.experimental }} + timeout-minutes: 60 run: sfw yarn install --with-frozen-lockfile --ignore-scripts + - name: save lerna dependencies + id: save-lerna-cache + if: steps.lerna-cache.outputs.cache-hit != 'true' && steps.install-packages.outcome == 'success' + continue-on-error: ${{ matrix.experimental }} + uses: actions/cache/save@v5 + with: + path: | + node_modules + modules/*/node_modules + key: ${{ steps.lerna-cache.outputs.cache-primary-key }} + - name: Check In-Repo Package Versions + id: check-versions + continue-on-error: ${{ matrix.experimental }} run: yarn run check-versions - name: build packages + id: build-packages + continue-on-error: ${{ matrix.experimental }} + timeout-minutes: 60 env: # Workaround for https://github.com/nodejs/node/issues/51555 DISABLE_V8_COMPILE_CACHE: '1' @@ -123,18 +153,36 @@ jobs: - name: Unit Test id: unit-test continue-on-error: ${{ matrix.experimental }} + timeout-minutes: 60 run: yarn run unit-test-changed env: BITGOJS_TEST_PASSWORD: ${{ secrets.BITGOJS_TEST_PASSWORD }} - - name: Comment on Node Current failure - if: matrix.experimental && steps.unit-test.outcome == 'failure' && github.event_name == 'pull_request' + - name: Comment on experimental Node.js failure + if: >- + always() && + matrix.experimental && + github.event_name == 'pull_request' && + ( + steps.socket-firewall.outcome == 'failure' || + steps.checkout.outcome == 'failure' || + steps.setup-node.outcome == 'failure' || + steps.build-info.outcome == 'failure' || + steps.fetch-base-ref.outcome == 'failure' || + steps.lerna-cache.outcome == 'failure' || + steps.install-packages.outcome == 'failure' || + steps.save-lerna-cache.outcome == 'failure' || + steps.check-versions.outcome == 'failure' || + steps.build-packages.outcome == 'failure' || + steps.unit-test.outcome == 'failure' + ) + continue-on-error: true uses: actions/github-script@v7 with: script: | const marker = ''; - const body = `${marker}\n⚠️ Unit tests are failing on Node ${{ matrix.node-version }} (Current release line, non-blocking). ` + - `This is not an LTS version yet, so it does not block merge, but it signals an incompatibility to fix before Node ${{ matrix.node-version }} becomes LTS.\n\n` + + const body = `${marker}\n⚠️ Compatibility checks are failing on Node.js ${{ matrix.node-version }} (experimental, non-blocking). ` + + `This does not block merge, but it signals an incompatibility to fix before this release becomes supported.\n\n` + `[View run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`; const { data: comments } = await github.rest.issues.listComments({ @@ -142,7 +190,7 @@ jobs: repo: context.repo.repo, issue_number: context.payload.pull_request.number, }); - const existing = comments.find((c) => c.body.includes(marker)); + const existing = comments.find((comment) => comment.body.includes(marker)); if (existing) { await github.rest.issues.updateComment({ @@ -160,8 +208,23 @@ jobs: }); } - - name: Resolve Node Current failure comment - if: matrix.experimental && steps.unit-test.outcome == 'success' && github.event_name == 'pull_request' + - name: Resolve experimental Node.js failure comment + if: >- + always() && + matrix.experimental && + github.event_name == 'pull_request' && + steps.socket-firewall.outcome != 'failure' && + steps.checkout.outcome != 'failure' && + steps.setup-node.outcome != 'failure' && + steps.build-info.outcome != 'failure' && + steps.fetch-base-ref.outcome != 'failure' && + steps.lerna-cache.outcome != 'failure' && + steps.install-packages.outcome != 'failure' && + steps.save-lerna-cache.outcome != 'failure' && + steps.check-versions.outcome != 'failure' && + steps.build-packages.outcome != 'failure' && + steps.unit-test.outcome == 'success' + continue-on-error: true uses: actions/github-script@v7 with: script: | @@ -171,13 +234,13 @@ jobs: repo: context.repo.repo, issue_number: context.payload.pull_request.number, }); - const existing = comments.find((c) => c.body.includes(marker)); + const existing = comments.find((comment) => comment.body.includes(marker)); if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, - body: `${marker}\n✅ Unit tests now pass on Node ${{ matrix.node-version }} (Current release line).`, + body: `${marker}\n✅ Unit tests now pass on Node.js ${{ matrix.node-version }} (experimental).`, }); } diff --git a/CLAUDE.md b/CLAUDE.md index 037df89541..38b2a7266d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,11 +5,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Build and Development Commands ### Installation + ```bash yarn install ``` ### Development + ```bash # Watch mode for development (compiles all TypeScript packages and watches for changes) yarn dev @@ -19,6 +21,7 @@ yarn ``` ### Linting + ```bash # Lint all packages yarn lint @@ -34,6 +37,7 @@ yarn check-commits ``` ### Testing + ```bash # Run all unit tests yarn unit-test @@ -55,6 +59,7 @@ yarn coverage ``` ### Browser Compatibility + ```bash # Run browser tests yarn browser-tests @@ -73,11 +78,13 @@ For modules that need browser support (especially those using `@bitgo/wasm-utxo` ## Code Style ### Commits + BitGoJS uses conventional commits. All commits MUST pass the spec described in `commitlint.config.js`. Every commit needs a footer issue reference (`references-empty: never`). Web Experience side work without a Linear ticket may use `WEB-000` in the footer (junk-drawer reference, same pattern as `BTC-000` / `CSHLD-000`). Branches named `WEB-000-*` get `TICKET: WEB-000` appended automatically by `scripts/prepare-commit-msg.js`. ### TypeScript Guidelines + - **Avoid `any` type**: Use specific types, interfaces, or union types instead of `any`. If absolutely necessary, prefer `unknown` and use type guards for safety. ## Code Architecture @@ -85,20 +92,24 @@ Every commit needs a footer issue reference (`references-empty: never`). Web Exp BitGoJS is a monorepo composed of multiple modules, each implementing specific functionality: ### Core Modules + - `bitgo`: Main module providing authentication, wallet management, cryptographic primitives, and coin interfaces - `@bitgo/sdk-core`: Core SDK functionality, base classes, and interfaces - `@bitgo/sdk-api`: API client utilities - `@bitgo/statics`: Static configuration values used across the platform ### Coin Implementation Modules + - `@bitgo/sdk-coin-*`: Individual cryptocurrency implementations (e.g., btc, eth, sol, etc.) - `@bitgo/abstract-eth`, `@bitgo/abstract-utxo`, etc.: Abstract base classes for similar coin families ### Transaction Building and Signing + - `@bitgo/account-lib`: Build and sign transactions for account-based coins - `@bitgo/utxo-lib`: Build and sign transactions for UTXO-based coins ### Other Utility Modules + - `@bitgo/blockapis`: Access public block explorer APIs for various coins - `@bitgo/express`: Local BitGo transaction signing server and proxy - `@bitgo/unspents`: Utilities for UTXO chains @@ -122,6 +133,7 @@ The BitGoJS SDK follows a modular architecture: ## Adding New Coins To implement a new coin: + ```bash yarn sdk-coin:new ``` @@ -130,4 +142,4 @@ This will generate the necessary boilerplate for a new coin implementation. ## Node.js Version Support -BitGoJS supports Node.js versions >=22, with NPM >=3.10.10. +BitGoJS supports Node.js versions 22 and later, including Node.js 26, with NPM >=3.10.10. diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 4a7f10002e..bc4da4e237 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -1,7 +1,7 @@ # Requirements - yarn version 1 (classic) -- node version >=22 and <25 +- Node.js version 22 or later # Installing @@ -180,7 +180,7 @@ Engines should be set to the following: ```json { "engines": { - "node": ">=22 <25", + "node": ">=22", "npm": ">=3.10.10" } } diff --git a/README.md b/README.md index c3e9b423b7..093ac80661 100644 --- a/README.md +++ b/README.md @@ -45,26 +45,27 @@ Other tags may be released to npm (e.g. `hotfix`, `dev`, etc...), but are not co Examples can be found in each of the modules specific to the module use cases. Starter examples can be found [here](https://github.com/BitGo/BitGoJS/tree/master/examples). -# NodeJS Version Support Policy +# Node.js Version Support Policy BitGoJS currently provides support for the following Node versions per package.json engines policy: ``` "engines": { - "node": ">=22 <25", + "node": ">=22", "npm": ">=3.10.10" } ``` -We specifically limit our support to these versions of Node, not because this package won't work on other versions, but because these versions tend to be the most widely used in practice. It's possible the packages in this repository will work correctly on newer or older versions of Node, but we typically don't run automated tests against non-specified versions of Node (including odd versions), with the possible exception of the latest odd numbered version for advanced awareness of upcoming breaks in version support. +We specifically limit our support to these versions of Node, not because this package won't work on other versions, but because these versions tend to be the most widely used in practice. The packages may work correctly on newer or older versions, but only the supported versions below are required to pass continuous integration. We also run one newer Node.js release as a non-blocking early-warning check so incompatibilities are visible before that release becomes supported. As each Node LTS version reaches its end-of-life we will exclude that version from the node engines property of our package's package.json file. Removing a Node version is considered a breaking change and will entail the publishing of a new major version of this package. We will not accept any requests to support an end-of-life version of Node, and any pull requests or issues regarding support for an end-of-life version of Node will be closed. We will accept code that allows this package to run on newer, non-LTS, versions of Node. Furthermore, we will attempt to ensure our own changes work on the latest version of Node. To help in that commitment, our continuous integration setup runs the full test suite on the latest release of the following versions of node: - `22` (Maintenance LTS) — required to pass -- `24` (Active LTS) — required to pass -- `26` (Current) — run non-blocking; a failure posts a warning comment on the PR instead of blocking merge +- `24` (Maintenance LTS) — required to pass +- `26` (Current) — required to pass +- `27` nightly (experimental) — non-blocking; a failure posts a warning comment on the pull request -When a new Node major becomes Active LTS, update the `node-version` matrix in `.github/workflows/ci.yml` (moving the outgoing Current version into the required set and adding the new Current release), and bump the `engines.node` range across all `modules/*/package.json` files accordingly. +When a new Node.js major becomes supported, add it to the required `node-version` matrix in `.github/workflows/ci.yml` and update the `engines.node` range across all `modules/*/package.json` files if the range does not already include it. Keep exactly one newer release as the non-blocking early-warning check: when Node.js 28 development builds become available, replace the Node.js 27 nightly entry rather than adding another experimental matrix entry. Node.js 26 is required before its Long Term Support (LTS) transition, so its CI check is intentionally blocking already. JavaScript package managers should allow you to install this package with any version of Node, with, at most, a warning if your version of Node does not fall within the range specified by our node engines property. If you encounter issues installing this package on a supported version of Node, please report the issue to us. diff --git a/modules/bitgo/README.md b/modules/bitgo/README.md index 73cc69e7a4..ebb9d53ec7 100644 --- a/modules/bitgo/README.md +++ b/modules/bitgo/README.md @@ -15,7 +15,7 @@ If you have questions or comments about this API, email support@bitgo.com. # Installation -Ensure you're running Node version 20 or higher (preferably the latest LTS release) and NPM version 10. +Ensure you're running Node.js version 22 or later, including Node.js 26, and NPM version 10. For setting your Node version, we recommend using `nvm` - the [Node Version Manager](https://github.com/creationix/nvm/blob/master/README.markdown#installation). `npm install --save bitgo` diff --git a/modules/express/README.md b/modules/express/README.md index 12273b7af0..cad19fe20e 100644 --- a/modules/express/README.md +++ b/modules/express/README.md @@ -117,13 +117,15 @@ $ yarn build-docker-express $ docker run -it bitgo-express:latest ``` +The BitGo Express Docker image tracks the older of the two most recent Node.js Long-Term Support (LTS) releases. It currently builds and runs on Node.js 24. + ## From source For users who are unable to run BitGo Express as a Docker container, we recommend building and running from the source code. ### Prerequisites -Ensure you're running at least Node version 16. We recommend using `nvm`, the [Node Version Manager](https://github.com/creationix/nvm/blob/master/README.markdown#installation), for setting your Node version. BitGo Express has become a first class citizen of the monorepo so we recommend using `yarn` for local setup. +Use Node.js 22 or later. The required continuous integration checks cover Node.js 22, 24, and 26. We recommend using `nvm`, the [Node Version Manager](https://github.com/creationix/nvm/blob/master/README.markdown#installation), for setting your Node.js version. BitGo Express is part of the monorepo, so use `yarn` for local setup. ### Cloning the repository and installing dependencies diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 0d8b078062..f460faad45 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -453,9 +453,11 @@ async function decryptPrivKey(bg: BitGo, encryptedPrivKey: string, walletPw: str export async function handleV2GenerateShareTSS( req: ExpressApiRouteRequest<'express.v2.tssshare.generate', 'post'> ): Promise { - const walletId = req.body.txRequest ? req.body.txRequest.walletId : req.body.tssParams.txRequest.walletId; - if (!walletId) { - throw new Error('Missing required field: walletId'); + const txRequest = req.body.txRequest ?? req.body.tssParams?.txRequest; + const walletId = + txRequest && typeof txRequest === 'object' && !Array.isArray(txRequest) ? txRequest.walletId : undefined; + if (typeof walletId !== 'string' || !walletId) { + throw new ApiResponseError('Missing required field: walletId', 400); } const walletPw = getWalletPwFromEnv(walletId); diff --git a/modules/express/test/unit/typedRoutes/expressWalletUpdate.ts b/modules/express/test/unit/typedRoutes/expressWalletUpdate.ts index 11a366c48e..80233d5c83 100644 --- a/modules/express/test/unit/typedRoutes/expressWalletUpdate.ts +++ b/modules/express/test/unit/typedRoutes/expressWalletUpdate.ts @@ -270,29 +270,64 @@ describe('Express Wallet Update Typed Routes Tests', function () { }), }); + const userAuthKey = { + id: 'userAuthKeyId', + pub: 'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8', + encryptedPrv: 'encryptedPrivateKey', + source: 'user' as const, + coinSpecific: { + [coin]: { + purpose: 'userAuth', + }, + }, + }; + const nodeAuthKey = { + id: 'nodeAuthKeyId', + pub: 'xpub661MyMwAqRbcGczjuMoRm6dXaLDEhW1u34gKenbeYqAix21mdUKJyuyu5F1rzYGVxyL6tmgBUAEPrEz92mBXjByMRiJdba9wpnN37RLLAXa', + encryptedPrv: 'encryptedNodeAuthKey', + source: 'user' as const, + coinSpecific: { + [coin]: { + purpose: 'nodeAuth', + }, + }, + }; + + const keychainsGetStub = sinon.stub(); + keychainsGetStub.withArgs({ id: userAuthKey.id }).resolves(userAuthKey); + keychainsGetStub.withArgs({ id: nodeAuthKey.id }).resolves(nodeAuthKey); + + const keychainsStub = { + get: keychainsGetStub, + } as any; + + const baseCoinStub = { + getFamily: sinon.stub().returns('lnbtc'), + getChain: sinon.stub().returns(coin), + keychains: sinon.stub().returns(keychainsStub), + } as any; + const walletStub = { url: sinon.stub().returns(`/api/v2/${coin}/wallet/${walletId}`), coin: sinon.stub().returns(coin), subType: sinon.stub().returns('lightningSelfCustody'), + baseCoin: baseCoinStub, + coinSpecific: sinon.stub().returns({ + keys: [userAuthKey.id, nodeAuthKey.id], + }), bitgo: { - decrypt: sinon.stub().returns('decryptedPrivateKey'), + decrypt: sinon + .stub() + .resolves( + 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi' + ), put: putStub, }, - keys: ['userAuthKeyId', 'backupKeyId', 'nodeAuthKeyId'], } as any; const getWalletStub = sinon.stub().resolves(walletStub); const walletsStub = { get: getWalletStub } as any; - const keychainsGetStub = sinon.stub().resolves({ - id: 'keyId', - encryptedPrv: 'encryptedPrivateKey', - }); - - const keychainsStub = { - get: keychainsGetStub, - } as any; - const coinStub = { wallets: sinon.stub().returns(walletsStub), keychains: sinon.stub().returns(keychainsStub), @@ -308,7 +343,8 @@ describe('Express Wallet Update Typed Routes Tests', function () { }); res.status.should.equal(500); - res.body.should.have.property('error'); + res.body.error.should.equal('API error'); + putStub.should.have.been.calledOnce(); }); it('should return 400 when signerHost is missing', async function () { diff --git a/modules/express/test/unit/typedRoutes/generateShareTSS.ts b/modules/express/test/unit/typedRoutes/generateShareTSS.ts index 98e7dd29d7..5c45f50713 100644 --- a/modules/express/test/unit/typedRoutes/generateShareTSS.ts +++ b/modules/express/test/unit/typedRoutes/generateShareTSS.ts @@ -788,9 +788,8 @@ describe('GenerateShareTSS codec tests (External Signer Mode)', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Verify error response - runtime error returns 500 - assert.strictEqual(result.status, 500); - assert.ok(result.body); + assert.strictEqual(result.status, 400); + assert.strictEqual(result.body.error, 'Missing required field: walletId'); }); it('should fail when neither txRequest nor tssParams is provided', async function () { @@ -806,9 +805,8 @@ describe('GenerateShareTSS codec tests (External Signer Mode)', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Verify error response - assert.strictEqual(result.status, 500); - assert.ok(result.body); + assert.strictEqual(result.status, 400); + assert.strictEqual(result.body.error, 'Missing required field: walletId'); }); it('should fail when wallet passphrase environment variable is missing', async function () { diff --git a/modules/express/test/unit/typedRoutes/walletTxSignTSS.ts b/modules/express/test/unit/typedRoutes/walletTxSignTSS.ts index 990abf3adc..513a91caf1 100644 --- a/modules/express/test/unit/typedRoutes/walletTxSignTSS.ts +++ b/modules/express/test/unit/typedRoutes/walletTxSignTSS.ts @@ -25,6 +25,16 @@ describe('WalletTxSignTSS codec tests', function () { const coin = 'tbtc'; const walletId = '68c02f96aa757d9212bd1a536f123456'; + function assertErrorResponse( + result: { status: number; body: { error?: unknown; message?: unknown } }, + status: number, + message: string + ) { + assert.strictEqual(result.status, status); + assert.strictEqual(result.body.error, message); + assert.strictEqual(result.body.message, message); + } + const mockFullySignedResponse = { txHex: '0100000001c7dad3d9607a23c45a6c1c5ad7bce02acff71a0f21eb4a72a59d0c0e19402d0f0000000000ffffffff0180a21900000000001976a914c918e1b36f2c72b1aaef94dbb7f578a4b68b542788ac00000000', @@ -626,12 +636,10 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Verify error response - assert.strictEqual(result.status, 500); - result.body.should.have.property('error'); + assertErrorResponse(result, 500, 'Wallet not found'); }); - it('should handle signTransaction failure', async function () { + it('should handle wallet signing failure', async function () { const requestBody = { txPrebuild: { txHex: @@ -642,9 +650,8 @@ describe('WalletTxSignTSS codec tests', function () { apiVersion: 'lite' as const, }; - // Create mock wallet where signTransaction fails const mockWallet = { - signTransaction: sinon.stub().rejects(new Error('Invalid passphrase')), + ensureCleanSigSharesAndSignTransaction: sinon.stub().rejects(new Error('Invalid passphrase')), }; const walletsGetStub = sinon.stub().resolves(mockWallet); @@ -666,9 +673,8 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Verify error response - assert.strictEqual(result.status, 500); - result.body.should.have.property('error'); + assertErrorResponse(result, 500, 'Invalid passphrase'); + assert.strictEqual(mockWallet.ensureCleanSigSharesAndSignTransaction.calledOnce, true); }); it('should handle invalid coin error', async function () { @@ -693,24 +699,27 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Verify error response - assert.strictEqual(result.status, 500); - result.body.should.have.property('error'); + assertErrorResponse(result, 500, `Coin ${invalidCoin} is not supported`); }); }); describe('Invalid Request Body', function () { - it('should reject request with empty body', async function () { - // Make the request with empty body + it('should return the wallet signing error for an empty body', async function () { + const mockWallet = { + ensureCleanSigSharesAndSignTransaction: sinon.stub().rejects(new Error('Missing transaction data')), + }; + const walletsGetStub = sinon.stub().resolves(mockWallet); + const mockCoin = { wallets: sinon.stub().returns({ get: walletsGetStub }) }; + sinon.stub(BitGo.prototype, 'coin').returns(mockCoin as any); + const result = await agent .post(`/api/v2/${coin}/wallet/${walletId}/signtxtss`) .set('Authorization', 'Bearer test_access_token_12345') .set('Content-Type', 'application/json') .send({}); - // io-ts validation should fail - // Note: Depending on your route config, this might be 400 or 500 - assert.ok(result.status >= 400); + assertErrorResponse(result, 500, 'Missing transaction data'); + assert.strictEqual(mockWallet.ensureCleanSigSharesAndSignTransaction.calledOnce, true); }); it('should reject request with invalid txPrebuild type', async function () { @@ -727,8 +736,8 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Should fail validation - assert.ok(result.status >= 400); + assert.strictEqual(result.status, 400); + assert.match(result.body.error, /txPrebuild/); }); it('should handle request with invalid apiVersion', async function () { @@ -749,8 +758,8 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Should fail validation - assert.ok(result.status >= 400); + assert.strictEqual(result.status, 400); + assert.match(result.body.error, /apiVersion/); }); it('should handle request with malformed JSON', async function () { @@ -761,8 +770,7 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send('{ invalid json ]'); - // Should fail parsing - assert.ok(result.status >= 400); + assert.strictEqual(result.status, 400); }); }); @@ -775,7 +783,7 @@ describe('WalletTxSignTSS codec tests', function () { }; const mockWallet = { - signTransaction: sinon.stub().rejects(new Error('Missing transaction data')), + ensureCleanSigSharesAndSignTransaction: sinon.stub().rejects(new Error('Missing transaction data')), }; const walletsGetStub = sinon.stub().resolves(mockWallet); @@ -789,8 +797,8 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Should handle empty txPrebuild gracefully - assert.ok(result.status >= 400); + assertErrorResponse(result, 500, 'Missing transaction data'); + assert.strictEqual(mockWallet.ensureCleanSigSharesAndSignTransaction.calledOnce, true); }); it('should handle very long wallet ID', async function () { @@ -816,8 +824,7 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Should handle gracefully - assert.ok(result.status >= 400); + assertErrorResponse(result, 500, 'Invalid wallet ID'); }); it('should handle wallet ID with special characters', async function () { @@ -842,8 +849,7 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Should handle special characters safely - assert.ok(result.status >= 400); + assertErrorResponse(result, 500, 'Invalid wallet ID'); }); it('should handle txRequestId in both txPrebuild and body', async function () { @@ -860,7 +866,7 @@ describe('WalletTxSignTSS codec tests', function () { }; const mockWallet = { - signTransaction: sinon.stub().resolves(mockFullySignedResponse), + ensureCleanSigSharesAndSignTransaction: sinon.stub().resolves(mockFullySignedResponse), }; const walletsGetStub = sinon.stub().resolves(mockWallet); @@ -874,8 +880,12 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Should handle gracefully (accept or reject consistently) - assert.ok(result.status === 200 || result.status >= 400); + assert.strictEqual(result.status, 200); + assert.deepStrictEqual(result.body, mockFullySignedResponse); + assert.strictEqual(mockWallet.ensureCleanSigSharesAndSignTransaction.calledOnce, true); + const signingParams = mockWallet.ensureCleanSigSharesAndSignTransaction.firstCall.args[0]; + assert.strictEqual(signingParams.txRequestId, requestBody.txRequestId); + assert.strictEqual(signingParams.txPrebuild.txRequestId, requestBody.txPrebuild.txRequestId); }); it('should handle missing walletPassphrase (edge case for TSS)', async function () { @@ -890,7 +900,7 @@ describe('WalletTxSignTSS codec tests', function () { }; const mockWallet = { - signTransaction: sinon.stub().rejects(new Error('Wallet passphrase required')), + ensureCleanSigSharesAndSignTransaction: sinon.stub().rejects(new Error('Wallet passphrase required')), }; const walletsGetStub = sinon.stub().resolves(mockWallet); @@ -904,8 +914,8 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Should fail if passphrase is required - assert.ok(result.status >= 400); + assertErrorResponse(result, 500, 'Wallet passphrase required'); + assert.strictEqual(mockWallet.ensureCleanSigSharesAndSignTransaction.calledOnce, true); }); }); @@ -925,7 +935,7 @@ describe('WalletTxSignTSS codec tests', function () { const invalidResponse = {}; const mockWallet = { - signTransaction: sinon.stub().resolves(invalidResponse), + ensureCleanSigSharesAndSignTransaction: sinon.stub().resolves(invalidResponse), }; const walletsGetStub = sinon.stub().resolves(mockWallet); @@ -939,16 +949,11 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Even if SDK returns 200, response should fail codec validation - // This depends on where validation happens - assert.ok(result.status === 200 || result.status >= 400); - - // If status is 200 but response is invalid, codec validation should catch it - if (result.status === 200) { - assert.throws(() => { - assertDecode(FullySignedTransactionResponse, result.body); - }); - } + assert.strictEqual(result.status, 200); + assert.strictEqual(mockWallet.ensureCleanSigSharesAndSignTransaction.calledOnce, true); + assert.throws(() => { + assertDecode(FullySignedTransactionResponse, result.body); + }); }); it('should reject response with wrong type in txHex field', async function () { @@ -968,7 +973,7 @@ describe('WalletTxSignTSS codec tests', function () { }; const mockWallet = { - signTransaction: sinon.stub().resolves(invalidResponse), + ensureCleanSigSharesAndSignTransaction: sinon.stub().resolves(invalidResponse), }; const walletsGetStub = sinon.stub().resolves(mockWallet); @@ -982,12 +987,11 @@ describe('WalletTxSignTSS codec tests', function () { .set('Content-Type', 'application/json') .send(requestBody); - // Response codec validation should catch type mismatch - if (result.status === 200) { - assert.throws(() => { - assertDecode(FullySignedTransactionResponse, result.body); - }); - } + assert.strictEqual(result.status, 200); + assert.strictEqual(mockWallet.ensureCleanSigSharesAndSignTransaction.calledOnce, true); + assert.throws(() => { + assertDecode(FullySignedTransactionResponse, result.body); + }); }); }); }); diff --git a/package.json b/package.json index b386c35fb2..51069d79c3 100644 --- a/package.json +++ b/package.json @@ -291,8 +291,8 @@ ], "scripts": { "postinstall": "test -n \"$NOYARNPOSTINSTALL\" || lerna run build --stream", - "lint": "lerna run lint --stream", - "lint-changed": "lerna run lint --since origin/${GITHUB_REPO_BRANCH:-master}..HEAD --stream", + "lint": "lerna run lint --stream --concurrency 2", + "lint-changed": "lerna run lint --since origin/${GITHUB_REPO_BRANCH:-master}..HEAD --stream --concurrency 2", "unit-test-changed": "lerna run unit-test --since $(git merge-base HEAD~ origin/${GITHUB_REPO_BRANCH:-master}) --private --stream", "browser-tests": "lerna run --scope bitgo compile-test && lerna run --scope bitgo browser-test && lerna run --scope @bitgo/web-demo test", "gen-coverage-changed": "lerna run gen-coverage --since origin/${GITHUB_REPO_BRANCH:-master}..HEAD --stream --parallel", @@ -305,8 +305,8 @@ "artifacts": "lerna run upload-artifacts --stream --parallel", "upload-docs": "lerna run upload-docs --stream --parallel", "gen-docs": "lerna run gen-docs --stream --parallel", - "check-fmt-changed": "lerna run check-fmt --since origin/${GITHUB_REPO_BRANCH:-master}..HEAD --stream --parallel", - "check-fmt": "lerna run check-fmt --stream --parallel", + "check-fmt-changed": "lerna run check-fmt --since origin/${GITHUB_REPO_BRANCH:-master}..HEAD --stream --concurrency 8", + "check-fmt": "lerna run check-fmt --stream --concurrency 8", "check-commits": "yarn commitlint --from=origin/${GITHUB_REPO_BRANCH:-master} -V", "check-deps": "tsx ./scripts/check-package-dependencies.ts", "check-versions": "node ./check-package-versions.js", diff --git a/scripts/sdk-coin-generator-v2/core/generator.ts b/scripts/sdk-coin-generator-v2/core/generator.ts index d758c3c1e3..bb4c3ee1b4 100644 --- a/scripts/sdk-coin-generator-v2/core/generator.ts +++ b/scripts/sdk-coin-generator-v2/core/generator.ts @@ -103,7 +103,11 @@ export class Generator { if (config.symbol.includes('..') || config.symbol.includes('/') || config.symbol.includes('\\')) { throw new Error('Symbol cannot contain path traversal characters'); } - if (config.testnetSymbol.includes('..') || config.testnetSymbol.includes('/') || config.testnetSymbol.includes('\\')) { + if ( + config.testnetSymbol.includes('..') || + config.testnetSymbol.includes('/') || + config.testnetSymbol.includes('\\') + ) { throw new Error('Testnet symbol cannot contain path traversal characters'); } @@ -350,9 +354,6 @@ export class Generator { 'Run tests: yarn test', ]; - p.note( - nextSteps.map((step, i) => `${i + 1}. ${step}`).join('\n'), - '📋 Next steps' - ); + p.note(nextSteps.map((step, i) => `${i + 1}. ${step}`).join('\n'), '📋 Next steps'); } } diff --git a/yarn.lock b/yarn.lock index 5a4634ac80..c400f9cff8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17536,7 +17536,7 @@ qrcode@^1.5.1: pngjs "^5.0.0" yargs "^15.3.1" -qs@6.13.0, qs@6.14.0, qs@6.16.0, qs@^6.11.0, qs@^6.11.2, qs@^6.12.3, qs@^6.5.1, qs@~6.15.1: +qs@6.13.0, qs@6.14.0, qs@6.16.0, qs@^6.11.0, qs@^6.11.2, qs@^6.12.3, qs@^6.5.1, qs@~6.16.0: version "6.16.0" resolved "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz#c22c723a28a920f3aacdce8289fabd43eccb79fd" integrity sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA== @@ -21184,9 +21184,9 @@ yargs@^15.0.2, yargs@^15.3.1: yargs-parser "^18.1.2" yargs@^16.1.1, yargs@^16.2.0: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + version "16.2.2" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz" + integrity sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w== dependencies: cliui "^7.0.2" escalade "^3.1.1"