-
Notifications
You must be signed in to change notification settings - Fork 76
Feature/social login sdk integration #212
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feature/social login sdk integration #212
Conversation
📝 WalkthroughWalkthroughMigrates 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this 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 inindex.ts. However,apps/backend/tests/integration/wallet-auth.test.tsstill 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 inapps/web/tests/e2e/register.spec.tsmock/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.gitignoreto exclude them, or delete this file if it's no longer needed. The actualpackage.jsonshould 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 viahttpEquivmeta tag is ineffective.The
Permissions-Policyheader 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:
- Reproducibility risk: Anyone with the user's provider ID (e.g., Google
sub) and knowledge of the hardcoded saltstellar-social-v1can derive the same keypair. If the provider ID is discoverable through normal account flows or exposed in logs/errors, the wallet keypair is compromised.- 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-existentdist/test.js.The test script runs
node dist/test.js, but the build configuration (Rollup) only generatesindex.js,index.esm.js, andindex.d.ts. This script will fail unlesstest.jsis explicitly built or the script is updated.Consider either:
- Adding a proper test framework (e.g., vitest, jest)
- 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 callsetState/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+). Theliquidity_pool_sharestype does not includeasset_codeorasset_issuerfields—instead it hasliquidity_pool_id—but your code assumes these fields always exist for non-native types. This producesundefined:undefinedfor 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 usesmemo.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:isValidJWTprovides 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
hasJWTStructureor 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:createNewAccountandcreateNewAccountWithKeypairare 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 thestellar-social-sdkpath alias instead of a relative path.Since
tsconfig.jsonalready defines a path alias forstellar-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 innext.config.jsinstead.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 onmoduleResolution.The configuration is appropriate for building the SDK. Consider updating
moduleResolutionto"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.nameis empty anduser.publicKeyis 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 inbeforeEachbut 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 forpublicKeyfield when normalizingrequestAccess()response.The code currently handles
stringand{ address }, but the local type definition in the codebase declares{ publicKey }. While@stellar/freighter-apiv4.1.0 appears to useaddress, 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 defaultallowedAuthTypesto 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 sharedAuthMethodTypeunion to avoid drift.
AuthMethodData.typeisstringwhileSocialUser.authMethodandAuthContextType.authMethodare 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.backupsource file to avoid accidental packaging.Leaving a full SDK implementation in a
.backupfile can confuse tooling or accidentally ship stale APIs. Consider deleting it or excluding it from build/package inputs.
respp
left a comment
There was a problem hiding this 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.
There was a problem hiding this 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
TokenExpiredErrorsubclassesJsonWebTokenError, 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
logIdfrom 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
logIddeclaration 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 toconsole.errorso 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. SinceBlockchainEventDatahas typed fields, consider updatingsyncBookingFromBlockchainto acceptBlockchainEventDatadirectly, or use a type-safe conversion utility.
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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
INSERTin theEXCEPTIONhandler and keep it?
Yes—if you handle the error and let execution continue normally, theINSERTyou 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
RAISEagain (re-raise)?
A bareRAISE;inside anEXCEPTIONhandler re-throws the currently handled error. [3] If that propagated error is not caught by an outerEXCEPTIONblock, 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,
INSERTlog 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/ROLLBACKinside a function). [4]
- Want to log and continue: catch exception,
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:
- 1: https://www.postgresql.org/docs/9.3/plpgsql-control-structures.html?utm_source=openai
- 2: https://www.cybertec-postgresql.com/en/subtransactions-and-performance-in-postgresql/?utm_source=openai
- 3: https://www.postgresql.org/docs/current/plpgsql-errors-and-messages.html?utm_source=openai
- 4: https://stackoverflow.com/questions/25650458/perform-action-before-exception-call-in-postgres-trigger-function?utm_source=openai
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.
| -- 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find all call sites of confirmBookingPayment
rg -n 'confirmBookingPayment\s*\(' --type=tsRepository: 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.tsRepository: 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 10Repository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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 2Repository: 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 5Repository: 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.
| // 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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).
| 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); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null dereference and confusing return value semantics.
Two issues:
- Line 834 accesses
data.successwithout verifyingdatais truthy. If the RPC returns{ data: null, error: null }, this will throw a runtime error. - 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.
| 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.
There was a problem hiding this 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:
datacan be null/undefined beforedata.success, and returning{ success: true, error: 'DUPLICATE_EVENT' }is ambiguous. Consider a null guard and a clearerskippedflag.🔧 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/**",
| "./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/**", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| "./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.
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):
SocialLoginButtoncomponent for Google Sign-InFreighterFallbackcomponent for manual wallet connectionuseAuthhook with session persistence (localStorage)SDK Integration:
@stellar/freighter-api(v4.1.0)Backend:
_deprecated/)Config:
🖼️ Current Output
🧪 Testing
✅ Testing Checklist
use-auth.test.tsx- 305 lines covering login/logout flows)Manual Testing Performed:
🚀 Next Steps & Improvements
💬 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
✏️ Tip: You can customize this high-level summary in your review settings.