Skip to content

feat(sdk): add round-robin test example for Go and TS SDKs#789

Merged
philanton merged 7 commits into
mainfrom
feat/round-robin-test
May 27, 2026
Merged

feat(sdk): add round-robin test example for Go and TS SDKs#789
philanton merged 7 commits into
mainfrom
feat/round-robin-test

Conversation

@philanton
Copy link
Copy Markdown
Contributor

@philanton philanton commented May 26, 2026

Summary

  • Adds sdk/go/examples/round_robin/main.go and sdk/ts/examples/round_robin/{lifecycle.ts,package.json,tsconfig.json}.
  • Each example exercises deposit / transfer-send / close / transfer-receive / withdraw on every chain on which the configured asset is supported. Funds circulate: deposit chain[i] → A→B transfer → close → B→A transfer (off-chain credit on void state) → withdraw chain[i+1]; last iteration wraps to chain[0].
  • Preflight verifies per-chain native gas balance and seed-chain asset balance before any tx runs; bails with a per-chain shortfall table if anything is missing.
  • Optional channel session key path (sessionKeyPriv) mirrored from the existing channel_session_key example — registers a v1 key for asset, builds a session-key-backed client, and runs the entire loop through it.
  • Header comments document the full 1.a–1.e + 2.a–2.e flow; in-code section banners and per-step comments reference those identifiers.

Test plan

  • go vet ./sdk/go/examples/round_robin/...
  • go build ./sdk/go/examples/round_robin/...
  • tsc --noEmit against sdk/ts/examples/round_robin/lifecycle.ts (skipLibCheck, ES2022+DOM lib)
  • End-to-end run against sandbox nitronode with funded wallets (manual, requires real keys)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added Go and TypeScript round‑robin examples demonstrating end‑to‑end channel lifecycle across supported chains.
  • Bug Fixes

    • Enforced chain-binding checks for deposits/withdrawals to prevent operations on the wrong chain.
    • Improved transaction/receipt handling and simulation account behavior for deposit/withdraw flows.
  • Documentation

    • Added TypeScript example package and config for running the lifecycle demo.
  • Tests

    • Added unit and integration tests covering cross-chain guard behavior.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

Adds runnable Go and TypeScript round‑robin channel lifecycle examples (asset discovery, preflight, iterative deposit/transfer/close/withdraw, optional session keys), updates TypeScript packaging/tsconfig, tweaks EVM simulate/account and ERC‑20 approve behavior, and adds cross‑chain home‑ledger blockchain ID guards with tests.

Changes

Round‑Robin Channel Lifecycle Examples + Guards

Layer / File(s) Summary
Go example: docs, config, main, session-key, helpers
sdk/go/examples/round_robin/main.go
Full Go round‑robin example with documentation, config, signer/client setup, optional session‑key registration, lifecycle loop (approve→deposit→off‑chain transfer→close→return→withdraw), preflight checks, and polling helpers (waitForTxReceipt, checkpointAndWait, closeAndWait, waitForOnChain).
TypeScript example: main flow, config, helpers, package/tsconfig
sdk/ts/examples/round_robin/lifecycle.ts, sdk/ts/examples/round_robin/package.json, sdk/ts/examples/round_robin/tsconfig.json
Node WS polyfill, imports, config constants, main, runIteration, preflight, discoverTokenSet, nativeBalance, newClient, setupSessionKeyClient, checkpoint/close/on‑chain polling helpers, package manifest and tsconfig for the example.
EVM simulateContract & ERC20 approve
sdk/ts/src/blockchain/evm/client.ts, sdk/ts/src/blockchain/evm/erc20.ts
Passes walletSigner.account! (signer account object) to simulateContract calls and simplifies ERC20.approve to call writeContract directly and wait for the receipt with 3 confirmations.
Cross‑chain guard checks + tests
sdk/go/channel.go, sdk/go/client_test.go, sdk/ts/src/client.ts, sdk/ts/test/unit/client.test.ts
Deposit/withdraw now validate that an existing home channel’s homeLedger.blockchainId matches the requested chain ID and return/throw an error on mismatch; added unit and Go integration tests asserting rejection on foreign‑chain operations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • layer-3/nitrolite#698: Related on-chain balance API changes used by the examples' polling for ERC‑20 settlement.

Suggested reviewers

  • ihsraham
  • nksazonov
  • dimast-x

Poem

🐇 I hopped from chain to chain at dawn,
Approved and routed tokens on the lawn,
Sessions signed and checkpoints polled,
Off‑chain credits neatly told,
A rabbit cheers: the loop runs on.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main addition: two round-robin test examples for the Go and TypeScript SDKs, which matches the primary change across the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/round-robin-test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
sdk/ts/examples/round_robin/lifecycle.ts (1)

288-291: 💤 Low value

Avoid any types for signer parameters.

The stateSigner and txSigner parameters are typed as any, which bypasses TypeScript's type checking. The SDK should have proper types for these signers (e.g., ChannelSigner or similar interfaces).

Suggested typing improvement
-async function newClient(stateSigner: any, txSigner: any): Promise<Client> {
+async function newClient(stateSigner: EthereumMsgSigner | ChannelSessionKeyStateSigner, txSigner: EthereumRawSigner): Promise<Client> {

Alternatively, import and use the proper signer interface types from the SDK if available.

As per coding guidelines, "Use strict TypeScript with no any types unless unavoidable (e.g., RPC wire types)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/ts/examples/round_robin/lifecycle.ts` around lines 288 - 291, The
function newClient currently types stateSigner and txSigner as any which
disables TypeScript checks; change the signature to use the SDK signer
interfaces (e.g., replace stateSigner: any, txSigner: any with the appropriate
types such as ChannelSigner or WalletSigner exported by the SDK), import those
signer types at top of the file, and update any callers to pass objects matching
those interfaces; ensure the types are used in the call to Client.create so the
compiler validates the signers passed to Client.create(wsURL, stateSigner,
txSigner, ...opts).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@sdk/ts/examples/round_robin/lifecycle.ts`:
- Around line 288-291: The function newClient currently types stateSigner and
txSigner as any which disables TypeScript checks; change the signature to use
the SDK signer interfaces (e.g., replace stateSigner: any, txSigner: any with
the appropriate types such as ChannelSigner or WalletSigner exported by the
SDK), import those signer types at top of the file, and update any callers to
pass objects matching those interfaces; ensure the types are used in the call to
Client.create so the compiler validates the signers passed to
Client.create(wsURL, stateSigner, txSigner, ...opts).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a8b58cc0-d53f-4ad9-8223-3a71a61d3d3d

📥 Commits

Reviewing files that changed from the base of the PR and between 6c796fc and 8d23b94.

📒 Files selected for processing (4)
  • sdk/go/examples/round_robin/main.go
  • sdk/ts/examples/round_robin/lifecycle.ts
  • sdk/ts/examples/round_robin/package.json
  • sdk/ts/examples/round_robin/tsconfig.json

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
sdk/ts/examples/round_robin/lifecycle.ts (1)

87-88: ⚡ Quick win

Load the example private keys from env instead of editing the file.

These placeholders keep the example compiling, but the intended workflow is still “paste secrets into source.” Reading them from env and validating at startup is safer and gives a clearer failure than an as Hex cast.

🔐 Suggested direction
-const privA = '0x7d6071...' as Hex;
-const privB = '0xf63695...' as Hex;
+const privA = requiredHexEnv('ROUND_ROBIN_PRIV_A');
+const privB = requiredHexEnv('ROUND_ROBIN_PRIV_B');
function requiredHexEnv(name: string): Hex {
    const value = process.env[name];
    if (!value?.startsWith('0x')) {
        throw new Error(`${name} must be set to a 0x-prefixed private key`);
    }
    return value as Hex;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/ts/examples/round_robin/lifecycle.ts` around lines 87 - 88, Replace
hard-coded example private keys privA and privB with environment-driven
validation: add a helper like requiredHexEnv(name) that reads process.env[name],
verifies it exists and starts with "0x", and returns it typed as Hex; then call
requiredHexEnv('PRIV_A') and requiredHexEnv('PRIV_B') to initialize privA and
privB so the example fails fast with a clear error instead of using an as Hex
cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk/go/examples/round_robin/main.go`:
- Around line 322-346: The polling loops in waitForTxReceipt, closeAndWait,
checkpointAndWait, and waitForOnChain ignore ctx cancellation because they call
time.Sleep(2 * time.Second) directly; replace those sleeps with a cancellable
helper (e.g., sleepOrDone(ctx, 2*time.Second)) that returns early when
ctx.Done() is closed (or use select { case <-time.After(...): case <-ctx.Done():
return/continue } inline) so each loop honors context cancellation and deadlines
while keeping the existing retry logic.

---

Nitpick comments:
In `@sdk/ts/examples/round_robin/lifecycle.ts`:
- Around line 87-88: Replace hard-coded example private keys privA and privB
with environment-driven validation: add a helper like requiredHexEnv(name) that
reads process.env[name], verifies it exists and starts with "0x", and returns it
typed as Hex; then call requiredHexEnv('PRIV_A') and requiredHexEnv('PRIV_B') to
initialize privA and privB so the example fails fast with a clear error instead
of using an as Hex cast.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 916661ab-5658-4b27-8d3f-7f15bee30946

📥 Commits

Reviewing files that changed from the base of the PR and between 8d23b94 and c32d868.

⛔ Files ignored due to path filters (1)
  • sdk/ts/examples/round_robin/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • sdk/go/examples/round_robin/main.go
  • sdk/ts/examples/round_robin/lifecycle.ts
  • sdk/ts/src/blockchain/evm/client.ts
  • sdk/ts/src/blockchain/evm/erc20.ts

Comment thread sdk/go/examples/round_robin/main.go
philanton and others added 7 commits May 26, 2026 17:03
Exercises deposit, transfer, close, and withdraw across every supported chain
for one asset. Funds circulate through the loop: deposit on chain[i],
off-chain transfer to B, close, B transfers back to A (off-chain credit),
withdraw on chain[i+1]. Last iteration wraps to chain[0].

Preflight checks per-chain native gas balance and seed-chain asset balance
before running. Optional channel session key path mirrored from the
channel_session_key example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The blockchain client and ERC20 wrapper were stripping the wallet's
LocalAccount down to its `.address` before passing it into
simulateContract. viem parses a bare address as a JsonRpcAccount, which
then propagates into writeContract and routes through eth_sendTransaction
— a wallet-namespaced RPC method that public HTTP providers (DRPC,
Alchemy, etc.) reject because they do not hold the private key.

Pass the full account so viem retains the LocalAccount on the returned
request and writeContract signs locally + calls eth_sendRawTransaction.
Browser flow (JsonRpcAccount from window.ethereum) is unchanged — same
account type flows through; MetaMask still receives eth_sendTransaction
over the custom transport.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Wait for approve tx receipt before deposit (Go side; the TS SDK's
  approve already waits internally).
- Replace post-close checkpointAndWait with a dedicated closeAndWait
  that polls for channel == null or status == Void after the on-chain
  close, rather than waiting for a state_version bump that never lands.
- Special-case checkpointAndWait for expectedVersion == 0 (the
  channel-creation case after Withdraw on a void state): wait for
  status == Open instead of a stale state_version comparison.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… channel

Deposit and Withdraw silently created channel-creation transitions on the
caller-supplied blockchainID even when an open home channel for that asset
already existed on a different chain. The mismatch surfaced later as a
confusing on-chain failure rather than at the SDK call site.

Add an early guard in both methods (Go and TS) that returns an error when
the latest state has an active (non-final) home channel whose
HomeLedger.BlockchainID does not match the requested chain. The "no
channel" and "final channel" paths still create a fresh channel on the
requested chain, so the new rule only applies while an existing channel
is in use.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Non-standard tokens such as Tether USDT on Ethereum L1 declare `approve`
without a `returns (bool)`, so the pre-flight simulateContract path blew
up with ContractFunctionZeroDataError when viem tried to decode the
empty return value against the standard ERC-20 ABI. The transaction
itself would succeed because writeContract never decodes return data on
non-view calls.

Drop the simulate step in the ERC20 wrapper's approve and submit the
write directly. Loses the pre-flight revert check (acceptable for
approve specifically — failures cost only the failed-tx gas) but
unblocks every non-standard ERC-20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ound-robin

- waitForTxReceipt in the Go example now waits approveConfirmations (=3)
  additional blocks past the approve receipt, and the TS SDK's ERC-20
  approve waits the same on its post-receipt block. Both target the
  load-balanced public-RPC read-after-write race where a downstream
  allowance/eth_call read could hit a node that has not yet indexed the
  approve.
- setupSessionKeyClient in both examples now looks up the latest stored
  state for the configured session key (including inactive/expired) and
  resumes at version+1 instead of always submitting v1. Re-running the
  example against the same wallet+session-key no longer fails with
  "version must be monotonically increasing".
- Go example also honors sessionKeyPriv when provided (TS already did),
  matching the documented "If sessionKeyPriv is non-empty, register it"
  behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@philanton philanton force-pushed the feat/round-robin-test branch from c32d868 to e01cfc1 Compare May 26, 2026 16:01
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
sdk/ts/test/unit/client.test.ts (1)

346-394: ⚡ Quick win

Refactor these guard tests into a data-driven .forEach() table.

The new deposit/withdraw guard cases are structurally duplicated and can drift. A single table-driven block will keep coverage equivalent with less maintenance.

As per coding guidelines sdk/ts/test/**/*.test.ts: "Use data-driven tests with .forEach() pattern and manual mocks instead of jest.mock()."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/ts/test/unit/client.test.ts` around lines 346 - 394, Refactor the
duplicated deposit/withdraw guard tests into a single data-driven table using
Array.forEach so both operations (Client.deposit and Client.withdraw) are tested
uniformly: build a test table of cases with method name ('deposit'|'withdraw'),
targetChain values (8453n vs 137n), expected throw messages, and expectations
for signAndSubmitState/requestChannelCreation; for each row create the
latestState via openChannelState(), adjust latestState.homeLedger.blockchainId
and homeLedger.tokenAddress as needed, createHighLevelClient(latestState), then
invoke client[method](...) and assert either rejects.toThrow(expectedMessage)
and that signAndSubmitState/requestChannelCreation were not called, or that
signAndSubmitState was called once and requestChannelCreation was not called.
Keep references to Client.deposit/Client.withdraw, createHighLevelClient,
openChannelState, TOKEN_ADDRESS, signAndSubmitState, and requestChannelCreation
to locate code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk/go/channel.go`:
- Around line 109-113: The chain-mismatch guard (checking
state.HomeLedger.BlockchainID vs blockchainID) must run before any call to
getTokenAddress to avoid returning token lookup errors for existing channels;
move the early return that compares state.HomeLedger.BlockchainID and
blockchainID to execute prior to getTokenAddress(...) in the functions where
it's currently after token resolution (references:
state.HomeLedger.BlockchainID, blockchainID, asset, and getTokenAddress),
ensuring both the code paths around the current checks at the locations
corresponding to lines ~109 and ~227-231 perform the blockchain ID check first
and only call getTokenAddress if the chain matches.

In `@sdk/ts/examples/round_robin/lifecycle.ts`:
- Around line 306-309: Change the newClient function signature to use the SDK
signer interfaces instead of any: replace stateSigner: any and txSigner: any
with stateSigner: StateSigner and txSigner: TransactionSigner, and add imports
for StateSigner and TransactionSigner from ../../src/signers; ensure the types
align with the Client.create(wsURL, stateSigner, txSigner, ...opts) call so
TypeScript type-checks correctly.

In `@sdk/ts/src/client.ts`:
- Around line 425-431: The active-channel chain-binding check (compare
state.homeLedger.blockchainId with blockchainId) must run before any call to
getTokenAddress to ensure the cross-chain guard throws first and to avoid
unnecessary token lookups; update the code paths that call getTokenAddress (the
block around the current check using state.homeLedger.blockchainId and the
similar block around the getTokenAddress usage at the later section) so the
mismatch check executes immediately and only if it passes then call
getTokenAddress(asset, ...). Ensure both occurrences (the one referencing
state.homeLedger.blockchainId vs blockchainId and the later similar block) are
reordered accordingly.

---

Nitpick comments:
In `@sdk/ts/test/unit/client.test.ts`:
- Around line 346-394: Refactor the duplicated deposit/withdraw guard tests into
a single data-driven table using Array.forEach so both operations
(Client.deposit and Client.withdraw) are tested uniformly: build a test table of
cases with method name ('deposit'|'withdraw'), targetChain values (8453n vs
137n), expected throw messages, and expectations for
signAndSubmitState/requestChannelCreation; for each row create the latestState
via openChannelState(), adjust latestState.homeLedger.blockchainId and
homeLedger.tokenAddress as needed, createHighLevelClient(latestState), then
invoke client[method](...) and assert either rejects.toThrow(expectedMessage)
and that signAndSubmitState/requestChannelCreation were not called, or that
signAndSubmitState was called once and requestChannelCreation was not called.
Keep references to Client.deposit/Client.withdraw, createHighLevelClient,
openChannelState, TOKEN_ADDRESS, signAndSubmitState, and requestChannelCreation
to locate code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1656155e-1c34-4582-9f28-767a8d5b54ec

📥 Commits

Reviewing files that changed from the base of the PR and between c32d868 and e01cfc1.

⛔ Files ignored due to path filters (1)
  • sdk/ts/examples/round_robin/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (10)
  • sdk/go/channel.go
  • sdk/go/client_test.go
  • sdk/go/examples/round_robin/main.go
  • sdk/ts/examples/round_robin/lifecycle.ts
  • sdk/ts/examples/round_robin/package.json
  • sdk/ts/examples/round_robin/tsconfig.json
  • sdk/ts/src/blockchain/evm/client.ts
  • sdk/ts/src/blockchain/evm/erc20.ts
  • sdk/ts/src/client.ts
  • sdk/ts/test/unit/client.test.ts
✅ Files skipped from review due to trivial changes (1)
  • sdk/ts/examples/round_robin/package.json

Comment thread sdk/go/channel.go
Comment thread sdk/ts/examples/round_robin/lifecycle.ts
Comment thread sdk/ts/src/client.ts
@philanton philanton merged commit cdb60ca into main May 27, 2026
16 checks passed
@philanton philanton deleted the feat/round-robin-test branch May 27, 2026 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant