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
66 changes: 66 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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@33f19f2eb8cbc49a61567a0781f3bc37bf2a32aa # support gualGrpxProxyPort
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
Expand Down
6 changes: 6 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
37 changes: 37 additions & 0 deletions src/Executable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 12 additions & 0 deletions src/Status.js
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,8 @@ export default class Status {
return "GRPC_WEB_PROXY_NOT_SUPPORTED";
case Status.NftTransfersOnlyAllowedForNonFungibleUnique:
return "NFT_TRANSFERS_ONLY_ALLOWED_FOR_NON_FUNGIBLE_UNIQUE";
case Status.NodeAccountHasZeroBalance:
return "NODE_ACCOUNT_HAS_ZERO_BALANCE";
default:
return `UNKNOWN (${this._code})`;
}
Expand Down Expand Up @@ -1472,6 +1474,8 @@ export default class Status {
return Status.GrpcWebProxyNotSupported;
case 400:
return Status.NftTransfersOnlyAllowedForNonFungibleUnique;
case 526:
return Status.NodeAccountHasZeroBalance;
default:
throw new Error(
`(BUG) Status.fromCode() does not handle code: ${code}`,
Expand Down Expand Up @@ -3351,3 +3355,11 @@ Status.GrpcWebProxyNotSupported = new Status(399);
* An NFT transfers list referenced a token type other than NON_FUNGIBLE_UNIQUE.
*/
Status.NftTransfersOnlyAllowedForNonFungibleUnique = new Status(400);

/**
* This operation cannot be completed because the target
* account has a zero balance.<br/>
* Node accounts require a positive balance. The transaction may be
* resubmitted once the account has been funded.
*/
Status.NodeAccountHasZeroBalance = new Status(526);
4 changes: 3 additions & 1 deletion src/channel/WebChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")
);
}

Expand Down
7 changes: 1 addition & 6 deletions src/client/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/transaction/Transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
11 changes: 11 additions & 0 deletions test/integration/dual-mode/NodeConstants.js
Original file line number Diff line number Diff line change
@@ -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 };
Loading
Loading