diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36285d4c3..e942576cd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -108,7 +108,7 @@ jobs: - name: Prepare Hiero Solo id: solo - uses: hiero-ledger/hiero-solo-action@b76850c1ac44466900f8e7412b309c3aa0f539c1 # v0.12 + uses: hiero-ledger/hiero-solo-action@fbca3e7a99ce9aa8a250563a81187abe115e0dad # v0.16 with: installMirrorNode: true hieroVersion: v0.65.0 @@ -169,6 +169,72 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true + dab-tests: + name: DAB Tests using Node ${{ matrix.node }} + runs-on: hiero-client-sdk-linux-medium + strategy: + matrix: + node: [ "22" ] + + steps: + - name: Harden Runner + uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 + with: + egress-policy: audit + + - name: Checkout Code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + submodules: recursive + + - name: Install Task + uses: arduino/setup-task@b91d5d2c96a56797b48ac1e0e89220bf64044611 # v2.0.0 + with: + version: 3.35.1 + + - name: Install PNPM + uses: step-security/action-setup@3d419c73e38e670dbffe349ffff26dd13c164640 # v4.2.0 + with: + version: 9.15.5 + + - name: Setup Node + uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: ${{ matrix.node }} + cache: pnpm + + - name: Build @hashgraph/sdk + id: build-sdk + run: task build + + - name: Install Playwright Dependencies + id: playwright-deps + if: ${{ steps.build-sdk.conclusion == 'success' && !cancelled() && always() }} + run: | + sudo npx playwright install-deps + npx playwright install + + - name: Prepare Hiero Solo + id: solo + uses: hiero-ledger/hiero-solo-action@fbca3e7a99ce9aa8a250563a81187abe115e0dad # 0.16.0 + with: + installMirrorNode: true + hieroVersion: v0.68.1-rc.1 + mirrorNodeVersion: v0.142.0 + grpcProxyPort: 8080 + dualMode: true + dualModeGrpcProxyPort: 8081 + + - name: Set Operator Account + run: | + echo "OPERATOR_KEY=${{ steps.solo.outputs.ed25519PrivateKey }}" >> $GITHUB_ENV + echo "OPERATOR_ID=${{ steps.solo.outputs.ed25519AccountId }}" >> $GITHUB_ENV + echo "HEDERA_NETWORK=local-node" >> $GITHUB_ENV + + - name: Run DAB Integration Tests + if: ${{ steps.build-sdk.conclusion == 'success' && !cancelled() && always() }} + run: task test:integration:dual-mode + examples: name: Run examples using Node ${{ matrix.node }} runs-on: hiero-client-sdk-linux-medium @@ -208,7 +274,7 @@ jobs: - name: Prepare Hiero Solo id: solo - uses: hiero-ledger/hiero-solo-action@b76850c1ac44466900f8e7412b309c3aa0f539c1 # v0.12 + uses: hiero-ledger/hiero-solo-action@fbca3e7a99ce9aa8a250563a81187abe115e0dad # v0.16 with: installMirrorNode: true hieroVersion: v0.65.0 diff --git a/Taskfile.yml b/Taskfile.yml index 313e07866..bd8ab5ce0 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -168,6 +168,12 @@ tasks: cmds: - npx vitest --coverage --config=test/vitest-node-integration.config.ts - npx vitest --coverage --config=test/vitest-browser-integration.config.ts + + "test:integration:dual-mode": + cmds: + - npx vitest --poolOptions.threads.singleThread --config=test/vitest-node-integration-dual-mode.config.ts + - npx vitest --poolOptions.threads.singleThread --config=test/vitest-browser-integration-dual-mode.config.ts + "update:proto": deps: - "proto:update" diff --git a/src/Executable.js b/src/Executable.js index f7ea41d3d..bd32bca6c 100644 --- a/src/Executable.js +++ b/src/Executable.js @@ -800,6 +800,43 @@ export default class Executable { // Determine by the executing state what we should do switch (shouldRetry) { case ExecutionState.Retry: + // Special handling for INVALID_NODE_ACCOUNT: mark node as unusable + // and update network to get latest node account IDs + if (status === Status.InvalidNodeAccount) { + if (this._logger) { + this._logger.debug( + `[${this._getLogId()}] node with accountId: ${node.accountId.toString()} and proxy IP: ${node.address.toString()} has invalid node account ID, marking as unhealthy and updating network`, + ); + } + + // Mark the node as unusable by increasing its backoff and removing it from the healthy nodes list + client._network.increaseBackoff(node); + + // Initiate addressbook query and update the client's network + // This will make the SDK client have the latest node account IDs for subsequent transactions + try { + if (client.mirrorNetwork.length > 0) { + await client.updateNetwork(); + } else { + if (this._logger) { + this._logger.warn( + "Cannot update address book: no mirror network configured. Retrying with existing network configuration.", + ); + } + } + } catch (error) { + if (this._logger) { + const errorMessage = + error instanceof Error + ? error.message + : String(error); + this._logger.trace( + `failed to update client address book after INVALID_NODE_ACCOUNT_ID: ${errorMessage}`, + ); + } + } + } + await delayForAttempt( isLocalNode, attempt, diff --git a/src/channel/WebChannel.js b/src/channel/WebChannel.js index dae35b50f..016e17933 100644 --- a/src/channel/WebChannel.js +++ b/src/channel/WebChannel.js @@ -50,7 +50,9 @@ export default class WebChannel extends Channel { */ _shouldUseHttps(address) { return !( - address.includes("localhost") || address.includes("127.0.0.1") + address.includes("localhost") || + address.includes("127.0.0.1") || + address.includes(".cluster.local") ); } diff --git a/src/client/Network.js b/src/client/Network.js index ffe36e193..dea535cc4 100644 --- a/src/client/Network.js +++ b/src/client/Network.js @@ -284,12 +284,7 @@ export default class Network extends ManagedNetwork { if (this._maxNodesPerTransaction > 0) { return this._maxNodesPerTransaction; } - // ultimately it does not matter if we round up or down - // if we round up, we will eventually take one more healthy node for execution - // and we would hit the 'nodes.length == count' check in _getNumberOfMostHealthyNodes() less often - return this._nodes.length <= 9 - ? this._nodes.length - : Math.floor((this._nodes.length + 3 - 1) / 3); + return this._nodes.length; } /** diff --git a/src/transaction/Transaction.js b/src/transaction/Transaction.js index 4717ef1dc..ba5e2e1ca 100644 --- a/src/transaction/Transaction.js +++ b/src/transaction/Transaction.js @@ -1966,6 +1966,7 @@ export default class Transaction extends Executable { case Status.Unknown: case Status.PlatformTransactionNotCreated: case Status.PlatformNotActive: + case Status.InvalidNodeAccount: return [status, ExecutionState.Retry]; case Status.Ok: return [status, ExecutionState.Finished]; diff --git a/test/integration/dual-mode/NodeConstants.js b/test/integration/dual-mode/NodeConstants.js new file mode 100644 index 000000000..0e2c13cb5 --- /dev/null +++ b/test/integration/dual-mode/NodeConstants.js @@ -0,0 +1,11 @@ +import { SOLO_NAMESPACE } from "./SharedConstants.js"; +const node2Address = `network-node2-svc.${SOLO_NAMESPACE}.svc.cluster.local:50211`; +const node2PortToReplace = 51211; +const network = { + "127.0.0.1:50211": "0.0.3", + "127.0.0.1:51211": "0.0.4", +}; + +const mirrorNetwork = ["localhost:5600"]; + +export { network, mirrorNetwork, node2Address, node2PortToReplace }; diff --git a/test/integration/dual-mode/NodeUpdateIntegrationTest.js b/test/integration/dual-mode/NodeUpdateIntegrationTest.js new file mode 100644 index 000000000..c4451482b --- /dev/null +++ b/test/integration/dual-mode/NodeUpdateIntegrationTest.js @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Apache-2.0 + +import { + AccountId, + AccountCreateTransaction, + AccountDeleteTransaction, + Hbar, + NodeUpdateTransaction, + PrivateKey, + Status, + ServiceEndpoint, +} from "../../../src/exports.js"; +import { Client } from "../../../src/index.js"; +import { + mirrorNetwork, + node2Address, + node2PortToReplace, + network, +} from "./NodeConstants.js"; + +const restoreOriginalGrpcWebProxyEndpoint = async (client) => { + const response = await new NodeUpdateTransaction() + .setNodeId(1) + .setNodeAccountIds([AccountId.fromString("0.0.3")]) + .setGrpcWebProxyEndpoint( + new ServiceEndpoint() + .setDomainName(node2Address.split(":")[0]) + .setPort(Number(node2Address.split(":")[1])), + ) + .execute(client); + const receipt = await response.getReceipt(client); + expect(receipt.status).to.equal(Status.Success); +}; + +describe("Node Update Integration Tests", function () { + let client; + let operatorAccountId; + let operatorKey; + + beforeEach(function () { + // Initialize client with integration network + client = Client.forNetwork(network).setMirrorNetwork(mirrorNetwork); + + // Set the operator to be account 0.0.2 + operatorAccountId = AccountId.fromString("0.0.2"); + operatorKey = PrivateKey.fromStringDer( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137", + ); + + client.setOperator(operatorAccountId, operatorKey); + }); + + afterEach(function () { + if (client) { + client.close(); + } + }); + + it("should execute node update transaction", async function () { + const response = await new NodeUpdateTransaction() + .setNodeId(1) + .setNodeAccountIds([AccountId.fromString("0.0.3")]) + .setDescription("testUpdated") + .setDeclineReward(true) + .setGrpcWebProxyEndpoint( + new ServiceEndpoint() + .setDomainName("testWebUpdatedsdfsdfsdfsdf.com") + .setPort(123456), + ) + .execute(client); + + const receipt = await response.getReceipt(client); + expect(receipt.status).to.equal(Status.Success); + }); + + it("should delete grpc web proxy endpoint", async function () { + const response = await new NodeUpdateTransaction() + .setNodeId(1) + .setNodeAccountIds([AccountId.fromString("0.0.3")]) + .deleteGrpcWebProxyEndpoint() + .execute(client); + + const receipt = await response.getReceipt(client); + expect(receipt.status).to.equal(Status.Success); + + // Restore the original grpc web proxy endpoint + await restoreOriginalGrpcWebProxyEndpoint(client); + }); + + it("should change node account ID and revert back", async function () { + // Change node account ID from 0.0.3 to 0.0.2 + const response1 = await new NodeUpdateTransaction() + .setNodeId(0) + .setNodeAccountIds([AccountId.fromString("0.0.4")]) + .setAccountId(AccountId.fromString("0.0.2")) + .execute(client); + + const receipt1 = await response1.getReceipt(client); + expect(receipt1.status).to.equal(Status.Success); + + // Revert the ID back to 0.0.3 + const response2 = await new NodeUpdateTransaction() + .setNodeId(0) + .setNodeAccountIds([AccountId.fromString("0.0.4")]) + .setAccountId(AccountId.fromString("0.0.3")) + .execute(client); + + const receipt2 = await response2.getReceipt(client); + expect(receipt2.status).to.equal(Status.Success); + }); + + it("should fail with INVALID_SIGNATURE when updating without admin key", async function () { + // Create a new account to be the operator + const newOperatorKey = PrivateKey.generateED25519(); + const createResp = await new AccountCreateTransaction() + .setKey(newOperatorKey.publicKey) + .setInitialBalance(new Hbar(2)) + .execute(client); + const createReceipt = await createResp.getReceipt(client); + const newOperator = createReceipt.accountId; + + // Set the new account as operator + client.setOperator(newOperator, newOperatorKey); + + try { + // Try to update node account ID without admin key signature + const response = await new NodeUpdateTransaction() + .setNodeId(0) + .setDescription("testUpdated") + .setAccountId(AccountId.fromString("0.0.50")) + .execute(client); + + await response.getReceipt(client); + expect.fail("Should have thrown INVALID_SIGNATURE error"); + } catch (error) { + expect(error.message).to.include("INVALID_SIGNATURE"); + } + }); + + it("should change node account ID to the same account", async function () { + const response = await new NodeUpdateTransaction() + .setNodeId(1) + .setNodeAccountIds([AccountId.fromString("0.0.3")]) + .setAccountId(AccountId.fromString("0.0.4")) + .execute(client); + + const receipt = await response.getReceipt(client); + expect(receipt.status).to.equal(Status.Success); + }); + + it("should fail when changing to non-existent account ID", async function () { + try { + const response = await new NodeUpdateTransaction() + .setNodeId(0) + .setDescription("testUpdated") + .setAccountId(AccountId.fromString("0.0.999999999")) + .execute(client); + + await response.getReceipt(client); + expect.fail("Should have thrown INVALID_SIGNATURE error"); + } catch (error) { + expect(error.message).to.include("INVALID_SIGNATURE"); + } + }); + + it("should fail when changing node account ID without account key", async function () { + // Create a new account + const newKey = PrivateKey.generateED25519(); + const createResp = await new AccountCreateTransaction() + .setKey(newKey.publicKey) + .setInitialBalance(new Hbar(2)) + .execute(client); + + const createReceipt = await createResp.getReceipt(client); + const newNodeAccountId = createReceipt.accountId; + + try { + // Try to set node account ID to new account without signing with new account's key + const response = await new NodeUpdateTransaction() + .setNodeId(0) + .setDescription("testUpdated") + .setAccountId(newNodeAccountId) + .execute(client); + + await response.getReceipt(client); + expect.fail("Should have thrown INVALID_SIGNATURE error"); + } catch (error) { + expect(error.message).to.include("INVALID_SIGNATURE"); + } + }); + + it("should fail when changing to deleted account ID", async function () { + // Create a new account + const newAccountKey = PrivateKey.generateED25519(); + const createResp = await new AccountCreateTransaction() + .setKey(newAccountKey.publicKey) + .execute(client); + + const createReceipt = await createResp.getReceipt(client); + const newAccount = createReceipt.accountId; + + // Delete the account + const deleteResp = await ( + await new AccountDeleteTransaction() + .setAccountId(newAccount) + .setTransferAccountId(client.operatorAccountId) + .freezeWith(client) + .sign(newAccountKey) + ).execute(client); + + await deleteResp.getReceipt(client); + + try { + // Try to set node account ID to deleted account + const updateResp = await ( + await new NodeUpdateTransaction() + .setNodeId(0) + .setDescription("testUpdated") + .setAccountId(newAccount) + .freezeWith(client) + .sign(newAccountKey) + ).execute(client); + + await updateResp.getReceipt(client); + expect.fail("Should have thrown ACCOUNT_DELETED error"); + } catch (error) { + expect(error.message).to.include("ACCOUNT_DELETED"); + } + }); + + it("should fail when new node account has zero balance", async function () { + // Create a new account with zero balance + const newAccountKey = PrivateKey.generateED25519(); + const createResp = await new AccountCreateTransaction() + .setKey(newAccountKey.publicKey) + .execute(client); + + const createReceipt = await createResp.getReceipt(client); + const newAccount = createReceipt.accountId; + + try { + // Try to set node account ID to account with zero balance + const updateResp = await ( + await new NodeUpdateTransaction() + .setNodeId(0) + .setNodeAccountIds([AccountId.fromString("0.0.4")]) + .setDescription("testUpdated") + .setAccountId(newAccount) + .freezeWith(client) + .sign(newAccountKey) + ).execute(client); + + await updateResp.getReceipt(client); + expect.fail( + "Should have thrown NODE_ACCOUNT_HAS_ZERO_BALANCE error", + ); + } catch (error) { + expect(error.message).to.include("NODE_ACCOUNT_HAS_ZERO_BALANCE"); + } + }); + + it("should update addressbook and retry after node account ID change", async function () { + // Create the account that will be the new node account ID + const newAccountKey = PrivateKey.generateED25519(); + const createResp = await new AccountCreateTransaction() + .setKey(newAccountKey.publicKey) + .setInitialBalance(new Hbar(1)) + .execute(client); + + const createReceipt = await createResp.getReceipt(client); + const newNodeAccountID = createReceipt.accountId; + + // Update node account ID (0.0.8 -> newNodeAccountID) + const updateResp = await ( + await new NodeUpdateTransaction() + .setNodeId(1) + .setNodeAccountIds([AccountId.fromString("0.0.3")]) + .setAccountId(newNodeAccountID) + .freezeWith(client) + .sign(newAccountKey) + ).execute(client); + + await updateResp.getReceipt(client); + + // Wait for mirror node to import data + await new Promise((resolve) => setTimeout(resolve, 5000)); + + const anotherNewKey = PrivateKey.generateED25519(); + // Submit to the updated node - should trigger addressbook refresh + const testResp = await new AccountCreateTransaction() + .setKey(anotherNewKey.publicKey) + .setNodeAccountIds([ + AccountId.fromString("0.0.4"), + AccountId.fromString("0.0.3"), + ]) + .execute(client); + const testReceipt = await testResp.getReceipt(client); + expect(testReceipt.status).to.equal(Status.Success); + // Verify address book has been updated + const network = client.network; + const hasNewNodeAccount = Object.values(network).some( + (accountId) => accountId.toString() === newNodeAccountID.toString(), + ); + expect(hasNewNodeAccount).to.be.true; + + // Find the address of the newly added node + const newNodeAddress = Object.entries(network).find( + ([, accountId]) => + accountId.toString() === newNodeAccountID.toString(), + )?.[0]; + + // Assert the address matches the expected value + expect(newNodeAddress).to.equal(node2Address); + + // This is not an ideal workaround - reconstruct the network state + // because the mirror node returns a different address than expected + if (newNodeAddress === node2Address) { + const oldNetworkState = { ...network }; + delete oldNetworkState[newNodeAddress]; + const newNetworkState = { + ...oldNetworkState, + [node2Address.replace( + node2Address.split(":")[1], + node2PortToReplace, + )]: newNodeAccountID, + }; + client.setNetwork(newNetworkState); + } + + // This transaction should succeed with the new node account ID + const finalResp = await new AccountCreateTransaction() + .setKey(anotherNewKey.publicKey) + .setNodeAccountIds([newNodeAccountID]) + .execute(client); + + const finalReceipt = await finalResp.getReceipt(client); + expect(finalReceipt.status).to.equal(Status.Success); + + // Revert the node account ID + const revertResp = await new NodeUpdateTransaction() + .setNodeId(1) + .setNodeAccountIds([AccountId.fromString("0.0.3")]) + .setAccountId(AccountId.fromString("0.0.4")) + .execute(client); + + await revertResp.getReceipt(client); + }); + + it("should handle node account ID change without mirror node setup", async function () { + // Create a client without mirror network + const networkClient = Client.forNetwork(network); + + networkClient.setOperator(operatorAccountId, operatorKey); + + try { + // Create the account that will be the new node account ID + const newAccountKey = PrivateKey.generateED25519(); + const createResp = await new AccountCreateTransaction() + .setKey(newAccountKey.publicKey) + .setNodeAccountIds([ + AccountId.fromString("0.0.3"), + AccountId.fromString("0.0.4"), + ]) + .setInitialBalance(new Hbar(1)) + .execute(networkClient); + + const createReceipt = await createResp.getReceipt(networkClient); + const newNodeAccountID = createReceipt.accountId; + + // Update node account ID + const updateResp = await ( + await new NodeUpdateTransaction() + .setNodeId(0) + .setAccountId(newNodeAccountID) + .freezeWith(networkClient) + .sign(newAccountKey) + ).execute(networkClient); + + await updateResp.getReceipt(networkClient); + + // Wait for changes to propagate + await new Promise((resolve) => setTimeout(resolve, 5000)); + + const anotherNewKey = PrivateKey.generateED25519(); + + // Submit transaction - should retry since no mirror node to update addressbook + const testResp = await new AccountCreateTransaction() + .setKey(anotherNewKey.publicKey) + .setNodeAccountIds([ + AccountId.fromString("0.0.3"), + AccountId.fromString("0.0.4"), + ]) + .execute(networkClient); + + const testReceipt = await testResp.getReceipt(networkClient); + expect(testReceipt.status).to.equal(Status.Success); + + // Verify address book has NOT been updated (no mirror node) + const network = networkClient.network; + const node1 = Object.entries(network).find( + ([, accountId]) => accountId.toString() === "0.0.3", + ); + const node2 = Object.entries(network).find( + ([, accountId]) => accountId.toString() === "0.0.4", + ); + + expect(node1).to.not.be.undefined; + expect(node2).to.not.be.undefined; + + // This transaction should succeed with retries + const finalResp = await new AccountCreateTransaction() + .setKey(anotherNewKey.publicKey) + .execute(networkClient); + + const finalReceipt = await finalResp.getReceipt(networkClient); + expect(finalReceipt.status).to.equal(Status.Success); + + // Revert the node account ID + const revertResp = await new NodeUpdateTransaction() + .setNodeId(0) + .setNodeAccountIds([AccountId.fromString("0.0.4")]) + .setAccountId(AccountId.fromString("0.0.3")) + .execute(networkClient); + + await revertResp.getReceipt(networkClient); + } finally { + networkClient.close(); + } + }); +}); diff --git a/test/integration/dual-mode/SharedConstants.js b/test/integration/dual-mode/SharedConstants.js new file mode 100644 index 000000000..8499329fe --- /dev/null +++ b/test/integration/dual-mode/SharedConstants.js @@ -0,0 +1,3 @@ +// This should be exactly the same as the namespace in the Hiero Solo action +// otherwise the DAB tests will fail, as domain names for nodes will not be resolved +export const SOLO_NAMESPACE = "solo"; diff --git a/test/integration/dual-mode/WebConstants.js b/test/integration/dual-mode/WebConstants.js new file mode 100644 index 000000000..c990a06d9 --- /dev/null +++ b/test/integration/dual-mode/WebConstants.js @@ -0,0 +1,11 @@ +import { SOLO_NAMESPACE } from "./SharedConstants.js"; +const node2Address = `envoy-proxy-node2-svc.${SOLO_NAMESPACE}.svc.cluster.local:8080`; +const node2PortToReplace = 8081; +const network = { + "localhost:8080": "0.0.3", + "localhost:8081": "0.0.4", +}; + +const mirrorNetwork = ["localhost:5551"]; + +export { network, mirrorNetwork, node2Address, node2PortToReplace }; diff --git a/test/vitest-browser-integration-dual-mode.config.ts b/test/vitest-browser-integration-dual-mode.config.ts new file mode 100644 index 000000000..aa3db1b3c --- /dev/null +++ b/test/vitest-browser-integration-dual-mode.config.ts @@ -0,0 +1,102 @@ +import { defineConfig } from "vitest/config"; + +import path from "path"; +import fs from "fs"; + +const pkg = JSON.parse( + fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf-8"), +); + +/** @type {import("vitest").UserConfig} */ +export default defineConfig({ + test: { + environment: "jsdom", + watch: false, + globals: true, + + browser: { + screenshotFailures: false, + headless: true, + provider: "playwright", + enabled: true, + instances: [{ browser: "chromium" }], + }, + include: ["test/integration/dual-mode/**/*.js"], + exclude: [ + "test/integration/client/*", + "test/integration/resources/*", + "test/integration/utils/*", + "test/integration/dual-mode/NodeConstants.js", + "test/integration/dual-mode/WebConstants.js", + "test/integration/dual-mode/SharedConstants.js", + ], + hookTimeout: 120000, + testTimeout: 120000, + coverage: { + include: ["src/**/*.js"], + provider: "v8", + reporter: ["text-summary", "lcov"], + reportsDirectory: "./coverage", + }, + }, + define: { + __SDK_VERSION__: JSON.stringify(pkg.version), + "import.meta.env.VITE_OPERATOR_ID": JSON.stringify( + process.env.OPERATOR_ID || "", + ), + "import.meta.env.VITE_OPERATOR_KEY": JSON.stringify( + process.env.OPERATOR_KEY || "", + ), + "import.meta.env.VITE_HEDERA_NETWORK": JSON.stringify( + process.env.HEDERA_NETWORK || "", + ), + }, + resolve: { + alias: { + // redirect src/ to src/browser + // note that this is NOT needed when consuming this package as the browser field in package.json + // will take care of this + "../../src/index.js": "../../src/browser.js", + "../src/index.js": "../src/browser.js", + // TODO: extract `encoding/hex.js` etc into a variable and call a function to generate + // all the prefixes. + "../../../src/encoding/hex.js": + "../../../src/encoding/hex.browser.js", + "../../src/encoding/hex.js": "../../src/encoding/hex.browser.js", + "../src/encoding/hex.js": "../src/encoding/hex.browser.js", + "src/encoding/hex.js": "src/encoding/hex.browser.js", + "../encoding/hex.js": "../encoding/hex.browser.js", + "./encoding/hex.js": "./encoding/hex.browser.js", + "../src/encoding/utf8.js": "../src/encoding/utf8.browser.js", + "../../src/encoding/utf8.js": "../../src/encoding/utf8.browser.js", + "../encoding/utf8.js": "../encoding/utf8.browser.js", + "../src/cryptography/sha384.js": + "../src/cryptography/sha384.browser.js", + "../cryptography/sha384.js": "../cryptography/sha384.browser.js", + "./client/NodeIntegrationTestEnv.js": + "./client/WebIntegrationTestEnv.js", + "../integration/client/NodeIntegrationTestEnv.js": + "../integration/client/WebIntegrationTestEnv.js", + "../../src/client/NodeClient.js": "../../src/client/WebClient.js", + "../../../src/client/NodeClient.js": + "../../../src/client/WebClient.js", + "./client/NodeClient.js": "./client/WebClient.js", + "../../../src/LocalProvider.js": "../../../src/LocalProviderWeb.js", + "../../src/LocalProvider.js": "../../src/LocalProviderWeb.js", + "../src/LocalProvider.js": "../src/LocalProviderWeb.js", + "src/LocalProvider.js": "src/LocalProviderWeb.js", + "./NodeConstants.js": "./WebConstants.js", + // Add more comprehensive aliases for NodeIntegrationTestEnv + "NodeIntegrationTestEnv.js": "WebIntegrationTestEnv.js", + "./client/NodeIntegrationTestEnv": "./client/WebIntegrationTestEnv", + "../client/NodeIntegrationTestEnv": + "../client/WebIntegrationTestEnv", + "../../client/NodeIntegrationTestEnv": + "../../client/WebIntegrationTestEnv", + "../../../src/client/NodeIntegrationTestEnv": + "../../../src/client/WebIntegrationTestEnv", + // Add aliases for NodeClient + "NodeClient.js": "WebClient.js", + }, + }, +}); diff --git a/test/vitest-browser-integration.config.ts b/test/vitest-browser-integration.config.ts index d51339950..2c9b1e99e 100644 --- a/test/vitest-browser-integration.config.ts +++ b/test/vitest-browser-integration.config.ts @@ -32,6 +32,7 @@ export default defineConfig({ "test/integration/TopicMessageIntegrationTest.js", "test/integration/TokenNftsUpdateTransactionIntegrationTest.js", "test/integration/ClientIntegrationTest.js", + "test/integration/dual-mode/**/*.js", ], hookTimeout: 120000, testTimeout: 120000, diff --git a/test/vitest-node-integration-dual-mode.config.ts b/test/vitest-node-integration-dual-mode.config.ts new file mode 100644 index 000000000..931b17179 --- /dev/null +++ b/test/vitest-node-integration-dual-mode.config.ts @@ -0,0 +1,37 @@ +import { defineConfig } from "vitest/config"; + +import path from "path"; +import fs from "fs"; + +const pkg = JSON.parse( + fs.readFileSync(path.resolve(__dirname, "../package.json"), "utf-8"), +); + +/** @type {import("vitest").UserConfig} */ +export default defineConfig({ + test: { + watch: false, + globals: true, + environment: "node", + include: ["test/integration/dual-mode/**/*.js"], + exclude: [ + "test/integration/client/*", + "test/integration/resources/*", + "test/integration/utils/*", + "test/integration/dual-mode/NodeConstants.js", + "test/integration/dual-mode/WebConstants.js", + "test/integration/dual-mode/SharedConstants.js", + ], + hookTimeout: 120000, + testTimeout: 120000, + coverage: { + include: ["src/**/*.js"], + provider: "v8", + reporter: ["text-summary", "lcov"], + reportsDirectory: "./coverage", + }, + }, + define: { + __SDK_VERSION__: JSON.stringify(pkg.version), + }, +}); diff --git a/test/vitest-node-integration.config.ts b/test/vitest-node-integration.config.ts index 1478ec128..a3ad4420f 100644 --- a/test/vitest-node-integration.config.ts +++ b/test/vitest-node-integration.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ "test/integration/resources/*", "test/integration/utils/*", "test/integration/contents.js", + "test/integration/dual-mode/**/*.js", ], hookTimeout: 120000, testTimeout: 120000,