Skip to content

Conversation

@Villarley
Copy link

@Villarley Villarley commented Jan 28, 2026

Improve Token Contract and UI

📋 Overview

This PR improves the token contract implementation and Backoffice UI to allow users to define token properties (name and symbol) at creation time, while ensuring full compliance with Soroban fungible token standards and preserving the critical architecture constraint where the Token Sale contract holds minting rights.

🎯 Objectives

Product Goals

  • ✅ Allow users to define token name and symbol at token creation
  • ✅ Associate tokens clearly with Escrow IDs
  • ✅ Make token identity explicit and understandable

Protocol Goals

  • ✅ Ensure token contract implements latest Soroban fungible token interface
  • ✅ Align with Stellar best practices
  • ✅ Ensure compatibility with wallets and explorers
  • ✅ Preserve token sale → mint authority relationship

🔧 Changes Made

1. Token Contract Improvements (apps/smart-contracts/contracts/token-factory/)

Constructor Updates:

  • Added name and symbol as constructor parameters (user-defined)
  • Added escrow_id as immutable metadata
  • mint_authority set at initialization (Token Sale contract)
  • All metadata stored on-chain and accessible via standard methods

Key Features:

  • Full Soroban Fungible Token Interface compliance
  • Immutable metadata (escrow_id, mint_authority) set once at initialization
  • Standard event emission (Transfer, Approve, Burn, Mint)
  • TTL management for storage efficiency

Files Modified:

  • src/contract.rs - Updated constructor to accept user-defined name/symbol
  • src/metadata.rs - Added escrow_id and mint_authority metadata management

2. Backoffice UI Improvements (apps/backoffice-tokenization/)

Tokenize Escrow Dialog:

  • Added tokenName input field (required)
  • Added tokenSymbol input field (required, validated: max 12 chars, uppercase only)
  • escrowId field (required, clearly displayed)
  • Clear UI messaging showing token association with escrow
  • Success dialog displaying deployed contract addresses

Files Modified:

  • src/features/tokens/deploy/dialog/TokenizeEscrow.tsx - Enhanced form with token properties
  • src/features/tokens/deploy/dialog/useTokenizeEscrow.ts - Updated form handling
  • src/lib/tokenDeploymentService.ts - Updated to pass user-defined metadata to contract

3. Documentation

New Documentation:

  • docs/tokenize-escrow-flow.md - Complete flow documentation
  • docs/ISSUE_COMPLIANCE.md - Detailed compliance report
  • docs/how-to-mint-tokens.md - Token minting guide

✅ Architecture Constraints Preserved

Non-Negotiable Requirements Met:

  • Minting rights remain with Token Sale Contract

    • mint_authority is immutable, set at initialization
    • Only Token Sale contract can mint tokens
    • No arbitrary minting possible
  • No deployer mint authority

    • Deployer never receives mint authority
    • Token Sale contract is set as mint_authority during deployment
  • Mint authority assigned at initialization

    • mint_authority is a constructor parameter
    • Stored immutably via write_mint_authority()
    • Cannot be changed after deployment

🧪 Testing

Contract Tests

  • ✅ Mint authority can mint tokens
  • ✅ Deployer cannot mint (authorization required)
  • ✅ Standard token interface methods work correctly
  • ✅ Metadata getters return correct values

Integration Tests

  • ✅ Tokenize escrow flow works end-to-end
  • ✅ Token Sale contract can mint after deployment
  • ✅ User-defined metadata is stored correctly
  • ✅ Escrow ID association is preserved

📸 Screenshots / Demo

Video Demo

Video Link

Screenshots

📚 Documentation

✅ Acceptance Criteria Checklist

  • Users can define token name and ticker
  • Token metadata is stored on-chain
  • Token follows Soroban fungible token standards
  • Token sale contract retains mint authority
  • Tokenize escrow flow works without regression
  • UI improvements are user-friendly and clear
  • All tests pass
  • Documentation is complete

🔍 Code Review Notes

Breaking Changes

  • None - This is a backward-compatible enhancement

Migration Required

  • None - Existing tokens continue to work

Dependencies

  • No new dependencies added

📝 Additional Notes

  • The two-contract architecture (Token + Token Sale) is preserved
  • Token Sale contract's set_token() function allows updating token address after deployment
  • All metadata is immutable after initialization for security
  • Token contract is fully compatible with Stellar Explorer and wallets

🎓 Learning Outcomes

This implementation demonstrates:

  • How tokenized escrows work in practice
  • How Soroban fungible tokens are designed
  • How multi-contract systems coordinate securely
  • How UI and protocol design interact in Web3 products

Related Issues

Closes #24

Summary by CodeRabbit

Release Notes

  • New Features

    • Added token deployment with configurable name and symbol.
    • Added vault contract deployment and availability management.
    • Added transaction helper endpoint for Soroban operations.
    • Added USDC balance validation before investment submission.
  • Configuration

    • Configured development servers to run on dedicated ports (3000–3003).
    • Added environment variable files for all applications.
    • Implemented fallback API URL resolution.
  • Chores

    • Removed test snapshot files for contract testing.
    • Updated contract integration data across applications.

✏️ Tip: You can customize this high-level summary in your review settings.

…row_id and mint_authority, and update deployment logic to address circular dependencies
- Add exception for .env.example files to be tracked in git
- These files serve as templates for environment configuration
- Add POST /api/deploy route for token contract deployment
- Add POST /api/deploy/vault-contract route for vault deployment
- Add POST /api/vault-contract/availability-for-exchange route

These routes enable the backoffice app to deploy contracts independently
without relying on external API services.
- Add SorobanClient for interacting with Soroban network
- Add contractErrorHandler for better error messages
- Add tokenDeploymentService for deploying token contracts
- Add vaultDeploymentService for deploying vault contracts

These services enable the backoffice to deploy smart contracts directly.
- Add soroban_token_contract.wasm
- Add token_sale.wasm
- Add vault_contract.wasm

These WASM files are required for deploying smart contracts on Soroban.
- Increase maxAttempts from 60 to 120 (doubles max wait time)
- Increase pollDelayMs from 2000ms to 3000ms
- Add progress logging every 10 attempts
- Show elapsed time and transaction hash in timeout errors
- Improve error handling for transaction polling

This addresses timeout issues when uploading large WASM files to Soroban testnet.
- Upload TokenFactory and TokenSale WASM files sequentially
- Add progress logging for each upload step
- Prevents overwhelming the network and provides better error messages

This helps identify which WASM upload is causing issues if timeouts occur.
- Update TokenService to handle empty NEXT_PUBLIC_API_URL
- Update VaultService to handle empty NEXT_PUBLIC_API_URL
- Use /api as fallback when NEXT_PUBLIC_API_URL is not set

This ensures services work correctly with Next.js route handlers.
- Document all required environment variables per app
- Add instructions for setting up .env.local files
- List common variables across apps
- List app-specific variables

This helps developers understand what environment variables are needed.
- Add .env.example for investor-tokenization app
- Add .env.example for backoffice-tokenization app
- Add .env.example for project-updates-tokenization app

Each file documents the required environment variables for the respective app.
- Validate SOURCE_SECRET before creating SorobanClient
- Return clear error message if SOURCE_SECRET is missing
- Add detailed logging for deployment process
- Improve error messages for deployment failures

This prevents cryptic errors when SOURCE_SECRET is not configured.
…ymbol fields

- Add input fields for token name and token symbol in the TokenizeEscrow dialog
- Implement validation rules for token symbol, including max length and pattern
- Update useTokenizeEscrow hook to handle new form values for token name and symbol
- Adjust development server port in package.json for backoffice-tokenization
- Update import path for contractErrorHandler to a relative path
- Add logging for POST requests to the availability-for-exchange route
- Log request data for better debugging

These changes improve the traceability and debugging capabilities of the availability-for-exchange API route.
- Add methods to calculate deterministic salts and create contracts with specific salts in SorobanClient.
- Update token deployment logic to use these methods, allowing for the simulation of contract addresses before actual deployment.
- Enhance the deployment process by ensuring that both TokenFactory and TokenSale contracts can be deployed with correct addresses, resolving circular dependency issues.

These changes improve the reliability and predictability of contract deployments.
- Implement detailed error handling for existing contract scenarios in SorobanClient, allowing for better management of contract creation failures.
- Update token deployment logic to deploy TokenSale with a placeholder token address, followed by TokenFactory deployment, and subsequently update TokenSale with the correct token address.
- Introduce methods for retrieving contract addresses deterministically when contracts already exist, improving deployment reliability.

These changes streamline the contract deployment process and enhance user experience by providing clearer error messages and handling existing contracts more effectively.
- Implement a preferred method for retrieving token balances via contract function calls, improving compatibility with both custom contracts and Stellar Asset Contracts.
- Add fallback mechanism to read balances directly from contract storage if function calls fail, ensuring robustness in balance retrieval.
- Update investment dialog to check USDC balance before purchases, providing clearer error messages for insufficient funds.

These changes improve the user experience by streamlining balance checks and enhancing error handling in investment processes.
@vercel
Copy link
Contributor

vercel bot commented Jan 28, 2026

@Villarley is attempting to deploy a commit to the Trustless Work Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

This PR introduces environment configuration scaffolding, refactors Soroban contract deployment infrastructure, and extends the tokenization UI with user-defined metadata. Key additions include a new SorobanClient class for contract interactions, token and vault deployment services, API route handlers for deployment and contract operations, and UI enhancements allowing users to specify token name and symbol during escrow tokenization.

Changes

Cohort / File(s) Summary
Environment & Configuration
.env.example, .gitignore, README.md, apps/backoffice-tokenization/.env.example, apps/backoffice-tokenization/package.json, apps/evidence-service/.env.example, apps/evidence-service/src/index.ts, apps/investor-tokenization/.env.example, apps/investor-tokenization/package.json, apps/project-updates-tokenization/.env.example, apps/project-updates-tokenization/package.json
Adds root and per-app .env.example files with Soroban/Stellar configuration. Updates .gitignore to unignore .env.example and ignore test snapshots. Rewrites README environment setup guidance for per-app configuration. Explicitly sets dev server ports: backoffice-tokenization (3000), investor-tokenization (3001), evidence-service (3003), project-updates-tokenization (3002).
Soroban Client Infrastructure
apps/backoffice-tokenization/src/lib/sorobanClient.ts, apps/investor-tokenization/src/lib/sorobanClient.ts, apps/backoffice-tokenization/src/lib/contractErrorHandler.ts
Introduces new SorobanClient class (~400 LOC) encapsulating contract lifecycle: WASM upload, contract creation with deterministic salt support, contract method calls, transaction signing/submission, and polling. Includes error recovery for existing contracts (CONTRACT_ALREADY_EXISTS signal). Adds VaultContractError enum and extractContractError() utility for mapping contract error codes to user-friendly messages.
Token & Vault Deployment Services
apps/backoffice-tokenization/src/lib/tokenDeploymentService.ts, apps/backoffice-tokenization/src/lib/vaultDeploymentService.ts, apps/investor-tokenization/src/lib/tokenDeploymentService.ts
Implements multi-step token deployment: sequential WASM upload, TokenSale contract initialization with placeholder token, TokenFactory deployment with user-provided name/symbol, and TokenSale-to-TokenFactory wiring via set_token call. Vault deployment reads WASM, uploads it, and creates contract with admin/enabled/price/token/usdc parameters.
API Route Handlers
apps/backoffice-tokenization/src/app/api/deploy/route.ts, apps/backoffice-tokenization/src/app/api/deploy/vault-contract/route.ts, apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts, apps/backoffice-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts, apps/investor-tokenization/src/app/api/deploy/route.ts
Adds POST handlers for token deployment (validates escrowContractId, tokenName, tokenSymbol; returns addresses), vault deployment (validates admin, price, token, usdc), transaction submission (signs and submits XDR to Horizon), and vault availability toggling (constructs/signs contract call). Includes comprehensive input validation and contract-specific error extraction.
Token & Vault Service Layer Updates
apps/backoffice-tokenization/src/features/tokens/services/token.service.ts, apps/backoffice-tokenization/src/features/vaults/services/vault.service.ts
Updates TokenService.deployToken() to accept DeployTokenParams object (escrowContractId, tokenName, tokenSymbol) instead of single string. Adds NEXT_PUBLIC_API_URL fallback logic with "/api" default. VaultService similarly implements environment-based API URL resolution.
UI Components & Hooks
apps/backoffice-tokenization/src/features/tokens/deploy/dialog/TokenizeEscrow.tsx, apps/backoffice-tokenization/src/features/tokens/deploy/dialog/useTokenizeEscrow.ts, apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx
Extends TokenizeEscrow dialog with required inputs: token name and token symbol (uppercase alphanumeric, max 12 chars). Updates form hook to pass tokenName/tokenSymbol to deployment service. Adds USDC balance validation in InvestDialog before investment submission with user-friendly insufficient balance messaging.
Investor Tokenization Data & Logic
apps/investor-tokenization/src/features/claim-roi/VaultList.tsx, apps/investor-tokenization/src/features/investments/hooks/useProjectTokenBalances.hook.ts, apps/investor-tokenization/src/features/investments/hooks/useUserInvestments.hook.ts, apps/investor-tokenization/src/features/transparency/ProjectList.tsx, apps/investor-tokenization/src/app/api/token-balance/route.ts
Updates hardcoded vault/project data with new contract IDs (escrowId, tokenSale, vaultContractId). Reduces data set to single vault/project entries. Token balance route adds contract function call fallback before falling back to storage-read for balance retrieval.
Test Snapshot Removals
apps/smart-contracts/contracts/escrow/test_snapshots/tests/test/test_*.1.json (16+ files deleted)
Removes comprehensive test snapshot fixtures for escrow contract scenarios (initialization, funding, dispute flows, etc.), including generator metadata, auth invocations, ledger state, and contract storage. Each snapshot was ~500–1400 LOC.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/UI
    participant Route as Deploy Route Handler
    participant Service as tokenDeploymentService
    participant Client as SorobanClient
    participant Stellar as Stellar/Soroban RPC
    
    User->>Route: POST /api/deploy {escrowContractId, tokenName, tokenSymbol}
    Route->>Route: Validate required fields
    Route->>Route: Check SOURCE_SECRET env
    Route->>Client: new SorobanClient(RPC_URL, SOURCE_SECRET)
    Route->>Service: deployTokenContracts(client, {escrowContractId, tokenName, tokenSymbol})
    
    Service->>Service: Read TokenFactory WASM
    Service->>Service: Read TokenSale WASM
    
    Service->>Client: uploadContractWasm(tokenFactoryWasm)
    Client->>Stellar: buildTx + submitTransaction
    Stellar-->>Client: tokenFactoryHash
    
    Service->>Client: uploadContractWasm(tokenSaleWasm)
    Client->>Stellar: buildTx + submitTransaction
    Stellar-->>Client: tokenSaleHash
    
    Service->>Client: createContract(tokenSaleHash, [placeholder token], "TokenSale")
    Client->>Stellar: buildTx + submitTransaction + waitForTransaction
    Stellar-->>Client: tokenSaleAddress
    
    Service->>Client: createContract(tokenFactoryHash, [name, symbol, escrowId, tokenSale], "TokenFactory")
    Client->>Stellar: buildTx + submitTransaction + waitForTransaction
    Stellar-->>Client: tokenFactoryAddress
    
    Service->>Client: callContract(tokenSaleAddress, "set_token", [tokenFactoryAddress])
    Client->>Stellar: buildTx + submitTransaction
    Stellar-->>Client: success
    
    Service-->>Route: {tokenFactoryAddress, tokenSaleAddress}
    Route-->>User: {success: true, tokenFactoryAddress, tokenSaleAddress}
Loading
sequenceDiagram
    participant UI as InvestDialog UI
    participant Service as InvestmentService
    participant Route as Token Balance Route
    participant Stellar as Stellar SDK/RPC
    
    UI->>Service: Check USDC balance before investment
    Service->>Route: GET /api/token-balance?contractId=usdc&userAddress=X
    
    Route->>Stellar: Simulate contract call: balance(userAddress)
    alt Contract call succeeds
        Stellar-->>Route: balance result
        Route-->>Service: balance value
    else Contract call fails
        Route->>Stellar: Read balance from contract storage
        Stellar-->>Route: stored balance
        Route-->>Service: balance value
    end
    
    Service-->>UI: balance
    UI->>UI: Compare balance vs requested amount
    alt Insufficient balance
        UI-->>UI: Show "Insufficient USDC balance" error
    else Sufficient balance
        UI->>UI: Proceed with investment flow
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Feature/create and enable vault #13: Modifies backoffice-tokenization vault/token deployment code paths including SorobanClient, deployment services, VaultService, and related dialogs/routes.
  • Main #21: Introduces contract error handler and shared Soroban client/token/vault deployment service infrastructure with overlapping implementations.
  • Feature/tokenize escrow #11: Updates TokenizeEscrow UI components, hooks, and token deployment service invocations for escrow tokenization flow.

Poem

🐰 A rabbit hops through Soroban's halls,
With contracts that scale and salts that fall,
Two tokens dance—factory, sale—so free,
While names and symbols bloom for all to see!
Deterministic paths through ledgers deep,
The vault's secrets the token-sale will keep. 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% 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 'Feature/improve token contract and UI' directly describes the main changes: improvements to token contract and backoffice UI for token creation.
Linked Issues check ✅ Passed PR implements all major objectives: user-defined tokenName/tokenSymbol in UI [#24], on-chain metadata storage, Token Sale retains mint authority, Soroban standards compliance, full end-to-end tokenize escrow flow with deployment services, and comprehensive error handling.
Out of Scope Changes check ✅ Passed All changes align with issue scope. Environment files, port configurations, package.json updates, test snapshot deletions, and dependency additions support the core token contract/UI improvements and are not out-of-scope.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

- Update account balance retrieval to handle TypeScript type discrepancies, ensuring accurate balance checks.
- Enhance simulation result validation to ensure robust handling of transaction simulations, improving error handling and user feedback.

These changes enhance the reliability of balance checks and improve the overall robustness of the token balance retrieval process.
Copy link

@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: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/investor-tokenization/src/app/api/deploy/route.ts (2)

5-6: Add early validation for SOURCE_SECRET.

If SOURCE_SECRET is missing, deployment will fail deep in the Soroban client with a cryptic error. Fail fast with a clear message.

Proposed fix
 const RPC_URL = "https://soroban-testnet.stellar.org";
 const SOURCE_SECRET = process.env.SOURCE_SECRET || "";
+
+if (!SOURCE_SECRET) {
+  console.error("SOURCE_SECRET environment variable is not configured");
+}

 export async function POST(request: Request) {
+  if (!SOURCE_SECRET) {
+    return new Response(
+      JSON.stringify({
+        error: "Server configuration error",
+        details: "Deployment service is not properly configured",
+      }),
+      { status: 503 },
+    );
+  }

10-20: Add validation for tokenSymbol format.

Per the PR objectives, the symbol should be validated: "max 12 chars, uppercase". This validation appears to be implemented in the Backoffice UI but is missing from the API, allowing invalid symbols to reach the contract.

Proposed validation
   const { escrowContractId, tokenName, tokenSymbol } = data ?? {};

   if (!escrowContractId || !tokenName || !tokenSymbol) {
     return new Response(
       JSON.stringify({
         error: "Missing required fields",
         details: "escrowContractId, tokenName, and tokenSymbol are required",
       }),
       { status: 400 },
     );
   }

+  if (tokenSymbol.length > 12 || !/^[A-Z]+$/.test(tokenSymbol)) {
+    return new Response(
+      JSON.stringify({
+        error: "Invalid tokenSymbol",
+        details: "tokenSymbol must be uppercase letters only, max 12 characters",
+      }),
+      { status: 400 },
+    );
+  }
🤖 Fix all issues with AI agents
In `@apps/backoffice-tokenization/src/app/api/deploy/route.ts`:
- Around line 8-20: The POST handler calls request.json() without error
handling; wrap the JSON parse in a try/catch inside the POST function to catch
malformed JSON, return a 400 Response with a clear error message if parsing
fails, and only proceed to destructure escrowContractId, tokenName, tokenSymbol
after successful parse; update the Response returned on parse error to match the
route’s existing error shape and reference the POST function and request.json()
call when making changes.
- Around line 96-103: The user-facing error text references an unsupported
"deploymentId" parameter; update the handler in route.ts to either stop
suggesting it or actually accept and forward it: locate the block that builds
userFriendlyMessage (uses isExistingContractError) and either remove the
"deploymentId" suggestion from that message, or parse and validate deploymentId
from the incoming request (body or query) where other params are read, then pass
that deploymentId into deployTokenContracts (and any other places that need it)
so the suggestion is accurate. Ensure you reference the
isExistingContractError/userFriendlyMessage logic and the deployTokenContracts
call when making the change.

In `@apps/backoffice-tokenization/src/app/api/deploy/vault-contract/route.ts`:
- Around line 8-11: The POST handler's call to request.json() can throw on
malformed JSON; wrap the JSON parsing in a try/catch inside the POST function to
catch SyntaxError (and other parsing errors), and return a 400 response with a
clear message when parsing fails; after successful parse, validate the expected
properties ({ admin, enabled, price, token, usdc }) and return a 400 if required
fields are missing or invalid. Use the POST function name and the destructured
variables to locate where to add the try/catch and validation logic.

In `@apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts`:
- Around line 4-11: The POST handler in route.ts reads signedXdr without
validation; add input checks in the POST function to ensure the request body
contains a non-empty string signedXdr before calling TransactionBuilder.fromXDR.
If signedXdr is missing or not a string return a 400 response stating the
parameter is required/invalid; also wrap TransactionBuilder.fromXDR(...) in a
try/catch to return a 400 with the parse error message when XDR is malformed.
Update references in the POST flow (where signedXdr is destructured and where
TransactionBuilder.fromXDR is called) to use the validated value.
- Around line 19-26: The code is incorrectly checking response.successful after
calling server.submitTransaction (which never returns a successful property);
replace that check with try-catch around server.submitTransaction in the handler
so a non-throwing call is treated as success and thrown errors are handled in
the catch block; specifically, wrap the call to
server.submitTransaction(transaction) in a try, use the returned object
(hash/ledger/etc.) as the success path (returning the success
NextResponse.json), and in catch return the FAILED NextResponse.json including
the caught error message for diagnostics.

In
`@apps/backoffice-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts`:
- Around line 8-13: Wrap the call to request.json() in a try-catch to handle
malformed JSON and return a proper 400/Bad Request response (or throw a
structured error) when parsing fails; replace the two console.log calls with
conditional/structured logging (e.g., use a logger or only log when NODE_ENV !==
'production') so debug output isn’t emitted in production, and keep references
to the existing request.json() usage and the console.log lines in route.ts when
making these changes.
- Around line 36-46: Replace the redundant use of StellarSDK.nativeToScVal for
an Address with the Address instance method: when building the contract call for
"availability_for_exchange" (the contract.call invocation), pass new
StellarSDK.Address(adminAddress).toScVal() instead of
StellarSDK.nativeToScVal(new StellarSDK.Address(adminAddress), { type: "address"
}); keep the boolean argument for enabled as-is
(StellarSDK.nativeToScVal(enabled, { type: "bool" })) so the call uses the
Address.toScVal() method for correct and idiomatic address conversion.

In `@apps/investor-tokenization/.env.example`:
- Around line 2-3: The NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE value in
.env.example contains a semicolon and should be wrapped in quotes so shells
parse it as a single value; update the NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE
entry to enclose "Test SDF Network ; September 2015" in quotes (e.g., use
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015") so
the variable is read correctly.

In `@apps/investor-tokenization/src/app/api/token-balance/route.ts`:
- Around line 46-56: The code converts balanceVal (from
StellarSDK.scValToNative) to Number causing precision loss for i128 bigints;
update the handling in the block that computes retval/balanceVal so that if
typeof balanceVal === "bigint" you call balanceVal.toString() and use that
string as the returned balance, otherwise coerce the non-bigint value to string
(e.g., String(balanceVal)), and return that string in NextResponse.json (refer
to variables simulation, retval, StellarSDK.scValToNative, balanceVal, and the
final NextResponse.json).

In `@apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx`:
- Around line 95-113: Replace the Math.floor conversion and silent-fail
handling: in InvestDialog.tsx where you call new
InvestmentService().getTokenBalance(...) and compute adjustedAmount, use
Math.round(values.amount * 1_000_000) (not Math.floor) to compute
adjustedAmount; then check balanceResponse.success and if false throw an Error
including the backend error/message from balanceResponse.error (or a descriptive
message) instead of treating balance as 0 so fetch failures surface rather than
producing an "insufficient balance" message; continue to parse
balanceResponse.balance when success is true and compare currentBalance to
adjustedAmount using those corrected values.
🧹 Nitpick comments (17)
apps/investor-tokenization/src/features/investments/hooks/useUserInvestments.hook.ts (1)

8-19: Gitleaks false positive; consider externalizing contract addresses.

The static analysis warnings are false positives—these are public Stellar/Soroban contract addresses (standard 56-character StrKey format), not API keys or secrets.

However, hardcoding contract addresses in source code creates a maintenance burden: any address change requires a code deployment. Consider moving these to environment variables or fetching from a configuration API/database for easier updates across environments (testnet vs mainnet).

Also, note the inconsistent field ordering between entries (entry 1: escrowId, tokenSale, tokenFactory; entry 2: escrowId, tokenFactory, tokenSale). While functionally irrelevant, consistent ordering improves readability.

apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx (1)

169-181: Make insufficient-balance detection case-insensitive for consistent messaging.

The pre-check error begins with “Insufficient …” (capital I), so the current case-sensitive checks won’t normalize it. Lowercasing first avoids missed matches.

♻️ Suggested refactor
-      if (
-        message.includes("resulting balance is not within the allowed range") ||
-        message.includes("balance is not within") ||
-        message.includes("insufficient balance")
-      ) {
+      const normalizedMessage = message.toLowerCase();
+      if (
+        normalizedMessage.includes("resulting balance is not within the allowed range") ||
+        normalizedMessage.includes("balance is not within") ||
+        normalizedMessage.includes("insufficient balance")
+      ) {
         message = "Insufficient USDC balance. Please ensure your wallet has enough USDC to complete this transaction. You can get testnet USDC from a Stellar testnet faucet.";
       }
apps/backoffice-tokenization/.env.example (1)

3-3: Quote the network passphrase value.

The passphrase contains spaces and a semicolon, which can cause parsing issues with some shell scripts or environment loaders. Wrapping it in quotes ensures consistent behavior.

Suggested fix
-NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
+NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
apps/investor-tokenization/src/features/transparency/ProjectList.tsx (1)

4-12: Consider externalizing hardcoded contract IDs.

Hardcoding contract addresses in component files makes it harder to maintain across different environments (testnet, mainnet, etc.). These could be sourced from environment variables or a shared config, consistent with how other contract IDs are handled in .env.example.

If this is intentional demo data, consider adding a comment to clarify the purpose.

apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts (2)

6-11: Extract Horizon URL to environment variable.

The Horizon URL is hardcoded while other routes use environment variables for RPC endpoints. This improves consistency and makes it easier to switch between testnet/mainnet.

Suggested fix
+const HORIZON_URL = process.env.NEXT_PUBLIC_HORIZON_URL || "https://horizon-testnet.stellar.org";
+
 export async function POST(request: Request) {
   const { signedXdr } = await request.json();
-  const server = new StellarSDK.Horizon.Server(
-    "https://horizon-testnet.stellar.org",
-    {
-      allowHttp: true,
-    },
-  );
+  const server = new StellarSDK.Horizon.Server(HORIZON_URL);

Also add to .env.example:

# Horizon API (optional, defaults to testnet)
NEXT_PUBLIC_HORIZON_URL=https://horizon-testnet.stellar.org

8-10: Remove unnecessary allowHttp option.

The allowHttp: true option is unnecessary when using an HTTPS URL and could pose a security risk if the URL is later changed to use environment variables (accidentally allowing HTTP).

apps/backoffice-tokenization/src/lib/tokenDeploymentService.ts (2)

26-27: Consider async file reads and existence checks.

fs.readFileSync blocks the event loop. While acceptable in server-side handlers, using fs.promises.readFile would be more idiomatic in an async function. Additionally, if a WASM file is missing, the error will be cryptic.

Suggested improvement
+import { access, readFile } from "fs/promises";
+import { constants } from "fs";
-import fs from "fs";
 import path from "path";
 import { SorobanClient } from "./sorobanClient";

...

 export const deployTokenContracts = async (
   client: SorobanClient,
   { escrowContractId, tokenName, tokenSymbol }: TokenDeploymentParams,
 ): Promise<TokenDeploymentResult> => {
+  // Verify WASM files exist before starting deployment
+  await Promise.all([
+    access(tokenFactoryPath, constants.R_OK),
+    access(tokenSalePath, constants.R_OK),
+  ]).catch(() => {
+    throw new Error("WASM files not found. Ensure token_sale.wasm and soroban_token_contract.wasm exist in services/wasm/");
+  });
+
-  const tokenFactoryWasm = fs.readFileSync(tokenFactoryPath);
-  const tokenSaleWasm = fs.readFileSync(tokenSalePath);
+  const [tokenFactoryWasm, tokenSaleWasm] = await Promise.all([
+    readFile(tokenFactoryPath),
+    readFile(tokenSalePath),
+  ]);

72-72: Document or parameterize the hardcoded decimal value.

The decimal value 7 is hardcoded. Consider adding a comment explaining why 7 decimals is used, or making it configurable via TokenDeploymentParams for flexibility.

apps/investor-tokenization/src/features/investments/hooks/useProjectTokenBalances.hook.ts (1)

14-26: Consider centralizing project/contract data.

The hardcoded PROJECT_DATA duplicates information also present in VaultList.tsx. Changes to contract addresses require updating multiple files. Consider:

  1. A shared configuration file for contract addresses.
  2. Fetching project data from an API or on-chain registry.

This would reduce maintenance burden as deployments evolve.

Note: The Gitleaks warning is a false positive—these are public Stellar contract addresses, not secrets.

apps/backoffice-tokenization/src/features/tokens/deploy/dialog/TokenizeEscrow.tsx (1)

131-139: Consider memoizing the watched value to reduce re-renders.

Using form.watch("escrowId") directly in the render causes re-renders on every keystroke in the escrowId field. For a small form this is acceptable, but if performance becomes a concern, consider extracting this to a separate component or using useWatch.

apps/backoffice-tokenization/src/features/tokens/services/token.service.ts (1)

30-38: Consider adding error handling for better error messages.

The method passes through raw axios errors. Consider catching and transforming errors to provide more context to the caller.

♻️ Optional: Add error handling
 async deployToken(params: DeployTokenParams): Promise<DeployTokenResponse> {
+  try {
     const response = await this.axios.post<DeployTokenResponse>("/deploy", {
       escrowContractId: params.escrowContractId,
       tokenName: params.tokenName,
       tokenSymbol: params.tokenSymbol,
     });
 
     return response.data;
+  } catch (error) {
+    if (axios.isAxiosError(error) && error.response?.data?.details) {
+      throw new Error(error.response.data.details);
+    }
+    throw error;
+  }
 }
apps/backoffice-tokenization/src/app/api/deploy/vault-contract/route.ts (2)

5-6: Hardcoded RPC URL reduces deployment flexibility.

RPC_URL is hardcoded to testnet. Consider making this configurable via environment variable for different environments (testnet/mainnet).

♻️ Suggested fix
-const RPC_URL = "https://soroban-testnet.stellar.org";
+const RPC_URL = process.env.SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org";

12-31: Input validation could be more robust.

The validation checks presence but doesn't validate format. For Stellar addresses (admin, token, usdc), consider validating they match Stellar address format to fail fast with a clear error rather than failing deeper in the deployment flow.

apps/backoffice-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts (1)

5-5: Hardcoded RPC URL - same issue as other routes.

Consider using an environment variable for consistency across routes.

♻️ Suggested fix
-const RPC_URL = "https://soroban-testnet.stellar.org";
+const RPC_URL = process.env.SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org";
apps/backoffice-tokenization/src/app/api/deploy/route.ts (3)

5-6: Same RPC URL hardcoding issue.

Consider extracting to a shared constant or environment variable.


33-51: Consider using structured logging instead of console.log.

Multiple console.log statements are present for debugging. For production, consider using a logging library with log levels or making these conditional on a debug flag.


41-51: Balance extraction logic is fragile.

The code assumes account.balances[0] is the relevant balance. Stellar accounts can have multiple balances (XLM native + trustlines). Consider explicitly finding the native XLM balance.

♻️ More robust balance extraction
       const account = await server.getAccount(sorobanClient.publicKey);
-      const balance = account.balances && account.balances.length > 0 
-        ? account.balances[0]?.balance || "unknown"
-        : "unknown";
+      const nativeBalance = account.balances?.find(
+        (b: { asset_type: string }) => b.asset_type === "native"
+      );
+      const balance = nativeBalance?.balance || "unknown";
       console.log(`Account verified. Balance: ${balance}`);

Comment on lines +8 to +20
export async function POST(request: Request) {
const data = await request.json();
const { escrowContractId, tokenName, tokenSymbol } = data ?? {};

if (!escrowContractId || !tokenName || !tokenSymbol) {
return new Response(
JSON.stringify({
error: "Missing required fields",
details: "escrowContractId, tokenName, and tokenSymbol are required",
}),
{ status: 400 },
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add JSON parsing error handling.

Same pattern as other routes - wrap request.json() in try-catch to handle malformed requests gracefully.

🤖 Prompt for AI Agents
In `@apps/backoffice-tokenization/src/app/api/deploy/route.ts` around lines 8 -
20, The POST handler calls request.json() without error handling; wrap the JSON
parse in a try/catch inside the POST function to catch malformed JSON, return a
400 Response with a clear error message if parsing fails, and only proceed to
destructure escrowContractId, tokenName, tokenSymbol after successful parse;
update the Response returned on parse error to match the route’s existing error
shape and reference the POST function and request.json() call when making
changes.

Comment on lines +96 to +103
if (isExistingContractError) {
userFriendlyMessage =
"Contracts are already deployed for this escrowContractId. " +
"To redeploy, you can either: " +
"1. Use a different escrowContractId, or " +
"2. Provide a 'deploymentId' parameter in your request (e.g., {\"deploymentId\": \"v2\"}) to create unique contract addresses. " +
`Error details: ${errorMessage}`;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error message references unsupported deploymentId parameter.

The error message suggests using a deploymentId parameter, but this parameter is not being parsed or validated in the request handling (lines 9-10). Either remove this suggestion or implement the parameter.

🐛 Options to fix

Option 1: Remove the suggestion if not implemented:

       if (isExistingContractError) {
         userFriendlyMessage = 
           "Contracts are already deployed for this escrowContractId. " +
-          "To redeploy, you can either: " +
-          "1. Use a different escrowContractId, or " +
-          "2. Provide a 'deploymentId' parameter in your request (e.g., {\"deploymentId\": \"v2\"}) to create unique contract addresses. " +
+          "Please use a different escrowContractId to deploy new contracts. " +
           `Error details: ${errorMessage}`;
       }

Option 2: Add deploymentId to the request parsing:

-  const { escrowContractId, tokenName, tokenSymbol } = data ?? {};
+  const { escrowContractId, tokenName, tokenSymbol, deploymentId } = data ?? {};

And pass it to deployTokenContracts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isExistingContractError) {
userFriendlyMessage =
"Contracts are already deployed for this escrowContractId. " +
"To redeploy, you can either: " +
"1. Use a different escrowContractId, or " +
"2. Provide a 'deploymentId' parameter in your request (e.g., {\"deploymentId\": \"v2\"}) to create unique contract addresses. " +
`Error details: ${errorMessage}`;
}
if (isExistingContractError) {
userFriendlyMessage =
"Contracts are already deployed for this escrowContractId. " +
"Please use a different escrowContractId to deploy new contracts. " +
`Error details: ${errorMessage}`;
}
🤖 Prompt for AI Agents
In `@apps/backoffice-tokenization/src/app/api/deploy/route.ts` around lines 96 -
103, The user-facing error text references an unsupported "deploymentId"
parameter; update the handler in route.ts to either stop suggesting it or
actually accept and forward it: locate the block that builds userFriendlyMessage
(uses isExistingContractError) and either remove the "deploymentId" suggestion
from that message, or parse and validate deploymentId from the incoming request
(body or query) where other params are read, then pass that deploymentId into
deployTokenContracts (and any other places that need it) so the suggestion is
accurate. Ensure you reference the isExistingContractError/userFriendlyMessage
logic and the deployTokenContracts call when making the change.

Comment on lines +8 to +11
export async function POST(request: Request) {
const data = await request.json();
const { admin, enabled, price, token, usdc } = data ?? {};

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing error handling for malformed JSON requests.

request.json() can throw if the request body is not valid JSON. This will result in an unhandled exception and a generic 500 error instead of a meaningful 400 response.

🐛 Proposed fix
 export async function POST(request: Request) {
+  let data;
+  try {
+    data = await request.json();
+  } catch {
+    return new Response(
+      JSON.stringify({
+        error: "Invalid request",
+        details: "Request body must be valid JSON",
+      }),
+      { status: 400 },
+    );
+  }
-  const data = await request.json();
   const { admin, enabled, price, token, usdc } = data ?? {};
🤖 Prompt for AI Agents
In `@apps/backoffice-tokenization/src/app/api/deploy/vault-contract/route.ts`
around lines 8 - 11, The POST handler's call to request.json() can throw on
malformed JSON; wrap the JSON parsing in a try/catch inside the POST function to
catch SyntaxError (and other parsing errors), and return a 400 response with a
clear message when parsing fails; after successful parse, validate the expected
properties ({ admin, enabled, price, token, usdc }) and return a 400 if required
fields are missing or invalid. Use the POST function name and the destructured
variables to locate where to add the try/catch and validation logic.

Comment on lines +4 to +11
export async function POST(request: Request) {
const { signedXdr } = await request.json();
const server = new StellarSDK.Horizon.Server(
"https://horizon-testnet.stellar.org",
{
allowHttp: true,
},
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add input validation for signedXdr.

The route immediately destructures signedXdr without validation. If the request body is malformed or missing the field, TransactionBuilder.fromXDR() will throw with a confusing error. Other routes in this PR (e.g., availability-for-exchange) validate inputs before processing.

Suggested fix
 export async function POST(request: Request) {
-  const { signedXdr } = await request.json();
+  const data = await request.json();
+  const { signedXdr } = data ?? {};
+
+  if (!signedXdr || typeof signedXdr !== "string") {
+    return NextResponse.json(
+      { 
+        status: "FAILED",
+        error: "Missing required field: signedXdr" 
+      },
+      { status: 400 }
+    );
+  }
+
   const server = new StellarSDK.Horizon.Server(
🤖 Prompt for AI Agents
In `@apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts`
around lines 4 - 11, The POST handler in route.ts reads signedXdr without
validation; add input checks in the POST function to ensure the request body
contains a non-empty string signedXdr before calling TransactionBuilder.fromXDR.
If signedXdr is missing or not a string return a 400 response stating the
parameter is required/invalid; also wrap TransactionBuilder.fromXDR(...) in a
try/catch to return a 400 with the parse error message when XDR is malformed.
Update references in the POST flow (where signedXdr is destructured and where
TransactionBuilder.fromXDR is called) to use the validated value.

Comment on lines +19 to +26
const response = await server.submitTransaction(transaction);
if (!response.successful) {
return NextResponse.json({
status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
message:
"The transaction could not be sent to the Stellar network for some unknown reason. Please try again.",
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the specific file under review to understand the context
cat -n apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts | head -50

Repository: Trustless-Work/Tokenization-Hack

Length of output: 1726


🏁 Script executed:

# Search for submitTransaction usage across the codebase
rg -n "submitTransaction" --type ts -A 5 -B 2

Repository: Trustless-Work/Tokenization-Hack

Length of output: 10766


🏁 Script executed:

# Check the imports in the file to understand what library is being used
head -30 apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts

Repository: Trustless-Work/Tokenization-Hack

Length of output: 1057


🌐 Web query:

Stellar SDK Horizon Server submitTransaction response structure

💡 Result:

HorizonServer.submitTransaction() (POST /transactions) success response (HTTP 200)

The Stellar SDK’s HorizonServer.submitTransaction() resolves with Horizon’s JSON response body, which has these fields: hash, ledger, envelope_xdr, result_xdr, result_meta_xdr. [1]

{
  "hash": "hex transaction hash",
  "ledger": 123456,
  "envelope_xdr": "base64 TransactionEnvelope XDR",
  "result_xdr": "base64 TransactionResult XDR",
  "result_meta_xdr": "base64 TransactionMeta XDR"
}

Failure response (HTTP 400, transaction_failed, etc.)

On failure, Horizon returns a standard error JSON document with type, title, status, detail, plus an extras object that includes (for transaction failures) envelope_xdr, result_xdr, and result_codes (transaction and optionally operations). [2]

{
  "type": "https://stellar.org/horizon-errors/transaction_failed",
  "title": "Transaction Failed",
  "status": 400,
  "detail": "...",
  "extras": {
    "envelope_xdr": "base64 TransactionEnvelope XDR",
    "result_xdr": "base64 TransactionResult XDR",
    "result_codes": {
      "transaction": "tx_failed",
      "operations": ["op_*"]
    }
  }
}

Notes on the *_xdr fields

envelope_xdr, result_xdr, result_meta_xdr are the canonical XDR payloads Horizon returns for transactions. [3]

Sources: [1] [2] [3]


🏁 Script executed:

# Check if there are any TypeScript type definitions in the project for the response
fd -e d.ts -e ts | xargs grep -l "submitTransaction" | head -5

Repository: Trustless-Work/Tokenization-Hack

Length of output: 327


🏁 Script executed:

# Look for package.json to see which version of stellar-sdk is being used
cat apps/backoffice-tokenization/package.json | grep -A 2 -B 2 "stellar-sdk"

Repository: Trustless-Work/Tokenization-Hack

Length of output: 58


🏁 Script executed:

# Check the actual `@stellar/stellar-sdk` version in the project
cat package.json | grep -A 2 -B 2 "stellar-sdk" || cat apps/backoffice-tokenization/package.json | grep "stellar-sdk"

Repository: Trustless-Work/Tokenization-Hack

Length of output: 272


🏁 Script executed:

# Search for Horizon Server type definitions or any custom wrappers
rg -n "Horizon\.Server" --type ts -A 10 | head -50

Repository: Trustless-Work/Tokenization-Hack

Length of output: 2254


🏁 Script executed:

# Check if there's any custom response handling or wrapping
rg -n "successful" --type ts -B 3 -A 3

Repository: Trustless-Work/Tokenization-Hack

Length of output: 30188


Fix error handling: response.successful property does not exist in Horizon API responses.

The Stellar Horizon API's submitTransaction throws on errors rather than returning a response with successful: false. On success, it returns an object with hash, ledger, envelope_xdr, and related fields—but no successful property. The current check at line 20 (if (!response.successful)) will always evaluate to true since the property is undefined, causing all successful transactions to be incorrectly treated as failures.

Replace the successful check with proper try-catch handling: if submitTransaction doesn't throw, the transaction succeeded and the response object is available.

🤖 Prompt for AI Agents
In `@apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts`
around lines 19 - 26, The code is incorrectly checking response.successful after
calling server.submitTransaction (which never returns a successful property);
replace that check with try-catch around server.submitTransaction in the handler
so a non-throwing call is treated as success and thrown errors are handled in
the catch block; specifically, wrap the call to
server.submitTransaction(transaction) in a try, use the returned object
(hash/ledger/etc.) as the success path (returning the success
NextResponse.json), and in catch return the FAILED NextResponse.json including
the caught error message for diagnostics.

Comment on lines +8 to +13
console.log("POST /api/vault-contract/availability-for-exchange called");

const data = await request.json();
const { vaultContractId, adminAddress, enabled } = data ?? {};

console.log("Request data:", { vaultContractId, adminAddress, enabled });
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add JSON parsing error handling and consider removing debug logs in production.

Same pattern issue as other routes - request.json() should be wrapped in try-catch. Also, console.log statements (lines 8, 13) should be removed or converted to conditional/structured logging for production.

🤖 Prompt for AI Agents
In
`@apps/backoffice-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts`
around lines 8 - 13, Wrap the call to request.json() in a try-catch to handle
malformed JSON and return a proper 400/Bad Request response (or throw a
structured error) when parsing fails; replace the two console.log calls with
conditional/structured logging (e.g., use a logger or only log when NODE_ENV !==
'production') so debug output isn’t emitted in production, and keep references
to the existing request.json() usage and the console.log lines in route.ts when
making these changes.

Comment on lines +36 to +46
.addOperation(
contract.call(
"availability_for_exchange",
StellarSDK.nativeToScVal(new StellarSDK.Address(adminAddress), {
type: "address",
}),
StellarSDK.nativeToScVal(enabled, { type: "bool" }),
),
)
.setTimeout(300)
.build();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Stellar SDK nativeToScVal Address Soroban documentation

💡 Result:

In the JavaScript Stellar SDK (@stellar/stellar-sdk), nativeToScVal will convert an Address (or Contract) directly into an scvAddress xdr.ScVal (the Soroban “address” contract value). [1]

Examples (JS)

import StellarSdk from "@stellar/stellar-sdk";

// from a Stellar account (G...) or contract id (C...)
const addr = new StellarSdk.Address("GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN");

// Option A: nativeToScVal (works because it's an Address instance)
const scVal1 = StellarSdk.nativeToScVal(addr);

// Option B: Address helper
const scVal2 = addr.toScVal();

Address exposes toScVal() (and toScAddress()), which is often the simplest/clearest way to produce the correct Soroban XDR. [2]

Soroban/Stellar docs pages that cover this

  • Stellar SDK doc for nativeToScVal (lists “Address or Contract -> scvAddress”). [1]
  • Stellar Docs guide: Convert an address to other types (shows new StellarSdk.Address(...); address.toScVal(); ...). [3]
  • Stellar Docs guide: ScVal conversions (includes “ScVal to address” via Address.fromScVal). [4]

Sources: [1] [2] [3] [4]


🌐 Web query:

Stellar SDK Address toScVal method Soroban

💡 Result:

In the JavaScript Stellar SDK (@stellar/stellar-sdk / stellar-base), Soroban contract arguments use XDR ScVal, and the SDK’s Address class provides toScVal() for this.

JS: Address.toScVal() (Soroban)

  • Create an Address from a StrKey (account G... or contract C...)
  • Call toScVal() to get an xdr.ScVal of type SCV_ADDRESS (internally it wraps toScAddress())
import { Address } from "@stellar/stellar-sdk";

const addr = new Address("GCM5WPR4DDR24FSAX5LIEM4J7AI3KOWJYANSXEPKYXCSZOTAYXE75AFN");
const scVal = addr.toScVal();          // xdr.ScVal (SCV_ADDRESS)
const raw = scVal.toXDR("raw");        // bytes if you need them

Docs: Address.toScVal() / toScAddress() are documented here [1], and Stellar’s conversions guide shows the same usage for Soroban [2]. The implementation is effectively xdr.ScVal.scvAddress(this.toScAddress()) [3].

Python note (different API)

In Python stellar-sdk, the equivalent is stellar_sdk.scval.to_address(address) (not a method on Address) [4].

Sources: [1] [2] [3] [4]


🌐 Web query:

StellarSDK.nativeToScVal address type hint Soroban correct usage

💡 Result:

In js-stellar-sdk, nativeToScVal does not support a "address" type hint for converting a string into an scvAddress. A plain string is interpreted as an scvString (or scvSymbol if you force that) — to get an scvAddress, you must pass an Address (or Contract) instance. (stellar.github.io)

Correct usage (account / contract address)

import { Address, Contract, nativeToScVal } from "@stellar/stellar-sdk";

// G... / M... account address -> scvAddress
const addrScVal = nativeToScVal(new Address("GB..."));
// (equivalently) const addrScVal = new Address("GB...").toScVal();

// C... contract ID -> scvAddress
const contractScVal = nativeToScVal(new Contract("CA..."));
// (equivalently) const contractScVal = new Contract("CA...").address().toScVal();

Address.toScVal() is the direct “address -> ScVal” conversion helper. (stellar.github.io)

Citations:


Use Address.toScVal() instead of nativeToScVal() with type hint.

The pattern StellarSDK.nativeToScVal(new StellarSDK.Address(adminAddress), { type: "address" }) is redundant. When passing an Address instance, the type hint is unnecessary and not required. Use new StellarSDK.Address(adminAddress).toScVal() directly—this is the documented and clearest approach for Soroban address conversions.

🤖 Prompt for AI Agents
In
`@apps/backoffice-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts`
around lines 36 - 46, Replace the redundant use of StellarSDK.nativeToScVal for
an Address with the Address instance method: when building the contract call for
"availability_for_exchange" (the contract.call invocation), pass new
StellarSDK.Address(adminAddress).toScVal() instead of
StellarSDK.nativeToScVal(new StellarSDK.Address(adminAddress), { type: "address"
}); keep the boolean argument for enabled as-is
(StellarSDK.nativeToScVal(enabled, { type: "bool" })) so the call uses the
Address.toScVal() method for correct and idiomatic address conversion.

Comment on lines +2 to +3
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Quote the network passphrase value.

The passphrase contains a semicolon which some shell environments interpret as a command separator. Wrap it in quotes to ensure it's parsed correctly.

Proposed fix
-NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
+NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 3-3: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
In `@apps/investor-tokenization/.env.example` around lines 2 - 3, The
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE value in .env.example contains a
semicolon and should be wrapped in quotes so shells parse it as a single value;
update the NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE entry to enclose "Test SDF
Network ; September 2015" in quotes (e.g., use
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015") so
the variable is read correctly.

Comment on lines 46 to 56
if (simulation?.result?.retval || simulation?.results?.[0]?.retval) {
const retval = simulation.result?.retval || simulation.results?.[0]?.retval;
const balanceVal = StellarSDK.scValToNative(retval);
const balance = typeof balanceVal === "bigint"
? Number(balanceVal)
: Number(balanceVal);

return NextResponse.json({
success: true,
balance: balance.toString(),
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Stellar SDK scValToNative i128 return type bigint

💡 Result:

In @stellar/stellar-sdk (and js-stellar-base / js-soroban-client), scValToNative(scv) converts i128 (and other 64/128/256-bit ints) to a JavaScript bigint (not number). (stellar.github.io)

So if your contract returns an i128, you should expect:

const v = scValToNative(scv); // v is a bigint

If you want to keep the “this is i128” type information (instead of a plain bigint), wrap it:

const bigi = scValToNative(scv) as bigint;
const i128 = new XdrLargeInt('i128', bigi); // typed wrapper

XdrLargeInt supports toBigInt() (and toI128() to re-encode back into an ScVal). (stellar.github.io)

Citations:


🌐 Web query:

Stellar SDK JavaScript scValToNative bigint precision loss i128

💡 Result:

scValToNative() does not lose precision for i128—it converts u64/i64/u128/i128/u256/i256 contract integers to JavaScript bigint (not number), so the full 128-bit value is preserved. [1]

If you’re seeing “precision loss”, it’s almost always happening after conversion, e.g.:

  • You (or a library) coerces it to Number(...) / uses +value / puts it in a place that expects a JS number (numbers can’t exactly represent large integers).
  • You try to JSON.stringify an object containing a bigint (JSON can’t serialize bigint; people often “fix” this by converting to number, which can lose precision). [2]

How to avoid issues:

  • Keep the value as bigint end-to-end:
    • const x = scValToNative(scv); // bigint for i128 [1]
  • If you must serialize, convert to string (lossless):
    • const s = x.toString();
  • When sending back to a contract, convert from bigint/string using ScInt (and force i128 if needed):
    • nativeToScVal(new ScInt(s, { type: "i128" }).toI128()) (or equivalent) [3]

Sources:
[1] Stellar JS SDK docs: scValToNative returns bigint for i128 and other large integer ScVals. (stellar.github.io)
[2] MDN: BigInt can’t be serialized by JSON.stringify (common source of downstream conversion/precision problems). (developer.mozilla.org)
[3] Stellar JS SDK docs: ScInt supports i128 and conversions like toI128() / toBigInt(). (stellar.github.io)

Citations:


🌐 Web query:

JavaScript Number.MAX_SAFE_INTEGER bigint overflow precision

💡 Result:

  • Number precision limit (why MAX_SAFE_INTEGER matters): JavaScript Number is IEEE‑754 double; integers are only exact in the range -(2^53−1) … (2^53−1). Number.MAX_SAFE_INTEGER === 2**53 - 1 === 9007199254740991. Beyond that, integer math and even comparisons can become wrong because different integers round to the same Number value. (developer.mozilla.org)

    Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true (precision lost)
  • BigInt for exact large integers: BigInt represents integers with arbitrary magnitude and supports exact integer arithmetic (no fixed “overflow” like 64-bit ints; practically limited by memory/time). (developer.mozilla.org)

    const x = BigInt(Number.MAX_SAFE_INTEGER);
    x + 1n === x + 2n // false (exact)
  • Don’t mix Number and BigInt implicitly: Arithmetic operators require both operands to be the same numeric type; mixing throws a TypeError unless you convert explicitly. (developer.mozilla.org)

    1n + 1        // TypeError
    1n + BigInt(1) // ok
  • Conversion gotcha (common precision trap): If you do BigInt(hugeNumber), the precision may already be lost before conversion; prefer converting from a string when values may exceed MAX_SAFE_INTEGER. (developer.mozilla.org)

    BigInt("9007199254740993") // exact
    BigInt(9007199254740993)   // NOT reliable (the Number literal may already be rounded)
  • Quick check: use Number.isSafeInteger(n) to detect when a Number can represent an integer safely. (developer.mozilla.org)

Citations:


Avoid precision loss by converting bigint directly to string.
scValToNative returns bigint for i128, which can exceed JavaScript's Number.MAX_SAFE_INTEGER (2^53−1). Converting to Number causes precision loss; use bigint.toString() instead.

✅ Suggested fix
-        const balanceVal = StellarSDK.scValToNative(retval);
-        const balance = typeof balanceVal === "bigint" 
-          ? Number(balanceVal) 
-          : Number(balanceVal);
-        
-        return NextResponse.json({
-          success: true,
-          balance: balance.toString(),
-        });
+        const balanceVal = StellarSDK.scValToNative(retval);
+        const balance =
+          typeof balanceVal === "bigint" ? balanceVal.toString() : String(balanceVal);
+
+        return NextResponse.json({
+          success: true,
+          balance,
+        });
🤖 Prompt for AI Agents
In `@apps/investor-tokenization/src/app/api/token-balance/route.ts` around lines
46 - 56, The code converts balanceVal (from StellarSDK.scValToNative) to Number
causing precision loss for i128 bigints; update the handling in the block that
computes retval/balanceVal so that if typeof balanceVal === "bigint" you call
balanceVal.toString() and use that string as the returned balance, otherwise
coerce the non-bigint value to string (e.g., String(balanceVal)), and return
that string in NextResponse.json (refer to variables simulation, retval,
StellarSDK.scValToNative, balanceVal, and the final NextResponse.json).

Comment on lines +95 to +113
// Check USDC balance before attempting purchase
const investmentService = new InvestmentService();
const adjustedAmount = Math.floor(values.amount * 1_000_000); // Convert to microUSDC

const balanceResponse = await investmentService.getTokenBalance({
tokenFactoryAddress: DEFAULT_USDC_ADDRESS,
address: walletAddress,
});

const currentBalance = balanceResponse.success
? parseFloat(balanceResponse.balance || "0")
: 0;

if (currentBalance < adjustedAmount) {
const balanceInUSDC = currentBalance / 1_000_000;
throw new Error(
`Insufficient USDC balance. You have ${balanceInUSDC.toFixed(2)} USDC but need ${values.amount.toFixed(2)} USDC. Please add more USDC to your wallet.`
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "token-balance" -C3

Repository: Trustless-Work/Tokenization-Hack

Length of output: 4025


🏁 Script executed:

rg -n "TokenBalanceResponse" -C3

Repository: Trustless-Work/Tokenization-Hack

Length of output: 2357


🏁 Script executed:

rg -n "getTokenBalance" -C5

Repository: Trustless-Work/Tokenization-Hack

Length of output: 11981


🏁 Script executed:

fd -type f -name "*.ts" -path "*/api/token-balance*" -o -path "*/app/api/*" | head -20

Repository: Trustless-Work/Tokenization-Hack

Length of output: 246


🏁 Script executed:

find apps/investor-tokenization/app/api -type f -name "*.ts" -o -name "*.js" | head -20

Repository: Trustless-Work/Tokenization-Hack

Length of output: 145


🏁 Script executed:

rg -n "balance.*return\|return.*balance" apps/investor-tokenization/app/api -A2 -B2

Repository: Trustless-Work/Tokenization-Hack

Length of output: 150


🏁 Script executed:

find apps/investor-tokenization -type f -name "*.ts" -o -name "*.js" | grep -i api | head -20

Repository: Trustless-Work/Tokenization-Hack

Length of output: 618


🏁 Script executed:

rg -l "token-balance" --type ts --type tsx

Repository: Trustless-Work/Tokenization-Hack

Length of output: 103


🏁 Script executed:

rg -n "scValToNative.*balance\|balance.*toFixed\|balance.*1.*000.*000" apps/investor-tokenization -C3

Repository: Trustless-Work/Tokenization-Hack

Length of output: 58


🏁 Script executed:

cat -n apps/investor-tokenization/src/app/api/token-balance/route.ts

Repository: Trustless-Work/Tokenization-Hack

Length of output: 5889


🏁 Script executed:

cat -n apps/investor-tokenization/TOKEN_BALANCE_SYSTEM.md | head -150

Repository: Trustless-Work/Tokenization-Hack

Length of output: 5044


🏁 Script executed:

rg -n "microUSDC\|decimal\|unit\|scale" apps/investor-tokenization/TOKEN_BALANCE_SYSTEM.md

Repository: Trustless-Work/Tokenization-Hack

Length of output: 58


🏁 Script executed:

rg -n "decimals\|1_000_000\|balance.*amount\|amount.*balance" apps/investor-tokenization/src -C3

Repository: Trustless-Work/Tokenization-Hack

Length of output: 58


🏁 Script executed:

cat -n apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx | sed -n '85,125p'

Repository: Trustless-Work/Tokenization-Hack

Length of output: 1703


🏁 Script executed:

rg -n "decimals\|USDC\|6" apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx -C2

Repository: Trustless-Work/Tokenization-Hack

Length of output: 58


🏁 Script executed:

cat -n apps/investor-tokenization/src/app/api/token-metadata/route.ts

Repository: Trustless-Work/Tokenization-Hack

Length of output: 4444


Use Math.round() for microUSDC conversion and throw on balance retrieval failures.

The current code has two issues:

  1. Math.floor on float conversion: IEEE 754 rounding can cause undercount. For example, 100.1 * 1_000_000 becomes 100099999.99999999, and Math.floor yields 100099999 instead of 100100000. This lets insufficient balances pass the pre-check and fail on-chain.

  2. Silent error masking: The TokenBalanceResponse has an error? field, but the code treats a failed response (success: false) as a 0 balance, hiding the actual backend error. This always shows "insufficient balance" even when the real issue is a fetch failure.

Suggested fix
-      const adjustedAmount = Math.floor(values.amount * 1_000_000); // Convert to microUSDC
+      const adjustedAmount = Math.round(values.amount * 1_000_000); // Convert to microUSDC

-      const currentBalance = balanceResponse.success 
-        ? parseFloat(balanceResponse.balance || "0")
-        : 0;
+      if (!balanceResponse.success) {
+        throw new Error(
+          balanceResponse.error ?? "Failed to fetch USDC balance."
+        );
+      }
+      const currentBalance = Number(balanceResponse.balance ?? 0);
🤖 Prompt for AI Agents
In `@apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx`
around lines 95 - 113, Replace the Math.floor conversion and silent-fail
handling: in InvestDialog.tsx where you call new
InvestmentService().getTokenBalance(...) and compute adjustedAmount, use
Math.round(values.amount * 1_000_000) (not Math.floor) to compute
adjustedAmount; then check balanceResponse.success and if false throw an Error
including the backend error/message from balanceResponse.error (or a descriptive
message) instead of treating balance as 0 so fetch failures surface rather than
producing an "insufficient balance" message; continue to parse
balanceResponse.balance when success is true and compare currentBalance to
adjustedAmount using those corrected values.

Copy link

@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: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/investor-tokenization/src/app/api/token-balance/route.ts (2)

4-4: Use environment variable for RPC URL.

RPC_URL is hardcoded but NEXT_PUBLIC_SOROBAN_RPC_URL is defined in .env.example. This should use the environment variable for consistency and configurability.

Proposed fix
-const RPC_URL = "https://soroban-testnet.stellar.org";
+const RPC_URL = process.env.NEXT_PUBLIC_SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org";

115-121: Same precision loss issue in fallback path.

Apply the same fix here to avoid Number conversion for large balances.

Proposed fix
     let balance: number;
     try {
       const balanceVal = StellarSDK.scValToNative(storageValue);
-      balance = typeof balanceVal === "bigint" 
-        ? Number(balanceVal) 
-        : Number(balanceVal);
+      const balanceStr = typeof balanceVal === "bigint" 
+        ? balanceVal.toString() 
+        : String(balanceVal);
+      
+      return NextResponse.json({
+        success: true,
+        balance: balanceStr,
+      });
     } catch (parseError) {

Note: This requires restructuring the control flow slightly since balance was used after the try-catch block.

apps/investor-tokenization/src/app/api/deploy/route.ts (1)

10-33: Add a SOURCE_SECRET guard before deployment

Without an explicit check, this route can fail with unclear errors when the secret is unset.

🐛 Proposed fix
   if (!escrowContractId || !tokenName || !tokenSymbol) {
     return new Response(
       JSON.stringify({
         error: "Missing required fields",
         details: "escrowContractId, tokenName, and tokenSymbol are required",
       }),
       { status: 400 },
     );
   }
+
+  if (!SOURCE_SECRET) {
+    return new Response(
+      JSON.stringify({
+        error: "Configuration Error",
+        details:
+          "SOURCE_SECRET environment variable is not set. Please configure it in your .env.local file.",
+      }),
+      { status: 500 },
+    );
+  }
🤖 Fix all issues with AI agents
In `@apps/backoffice-tokenization/.env.example`:
- Line 3: The NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE entry contains spaces and a
semicolon which can break shell/dotenv parsing; update the .env example so the
value for NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE is wrapped in quotes (e.g.,
"Test SDF Network ; September 2015") to ensure it is parsed as a single string;
change the line defining NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE accordingly in
apps/backoffice-tokenization/.env.example.

In `@apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts`:
- Around line 4-42: Validate the incoming signedXdr at the top of the POST
handler in route.ts (ensure it's present and a non-empty string) and return a
400 Bad Request via NextResponse when invalid; wrap request.json parse in
try/catch to return 400 for malformed JSON. After submitTransaction, map
outcomes to appropriate HTTP statuses: return 200 on success (include hash), 502
Bad Gateway when submission to Horizon fails (response.successful === false),
and 422 Unprocessable Entity for XDR decode errors thrown by
TransactionBuilder.fromXDR; finally, in the outer catch return 500 Internal
Server Error for unexpected errors and include the error.message in the JSON
body for debugging. Ensure you use NextResponse.json(payload, { status: <code>
}) in each case and reference the POST function, signedXdr variable,
TransactionBuilder.fromXDR, server.submitTransaction, and response.successful
when making the changes.

In `@apps/backoffice-tokenization/src/lib/sorobanClient.ts`:
- Around line 164-180: getContractAddressFromSalt currently just throws and
breaks the path in simulateContractCreation; either implement the deterministic
Soroban contract address calculation here or remove the method and adjust
simulateContractCreation to handle existing contracts without calling it. Fix
option A: implement getContractAddressFromSalt(wasmHash, salt) to return the
deterministic address using the same algorithm/formula used elsewhere (e.g.,
hashing salt + wasmHash + network prefixes or reusing any existing createAddress
utility) and return a string address. Fix option B: remove
getContractAddressFromSalt and update simulateContractCreation to treat the
"contract already exists" case as a success (return existing address from the
network query) or throw a clearer error upstream; in either case update
references to getContractAddressFromSalt so no code path relies on a method that
always throws.
- Around line 255-296: The code in simulateContractCreation calls
this.server.prepareTransaction(transaction) and then redundantly calls
this.server.simulateTransaction(preparedTx); remove the explicit
simulateTransaction call and instead use the simulation data returned/applied by
prepareTransaction (use the simulation/auth/fee info available on preparedTx) —
update the logic that currently reads and checks the local simulation variable
(simulation/result/retval) to reference the simulation data on preparedTx,
retain the existing error handling for "contract already exists"/"ExistingValue"
and the final Address.fromScVal(...) return, and delete the extra
this.server.simulateTransaction(preparedTx) invocation and its local simulation
assignment.

In `@apps/evidence-service/src/index.ts`:
- Line 9: The default port constant PORT in index.ts was changed to 3003 but
documentation and examples still reference 3001; update the PORT usage and all
examples accordingly: change the PORT default to 3003 where declared (the PORT
constant), update README references that mention the old default, update the
environment sample to set PORT=3003 and adjust its comment, update the example
client URL (the localhost URL in evidenceClient.example.ts) to use :3003, and
change the dev script port in package.json to 3003 so the services connect
correctly.

In `@apps/investor-tokenization/.env.example`:
- Line 3: The NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE value contains spaces and a
semicolon which can break dotenv parsing; update the .env example by surrounding
the value for NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE with quotes (e.g., use
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="...") so the entire string, including
spaces/semicolons, is treated as a single value.

In `@apps/investor-tokenization/src/app/api/token-balance/route.ts`:
- Around line 50-58: The code converts balanceVal (from
StellarSDK.scValToNative) to Number which can lose precision for i128 values;
instead, in the route handler keep bigint/string semantics: detect if balanceVal
is a bigint and call toString(), otherwise convert numeric values to string via
String(balanceVal), then return that string in NextResponse.json (refer to
balanceVal, StellarSDK.scValToNative, and NextResponse.json) so no intermediate
Number conversion occurs and large balances are preserved.

In
`@apps/investor-tokenization/src/features/investments/hooks/useUserInvestments.hook.ts`:
- Around line 8-19: Duplicate PROJECT_DATA exists in useUserInvestments.hook.ts
and useProjectTokenBalances.hook.ts; extract PROJECT_DATA into a single shared
module (e.g., shared/projectData.ts) and export it, then replace the inline
array in both hooks with an import (import { PROJECT_DATA } from
"@/shared/projectData") so both use the same source of truth; update any
relative import paths in useUserInvestments and useProjectTokenBalances to
reference the new module and remove the duplicated constant declarations.

In `@apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx`:
- Around line 104-113: The code currently treats a failed balance API
(balanceResponse.success === false) as a zero balance which causes misleading
"Insufficient USDC" errors; update the logic in InvestDialog where
currentBalance is derived (the balanceResponse parsing and the subsequent
insufficient-balance check comparing currentBalance to adjustedAmount) to
explicitly handle balanceResponse.success === false by throwing or returning a
distinct, retryable error (e.g., "Failed to retrieve balance" with the API
error/details) instead of using 0; only perform the numeric comparison and
potential "insufficient funds" error when balanceResponse.success is true and a
numeric balance is available.
- Around line 95-109: Replace the manual scaling/rounding in InvestDialog.tsx
that uses Math.floor(values.amount * 1_000_000) with the shared utility
adjustPricesToMicroUSDC (which uses the backend's 1e7 scale) and use Decimal for
exact arithmetic; specifically, import adjustPricesToMicroUSDC from
"@/utils/adjustedAmounts" and compute adjustedAmount =
adjustPricesToMicroUSDC(values.amount) (or the equivalent API call the util
exposes), use Decimal for any rounding/formatting instead of Math.floor, and
compare currentBalance (parsed from InvestmentService.getTokenBalance) against
that adjustedAmount and compute balanceInUSDC by dividing currentBalance by the
same 1e7 scale (via Decimal) so balance validation and messages use the correct
precision.

In `@apps/investor-tokenization/src/features/transparency/ProjectList.tsx`:
- Around line 4-12: The array named "data" in ProjectList.tsx duplicates
contract addresses already defined as "PROJECT_DATA" in the hooks; extract this
project list (including escrowId, tokenSale, tokenFactory, src and the
DummyContent reference) into a single shared module (e.g., export const
PROJECT_DATA or PROJECTS) and update ProjectList.tsx to import that constant
instead of declaring "data", and update the two hook files to import the same
constant rather than duplicating the entries so all components reference the
single source of truth.

In `@apps/investor-tokenization/src/lib/sorobanClient.ts`:
- Around line 164-180: getContractAddressFromSalt currently always throws
despite returning Promise<string>, making the simulateContractCreation code path
that calls it unreachable; either implement the deterministic address derivation
here or remove the method and surface the error where it’s used. Fix option A:
implement deterministic address calculation inside getContractAddressFromSalt
(use wasmHash and salt per Soroban address derivation rules) and return the
string address so simulateContractCreation can continue; Fix option B: delete
getContractAddressFromSalt and replace the call in simulateContractCreation with
an explicit thrown Error (or alternative handling) referencing the
escrowContractId so behavior is obvious. Ensure you update/type signatures
accordingly and keep references to getContractAddressFromSalt and
simulateContractCreation consistent.
- Around line 277-278: The code redundantly calls this.server.prepareTransaction
which itself runs simulation; remove the unnecessary prepareTransaction call and
invoke this.server.simulateTransaction directly with the original transaction
object so you only perform one RPC. Update the block that currently declares
preparedTx and calls simulateTransaction(preparedTx) to call
simulateTransaction(transaction) and then use simulation.result.retval as
before; ensure you also remove the preparedTx variable and any references to it
(symbols: prepareTransaction, simulateTransaction, preparedTx,
simulation.result.retval, transaction).
🧹 Nitpick comments (16)
apps/investor-tokenization/src/features/investments/hooks/useProjectTokenBalances.hook.ts (2)

15-26: Consider externalizing hardcoded contract addresses.

PROJECT_DATA contains hardcoded contract IDs that are duplicated across multiple files (useUserInvestments.hook.ts, ProjectList.tsx). This creates maintenance burden when addresses change.

Consider:

  1. Moving this data to a shared config file or constants module
  2. Loading dynamically from an API or environment configuration

Note: The static analysis "API Key" warning is a false positive—these are public Stellar contract addresses, not secrets.


32-34: Consider memoizing InvestmentService instance.

A new InvestmentService is instantiated on every render. If the service is stateless, consider moving it outside the component or memoizing it.

// Option 1: Move outside if stateless
const investmentService = new InvestmentService();

export const useProjectTokenBalances = () => {
  const { walletAddress } = useWalletContext();
  // ...
apps/investor-tokenization/src/features/investments/hooks/useUserInvestments.hook.ts (1)

35-38: Same InvestmentService instantiation pattern.

Same observation as useProjectTokenBalances.hook.ts—consider memoizing or extracting the service instance.

apps/investor-tokenization/src/lib/sorobanClient.ts (2)

145-150: Hardcoded Networks.TESTNET limits flexibility.

This hardcodes the network passphrase to testnet. If this client needs to work on mainnet or futurenet, consider accepting the network passphrase via configuration.

Proposed change

Add networkPassphrase to SorobanClientConfig:

 export type SorobanClientConfig = {
   rpcUrl: string;
   sourceSecret: string;
+  networkPassphrase?: string;
   fee?: string;
   timeoutSeconds?: number;
   maxAttempts?: number;
   pollDelayMs?: number;
 };

Then use it in buildBaseTx:

 private buildBaseTx(account: AccountLike) {
   return new TransactionBuilder(account, {
     fee: this.config.fee,
-    networkPassphrase: Networks.TESTNET,
+    networkPassphrase: this.config.networkPassphrase,
   });
 }

213-252: Consider extracting duplicated "contract exists" error detection logic.

The pattern matching for existing contract errors is repeated in createContractWithSalt (lines 220-225), submitTransaction (lines 316-318), and result checking (lines 343-347). Extracting to a helper function would improve maintainability.

Proposed helper function
private isContractExistsError(errorStr: string): boolean {
  const normalized = errorStr.toLowerCase();
  return (
    normalized.includes("contract already exists") ||
    normalized.includes("existingvalue") ||
    (normalized.includes("storage") && normalized.includes("existing")) ||
    (normalized.includes("hosterror") && normalized.includes("storage"))
  );
}

Then use it in all three locations:

-const isExistingContractError = 
-  errorMessage.includes("CONTRACT_ALREADY_EXISTS") ||
-  errorStr.includes("contract already exists") || 
-  ...
+const isExistingContractError = 
+  errorMessage.includes("CONTRACT_ALREADY_EXISTS") ||
+  this.isContractExistsError(errorStr);
apps/backoffice-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts (1)

8-13: Redact addresses in request logs

Wallet/contract IDs can be sensitive; consider masking or gating logs by environment.

♻️ Suggested change
-  console.log("Request data:", { vaultContractId, adminAddress, enabled });
+  const redact = (value?: string) =>
+    value ? `${value.slice(0, 4)}…${value.slice(-4)}` : value;
+  if (process.env.NODE_ENV !== "production") {
+    console.log("Request data:", {
+      vaultContractId: redact(vaultContractId),
+      adminAddress: redact(adminAddress),
+      enabled,
+    });
+  }
apps/backoffice-tokenization/src/lib/vaultDeploymentService.ts (1)

23-47: Consider adding error handling for WASM file read.

If the WASM file is missing or unreadable, fs.readFileSync will throw a low-level error. A more descriptive error would improve debuggability.

Proposed improvement
 export const deployVaultContract = async (
   client: SorobanClient,
   { admin, enabled, price, token, usdc }: VaultDeploymentParams,
 ): Promise<VaultDeploymentResult> => {
-  const vaultWasm = fs.readFileSync(vaultContractPath);
+  let vaultWasm: Buffer;
+  try {
+    vaultWasm = fs.readFileSync(vaultContractPath);
+  } catch (err) {
+    throw new Error(`Failed to read Vault WASM at ${vaultContractPath}: ${err instanceof Error ? err.message : String(err)}`);
+  }
apps/backoffice-tokenization/src/app/api/deploy/route.ts (2)

5-6: Hardcoded RPC URL limits flexibility.

The testnet RPC URL is hardcoded. Consider making this configurable via environment variable for easier deployment to different networks.

Proposed improvement
-const RPC_URL = "https://soroban-testnet.stellar.org";
+const RPC_URL = process.env.SOROBAN_RPC_URL || "https://soroban-testnet.stellar.org";

41-54: Dynamic import is unnecessary.

The @stellar/stellar-sdk is already imported in sorobanClient.ts. The dynamic import adds overhead and the balance check is for logging only. Consider either removing this check entirely or using the SDK consistently.

Option 1: Remove unnecessary balance check

Since the balance check is purely informational and continues regardless of outcome, consider removing it to simplify the code:

-    // Verify account exists and has balance
-    try {
-      const StellarSDK = await import("@stellar/stellar-sdk");
-      const server = new StellarSDK.rpc.Server(RPC_URL);
-      const account = await server.getAccount(sorobanClient.publicKey);
-      // Account object has balances property, but TypeScript types may not include it
-      const balances = (account as any).balances;
-      const balance = balances && Array.isArray(balances) && balances.length > 0 
-        ? balances[0]?.balance || "unknown"
-        : "unknown";
-      console.log(`Account verified. Balance: ${balance}`);
-    } catch (accountError) {
-      console.warn("Could not verify account balance:", accountError);
-      // Continue anyway, the transaction will fail with a clearer error if account doesn't exist
-    }
apps/backoffice-tokenization/src/app/api/deploy/vault-contract/route.ts (2)

5-6: Consider extracting shared configuration.

Both deploy/route.ts and deploy/vault-contract/route.ts define the same hardcoded RPC_URL. Consider extracting to a shared configuration module.


61-70: Error handling is less comprehensive than the sibling route.

The token deployment route at deploy/route.ts provides user-friendly error messages for timeout, insufficient funds, and existing contract scenarios. This vault route only returns the raw error message. Consider applying consistent error handling patterns.

The related route at apps/backoffice-tokenization/src/app/api/vault-contract/availability-for-exchange/route.ts (lines 56-65) uses extractContractError for structured error responses.

Proposed improvement to match sibling route patterns
   } catch (error) {
     console.error("Vault deployment error:", error);
+    const errorMessage = error instanceof Error ? error.message : String(error);
+    
+    let userFriendlyMessage = errorMessage;
+    if (errorMessage.includes("timeout")) {
+      userFriendlyMessage = 
+        "The deployment is taking longer than expected. " +
+        "Please wait and try again. " +
+        `Error details: ${errorMessage}`;
+    } else if (errorMessage.includes("insufficient")) {
+      userFriendlyMessage = 
+        "Insufficient balance for transaction fees. " +
+        `Error details: ${errorMessage}`;
+    }
+    
     return new Response(
       JSON.stringify({
         error: "Internal Server Error",
-        details: error instanceof Error ? error.message : String(error),
+        details: userFriendlyMessage,
       }),
       { status: 500 },
     );
   }
apps/investor-tokenization/src/lib/tokenDeploymentService.ts (2)

29-39: WASM upload strategy differs from backoffice version.

This file uses Promise.all for parallel uploads, while apps/backoffice-tokenization/src/lib/tokenDeploymentService.ts performs sequential uploads. The backoffice version comment states "Upload WASM files sequentially to avoid overwhelming the network". Consider aligning the approach for consistency and to avoid potential network issues.

Option: Align with sequential upload pattern
-  // Upload WASM files in parallel for better performance
-  const [tokenFactoryWasmHash, tokenSaleWasmHash] = await Promise.all([
-    client.uploadContractWasm(
-      tokenFactoryWasm,
-      "TokenFactory WASM upload",
-    ),
-    client.uploadContractWasm(
-      tokenSaleWasm,
-      "TokenSale WASM upload",
-    ),
-  ]);
+  // Upload WASM files sequentially to avoid overwhelming the network
+  // and to get better error messages if one fails
+  console.log("Starting TokenFactory WASM upload...");
+  const tokenFactoryWasmHash = await client.uploadContractWasm(
+    tokenFactoryWasm,
+    "TokenFactory WASM upload",
+  );
+  console.log("TokenFactory WASM uploaded successfully");
+
+  console.log("Starting TokenSale WASM upload...");
+  const tokenSaleWasmHash = await client.uploadContractWasm(
+    tokenSaleWasm,
+    "TokenSale WASM upload",
+  );
+  console.log("TokenSale WASM uploaded successfully");

22-85: Significant code duplication with backoffice tokenDeploymentService.

This file is nearly identical to apps/backoffice-tokenization/src/lib/tokenDeploymentService.ts. Consider extracting the shared deployment logic into a common package to avoid drift and maintenance burden.

apps/backoffice-tokenization/src/lib/tokenDeploymentService.ts (1)

11-15: Consider adding validation constraints documentation.

Per PR objectives, tokenSymbol should be validated (max 12 chars, uppercase). While the UI validates this, consider documenting the constraints in the type definition or adding server-side validation for defense in depth.

Optional: Add JSDoc constraints
 export type TokenDeploymentParams = {
   escrowContractId: string;
+  /** Token name for the contract */
   tokenName: string;
+  /** Token symbol (max 12 chars, uppercase recommended) */
   tokenSymbol: string;
 };
apps/backoffice-tokenization/src/lib/sorobanClient.ts (2)

145-154: Network passphrase is hardcoded to TESTNET.

This limits the client to testnet usage only. Consider making the network configurable for production deployment flexibility.

Proposed improvement
 export type SorobanClientConfig = {
   rpcUrl: string;
   sourceSecret: string;
   fee?: string;
   timeoutSeconds?: number;
   maxAttempts?: number;
   pollDelayMs?: number;
+  networkPassphrase?: string;
 };

 // In constructor:
-    networkPassphrase: Networks.TESTNET,
+    networkPassphrase: this.config.networkPassphrase,

 // Add to config defaults:
+  networkPassphrase = Networks.TESTNET,

213-252: Duplicated error detection logic.

The "contract already exists" error detection pattern is duplicated in createContractWithSalt, submitTransaction, and the deploy route. Consider extracting to a helper function.

Proposed helper extraction
private static isContractExistsError(errorMessage: string): boolean {
  const errorStr = errorMessage.toLowerCase();
  return (
    errorMessage.includes("CONTRACT_ALREADY_EXISTS") ||
    errorStr.includes("contract already exists") ||
    errorStr.includes("existingvalue") ||
    (errorStr.includes("storage") && errorStr.includes("existing")) ||
    (errorStr.includes("hosterror") && errorStr.includes("storage") && errorStr.includes("existing"))
  );
}

@@ -0,0 +1,17 @@
# Soroban/Stellar Network Configuration
NEXT_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Quote the network passphrase to prevent parsing issues.

The passphrase contains spaces and a semicolon which can cause issues when parsed by shell or dotenv libraries.

Proposed fix
-NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
+NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 3-3: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
In `@apps/backoffice-tokenization/.env.example` at line 3, The
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE entry contains spaces and a semicolon
which can break shell/dotenv parsing; update the .env example so the value for
NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE is wrapped in quotes (e.g., "Test SDF
Network ; September 2015") to ensure it is parsed as a single string; change the
line defining NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE accordingly in
apps/backoffice-tokenization/.env.example.

Comment on lines +4 to +42
export async function POST(request: Request) {
const { signedXdr } = await request.json();
const server = new StellarSDK.Horizon.Server(
"https://horizon-testnet.stellar.org",
{
allowHttp: true,
},
);

try {
const transaction = StellarSDK.TransactionBuilder.fromXDR(
signedXdr,
StellarSDK.Networks.TESTNET,
);

const response = await server.submitTransaction(transaction);
if (!response.successful) {
return NextResponse.json({
status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
message:
"The transaction could not be sent to the Stellar network for some unknown reason. Please try again.",
});
}
return NextResponse.json({
status: StellarSDK.rpc.Api.GetTransactionStatus.SUCCESS,
message:
"The transaction has been successfully sent to the Stellar network.",
hash: response.hash,
});
} catch (error) {
console.error("Transaction submission error:", error);
return NextResponse.json({
status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
message:
error instanceof Error
? error.message
: "An unknown error occurred while submitting the transaction.",
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's verify the file exists and check its current content
cd apps/backoffice-tokenization && find . -name "route.ts" -path "*/send-transaction/*" 2>/dev/null | head -5

Repository: Trustless-Work/Tokenization-Hack

Length of output: 122


🏁 Script executed:

# Let's locate the file more broadly
find . -type f -name "route.ts" | grep -i "send-transaction" 2>/dev/null

Repository: Trustless-Work/Tokenization-Hack

Length of output: 225


🏁 Script executed:

# Search more broadly for the file
git ls-files | grep -i "send-transaction"

Repository: Trustless-Work/Tokenization-Hack

Length of output: 221


🏁 Script executed:

# Read the backoffice send-transaction route file
cat -n apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts

Repository: Trustless-Work/Tokenization-Hack

Length of output: 1726


🏁 Script executed:

# Search for NextResponse.json usage in the codebase to see patterns
rg "NextResponse\.json" apps/backoffice-tokenization --type ts -A 2 | head -50

Repository: Trustless-Work/Tokenization-Hack

Length of output: 1969


🏁 Script executed:

# Also check the investor-tokenization app for comparison
rg "NextResponse\.json" apps/investor-tokenization --type ts -A 2 | head -50

Repository: Trustless-Work/Tokenization-Hack

Length of output: 3635


🏁 Script executed:

# Check the investor-tokenization send-transaction route to see if it has similar issues
cat -n apps/investor-tokenization/src/app/api/helper/send-transaction/route.ts

Repository: Trustless-Work/Tokenization-Hack

Length of output: 1726


🏁 Script executed:

# Search for any error handling with explicit status codes in the entire codebase
rg "NextResponse\.json.*status:" apps/ --type ts -B 2 -A 2

Repository: Trustless-Work/Tokenization-Hack

Length of output: 58


Add input validation and HTTP status codes to transaction submission endpoint

The signedXdr parameter is used without validation, and all responses currently return HTTP 200 regardless of success or failure type. This prevents clients from distinguishing between bad input, network failures, and server errors.

🐛 Proposed fix
 export async function POST(request: Request) {
   const { signedXdr } = await request.json();
+  if (!signedXdr || typeof signedXdr !== "string") {
+    return NextResponse.json(
+      {
+        status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
+        message: "signedXdr is required and must be a string.",
+      },
+      { status: 400 },
+    );
+  }
   const server = new StellarSDK.Horizon.Server(
     "https://horizon-testnet.stellar.org",
     {
       allowHttp: true,
     },
   );
@@
     const response = await server.submitTransaction(transaction);
     if (!response.successful) {
-      return NextResponse.json({
-        status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
-        message:
-          "The transaction could not be sent to the Stellar network for some unknown reason. Please try again.",
-      });
+      return NextResponse.json(
+        {
+          status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
+          message:
+            "The transaction could not be sent to the Stellar network for some unknown reason. Please try again.",
+        },
+        { status: 502 },
+      );
     }
     return NextResponse.json({
       status: StellarSDK.rpc.Api.GetTransactionStatus.SUCCESS,
       message:
         "The transaction has been successfully sent to the Stellar network.",
       hash: response.hash,
     });
   } catch (error) {
     console.error("Transaction submission error:", error);
-    return NextResponse.json({
-      status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
-      message:
-        error instanceof Error
-          ? error.message
-          : "An unknown error occurred while submitting the transaction.",
-    });
+    return NextResponse.json(
+      {
+        status: StellarSDK.rpc.Api.GetTransactionStatus.FAILED,
+        message:
+          error instanceof Error
+            ? error.message
+            : "An unknown error occurred while submitting the transaction.",
+      },
+      { status: 500 },
+    );
   }
 }
🤖 Prompt for AI Agents
In `@apps/backoffice-tokenization/src/app/api/helper/send-transaction/route.ts`
around lines 4 - 42, Validate the incoming signedXdr at the top of the POST
handler in route.ts (ensure it's present and a non-empty string) and return a
400 Bad Request via NextResponse when invalid; wrap request.json parse in
try/catch to return 400 for malformed JSON. After submitTransaction, map
outcomes to appropriate HTTP statuses: return 200 on success (include hash), 502
Bad Gateway when submission to Horizon fails (response.successful === false),
and 422 Unprocessable Entity for XDR decode errors thrown by
TransactionBuilder.fromXDR; finally, in the outer catch return 500 Internal
Server Error for unexpected errors and include the error.message in the JSON
body for debugging. Ensure you use NextResponse.json(payload, { status: <code>
}) in each case and reference the POST function, signedXdr variable,
TransactionBuilder.fromXDR, server.submitTransaction, and response.successful
when making the changes.

Comment on lines +164 to +180
/**
* Get contract address from salt and wasm hash (deterministic calculation)
* This is used when simulation fails because contract already exists
*/
async getContractAddressFromSalt(
wasmHash: Buffer,
salt: Buffer,
): Promise<string> {
// When a contract already exists, we cannot simulate its creation
// The address is deterministic, but we can't easily calculate it without the network
// The best we can do is throw a helpful error
throw new Error(
`Cannot determine address for existing contract. ` +
`Contracts are already deployed for this escrowContractId. ` +
`Please use a different escrowContractId or check if the contracts are already deployed.`
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

getContractAddressFromSalt always throws - appears to be dead code path.

This method unconditionally throws an error and never returns an address. It's called from simulateContractCreation (line 285) when a contract already exists, which means that path will always fail. Either implement the deterministic address calculation or remove this method and handle the scenario differently.

Option 1: Remove the unused method
-  /**
-   * Get contract address from salt and wasm hash (deterministic calculation)
-   * This is used when simulation fails because contract already exists
-   */
-  async getContractAddressFromSalt(
-    wasmHash: Buffer,
-    salt: Buffer,
-  ): Promise<string> {
-    // When a contract already exists, we cannot simulate its creation
-    // The address is deterministic, but we can't easily calculate it without the network
-    // The best we can do is throw a helpful error
-    throw new Error(
-      `Cannot determine address for existing contract. ` +
-      `Contracts are already deployed for this escrowContractId. ` +
-      `Please use a different escrowContractId or check if the contracts are already deployed.`
-    );
-  }

And update line 285 in simulateContractCreation:

       if (errorStr.includes("contract already exists") || errorStr.includes("ExistingValue")) {
-        return this.getContractAddressFromSalt(wasmHash, salt);
+        throw new Error(
+          `Cannot determine address for existing contract. ` +
+          `Contracts are already deployed for this escrowContractId. ` +
+          `Please use a different escrowContractId or check if the contracts are already deployed.`
+        );
       }
🤖 Prompt for AI Agents
In `@apps/backoffice-tokenization/src/lib/sorobanClient.ts` around lines 164 -
180, getContractAddressFromSalt currently just throws and breaks the path in
simulateContractCreation; either implement the deterministic Soroban contract
address calculation here or remove the method and adjust
simulateContractCreation to handle existing contracts without calling it. Fix
option A: implement getContractAddressFromSalt(wasmHash, salt) to return the
deterministic address using the same algorithm/formula used elsewhere (e.g.,
hashing salt + wasmHash + network prefixes or reusing any existing createAddress
utility) and return a string address. Fix option B: remove
getContractAddressFromSalt and update simulateContractCreation to treat the
"contract already exists" case as a success (return existing address from the
network query) or throw a clearer error upstream; in either case update
references to getContractAddressFromSalt so no code path relies on a method that
always throws.

Comment on lines +255 to +296
/**
* Simulate contract creation to get the address before deploying
*/
async simulateContractCreation(
wasmHash: Buffer,
salt: Buffer,
constructorArgs: ScVal[],
): Promise<string> {
const account = (await this.server.getAccount(this.publicKey)) as AccountLike;

const transaction = this.buildBaseTx(account)
.addOperation(
Operation.createCustomContract({
wasmHash,
address: new Address(this.publicKey),
salt,
constructorArgs,
}),
)
.setTimeout(30)
.build();

const preparedTx = await this.server.prepareTransaction(transaction);
const simulation = await this.server.simulateTransaction(preparedTx);

// Handle both success and error response types
if ("error" in simulation) {
const errorStr = JSON.stringify(simulation.error);
// If contract already exists, try with empty args to get address
if (errorStr.includes("contract already exists") || errorStr.includes("ExistingValue")) {
return this.getContractAddressFromSalt(wasmHash, salt);
}
throw new Error(`Simulation failed: ${errorStr}`);
}

// Access result from success response
if (!simulation.result?.retval) {
throw new Error("Simulation did not return a contract address");
}

return Address.fromScVal(simulation.result.retval).toString();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Stellar SDK prepareTransaction simulateTransaction internal implementation

💡 Result:

In @stellar/stellar-sdk (JS), the two methods are thin wrappers around the Soroban RPC simulateTransaction JSON-RPC method plus a local “apply simulation to tx” step:

Server.simulateTransaction(tx, addlResources?, authMode?)

  • Calls a private _simulateTransaction(...), which sends a JSON-RPC request named "simulateTransaction" with:
    • transaction: tx.toXDR() (base64 XDR string)
    • authMode (passed through if provided)
    • resourceConfig.instructionLeeway = addlResources.cpuInstructions (only if addlResources is provided)
  • Then parses the raw RPC response via parseRawSimulation. [1]

Server.prepareTransaction(tx)

  • await this.simulateTransaction(tx)
  • If the parsed response is a simulation error, it throws.
  • Otherwise it calls assembleTransaction(tx, simResponse).build() and returns the built transaction. [2]

assembleTransaction(rawTx, simulation) (implementation used by prepareTransaction)

  • Rejects non-Soroban transactions unless they contain exactly one op and it’s one of: invokeHostFunction, extendFootprintTtl, restoreFootprint. [3]
  • Fee-bump txs aren’t fully handled yet; if it sees innerTransaction it recurses into it (TODO: Handle feebump transactions). [3]
  • Parses the simulation (parseRawSimulation) and requires it to be a “success” response. [3]
  • Clones the original transaction via TransactionBuilder.cloneFrom(rawTx, {...}) while applying:
    • fee = (parseInt(rawTx.fee) + parseInt(sim.minResourceFee)).toString()
    • sorobanData = sim.transactionData.build() (simulation-derived Soroban transaction data: footprint/resources/etc.)
    • networkPassphrase = rawTx.networkPassphrase [3]
  • If the tx’s single op is invokeHostFunction, it replaces the operation to control auth:
    • If the original op already had auth, that is preserved and all simulated auth entries are ignored.
    • Otherwise it uses sim.result.auth. [3]

Sources: [1] [2] [3]


Remove redundant simulation call.

The prepareTransaction method already internally calls simulateTransaction and applies the simulation results (fees, soroban data, auth) to the transaction. The subsequent explicit simulateTransaction call on the prepared transaction is redundant. Either use only prepareTransaction to get a fully prepared transaction, or use only simulateTransaction if you only need the simulation result without fee/resource adjustments.

🤖 Prompt for AI Agents
In `@apps/backoffice-tokenization/src/lib/sorobanClient.ts` around lines 255 -
296, The code in simulateContractCreation calls
this.server.prepareTransaction(transaction) and then redundantly calls
this.server.simulateTransaction(preparedTx); remove the explicit
simulateTransaction call and instead use the simulation data returned/applied by
prepareTransaction (use the simulation/auth/fee info available on preparedTx) —
update the logic that currently reads and checks the local simulation variable
(simulation/result/retval) to reference the simulation data on preparedTx,
retain the existing error handling for "contract already exists"/"ExistingValue"
and the final Address.fromScVal(...) return, and delete the extra
this.server.simulateTransaction(preparedTx) invocation and its local simulation
assignment.


const app = express();
const PORT = process.env.PORT || 3001;
const PORT = process.env.PORT || 3003;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all references to port 3001
rg -n --fixed-strings '3001' -g '!**/node_modules/**' -g '!**/dist/**'

Repository: Trustless-Work/Tokenization-Hack

Length of output: 1005


🏁 Script executed:

#!/bin/bash
# Search for docker-compose, env files, and k8s configs
git ls-files | grep -E '\.(yml|yaml|env|compose)$|docker-compose' | head -20

Repository: Trustless-Work/Tokenization-Hack

Length of output: 58


🏁 Script executed:

#!/bin/bash
# Check the current file to see the context of the PORT change
cat -n apps/evidence-service/src/index.ts | head -20

Repository: Trustless-Work/Tokenization-Hack

Length of output: 653


Update documentation, environment examples, and code examples to reflect the new default port (3003).

The default PORT was changed to 3003, but multiple files still reference the old default of 3001 and will mislead developers:

  • apps/evidence-service/README.md – Update lines 64, 83, 102, 116, 137 from 3001 to 3003
  • apps/evidence-service/.env.example – Update lines 2–3 to reflect PORT=3003 and update comment
  • apps/evidence-service/src/services/evidenceClient.example.ts – Update line 34 URL from localhost:3001 to localhost:3003
  • apps/investor-tokenization/package.json – Update dev script port from 3001 to 3003 to ensure connectivity with evidence-service
🤖 Prompt for AI Agents
In `@apps/evidence-service/src/index.ts` at line 9, The default port constant PORT
in index.ts was changed to 3003 but documentation and examples still reference
3001; update the PORT usage and all examples accordingly: change the PORT
default to 3003 where declared (the PORT constant), update README references
that mention the old default, update the environment sample to set PORT=3003 and
adjust its comment, update the example client URL (the localhost URL in
evidenceClient.example.ts) to use :3003, and change the dev script port in
package.json to 3003 so the services connect correctly.

Comment on lines +95 to +109
// Check USDC balance before attempting purchase
const investmentService = new InvestmentService();
const adjustedAmount = Math.floor(values.amount * 1_000_000); // Convert to microUSDC

const balanceResponse = await investmentService.getTokenBalance({
tokenFactoryAddress: DEFAULT_USDC_ADDRESS,
address: walletAddress,
});

const currentBalance = balanceResponse.success
? parseFloat(balanceResponse.balance || "0")
: 0;

if (currentBalance < adjustedAmount) {
const balanceInUSDC = currentBalance / 1_000_000;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify token-balance response units and any existing amount conversion helpers
rg -n "token-balance|TokenBalanceResponse|TokenBalancePayload" -S
rg -n "microUSDC|stroop|toMicro|parseUnits|amount.*micro" -S

Repository: Trustless-Work/Tokenization-Hack

Length of output: 1969


🏁 Script executed:

#!/bin/bash
# Examine the TokenBalanceResponse definition and TOKEN_BALANCE_SYSTEM documentation
cat -n apps/investor-tokenization/src/features/investments/services/investment.service.ts | head -50

echo "---"

# View the adjustedAmounts utility which appears to have proper rounding
cat -n apps/investor-tokenization/src/utils/adjustedAmounts.ts

echo "---"

# View TOKEN_BALANCE_SYSTEM.md to understand unit specifications
head -100 apps/investor-tokenization/TOKEN_BALANCE_SYSTEM.md

Repository: Trustless-Work/Tokenization-Hack

Length of output: 5080


🏁 Script executed:

#!/bin/bash
# Get more of the TOKEN_BALANCE_SYSTEM.md documentation, especially the response format
sed -n '96,250p' apps/investor-tokenization/TOKEN_BALANCE_SYSTEM.md

echo "---"

# Check for USDC decimal definitions and specifications
rg -n "USDC.*decim|decim.*USDC|6|1e6|1e7" apps/investor-tokenization/TOKEN_BALANCE_SYSTEM.md

echo "---"

# Search for DEFAULT_USDC_ADDRESS to understand which token is being used
rg -n "DEFAULT_USDC_ADDRESS" -A 2 -B 2

Repository: Trustless-Work/Tokenization-Hack

Length of output: 5871


🏁 Script executed:

#!/bin/bash
# Check the full context of the InvestDialog code to see if adjustedAmounts is imported
cat -n apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx | sed -n '1,130p'

echo "---"

# Check for uses of adjustPricesToMicroUSDC to see how it's properly used
rg -n "adjustPricesToMicroUSDC" -B 3 -A 3

Repository: Trustless-Work/Tokenization-Hack

Length of output: 7213


Use the existing adjustPricesToMicroUSDC utility to fix scale factor and precision issues.

The balance check uses the wrong decimal scale: it converts with 1_000_000 (1e6), but USDC on Soroban has 7 decimals and the backend uses 1e7 via adjustPricesToMicroUSDC. This causes a 10x discrepancy in balance validation and error messages. Additionally, the code should use the Decimal library for precise rounding rather than Math.floor.

Import and use adjustPricesToMicroUSDC from @/utils/adjustedAmounts:

Proposed fix
+import { adjustPricesToMicroUSDC } from "@/utils/adjustedAmounts";
+
-      const adjustedAmount = Math.floor(values.amount * 1_000_000); // Convert to microUSDC
+      const adjustedAmount = parseInt(adjustPricesToMicroUSDC(values.amount));
       
       const balanceResponse = await investmentService.getTokenBalance({
         tokenFactoryAddress: DEFAULT_USDC_ADDRESS,
         address: walletAddress,
       });

       const currentBalance = balanceResponse.success 
         ? parseFloat(balanceResponse.balance || "0")
         : 0;

       if (currentBalance < adjustedAmount) {
-        const balanceInUSDC = currentBalance / 1_000_000;
+        const balanceInUSDC = currentBalance / 1e7;
         throw new Error(
🤖 Prompt for AI Agents
In `@apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx`
around lines 95 - 109, Replace the manual scaling/rounding in InvestDialog.tsx
that uses Math.floor(values.amount * 1_000_000) with the shared utility
adjustPricesToMicroUSDC (which uses the backend's 1e7 scale) and use Decimal for
exact arithmetic; specifically, import adjustPricesToMicroUSDC from
"@/utils/adjustedAmounts" and compute adjustedAmount =
adjustPricesToMicroUSDC(values.amount) (or the equivalent API call the util
exposes), use Decimal for any rounding/formatting instead of Math.floor, and
compare currentBalance (parsed from InvestmentService.getTokenBalance) against
that adjustedAmount and compute balanceInUSDC by dividing currentBalance by the
same 1e7 scale (via Decimal) so balance validation and messages use the correct
precision.

Comment on lines +104 to +113
const currentBalance = balanceResponse.success
? parseFloat(balanceResponse.balance || "0")
: 0;

if (currentBalance < adjustedAmount) {
const balanceInUSDC = currentBalance / 1_000_000;
throw new Error(
`Insufficient USDC balance. You have ${balanceInUSDC.toFixed(2)} USDC but need ${values.amount.toFixed(2)} USDC. Please add more USDC to your wallet.`
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle balance API failures explicitly (avoid false “insufficient” errors).
If the balance call fails, treating it as 0 blocks valid purchases and shows a misleading message. Prefer a dedicated error path so users can retry or get a clear failure reason.

🔧 Proposed fix
-      const currentBalance = balanceResponse.success 
-        ? parseFloat(balanceResponse.balance || "0")
-        : 0;
+      if (!balanceResponse.success) {
+        throw new Error(
+          "Unable to fetch USDC balance. Please try again in a moment."
+        );
+      }
+
+      const currentBalance = parseFloat(balanceResponse.balance ?? "0");
🤖 Prompt for AI Agents
In `@apps/investor-tokenization/src/features/tokens/components/InvestDialog.tsx`
around lines 104 - 113, The code currently treats a failed balance API
(balanceResponse.success === false) as a zero balance which causes misleading
"Insufficient USDC" errors; update the logic in InvestDialog where
currentBalance is derived (the balanceResponse parsing and the subsequent
insufficient-balance check comparing currentBalance to adjustedAmount) to
explicitly handle balanceResponse.success === false by throwing or returning a
distinct, retryable error (e.g., "Failed to retrieve balance" with the API
error/details) instead of using 0; only perform the numeric comparison and
potential "insufficient funds" error when balanceResponse.success is true and a
numeric balance is available.

Comment on lines 4 to 12
const data = [
{
escrowId: "CBDLIY7HAJ73E6SPAOOKZFCJH3C4H6YBWATWQTON5Z7MY5JRVIIW7LQW",
tokenSale: "CAL7JK6HOQOW5KU7VKASIZ2RF4GFVQTZJAI7EHCX7VXTAXQ2B27QIEZL",
tokenFactory: "CDARBSD3OVSVUJWZV4W5HA66QDHY6A3YEH5EQGZPYFGS4DPDYW2UXWX3",
escrowId: "CCZHTYVLK6R2QMIFBTEN65ZVCSFBD3L5TXYCZJT5WTXE63ABYXBBCSEB",
tokenSale: "CC2AGB3AW5IITDIPEZGVX6XT5RTDIVINRZL7F6KZPIHEWN2GRXL5CRCT",
tokenFactory: "CDJTII2GR2FY6Q4NDJGZI7NW2SHQ7GR5Y2H7B7Q253PTZZAZZ25TFYYU",
src: "/escrows/car.png",
content: <DummyContent />,
},
{
escrowId: "CC6Y3XYVB4PSPVX4XED2K4GXKPBPS4EAKTZC3BIGEPAE4V2FKMBGEQXY",
tokenSale: "CBFTQZ3NATN6Y7PKYWGCF7OH6JOFTWUMYAJQDCBPSKGPWWQ7N23RTSNK",
tokenFactory: "CDARBSD3OVSVUJWZV4W5HA66QDHY6A3YEH5EQGZPYFGS4DPDYW2UXWX3",
src: "/escrows/art.jpg",
content: <DummyContent />,
},
{
escrowId: "CDB6F6FLFM3VOGNU3FWETULM4QVPWNSJCZQNVQLWU6B4XYVOZVI3YD6X",
tokenSale: "CA5DPVX6YWUYUASZOQK3WJYDWHYEMRHGCOQGQTDUSDJLB6EQNDRS3FOG",
tokenFactory: "CDARBSD3OVSVUJWZV4W5HA66QDHY6A3YEH5EQGZPYFGS4DPDYW2UXWX3",
src: "/escrows/hotel.png",
content: <DummyContent />,
},
{
escrowId: "CDEDNEQEXSUYTAKHT7VS47F2VKSN4PWENXU5U7SXVPEGXLVD3U7LIZI3",
tokenSale: "CB4XSZUQF2TF7OYOZY55CICXBNAL6RIZOJP7TITFA2PKCSZBHYSSRSNY",
tokenFactory: "CDARBSD3OVSVUJWZV4W5HA66QDHY6A3YEH5EQGZPYFGS4DPDYW2UXWX3",
src: "/escrows/clock.jpg",
content: <DummyContent />,
},
{
escrowId: "CAO6ZONKBYQGE7E3OIPJTMSE2J2PKNROSXGGC3VQOO7JF2LXY47XG6QV",
tokenSale: "CBW4W4GEGD5MNXCUHGOAJ64IXLFHDMDDD65ITVM3HVIYSK22PGSHIJ5N",
tokenFactory: "CC5ESBB3DK2H6IRPHPJMAU5SOAVLWHY2TPJ2XTLJLXVHUZ6ZMW4QCYFA",
src: "/escrows/building.png",
content: <DummyContent />,
},
];
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Third instance of duplicated project data.

This data array contains the same contract addresses as PROJECT_DATA in both hook files. This is the third duplication point.

Centralizing this data is now essential to prevent inconsistencies when addresses need to be updated.

🤖 Prompt for AI Agents
In `@apps/investor-tokenization/src/features/transparency/ProjectList.tsx` around
lines 4 - 12, The array named "data" in ProjectList.tsx duplicates contract
addresses already defined as "PROJECT_DATA" in the hooks; extract this project
list (including escrowId, tokenSale, tokenFactory, src and the DummyContent
reference) into a single shared module (e.g., export const PROJECT_DATA or
PROJECTS) and update ProjectList.tsx to import that constant instead of
declaring "data", and update the two hook files to import the same constant
rather than duplicating the entries so all components reference the single
source of truth.

Comment on lines +164 to +180
/**
* Get contract address from salt and wasm hash (deterministic calculation)
* This is used when simulation fails because contract already exists
*/
async getContractAddressFromSalt(
wasmHash: Buffer,
salt: Buffer,
): Promise<string> {
// When a contract already exists, we cannot simulate its creation
// The address is deterministic, but we can't easily calculate it without the network
// The best we can do is throw a helpful error
throw new Error(
`Cannot determine address for existing contract. ` +
`Contracts are already deployed for this escrowContractId. ` +
`Please use a different escrowContractId or check if the contracts are already deployed.`
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

getContractAddressFromSalt always throws, creating unreachable code paths.

This method is declared to return Promise<string> but unconditionally throws. It's called at line 285 in simulateContractCreation, meaning that code path will always rethrow. Either implement the actual address derivation or remove this method and the call site.

Option 1: Remove the misleading method

If address derivation cannot be implemented, remove the method and inline the error at the call site:

-  async getContractAddressFromSalt(
-    wasmHash: Buffer,
-    salt: Buffer,
-  ): Promise<string> {
-    throw new Error(
-      `Cannot determine address for existing contract. ` +
-      `Contracts are already deployed for this escrowContractId. ` +
-      `Please use a different escrowContractId or check if the contracts are already deployed.`
-    );
-  }

And update line 285 in simulateContractCreation:

       if (errorStr.includes("contract already exists") || errorStr.includes("ExistingValue")) {
-        return this.getContractAddressFromSalt(wasmHash, salt);
+        throw new Error(
+          `Cannot determine address for existing contract. ` +
+          `Contracts are already deployed for this escrowContractId. ` +
+          `Please use a different escrowContractId or check if the contracts are already deployed.`
+        );
       }
🤖 Prompt for AI Agents
In `@apps/investor-tokenization/src/lib/sorobanClient.ts` around lines 164 - 180,
getContractAddressFromSalt currently always throws despite returning
Promise<string>, making the simulateContractCreation code path that calls it
unreachable; either implement the deterministic address derivation here or
remove the method and surface the error where it’s used. Fix option A: implement
deterministic address calculation inside getContractAddressFromSalt (use
wasmHash and salt per Soroban address derivation rules) and return the string
address so simulateContractCreation can continue; Fix option B: delete
getContractAddressFromSalt and replace the call in simulateContractCreation with
an explicit thrown Error (or alternative handling) referencing the
escrowContractId so behavior is obvious. Ensure you update/type signatures
accordingly and keep references to getContractAddressFromSalt and
simulateContractCreation consistent.

Comment on lines +277 to +278
const preparedTx = await this.server.prepareTransaction(transaction);
const simulation = await this.server.simulateTransaction(preparedTx);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and read the sorobanClient.ts file to understand the context
fd -t f "sorobanClient.ts" --exec wc -l {} \;

Repository: Trustless-Work/Tokenization-Hack

Length of output: 193


🏁 Script executed:

# Read the file around lines 277-278 to get context
cat -n apps/investor-tokenization/src/lib/sorobanClient.ts | sed -n '260,295p'

Repository: Trustless-Work/Tokenization-Hack

Length of output: 1589


🌐 Web query:

Stellar SDK Soroban prepareTransaction simulateTransaction differences behavior

💡 Result:

simulateTransaction

  • What it does: Calls the Soroban RPC simulateTransaction endpoint to trial-run the transaction and return estimated resource usage/fees, recorded auth, recommended Soroban transactionData (incl. footprint), events, and possibly a restore preamblewithout submitting anything on-chain. It’s also used for read-only invocations. [1][2]
  • Output: A simulation response (cost, minResourceFee, transactionData, results.auth, events, errors, etc.), not a modified transaction. [1][2]
  • Behavior notes: In the JS SDK, you can optionally influence simulation via addlResources and authMode. [3]

prepareTransaction

  • What it does: A convenience wrapper in the SDK that:
    1. runs server.simulateTransaction(tx), then
    2. applies the simulation’s footprint / Soroban transactionData / auth / resource fee onto a new copy of the transaction so it’s ready to sign and send. [3][4]
  • Output: A prepared Transaction/FeeBumpTransaction (not the simulation object). [3][4]
  • Fee behavior: The returned transaction’s fee is (original tx fee) + (minimum resource fee from simulation), so you should inspect/confirm it. [3][4]
  • Auth precedence behavior: If your original invocation already includes auth entries, prepareTransaction will prefer those over simulation-produced auth (while still filling footprint/resources normally). [3][4]
  • Error behavior: If simulation returns an error, prepareTransaction throws (i.e., you don’t get a prepared tx). [3][5]

Rule of thumb: use simulateTransaction when you want to inspect costs/results/errors first (or do read-only calls); use prepareTransaction when you want the SDK to auto-fill footprint/auth/resources and give you a tx that’s ready to sign + sendTransaction. [1][3][4]

Sources: [1] [2] [3] [4] [5]


Remove redundant prepareTransaction call.

prepareTransaction internally calls simulateTransaction and applies its results to the transaction. Since preparedTx is never used and the code only needs the simulation result (via simulation.result.retval), call simulateTransaction directly on the original transaction instead to avoid the extra RPC call.

Current code
const preparedTx = await this.server.prepareTransaction(transaction);
const simulation = await this.server.simulateTransaction(preparedTx);
🤖 Prompt for AI Agents
In `@apps/investor-tokenization/src/lib/sorobanClient.ts` around lines 277 - 278,
The code redundantly calls this.server.prepareTransaction which itself runs
simulation; remove the unnecessary prepareTransaction call and invoke
this.server.simulateTransaction directly with the original transaction object so
you only perform one RPC. Update the block that currently declares preparedTx
and calls simulateTransaction(preparedTx) to call
simulateTransaction(transaction) and then use simulation.result.retval as
before; ensure you also remove the preparedTx variable and any references to it
(symbols: prepareTransaction, simulateTransaction, preparedTx,
simulation.result.retval, transaction).

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.

Improve Token Contract + Tokenize Escrow UI

1 participant