Skip to content

feat(zk-sdk): add StellarTxBuilder for Soroban register and resolve f…#229

Open
Hassan-oladipupo wants to merge 1 commit intoAlien-Protocol:mainfrom
Hassan-oladipupo:StellarTxBuilder
Open

feat(zk-sdk): add StellarTxBuilder for Soroban register and resolve f…#229
Hassan-oladipupo wants to merge 1 commit intoAlien-Protocol:mainfrom
Hassan-oladipupo:StellarTxBuilder

Conversation

@Hassan-oladipupo
Copy link
Copy Markdown
Contributor

@Hassan-oladipupo Hassan-oladipupo commented Mar 26, 2026

feat(zk-sdk): add StellarTxBuilder for Soroban register and resolve flows
close #198

Summary by CodeRabbit

  • New Features

    • Introduced a new ZK SDK package for building and submitting Stellar-based transactions.
    • Supports key operations including commitment registration, resolver configuration, Stellar address binding, and name resolution.
    • Provides integrated transaction preparation, signing, and submission to the network.
  • Tests

    • Added integration tests validating transaction generation and XDR payload correctness.

@drips-wave
Copy link
Copy Markdown

drips-wave bot commented Mar 26, 2026

@Hassan-oladipupo Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new ZK SDK package implementing a StellarTxBuilder class for constructing and submitting Soroban contract transactions on the Stellar network. The package includes type definitions, transaction building logic, public API exports, and integration tests.

Changes

Cohort / File(s) Summary
Package Configuration
zk/sdk/package.json
Defines private Node/TypeScript package @alien-protocol/zk-sdk with @stellar/stellar-sdk as peer and dev dependency; includes test script for running Node test runner.
Type Definitions & Public API
zk/sdk/src/types.ts, zk/sdk/src/index.ts
Introduces interfaces and type aliases for Stellar transaction building (StellarTxBuilderConfig, TxBuildOptions, input/output types); re-exports StellarTxBuilder class and all type definitions from public API entry point.
Core Transaction Builder
zk/sdk/src/tx.ts
Implements StellarTxBuilder class with methods for buildRegister, buildRegisterResolver, buildAddStellarAddress, and buildResolve; includes submitTransaction for signing and submission. Features source account resolution, input normalization (hex/base64 to bytes), and Soroban contract invocation preparation.
Integration Testing
zk/sdk/src/__tests__/tx.test.ts
Validates StellarTxBuilder produces valid Soroban transaction XDR envelopes; tests round-trip XDR serialization/deserialization across register, add address, and resolve operations using environment-provided testnet configuration.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant StellarTxBuilder
    participant SorobanRpc as Soroban RPC Server
    participant Contract

    Client->>StellarTxBuilder: buildRegister(params)
    StellarTxBuilder->>SorobanRpc: Fetch source account
    SorobanRpc-->>StellarTxBuilder: Account data
    StellarTxBuilder->>StellarTxBuilder: Convert inputs to ScVal
    StellarTxBuilder->>StellarTxBuilder: Build transaction
    StellarTxBuilder->>SorobanRpc: prepareTransaction(tx)
    SorobanRpc-->>StellarTxBuilder: Prepared XDR + metadata
    StellarTxBuilder-->>Client: BuiltTransaction {xdr, transaction, method, source}
    
    Client->>StellarTxBuilder: submitTransaction(built, signer)
    StellarTxBuilder->>StellarTxBuilder: Sign with Keypair
    StellarTxBuilder->>SorobanRpc: sendTransaction(signedXdr)
    SorobanRpc->>Contract: Invoke contract method
    Contract-->>SorobanRpc: Transaction result
    SorobanRpc-->>StellarTxBuilder: Response
    StellarTxBuilder-->>Client: Submission result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Whisker-twitched with joy, I craft
Stellar transactions, swift and daft,
Types and builders, XDR dreams,
Soroban magic flows in streams!
Register, resolve, address too—
The ZK SDK hops straight through! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding StellarTxBuilder for Soroban register and resolve flows, which accurately summarizes the PR's primary objective.
Linked Issues check ✅ Passed The PR implements all required coding objectives: StellarTxBuilder with buildRegister, buildAddStellarAddress, buildResolve methods [#198], proper XDR envelope handling, types.ts definitions, package.json peer dependency, and exports from index.ts.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing StellarTxBuilder functionality: package.json, tx.ts, types.ts, index.ts exports, and integration tests—no extraneous modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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 (4)
zk/sdk/src/__tests__/tx.test.ts (1)

35-44: Missing test coverage for buildRegisterResolver.

The test exercises buildRegister, buildAddStellarAddress, and buildResolve, but does not cover buildRegisterResolver. Consider adding a test case for this method to ensure complete coverage of the public API.

🧪 Proposed addition for buildRegisterResolver test
     const commitment = new Uint8Array(32).fill(7);
+    const proof = new Uint8Array(128).fill(1); // Example proof bytes
+    const publicSignals = {
+      oldRoot: new Uint8Array(32).fill(2),
+      newRoot: new Uint8Array(32).fill(3),
+    };
 
     const registerTx = await builder.buildRegister({
       caller: source,
       commitment,
     });
+    const registerResolverTx = await builder.buildRegisterResolver({
+      caller: source,
+      commitment,
+      proof,
+      publicSignals,
+    });
     const addAddressTx = await builder.buildAddStellarAddress({

Then include registerResolverTx in the validation loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/__tests__/tx.test.ts` around lines 35 - 44, Add a test that calls
builder.buildRegisterResolver(...) and assigns the result to registerResolverTx,
then include registerResolverTx in the same validation/assertion loop used for
registerTx/addAddressTx/resolveTx. Specifically, invoke the
buildRegisterResolver method on the same builder (e.g.,
builder.buildRegisterResolver with the appropriate caller and
commitment/usernameHash arguments), store it as registerResolverTx, and add it
to the array or series of transactions being validated so the test covers
buildRegisterResolver alongside buildRegister, buildAddStellarAddress, and
buildResolve.
zk/sdk/package.json (1)

5-6: Consider pointing main and types to compiled output for better compatibility.

For a TypeScript package, pointing main and types directly to .ts source files works only when consumers use ts-node or similar. If this SDK may be consumed by projects without TypeScript compilation at import time, consider building to dist/ and updating these fields accordingly. Since this is marked "private": true, this is acceptable for internal monorepo use.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/package.json` around lines 5 - 6, The package.json currently sets
"main": "src/index.ts" and "types": "src/index.ts", which points consumers to
TypeScript sources; change these to the compiled outputs (e.g., "main":
"dist/index.js" and "types": "dist/index.d.ts"), ensure the build step outputs
into dist (adjust the build script and tsconfig.outDir if needed), and confirm
that the compiled artifacts are produced before publishing or being consumed;
look for the "main" and "types" entries in package.json and the build
scripts/tsconfig to implement this.
zk/sdk/src/types.ts (1)

27-30: Note: PublicSignalsInput requires both roots from caller.

Per the circuit at zk/circuits/merkle/merkle_update_proof.circom, publicSignals from snarkjs only contains the single output out_newRoot. The oldRoot must be provided separately (typically the current on-chain SMT root). This interface correctly requires callers to supply both values for the contract's PublicSignals struct. Consider adding a JSDoc comment to clarify this for SDK consumers.

📝 Proposed documentation
+/**
+ * Public signals for the Groth16 non-inclusion proof.
+ * Note: `oldRoot` should be the current on-chain SMT root (fetched from contract state).
+ * `newRoot` is the expected root after registration (from proof generation).
+ */
 export interface PublicSignalsInput {
   oldRoot: Bytes32Input;
   newRoot: Bytes32Input;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/types.ts` around lines 27 - 30, The PublicSignalsInput interface
requires callers to supply both oldRoot and newRoot, but it's unclear because
the circom circuit (merkle_update_proof.circom) and snarkjs publicSignals only
emit out_newRoot; update the SDK by adding a JSDoc comment above the
PublicSignalsInput interface explaining that publicSignals from snarkjs contains
only out_newRoot and that oldRoot must be provided separately (e.g., the current
on-chain SMT root) when constructing the contract's PublicSignals struct;
reference PublicSignalsInput, publicSignals, and out_newRoot in the comment so
consumers know where the values originate.
zk/sdk/src/tx.ts (1)

148-159: Edge case: base64 strings starting with valid hex prefix.

If a base64-encoded value happens to start with characters that look like 0x followed by valid hex (e.g., 0xABC... which is valid base64), the function will incorrectly decode it as hex. While rare, consider requiring an explicit format parameter or documenting the precedence behavior.

💡 Alternative: explicit format or different detection
-function normalizeBytes(value: string | Uint8Array): Buffer {
+function normalizeBytes(value: string | Uint8Array, format?: "hex" | "base64"): Buffer {
   if (typeof value !== "string") {
     return Buffer.from(value);
   }
 
+  if (format === "hex") {
+    const normalized = value.startsWith("0x") ? value.slice(2) : value;
+    return Buffer.from(normalized, "hex");
+  }
+  if (format === "base64") {
+    return Buffer.from(value, "base64");
+  }
+
+  // Auto-detect: 0x prefix indicates hex
   const normalized = value.startsWith("0x") ? value.slice(2) : value;
   if (normalized.length % 2 === 0 && /^[0-9a-fA-F]+$/.test(normalized)) {
     return Buffer.from(normalized, "hex");
   }
 
   return Buffer.from(value, "base64");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/tx.ts` around lines 148 - 159, The normalizeBytes function can
mis-detect ambiguous inputs (e.g., strings like "0xABC..." that are both valid
hex and valid base64); add an optional parameter to normalizeBytes such as
format?: 'hex' | 'base64' | 'auto' (default 'auto') and change the logic so that
in 'auto' mode you still detect hex/base64 but if an input is ambiguous (passes
the hex regex and could be base64) you throw an explicit error asking the caller
to supply format, and when format is provided enforce that decoding branch;
update callers or document that callers must pass format to disambiguate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@zk/sdk/src/tx.ts`:
- Around line 70-83: The submitTransaction implementation ignores the
SubmitTransactionOptions.pollUntilSuccess flag; update submitTransaction (and
respect _options.pollUntilSuccess) so that after signing and calling
this.server.sendTransaction you extract the returned transaction hash/id and,
when pollUntilSuccess is true, poll the network (using existing server methods
such as a transaction status endpoint like getTransaction/getTx or
waitForTransaction) until the transaction reaches a success or final failure
state, then return the final confirmed result; if pollUntilSuccess is false,
keep the current behavior and return the immediate sendTransaction response.
Ensure you reference submitTransaction, SubmitTransactionOptions,
pollUntilSuccess, and this.server.sendTransaction when making the changes.

---

Nitpick comments:
In `@zk/sdk/package.json`:
- Around line 5-6: The package.json currently sets "main": "src/index.ts" and
"types": "src/index.ts", which points consumers to TypeScript sources; change
these to the compiled outputs (e.g., "main": "dist/index.js" and "types":
"dist/index.d.ts"), ensure the build step outputs into dist (adjust the build
script and tsconfig.outDir if needed), and confirm that the compiled artifacts
are produced before publishing or being consumed; look for the "main" and
"types" entries in package.json and the build scripts/tsconfig to implement
this.

In `@zk/sdk/src/__tests__/tx.test.ts`:
- Around line 35-44: Add a test that calls builder.buildRegisterResolver(...)
and assigns the result to registerResolverTx, then include registerResolverTx in
the same validation/assertion loop used for registerTx/addAddressTx/resolveTx.
Specifically, invoke the buildRegisterResolver method on the same builder (e.g.,
builder.buildRegisterResolver with the appropriate caller and
commitment/usernameHash arguments), store it as registerResolverTx, and add it
to the array or series of transactions being validated so the test covers
buildRegisterResolver alongside buildRegister, buildAddStellarAddress, and
buildResolve.

In `@zk/sdk/src/tx.ts`:
- Around line 148-159: The normalizeBytes function can mis-detect ambiguous
inputs (e.g., strings like "0xABC..." that are both valid hex and valid base64);
add an optional parameter to normalizeBytes such as format?: 'hex' | 'base64' |
'auto' (default 'auto') and change the logic so that in 'auto' mode you still
detect hex/base64 but if an input is ambiguous (passes the hex regex and could
be base64) you throw an explicit error asking the caller to supply format, and
when format is provided enforce that decoding branch; update callers or document
that callers must pass format to disambiguate.

In `@zk/sdk/src/types.ts`:
- Around line 27-30: The PublicSignalsInput interface requires callers to supply
both oldRoot and newRoot, but it's unclear because the circom circuit
(merkle_update_proof.circom) and snarkjs publicSignals only emit out_newRoot;
update the SDK by adding a JSDoc comment above the PublicSignalsInput interface
explaining that publicSignals from snarkjs contains only out_newRoot and that
oldRoot must be provided separately (e.g., the current on-chain SMT root) when
constructing the contract's PublicSignals struct; reference PublicSignalsInput,
publicSignals, and out_newRoot in the comment so consumers know where the values
originate.
🪄 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: f9911cde-d193-43a7-ba94-3eefdd8ffb1e

📥 Commits

Reviewing files that changed from the base of the PR and between d7597d2 and acd3b77.

⛔ Files ignored due to path filters (1)
  • zk/sdk/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • zk/sdk/package.json
  • zk/sdk/src/__tests__/tx.test.ts
  • zk/sdk/src/index.ts
  • zk/sdk/src/tx.ts
  • zk/sdk/src/types.ts

Comment on lines +70 to +83
public async submitTransaction(
built: BuiltTransaction | string,
signer: Keypair | string,
_options: SubmitTransactionOptions = {},
) {
const signed = typeof built === "string"
? TransactionBuilder.fromXDR(built, this.config.networkPassphrase)
: TransactionBuilder.fromXDR(built.xdr, this.config.networkPassphrase);
const keypair = typeof signer === "string" ? Keypair.fromSecret(signer) : signer;

signed.sign(keypair);

return this.server.sendTransaction(signed);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SubmitTransactionOptions.pollUntilSuccess is declared but not used.

The _options parameter includes pollUntilSuccess (per types.ts), but the implementation ignores it and returns immediately after sendTransaction. This could leave callers expecting confirmation polling that never happens.

🔧 Proposed implementation for pollUntilSuccess
   public async submitTransaction(
     built: BuiltTransaction | string,
     signer: Keypair | string,
-    _options: SubmitTransactionOptions = {},
+    options: SubmitTransactionOptions = {},
   ) {
     const signed = typeof built === "string"
       ? TransactionBuilder.fromXDR(built, this.config.networkPassphrase)
       : TransactionBuilder.fromXDR(built.xdr, this.config.networkPassphrase);
     const keypair = typeof signer === "string" ? Keypair.fromSecret(signer) : signer;
 
     signed.sign(keypair);
 
-    return this.server.sendTransaction(signed);
+    const response = await this.server.sendTransaction(signed);
+    
+    if (options.pollUntilSuccess && response.status === "PENDING") {
+      // Poll for transaction result
+      const hash = response.hash;
+      let result = await this.server.getTransaction(hash);
+      while (result.status === "NOT_FOUND") {
+        await new Promise((resolve) => setTimeout(resolve, 1000));
+        result = await this.server.getTransaction(hash);
+      }
+      return result;
+    }
+    
+    return response;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@zk/sdk/src/tx.ts` around lines 70 - 83, The submitTransaction implementation
ignores the SubmitTransactionOptions.pollUntilSuccess flag; update
submitTransaction (and respect _options.pollUntilSuccess) so that after signing
and calling this.server.sendTransaction you extract the returned transaction
hash/id and, when pollUntilSuccess is true, poll the network (using existing
server methods such as a transaction status endpoint like getTransaction/getTx
or waitForTransaction) until the transaction reaches a success or final failure
state, then return the final confirmed result; if pollUntilSuccess is false,
keep the current behavior and return the immediate sendTransaction response.
Ensure you reference submitTransaction, SubmitTransactionOptions,
pollUntilSuccess, and this.server.sendTransaction when making the changes.

@ryzen-xp ryzen-xp self-requested a review March 26, 2026 16:47
@ryzen-xp ryzen-xp added Approved This PR is ready for merging . CI Failed Please check why you CI is faileing fix your code and removed Approved This PR is ready for merging . labels Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI Failed Please check why you CI is faileing fix your code

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

[Feature][SDK] Implement StellarTxBuilder — build Soroban transactions for register and resolve

2 participants