Skip to content

Conversation

@AnoukRImola
Copy link

@AnoukRImola AnoukRImola commented Jan 23, 2026

StellarRent Logo

Pull Request | StellarRent

📝 Summary

Replace the existing email/password authentication system with social login using the Stellar-Account-Abstraction-SDK. Users can now sign in with Google and automatically receive a Stellar wallet through account abstraction, eliminating the need to manage private keys.

🔗 Related Issues

Closes: #199

🔄 Changes Made

Frontend (apps/web):

  • Integrated Stellar-Account-Abstraction-SDK for social login
  • Added SocialLoginButton component for Google Sign-In
  • Added FreighterFallback component for manual wallet connection
  • Implemented useAuth hook with session persistence (localStorage)
  • Added logout functionality to tenant dashboard header
  • Updated login page to use new social login flow
  • Added TypeScript types for auth system

SDK Integration:

  • Copied and configured stellar-social-sdk locally
  • Updated FreighterProvider to use @stellar/freighter-api (v4.1.0)
  • Fixed Google button initialization timing issue

Backend:

  • Deprecated old wallet-auth system (moved to _deprecated/)

Config:

  • Updated biome.json to exclude SDK from linting

🖼️ Current Output

  • Google login flow
  • Wallet creation logs
  • Session persistence
Captura desde 2026-01-22 18-24-23 Captura desde 2026-01-22 18-23-49 Captura desde 2026-01-22 18-25-32

🧪 Testing

✅ Testing Checklist

  • Unit tests added/modified (use-auth.test.tsx - 305 lines covering login/logout flows)
  • Integration tests performed
  • Manual tests executed (Google login, logout, Freighter detection)
  • All tests pass in CI/CD

Manual Testing Performed:

  • Google Sign-In → Wallet created automatically
  • Logout → Session cleared, redirected to login
  • Freighter detection → Shows "Connect with Freighter" when installed

⚠️ Potential Risks

  • Google OAuth requires proper configuration in Google Cloud Console (origins + redirect URIs)
  • Testnet only - Friendbot used for account funding
  • SDK is copied locally (not from npm) - may need future updates

🚀 Next Steps & Improvements

  • 🔹 Add more social providers (GitHub, Facebook)
  • 🔹 Implement mainnet support with proper account funding
  • 🔹 Add E2E tests for complete auth flow
  • 🔹 Consider publishing SDK to npm for easier updates

💬 Comments

The account abstraction approach generates deterministic wallets from Google credentials - same user always gets the same wallet address. This greatly simplifies onboarding for non-crypto users.

Summary by CodeRabbit

  • New Features
    • Stellar Social SDK added with Google and Freighter authentication, account/balance and payment capabilities; Freighter fallback component and Google sign-in button introduced.
  • Changes
    • Email/password auth removed in favor of social/wallet flows; register redirects to login; backend no longer exposes legacy auth endpoint.
    • Logout action added to navbar and dashboards.
  • Documentation
    • Deprecation README for legacy wallet auth added.
  • Tests
    • Expanded auth hook test coverage.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

Migrates frontend authentication to a local Stellar Social SDK (Google + Freighter), deprecates backend wallet-auth routes, adds SDK package and types, updates frontend auth hooks/components/pages, and introduces atomic DB RPCs and new backend error types for sync/payment flows.

Changes

Cohort / File(s) Summary
Backend deprecation & routing
apps/backend/src/_deprecated/README.md, apps/backend/src/index.ts
Documents deprecation of wallet-auth sources and removes mounting/import of wallet-auth.routes (no /api/auth endpoint).
Backend DB atomic functions & payment constraints
apps/backend/database/migrations/00006_add_atomic_functions.sql, apps/backend/database/migrations/00007_add_payment_constraints.sql
Adds process_sync_event_atomic and confirm_booking_payment_atomic RPCs, unique partial index for tx hashes, structured logging, error handling, and EXECUTE grants.
Backend error types & services
apps/backend/src/types/errors.ts, apps/backend/src/middleware/error.middleware.ts, apps/backend/src/services/sync.service.ts, apps/backend/src/services/booking.service.ts, apps/backend/src/blockchain/trustlessWork.ts, apps/backend/src/services/cache.service.ts
Introduces EscrowError, SyncError, CacheError; middleware handling; sync/booking/escrow refactors to use structured errors and RPCs; cache logs via loggingService.
Frontend SDK package
apps/web/stellar-social-sdk/* (e.g., package.json, rollup.config.js, tsconfig.json, src/**, test-quick.js)
Adds local Stellar Social SDK: core StellarSocialSDK, StellarSocialAccount, providers (GoogleAuthProvider, FreighterProvider), CryptoUtils, types, configs, build/test scripts.
Frontend package/config & env
apps/web/package.json, apps/web/tsconfig.json, biome.json, apps/web/.env.example
Adds local dependency "stellar-social-sdk": "file:./stellar-social-sdk", tsconfig path mapping to SDK ESM, Biome ignore entries, and fills .env.example with network, API URL, Google client ID, and contract ID.
Auth hook, types, and SDK integration
apps/web/src/hooks/auth/use-auth.tsx, apps/web/src/lib/stellar-social-sdk.ts, apps/web/src/types/auth.ts
Replaces placeholder auth with StellarSocialSDK integration: session restore, loginWithGoogle/loginWithFreighter, account, authMethod, getBalance, sendPayment, and new TS types + window augmentations.
Protected route & user role changes
apps/web/src/hooks/auth/procted-route.tsx, apps/web/src/hooks/useUserRole.tsx
ProtectedRoute now checks authMethod with allowed types ['google','freighter']; useUserRole fetches profile by publicKey, adds isLoading and localStorage caching.
Auth UI components
apps/web/src/components/auth/SocialLoginButton.tsx, apps/web/src/components/auth/FreighterFallback.tsx
Adds SocialLoginButton (Google Identity Services integration with retries/fallback) and FreighterFallback (installation detection, connect flow, callbacks).
Pages, layout & navbar updates
apps/web/src/app/login/page.tsx, apps/web/src/app/register/page.tsx, apps/web/src/app/layout.tsx, apps/web/src/app/dashboard/tenant-dashboard/page.tsx, apps/web/src/app/tenant-dashboard/page.tsx, apps/web/src/components/layout/Navbar.tsx
Replaces email/password register/login with social/wallet flows and redirect logic; adds Google Identity script and Permissions-Policy meta; adds logout wiring and auth-aware navbar.
Auth tests & test utils
apps/web/src/hooks/auth/__tests__/use-auth.test.tsx, apps/backend/tests/utils/booking-test.utils.ts
Adds comprehensive useAuth tests (mocking SDK, Google, Freighter) and simplifies Supabase test mocks while adding an rpc mock for RPC calls.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend as Frontend App
    participant GoogleAPI as Google Identity Services
    participant SDK as Stellar Social SDK
    participant StellarNet as Stellar Network
    participant Freighter as Freighter Wallet

    User->>Frontend: Click "Continue with Google"
    Frontend->>GoogleAPI: Render Google Sign-In / OneTap
    User->>GoogleAPI: Authenticate with Google
    GoogleAPI-->>Frontend: credentialResponse
    Frontend->>SDK: authenticateWithGoogle(credentialResponse)
    SDK->>SDK: Derive deterministic seed/keypair
    SDK->>StellarNet: Check/load account
    alt Account exists
        StellarNet-->>SDK: Return account data
    else Account missing
        SDK->>StellarNet: Fund (testnet) & create account
        StellarNet-->>SDK: Account created
        SDK->>SDK: Initialize account with contract
    end
    SDK-->>Frontend: AuthResult { account }
    Frontend->>Frontend: Persist session, set AuthContext
    Frontend-->>User: Redirect to dashboard
Loading
sequenceDiagram
    participant User
    participant Frontend as Frontend App
    participant Freighter as Freighter Wallet
    participant SDK as Stellar Social SDK
    participant StellarNet as Stellar Network

    User->>Frontend: Click "Connect Freighter"
    Frontend->>Freighter: Check availability (isInstalled)
    alt Not installed
        Frontend-->>User: Show install link
    else Installed
        User->>Freighter: Approve connect
        Freighter-->>Frontend: publicKey / auth info
        Frontend->>SDK: connectFreighter()
        SDK->>StellarNet: Load or create account for publicKey
        alt Account exists
            StellarNet-->>SDK: Return account
        else Create account
            SDK->>StellarNet: Create & initialize account
        end
        SDK-->>Frontend: AuthResult { account }
        Frontend->>Frontend: Persist session, set AuthContext
        Frontend-->>User: Redirect to dashboard
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • respp

"🐰
I nibble at tokens, then hop to the sky,
Google hands keys, Freighter waves hi,
Seeds spun to stars, accounts multiply,
Dashboard awaits — carrot, goodbye! 🥕✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/social login sdk integration' is clear and directly describes the main change: integrating social login via an SDK. It accurately reflects the primary objective of the PR.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from #199: social login (Google) via SDK integration, automatic wallet mapping, session management with localStorage, logout functionality, Freighter fallback, useAuth hook, and unit tests covering login/logout flows.
Out of Scope Changes check ✅ Passed All changes are directly scoped to #199 objectives: frontend SDK integration, auth implementation, type definitions, migrations for payment/sync atomicity, and deprecation of old wallet-auth. Database changes (migrations 00006-00007) align with atomicity requirements for payment confirmation and event processing mentioned in backend scope.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
apps/web/src/hooks/useUserRole.tsx (1)

33-80: Scope cached host status to the current user.
Global localStorage keys can leak a previous user’s host status if the API fails, resulting in incorrect roles/permissions UI. Use user-scoped keys (e.g., by publicKey) or clear on logout.

🐛 Suggested fix
-        try {
-          const userId = user.publicKey || 'unknown';
-          const response = await profileAPI.getUserProfile(userId);
+        const userId = user.publicKey || 'unknown';
+        const hostStatusKey = `hostStatus:${userId}`;
+        const hasPropertiesKey = `hasProperties:${userId}`;
+
+        try {
+          const response = await profileAPI.getUserProfile(userId);
           const profile = response.data;
@@
-          if (hostStatus) {
-            localStorage.setItem('hostStatus', hostStatus);
-          }
-          localStorage.setItem('hasProperties', String(hasProperties));
+          if (hostStatus) {
+            localStorage.setItem(hostStatusKey, hostStatus);
+          }
+          localStorage.setItem(hasPropertiesKey, String(hasProperties));
         } catch (apiError) {
@@
-          const storedHostStatus = localStorage.getItem('hostStatus');
-          const storedHasProperties = localStorage.getItem('hasProperties') === 'true';
+          const storedHostStatus = localStorage.getItem(hostStatusKey);
+          const storedHasProperties = localStorage.getItem(hasPropertiesKey) === 'true';
🤖 Fix all issues with AI agents
In `@apps/web/package.json`:
- Line 15: The local package dependency for `@stellar/stellar-sdk` in the
stellar-social-sdk package.json is pinned to ^12.3.0 while the parent app uses
^13.3.0; update the dependency version in stellar-social-sdk's package.json to
"^13.3.0", then reinstall and regenerate the lockfile (npm/yarn/pnpm) so the
workspace resolves a single `@stellar/stellar-sdk` version; after upgrading, run
the stellar-social-sdk build/tests and address any API breakages in
functions/classes that import `@stellar/stellar-sdk`.

In `@apps/web/src/app/login/page.tsx`:
- Around line 13-20: The redirect query value pulled from
searchParams.get('redirect') is used unvalidated in router.push, allowing
open-redirects; update the logic around const redirect and the useEffect so you
first validate the value (e.g. ensure it is an internal path that starts with
'/' but not '//' and contains no scheme like 'http:' or host), and if the check
fails fall back to '/dashboard'; then use the validatedRedirect variable in the
useEffect where router.push is called (references: searchParams.get, redirect,
useEffect, router.push, isAuthenticated, isLoading).

In `@apps/web/src/components/auth/FreighterFallback.tsx`:
- Around line 27-31: The code treats Freighter as installed by checking property
existence on the result of isConnected() instead of its boolean value; update
the assignment that uses the connected variable so setIsFreighterInstalled
receives the actual boolean (connected.isConnected) rather than doing an
existence check, e.g. change the expression in the setIsFreighterInstalled call
to use connected.isConnected (or safely coerce it to a boolean) so false is
respected; refer to the isConnected() call and the setIsFreighterInstalled
invocation to locate the change.

In `@apps/web/src/hooks/auth/use-auth.tsx`:
- Around line 96-121: The session restore currently in the useEffect
(restoreSession) sets user and authMethod from STORAGE_KEYS but never rehydrates
account, causing isAuthenticated to be true while account is null; update the
hook to, after setting user/authMethod, attempt to rehydrate account via the SDK
(wait for sdk readiness), e.g. call sdk.loadExistingAccount(user.publicKey) (or
equivalent) and setAccount(restored) when successful, and on failure call
clearStorage(), setUser(null), setAuthMethod(null); also ensure isAuthenticated
is gated on account rather than only on user/authMethod so UI won’t show a
logged-in state until account is restored.

In `@apps/web/stellar-social-sdk/package.json`:
- Around line 17-23: The project currently depends on crypto-js and uses
CryptoUtils.generateKeypair() to derive Ed25519 seeds; replace the JS-based
CryptoJS.SHA256 usage with the Web Crypto API by converting the input to an
ArrayBuffer and calling crypto.subtle.digest('SHA-256', ...) and using the
resulting hash as the deterministic seed in CryptoUtils.generateKeypair(),
removing the crypto-js dependency from package.json; also move
"@stellar/stellar-sdk" and "@stellar/freighter-api" from dependencies into
peerDependencies in package.json so consuming apps control the SDK versions and
avoid duplicate installs.

In `@apps/web/stellar-social-sdk/src/index.ts`:
- Around line 64-70: This code logs user PII (authMethod.metadata?.name and
authMethod.metadata?.email) to the client console; remove those console.log
calls or redact/gate them behind a debug flag. Locate the block using
CryptoUtils.generateKeypair('google', googleSub) and keypair.publicKey(), keep
the public key log if needed, but delete or replace the
authMethod.metadata?.name/email console.log with either a non-PII message,
masked values, or wrap it in a runtime DEBUG check so it only runs in
development builds.
- Around line 215-238: The current catch blocks in getOrCreateAccount,
getOrCreateAccountWithKeypair, and loadExistingAccount treat any loadAccount
failure as "account missing" and always call createNewAccount; change each catch
to inspect the thrown error and only proceed to createNewAccount when the
Horizon response indicates a 404 (e.g., error.response?.status === 404 or
error.status === 404 or error is StellarSdk.NotFoundError), otherwise rethrow
the error so network/rate-limit/other Horizon errors bubble up; specifically
update the catch around this.server.loadAccount(...) in getOrCreateAccount,
getOrCreateAccountWithKeypair, and loadExistingAccount to conditionally call
createNewAccount(keypair, authMethod) only on 404 and throw the original error
in all other cases.

In `@apps/web/stellar-social-sdk/src/index.ts.corrupted`:
- Around line 124-154: In authenticateWithPhone enforce that the hardcoded test
verification code ('123456') is only accepted when a development/test flag is
enabled: check an environment flag (e.g. process.env.NODE_ENV === 'development'
or a dedicated flag like process.env.ALLOW_TEST_PHONE_CODE) before allowing that
bypass; otherwise treat '123456' as invalid and require real verification; add a
clear warning log when the test code path is used and keep existing error
handling intact (references: authenticateWithPhone, CryptoUtils.hashPhone,
getOrCreateAccount).
- Around line 88-119: The current authenticateWithFacebook method in the class
returns nondeterministic mock accounts (uses Math.random()), which breaks
deterministic account behavior; replace the implementation with an immediate
not-implemented error to prevent accidental use: update
authenticateWithFacebook() to synchronously/async-throw a clear Error (e.g.
"authenticateWithFacebook is not implemented") or return a rejected Promise, and
remove any calls that create random AuthMethod/getOrCreateAccount flow;
reference the authenticateWithFacebook function and avoid constructing
AuthMethod or calling getOrCreateAccount so callers cannot obtain random
wallets.
- Around line 314-390: The two methods getOrCreateAccountWithKeypair and
createNewAccountWithKeypair are defined outside the StellarSocialSDK class
(causing a syntax error); move both method definitions into the StellarSocialSDK
class body (e.g., before the export statements) preserving their signatures,
async/private modifiers and usage of this.server/this.contractId/this.network,
then remove the out-of-class duplicates so only the class contains these methods
and exports remain after the class definition.

In `@apps/web/stellar-social-sdk/src/providers/GoogleAuthProvider.ts`:
- Around line 172-185: The decodeJWT function fails for base64url payloads
missing '=' padding; update decodeJWT to normalize the payload by replacing
URL-safe characters (replace '-' -> '+' and '_' -> '/'), then append '='
characters until payload.length % 4 === 0 before calling atob, and keep the
existing JSON.parse and error handling flow so the JWT payload decodes reliably.
- Around line 136-167: The verifyToken implementation currently calls decodeJWT
without checking the signature; update verifyToken to perform JWKS-based
signature verification: fetch Google's JWKS from
https://www.googleapis.com/oauth2/v3/certs, select the key matching the token's
header.kid, convert that JWKS entry to a usable public key, verify the JWT
signature before trusting the payload returned by decodeJWT, and then enforce
claim checks (iss === 'https://accounts.google.com' or 'accounts.google.com',
aud === this.clientId, exp > now, and presence of sub and email). Use
verifyToken and decodeJWT names to locate the logic and ensure errors surface
descriptive messages on verification failure; if this code must remain in the
frontend, move signature verification into a backend endpoint instead.

In `@apps/web/stellar-social-sdk/src/utils/crypto.ts`:
- Around line 45-52: The hashPhone method currently in utils/crypto.ts (static
hashPhone) is vulnerable to brute-force because it hashes low-entropy phone
numbers and then truncates the result; change it to combine the cleaned phone
number with a secret pepper (server-side) and run it through a computational KDF
(e.g., PBKDF2 or scrypt) with a per-item salt and sufficient iterations/work
factor, then output the full derived value (avoid arbitrary truncation). Ensure
the secret pepper is not stored in client code, and update any consumers of
hashPhone to expect the KDF output format (salt + derived key) rather than the
current truncated hex.
🟡 Minor comments (10)
apps/backend/src/_deprecated/README.md-31-33 (1)

31-33: Use a precise migration date.

“2024” is ambiguous; please update this to an exact YYYY‑MM‑DD date once confirmed so the deprecation timeline is unambiguous.

apps/backend/src/_deprecated/README.md-3-16 (1)

3-16: Align SDK naming and provider list with actual support.

The doc alternates between “Stellar Social SDK” and “Stellar Account Abstraction SDK,” and it claims Facebook/Phone support even though the PR objectives say Google-only. Please make the naming consistent and mark non‑Google providers as planned if they’re not live yet.

✍️ Proposed doc tweak
-These files were deprecated as part of the migration to **Stellar Social SDK** for authentication.
+These files were deprecated as part of the migration to **Stellar Account Abstraction (Social) SDK** for authentication.

-This has been replaced with the **Stellar Account Abstraction SDK** which:
+This has been replaced with the **Stellar Account Abstraction (Social) SDK** which:
 - Handles authentication entirely on the client-side
-- Supports social login (Google, Facebook, Phone)
+- Supports social login (Google; Facebook/Phone planned)
apps/backend/src/index.ts-66-69 (1)

66-69: Remove or update stale wallet-auth tests that reference non-existent endpoints.

The wallet-auth endpoints (/api/auth/challenge, /api/auth/wallet) have already been removed and are not mounted in index.ts. However, apps/backend/tests/integration/wallet-auth.test.ts still contains tests that attempt to call these removed endpoints. These tests should either be deleted (if no longer needed) or moved to a deprecated test suite. Additionally, web tests in apps/web/tests/e2e/register.spec.ts mock /api/auth/register, but the actual mounted endpoint is /auth/register—verify test paths align with current route definitions.

apps/web/stellar-social-sdk/package.json.backup-1-16 (1)

1-16: Remove backup file from version control.

Backup files (.backup) should not be committed. Use .gitignore to exclude them, or delete this file if it's no longer needed. The actual package.json should be the source of truth.

Additionally, the author field contains a placeholder "tu-nombre" that should be updated.

apps/web/src/app/layout.tsx-23-26 (1)

23-26: Permissions-Policy via httpEquiv meta tag is ineffective.

The Permissions-Policy header cannot be set via a <meta http-equiv> tag—browsers ignore it. This policy must be delivered as an HTTP response header.

If you need to restrict these permissions, configure your server or Next.js middleware to return the header:

// middleware.ts or next.config.js headers()
{
  key: 'Permissions-Policy',
  value: 'identity-credentials-get=(), publickey-credentials-get=()'
}
apps/web/stellar-social-sdk/src/utils/crypto.ts-8-27 (1)

8-27: Security consideration: deterministic key derivation from social identity using custom KDF.

This implementation derives a Stellar keypair deterministically from provider ID + hardcoded salt using a custom SHA256-based scheme. This approach diverges from Stellar's recommended SEP-0005 standard (which uses BIP-39 + SLIP-0010 hardened derivation) and introduces specific risks:

  1. Reproducibility risk: Anyone with the user's provider ID (e.g., Google sub) and knowledge of the hardcoded salt stellar-social-v1 can derive the same keypair. If the provider ID is discoverable through normal account flows or exposed in logs/errors, the wallet keypair is compromised.
  2. Non-standard KDF: Using ad-hoc hashing for key material differs from established key derivation standards. Consider adopting SEP-0005 or documenting why a custom approach is necessary.

This is a deliberate trade-off for seedless wallet UX. Mitigate by:

  • Ensuring provider IDs are never logged, exposed in errors, or accessible to untrusted parties
  • Documenting this security model so users and operators understand the trust assumptions
  • Considering whether the salt should be per-user or environment-specific rather than hardcoded globally
apps/web/stellar-social-sdk/package.json-12-12 (1)

12-12: Test script references non-existent dist/test.js.

The test script runs node dist/test.js, but the build configuration (Rollup) only generates index.js, index.esm.js, and index.d.ts. This script will fail unless test.js is explicitly built or the script is updated.

Consider either:

  1. Adding a proper test framework (e.g., vitest, jest)
  2. Updating the script to reference the correct test file location
apps/web/src/components/auth/SocialLoginButton.tsx-16-105 (1)

16-105: Clear retry timers on unmount to avoid post‑unmount state updates.

The retry setTimeouts aren’t cleared; if the component unmounts during navigation, callbacks can still fire and call setState/handleError. Add a cancel flag + cleanup to prevent leaks and React warnings.

🧹 Proposed fix
   const [state, setState] = useState<ButtonState>('loading');
   const [errorMessage, setErrorMessage] = useState<string | null>(null);
   const buttonContainerRef = useRef<HTMLDivElement>(null);
   const renderAttempts = useRef(0);
   const hasRendered = useRef(false);
+  const timeoutIds = useRef<number[]>([]);
   const maxAttempts = 20; // 10 seconds max wait

   useEffect(() => {
+    let cancelled = false;
+    const schedule = (fn: () => void, ms: number) => {
+      const id = window.setTimeout(() => {
+        if (!cancelled) fn();
+      }, ms);
+      timeoutIds.current.push(id);
+    };
+
     if (!GOOGLE_CLIENT_ID) {
       handleError('Google Client ID not configured');
       return;
     }

     const renderGoogleButton = () => {
+      if (cancelled) return;
       // Wait for the ref to be available
       if (!buttonContainerRef.current) {
         renderAttempts.current += 1;
         if (renderAttempts.current < maxAttempts) {
-          setTimeout(renderGoogleButton, 100);
+          schedule(renderGoogleButton, 100);
         } else {
           handleError('Button container not available');
         }
         return;
       }
@@
       } else {
         // Retry if script hasn't loaded yet
         renderAttempts.current += 1;

         if (renderAttempts.current < maxAttempts) {
-          setTimeout(renderGoogleButton, 500);
+          schedule(renderGoogleButton, 500);
         } else {
           handleError('Google Sign-In unavailable');
         }
       }
     };

     // Start trying to render immediately
     renderGoogleButton();
+    return () => {
+      cancelled = true;
+      timeoutIds.current.forEach(clearTimeout);
+      timeoutIds.current = [];
+    };
   }, [handleError]);
apps/web/stellar-social-sdk/src/auth/StellarSocialAccount.ts-100-107 (1)

100-107: Handle non‑standard asset types in balances.

Horizon returns multiple asset types in account balances, including liquidity_pool_shares (Protocol 18+). The liquidity_pool_shares type does not include asset_code or asset_issuer fields—instead it has liquidity_pool_id—but your code assumes these fields always exist for non-native types. This produces undefined:undefined for pool share balances.

Handle each asset type explicitly: native'XLM', liquidity_pool_shares'LP:{liquidity_pool_id}', standard credit types (credit_alphanum4/12) → '{asset_code}:{asset_issuer}', and add a fallback for any future types.

Suggested fix
-      return account.balances.map((balance: any) => ({
-        balance: balance.balance,
-        asset: balance.asset_type === 'native' ? 'XLM' : 
-               `${balance.asset_code}:${balance.asset_issuer}`
-      }));
+      return account.balances.map((balance: any) => {
+        if (balance.asset_type === 'native') {
+          return { balance: balance.balance, asset: 'XLM' };
+        }
+        if (balance.asset_type === 'liquidity_pool_shares') {
+          return { balance: balance.balance, asset: `LP:${balance.liquidity_pool_id}` };
+        }
+        if (balance.asset_code && balance.asset_issuer) {
+          return { balance: balance.balance, asset: `${balance.asset_code}:${balance.asset_issuer}` };
+        }
+        return { balance: balance.balance, asset: balance.asset_type };
+      });
apps/web/stellar-social-sdk/src/auth/StellarSocialAccount.ts-44-84 (1)

44-84: Truncate memo by byte length, not character length.

Stellar text memos are limited to 28 bytes in the protocol and the SDK enforces this via Memo._validateTextValue(). The current code uses memo.length > 28, which counts UTF-16 code units instead of bytes. Non-ASCII UTF-8 characters (e.g., emojis, accented characters) can encode to multiple bytes, causing the string to exceed 28 bytes even when the character count is under the limit. This will fail when submitted to the network.

Use TextEncoder.encodeInto() to safely truncate at byte boundaries without splitting multibyte UTF-8 sequences:

🧭 Proposed fix
       if (memo) {
         // Stellar text memos have a 28-byte limit
-        const truncatedMemo = memo.length > 28 ? memo.substring(0, 28) : memo;
+        const encoder = new TextEncoder();
+        const buffer = new Uint8Array(28);
+        const { read } = encoder.encodeInto(memo, buffer);
+        const truncatedMemo = memo.slice(0, read);
         txBuilder.addMemo(Memo.text(truncatedMemo));
       }
🧹 Nitpick comments (13)
apps/web/stellar-social-sdk/src/utils/crypto.ts (1)

37-43: isValidJWT provides only structural validation, not security validation.

This method checks only that the token has three dot-separated parts. It does not verify the signature, expiration, or issuer. Consider renaming to hasJWTStructure or adding a clear docstring warning that this is insufficient for auth validation.

Suggested rename for clarity
  /**
-  * Verify JWT token structure (basic validation)
+  * Check if string has JWT structure (3 dot-separated parts).
+  * WARNING: Does NOT verify signature, expiry, or claims.
   */
- static isValidJWT(token: string): boolean {
+ static hasJWTStructure(token: string): boolean {
    const parts = token.split('.');
    return parts.length === 3;
  }
apps/web/stellar-social-sdk/src/index.ts.corrupted (2)

237-269: Duplicate logic: createNewAccount and createNewAccountWithKeypair are nearly identical.

These two methods differ only in how the keypair is passed to StellarSocialAccount. Consider consolidating into a single method that always accepts a keypair parameter.

Also applies to: 358-390


247-247: Fixed 3-second delay may be insufficient or excessive.

The hardcoded setTimeout(resolve, 3000) after Friendbot funding is arbitrary. Network conditions vary, and this could either fail (if funding takes longer) or unnecessarily slow the UX.

Consider polling the account existence instead:

// Poll until account exists or timeout
const maxAttempts = 10;
for (let i = 0; i < maxAttempts; i++) {
  try {
    await this.server.loadAccount(publicKey);
    break;
  } catch {
    await new Promise(r => setTimeout(r, 1000));
  }
}

Also applies to: 367-368

apps/web/src/lib/stellar-social-sdk.ts (1)

1-10: Use the stellar-social-sdk path alias instead of a relative path.

Since tsconfig.json already defines a path alias for stellar-social-sdk, consider using it here for consistency and easier maintenance:

-export { StellarSocialSDK } from '../../stellar-social-sdk/dist/index.esm.js';
+export { StellarSocialSDK } from 'stellar-social-sdk';
 export type {
   SocialAuthConfig,
   AuthMethod,
   AuthResult,
   SocialAccountData,
-} from '../../stellar-social-sdk/dist/index.esm.js';
+} from 'stellar-social-sdk';

Also, consider using English for comments to maintain consistency with the rest of the codebase.

apps/web/src/app/register/page.tsx (1)

12-27: Consider using a server-side redirect in next.config.js instead.

A client-side redirect requires JavaScript execution and shows a loading spinner. For better UX and SEO, consider using Next.js permanent redirects in next.config.js:

// next.config.js
module.exports = {
  async redirects() {
    return [
      {
        source: '/register',
        destination: '/login',
        permanent: true, // 308 redirect
      },
    ];
  },
};

This eliminates the need for this page entirely, provides instant redirection, and works without JavaScript.

apps/web/stellar-social-sdk/tsconfig.json (1)

1-17: LGTM with a minor suggestion on moduleResolution.

The configuration is appropriate for building the SDK. Consider updating moduleResolution to "bundler" or "node16" for better ESM support and consistency with the parent app's tsconfig:

-    "moduleResolution": "node"
+    "moduleResolution": "bundler"

The "node" resolution mode is the legacy CommonJS resolution algorithm and may behave differently than the parent app's "bundler" mode.

apps/web/stellar-social-sdk/src/config.ts (1)

1-3: Clarify that the default contract ID is testnet-specific.

Since the default appears tied to testnet, consider renaming to DEFAULT_TESTNET_CONTRACT_ID (or removing the default entirely) to avoid accidental mainnet usage without an explicit contract ID.

apps/web/src/components/layout/Navbar.tsx (1)

51-53: Avoid “undefined...” in the user name fallback.

If user.name is empty and user.publicKey is missing, the UI can render “undefined...”. Consider a safer fallback.

💡 Suggested change
-                <span className="hidden sm:inline text-muted-foreground">
-                  {user.name || `${user.publicKey?.slice(0, 8)}...`}
-                </span>
+                <span className="hidden sm:inline text-muted-foreground">
+                  {user.name ?? (user.publicKey ? `${user.publicKey.slice(0, 8)}...` : 'User')}
+                </span>
apps/web/src/hooks/auth/__tests__/use-auth.test.tsx (1)

67-75: Reset NEXT_PUBLIC_ env vars after each test run.*
They’re overwritten in beforeEach but never restored, which can leak into other test files with different env expectations.

♻️ Suggested fix
 // Wrapper para el provider
 const wrapper = ({ children }: { children: ReactNode }) => <AuthProvider>{children}</AuthProvider>;
+
+const ORIGINAL_ENV = {
+  NEXT_PUBLIC_GOOGLE_CLIENT_ID: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
+  NEXT_PUBLIC_CONTRACT_ID: process.env.NEXT_PUBLIC_CONTRACT_ID,
+  NEXT_PUBLIC_STELLAR_NETWORK: process.env.NEXT_PUBLIC_STELLAR_NETWORK,
+};
+
+const restoreEnvVar = (key: keyof typeof ORIGINAL_ENV) => {
+  const value = ORIGINAL_ENV[key];
+  if (value === undefined) {
+    delete process.env[key];
+  } else {
+    process.env[key] = value;
+  }
+};
 
 describe('useAuth Hook', () => {
   beforeEach(() => {
     jest.clearAllMocks();
     localStorageMock.clear();
 
     // Reset environment variables
     process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID = 'test-client-id';
     process.env.NEXT_PUBLIC_CONTRACT_ID = 'test-contract-id';
     process.env.NEXT_PUBLIC_STELLAR_NETWORK = 'testnet';
   });
+
+  afterEach(() => {
+    restoreEnvVar('NEXT_PUBLIC_GOOGLE_CLIENT_ID');
+    restoreEnvVar('NEXT_PUBLIC_CONTRACT_ID');
+    restoreEnvVar('NEXT_PUBLIC_STELLAR_NETWORK');
+  });
apps/web/stellar-social-sdk/src/providers/FreighterProvider.ts (1)

33-40: Add fallback for publicKey field when normalizing requestAccess() response.

The code currently handles string and { address }, but the local type definition in the codebase declares { publicKey }. While @stellar/freighter-api v4.1.0 appears to use address, adding a fallback improves defensive compatibility in case of API variations.

♻️ Suggested fix
-      const publicKey = typeof accessResult === 'string'
-        ? accessResult
-        : accessResult.address;
+      const publicKey =
+        typeof accessResult === 'string'
+          ? accessResult
+          : accessResult.address ?? accessResult.publicKey;
apps/web/src/hooks/auth/procted-route.tsx (1)

12-19: Stabilize default allowedAuthTypes to avoid effect churn.

The default array literal is recreated each render, so the effect dependency changes every time. Define a stable constant to prevent unnecessary re-runs/redirect checks.

♻️ Proposed fix
+const DEFAULT_ALLOWED_AUTH_TYPES = ['google', 'freighter'] as const;
+
 export default function ProtectedRoute({
   children,
   redirectTo = '/login',
-  allowedAuthTypes = ['google', 'freighter'],
+  allowedAuthTypes = DEFAULT_ALLOWED_AUTH_TYPES,
 }: ProtectedRouteProps) {

Also applies to: 39-39

apps/web/src/types/auth.ts (1)

14-52: Use a shared AuthMethodType union to avoid drift.

AuthMethodData.type is string while SocialUser.authMethod and AuthContextType.authMethod are unions. A shared union keeps types aligned and prevents accidental widening.

🧩 Proposed fix
+export type AuthMethodType = 'google' | 'freighter' | 'facebook' | 'phone' | 'passkey';
+
 export interface SocialUser {
   publicKey: string;
   name?: string;
   email?: string;
   picture?: string;
-  authMethod: 'google' | 'freighter';
+  authMethod: AuthMethodType;
 }
@@
 export interface AuthMethodData {
-  type: string;
+  type: AuthMethodType;
   identifier: string;
   metadata?: {
     name?: string;
     email?: string;
     picture?: string;
   };
 }
@@
 export interface AuthContextType {
@@
-  authMethod: 'google' | 'freighter' | null;
+  authMethod: AuthMethodType | null;
apps/web/stellar-social-sdk/src/index.ts.backup (1)

1-299: Remove the .backup source file to avoid accidental packaging.

Leaving a full SDK implementation in a .backup file can confuse tooling or accidentally ship stale APIs. Consider deleting it or excluding it from build/package inputs.

Copy link
Contributor

@respp respp left a comment

Choose a reason for hiding this comment

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

We need to address 2 troubles before merging: First, the implementation of atomic database transactions to ensure that event logging and state updates occur as a single unit. Second, the system must validate the Stellar transaction ID before processing any event to prevent duplicate records if the listener retries.

Finally, please avoid silent catches just with clg. If an error occurs and it's only printed to the console without being re-thrown or properly logged to a monitoring system, it creates a false sense of security while critical payment syncs might be failing in the background.

Once these points and the CodeRabbit suggestions are addressed, the PR will be approved.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
apps/backend/src/middleware/error.middleware.ts (1)

54-60: TokenExpiredError branch is shadowed by JsonWebTokenError.

Because TokenExpiredError subclasses JsonWebTokenError, the expired-token path won't run. Swap the checks so expired tokens return the correct message.

🛠️ Proposed fix
-  } else if (err instanceof JsonWebTokenError) {
-    statusCode = 401;
-    response.error = 'Invalid token';
-  } else if (err instanceof TokenExpiredError) {
-    statusCode = 401;
-    response.error = 'Token expired';
-  } else if (err instanceof BookingError) {
+  } else if (err instanceof TokenExpiredError) {
+    statusCode = 401;
+    response.error = 'Token expired';
+  } else if (err instanceof JsonWebTokenError) {
+    statusCode = 401;
+    response.error = 'Invalid token';
+  } else if (err instanceof BookingError) {
🤖 Fix all issues with AI agents
In `@apps/backend/database/migrations/00006_add_atomic_functions.sql`:
- Around line 11-23: The SECURITY DEFINER function process_sync_event_atomic
lacks an explicit search_path; add a fixed, trusted schema search_path (the
schema that owns your DB objects, not public) with pg_temp last by adding a SET
search_path clause to the function declaration (or a SET statement at the top of
the function body) so object resolution cannot be hijacked; ensure "public" is
not included and that pg_temp is the final entry in the search_path.
- Around line 71-86: The EXCEPTION block inside process_sync_event_atomic
currently INSERTs into sync_logs then RAISEs, but that insert is rolled back
when the exception aborts the transaction; change the handler to emit a durable
out-of-transaction notification or server log instead: replace the INSERT with
either a RAISE LOG/NOTICE that includes SQLERRM/SQLSTATE/p_event_id/p_event_type
or call pg_notify (e.g., PERFORM pg_notify('sync_errors', json_build_object(...
)::text)) so a separate listener/process can persist the log, or implement an
out-of-transaction commit via a helper (dblink/autonomous transaction) to write
to sync_logs before re-raising; update the EXCEPTION block in
process_sync_event_atomic accordingly.

In `@apps/backend/database/migrations/00007_add_payment_constraints.sql`:
- Around line 36-57: The payment confirmation allows NULL/empty
p_transaction_hash, letting calls bypass uniqueness—add a guard in the same
function/procedure before updating booking status: validate p_transaction_hash
is NOT NULL and not an empty/whitespace string (check p_transaction_hash IS NOT
NULL AND trim(p_transaction_hash) <> ''), and if invalid RETURN
jsonb_build_object with success=false and an error like 'MISSING_TRANSACTION'
(similar shape to existing returns referencing v_booking and
payment_transaction_hash); keep the subsequent duplicate-exists check as-is so
only non-empty hashes reach it.

In `@apps/backend/src/blockchain/trustlessWork.ts`:
- Around line 306-309: The thrown EscrowError in
apps/backend/src/blockchain/trustlessWork.ts uses the code 'CREATE_ESCROW_FAIL'
which mismatches the middleware's expected 'ESCROW_CREATE_FAIL'; update the
throw to use 'ESCROW_CREATE_FAIL' (i.e., change the errorCode argument in the
EscrowError constructor) so EscrowError instantiations in this file align with
the middleware mapping; verify the change where EscrowError is thrown (the catch
block that currently throws new EscrowError('Failed to create escrow',
'CREATE_ESCROW_FAIL', error)) and adjust any related unit tests or callers if
they assert the previous code.

In `@apps/backend/src/services/booking.service.ts`:
- Around line 516-574: Add an explicit union return type to
confirmBookingPayment to make the fallback shape explicit: change the signature
of confirmBookingPayment to return Promise<Booking | { id: string; status:
string; payment_transaction_hash: string }>, or define and use a named type
(e.g., PartialConfirmedBooking) and return that; update callers (notably
booking.controller.ts usage at the endpoint handling the result) to discriminate
the union and handle both full Booking and the minimal fallback object
accordingly.

In `@apps/backend/src/services/cache.service.ts`:
- Around line 78-81: The catch blocks in cache service currently await
loggingService.logBlockchainOperation and loggingService.logBlockchainError
which can throw and break the "should not stop the application" guarantee;
update each cache error catch (the blocks that call
loggingService.logBlockchainOperation and loggingService.logBlockchainError) to
protect logging by wrapping those logging calls in their own try/catch or call
them without awaiting and attach .catch(...) so any logging error is swallowed,
and then continue to return the existing default (e.g., null or fallback) from
the cache method; apply this pattern to all cache-related catch blocks that
invoke logBlockchainOperation/logBlockchainError (the instances around the shown
snippet and the other occurrences mentioned).

In `@apps/backend/src/services/sync.service.ts`:
- Around line 829-841: Guard against a possible null/undefined RPC payload
before accessing data.success in the atomic processing branch: check `if
(!data)` and log/throw or treat as failure (e.g., logBlockchainError and throw a
SyncError) so `data.success` is only read when `data` is truthy; and change the
duplicate-event return shape from `{ success: true, error: 'DUPLICATE_EVENT' }`
to a clearer form such as `{ success: true, skipped: true, reason: 'duplicate'
}` (and update the method signature to reflect the new optional fields like
skipped/reason/syncEventId) so callers aren’t confused by a success=true with an
error string.
🧹 Nitpick comments (3)
apps/backend/src/services/sync.service.ts (3)

333-343: Log correlation is broken on error path.

The original logId from line 311 should be used for error logging to maintain correlation with the operation that failed. Creating a new log entry loses the link to the original operation context.

♻️ Suggested fix
     } catch (error) {
       this.failedEvents++;
 
-      // Log the error using loggingService for proper error serialization
-      const errorLog = await loggingService.logBlockchainOperation('pollForEvents', {
-        lastProcessedBlock: this.lastProcessedBlock,
-      });
-      await loggingService.logBlockchainError(errorLog, {
+      // Log the error using the original logId for correlation
+      await loggingService.logBlockchainError(logId, {
         error,
         context: 'Failed to poll for blockchain events',
+        lastProcessedBlock: this.lastProcessedBlock,
       });
 
       // Re-throw as SyncError to propagate to callers
       throw new SyncError('Failed to poll for blockchain events', 'POLL_EVENTS_FAIL', error);
     }

Note: This requires moving the logId declaration to a scope accessible in the catch block (e.g., declaring it before the try block).


521-523: Silent catch block may mask logging infrastructure issues.

The empty .catch(() => {}) swallows all errors silently. While the intent to prevent cascading errors is valid, consider at minimum logging to console.error so logging infrastructure failures are observable.

♻️ Suggested improvement
         });
       })
-      .catch(() => {
-        // Ignore logging failures to prevent cascading errors
+      .catch((loggingError) => {
+        // Log to console as last resort - don't let logging failures cascade
+        console.error('Failed to log event parsing error:', loggingError);
       });

631-631: Double type assertion bypasses type checking.

The as unknown as Record<string, unknown> cast is a type safety escape hatch. Since BlockchainEventData has typed fields, consider updating syncBookingFromBlockchain to accept BlockchainEventData directly, or use a type-safe conversion utility.

Comment on lines +71 to +86
EXCEPTION WHEN OTHERS THEN
-- Log error
INSERT INTO public.sync_logs (operation, status, error_details, created_at)
VALUES (
'process_sync_event_atomic',
'error',
jsonb_build_object(
'error', SQLERRM,
'error_state', SQLSTATE,
'event_id', p_event_id,
'event_type', p_event_type
),
now()
);
-- Re-raise the exception to rollback the transaction
RAISE;
Copy link
Contributor

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:

PostgreSQL plpgsql EXCEPTION block INSERT rollback RAISE re-raise transaction

💡 Result:

In PL/pgSQL, a BEGIN … EXCEPTION … END block is implemented as an implicit subtransaction (savepoint): entering such a block creates a savepoint; if an error occurs, PostgreSQL rolls back to that savepoint and then runs the EXCEPTION handler. Changes made inside the block’s main section are undone, but work done before the block is not. [1], [2]

  • What gets rolled back?
    All persistent DB changes made since the start of that block (i.e., inside the block’s “try” part) are rolled back when the handler is entered. [1]

  • Can I INSERT in the EXCEPTION handler and keep it?
    Yes—if you handle the error and let execution continue normally, the INSERT you do in the handler is part of the surrounding transaction and can be committed later (assuming you don’t abort the transaction afterward). This works because the rollback happens before the handler runs (rollback-to-savepoint, then handler). [1], [2]

  • What if I RAISE again (re-raise)?
    A bare RAISE; inside an EXCEPTION handler re-throws the currently handled error. [3] If that propagated error is not caught by an outer EXCEPTION block, it will abort the surrounding transaction—so any “log insert” you did in the handler will be rolled back along with everything else in that transaction. [4]

  • Key pattern

    • Want to log and continue: catch exception, INSERT log row, do not re-raise.
    • Want to log and fail the call: either accept that the log row will roll back too, or catch the error at a higher level and commit there; otherwise you need an “outside the transaction” logging mechanism (not something PL/pgSQL can do with normal COMMIT/ROLLBACK inside a function). [4]

Sources: [1] PostgreSQL PL/pgSQL control structures / exception rollback behavior (postgresql.org), [2] subtransaction/savepoint behavior of EXCEPTION blocks (cybertec-postgresql.com), [3] RAISE; re-throws the current error (postgresql.org), [4] re-raising causes log insert to roll back unless caught outside (stackoverflow.com)

Citations:


Exception log insert will be rolled back when re-raising.

In PostgreSQL, re-raising an exception aborts the surrounding transaction, so the error row inserted into sync_logs won't persist. If you need durable error logs, use RAISE LOG, pg_notify, or handle the exception in the caller and commit the log separately.

🤖 Prompt for AI Agents
In `@apps/backend/database/migrations/00006_add_atomic_functions.sql` around lines
71 - 86, The EXCEPTION block inside process_sync_event_atomic currently INSERTs
into sync_logs then RAISEs, but that insert is rolled back when the exception
aborts the transaction; change the handler to emit a durable out-of-transaction
notification or server log instead: replace the INSERT with either a RAISE
LOG/NOTICE that includes SQLERRM/SQLSTATE/p_event_id/p_event_type or call
pg_notify (e.g., PERFORM pg_notify('sync_errors', json_build_object(...
)::text)) so a separate listener/process can persist the log, or implement an
out-of-transaction commit via a helper (dblink/autonomous transaction) to write
to sync_logs before re-raising; update the EXCEPTION block in
process_sync_event_atomic accordingly.

Comment on lines +36 to +57
-- Check if booking is already paid
IF v_booking.payment_transaction_hash IS NOT NULL THEN
RETURN jsonb_build_object(
'success', false,
'error', 'ALREADY_PAID',
'message', 'Booking has already been paid',
'existing_hash', v_booking.payment_transaction_hash
);
END IF;

-- Check if transaction hash is already used by another booking
IF EXISTS (
SELECT 1 FROM public.bookings
WHERE payment_transaction_hash = p_transaction_hash
AND id != p_booking_id
) THEN
RETURN jsonb_build_object(
'success', false,
'error', 'DUPLICATE_TRANSACTION',
'message', 'Transaction hash is already used by another booking'
);
END IF;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate transaction hash before confirming payment.

p_transaction_hash can be NULL/empty, which allows confirming a booking without proof and bypasses the unique index (NULLs are allowed). Add a guard before status update.

🛠️ Proposed fix
     -- Check if booking is already paid
+    IF p_transaction_hash IS NULL OR length(trim(p_transaction_hash)) = 0 THEN
+        RETURN jsonb_build_object(
+            'success', false,
+            'error', 'INVALID_TRANSACTION',
+            'message', 'Transaction hash is required'
+        );
+    END IF;
+
+    -- Check if booking is already paid
     IF v_booking.payment_transaction_hash IS NOT NULL THEN
🤖 Prompt for AI Agents
In `@apps/backend/database/migrations/00007_add_payment_constraints.sql` around
lines 36 - 57, The payment confirmation allows NULL/empty p_transaction_hash,
letting calls bypass uniqueness—add a guard in the same function/procedure
before updating booking status: validate p_transaction_hash is NOT NULL and not
an empty/whitespace string (check p_transaction_hash IS NOT NULL AND
trim(p_transaction_hash) <> ''), and if invalid RETURN jsonb_build_object with
success=false and an error like 'MISSING_TRANSACTION' (similar shape to existing
returns referencing v_booking and payment_transaction_hash); keep the subsequent
duplicate-exists check as-is so only non-empty hashes reach it.

Comment on lines 306 to 309
return response.escrowAddress;
} catch (error) {
console.error('Error creating escrow:', error);
throw new Error('Failed to create escrow');
throw new EscrowError('Failed to create escrow', 'CREATE_ESCROW_FAIL', error);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use consistent escrow error codes across layers.

The middleware maps ESCROW_CREATE_FAIL, but this throws CREATE_ESCROW_FAIL. Align the code to avoid mismatched response codes.

🛠️ Proposed fix
-    throw new EscrowError('Failed to create escrow', 'CREATE_ESCROW_FAIL', error);
+    throw new EscrowError('Failed to create escrow', 'ESCROW_CREATE_FAIL', error);
📝 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
return response.escrowAddress;
} catch (error) {
console.error('Error creating escrow:', error);
throw new Error('Failed to create escrow');
throw new EscrowError('Failed to create escrow', 'CREATE_ESCROW_FAIL', error);
}
return response.escrowAddress;
} catch (error) {
throw new EscrowError('Failed to create escrow', 'ESCROW_CREATE_FAIL', error);
}
🤖 Prompt for AI Agents
In `@apps/backend/src/blockchain/trustlessWork.ts` around lines 306 - 309, The
thrown EscrowError in apps/backend/src/blockchain/trustlessWork.ts uses the code
'CREATE_ESCROW_FAIL' which mismatches the middleware's expected
'ESCROW_CREATE_FAIL'; update the throw to use 'ESCROW_CREATE_FAIL' (i.e., change
the errorCode argument in the EscrowError constructor) so EscrowError
instantiations in this file align with the middleware mapping; verify the change
where EscrowError is thrown (the catch block that currently throws new
EscrowError('Failed to create escrow', 'CREATE_ESCROW_FAIL', error)) and adjust
any related unit tests or callers if they assert the previous code.

Comment on lines 516 to 574
export async function confirmBookingPayment(bookingId: string, transactionHash: string) {
const log = await loggingService.logBlockchainOperation('confirmBookingPayment', {
bookingId,
transactionHash,
});

try {
const { data: existingBooking, error: fetchError } = await supabase
.from('bookings')
.select('*')
.eq('id', bookingId)
.single();
// Use atomic RPC function to validate and confirm payment
const { data, error } = await supabase.rpc('confirm_booking_payment_atomic', {
p_booking_id: bookingId,
p_transaction_hash: transactionHash,
});

if (fetchError || !existingBooking) {
throw new BookingError('Booking not found or failed to retrieve', 'NOT_FOUND', fetchError);
if (error) {
await loggingService.logBlockchainError(log, { error, context: 'RPC call failed' });
throw new BookingError('Database error during payment confirmation', 'DB_ERROR', error);
}

const { data, error } = await supabase
// Handle validation errors from the RPC function
if (!data.success) {
const errorMap: Record<string, [string, string]> = {
NOT_FOUND: ['Booking not found', 'NOT_FOUND'],
ALREADY_PAID: ['Booking has already been paid', 'ALREADY_PAID'],
DUPLICATE_TRANSACTION: [
'Transaction hash is already used by another booking',
'DUPLICATE_TRANSACTION',
],
DB_ERROR: ['Database error occurred', 'DB_ERROR'],
};
const [msg, code] = errorMap[data.error] || ['Payment confirmation failed', 'CONFIRM_FAIL'];
throw new BookingError(msg, code, data);
}

// Fetch the updated booking to return full data
const { data: booking, error: fetchError } = await supabase
.from('bookings')
.update({
status: 'confirmed',
payment_transaction_hash: transactionHash,
paid_at: new Date().toISOString(),
})
.select('*')
.eq('id', bookingId)
.select()
.single();

if (error || !data) {
throw new BookingError('Failed to confirm booking status update', 'CONFIRM_FAIL', error);
if (fetchError || !booking) {
// Payment was confirmed but fetch failed - log but don't throw
await loggingService.logBlockchainError(log, {
error: fetchError,
context: 'Payment confirmed but failed to fetch updated booking',
});
// Return minimal data since confirmation was successful
return { id: bookingId, status: 'confirmed', payment_transaction_hash: transactionHash };
}
return data;

await loggingService.logBlockchainSuccess(log, { booking });
return booking;
} catch (error) {
if (error instanceof BookingError) {
throw error;
}
throw new BookingError('Confirmation error', 'CONFIRM_FAIL', error);
await loggingService.logBlockchainError(log, { error, context: 'confirmBookingPayment' });
throw new BookingError('Payment confirmation failed', 'CONFIRM_FAIL', error);
}
Copy link
Contributor

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:

# Find all call sites of confirmBookingPayment
rg -n 'confirmBookingPayment\s*\(' --type=ts

Repository: Stellar-Rent/stellar-rent

Length of output: 338


🏁 Script executed:

# Check the current return type annotation in the booking.service.ts file
sed -n '516,530p' apps/backend/src/services/booking.service.ts

Repository: Stellar-Rent/stellar-rent

Length of output: 631


🏁 Script executed:

# Search for the Booking type definition to understand the full structure
rg -n 'type Booking|interface Booking' --type=ts -A 10

Repository: Stellar-Rent/stellar-rent

Length of output: 19887


🏁 Script executed:

# Check the call site in booking.controller.ts around line 221
sed -n '210,235p' apps/backend/src/controllers/booking.controller.ts

Repository: Stellar-Rent/stellar-rent

Length of output: 838


🏁 Script executed:

# Check if confirmBookingPayment has any return type annotation
rg -A 2 'export async function confirmBookingPayment' apps/backend/src/services/booking.service.ts

Repository: Stellar-Rent/stellar-rent

Length of output: 258


🏁 Script executed:

# Look at the full Booking type structure in backend
cat apps/backend/src/types/booking.types.ts

Repository: Stellar-Rent/stellar-rent

Length of output: 2795


🏁 Script executed:

# Verify if there are other calls to confirmBookingPayment
rg -n 'confirmBookingPayment' --type=ts -B 2 -A 2

Repository: Stellar-Rent/stellar-rent

Length of output: 3475


🏁 Script executed:

# Check if there's a BookingConfirmationResponse type that might be used
rg -n 'BookingConfirmationResponse' apps/backend/src/types/booking.types.ts -A 5

Repository: Stellar-Rent/stellar-rent

Length of output: 220


Add explicit return type to clarify the fallback object shape.

The function can return either a full Booking or a partial object { id, status, payment_transaction_hash }. Without an explicit return type, the controller passes inconsistent shapes to the API response. Consider adding a union return type:

♻️ Suggested typing
-export async function confirmBookingPayment(bookingId: string, transactionHash: string) {
+export async function confirmBookingPayment(
+  bookingId: string,
+  transactionHash: string
+): Promise<
+  | Booking
+  | Pick<Booking, 'id' | 'status' | 'payment_transaction_hash'>
+> {

This ensures callers at booking.controller.ts:221 explicitly handle both cases and prevents assumptions about which fields will be present.

🤖 Prompt for AI Agents
In `@apps/backend/src/services/booking.service.ts` around lines 516 - 574, Add an
explicit union return type to confirmBookingPayment to make the fallback shape
explicit: change the signature of confirmBookingPayment to return
Promise<Booking | { id: string; status: string; payment_transaction_hash: string
}>, or define and use a named type (e.g., PartialConfirmedBooking) and return
that; update callers (notably booking.controller.ts usage at the endpoint
handling the result) to discriminate the union and handle both full Booking and
the minimal fallback object accordingly.

Comment on lines +78 to 81
// Cache errors should be logged but not stop the application
const log = await loggingService.logBlockchainOperation('cache_get', { key });
await loggingService.logBlockchainError(log, { error, context: 'Cache get failed' });
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent logging failures from escaping cache error paths.

These catch blocks now await logging calls; if logging throws, cache calls will reject, contradicting the “should not stop the application” intent. Wrap the logging in its own try/catch (or fire-and-forget with a .catch) and keep returning defaults. Apply the same pattern to all cache operations here.

🔧 Suggested fix pattern (apply to all cache catch blocks)
-    } catch (error) {
-      // Cache errors should be logged but not stop the application
-      const log = await loggingService.logBlockchainOperation('cache_get', { key });
-      await loggingService.logBlockchainError(log, { error, context: 'Cache get failed' });
-      return null;
-    }
+    } catch (error) {
+      // Cache errors should be logged but not stop the application
+      try {
+        const log = await loggingService.logBlockchainOperation('cache_get', { key });
+        await loggingService.logBlockchainError(log, { error, context: 'Cache get failed' });
+      } catch {
+        // best-effort logging; swallow to keep cache failure non-fatal
+      }
+      return null;
+    }

Also applies to: 99-102, 117-120, 138-143, 272-275

🤖 Prompt for AI Agents
In `@apps/backend/src/services/cache.service.ts` around lines 78 - 81, The catch
blocks in cache service currently await loggingService.logBlockchainOperation
and loggingService.logBlockchainError which can throw and break the "should not
stop the application" guarantee; update each cache error catch (the blocks that
call loggingService.logBlockchainOperation and
loggingService.logBlockchainError) to protect logging by wrapping those logging
calls in their own try/catch or call them without awaiting and attach
.catch(...) so any logging error is swallowed, and then continue to return the
existing default (e.g., null or fallback) from the cache method; apply this
pattern to all cache-related catch blocks that invoke
logBlockchainOperation/logBlockchainError (the instances around the shown
snippet and the other occurrences mentioned).

Comment on lines +829 to +841
if (error) {
await loggingService.logBlockchainError(log, { error, context: 'RPC call failed' });
throw new SyncError('Failed to process event atomically', 'ATOMIC_PROCESS_FAIL', error);
}

if (!data.success) {
if (data.error === 'DUPLICATE_EVENT') {
// Duplicate events are not errors, just skip them
await loggingService.logBlockchainSuccess(log, { skipped: true, reason: 'duplicate' });
return { success: true, error: 'DUPLICATE_EVENT' };
}
throw new SyncError(`Atomic processing failed: ${data.error}`, data.error, data);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential null dereference and confusing return value semantics.

Two issues:

  1. Line 834 accesses data.success without verifying data is truthy. If the RPC returns { data: null, error: null }, this will throw a runtime error.
  2. Returning { success: true, error: 'DUPLICATE_EVENT' } is semantically confusing—success is true but there's an error string. Consider using a clearer structure.
🔧 Suggested fix
       if (error) {
         await loggingService.logBlockchainError(log, { error, context: 'RPC call failed' });
         throw new SyncError('Failed to process event atomically', 'ATOMIC_PROCESS_FAIL', error);
       }
 
+      if (!data) {
+        throw new SyncError('RPC returned no data', 'ATOMIC_PROCESS_FAIL', { eventId });
+      }
+
       if (!data.success) {
         if (data.error === 'DUPLICATE_EVENT') {
           // Duplicate events are not errors, just skip them
           await loggingService.logBlockchainSuccess(log, { skipped: true, reason: 'duplicate' });
-          return { success: true, error: 'DUPLICATE_EVENT' };
+          return { success: true, skipped: true };
         }
         throw new SyncError(`Atomic processing failed: ${data.error}`, data.error, data);
       }

Note: If changing the return type, update the method signature accordingly:

Promise<{ success: boolean; syncEventId?: string; skipped?: boolean; error?: string }>
📝 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 (error) {
await loggingService.logBlockchainError(log, { error, context: 'RPC call failed' });
throw new SyncError('Failed to process event atomically', 'ATOMIC_PROCESS_FAIL', error);
}
if (!data.success) {
if (data.error === 'DUPLICATE_EVENT') {
// Duplicate events are not errors, just skip them
await loggingService.logBlockchainSuccess(log, { skipped: true, reason: 'duplicate' });
return { success: true, error: 'DUPLICATE_EVENT' };
}
throw new SyncError(`Atomic processing failed: ${data.error}`, data.error, data);
}
if (error) {
await loggingService.logBlockchainError(log, { error, context: 'RPC call failed' });
throw new SyncError('Failed to process event atomically', 'ATOMIC_PROCESS_FAIL', error);
}
if (!data) {
throw new SyncError('RPC returned no data', 'ATOMIC_PROCESS_FAIL', { eventId });
}
if (!data.success) {
if (data.error === 'DUPLICATE_EVENT') {
// Duplicate events are not errors, just skip them
await loggingService.logBlockchainSuccess(log, { skipped: true, reason: 'duplicate' });
return { success: true, skipped: true };
}
throw new SyncError(`Atomic processing failed: ${data.error}`, data.error, data);
}
🤖 Prompt for AI Agents
In `@apps/backend/src/services/sync.service.ts` around lines 829 - 841, Guard
against a possible null/undefined RPC payload before accessing data.success in
the atomic processing branch: check `if (!data)` and log/throw or treat as
failure (e.g., logBlockchainError and throw a SyncError) so `data.success` is
only read when `data` is truthy; and change the duplicate-event return shape
from `{ success: true, error: 'DUPLICATE_EVENT' }` to a clearer form such as `{
success: true, skipped: true, reason: 'duplicate' }` (and update the method
signature to reflect the new optional fields like skipped/reason/syncEventId) so
callers aren’t confused by a success=true with an error string.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@biome.json`:
- Around line 11-21: The JSON array in biome.json's ignore list is invalid
because the entry "apps/web/stellar-social-sdk" is missing a trailing comma; fix
by adding a comma after the "apps/web/stellar-social-sdk" element so the array
items (e.g., "./apps/web/.next", "node_modules", "*.lock",
"apps/web/stellar-social-sdk", "**/node_modules/**") are properly separated and
the file parses as valid JSON.
♻️ Duplicate comments (1)
apps/backend/src/services/sync.service.ts (1)

908-955: Guard against null RPC data and avoid success+error return.

Two issues remain: data can be null/undefined before data.success, and returning { success: true, error: 'DUPLICATE_EVENT' } is ambiguous. Consider a null guard and a clearer skipped flag.

🔧 Suggested fix
-  ): Promise<{ success: boolean; syncEventId?: string; error?: string }> {
+  ): Promise<{ success: boolean; syncEventId?: string; skipped?: boolean; error?: string }> {
     const log = await loggingService.logBlockchainOperation('processEventAtomic', {
       eventId,
       eventType,
       bookingId,
       newStatus,
     });
 
     try {
       const { data, error } = await supabase.rpc('process_sync_event_atomic', {
         p_event_id: eventId,
         p_event_type: eventType,
         p_booking_id: bookingId,
         p_property_id: propertyId,
         p_user_id: userId,
         p_event_data: eventData,
         p_new_status: newStatus || null,
       });
 
       if (error) {
         await loggingService.logBlockchainError(log, { error, context: 'RPC call failed' });
         throw new SyncError('Failed to process event atomically', 'ATOMIC_PROCESS_FAIL', error);
       }
 
+      if (!data) {
+        await loggingService.logBlockchainError(log, {
+          error: 'RPC returned no data',
+          context: 'processEventAtomic',
+        });
+        throw new SyncError('RPC returned no data', 'ATOMIC_PROCESS_FAIL', { eventId });
+      }
+
       if (!data.success) {
         if (data.error === 'DUPLICATE_EVENT') {
           // Duplicate events are not errors, just skip them
           await loggingService.logBlockchainSuccess(log, { skipped: true, reason: 'duplicate' });
-          return { success: true, error: 'DUPLICATE_EVENT' };
+          return { success: true, skipped: true };
         }
         throw new SyncError(`Atomic processing failed: ${data.error}`, data.error, data);
       }
🧹 Nitpick comments (1)
biome.json (1)

11-29: Consider consolidating duplicate ignore patterns for clarity.

There are redundant entries (node_modules, coverage, *.lock, apps/backend/src/config/supabase.ts, etc.) alongside broader globs. Consider keeping only one form to reduce noise and avoid confusion.

♻️ Example consolidation (one possible approach)
-      "node_modules",
-      "*.lock",
-      "coverage",
-      "apps/backend/src/config/supabase.ts",
+      "**/node_modules/**",
+      "**/*.lock",
+      "**/coverage/**",

Comment on lines +11 to 21
"./apps/web/.next",
"./apps/contract/dist",
"./apps/*/dist",
"./apps/*/build",
"node_modules",
"*.lock",
"coverage",
"apps/backend/src/config/supabase.ts",
"apps/backend/tests/**/*.ts",
"apps/web/stellar-social-sdk"
"**/node_modules/**",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix invalid JSON: missing comma after Line 20.

The ignore array is invalid because Line 20 lacks a trailing comma before the next element, so Biome won’t parse the config.

🐛 Suggested fix
-      "apps/web/stellar-social-sdk"
+      "apps/web/stellar-social-sdk",
📝 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
"./apps/web/.next",
"./apps/contract/dist",
"./apps/*/dist",
"./apps/*/build",
"node_modules",
"*.lock",
"coverage",
"apps/backend/src/config/supabase.ts",
"apps/backend/tests/**/*.ts",
"apps/web/stellar-social-sdk"
"**/node_modules/**",
"./apps/web/.next",
"./apps/contract/dist",
"./apps/*/dist",
"./apps/*/build",
"node_modules",
"*.lock",
"coverage",
"apps/backend/src/config/supabase.ts",
"apps/backend/tests/**/*.ts",
"apps/web/stellar-social-sdk",
"**/node_modules/**",
🤖 Prompt for AI Agents
In `@biome.json` around lines 11 - 21, The JSON array in biome.json's ignore list
is invalid because the entry "apps/web/stellar-social-sdk" is missing a trailing
comma; fix by adding a comma after the "apps/web/stellar-social-sdk" element so
the array items (e.g., "./apps/web/.next", "node_modules", "*.lock",
"apps/web/stellar-social-sdk", "**/node_modules/**") are properly separated and
the file parses as valid JSON.

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.

Implement Social Login Using Stellar-Account-Abstraction-SDK

2 participants